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/CHANGELOG.md b/CHANGELOG.md index 427fa7403e4c..141bfd40ac5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,67 @@ 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 + +- **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 + +- **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 + +- **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 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/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. 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/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/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/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/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/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/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/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/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/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, 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/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/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/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_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/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index f9c666cbb954..80daf66a6926 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) @@ -376,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)?; @@ -426,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)?; @@ -466,6 +532,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 +547,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) @@ -491,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)?; @@ -543,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)?; @@ -2193,6 +2266,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..3b4d057e6025 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, } @@ -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(); @@ -428,6 +441,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 +462,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.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_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/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/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/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/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, 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/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"), 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, 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-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/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..be66a1bff92c 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": { @@ -8219,7 +8221,9 @@ "properties": { "merchant_logo": { "type": "string", - "nullable": true + "example": "https://i.imgur.com/RfxPFQo.png", + "nullable": true, + "maxLength": 255 }, "color_scheme": { "allOf": [ @@ -8248,6 +8252,9 @@ }, "PaymentLinkObject": { "type": "object", + "required": [ + "payment_link_config" + ], "properties": { "link_expiry": { "type": "string", @@ -8258,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", 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 @@ +[] 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",