diff --git a/CHANGELOG.md b/CHANGELOG.md index 48f61fc3f505..cf6a551700f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.12.19.1 + +### Features + +- **core:** Added customer phone_number and email to session token response for click to pay ([#6863](https://github.com/juspay/hyperswitch/pull/6863)) ([`092c79e`](https://github.com/juspay/hyperswitch/commit/092c79ec40c6af47a5d6654129411300e42eac56)) +- **klarna:** Klarna Kustom Checkout Integration ([#6839](https://github.com/juspay/hyperswitch/pull/6839)) ([`c525c9f`](https://github.com/juspay/hyperswitch/commit/c525c9f4c9d23802989bc594a4acd26c7d7cd27d)) +- **payment_methods:** Add support to pass apple pay recurring details to obtain apple pay merchant token ([#6770](https://github.com/juspay/hyperswitch/pull/6770)) ([`6074249`](https://github.com/juspay/hyperswitch/commit/607424992af4196f5a3e01477f64d794b3594a47)) +- **payments:** [Payment links] Add config for changing button text for payment links ([#6860](https://github.com/juspay/hyperswitch/pull/6860)) ([`46aad50`](https://github.com/juspay/hyperswitch/commit/46aad503b04efe60c54bbf4d5d5122696d9b1157)) +- **users:** Handle email url for users in different tenancies ([#6809](https://github.com/juspay/hyperswitch/pull/6809)) ([`839e69d`](https://github.com/juspay/hyperswitch/commit/839e69df241cf0eb2495f0ad3fc19cf32632c741)) + +### Bug Fixes + +- **connector:** [UNIFIED_AUTHENTICATION_SERVICE] change url path to `pre_authentication_processing` in pre-auth flow ([#6885](https://github.com/juspay/hyperswitch/pull/6885)) ([`f219b74`](https://github.com/juspay/hyperswitch/commit/f219b74cb6a100e07084afe6d9242a88f7127971)) + +### Refactors + +- **users:** Move roles schema to global interface ([#6862](https://github.com/juspay/hyperswitch/pull/6862)) ([`2d8af88`](https://github.com/juspay/hyperswitch/commit/2d8af882046bbfe309c5dbb5be9bfbd43e0c3831)) + +**Full Changelog:** [`2024.12.19.0...2024.12.19.1`](https://github.com/juspay/hyperswitch/compare/2024.12.19.0...2024.12.19.1) + +- - - + ## 2024.12.19.0 ### Refactors diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index e1a745fd9b17..3f1a0ae01de8 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -6495,88 +6495,6 @@ "greater_than_equal" ] }, - "ConfirmIntentAmountDetailsResponse": { - "type": "object", - "required": [ - "currency", - "external_tax_calculation", - "surcharge_calculation", - "net_amount", - "amount_capturable" - ], - "properties": { - "order_amount": { - "type": "integer", - "format": "int64", - "description": "The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and 1 for 1¥ since ¥ is a zero-decimal currency. Read more about [the Decimal and Non-Decimal Currencies](https://github.com/juspay/hyperswitch/wiki/Decimal-and-Non%E2%80%90Decimal-Currencies)", - "example": 6540, - "minimum": 0 - }, - "currency": { - "$ref": "#/components/schemas/Currency" - }, - "shipping_cost": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "order_tax_amount": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "external_tax_calculation": { - "$ref": "#/components/schemas/TaxCalculationOverride" - }, - "surcharge_calculation": { - "$ref": "#/components/schemas/SurchargeCalculationOverride" - }, - "surcharge_amount": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "tax_on_surcharge": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "net_amount": { - "$ref": "#/components/schemas/MinorUnit" - }, - "amount_to_capture": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "amount_capturable": { - "$ref": "#/components/schemas/MinorUnit" - }, - "amount_captured": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - } - } - }, "Connector": { "type": "string", "description": "A connector is an integration to fulfill payments", @@ -12605,6 +12523,88 @@ } } }, + "PaymentAmountDetailsResponse": { + "type": "object", + "required": [ + "currency", + "external_tax_calculation", + "surcharge_calculation", + "net_amount", + "amount_capturable" + ], + "properties": { + "order_amount": { + "type": "integer", + "format": "int64", + "description": "The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and 1 for 1¥ since ¥ is a zero-decimal currency. Read more about [the Decimal and Non-Decimal Currencies](https://github.com/juspay/hyperswitch/wiki/Decimal-and-Non%E2%80%90Decimal-Currencies)", + "example": 6540, + "minimum": 0 + }, + "currency": { + "$ref": "#/components/schemas/Currency" + }, + "shipping_cost": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "order_tax_amount": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "external_tax_calculation": { + "$ref": "#/components/schemas/TaxCalculationOverride" + }, + "surcharge_calculation": { + "$ref": "#/components/schemas/SurchargeCalculationOverride" + }, + "surcharge_amount": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "tax_on_surcharge": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "net_amount": { + "$ref": "#/components/schemas/MinorUnit" + }, + "amount_to_capture": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + }, + "amount_capturable": { + "$ref": "#/components/schemas/MinorUnit" + }, + "amount_captured": { + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], + "nullable": true + } + } + }, "PaymentAttemptResponse": { "type": "object", "required": [ @@ -14467,42 +14467,12 @@ }, "PaymentsCaptureRequest": { "type": "object", - "required": [ - "amount_to_capture" - ], "properties": { - "merchant_id": { - "type": "string", - "description": "The unique identifier for the merchant", - "nullable": true - }, "amount_to_capture": { "type": "integer", "format": "int64", - "description": "The Amount to be captured/ debited from the user's payment method.", - "example": 6540 - }, - "refund_uncaptured_amount": { - "type": "boolean", - "description": "Decider to refund the uncaptured amount", - "nullable": true - }, - "statement_descriptor_suffix": { - "type": "string", - "description": "Provides information about a card payment that customers see on their statements.", - "nullable": true - }, - "statement_descriptor_prefix": { - "type": "string", - "description": "Concatenated with the statement descriptor suffix that’s set on the account to form the complete statement descriptor.", - "nullable": true - }, - "merchant_connector_details": { - "allOf": [ - { - "$ref": "#/components/schemas/MerchantConnectorDetailsWrap" - } - ], + "description": "The Amount to be captured/ debited from the user's payment method. If not passed the full amount will be captured.", + "example": 6540, "nullable": true } } @@ -14585,6 +14555,7 @@ "id", "status", "amount", + "customer_id", "connector", "client_secret", "created", @@ -14604,7 +14575,14 @@ "$ref": "#/components/schemas/IntentStatus" }, "amount": { - "$ref": "#/components/schemas/ConfirmIntentAmountDetailsResponse" + "$ref": "#/components/schemas/PaymentAmountDetailsResponse" + }, + "customer_id": { + "type": "string", + "description": "The identifier for the customer", + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", + "maxLength": 64, + "minLength": 32 }, "connector": { "type": "string", @@ -16330,6 +16308,7 @@ "id", "status", "amount", + "customer_id", "client_secret", "created" ], @@ -16345,7 +16324,14 @@ "$ref": "#/components/schemas/IntentStatus" }, "amount": { - "$ref": "#/components/schemas/ConfirmIntentAmountDetailsResponse" + "$ref": "#/components/schemas/PaymentAmountDetailsResponse" + }, + "customer_id": { + "type": "string", + "description": "The identifier for the customer", + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", + "maxLength": 64, + "minLength": 32 }, "connector": { "type": "string", diff --git a/api-reference/api-reference/relay/relay--retrieve.mdx b/api-reference/api-reference/relay/relay--retrieve.mdx new file mode 100644 index 000000000000..d65e62d31d74 --- /dev/null +++ b/api-reference/api-reference/relay/relay--retrieve.mdx @@ -0,0 +1,3 @@ +--- +openapi: openapi_spec get /relay/{relay_id} +--- \ No newline at end of file diff --git a/api-reference/api-reference/relay/relay.mdx b/api-reference/api-reference/relay/relay.mdx new file mode 100644 index 000000000000..a6b5962740a6 --- /dev/null +++ b/api-reference/api-reference/relay/relay.mdx @@ -0,0 +1,3 @@ +--- +openapi: openapi_spec post /relay +--- \ No newline at end of file diff --git a/api-reference/mint.json b/api-reference/mint.json index 04b16682e3c4..5e9c9c6d36c6 100644 --- a/api-reference/mint.json +++ b/api-reference/mint.json @@ -234,6 +234,13 @@ "api-reference/routing/routing--activate-config" ] }, + { + "group": "Relay", + "pages": [ + "api-reference/relay/relay", + "api-reference/relay/relay--retrieve" + ] + }, { "group": "Schemas", "pages": ["api-reference/schemas/outgoing--webhook"] diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 4956df4713cc..0d750ea7bf53 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -950,6 +950,123 @@ ] } }, + "/relay": { + "post": { + "tags": [ + "Relay" + ], + "summary": "Relay - Create", + "description": "Creates a relay request.", + "operationId": "Relay Request", + "parameters": [ + { + "name": "X-Profile-Id", + "in": "header", + "description": "Profile ID for authentication", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-Idempotency-Key", + "in": "header", + "description": "Idempotency Key for relay request", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelayRequest" + }, + "examples": { + "Create a relay request": { + "value": { + "connector_id": "mca_5apGeP94tMts6rg3U3kR", + "connector_resource_id": "7256228702616471803954", + "data": { + "amount": 6540, + "currency": "USD" + }, + "type": "refund" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Relay request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelayResponse" + } + } + } + }, + "400": { + "description": "Invalid data" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/relay/{relay_id}": { + "get": { + "tags": [ + "Relay" + ], + "summary": "Relay - Retrieve", + "description": "Retrieves a relay details.", + "operationId": "Retrieve a Relay details", + "parameters": [ + { + "name": "X-Profile-Id", + "in": "header", + "description": "Profile ID for authentication", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Relay Retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelayResponse" + } + } + } + }, + "404": { + "description": "Relay details was not found" + } + }, + "security": [ + { + "api_key": [] + }, + { + "ephemeral_key": [] + } + ] + } + }, "/refunds": { "post": { "tags": [ @@ -17255,7 +17372,7 @@ "amount_to_capture": { "type": "integer", "format": "int64", - "description": "The Amount to be captured/ debited from the user's payment method.", + "description": "The Amount to be captured/ debited from the user's payment method. If not passed the full amount will be captured.", "example": 6540 }, "refund_uncaptured_amount": { @@ -23093,6 +23210,161 @@ }, "additionalProperties": false }, + "RelayData": { + "oneOf": [ + { + "$ref": "#/components/schemas/RelayRefundRequest" + } + ] + }, + "RelayError": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "The error code" + }, + "message": { + "type": "string", + "description": "The error message" + } + } + }, + "RelayRefundRequest": { + "type": "object", + "required": [ + "amount", + "currency" + ], + "properties": { + "amount": { + "type": "integer", + "format": "int64", + "description": "The amount that is being refunded", + "example": 6540 + }, + "currency": { + "$ref": "#/components/schemas/Currency" + }, + "reason": { + "type": "string", + "description": "The reason for the refund", + "example": "Customer returned the product", + "nullable": true, + "maxLength": 255 + } + } + }, + "RelayRequest": { + "type": "object", + "required": [ + "connector_resource_id", + "connector_id", + "type" + ], + "properties": { + "connector_resource_id": { + "type": "string", + "description": "The identifier that is associated to a resource at the connector to which the relay request is being made", + "example": "7256228702616471803954" + }, + "connector_id": { + "type": "string", + "description": "Identifier of the connector ( merchant connector account ) to which relay request is being made", + "example": "mca_5apGeP94tMts6rg3U3kR" + }, + "type": { + "$ref": "#/components/schemas/RelayType" + }, + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/RelayData" + } + ], + "nullable": true + } + } + }, + "RelayResponse": { + "type": "object", + "required": [ + "id", + "status", + "connector_resource_id", + "connector_id", + "profile_id", + "type" + ], + "properties": { + "id": { + "type": "string", + "description": "The unique identifier for the Relay", + "example": "relay_mbabizu24mvu3mela5njyhpit4" + }, + "status": { + "$ref": "#/components/schemas/RelayStatus" + }, + "connector_reference_id": { + "type": "string", + "description": "The reference identifier provided by the connector for the relay request", + "example": "pi_3MKEivSFNglxLpam0ZaL98q9", + "nullable": true + }, + "error": { + "allOf": [ + { + "$ref": "#/components/schemas/RelayError" + } + ], + "nullable": true + }, + "connector_resource_id": { + "type": "string", + "description": "The identifier that is associated to a resource at the connector to which the relay request is being made", + "example": "7256228702616471803954" + }, + "connector_id": { + "type": "string", + "description": "Identifier of the connector ( merchant connector account ) to which relay request is being made", + "example": "mca_5apGeP94tMts6rg3U3kR" + }, + "profile_id": { + "type": "string", + "description": "The business profile that is associated with this relay request", + "example": "pro_abcdefghijklmnopqrstuvwxyz" + }, + "type": { + "$ref": "#/components/schemas/RelayType" + }, + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/RelayData" + } + ], + "nullable": true + } + } + }, + "RelayStatus": { + "type": "string", + "enum": [ + "success", + "processing", + "failure" + ] + }, + "RelayType": { + "type": "string", + "enum": [ + "refund" + ] + }, "RequestPaymentMethodTypes": { "type": "object", "required": [ diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 61528c803c75..b4eba3745775 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -159,19 +159,31 @@ payout_connector_list = "stripe,wise" connectors_with_delayed_session_response = "trustpay,payme" # List of connectors which have delayed session response [mandates.supported_payment_methods] -bank_debit.ach = { connector_list = "gocardless,adyen" } # 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.bacs = { connector_list = "adyen" } # Mandate supported payment method type and connector for bank_debit -bank_debit.sepa = { connector_list = "gocardless,adyen" } # Mandate supported payment method type and connector for bank_debit -card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree" # Mandate supported payment method type and connector for card -card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree" # Mandate supported payment method type and connector for card -pay_later.klarna.connector_list = "adyen" # Mandate supported payment method type and connector for pay_later -wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica" # Mandate supported payment method type and connector for wallets -wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica" # Mandate supported payment method type and connector for wallets -wallet.paypal.connector_list = "adyen" # Mandate supported payment method type and connector for wallets -bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay" # Mandate supported payment method type and connector for bank_redirect -bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" # Mandate supported payment method type and connector for bank_redirect -bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay" # Mandate supported payment method type and connector for bank_redirect +bank_debit.ach = { connector_list = "gocardless,adyen,stripe" } +bank_debit.becs = { connector_list = "gocardless,stripe,adyen" } +bank_debit.bacs = { connector_list = "stripe,gocardless" } +bank_debit.sepa = { connector_list = "gocardless,adyen,stripe,deutschebank" } +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet" +wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica,noon,globalpay,multisafepay,novalnet" +wallet.paypal.connector_list = "adyen,globalpay,nexinets,novalnet,paypal" +wallet.momo.connector_list = "adyen" +wallet.kakao_pay.connector_list = "adyen" +wallet.go_pay.connector_list = "adyen" +wallet.gcash.connector_list = "adyen" +wallet.dana.connector_list = "adyen" +wallet.twint.connector_list = "adyen" +wallet.vipps.connector_list = "adyen" + +bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay,nexinets" +bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" +bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay,nexinets" +bank_redirect.bancontact_card.connector_list="adyen,stripe" +bank_redirect.trustly.connector_list="adyen" +bank_redirect.open_banking_uk.connector_list="adyen" +bank_redirect.eps.connector_list="globalpay,nexinets" [mandates.update_mandate_supported] card.credit = { connector_list = "cybersource" } # Update Mandate supported payment method type and connector for card diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 2dc587813cb3..afa17b62a354 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -159,19 +159,31 @@ force_cookies = false enabled = false [mandates.supported_payment_methods] -bank_debit.ach = { connector_list = "gocardless,adyen" } # 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.bacs = { connector_list = "adyen" } # Mandate supported payment method type and connector for bank_debit -bank_debit.sepa = { connector_list = "gocardless,adyen" } # Mandate supported payment method type and connector for bank_debit -card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree" # Mandate supported payment method type and connector for card -card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree" # Mandate supported payment method type and connector for card -pay_later.klarna.connector_list = "adyen" # Mandate supported payment method type and connector for pay_later -wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica" # Mandate supported payment method type and connector for wallets -wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica" # Mandate supported payment method type and connector for wallets -wallet.paypal.connector_list = "adyen" # Mandate supported payment method type and connector for wallets -bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay" # Mandate supported payment method type and connector for bank_redirect -bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" # Mandate supported payment method type and connector for bank_redirect -bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay" # Mandate supported payment method type and connector for bank_redirect +bank_debit.ach = { connector_list = "gocardless,adyen,stripe" } +bank_debit.becs = { connector_list = "gocardless,stripe,adyen" } +bank_debit.bacs = { connector_list = "stripe,gocardless" } +bank_debit.sepa = { connector_list = "gocardless,adyen,stripe,deutschebank" } +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet" +wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica,noon,globalpay,multisafepay,novalnet" +wallet.paypal.connector_list = "adyen,globalpay,nexinets,novalnet,paypal" +wallet.momo.connector_list = "adyen" +wallet.kakao_pay.connector_list = "adyen" +wallet.go_pay.connector_list = "adyen" +wallet.gcash.connector_list = "adyen" +wallet.dana.connector_list = "adyen" +wallet.twint.connector_list = "adyen" +wallet.vipps.connector_list = "adyen" + +bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay,nexinets" +bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" +bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay,nexinets" +bank_redirect.bancontact_card.connector_list="adyen,stripe" +bank_redirect.trustly.connector_list="adyen" +bank_redirect.open_banking_uk.connector_list="adyen" +bank_redirect.eps.connector_list="globalpay,nexinets" [mandates.update_mandate_supported] card.credit = { connector_list = "cybersource" } # Update Mandate supported payment method type and connector for card diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 3a348e9a3e54..047908a1e460 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -159,19 +159,31 @@ force_cookies = false enabled = true [mandates.supported_payment_methods] -bank_debit.ach = { connector_list = "gocardless,adyen" } # 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.bacs = { connector_list = "adyen" } # Mandate supported payment method type and connector for bank_debit -bank_debit.sepa = { connector_list = "gocardless,adyen" } # Mandate supported payment method type and connector for bank_debit -card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree" # Mandate supported payment method type and connector for card -card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree" # Mandate supported payment method type and connector for card -pay_later.klarna.connector_list = "adyen" # Mandate supported payment method type and connector for pay_later -wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica" # Mandate supported payment method type and connector for wallets -wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica" # Mandate supported payment method type and connector for wallets -wallet.paypal.connector_list = "adyen" # Mandate supported payment method type and connector for wallets -bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay" # Mandate supported payment method type and connector for bank_redirect -bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" # Mandate supported payment method type and connector for bank_redirect -bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay" # Mandate supported payment method type and connector for bank_redirect +bank_debit.ach = { connector_list = "gocardless,adyen,stripe" } +bank_debit.becs = { connector_list = "gocardless,stripe,adyen" } +bank_debit.bacs = { connector_list = "stripe,gocardless" } +bank_debit.sepa = { connector_list = "gocardless,adyen,stripe,deutschebank" } +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet" +wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica,noon,globalpay,multisafepay,novalnet" +wallet.paypal.connector_list = "adyen,globalpay,nexinets,novalnet,paypal" +wallet.momo.connector_list = "adyen" +wallet.kakao_pay.connector_list = "adyen" +wallet.go_pay.connector_list = "adyen" +wallet.gcash.connector_list = "adyen" +wallet.dana.connector_list = "adyen" +wallet.twint.connector_list = "adyen" +wallet.vipps.connector_list = "adyen" + +bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay,nexinets" +bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" +bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay,nexinets" +bank_redirect.bancontact_card.connector_list="adyen,stripe" +bank_redirect.trustly.connector_list="adyen" +bank_redirect.open_banking_uk.connector_list="adyen" +bank_redirect.eps.connector_list="globalpay,nexinets" [mandates.update_mandate_supported] card.credit = { connector_list = "cybersource" } # Update Mandate supported payment method type and connector for card diff --git a/config/development.toml b/config/development.toml index 6250e431bd4f..7008af1f28a2 100644 --- a/config/development.toml +++ b/config/development.toml @@ -614,19 +614,31 @@ connectors_with_delayed_session_response = "trustpay,payme" connectors_with_webhook_source_verification_call = "paypal" [mandates.supported_payment_methods] -pay_later.klarna = { connector_list = "adyen" } -wallet.google_pay = { connector_list = "stripe,adyen,cybersource,bankofamerica" } -wallet.apple_pay = { connector_list = "stripe,adyen,cybersource,noon,bankofamerica" } -wallet.paypal = { connector_list = "adyen" } -card.credit = { connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,fiuu" } -card.debit = { connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,fiuu" } -bank_debit.ach = { connector_list = "gocardless,adyen" } -bank_debit.becs = { connector_list = "gocardless" } -bank_debit.bacs = { connector_list = "adyen" } -bank_debit.sepa = { connector_list = "gocardless,adyen" } -bank_redirect.ideal = { connector_list = "stripe,adyen,globalpay,multisafepay" } -bank_redirect.sofort = { connector_list = "stripe,adyen,globalpay" } -bank_redirect.giropay = { connector_list = "adyen,globalpay,multisafepay" } +bank_debit.ach = { connector_list = "gocardless,adyen,stripe" } +bank_debit.becs = { connector_list = "gocardless,stripe,adyen" } +bank_debit.bacs = { connector_list = "stripe,gocardless" } +bank_debit.sepa = { connector_list = "gocardless,adyen,stripe,deutschebank" } +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet" +wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica,noon,globalpay,multisafepay,novalnet" +wallet.paypal.connector_list = "adyen,globalpay,nexinets,novalnet,paypal" +wallet.momo.connector_list = "adyen" +wallet.kakao_pay.connector_list = "adyen" +wallet.go_pay.connector_list = "adyen" +wallet.gcash.connector_list = "adyen" +wallet.dana.connector_list = "adyen" +wallet.twint.connector_list = "adyen" +wallet.vipps.connector_list = "adyen" + +bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay,nexinets" +bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" +bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay,nexinets" +bank_redirect.bancontact_card.connector_list="adyen,stripe" +bank_redirect.trustly.connector_list="adyen" +bank_redirect.open_banking_uk.connector_list="adyen" +bank_redirect.eps.connector_list="globalpay,nexinets" [mandates.update_mandate_supported] card.credit = { connector_list = "cybersource" } diff --git a/crates/api_models/src/events/customer.rs b/crates/api_models/src/events/customer.rs index 891fb0e9d3c6..55a55fbcecdc 100644 --- a/crates/api_models/src/events/customer.rs +++ b/crates/api_models/src/events/customer.rs @@ -1,6 +1,8 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; -use crate::customers::{CustomerDeleteResponse, CustomerRequest, CustomerResponse}; +use crate::customers::{ + CustomerDeleteResponse, CustomerRequest, CustomerResponse, CustomerUpdateRequestInternal, +}; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] impl ApiEventMetric for CustomerDeleteResponse { @@ -55,7 +57,7 @@ impl ApiEventMetric for CustomerResponse { } #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] -impl ApiEventMetric for crate::customers::CustomerUpdateRequestInternal { +impl ApiEventMetric for CustomerUpdateRequestInternal { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Customer { customer_id: self.customer_id.clone(), @@ -64,7 +66,7 @@ impl ApiEventMetric for crate::customers::CustomerUpdateRequestInternal { } #[cfg(all(feature = "v2", feature = "customer_v2"))] -impl ApiEventMetric for crate::customers::CustomerUpdateRequestInternal { +impl ApiEventMetric for CustomerUpdateRequestInternal { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Customer { customer_id: Some(self.id.clone()), diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index 6015682f7c38..a78aaf483aa0 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -11,7 +11,7 @@ use super::{ ))] use crate::payment_methods::CustomerPaymentMethodsListResponse; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -use crate::payment_methods::CustomerPaymentMethodsListResponse; +use crate::{events, payment_methods::CustomerPaymentMethodsListResponse}; use crate::{ payment_methods::{ CustomerDefaultPaymentMethodResponse, DefaultPaymentMethod, ListCountriesCurrenciesRequest, @@ -418,3 +418,12 @@ impl ApiEventMetric for PaymentStartRedirectionRequest { }) } } + +#[cfg(feature = "v2")] +impl ApiEventMetric for events::PaymentsCaptureResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.id.clone(), + }) + } +} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index a28332e7fea0..2d1e87ebf01e 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -31,6 +31,7 @@ pub mod poll; #[cfg(feature = "recon")] pub mod recon; pub mod refunds; +pub mod relay; pub mod routing; pub mod surcharge_decision_configs; pub mod user; diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 1c2a5c5a7169..a7211e6f82ad 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -621,7 +621,7 @@ pub struct AmountDetailsResponse { #[cfg(feature = "v2")] #[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] -pub struct ConfirmIntentAmountDetailsResponse { +pub struct PaymentAmountDetailsResponse { /// The payment amount. Amount for the payment in the lowest denomination of the currency, (i.e) in cents for USD denomination, in yen for JPY denomination etc. E.g., Pass 100 to charge $1.00 and 1 for 1¥ since ¥ is a zero-decimal currency. Read more about [the Decimal and Non-Decimal Currencies](https://github.com/juspay/hyperswitch/wiki/Decimal-and-Non%E2%80%90Decimal-Currencies) #[schema(value_type = u64, example = 6540)] #[serde(default, deserialize_with = "amount::deserialize")] @@ -4077,6 +4077,7 @@ pub struct PhoneDetails { pub country_code: Option, } +#[cfg(feature = "v1")] #[derive(Debug, Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] pub struct PaymentsCaptureRequest { /// The unique identifier for the payment @@ -4085,7 +4086,7 @@ pub struct PaymentsCaptureRequest { /// The unique identifier for the merchant #[schema(value_type = Option)] pub merchant_id: Option, - /// The Amount to be captured/ debited from the user's payment method. + /// The Amount to be captured/ debited from the user's payment method. If not passed the full amount will be captured. #[schema(value_type = i64, example = 6540)] pub amount_to_capture: Option, /// Decider to refund the uncaptured amount @@ -4099,6 +4100,28 @@ pub struct PaymentsCaptureRequest { pub merchant_connector_details: Option, } +#[cfg(feature = "v2")] +#[derive(Debug, Clone, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct PaymentsCaptureRequest { + /// The Amount to be captured/ debited from the user's payment method. If not passed the full amount will be captured. + #[schema(value_type = Option, example = 6540)] + pub amount_to_capture: Option, +} + +#[cfg(feature = "v2")] +#[derive(Debug, Clone, serde::Serialize, ToSchema)] +pub struct PaymentsCaptureResponse { + /// The unique identifier for the payment + pub id: id_type::GlobalPaymentId, + + /// Status of the payment + #[schema(value_type = IntentStatus, example = "succeeded")] + pub status: common_enums::IntentStatus, + + /// Amount details related to the payment + pub amount: PaymentAmountDetailsResponse, +} + #[derive(Default, Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct UrlDetails { pub url: String, @@ -4801,7 +4824,16 @@ pub struct PaymentsConfirmIntentResponse { pub status: api_enums::IntentStatus, /// Amount related information for this payment and attempt - pub amount: ConfirmIntentAmountDetailsResponse, + pub amount: PaymentAmountDetailsResponse, + + /// The identifier for the customer + #[schema( + min_length = 32, + max_length = 64, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", + value_type = String + )] + pub customer_id: Option, /// The connector used for the payment #[schema(example = "stripe")] @@ -4870,7 +4902,16 @@ pub struct PaymentsRetrieveResponse { pub status: api_enums::IntentStatus, /// Amount related information for this payment and attempt - pub amount: ConfirmIntentAmountDetailsResponse, + pub amount: PaymentAmountDetailsResponse, + + /// The identifier for the customer + #[schema( + min_length = 32, + max_length = 64, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", + value_type = String + )] + pub customer_id: Option, /// The connector used for the payment #[schema(example = "stripe")] diff --git a/crates/api_models/src/relay.rs b/crates/api_models/src/relay.rs new file mode 100644 index 000000000000..7e094f283607 --- /dev/null +++ b/crates/api_models/src/relay.rs @@ -0,0 +1,103 @@ +pub use common_utils::types::MinorUnit; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::enums; + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayRequest { + /// The identifier that is associated to a resource at the connector to which the relay request is being made + #[schema(example = "7256228702616471803954")] + pub connector_resource_id: String, + /// Identifier of the connector ( merchant connector account ) to which relay request is being made + #[schema(example = "mca_5apGeP94tMts6rg3U3kR", value_type = String)] + pub connector_id: common_utils::id_type::MerchantConnectorAccountId, + /// The type of relay request + #[serde(rename = "type")] + pub relay_type: RelayType, + /// The data that is associated with the relay request + pub data: Option, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum RelayType { + /// The relay request is for a refund + Refund, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case", untagged)] +pub enum RelayData { + /// The data that is associated with a refund relay request + Refund(RelayRefundRequest), +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayRefundRequest { + /// The amount that is being refunded + #[schema(value_type = i64 , example = 6540)] + pub amount: MinorUnit, + /// The currency in which the amount is being refunded + #[schema(value_type = Currency)] + pub currency: enums::Currency, + /// The reason for the refund + #[schema(max_length = 255, example = "Customer returned the product")] + pub reason: Option, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayResponse { + /// The unique identifier for the Relay + #[schema(example = "relay_mbabizu24mvu3mela5njyhpit4", value_type = String)] + pub id: common_utils::id_type::RelayId, + /// The status of the relay request + pub status: RelayStatus, + /// The reference identifier provided by the connector for the relay request + #[schema(example = "pi_3MKEivSFNglxLpam0ZaL98q9")] + pub connector_reference_id: Option, + /// The error details if the relay request failed + pub error: Option, + /// The identifier that is associated to a resource at the connector to which the relay request is being made + #[schema(example = "7256228702616471803954")] + pub connector_resource_id: String, + /// Identifier of the connector ( merchant connector account ) to which relay request is being made + #[schema(example = "mca_5apGeP94tMts6rg3U3kR", value_type = String)] + pub connector_id: common_utils::id_type::MerchantConnectorAccountId, + /// The business profile that is associated with this relay request + #[schema(example = "pro_abcdefghijklmnopqrstuvwxyz", value_type = String)] + pub profile_id: common_utils::id_type::ProfileId, + /// The type of relay request + #[serde(rename = "type")] + pub relay_type: RelayType, + /// The data that is associated with the relay request + pub data: Option, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum RelayStatus { + /// The relay request is successful + Success, + /// The relay request is being processed + Processing, + /// The relay request has failed + Failure, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayError { + /// The error code + pub code: String, + /// The error message + pub message: String, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayRetrieveRequest { + /// The unique identifier for the Relay + #[serde(default)] + pub force_sync: bool, + /// The unique identifier for the Relay + pub id: String, +} diff --git a/crates/common_utils/src/id_type.rs b/crates/common_utils/src/id_type.rs index 594078bb4057..c7393155226b 100644 --- a/crates/common_utils/src/id_type.rs +++ b/crates/common_utils/src/id_type.rs @@ -11,6 +11,7 @@ mod organization; mod payment; mod profile; mod refunds; +mod relay; mod routing; mod tenant; @@ -43,6 +44,7 @@ pub use self::{ payment::{PaymentId, PaymentReferenceId}, profile::ProfileId, refunds::RefundReferenceId, + relay::RelayId, routing::RoutingId, tenant::TenantId, }; diff --git a/crates/common_utils/src/id_type/relay.rs b/crates/common_utils/src/id_type/relay.rs new file mode 100644 index 000000000000..5db48c492f9c --- /dev/null +++ b/crates/common_utils/src/id_type/relay.rs @@ -0,0 +1,7 @@ +crate::id_type!( + RelayId, + "A type for relay_id that can be used for relay ids" +); +crate::impl_id_type_methods!(RelayId, "relay_id"); + +crate::impl_debug_id_type!(RelayId); diff --git a/crates/diesel_models/src/dynamic_routing_stats.rs b/crates/diesel_models/src/dynamic_routing_stats.rs index 168699d7f566..c055359d8b03 100644 --- a/crates/diesel_models/src/dynamic_routing_stats.rs +++ b/crates/diesel_models/src/dynamic_routing_stats.rs @@ -19,6 +19,7 @@ pub struct DynamicRoutingStatsNew { pub payment_status: common_enums::AttemptStatus, pub conclusive_classification: common_enums::SuccessBasedRoutingConclusiveState, pub created_at: time::PrimitiveDateTime, + pub payment_method_type: Option, } #[derive(Clone, Debug, Eq, PartialEq, Queryable, Selectable, Insertable)] @@ -38,4 +39,5 @@ pub struct DynamicRoutingStats { pub payment_status: common_enums::AttemptStatus, pub conclusive_classification: common_enums::SuccessBasedRoutingConclusiveState, pub created_at: time::PrimitiveDateTime, + pub payment_method_type: Option, } diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 6ddc26d49bdb..03facc2ebab4 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -754,7 +754,6 @@ pub enum PaymentAttemptUpdate { #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] #[diesel(table_name = payment_attempt)] pub struct PaymentAttemptUpdateInternal { - // net_amount: Option, pub status: Option, // authentication_type: Option, pub error_message: Option, @@ -774,7 +773,8 @@ pub struct PaymentAttemptUpdateInternal { // multiple_capture_count: Option, // pub surcharge_amount: Option, // tax_on_surcharge: Option, - // amount_capturable: Option, + pub amount_capturable: Option, + pub amount_to_capture: Option, pub updated_by: String, pub merchant_connector_id: Option, pub connector: Option, diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index c5d0fb396fdf..20c14d0dd104 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -373,6 +373,23 @@ pub struct PaymentIntentNew { pub split_payments: Option, } +#[cfg(feature = "v2")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PaymentIntentUpdate { + /// Update the payment intent details on payment intent confirmation, before calling the connector + ConfirmIntent { + status: storage_enums::IntentStatus, + active_attempt_id: common_utils::id_type::GlobalAttemptId, + updated_by: String, + }, + /// Update the payment intent details on payment intent confirmation, after calling the connector + ConfirmIntentPostUpdate { + status: storage_enums::IntentStatus, + amount_captured: Option, + updated_by: String, + }, +} + #[cfg(feature = "v1")] #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PaymentIntentUpdate { @@ -518,8 +535,9 @@ pub struct PaymentIntentUpdateFields { #[diesel(table_name = payment_intent)] pub struct PaymentIntentUpdateInternal { pub status: Option, - pub active_attempt_id: Option, + pub amount_captured: Option, pub modified_at: PrimitiveDateTime, + pub active_attempt_id: Option, pub amount: Option, pub currency: Option, pub shipping_cost: Option, diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index ac447148b0a5..732e9655d114 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -417,6 +417,8 @@ diesel::table! { payment_status -> AttemptStatus, conclusive_classification -> SuccessBasedRoutingConclusiveState, created_at -> Timestamp, + #[max_length = 64] + payment_method_type -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 4cc97f46b880..148dc57f7e7d 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -429,6 +429,8 @@ diesel::table! { payment_status -> AttemptStatus, conclusive_classification -> SuccessBasedRoutingConclusiveState, created_at -> Timestamp, + #[max_length = 64] + payment_method_type -> Nullable, } } diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index a87702337eb7..4c3328bca218 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -227,7 +227,7 @@ impl AmountDetails { common_enums::TaxCalculationOverride::Calculate => None, }; - payment_attempt::AttemptAmountDetails { + payment_attempt::AttemptAmountDetails::from(payment_attempt::AttemptAmountDetailsSetter { net_amount, amount_to_capture: None, surcharge_amount, @@ -236,7 +236,7 @@ impl AmountDetails { amount_capturable: MinorUnit::zero(), shipping_cost: self.shipping_cost, order_tax_amount, - } + }) } pub fn update_from_request(self, req: &api_models::payments::AmountDetailsUpdate) -> Self { @@ -605,6 +605,17 @@ where pub should_sync_with_connector: bool, } +#[cfg(feature = "v2")] +#[derive(Clone)] +pub struct PaymentCaptureData +where + F: Clone, +{ + pub flow: PhantomData, + pub payment_intent: PaymentIntent, + pub payment_attempt: PaymentAttempt, +} + #[cfg(feature = "v2")] impl PaymentStatusData where diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index 591ccc4ff981..0a7ada39ecdb 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -189,6 +189,27 @@ pub trait PaymentAttemptInterface { #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct AttemptAmountDetails { + /// The total amount for this payment attempt. This includes all the surcharge and tax amounts. + net_amount: MinorUnit, + /// The amount that has to be captured, + amount_to_capture: Option, + /// Surcharge amount for the payment attempt. + /// This is either derived by surcharge rules, or sent by the merchant + surcharge_amount: Option, + /// Tax amount for the payment attempt + /// This is either derived by surcharge rules, or sent by the merchant + tax_on_surcharge: Option, + /// The total amount that can be captured for this payment attempt. + amount_capturable: MinorUnit, + /// Shipping cost for the payment attempt. + shipping_cost: Option, + /// Tax amount for the order. + /// This is either derived by calling an external tax processor, or sent by the merchant + order_tax_amount: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +pub struct AttemptAmountDetailsSetter { /// The total amount for this payment attempt. This includes all the surcharge and tax amounts. pub net_amount: MinorUnit, /// The amount that has to be captured, @@ -208,6 +229,67 @@ pub struct AttemptAmountDetails { pub order_tax_amount: Option, } +/// Set the fields of amount details, since the fields are not public +impl From for AttemptAmountDetails { + fn from(setter: AttemptAmountDetailsSetter) -> Self { + Self { + net_amount: setter.net_amount, + amount_to_capture: setter.amount_to_capture, + surcharge_amount: setter.surcharge_amount, + tax_on_surcharge: setter.tax_on_surcharge, + amount_capturable: setter.amount_capturable, + shipping_cost: setter.shipping_cost, + order_tax_amount: setter.order_tax_amount, + } + } +} + +impl AttemptAmountDetails { + pub fn get_net_amount(&self) -> MinorUnit { + self.net_amount + } + + pub fn get_amount_to_capture(&self) -> Option { + self.amount_to_capture + } + + pub fn get_surcharge_amount(&self) -> Option { + self.surcharge_amount + } + + pub fn get_tax_on_surcharge(&self) -> Option { + self.tax_on_surcharge + } + + pub fn get_amount_capturable(&self) -> MinorUnit { + self.amount_capturable + } + + pub fn get_shipping_cost(&self) -> Option { + self.shipping_cost + } + + pub fn get_order_tax_amount(&self) -> Option { + self.order_tax_amount + } + + pub fn set_amount_to_capture(&mut self, amount_to_capture: MinorUnit) { + self.amount_to_capture = Some(amount_to_capture); + } + + /// Validate the amount to capture that is sent in the request + pub fn validate_amount_to_capture( + &self, + request_amount_to_capture: MinorUnit, + ) -> Result<(), ValidationError> { + common_utils::fp_utils::when(request_amount_to_capture > self.get_net_amount(), || { + Err(ValidationError::IncorrectValueProvided { + field_name: "amount_to_capture", + }) + }) + } +} + #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct ErrorDetails { /// The error code that was returned by the connector. @@ -657,6 +739,7 @@ impl NetAmount { #[cfg(feature = "v2")] impl PaymentAttempt { + #[track_caller] pub fn get_total_amount(&self) -> MinorUnit { todo!(); } @@ -1339,15 +1422,28 @@ pub enum PaymentAttemptUpdate { updated_by: String, redirection_data: Option, connector_metadata: Option, + amount_capturable: Option, }, /// Update the payment attempt after force syncing with the connector SyncUpdate { status: storage_enums::AttemptStatus, + amount_capturable: Option, + updated_by: String, + }, + PreCaptureUpdate { + amount_to_capture: Option, + updated_by: String, + }, + /// Update the payment after attempting capture with the connector + CaptureUpdate { + status: storage_enums::AttemptStatus, + amount_capturable: Option, updated_by: String, }, /// Update the payment attempt on confirming the intent, after calling the connector on error response ErrorUpdate { status: storage_enums::AttemptStatus, + amount_capturable: Option, error: ErrorDetails, updated_by: String, connector_payment_id: Option, @@ -1985,11 +2081,14 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector: Some(connector), redirection_data: None, connector_metadata: None, + amount_capturable: None, + amount_to_capture: None, }, PaymentAttemptUpdate::ErrorUpdate { status, error, connector_payment_id, + amount_capturable, updated_by, } => Self { status: Some(status), @@ -2006,6 +2105,8 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector: None, redirection_data: None, connector_metadata: None, + amount_capturable, + amount_to_capture: None, }, PaymentAttemptUpdate::ConfirmIntentResponse { status, @@ -2013,8 +2114,10 @@ impl From for diesel_models::PaymentAttemptUpdateInternal updated_by, redirection_data, connector_metadata, + amount_capturable, } => Self { status: Some(status), + amount_capturable, error_message: None, error_code: None, modified_at: common_utils::date_time::now(), @@ -2029,9 +2132,15 @@ impl From for diesel_models::PaymentAttemptUpdateInternal redirection_data: redirection_data .map(diesel_models::payment_attempt::RedirectForm::from), connector_metadata, + amount_to_capture: None, }, - PaymentAttemptUpdate::SyncUpdate { status, updated_by } => Self { + PaymentAttemptUpdate::SyncUpdate { + status, + amount_capturable, + updated_by, + } => Self { status: Some(status), + amount_capturable, error_message: None, error_code: None, modified_at: common_utils::date_time::now(), @@ -2045,6 +2154,50 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector: None, redirection_data: None, connector_metadata: None, + amount_to_capture: None, + }, + PaymentAttemptUpdate::CaptureUpdate { + status, + amount_capturable, + updated_by, + } => Self { + status: Some(status), + amount_capturable, + amount_to_capture: None, + error_message: None, + error_code: None, + modified_at: common_utils::date_time::now(), + browser_info: None, + error_reason: None, + updated_by, + merchant_connector_id: None, + unified_code: None, + unified_message: None, + connector_payment_id: None, + connector: None, + redirection_data: None, + connector_metadata: None, + }, + PaymentAttemptUpdate::PreCaptureUpdate { + amount_to_capture, + updated_by, + } => Self { + amount_to_capture, + error_message: None, + modified_at: common_utils::date_time::now(), + browser_info: None, + error_code: None, + error_reason: None, + updated_by, + merchant_connector_id: None, + unified_code: None, + unified_message: None, + connector_payment_id: None, + connector: None, + redirection_data: None, + status: None, + connector_metadata: None, + amount_capturable: None, }, } } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 42acfc63a4bc..953d39c131a6 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -292,11 +292,18 @@ pub enum PaymentIntentUpdate { /// PostUpdate tracker of ConfirmIntent ConfirmIntentPostUpdate { status: common_enums::IntentStatus, + amount_captured: Option, updated_by: String, }, /// SyncUpdate of ConfirmIntent in PostUpdateTrackers SyncUpdate { status: common_enums::IntentStatus, + amount_captured: Option, + updated_by: String, + }, + CaptureUpdate { + status: common_enums::IntentStatus, + amount_captured: Option, updated_by: String, }, /// UpdateIntent @@ -361,6 +368,7 @@ impl From for diesel_models::PaymentIntentUpdateInternal { active_attempt_id: Some(active_attempt_id), modified_at: common_utils::date_time::now(), amount: None, + amount_captured: None, currency: None, shipping_cost: None, tax_details: None, @@ -392,12 +400,57 @@ impl From for diesel_models::PaymentIntentUpdateInternal { updated_by, }, - PaymentIntentUpdate::ConfirmIntentPostUpdate { status, updated_by } => Self { + PaymentIntentUpdate::ConfirmIntentPostUpdate { + status, + updated_by, + amount_captured, + } => Self { + status: Some(status), + active_attempt_id: None, + modified_at: common_utils::date_time::now(), + amount_captured, + amount: None, + currency: None, + shipping_cost: None, + tax_details: None, + skip_external_tax_calculation: None, + surcharge_applicable: None, + surcharge_amount: None, + tax_on_surcharge: None, + routing_algorithm_id: None, + capture_method: None, + authentication_type: None, + billing_address: None, + shipping_address: None, + customer_present: None, + description: None, + return_url: None, + setup_future_usage: None, + apply_mit_exemption: None, + statement_descriptor: None, + order_details: None, + allowed_payment_method_types: None, + metadata: None, + connector_metadata: None, + feature_metadata: None, + payment_link_config: None, + request_incremental_authorization: None, + session_expiry: None, + frm_metadata: None, + request_external_three_ds_authentication: None, + updated_by, + }, + PaymentIntentUpdate::SyncUpdate { + status, + amount_captured, + updated_by, + } => Self { status: Some(status), active_attempt_id: None, modified_at: common_utils::date_time::now(), amount: None, currency: None, + amount_captured, shipping_cost: None, tax_details: None, skip_external_tax_calculation: None, @@ -427,8 +480,13 @@ impl From for diesel_models::PaymentIntentUpdateInternal { request_external_three_ds_authentication: None, updated_by, }, - PaymentIntentUpdate::SyncUpdate { status, updated_by } => Self { + PaymentIntentUpdate::CaptureUpdate { + status, + amount_captured, + updated_by, + } => Self { status: Some(status), + amount_captured, active_attempt_id: None, modified_at: common_utils::date_time::now(), amount: None, @@ -499,7 +557,7 @@ impl From for diesel_models::PaymentIntentUpdateInternal { status: None, active_attempt_id: None, modified_at: common_utils::date_time::now(), - + amount_captured: None, amount, currency, shipping_cost, diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index b070c6bc848a..f69c1db0e10b 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -405,15 +405,30 @@ use crate::{ /// Get updatable trakcer objects of payment intent and payment attempt #[cfg(feature = "v2")] -pub trait TrackerPostUpdateObjects { +pub trait TrackerPostUpdateObjects { fn get_payment_intent_update( &self, + payment_data: &D, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentIntentUpdate; + fn get_payment_attempt_update( &self, + payment_data: &D, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentAttemptUpdate; + + /// Get the amount that can be captured for the payment + fn get_amount_capturable(&self, payment_data: &D) -> Option; + + /// Get the amount that has been captured for the payment + fn get_captured_amount(&self, payment_data: &D) -> Option; + + /// Get the attempt status based on parameters like captured amount and amount capturable + fn get_attempt_status_for_db_update( + &self, + payment_data: &D, + ) -> common_enums::enums::AttemptStatus; } #[cfg(feature = "v2")] @@ -421,6 +436,7 @@ impl TrackerPostUpdateObjects< router_flow_types::Authorize, router_request_types::PaymentsAuthorizeData, + payments::PaymentConfirmData, > for RouterData< router_flow_types::Authorize, @@ -430,11 +446,16 @@ impl { fn get_payment_intent_update( &self, + payment_data: &payments::PaymentConfirmData, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentIntentUpdate { + let amount_captured = self.get_captured_amount(payment_data); match self.response { Ok(ref _response) => PaymentIntentUpdate::ConfirmIntentPostUpdate { - status: common_enums::IntentStatus::from(self.status), + status: common_enums::IntentStatus::from( + self.get_attempt_status_for_db_update(payment_data), + ), + amount_captured, updated_by: storage_scheme.to_string(), }, Err(ref error) => PaymentIntentUpdate::ConfirmIntentPostUpdate { @@ -442,6 +463,7 @@ impl .attempt_status .map(common_enums::IntentStatus::from) .unwrap_or(common_enums::IntentStatus::Failed), + amount_captured, updated_by: storage_scheme.to_string(), }, } @@ -449,8 +471,11 @@ impl fn get_payment_attempt_update( &self, + payment_data: &payments::PaymentConfirmData, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentAttemptUpdate { + let amount_capturable = self.get_amount_capturable(payment_data); + match self.response { Ok(ref response_router_data) => match response_router_data { router_response_types::PaymentsResponseData::TransactionResponse { @@ -463,7 +488,8 @@ impl incremental_authorization_allowed, charge_id, } => { - let attempt_status = self.status; + let attempt_status = self.get_attempt_status_for_db_update(payment_data); + let connector_payment_id = match resource_id { router_request_types::ResponseId::NoResponseId => None, router_request_types::ResponseId::ConnectorTransactionId(id) @@ -475,6 +501,7 @@ impl connector_payment_id, updated_by: storage_scheme.to_string(), redirection_data: *redirection_data.clone(), + amount_capturable, connector_metadata: connector_metadata.clone().map(Secret::new), } } @@ -528,16 +555,310 @@ impl PaymentAttemptUpdate::ErrorUpdate { status: attempt_status, error: error_details, + amount_capturable, connector_payment_id: connector_transaction_id, updated_by: storage_scheme.to_string(), } } } } + + fn get_attempt_status_for_db_update( + &self, + _payment_data: &payments::PaymentConfirmData, + ) -> common_enums::AttemptStatus { + // For this step, consider whatever status was given by the connector module + // We do not need to check for amount captured or amount capturable here because we are authorizing the whole amount + self.status + } + + fn get_amount_capturable( + &self, + payment_data: &payments::PaymentConfirmData, + ) -> Option { + // Based on the status of the response, we can determine the amount capturable + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is already succeeded / failed we cannot capture any more amount + // So set the amount capturable to zero + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Cancelled => Some(MinorUnit::zero()), + // For these statuses, update the capturable amount when it reaches terminal / capturable state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + // If status is requires capture, get the total amount that can be captured + // This is in cases where the capture method will be `manual` or `manual_multiple` + // We do not need to handle the case where amount_to_capture is provided here as it cannot be passed in authroize flow + common_enums::IntentStatus::RequiresCapture => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // Invalid statues for this flow, after doing authorization this state is invalid + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } + + fn get_captured_amount( + &self, + payment_data: &payments::PaymentConfirmData, + ) -> Option { + // Based on the status of the response, we can determine the amount that was captured + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is succeeded then we have captured the whole amount + // we need not check for `amount_to_capture` here because passing `amount_to_capture` when authorizing is not supported + common_enums::IntentStatus::Succeeded => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // No amount is captured + common_enums::IntentStatus::Cancelled | common_enums::IntentStatus::Failed => { + Some(MinorUnit::zero()) + } + // For these statuses, update the amount captured when it reaches terminal state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + // No amount has been captured yet + common_enums::IntentStatus::RequiresCapture => Some(MinorUnit::zero()), + // Invalid statues for this flow + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } } #[cfg(feature = "v2")] -impl TrackerPostUpdateObjects +impl + TrackerPostUpdateObjects< + router_flow_types::Capture, + router_request_types::PaymentsCaptureData, + payments::PaymentCaptureData, + > + for RouterData< + router_flow_types::Capture, + router_request_types::PaymentsCaptureData, + router_response_types::PaymentsResponseData, + > +{ + fn get_payment_intent_update( + &self, + payment_data: &payments::PaymentCaptureData, + storage_scheme: common_enums::MerchantStorageScheme, + ) -> PaymentIntentUpdate { + let amount_captured = self.get_captured_amount(payment_data); + match self.response { + Ok(ref _response) => PaymentIntentUpdate::CaptureUpdate { + status: common_enums::IntentStatus::from( + self.get_attempt_status_for_db_update(payment_data), + ), + amount_captured, + updated_by: storage_scheme.to_string(), + }, + Err(ref error) => PaymentIntentUpdate::CaptureUpdate { + status: error + .attempt_status + .map(common_enums::IntentStatus::from) + .unwrap_or(common_enums::IntentStatus::Failed), + amount_captured, + updated_by: storage_scheme.to_string(), + }, + } + } + + fn get_payment_attempt_update( + &self, + payment_data: &payments::PaymentCaptureData, + storage_scheme: common_enums::MerchantStorageScheme, + ) -> PaymentAttemptUpdate { + let amount_capturable = self.get_amount_capturable(payment_data); + + match self.response { + Ok(ref response_router_data) => match response_router_data { + router_response_types::PaymentsResponseData::TransactionResponse { + resource_id, + redirection_data, + mandate_reference, + connector_metadata, + network_txn_id, + connector_response_reference_id, + incremental_authorization_allowed, + charge_id, + } => { + let attempt_status = self.status; + + PaymentAttemptUpdate::CaptureUpdate { + status: attempt_status, + amount_capturable, + updated_by: storage_scheme.to_string(), + } + } + router_response_types::PaymentsResponseData::MultipleCaptureResponse { .. } => { + todo!() + } + router_response_types::PaymentsResponseData::SessionResponse { .. } => todo!(), + router_response_types::PaymentsResponseData::SessionTokenResponse { .. } => todo!(), + router_response_types::PaymentsResponseData::TransactionUnresolvedResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::TokenizationResponse { .. } => todo!(), + router_response_types::PaymentsResponseData::ConnectorCustomerResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::ThreeDSEnrollmentResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::PreProcessingResponse { .. } => { + todo!() + } + router_response_types::PaymentsResponseData::IncrementalAuthorizationResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::PostProcessingResponse { .. } => { + todo!() + } + router_response_types::PaymentsResponseData::SessionUpdateResponse { .. } => { + todo!() + } + }, + Err(ref error_response) => { + let ErrorResponse { + code, + message, + reason, + status_code: _, + attempt_status, + connector_transaction_id, + } = error_response.clone(); + let attempt_status = attempt_status.unwrap_or(self.status); + + let error_details = ErrorDetails { + code, + message, + reason, + unified_code: None, + unified_message: None, + }; + + PaymentAttemptUpdate::ErrorUpdate { + status: attempt_status, + error: error_details, + amount_capturable, + connector_payment_id: connector_transaction_id, + updated_by: storage_scheme.to_string(), + } + } + } + } + + fn get_attempt_status_for_db_update( + &self, + payment_data: &payments::PaymentCaptureData, + ) -> common_enums::AttemptStatus { + match self.status { + common_enums::AttemptStatus::Charged => { + let amount_captured = self + .get_captured_amount(payment_data) + .unwrap_or(MinorUnit::zero()); + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + + if amount_captured == total_amount { + common_enums::AttemptStatus::Charged + } else { + common_enums::AttemptStatus::PartialCharged + } + } + _ => self.status, + } + } + + fn get_amount_capturable( + &self, + payment_data: &payments::PaymentCaptureData, + ) -> Option { + // Based on the status of the response, we can determine the amount capturable + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is already succeeded / failed we cannot capture any more amount + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Cancelled => Some(MinorUnit::zero()), + // For these statuses, update the capturable amount when it reaches terminal / capturable state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + common_enums::IntentStatus::RequiresCapture => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // Invalid statues for this flow + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } + + fn get_captured_amount( + &self, + payment_data: &payments::PaymentCaptureData, + ) -> Option { + // Based on the status of the response, we can determine the amount capturable + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is succeeded then we have captured the whole amount + common_enums::IntentStatus::Succeeded => { + let amount_to_capture = payment_data + .payment_attempt + .amount_details + .get_amount_to_capture(); + + let amount_captured = amount_to_capture + .unwrap_or(payment_data.payment_attempt.amount_details.get_net_amount()); + + Some(amount_captured) + } + // No amount is captured + common_enums::IntentStatus::Cancelled | common_enums::IntentStatus::Failed => { + Some(MinorUnit::zero()) + } + common_enums::IntentStatus::RequiresCapture => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // For these statuses, update the amount captured when it reaches terminal state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + // Invalid statues for this flow + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => { + todo!() + } + } + } +} + +#[cfg(feature = "v2")] +impl + TrackerPostUpdateObjects< + router_flow_types::PSync, + router_request_types::PaymentsSyncData, + payments::PaymentStatusData, + > for RouterData< router_flow_types::PSync, router_request_types::PaymentsSyncData, @@ -546,11 +867,16 @@ impl TrackerPostUpdateObjects, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentIntentUpdate { + let amount_captured = self.get_captured_amount(payment_data); match self.response { Ok(ref _response) => PaymentIntentUpdate::SyncUpdate { - status: common_enums::IntentStatus::from(self.status), + status: common_enums::IntentStatus::from( + self.get_attempt_status_for_db_update(payment_data), + ), + amount_captured, updated_by: storage_scheme.to_string(), }, Err(ref error) => PaymentIntentUpdate::SyncUpdate { @@ -558,6 +884,7 @@ impl TrackerPostUpdateObjects, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentAttemptUpdate { + let amount_capturable = self.get_amount_capturable(payment_data); + match self.response { Ok(ref response_router_data) => match response_router_data { router_response_types::PaymentsResponseData::TransactionResponse { @@ -579,15 +909,11 @@ impl TrackerPostUpdateObjects { - let attempt_status = self.status; - let connector_payment_id = match resource_id { - router_request_types::ResponseId::NoResponseId => None, - router_request_types::ResponseId::ConnectorTransactionId(id) - | router_request_types::ResponseId::EncodedData(id) => Some(id.to_owned()), - }; + let attempt_status = self.get_attempt_status_for_db_update(payment_data); PaymentAttemptUpdate::SyncUpdate { status: attempt_status, + amount_capturable, updated_by: storage_scheme.to_string(), } } @@ -641,10 +967,106 @@ impl TrackerPostUpdateObjects, + ) -> common_enums::AttemptStatus { + match self.status { + common_enums::AttemptStatus::Charged => { + let amount_captured = self + .get_captured_amount(payment_data) + .unwrap_or(MinorUnit::zero()); + + let total_amount = payment_data + .payment_attempt + .as_ref() + .map(|attempt| attempt.amount_details.get_net_amount()) + .unwrap_or(MinorUnit::zero()); + + if amount_captured == total_amount { + common_enums::AttemptStatus::Charged + } else { + common_enums::AttemptStatus::PartialCharged + } + } + _ => self.status, + } + } + + fn get_amount_capturable( + &self, + payment_data: &payments::PaymentStatusData, + ) -> Option { + let payment_attempt = payment_data.payment_attempt.as_ref()?; + + // Based on the status of the response, we can determine the amount capturable + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is already succeeded / failed we cannot capture any more amount + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Cancelled => Some(MinorUnit::zero()), + // For these statuses, update the capturable amount when it reaches terminal / capturable state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + common_enums::IntentStatus::RequiresCapture => { + let total_amount = payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // Invalid statues for this flow + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } + + fn get_captured_amount( + &self, + payment_data: &payments::PaymentStatusData, + ) -> Option { + let payment_attempt = payment_data.payment_attempt.as_ref()?; + + // Based on the status of the response, we can determine the amount capturable + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is succeeded then we have captured the whole amount or amount_to_capture + common_enums::IntentStatus::Succeeded => { + let amount_to_capture = payment_attempt.amount_details.get_amount_to_capture(); + + let amount_captured = + amount_to_capture.unwrap_or(payment_attempt.amount_details.get_net_amount()); + + Some(amount_captured) + } + // No amount is captured + common_enums::IntentStatus::Cancelled | common_enums::IntentStatus::Failed => { + Some(MinorUnit::zero()) + } + // For these statuses, update the amount captured when it reaches terminal state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + common_enums::IntentStatus::RequiresCapture => { + let total_amount = payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // Invalid statues for this flow + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } } diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index a55f6ed4214c..7b093ffd9612 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -84,6 +84,10 @@ Never share your secret api keys. Keep them guarded and secure. routes::payments::payments_complete_authorize, routes::payments::payments_post_session_tokens, + // Routes for relay + routes::relay, + routes::relay_retrieve, + // Routes for refunds routes::refunds::refunds_create, routes::refunds::refunds_retrieve, @@ -520,6 +524,13 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::PaymentMethodCollectLinkResponse, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, + api_models::relay::RelayRequest, + api_models::relay::RelayType, + api_models::relay::RelayData, + api_models::relay::RelayRefundRequest, + api_models::relay::RelayResponse, + api_models::relay::RelayStatus, + api_models::relay::RelayError, api_models::payments::AmountFilter, api_models::mandates::MandateRevokedResponse, api_models::mandates::MandateResponse, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index c7425aed2a51..7553a780c272 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -639,7 +639,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::ErrorDetails, api_models::payments::CtpServiceDetails, common_utils::types::BrowserInformation, - api_models::payments::ConfirmIntentAmountDetailsResponse, + api_models::payments::PaymentAmountDetailsResponse, routes::payments::ForceSync, )), modifiers(&SecurityAddon) diff --git a/crates/openapi/src/routes.rs b/crates/openapi/src/routes.rs index 453f7f557d34..2c1c58244124 100644 --- a/crates/openapi/src/routes.rs +++ b/crates/openapi/src/routes.rs @@ -16,10 +16,11 @@ pub mod payouts; pub mod poll; pub mod profile; pub mod refunds; +pub mod relay; pub mod routing; pub mod webhook_events; pub use self::{ customers::*, mandates::*, merchant_account::*, merchant_connector_account::*, organization::*, - payment_method::*, payments::*, poll::*, refunds::*, routing::*, webhook_events::*, + payment_method::*, payments::*, poll::*, refunds::*, relay::*, routing::*, webhook_events::*, }; diff --git a/crates/openapi/src/routes/relay.rs b/crates/openapi/src/routes/relay.rs new file mode 100644 index 000000000000..9100bf47f759 --- /dev/null +++ b/crates/openapi/src/routes/relay.rs @@ -0,0 +1,57 @@ +/// Relay - Create +/// +/// Creates a relay request. +#[utoipa::path( + post, + path = "/relay", + request_body( + content = RelayRequest, + examples(( + "Create a relay request" = ( + value = json!({ + "connector_resource_id": "7256228702616471803954", + "connector_id": "mca_5apGeP94tMts6rg3U3kR", + "type": "refund", + "data": { + "amount": 6540, + "currency": "USD" + } + }) + ) + )) + ), + responses( + (status = 200, description = "Relay request", body = RelayResponse), + (status = 400, description = "Invalid data") + ), + params( + ("X-Profile-Id" = String, Header, description = "Profile ID for authentication"), + ("X-Idempotency-Key" = String, Header, description = "Idempotency Key for relay request") + ), + tag = "Relay", + operation_id = "Relay Request", + security(("api_key" = [])) +)] + +pub async fn relay() {} + +/// Relay - Retrieve +/// +/// Retrieves a relay details. +#[utoipa::path( + get, + path = "/relay/{relay_id}", + params (("id" = String, Path, description = "The unique identifier for the Relay")), + responses( + (status = 200, description = "Relay Retrieved", body = RelayResponse), + (status = 404, description = "Relay details was not found") + ), + params( + ("X-Profile-Id" = String, Header, description = "Profile ID for authentication") + ), + tag = "Relay", + operation_id = "Retrieve a Relay details", + security(("api_key" = []), ("ephemeral_key" = [])) +)] + +pub async fn relay_retrieve() {} diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 600b7ab0e221..67204d9673c8 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -469,6 +469,21 @@ pub struct PaypalRedirectionResponse { attributes: Option, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct EpsRedirectionResponse { + name: Option>, + country_code: Option, + bic: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct IdealRedirectionResponse { + name: Option>, + country_code: Option, + bic: Option>, + iban_last_chars: Option>, +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AttributeResponse { vault: PaypalVaultResponse, @@ -518,6 +533,8 @@ pub struct CardVaultResponse { pub enum PaymentSourceItemResponse { Card(CardVaultResponse), Paypal(PaypalRedirectionResponse), + Eps(EpsRedirectionResponse), + Ideal(IdealRedirectionResponse), } #[derive(Debug, Serialize)] @@ -1826,6 +1843,8 @@ impl PaymentSourceItemResponse::Card(card) => { card.attributes.map(|attr| attr.vault.id) } + PaymentSourceItemResponse::Eps(_) + | PaymentSourceItemResponse::Ideal(_) => None, }, None => None, }, diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 262b0348b5ae..cb6c04c601f5 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -1703,7 +1703,13 @@ impl TryFrom<(&types::PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntent }; let mut payment_method_options = None; - let (mut payment_data, payment_method, billing_address, payment_method_types) = { + let ( + mut payment_data, + payment_method, + billing_address, + payment_method_types, + setup_future_usage, + ) = { match item .request .mandate_id @@ -1717,6 +1723,7 @@ impl TryFrom<(&types::PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntent connector_mandate_ids.get_connector_mandate_id(), StripeBillingAddress::default(), get_payment_method_type_for_saved_payment_method_payment(item)?, + None, ), Some(api_models::payments::MandateReferenceId::NetworkMandateId( network_transaction_id, @@ -1782,9 +1789,10 @@ impl TryFrom<(&types::PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntent None, StripeBillingAddress::default(), None, + None, ) } - _ => { + Some(api_models::payments::MandateReferenceId::NetworkTokenWithNTI(_)) | None => { let (payment_method_data, payment_method_type, billing_address) = create_stripe_payment_method( &item.request.payment_method_data, @@ -1806,6 +1814,7 @@ impl TryFrom<(&types::PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntent None, billing_address, payment_method_type, + item.request.setup_future_usage, ) } } @@ -1969,7 +1978,7 @@ impl TryFrom<(&types::PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntent customer, setup_mandate_details, off_session: item.request.off_session, - setup_future_usage: item.request.setup_future_usage, + setup_future_usage, payment_method_types, expand: Some(ExpandableObjects::LatestCharge), browser_info, diff --git a/crates/router/src/core/customers.rs b/crates/router/src/core/customers.rs index 059ac4c2f35c..65bbf736f1a7 100644 --- a/crates/router/src/core/customers.rs +++ b/crates/router/src/core/customers.rs @@ -568,7 +568,7 @@ pub async fn delete_customer( ) -> errors::CustomerResponse { let db = &*state.store; let key_manager_state = &(&state).into(); - id.fetch_domain_model_and_update_and_generate_delete_customer_response( + id.redact_customer_details_and_generate_response( db, &key_store, &merchant_account, @@ -585,7 +585,7 @@ pub async fn delete_customer( ))] #[async_trait::async_trait] impl CustomerDeleteBridge for id_type::GlobalCustomerId { - async fn fetch_domain_model_and_update_and_generate_delete_customer_response<'a>( + async fn redact_customer_details_and_generate_response<'a>( &'a self, db: &'a dyn StorageInterface, key_store: &'a domain::MerchantKeyStore, @@ -717,7 +717,7 @@ impl CustomerDeleteBridge for id_type::GlobalCustomerId { #[async_trait::async_trait] trait CustomerDeleteBridge { - async fn fetch_domain_model_and_update_and_generate_delete_customer_response<'a>( + async fn redact_customer_details_and_generate_response<'a>( &'a self, db: &'a dyn StorageInterface, key_store: &'a domain::MerchantKeyStore, @@ -742,7 +742,7 @@ pub async fn delete_customer( let db = &*state.store; let key_manager_state = &(&state).into(); customer_id - .fetch_domain_model_and_update_and_generate_delete_customer_response( + .redact_customer_details_and_generate_response( db, &key_store, &merchant_account, @@ -759,7 +759,7 @@ pub async fn delete_customer( ))] #[async_trait::async_trait] impl CustomerDeleteBridge for id_type::CustomerId { - async fn fetch_domain_model_and_update_and_generate_delete_customer_response<'a>( + async fn redact_customer_details_and_generate_response<'a>( &'a self, db: &'a dyn StorageInterface, key_store: &'a domain::MerchantKeyStore, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 5869dd061872..535a6b72bd67 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -40,7 +40,7 @@ use helpers::{decrypt_paze_token, ApplePayData}; use hyperswitch_domain_models::payments::payment_intent::CustomerData; #[cfg(feature = "v2")] use hyperswitch_domain_models::payments::{ - PaymentConfirmData, PaymentIntentData, PaymentStatusData, + PaymentCaptureData, PaymentConfirmData, PaymentIntentData, PaymentStatusData, }; #[cfg(feature = "v2")] use hyperswitch_domain_models::router_response_types::RedirectForm; @@ -149,7 +149,7 @@ where services::api::ConnectorIntegration, RouterData: - hyperswitch_domain_models::router_data::TrackerPostUpdateObjects, + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects, // To perform router related operation for PaymentResponse PaymentResponse: Operation, @@ -1531,8 +1531,12 @@ where FData: Send + Sync + Clone, Op: Operation + ValidateStatusForOperation + Send + Sync + Clone, Req: Debug, - D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, - Res: transformers::ToResponse, + D: OperationSessionGetters + + OperationSessionSetters + + transformers::GenerateResponse + + Send + + Sync + + Clone, // To create connector flow specific interface data D: ConstructFlowSpecificData, RouterData: Feature, @@ -1546,13 +1550,14 @@ where // To create updatable objects in post update tracker RouterData: - hyperswitch_domain_models::router_data::TrackerPostUpdateObjects, + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects, { // Validate the request fields - let validate_result = operation + operation .to_validate_request()? .validate_request(&req, &merchant_account)?; + // Get the tracker related information. This includes payment intent and payment attempt let get_tracker_response = operation .to_get_tracker()? .get_trackers( @@ -1582,12 +1587,8 @@ where ) .await?; - Res::generate_response( - payment_data, - customer, - &state.base_url, - operation, - &state.conf.connector_request_reference_id_config, + payment_data.generate_response( + &state, connector_http_status_code, external_latency, header_payload.x_hs_latency, @@ -6670,7 +6671,7 @@ pub async fn payment_start_redirection( services::RedirectionFormData { redirect_form: redirection_data, payment_method_data: None, - amount: payment_attempt.amount_details.net_amount.to_string(), + amount: payment_attempt.amount_details.get_net_amount().to_string(), currency: payment_intent.amount_details.currency.to_string(), }, ))) @@ -7830,3 +7831,218 @@ impl OperationSessionSetters for PaymentStatusData { todo!() } } + +#[cfg(feature = "v2")] +impl OperationSessionGetters for PaymentCaptureData { + #[track_caller] + fn get_payment_attempt(&self) -> &storage::PaymentAttempt { + &self.payment_attempt + } + + fn get_payment_intent(&self) -> &storage::PaymentIntent { + &self.payment_intent + } + + fn get_payment_method_info(&self) -> Option<&domain::PaymentMethod> { + todo!() + } + + fn get_mandate_id(&self) -> Option<&payments_api::MandateIds> { + todo!() + } + + // what is this address find out and not required remove this + fn get_address(&self) -> &PaymentAddress { + todo!() + } + + fn get_creds_identifier(&self) -> Option<&str> { + None + } + + fn get_token(&self) -> Option<&str> { + todo!() + } + + fn get_multiple_capture_data(&self) -> Option<&types::MultipleCaptureData> { + todo!() + } + + fn get_payment_link_data(&self) -> Option { + todo!() + } + + fn get_ephemeral_key(&self) -> Option { + todo!() + } + + fn get_setup_mandate(&self) -> Option<&MandateData> { + todo!() + } + + fn get_poll_config(&self) -> Option { + todo!() + } + + fn get_authentication(&self) -> Option<&storage::Authentication> { + todo!() + } + + fn get_frm_message(&self) -> Option { + todo!() + } + + fn get_refunds(&self) -> Vec { + todo!() + } + + fn get_disputes(&self) -> Vec { + todo!() + } + + fn get_authorizations(&self) -> Vec { + todo!() + } + + fn get_attempts(&self) -> Option> { + todo!() + } + + fn get_recurring_details(&self) -> Option<&RecurringDetails> { + todo!() + } + + fn get_payment_intent_profile_id(&self) -> Option<&id_type::ProfileId> { + Some(&self.payment_intent.profile_id) + } + + fn get_currency(&self) -> storage_enums::Currency { + self.payment_intent.amount_details.currency + } + + fn get_amount(&self) -> api::Amount { + todo!() + } + + fn get_payment_attempt_connector(&self) -> Option<&str> { + todo!() + } + + fn get_billing_address(&self) -> Option { + todo!() + } + + fn get_payment_method_data(&self) -> Option<&domain::PaymentMethodData> { + todo!() + } + + fn get_sessions_token(&self) -> Vec { + todo!() + } + + fn get_token_data(&self) -> Option<&storage::PaymentTokenData> { + todo!() + } + + fn get_mandate_connector(&self) -> Option<&MandateConnectorDetails> { + todo!() + } + + fn get_force_sync(&self) -> Option { + todo!() + } + + fn get_capture_method(&self) -> Option { + todo!() + } + + #[cfg(feature = "v2")] + fn get_optional_payment_attempt(&self) -> Option<&storage::PaymentAttempt> { + Some(&self.payment_attempt) + } +} + +#[cfg(feature = "v2")] +impl OperationSessionSetters for PaymentCaptureData { + fn set_payment_intent(&mut self, payment_intent: storage::PaymentIntent) { + self.payment_intent = payment_intent; + } + + fn set_payment_attempt(&mut self, payment_attempt: storage::PaymentAttempt) { + self.payment_attempt = payment_attempt; + } + + fn set_payment_method_data(&mut self, _payment_method_data: Option) { + todo!() + } + + fn set_payment_method_id_in_attempt(&mut self, _payment_method_id: Option) { + todo!() + } + + fn set_email_if_not_present(&mut self, _email: pii::Email) { + todo!() + } + + fn set_pm_token(&mut self, _token: String) { + todo!() + } + + fn set_connector_customer_id(&mut self, _customer_id: Option) { + // TODO: handle this case. Should we add connector_customer_id in paymentConfirmData? + } + + fn push_sessions_token(&mut self, _token: api::SessionToken) { + todo!() + } + + fn set_surcharge_details(&mut self, _surcharge_details: Option) { + todo!() + } + + #[track_caller] + fn set_merchant_connector_id_in_attempt( + &mut self, + merchant_connector_id: Option, + ) { + todo!() + } + + fn set_frm_message(&mut self, _frm_message: FraudCheck) { + todo!() + } + + fn set_payment_intent_status(&mut self, status: storage_enums::IntentStatus) { + self.payment_intent.status = status; + } + + fn set_authentication_type_in_attempt( + &mut self, + _authentication_type: Option, + ) { + todo!() + } + + fn set_recurring_mandate_payment_data( + &mut self, + _recurring_mandate_payment_data: + hyperswitch_domain_models::router_data::RecurringMandatePaymentData, + ) { + todo!() + } + + fn set_mandate_id(&mut self, _mandate_id: api_models::payments::MandateIds) { + todo!() + } + + fn set_setup_future_usage_in_payment_intent( + &mut self, + setup_future_usage: storage_enums::FutureUsage, + ) { + self.payment_intent.setup_future_usage = setup_future_usage; + } + + fn set_connector_in_payment_attempt(&mut self, connector: Option) { + todo!() + } +} diff --git a/crates/router/src/core/payments/flows/capture_flow.rs b/crates/router/src/core/payments/flows/capture_flow.rs index 8b6fa24fbfd7..1863d673ee9c 100644 --- a/crates/router/src/core/payments/flows/capture_flow.rs +++ b/crates/router/src/core/payments/flows/capture_flow.rs @@ -11,12 +11,12 @@ use crate::{ types::{self, api, domain}, }; +#[cfg(feature = "v1")] #[async_trait] impl ConstructFlowSpecificData for PaymentData { - #[cfg(feature = "v1")] async fn construct_router_data<'a>( &self, state: &SessionState, @@ -45,7 +45,24 @@ impl .await } - #[cfg(feature = "v2")] + async fn get_merchant_recipient_data<'a>( + &self, + _state: &SessionState, + _merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _merchant_connector_account: &helpers::MerchantConnectorAccountType, + _connector: &api::ConnectorData, + ) -> RouterResult> { + Ok(None) + } +} + +#[cfg(feature = "v2")] +#[async_trait] +impl + ConstructFlowSpecificData + for hyperswitch_domain_models::payments::PaymentCaptureData +{ async fn construct_router_data<'a>( &self, state: &SessionState, @@ -56,8 +73,21 @@ impl merchant_connector_account: &domain::MerchantConnectorAccount, merchant_recipient_data: Option, header_payload: Option, - ) -> RouterResult { - todo!() + ) -> RouterResult< + types::RouterData, + > { + Box::pin(transformers::construct_payment_router_data_for_capture( + state, + self.clone(), + connector_id, + merchant_account, + key_store, + customer, + merchant_connector_account, + merchant_recipient_data, + header_payload, + )) + .await } async fn get_merchant_recipient_data<'a>( diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 842e0610ee7d..4adb94034181 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -42,6 +42,9 @@ pub mod payment_update_intent; #[cfg(feature = "v2")] pub mod payment_get; +#[cfg(feature = "v2")] +pub mod payment_capture_v2; + use api_models::enums::FrmSuggestion; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] use api_models::routing::RoutableConnectorChoice; @@ -441,7 +444,7 @@ pub trait PostUpdateTracker: Send { where F: 'b + Send + Sync, types::RouterData: - hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; async fn save_pm_and_mandate<'b>( &self, diff --git a/crates/router/src/core/payments/operations/payment_capture_v2.rs b/crates/router/src/core/payments/operations/payment_capture_v2.rs new file mode 100644 index 000000000000..0ee62ea73c1f --- /dev/null +++ b/crates/router/src/core/payments/operations/payment_capture_v2.rs @@ -0,0 +1,324 @@ +use api_models::{enums::FrmSuggestion, payments::PaymentsCaptureRequest}; +use async_trait::async_trait; +use error_stack::ResultExt; +use hyperswitch_domain_models::payments::PaymentCaptureData; +use router_env::{instrument, tracing}; + +use super::{Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; +use crate::{ + core::{ + errors::{self, CustomResult, RouterResult, StorageErrorExt}, + payments::operations::{self, ValidateStatusForOperation}, + }, + routes::{app::ReqState, SessionState}, + types::{ + api::{self, ConnectorCallType}, + domain::{self}, + storage::{self, enums as storage_enums}, + }, + utils::OptionExt, +}; + +#[derive(Debug, Clone, Copy)] +pub struct PaymentsCapture; + +impl ValidateStatusForOperation for PaymentsCapture { + /// Validate if the current operation can be performed on the current status of the payment intent + fn validate_status_for_operation( + &self, + intent_status: common_enums::IntentStatus, + ) -> Result<(), errors::ApiErrorResponse> { + match intent_status { + common_enums::IntentStatus::RequiresCapture + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => Ok(()), + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Cancelled + | common_enums::IntentStatus::Processing + | common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::RequiresConfirmation => { + Err(errors::ApiErrorResponse::PaymentUnexpectedState { + current_flow: format!("{self:?}"), + field_name: "status".to_string(), + current_value: intent_status.to_string(), + states: [ + common_enums::IntentStatus::RequiresCapture, + common_enums::IntentStatus::PartiallyCapturedAndCapturable, + ] + .map(|enum_value| enum_value.to_string()) + .join(", "), + }) + } + } + } +} + +type BoxedConfirmOperation<'b, F> = + super::BoxedOperation<'b, F, PaymentsCaptureRequest, PaymentCaptureData>; + +// TODO: change the macro to include changes for v2 +// TODO: PaymentData in the macro should be an input +impl Operation for &PaymentsCapture { + type Data = PaymentCaptureData; + fn to_validate_request( + &self, + ) -> RouterResult<&(dyn ValidateRequest + Send + Sync)> + { + Ok(*self) + } + fn to_get_tracker( + &self, + ) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(*self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(*self) + } + fn to_update_tracker( + &self, + ) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> + { + Ok(*self) + } +} +#[automatically_derived] +impl Operation for PaymentsCapture { + type Data = PaymentCaptureData; + fn to_validate_request( + &self, + ) -> RouterResult<&(dyn ValidateRequest + Send + Sync)> + { + Ok(self) + } + fn to_get_tracker( + &self, + ) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(self) + } + fn to_domain(&self) -> RouterResult<&dyn Domain> { + Ok(self) + } + fn to_update_tracker( + &self, + ) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> + { + Ok(self) + } +} + +impl ValidateRequest> + for PaymentsCapture +{ + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + _request: &PaymentsCaptureRequest, + merchant_account: &'a domain::MerchantAccount, + ) -> RouterResult { + let validate_result = operations::ValidateResult { + merchant_id: merchant_account.get_id().to_owned(), + storage_scheme: merchant_account.storage_scheme, + requeue: false, + }; + + Ok(validate_result) + } +} + +#[async_trait] +impl GetTracker, PaymentsCaptureRequest> + for PaymentsCapture +{ + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a SessionState, + payment_id: &common_utils::id_type::GlobalPaymentId, + request: &PaymentsCaptureRequest, + merchant_account: &domain::MerchantAccount, + _profile: &domain::Profile, + key_store: &domain::MerchantKeyStore, + _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + ) -> RouterResult>> { + let db = &*state.store; + let key_manager_state = &state.into(); + + let storage_scheme = merchant_account.storage_scheme; + + let payment_intent = db + .find_payment_intent_by_id(key_manager_state, payment_id, key_store, storage_scheme) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + self.validate_status_for_operation(payment_intent.status)?; + + let active_attempt_id = payment_intent + .active_attempt_id + .as_ref() + .get_required_value("active_attempt_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Active attempt id is none when capturing the payment")?; + + let mut payment_attempt = db + .find_payment_attempt_by_id( + key_manager_state, + key_store, + active_attempt_id, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not find payment attempt given the attempt id")?; + + if let Some(amount_to_capture) = request.amount_to_capture { + payment_attempt + .amount_details + .validate_amount_to_capture(amount_to_capture) + .change_context(errors::ApiErrorResponse::PreconditionFailed { + message: format!( + "`amount_to_capture` is greater than the net amount {}", + payment_attempt.amount_details.get_net_amount() + ), + })?; + + payment_attempt + .amount_details + .set_amount_to_capture(amount_to_capture); + } + + let payment_data = PaymentCaptureData { + flow: std::marker::PhantomData, + payment_intent, + payment_attempt, + }; + + let get_trackers_response = operations::GetTrackerResponse { payment_data }; + + Ok(get_trackers_response) + } +} + +#[async_trait] +impl Domain> for PaymentsCapture { + async fn get_customer_details<'a>( + &'a self, + state: &SessionState, + payment_data: &mut PaymentCaptureData, + merchant_key_store: &domain::MerchantKeyStore, + storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult<(BoxedConfirmOperation<'a, F>, Option), errors::StorageError> + { + match payment_data.payment_intent.customer_id.clone() { + Some(id) => { + let customer = state + .store + .find_customer_by_global_id( + &state.into(), + &id, + &payment_data.payment_intent.merchant_id, + merchant_key_store, + storage_scheme, + ) + .await?; + + Ok((Box::new(self), Some(customer))) + } + None => Ok((Box::new(self), None)), + } + } + + #[instrument(skip_all)] + async fn make_pm_data<'a>( + &'a self, + _state: &'a SessionState, + _payment_data: &mut PaymentCaptureData, + _storage_scheme: storage_enums::MerchantStorageScheme, + _key_store: &domain::MerchantKeyStore, + _customer: &Option, + _business_profile: &domain::Profile, + ) -> RouterResult<( + BoxedConfirmOperation<'a, F>, + Option, + Option, + )> { + Ok((Box::new(self), None, None)) + } + + #[instrument(skip_all)] + async fn perform_routing<'a>( + &'a self, + _merchant_account: &domain::MerchantAccount, + _business_profile: &domain::Profile, + state: &SessionState, + // TODO: do not take the whole payment data here + payment_data: &mut PaymentCaptureData, + _mechant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + let payment_attempt = &payment_data.payment_attempt; + let connector = payment_attempt + .connector + .as_ref() + .get_required_value("connector") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Connector is none when constructing response")?; + + let merchant_connector_id = payment_attempt + .merchant_connector_id + .as_ref() + .get_required_value("merchant_connector_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Merchant connector id is none when constructing response")?; + + let connector_data = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + connector, + api::GetToken::Connector, + Some(merchant_connector_id.to_owned()), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid connector name received")?; + + Ok(ConnectorCallType::PreDetermined(connector_data)) + } +} + +#[async_trait] +impl UpdateTracker, PaymentsCaptureRequest> for PaymentsCapture { + #[instrument(skip_all)] + async fn update_trackers<'b>( + &'b self, + state: &'b SessionState, + _req_state: ReqState, + mut payment_data: PaymentCaptureData, + _customer: Option, + storage_scheme: storage_enums::MerchantStorageScheme, + _updated_customer: Option, + key_store: &domain::MerchantKeyStore, + _frm_suggestion: Option, + _header_payload: hyperswitch_domain_models::payments::HeaderPayload, + ) -> RouterResult<(BoxedConfirmOperation<'b, F>, PaymentCaptureData)> + where + F: 'b + Send, + { + let payment_attempt_update = hyperswitch_domain_models::payments::payment_attempt::PaymentAttemptUpdate::PreCaptureUpdate { amount_to_capture: payment_data.payment_attempt.amount_details.get_amount_to_capture(), updated_by: storage_scheme.to_string() }; + + let payment_attempt = state + .store + .update_payment_attempt( + &state.into(), + key_store, + payment_data.payment_attempt.clone(), + payment_attempt_update, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not update payment attempt")?; + + payment_data.payment_attempt = payment_attempt; + Ok((Box::new(self), payment_data)) + } +} diff --git a/crates/router/src/core/payments/operations/payment_get.rs b/crates/router/src/core/payments/operations/payment_get.rs index fab187f43eff..6419376f0902 100644 --- a/crates/router/src/core/payments/operations/payment_get.rs +++ b/crates/router/src/core/payments/operations/payment_get.rs @@ -1,38 +1,23 @@ -use api_models::{ - admin::ExtendedCardInfoConfig, - enums::FrmSuggestion, - payments::{ExtendedCardInfo, GetAddressFromPaymentMethodData, PaymentsRetrieveRequest}, -}; +use api_models::{enums::FrmSuggestion, payments::PaymentsRetrieveRequest}; use async_trait::async_trait; use common_utils::ext_traits::AsyncExt; use error_stack::ResultExt; -use hyperswitch_domain_models::payments::{ - payment_attempt::PaymentAttempt, PaymentIntent, PaymentStatusData, -}; +use hyperswitch_domain_models::payments::PaymentStatusData; use router_env::{instrument, tracing}; -use tracing_futures::Instrument; use super::{Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; use crate::{ core::{ - authentication, errors::{self, CustomResult, RouterResult, StorageErrorExt}, - payments::{ - self, helpers, - operations::{self, ValidateStatusForOperation}, - populate_surcharge_details, CustomerDetails, PaymentAddress, PaymentData, - }, - utils as core_utils, + payments::operations::{self, ValidateStatusForOperation}, }, routes::{app::ReqState, SessionState}, - services, types::{ - self, - api::{self, ConnectorCallType, PaymentIdTypeExt}, + api::{self, ConnectorCallType}, domain::{self}, storage::{self, enums as storage_enums}, }, - utils::{self, OptionExt}, + utils::OptionExt, }; #[derive(Debug, Clone, Copy)] @@ -107,7 +92,7 @@ impl ValidateRequest( &'b self, - request: &PaymentsRetrieveRequest, + _request: &PaymentsRetrieveRequest, merchant_account: &'a domain::MerchantAccount, ) -> RouterResult { let validate_result = operations::ValidateResult { @@ -232,12 +217,12 @@ impl Domain( &'a self, - state: &'a SessionState, - payment_data: &mut PaymentStatusData, - storage_scheme: storage_enums::MerchantStorageScheme, - key_store: &domain::MerchantKeyStore, - customer: &Option, - business_profile: &domain::Profile, + _state: &'a SessionState, + _payment_data: &mut PaymentStatusData, + _storage_scheme: storage_enums::MerchantStorageScheme, + _key_store: &domain::MerchantKeyStore, + _customer: &Option, + _business_profile: &domain::Profile, ) -> RouterResult<( BoxedConfirmOperation<'a, F>, Option, @@ -249,12 +234,12 @@ impl Domain( &'a self, - merchant_account: &domain::MerchantAccount, - business_profile: &domain::Profile, + _merchant_account: &domain::MerchantAccount, + _business_profile: &domain::Profile, state: &SessionState, // TODO: do not take the whole payment data here payment_data: &mut PaymentStatusData, - mechant_key_store: &domain::MerchantKeyStore, + _mechant_key_store: &domain::MerchantKeyStore, ) -> CustomResult { match &payment_data.payment_attempt { Some(payment_attempt) if payment_data.should_sync_with_connector => { @@ -295,15 +280,15 @@ impl UpdateTracker, PaymentsRetrieveReq #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - state: &'b SessionState, - req_state: ReqState, - mut payment_data: PaymentStatusData, - customer: Option, - storage_scheme: storage_enums::MerchantStorageScheme, - updated_customer: Option, - key_store: &domain::MerchantKeyStore, - frm_suggestion: Option, - header_payload: hyperswitch_domain_models::payments::HeaderPayload, + _state: &'b SessionState, + _req_state: ReqState, + payment_data: PaymentStatusData, + _customer: Option, + _storage_scheme: storage_enums::MerchantStorageScheme, + _updated_customer: Option, + _key_store: &domain::MerchantKeyStore, + _frm_suggestion: Option, + _header_payload: hyperswitch_domain_models::payments::HeaderPayload, ) -> RouterResult<(BoxedConfirmOperation<'b, F>, PaymentStatusData)> where F: 'b + Send, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 01a4e0860918..988429a0eecb 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -2200,6 +2200,88 @@ impl Operation for PaymentResp } } +#[cfg(feature = "v2")] +impl Operation for PaymentResponse { + type Data = hyperswitch_domain_models::payments::PaymentCaptureData; + fn to_post_update_tracker( + &self, + ) -> RouterResult< + &(dyn PostUpdateTracker + Send + Sync), + > { + Ok(self) + } +} + +#[cfg(feature = "v2")] +#[async_trait] +impl + PostUpdateTracker< + F, + hyperswitch_domain_models::payments::PaymentCaptureData, + types::PaymentsCaptureData, + > for PaymentResponse +{ + async fn update_tracker<'b>( + &'b self, + state: &'b SessionState, + mut payment_data: hyperswitch_domain_models::payments::PaymentCaptureData, + response: types::RouterData, + key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, + ) -> RouterResult> + where + F: 'b + Send + Sync, + types::RouterData: + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects< + F, + types::PaymentsCaptureData, + hyperswitch_domain_models::payments::PaymentCaptureData, + >, + { + use hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; + + let db = &*state.store; + let key_manager_state = &state.into(); + + let response_router_data = response; + + let payment_intent_update = + response_router_data.get_payment_intent_update(&payment_data, storage_scheme); + + let payment_attempt_update = + response_router_data.get_payment_attempt_update(&payment_data, storage_scheme); + + let updated_payment_intent = db + .update_payment_intent( + key_manager_state, + payment_data.payment_intent, + payment_intent_update, + key_store, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to update payment intent")?; + + let updated_payment_attempt = db + .update_payment_attempt( + key_manager_state, + key_store, + payment_data.payment_attempt, + payment_attempt_update, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to update payment attempt")?; + + payment_data.payment_intent = updated_payment_intent; + payment_data.payment_attempt = updated_payment_attempt; + + Ok(payment_data) + } +} + #[cfg(feature = "v2")] #[async_trait] impl PostUpdateTracker, types::PaymentsAuthorizeData> @@ -2219,6 +2301,7 @@ impl PostUpdateTracker, types::PaymentsAuthor hyperswitch_domain_models::router_data::TrackerPostUpdateObjects< F, types::PaymentsAuthorizeData, + PaymentConfirmData, >, { use hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; @@ -2228,9 +2311,10 @@ impl PostUpdateTracker, types::PaymentsAuthor let response_router_data = response; - let payment_intent_update = response_router_data.get_payment_intent_update(storage_scheme); + let payment_intent_update = + response_router_data.get_payment_intent_update(&payment_data, storage_scheme); let payment_attempt_update = - response_router_data.get_payment_attempt_update(storage_scheme); + response_router_data.get_payment_attempt_update(&payment_data, storage_scheme); let updated_payment_intent = db .update_payment_intent( @@ -2293,6 +2377,7 @@ impl PostUpdateTracker, types::PaymentsSyncDat hyperswitch_domain_models::router_data::TrackerPostUpdateObjects< F, types::PaymentsSyncData, + PaymentStatusData, >, { use hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; @@ -2302,9 +2387,10 @@ impl PostUpdateTracker, types::PaymentsSyncDat let response_router_data = response; - let payment_intent_update = response_router_data.get_payment_intent_update(storage_scheme); + let payment_intent_update = + response_router_data.get_payment_intent_update(&payment_data, storage_scheme); let payment_attempt_update = - response_router_data.get_payment_attempt_update(storage_scheme); + response_router_data.get_payment_attempt_update(&payment_data, storage_scheme); let payment_attempt = payment_data .payment_attempt diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 15f01074c97e..d4c3877e5d97 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -19,8 +19,6 @@ use diesel_models::{ }; use error_stack::{report, ResultExt}; #[cfg(feature = "v2")] -use hyperswitch_domain_models::payments::{PaymentConfirmData, PaymentIntentData}; -#[cfg(feature = "v2")] use hyperswitch_domain_models::ApiModelToDieselModelConvertor; use hyperswitch_domain_models::{payments::payment_intent::CustomerData, router_request_types}; #[cfg(feature = "v2")] @@ -183,7 +181,7 @@ where #[allow(clippy::too_many_arguments)] pub async fn construct_payment_router_data_for_authorize<'a>( state: &'a SessionState, - payment_data: PaymentConfirmData, + payment_data: hyperswitch_domain_models::payments::PaymentConfirmData, connector_id: &str, merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, @@ -259,9 +257,9 @@ pub async fn construct_payment_router_data_for_authorize<'a>( amount: payment_data .payment_attempt .amount_details - .net_amount + .get_net_amount() .get_amount_as_i64(), - minor_amount: payment_data.payment_attempt.amount_details.net_amount, + minor_amount: payment_data.payment_attempt.amount_details.get_net_amount(), order_tax_amount: None, currency: payment_data.payment_intent.amount_details.currency, browser_info: None, @@ -382,6 +380,174 @@ pub async fn construct_payment_router_data_for_authorize<'a>( Ok(router_data) } +#[cfg(feature = "v2")] +#[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +pub async fn construct_payment_router_data_for_capture<'a>( + state: &'a SessionState, + payment_data: hyperswitch_domain_models::payments::PaymentCaptureData, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &'a Option, + merchant_connector_account: &domain::MerchantConnectorAccount, + _merchant_recipient_data: Option, + header_payload: Option, +) -> RouterResult { + use masking::ExposeOptionInterface; + + fp_utils::when(merchant_connector_account.is_disabled(), || { + Err(errors::ApiErrorResponse::MerchantConnectorAccountDisabled) + })?; + + let auth_type = merchant_connector_account + .get_connector_account_details() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while parsing value for ConnectorAuthType")?; + + let customer_id = customer + .to_owned() + .map(|customer| common_utils::id_type::CustomerId::try_from(customer.id.clone())) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Invalid global customer generated, not able to convert to reference id", + )?; + + let payment_method = payment_data.payment_attempt.payment_method_type; + + let connector_request_reference_id = payment_data + .payment_intent + .merchant_reference_id + .map(|id| id.get_string_repr().to_owned()) + .unwrap_or(payment_data.payment_attempt.id.get_string_repr().to_owned()); + + let connector_mandate_request_reference_id = payment_data + .payment_attempt + .connector_mandate_detail + .as_ref() + .and_then(|detail| detail.get_connector_mandate_request_reference_id()); + + let connector = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + connector_id, + api::GetToken::Connector, + payment_data.payment_attempt.merchant_connector_id.clone(), + )?; + + let amount_to_capture = payment_data + .payment_attempt + .amount_details + .get_amount_to_capture() + .unwrap_or(payment_data.payment_attempt.amount_details.get_net_amount()); + + let amount = payment_data.payment_attempt.amount_details.get_net_amount(); + let request = types::PaymentsCaptureData { + capture_method: Some(payment_data.payment_intent.capture_method), + amount_to_capture: amount_to_capture.get_amount_as_i64(), // This should be removed once we start moving to connector module + minor_amount_to_capture: amount_to_capture, + currency: payment_data.payment_intent.amount_details.currency, + connector_transaction_id: connector + .connector + .connector_transaction_id(payment_data.payment_attempt.clone())? + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, + payment_amount: amount.get_amount_as_i64(), // This should be removed once we start moving to connector module + minor_payment_amount: amount, + connector_meta: payment_data + .payment_attempt + .connector_metadata + .clone() + .expose_option(), + // TODO: add multiple capture data + multiple_capture_data: None, + // TODO: why do we need browser info during capture? + browser_info: None, + metadata: payment_data.payment_intent.metadata.expose_option(), + integrity_object: None, + }; + + // TODO: evaluate the fields in router data, if they are required or not + let router_data = types::RouterData { + flow: PhantomData, + merchant_id: merchant_account.get_id().clone(), + // TODO: evaluate why we need customer id at the connector level. We already have connector customer id. + customer_id, + connector: connector_id.to_owned(), + // TODO: evaluate why we need payment id at the connector level. We already have connector reference id + payment_id: payment_data + .payment_attempt + .payment_id + .get_string_repr() + .to_owned(), + // TODO: evaluate why we need attempt id at the connector level. We already have connector reference id + attempt_id: payment_data + .payment_attempt + .get_id() + .get_string_repr() + .to_owned(), + status: payment_data.payment_attempt.status, + payment_method, + connector_auth_type: auth_type, + description: payment_data + .payment_intent + .description + .as_ref() + .map(|description| description.get_string_repr()) + .map(ToOwned::to_owned), + // TODO: evaluate why we need to send merchant's return url here + // This should be the return url of application, since application takes care of the redirection + return_url: payment_data + .payment_intent + .return_url + .as_ref() + .map(|description| description.get_string_repr()) + .map(ToOwned::to_owned), + // TODO: Create unified address + address: hyperswitch_domain_models::payment_address::PaymentAddress::default(), + auth_type: payment_data.payment_attempt.authentication_type, + connector_meta_data: None, + connector_wallets_details: None, + request, + response: Err(hyperswitch_domain_models::router_data::ErrorResponse::default()), + amount_captured: None, + minor_amount_captured: None, + access_token: None, + session_token: None, + reference_id: None, + payment_method_status: None, + payment_method_token: None, + connector_customer: None, + recurring_mandate_payment_data: None, + // TODO: This has to be generated as the reference id based on the connector configuration + // Some connectros might not accept accept the global id. This has to be done when generating the reference id + connector_request_reference_id, + preprocessing_id: payment_data.payment_attempt.preprocessing_step_id, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + // TODO: take this based on the env + test_mode: Some(true), + payment_method_balance: None, + connector_api_version: None, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, + connector_response: None, + integrity_check: Ok(()), + additional_merchant_data: None, + header_payload, + connector_mandate_request_reference_id, + psd2_sca_exemption_type: None, + authentication_id: None, + }; + + Ok(router_data) +} + #[cfg(feature = "v2")] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] @@ -424,7 +590,7 @@ pub async fn construct_router_data_for_psync<'a>( .unwrap_or(attempt.id.get_string_repr().to_owned()); let request = types::PaymentsSyncData { - amount: attempt.amount_details.net_amount, + amount: attempt.amount_details.get_net_amount(), integrity_object: None, mandate_id: None, connector_transaction_id: match attempt.get_connector_payment_id() { @@ -521,7 +687,7 @@ pub async fn construct_router_data_for_psync<'a>( #[allow(clippy::too_many_arguments)] pub async fn construct_payment_router_data_for_sdk_session<'a>( _state: &'a SessionState, - payment_data: PaymentIntentData, + payment_data: hyperswitch_domain_models::payments::PaymentIntentData, connector_id: &str, merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, @@ -915,6 +1081,57 @@ where ) -> RouterResponse; } +/// Generate a response from the given Data. This should be implemented on a payment data object +pub trait GenerateResponse +where + Self: Sized, +{ + #[cfg(feature = "v2")] + fn generate_response( + self, + state: &SessionState, + connector_http_status_code: Option, + external_latency: Option, + is_latency_header_enabled: Option, + merchant_account: &domain::MerchantAccount, + ) -> RouterResponse; +} + +#[cfg(feature = "v2")] +impl GenerateResponse + for hyperswitch_domain_models::payments::PaymentCaptureData +where + F: Clone, +{ + fn generate_response( + self, + state: &SessionState, + connector_http_status_code: Option, + external_latency: Option, + is_latency_header_enabled: Option, + merchant_account: &domain::MerchantAccount, + ) -> RouterResponse { + let payment_intent = &self.payment_intent; + let payment_attempt = &self.payment_attempt; + + let amount = api_models::payments::PaymentAmountDetailsResponse::foreign_from(( + &payment_intent.amount_details, + &payment_attempt.amount_details, + )); + + let response = api_models::payments::PaymentsCaptureResponse { + id: payment_intent.id.clone(), + amount, + status: payment_intent.status, + }; + + Ok(services::ApplicationResponse::JsonWithHeaders(( + response, + vec![], + ))) + } +} + #[cfg(feature = "v1")] impl ToResponse for api::PaymentsResponse where @@ -1170,28 +1387,23 @@ where } #[cfg(feature = "v2")] -impl ToResponse for api_models::payments::PaymentsConfirmIntentResponse +impl GenerateResponse + for hyperswitch_domain_models::payments::PaymentConfirmData where F: Clone, - Op: Debug, - D: OperationSessionGetters, { - #[allow(clippy::too_many_arguments)] fn generate_response( - payment_data: D, - _customer: Option, - base_url: &str, - operation: Op, - _connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig, - _connector_http_status_code: Option, - _external_latency: Option, - _is_latency_header_enabled: Option, + self, + state: &SessionState, + connector_http_status_code: Option, + external_latency: Option, + is_latency_header_enabled: Option, merchant_account: &domain::MerchantAccount, - ) -> RouterResponse { - let payment_intent = payment_data.get_payment_intent(); - let payment_attempt = payment_data.get_payment_attempt(); + ) -> RouterResponse { + let payment_intent = self.payment_intent; + let payment_attempt = self.payment_attempt; - let amount = api_models::payments::ConfirmIntentAmountDetailsResponse::foreign_from(( + let amount = api_models::payments::PaymentAmountDetailsResponse::foreign_from(( &payment_intent.amount_details, &payment_attempt.amount_details, )); @@ -1215,7 +1427,7 @@ where .clone() .map(api_models::payments::ErrorDetails::foreign_from); - let payment_address = payment_data.get_address(); + let payment_address = self.payment_address; let payment_method_data = Some(api_models::payments::PaymentMethodDataResponseWithBilling { @@ -1227,17 +1439,21 @@ where }); // TODO: Add support for other next actions, currently only supporting redirect to url - let redirect_to_url = payment_intent - .create_start_redirection_url(base_url, merchant_account.publishable_key.clone())?; + let redirect_to_url = payment_intent.create_start_redirection_url( + &state.base_url, + merchant_account.publishable_key.clone(), + )?; + let next_action = payment_attempt .redirection_data .as_ref() .map(|_| api_models::payments::NextActionData::RedirectToUrl { redirect_to_url }); - let response = Self { + let response = api_models::payments::PaymentsConfirmIntentResponse { id: payment_intent.id.clone(), status: payment_intent.status, amount, + customer_id: payment_intent.customer_id.clone(), connector, client_secret: payment_intent.client_secret.clone(), created: payment_intent.created_at, @@ -1260,69 +1476,73 @@ where } #[cfg(feature = "v2")] -impl ToResponse for api_models::payments::PaymentsRetrieveResponse +impl GenerateResponse + for hyperswitch_domain_models::payments::PaymentStatusData where F: Clone, - Op: Debug, - D: OperationSessionGetters, { - #[allow(clippy::too_many_arguments)] fn generate_response( - payment_data: D, - _customer: Option, - _base_url: &str, - operation: Op, - _connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig, - _connector_http_status_code: Option, - _external_latency: Option, - _is_latency_header_enabled: Option, - _merchant_account: &domain::MerchantAccount, - ) -> RouterResponse { - let payment_intent = payment_data.get_payment_intent(); - let payment_attempt = payment_data.get_optional_payment_attempt(); + self, + state: &SessionState, + connector_http_status_code: Option, + external_latency: Option, + is_latency_header_enabled: Option, + merchant_account: &domain::MerchantAccount, + ) -> RouterResponse { + let payment_intent = self.payment_intent; + let optional_payment_attempt = self.payment_attempt.as_ref(); - let amount = api_models::payments::ConfirmIntentAmountDetailsResponse::foreign_from(( + let amount = api_models::payments::PaymentAmountDetailsResponse::foreign_from(( &payment_intent.amount_details, - payment_attempt.map(|payment_attempt| &payment_attempt.amount_details), + optional_payment_attempt.map(|payment_attempt| &payment_attempt.amount_details), )); let connector = - payment_attempt.and_then(|payment_attempt| payment_attempt.connector.clone()); + optional_payment_attempt.and_then(|payment_attempt| payment_attempt.connector.clone()); - let merchant_connector_id = payment_attempt + let merchant_connector_id = optional_payment_attempt .and_then(|payment_attempt| payment_attempt.merchant_connector_id.clone()); - let error = payment_attempt + let error = optional_payment_attempt .and_then(|payment_attempt| payment_attempt.error.clone()) .map(api_models::payments::ErrorDetails::foreign_from); - let payment_address = payment_data.get_address(); - let payment_method_data = Some(api_models::payments::PaymentMethodDataResponseWithBilling { payment_method_data: None, - billing: payment_address + billing: self + .payment_address .get_request_payment_method_billing() .cloned() .map(From::from), }); - let response = Self { + let response = api_models::payments::PaymentsRetrieveResponse { id: payment_intent.id.clone(), status: payment_intent.status, amount, + customer_id: payment_intent.customer_id.clone(), connector, - billing: payment_address + billing: self + .payment_address .get_payment_billing() .cloned() .map(From::from), - shipping: payment_address.get_shipping().cloned().map(From::from), + shipping: self.payment_address.get_shipping().cloned().map(From::from), client_secret: payment_intent.client_secret.clone(), created: payment_intent.created_at, payment_method_data, - payment_method_type: payment_attempt.map(|attempt| attempt.payment_method_type), - payment_method_subtype: payment_attempt.map(|attempt| attempt.payment_method_subtype), - connector_transaction_id: payment_attempt + payment_method_type: self + .payment_attempt + .as_ref() + .map(|attempt| attempt.payment_method_type), + payment_method_subtype: self + .payment_attempt + .as_ref() + .map(|attempt| attempt.payment_method_subtype), + connector_transaction_id: self + .payment_attempt + .as_ref() .and_then(|attempt| attempt.connector_payment_id.clone()), connector_reference_id: None, merchant_connector_id, @@ -2620,10 +2840,7 @@ impl TryFrom> for types::PaymentsAuthoriz payment_method_data: (payment_method_data.get_required_value("payment_method_data")?), setup_future_usage: payment_data.payment_intent.setup_future_usage, mandate_id: payment_data.mandate_id.clone(), - off_session: payment_data - .mandate_id - .as_ref() - .and(payment_data.payment_intent.off_session), + off_session: payment_data.mandate_id.as_ref().map(|_| true), setup_mandate_details: payment_data.setup_mandate.clone(), confirm: payment_data.payment_attempt.confirm, statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix, @@ -2839,7 +3056,44 @@ impl TryFrom> for types::PaymentsCaptureD type Error = error_stack::Report; fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { - todo!() + use masking::ExposeOptionInterface; + + let payment_data = additional_data.payment_data; + let connector = api::ConnectorData::get_connector_by_name( + &additional_data.state.conf.connectors, + &additional_data.connector_name, + api::GetToken::Connector, + payment_data.payment_attempt.merchant_connector_id.clone(), + )?; + let amount_to_capture = payment_data + .payment_attempt + .amount_details + .get_amount_to_capture() + .unwrap_or(payment_data.payment_attempt.get_total_amount()); + + let amount = payment_data.payment_attempt.get_total_amount(); + Ok(Self { + capture_method: Some(payment_data.payment_intent.capture_method), + amount_to_capture: amount_to_capture.get_amount_as_i64(), // This should be removed once we start moving to connector module + minor_amount_to_capture: amount_to_capture, + currency: payment_data.currency, + connector_transaction_id: connector + .connector + .connector_transaction_id(payment_data.payment_attempt.clone())? + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, + payment_amount: amount.get_amount_as_i64(), // This should be removed once we start moving to connector module + minor_payment_amount: amount, + connector_meta: payment_data + .payment_attempt + .connector_metadata + .expose_option(), + // TODO: add multiple capture data + multiple_capture_data: None, + // TODO: why do we need browser info during capture? + browser_info: None, + metadata: payment_data.payment_intent.metadata.expose_option(), + integrity_object: None, + }) } } @@ -3358,10 +3612,7 @@ impl TryFrom> for types::SetupMandateRequ .get_required_value("payment_method_data")?), statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix, setup_future_usage: payment_data.payment_intent.setup_future_usage, - off_session: payment_data - .mandate_id - .as_ref() - .and(payment_data.payment_intent.off_session), + off_session: payment_data.mandate_id.as_ref().map(|_| true), mandate_id: payment_data.mandate_id.clone(), setup_mandate_details: payment_data.setup_mandate, customer_acceptance: payment_data.customer_acceptance, @@ -3476,10 +3727,7 @@ impl TryFrom> for types::CompleteAuthoriz Ok(Self { setup_future_usage: payment_data.payment_intent.setup_future_usage, mandate_id: payment_data.mandate_id.clone(), - off_session: payment_data - .mandate_id - .as_ref() - .and(payment_data.payment_intent.off_session), + off_session: payment_data.mandate_id.as_ref().map(|_| true), setup_mandate_details: payment_data.setup_mandate.clone(), confirm: payment_data.payment_attempt.confirm, statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix, @@ -3638,7 +3886,7 @@ impl ForeignFrom<( &hyperswitch_domain_models::payments::AmountDetails, &hyperswitch_domain_models::payments::payment_attempt::AttemptAmountDetails, - )> for api_models::payments::ConfirmIntentAmountDetailsResponse + )> for api_models::payments::PaymentAmountDetailsResponse { fn foreign_from( (intent_amount_details, attempt_amount_details): ( @@ -3649,15 +3897,15 @@ impl Self { order_amount: intent_amount_details.order_amount, currency: intent_amount_details.currency, - shipping_cost: attempt_amount_details.shipping_cost, - order_tax_amount: attempt_amount_details.order_tax_amount, + shipping_cost: attempt_amount_details.get_shipping_cost(), + order_tax_amount: attempt_amount_details.get_order_tax_amount(), external_tax_calculation: intent_amount_details.skip_external_tax_calculation, surcharge_calculation: intent_amount_details.skip_surcharge_calculation, - surcharge_amount: attempt_amount_details.surcharge_amount, - tax_on_surcharge: attempt_amount_details.tax_on_surcharge, - net_amount: attempt_amount_details.net_amount, - amount_to_capture: attempt_amount_details.amount_to_capture, - amount_capturable: attempt_amount_details.amount_capturable, + surcharge_amount: attempt_amount_details.get_surcharge_amount(), + tax_on_surcharge: attempt_amount_details.get_tax_on_surcharge(), + net_amount: attempt_amount_details.get_net_amount(), + amount_to_capture: attempt_amount_details.get_amount_to_capture(), + amount_capturable: attempt_amount_details.get_amount_capturable(), amount_captured: intent_amount_details.amount_captured, } } @@ -3670,7 +3918,7 @@ impl ForeignFrom<( &hyperswitch_domain_models::payments::AmountDetails, Option<&hyperswitch_domain_models::payments::payment_attempt::AttemptAmountDetails>, - )> for api_models::payments::ConfirmIntentAmountDetailsResponse + )> for api_models::payments::PaymentAmountDetailsResponse { fn foreign_from( (intent_amount_details, attempt_amount_details): ( @@ -3682,10 +3930,10 @@ impl order_amount: intent_amount_details.order_amount, currency: intent_amount_details.currency, shipping_cost: attempt_amount_details - .and_then(|attempt_amount| attempt_amount.shipping_cost) + .and_then(|attempt_amount| attempt_amount.get_shipping_cost()) .or(intent_amount_details.shipping_cost), order_tax_amount: attempt_amount_details - .and_then(|attempt_amount| attempt_amount.order_tax_amount) + .and_then(|attempt_amount| attempt_amount.get_order_tax_amount()) .or(intent_amount_details .tax_details .as_ref() @@ -3693,17 +3941,18 @@ impl external_tax_calculation: intent_amount_details.skip_external_tax_calculation, surcharge_calculation: intent_amount_details.skip_surcharge_calculation, surcharge_amount: attempt_amount_details - .and_then(|attempt| attempt.surcharge_amount) + .and_then(|attempt| attempt.get_surcharge_amount()) .or(intent_amount_details.surcharge_amount), tax_on_surcharge: attempt_amount_details - .and_then(|attempt| attempt.tax_on_surcharge) + .and_then(|attempt| attempt.get_tax_on_surcharge()) .or(intent_amount_details.tax_on_surcharge), net_amount: attempt_amount_details - .map(|attempt| attempt.net_amount) + .map(|attempt| attempt.get_net_amount()) .unwrap_or(intent_amount_details.calculate_net_amount()), - amount_to_capture: attempt_amount_details.and_then(|attempt| attempt.amount_to_capture), + amount_to_capture: attempt_amount_details + .and_then(|attempt| attempt.get_amount_to_capture()), amount_capturable: attempt_amount_details - .map(|attempt| attempt.amount_capturable) + .map(|attempt| attempt.get_amount_capturable()) .unwrap_or(MinorUnit::zero()), amount_captured: intent_amount_details.amount_captured, } diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index b3c951f0964f..2abb5e64cd4b 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -761,6 +761,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( amount: payment_attempt.get_total_amount(), success_based_routing_connector: first_success_based_connector.to_string(), payment_connector: payment_connector.to_string(), + payment_method_type: payment_attempt.payment_method_type, currency: payment_attempt.currency, payment_method: payment_attempt.payment_method, capture_method: payment_attempt.capture_method, diff --git a/crates/router/src/core/webhooks/incoming_v2.rs b/crates/router/src/core/webhooks/incoming_v2.rs index 139ae18c1717..569cd330a079 100644 --- a/crates/router/src/core/webhooks/incoming_v2.rs +++ b/crates/router/src/core/webhooks/incoming_v2.rs @@ -20,7 +20,10 @@ use crate::{ api_locking, errors::{self, ConnectorErrorExt, CustomResult, RouterResponse, StorageErrorExt}, metrics, - payments::{self, transformers::ToResponse}, + payments::{ + self, + transformers::{GenerateResponse, ToResponse}, + }, webhooks::utils::construct_webhook_router_data, }, db::StorageInterface, @@ -433,12 +436,8 @@ async fn payments_incoming_webhook_flow( )) .await?; - let response = api_models::payments::PaymentsRetrieveResponse::generate_response( - payment_data, - customer, - &state.base_url, - payments::operations::PaymentGet, - &state.conf.connector_request_reference_id_config, + let response = payment_data.generate_response( + &state, connector_http_status_code, external_latency, None, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 28957ca736f5..410a668c8bfc 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -575,6 +575,9 @@ impl Payments { .service( web::resource("/finish-redirection/{publishable_key}/{profile_id}") .route(web::get().to(payments::payments_finish_redirection)), + ) + .service( + web::resource("/capture").route(web::post().to(payments::payments_capture)), ), ); diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 7d1a0f52b3d3..46f6a5e492ae 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -2472,3 +2472,75 @@ pub async fn payments_finish_redirection( ) .await } + +#[cfg(feature = "v2")] +#[instrument(skip(state, req), fields(flow, payment_id))] +pub async fn payments_capture( + state: web::Data, + req: actix_web::HttpRequest, + payload: web::Json, + path: web::Path, +) -> impl Responder { + use hyperswitch_domain_models::payments::PaymentCaptureData; + let flow = Flow::PaymentsCapture; + + let global_payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", global_payment_id.get_string_repr()); + + let internal_payload = internal_payload_types::PaymentsGenericRequestWithResourceId { + global_payment_id, + payload: payload.into_inner(), + }; + + let header_payload = match HeaderPayload::foreign_try_from(req.headers()) { + Ok(headers) => headers, + Err(err) => { + return api::log_and_return_error_response(err); + } + }; + + let locking_action = internal_payload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow, + state, + &req, + internal_payload, + |state, auth: auth::AuthenticationData, req, req_state| async { + let payment_id = req.global_payment_id; + let request = req.payload; + + let operation = payments::operations::payment_capture_v2::PaymentsCapture; + + Box::pin(payments::payments_core::< + api_types::Capture, + api_models::payments::PaymentsCaptureResponse, + _, + _, + _, + PaymentCaptureData, + >( + state, + req_state, + auth.merchant_account, + auth.profile, + auth.key_store, + operation, + request, + payment_id, + payments::CallConnectorAction::Trigger, + header_payload.clone(), + )) + .await + }, + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuth { + permission: Permission::ProfileAccountWrite, + }, + req.headers(), + ), + locking_action, + )) + .await +} diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index de4c063c41aa..5ee7094700c8 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -470,7 +470,7 @@ impl Capturable for PaymentsSyncData { payment_data .payment_attempt .amount_details - .amount_to_capture + .get_amount_to_capture() .or_else(|| Some(payment_data.payment_attempt.get_total_amount())) .map(|amt| amt.get_amount_as_i64()) } diff --git a/cypress-tests/cypress.config.js b/cypress-tests/cypress.config.js index 56f6887761a7..a501f7b3e808 100644 --- a/cypress-tests/cypress.config.js +++ b/cypress-tests/cypress.config.js @@ -1,5 +1,5 @@ import { defineConfig } from "cypress"; -import "cypress-mochawesome-reporter/plugin.js"; +import mochawesome from "cypress-mochawesome-reporter/plugin.js"; let globalState; @@ -10,7 +10,9 @@ const reportName = process.env.REPORT_NAME || `${connectorId}_report`; export default defineConfig({ e2e: { - setupNodeEvents(on) { + setupNodeEvents(on, config) { + mochawesome(on); + on("task", { setGlobalState: (val) => { return (globalState = val || {}); @@ -26,6 +28,8 @@ export default defineConfig({ return null; }, }); + + return config; }, experimentalRunAllSpecs: true, @@ -43,6 +47,5 @@ export default defineConfig({ chromeWebSecurity: false, defaultCommandTimeout: 10000, pageLoadTimeout: 20000, - screenshotsFolder: screenshotsFolderName, }); diff --git a/cypress-tests/package-lock.json b/cypress-tests/package-lock.json index a2c2c4b8fb24..9fb61a5da8df 100644 --- a/cypress-tests/package-lock.json +++ b/cypress-tests/package-lock.json @@ -9,16 +9,16 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@eslint/js": "^9.16.0", - "cypress": "^13.16.0", + "@eslint/js": "^9.17.0", + "cypress": "^13.17.0", "cypress-mochawesome-reporter": "^3.8.2", - "eslint": "^9.16.0", + "eslint": "^9.17.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-cypress": "^4.1.0", "eslint-plugin-prettier": "^5.2.1", - "globals": "^15.13.0", + "globals": "^15.14.0", "jsqr": "^1.4.0", - "prettier": "^3.4.1" + "prettier": "^3.4.2" } }, "node_modules/@colors/colors": { @@ -188,9 +188,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", - "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", + "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", "dev": true, "license": "MIT", "engines": { @@ -1019,9 +1019,9 @@ } }, "node_modules/cypress": { - "version": "13.16.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.16.0.tgz", - "integrity": "sha512-g6XcwqnvzXrqiBQR/5gN+QsyRmKRhls1y5E42fyOvsmU7JuY+wM6uHJWj4ZPttjabzbnRvxcik2WemR8+xT6FA==", + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz", + "integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1331,9 +1331,9 @@ } }, "node_modules/eslint": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", - "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", + "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", "dev": true, "license": "MIT", "dependencies": { @@ -1342,7 +1342,7 @@ "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.9.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.16.0", + "@eslint/js": "9.17.0", "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -1351,7 +1351,7 @@ "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.5", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", @@ -1987,9 +1987,9 @@ } }, "node_modules/globals": { - "version": "15.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.13.0.tgz", - "integrity": "sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==", + "version": "15.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", + "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", "dev": true, "license": "MIT", "engines": { @@ -3388,9 +3388,9 @@ } }, "node_modules/prettier": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz", - "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "license": "MIT", "bin": { diff --git a/cypress-tests/package.json b/cypress-tests/package.json index ff168f61effb..7032a72fb084 100644 --- a/cypress-tests/package.json +++ b/cypress-tests/package.json @@ -19,15 +19,15 @@ "author": "Hyperswitch", "license": "ISC", "devDependencies": { - "@eslint/js": "^9.16.0", - "cypress": "^13.16.0", + "@eslint/js": "^9.17.0", + "cypress": "^13.17.0", "cypress-mochawesome-reporter": "^3.8.2", - "eslint": "^9.16.0", + "eslint": "^9.17.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-cypress": "^4.1.0", "eslint-plugin-prettier": "^5.2.1", - "globals": "^15.13.0", + "globals": "^15.14.0", "jsqr": "^1.4.0", - "prettier": "^3.4.1" + "prettier": "^3.4.2" } } diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 09b52bc357d2..f33ab82aa0dd 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -327,19 +327,31 @@ discord_invite_url = "https://discord.gg/wJZ7DVW8mm" payout_eligibility = true [mandates.supported_payment_methods] -pay_later.klarna = {connector_list = "adyen"} -wallet.google_pay = {connector_list = "stripe,adyen,bankofamerica"} -wallet.apple_pay = {connector_list = "stripe,adyen,bankofamerica"} -wallet.paypal = {connector_list = "adyen"} -card.credit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree"} -card.debit = {connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree"} -bank_debit.ach = { connector_list = "gocardless,adyen" } -bank_debit.becs = { connector_list = "gocardless" } -bank_debit.bacs = { connector_list = "adyen" } -bank_debit.sepa = { connector_list = "gocardless,adyen" } -bank_redirect.ideal = {connector_list = "stripe,adyen,globalpay"} -bank_redirect.sofort = {connector_list = "stripe,adyen,globalpay"} -bank_redirect.giropay = {connector_list = "adyen,globalpay"} +bank_debit.ach = { connector_list = "gocardless,adyen,stripe" } +bank_debit.becs = { connector_list = "gocardless,stripe,adyen" } +bank_debit.bacs = { connector_list = "stripe,gocardless" } +bank_debit.sepa = { connector_list = "gocardless,adyen,stripe,deutschebank" } +card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" +pay_later.klarna.connector_list = "adyen" +wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet" +wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica,noon,globalpay,multisafepay,novalnet" +wallet.paypal.connector_list = "adyen,globalpay,nexinets,novalnet,paypal" +wallet.momo.connector_list = "adyen" +wallet.kakao_pay.connector_list = "adyen" +wallet.go_pay.connector_list = "adyen" +wallet.gcash.connector_list = "adyen" +wallet.dana.connector_list = "adyen" +wallet.twint.connector_list = "adyen" +wallet.vipps.connector_list = "adyen" + +bank_redirect.ideal.connector_list = "stripe,adyen,globalpay,multisafepay,nexinets" +bank_redirect.sofort.connector_list = "stripe,adyen,globalpay" +bank_redirect.giropay.connector_list = "adyen,globalpay,multisafepay,nexinets" +bank_redirect.bancontact_card.connector_list="adyen,stripe" +bank_redirect.trustly.connector_list="adyen" +bank_redirect.open_banking_uk.connector_list="adyen" +bank_redirect.eps.connector_list="globalpay,nexinets" [cors] diff --git a/migrations/2024-12-16-111228_add_new_col_payment_method_type_in_dynamic_routing_stats/down.sql b/migrations/2024-12-16-111228_add_new_col_payment_method_type_in_dynamic_routing_stats/down.sql new file mode 100644 index 000000000000..bc2f40c91d21 --- /dev/null +++ b/migrations/2024-12-16-111228_add_new_col_payment_method_type_in_dynamic_routing_stats/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE dynamic_routing_stats +DROP COLUMN IF EXISTS payment_method_type; diff --git a/migrations/2024-12-16-111228_add_new_col_payment_method_type_in_dynamic_routing_stats/up.sql b/migrations/2024-12-16-111228_add_new_col_payment_method_type_in_dynamic_routing_stats/up.sql new file mode 100644 index 000000000000..2b5af8090f0d --- /dev/null +++ b/migrations/2024-12-16-111228_add_new_col_payment_method_type_in_dynamic_routing_stats/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE dynamic_routing_stats +ADD COLUMN IF NOT EXISTS payment_method_type VARCHAR(64);