diff --git a/.github/workflows/cypress-tests-runner.yml b/.github/workflows/cypress-tests-runner.yml index bb22a5692ade..bc83c2388f7e 100644 --- a/.github/workflows/cypress-tests-runner.yml +++ b/.github/workflows/cypress-tests-runner.yml @@ -14,6 +14,7 @@ env: CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 PAYMENTS_CONNECTORS: "cybersource stripe" + PAYOUTS_CONNECTORS: "adyenplatform wise" RUST_BACKTRACE: short RUSTUP_MAX_RETRIES: 10 RUN_TESTS: ${{ ((github.event_name == 'pull_request') && (github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name)) || (github.event_name == 'merge_group')}} @@ -127,7 +128,7 @@ jobs: CONNECTOR_AUTH_PASSPHRASE: ${{ secrets.CONNECTOR_AUTH_PASSPHRASE }} CONNECTOR_CREDS_S3_BUCKET_URI: ${{ secrets.CONNECTOR_CREDS_S3_BUCKET_URI}} DESTINATION_FILE_NAME: "creds.json.gpg" - S3_SOURCE_FILE_NAME: "6f8289a9-6da0-433b-8a24-18d4d7257b7f.json.gpg" + S3_SOURCE_FILE_NAME: "aa328308-b34e-41b7-a590-4fe45cfe7991.json.gpg" shell: bash run: | mkdir -p ".github/secrets" ".github/test" diff --git a/CHANGELOG.md b/CHANGELOG.md index cf6a551700f9..29ed75461f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,40 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.12.23.0 + +### Features + +- **connector:** [JPMORGAN] add Payment flows for cards ([#6668](https://github.com/juspay/hyperswitch/pull/6668)) ([`adcddd6`](https://github.com/juspay/hyperswitch/commit/adcddd643c002a5fe3e7c50c0f78fa5a46f210e7)) +- **payment_methods_v2:** Added Ephemeral auth for v2 ([#6813](https://github.com/juspay/hyperswitch/pull/6813)) ([`24401bc`](https://github.com/juspay/hyperswitch/commit/24401bc16f9677ce0f5fa70d739e5e6885c7e907)) +- **payments_v2:** Implement payments capture v2 ([#6722](https://github.com/juspay/hyperswitch/pull/6722)) ([`977cb70`](https://github.com/juspay/hyperswitch/commit/977cb704e7dcf35d0fa6bc0e3c6d335ad0601521)) +- **router:** + - Add /relay endpoint ([#6870](https://github.com/juspay/hyperswitch/pull/6870)) ([`22de8ad`](https://github.com/juspay/hyperswitch/commit/22de8ad132811b636fdb2594649e40b90810f564)) + - Add db interface for `/relay` ([#6879](https://github.com/juspay/hyperswitch/pull/6879)) ([`0f8b0b3`](https://github.com/juspay/hyperswitch/commit/0f8b0b3bc854be62942a77d08340510312157c67)) + +### Bug Fixes + +- **connector:** + - Paypal BankRedirects (Ideal/EPS) ([#6864](https://github.com/juspay/hyperswitch/pull/6864)) ([`dcd51a7`](https://github.com/juspay/hyperswitch/commit/dcd51a7fb8df673cc74130ee732542b55783602f)) + - [STRIPE] fix stripe mandate ([#6899](https://github.com/juspay/hyperswitch/pull/6899)) ([`9f2ce05`](https://github.com/juspay/hyperswitch/commit/9f2ce05b2591da0d757b267800f42b69fc38e3ee)) + - Update mandate PMT configs for Mandate Supported Connectors ([#6903](https://github.com/juspay/hyperswitch/pull/6903)) ([`02f0824`](https://github.com/juspay/hyperswitch/commit/02f0824d303fb9a36ee54123f52176014613a992)) +- Cypress reports generation ([#6894](https://github.com/juspay/hyperswitch/pull/6894)) ([`81b324c`](https://github.com/juspay/hyperswitch/commit/81b324caf1bef4f621de30824bfb1f05ef984362)) + +### Refactors + +- **connector:** [Airwallex] add device_data in payment request ([#6881](https://github.com/juspay/hyperswitch/pull/6881)) ([`573974b`](https://github.com/juspay/hyperswitch/commit/573974b3a5d53b279bd3959e400ac682aaacd474)) +- **customers_v2:** Include minor fixes for customer v2 flows ([#6876](https://github.com/juspay/hyperswitch/pull/6876)) ([`5cdeaf8`](https://github.com/juspay/hyperswitch/commit/5cdeaf8e6002ad087dba2a562f86b51e97516d29)) +- **dynamic_routing:** Add col payment_method_type in dynamic_routing_stats ([#6853](https://github.com/juspay/hyperswitch/pull/6853)) ([`492fd87`](https://github.com/juspay/hyperswitch/commit/492fd871a14e60e02f17fe073544bc40e79a7220)) +- **grpc:** Send `x-tenant-id` and `x-request-id` in grpc headers ([#6904](https://github.com/juspay/hyperswitch/pull/6904)) ([`dc0a92d`](https://github.com/juspay/hyperswitch/commit/dc0a92dc108c91d6c5f998af417e382aa7a0d9f1)) + +### Miscellaneous Tasks + +- **cypress:** Payout - fix test cases for adyenplatform bank ([#6887](https://github.com/juspay/hyperswitch/pull/6887)) ([`7540b74`](https://github.com/juspay/hyperswitch/commit/7540b7434766ff9dfa1aa2a56013ac89429dd1e6)) + +**Full Changelog:** [`2024.12.19.1...2024.12.23.0`](https://github.com/juspay/hyperswitch/compare/2024.12.19.1...2024.12.23.0) + +- - - + ## 2024.12.19.1 ### Features diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 937cc933b7cd..bfa27f06fc62 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -5613,6 +5613,21 @@ "type": "string", "description": "User-agent of the browser", "nullable": true + }, + "os_type": { + "type": "string", + "description": "The os type of the client device", + "nullable": true + }, + "os_version": { + "type": "string", + "description": "The os version of the client device", + "nullable": true + }, + "device_model": { + "type": "string", + "description": "The device model of the client", + "nullable": true } } }, @@ -6495,88 +6510,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", @@ -6624,6 +6557,7 @@ "helcim", "iatapay", "itaubank", + "jpmorgan", "klarna", "mifinity", "mollie", @@ -12682,6 +12616,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": [ @@ -13941,16 +13957,11 @@ "PaymentMethodIntentConfirm": { "type": "object", "required": [ - "client_secret", "payment_method_data", "payment_method_type", "payment_method_subtype" ], "properties": { - "client_secret": { - "type": "string", - "description": "For SDK based calls, client_secret would be required" - }, "customer_id": { "type": "string", "description": "The unique identifier of the customer.", @@ -14236,7 +14247,7 @@ "example": "2024-02-24T11:04:09.922Z", "nullable": true }, - "client_secret": { + "ephemeral_key": { "type": "string", "description": "For Client based calls", "nullable": true @@ -14385,14 +14396,6 @@ "properties": { "payment_method_data": { "$ref": "#/components/schemas/PaymentMethodUpdateData" - }, - "client_secret": { - "type": "string", - "description": "This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK", - "example": "secret_k2uj3he2893eiu2d", - "nullable": true, - "maxLength": 30, - "minLength": 30 } }, "additionalProperties": false @@ -14553,42 +14556,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 } } @@ -14691,7 +14664,7 @@ "$ref": "#/components/schemas/IntentStatus" }, "amount": { - "$ref": "#/components/schemas/ConfirmIntentAmountDetailsResponse" + "$ref": "#/components/schemas/PaymentAmountDetailsResponse" }, "customer_id": { "type": "string", @@ -16440,7 +16413,7 @@ "$ref": "#/components/schemas/IntentStatus" }, "amount": { - "$ref": "#/components/schemas/ConfirmIntentAmountDetailsResponse" + "$ref": "#/components/schemas/PaymentAmountDetailsResponse" }, "customer_id": { "type": "string", @@ -19537,6 +19510,7 @@ "helcim", "iatapay", "itaubank", + "jpmorgan", "klarna", "mifinity", "mollie", 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 8e212e26b6ec..3c80fd8c4634 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": [ @@ -8208,6 +8325,21 @@ "type": "string", "description": "User-agent of the browser", "nullable": true + }, + "os_type": { + "type": "string", + "description": "The os type of the client device", + "nullable": true + }, + "os_version": { + "type": "string", + "description": "The os version of the client device", + "nullable": true + }, + "device_model": { + "type": "string", + "description": "The device model of the client", + "nullable": true } } }, @@ -9144,6 +9276,7 @@ "helcim", "iatapay", "itaubank", + "jpmorgan", "klarna", "mifinity", "mollie", @@ -17341,7 +17474,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": { @@ -23179,6 +23312,170 @@ }, "additionalProperties": false }, + "RelayData": { + "oneOf": [ + { + "type": "object", + "required": [ + "refund" + ], + "properties": { + "refund": { + "$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 reference to which the relay request is being made", + "example": "7256228702616471803954" + }, + "connector_id": { + "type": "string", + "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", + "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_resource_id": { + "type": "string", + "description": "The identifier that is associated to a resource at the connector reference to which the relay request is being made", + "example": "pi_3MKEivSFNglxLpam0ZaL98q9" + }, + "error": { + "allOf": [ + { + "$ref": "#/components/schemas/RelayError" + } + ], + "nullable": true + }, + "connector_reference_id": { + "type": "string", + "description": "The identifier that is associated to a resource at the connector to which the relay request is being made", + "example": "re_3QY4TnEOqOywnAIx1Mm1p7GQ", + "nullable": true + }, + "connector_id": { + "type": "string", + "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", + "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": [ + "created", + "pending", + "success", + "failure" + ] + }, + "RelayType": { + "type": "string", + "enum": [ + "refund" + ] + }, "RequestPaymentMethodTypes": { "type": "object", "required": [ @@ -23632,6 +23929,7 @@ "helcim", "iatapay", "itaubank", + "jpmorgan", "klarna", "mifinity", "mollie", diff --git a/config/config.example.toml b/config/config.example.toml index 57671000b498..999266321144 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -223,6 +223,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url= "https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index cb25c7850df0..640f0d73bbb3 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -65,6 +65,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" @@ -159,19 +160,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 @@ -290,6 +303,10 @@ debit.currency = "USD" ali_pay.currency = "GBP,CNY" we_chat_pay.currency = "GBP,CNY" +[pm.filters.jpmorgan] +debit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } +credit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } + [pm_filters.klarna] klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } diff --git a/config/deployments/production.toml b/config/deployments/production.toml index f51697ac5d6e..79bedf358bec 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -69,6 +69,7 @@ iatapay.base_url = "https://iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://secure.api.itau/" jpmorgan.base_url = "https://api-ms.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.klarna.com/" mifinity.base_url = "https://secure.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" @@ -159,19 +160,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 @@ -324,6 +337,10 @@ debit.currency = "USD" ali_pay.currency = "GBP,CNY" we_chat_pay.currency = "GBP,CNY" +[pm.filters.jpmorgan] +debit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } +credit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } + [pm_filters.klarna] klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 2f2d427287a1..c582cf3314db 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -69,6 +69,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" @@ -159,19 +160,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 @@ -325,6 +338,10 @@ debit.currency = "USD" ali_pay.currency = "GBP,CNY" we_chat_pay.currency = "GBP,CNY" +[pm.filters.jpmorgan] +debit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } +credit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } + [pm_filters.klarna] klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "CHF,DKK,EUR,GBP,NOK,PLN,SEK,USD,AUD,NZD,CAD" } diff --git a/config/development.toml b/config/development.toml index 2d20f76b017b..037fc7738705 100644 --- a/config/development.toml +++ b/config/development.toml @@ -241,6 +241,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url= "https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" @@ -495,6 +496,10 @@ paypal = { currency = "AUD,BRL,CAD,CNY,CZK,DKK,EUR,HKD,HUF,ILS,JPY,MYR,MXN,TWD,N credit = { currency = "USD" } debit = { currency = "USD" } +[pm.filters.jpmorgan] +debit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } +credit = { country = "CA, EU, UK, US", currency = "CAD, EUR, GBP, USD" } + [pm_filters.klarna] klarna = { country = "AU,AT,BE,CA,CZ,DK,FI,FR,DE,GR,IE,IT,NL,NZ,NO,PL,PT,ES,SE,CH,GB,US", currency = "AUD,EUR,EUR,CAD,CZK,DKK,EUR,EUR,EUR,EUR,EUR,EUR,EUR,NZD,NOK,PLN,EUR,EUR,SEK,CHF,GBP,USD" } @@ -618,19 +623,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/config/docker_compose.toml b/config/docker_compose.toml index 3ca0bcb89fe1..58f3c71694a9 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -154,6 +154,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" diff --git a/crates/api_models/src/connector_enums.rs b/crates/api_models/src/connector_enums.rs index 0989614c8a04..3a3fe5e8f429 100644 --- a/crates/api_models/src/connector_enums.rs +++ b/crates/api_models/src/connector_enums.rs @@ -88,7 +88,7 @@ pub enum Connector { // Inespay, Iatapay, Itaubank, - //Jpmorgan, + Jpmorgan, Klarna, Mifinity, Mollie, @@ -175,6 +175,7 @@ impl Connector { (Self::Airwallex, _) | (Self::Deutschebank, _) | (Self::Globalpay, _) + | (Self::Jpmorgan, _) | (Self::Paypal, _) | (Self::Payu, _) | (Self::Trustpay, PaymentMethod::BankRedirect) @@ -234,7 +235,7 @@ impl Connector { | Self::Iatapay // | Self::Inespay | Self::Itaubank - //| Self::Jpmorgan + | Self::Jpmorgan | Self::Klarna | Self::Mifinity | Self::Mollie diff --git a/crates/api_models/src/ephemeral_key.rs b/crates/api_models/src/ephemeral_key.rs index d06490d6bac2..fa61642e353a 100644 --- a/crates/api_models/src/ephemeral_key.rs +++ b/crates/api_models/src/ephemeral_key.rs @@ -1,7 +1,10 @@ use common_utils::id_type; +#[cfg(feature = "v2")] +use masking::Secret; use serde; use utoipa::ToSchema; +#[cfg(feature = "v1")] /// Information required to create an ephemeral key. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] pub struct EphemeralKeyCreateRequest { @@ -15,12 +18,52 @@ pub struct EphemeralKeyCreateRequest { pub customer_id: id_type::CustomerId, } +#[cfg(feature = "v2")] +/// Information required to create an ephemeral key. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct EphemeralKeyCreateRequest { + /// Customer ID for which an ephemeral key must be created + #[schema( + min_length = 32, + max_length = 64, + value_type = String, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8" + )] + pub customer_id: id_type::GlobalCustomerId, +} + +#[cfg(feature = "v2")] +/// ephemeral_key for the customer_id mentioned +#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq, ToSchema)] +pub struct EphemeralKeyResponse { + /// Ephemeral key id + #[schema(value_type = String, max_length = 32, min_length = 1)] + pub id: id_type::EphemeralKeyId, + /// customer_id to which this ephemeral key belongs to + #[schema(value_type = String, max_length = 64, min_length = 32, example = "12345_cus_01926c58bc6e77c09e809964e72af8c8")] + pub customer_id: id_type::GlobalCustomerId, + /// time at which this ephemeral key was created + pub created_at: time::PrimitiveDateTime, + /// time at which this ephemeral key would expire + pub expires: time::PrimitiveDateTime, + #[schema(value_type=String)] + /// ephemeral key + pub secret: Secret, +} + impl common_utils::events::ApiEventMetric for EphemeralKeyCreateRequest { fn get_api_event_type(&self) -> Option { Some(common_utils::events::ApiEventsType::Miscellaneous) } } +#[cfg(feature = "v2")] +impl common_utils::events::ApiEventMetric for EphemeralKeyResponse { + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::Miscellaneous) + } +} + /// ephemeral_key for the customer_id mentioned #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq, ToSchema)] pub struct EphemeralKeyCreateResponse { 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 1cbade5554c4..60d1d11f27db 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -32,6 +32,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/payment_methods.rs b/crates/api_models/src/payment_methods.rs index c821feaff575..fe5cb2933f9e 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -163,9 +163,6 @@ pub struct PaymentMethodIntentCreate { #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] pub struct PaymentMethodIntentConfirm { - /// For SDK based calls, client_secret would be required - pub client_secret: String, - /// The unique identifier of the customer. #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] pub customer_id: Option, @@ -211,9 +208,6 @@ pub struct PaymentMethodIntentConfirmInternal { #[schema(value_type = PaymentMethodType,example = "credit")] pub payment_method_subtype: api_enums::PaymentMethodType, - /// For SDK based calls, client_secret would be required - pub client_secret: String, - /// The unique identifier of the customer. #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] pub customer_id: Option, @@ -226,7 +220,6 @@ pub struct PaymentMethodIntentConfirmInternal { impl From for PaymentMethodIntentConfirm { fn from(item: PaymentMethodIntentConfirmInternal) -> Self { Self { - client_secret: item.client_secret, payment_method_type: item.payment_method_type, payment_method_subtype: item.payment_method_subtype, customer_id: item.customer_id, @@ -408,10 +401,6 @@ pub struct PaymentMethodUpdate { pub struct PaymentMethodUpdate { /// payment method data to be passed pub payment_method_data: PaymentMethodUpdateData, - - /// This is a 15 minute expiry token which shall be used from the client to authenticate and perform sessions from the SDK - #[schema(max_length = 30, min_length = 30, example = "secret_k2uj3he2893eiu2d")] - pub client_secret: Option, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -826,7 +815,8 @@ pub struct PaymentMethodResponse { pub last_used_at: Option, /// For Client based calls - pub client_secret: Option, + #[schema(value_type=Option)] + pub ephemeral_key: Option>, pub payment_method_data: Option, } diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 0ae906f3dc0f..ed537c3efeea 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")] @@ -1282,6 +1282,15 @@ pub struct BrowserInformation { /// User-agent of the browser pub user_agent: Option, + + /// The os type of the client device + pub os_type: Option, + + /// The os version of the client device + pub os_version: Option, + + /// The device model of the client + pub device_model: Option, } impl RequestSurchargeDetails { @@ -4077,6 +4086,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 +4095,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 +4109,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 +4833,7 @@ 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( @@ -4879,7 +4911,7 @@ 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( diff --git a/crates/api_models/src/relay.rs b/crates/api_models/src/relay.rs new file mode 100644 index 000000000000..e4bc607fc0da --- /dev/null +++ b/crates/api_models/src/relay.rs @@ -0,0 +1,92 @@ +use common_utils::types::MinorUnit; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::enums as api_enums; + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayRequest { + /// The identifier that is associated to a resource at the connector reference to which the relay request is being made + #[schema(example = "7256228702616471803954")] + pub connector_resource_id: String, + /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment + #[schema(example = "mca_5apGeP94tMts6rg3U3kR", value_type = String)] + pub connector_id: common_utils::id_type::MerchantConnectorAccountId, + /// The type of relay request + #[serde(rename = "type")] + #[schema(value_type = RelayType)] + pub relay_type: api_enums::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 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: api_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 + #[schema(value_type = RelayStatus)] + pub status: api_enums::RelayStatus, + /// The identifier that is associated to a resource at the connector reference to which the relay request is being made + #[schema(example = "pi_3MKEivSFNglxLpam0ZaL98q9")] + pub connector_resource_id: String, + /// 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 = "re_3QY4TnEOqOywnAIx1Mm1p7GQ")] + pub connector_reference_id: Option, + /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment + #[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")] + #[schema(value_type = RelayType)] + pub relay_type: api_enums::RelayType, + /// The data that is associated with the relay request + pub data: Option, +} + +#[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, +} + +impl common_utils::events::ApiEventMetric for RelayRequest {} + +impl common_utils::events::ApiEventMetric for RelayResponse {} diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index 1776ade36661..127d5a9901c6 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -83,7 +83,7 @@ pub enum RoutableConnectors { Iatapay, // Inespay, Itaubank, - //Jpmorgan, + Jpmorgan, Klarna, Mifinity, Mollie, diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index fcd757d31aa1..0833ab37e3f8 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1728,6 +1728,53 @@ pub enum RefundStatus { TransactionFailure, } +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + Hash, + PartialEq, + strum::Display, + strum::EnumString, + strum::EnumIter, + serde::Serialize, + serde::Deserialize, + ToSchema, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum RelayStatus { + Created, + #[default] + Pending, + Success, + Failure, +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + PartialEq, + strum::Display, + strum::EnumString, + strum::EnumIter, + serde::Serialize, + serde::Deserialize, + ToSchema, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum RelayType { + Refund, +} + #[derive( Clone, Copy, @@ -3372,6 +3419,26 @@ impl From for TransactionType { } } +impl From for RelayStatus { + fn from(refund_status: RefundStatus) -> Self { + match refund_status { + RefundStatus::Failure | RefundStatus::TransactionFailure => Self::Failure, + RefundStatus::ManualReview | RefundStatus::Pending => Self::Pending, + RefundStatus::Success => Self::Success, + } + } +} + +impl From for RefundStatus { + fn from(relay_status: RelayStatus) -> Self { + match relay_status { + RelayStatus::Failure => Self::Failure, + RelayStatus::Pending | RelayStatus::Created => Self::Pending, + RelayStatus::Success => Self::Success, + } + } +} + #[derive( Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize, Default, ToSchema, )] diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 39e5483700a5..3b437b703bef 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -146,3 +146,6 @@ pub const CONNECTOR_TRANSACTION_ID_HASH_BYTES: usize = 25; /// Apple Pay validation url pub const APPLEPAY_VALIDATION_URL: &str = "https://apple-pay-gateway-cert.apple.com/paymentservices/startSession"; + +/// Request ID +pub const X_REQUEST_ID: &str = "x-request-id"; diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index b9af92786f58..9494b8209e75 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -102,6 +102,9 @@ pub enum ApiEventsType { poll_id: String, }, Analytics, + EphemeralKey { + key_id: id_type::EphemeralKeyId, + }, } impl ApiEventMetric for serde_json::Value {} diff --git a/crates/common_utils/src/id_type.rs b/crates/common_utils/src/id_type.rs index 594078bb4057..ea90c00121e8 100644 --- a/crates/common_utils/src/id_type.rs +++ b/crates/common_utils/src/id_type.rs @@ -3,6 +3,7 @@ mod api_key; mod customer; +mod ephemeral_key; #[cfg(feature = "v2")] mod global_id; mod merchant; @@ -11,6 +12,7 @@ mod organization; mod payment; mod profile; mod refunds; +mod relay; mod routing; mod tenant; @@ -37,12 +39,14 @@ pub use self::global_id::{ pub use self::{ api_key::ApiKeyId, customer::CustomerId, + ephemeral_key::EphemeralKeyId, merchant::MerchantId, merchant_connector_account::MerchantConnectorAccountId, organization::OrganizationId, payment::{PaymentId, PaymentReferenceId}, profile::ProfileId, refunds::RefundReferenceId, + relay::RelayId, routing::RoutingId, tenant::TenantId, }; diff --git a/crates/common_utils/src/id_type/ephemeral_key.rs b/crates/common_utils/src/id_type/ephemeral_key.rs new file mode 100644 index 000000000000..071980fc6a46 --- /dev/null +++ b/crates/common_utils/src/id_type/ephemeral_key.rs @@ -0,0 +1,31 @@ +crate::id_type!( + EphemeralKeyId, + "A type for key_id that can be used for Ephemeral key IDs" +); +crate::impl_id_type_methods!(EphemeralKeyId, "key_id"); + +// This is to display the `EphemeralKeyId` as EphemeralKeyId(abcd) +crate::impl_debug_id_type!(EphemeralKeyId); +crate::impl_try_from_cow_str_id_type!(EphemeralKeyId, "key_id"); + +crate::impl_generate_id_id_type!(EphemeralKeyId, "eki"); +crate::impl_serializable_secret_id_type!(EphemeralKeyId); +crate::impl_queryable_id_type!(EphemeralKeyId); +crate::impl_to_sql_from_sql_id_type!(EphemeralKeyId); + +impl crate::events::ApiEventMetric for EphemeralKeyId { + fn get_api_event_type(&self) -> Option { + Some(crate::events::ApiEventsType::EphemeralKey { + key_id: self.clone(), + }) + } +} + +crate::impl_default_id_type!(EphemeralKeyId, "key"); + +impl EphemeralKeyId { + /// Generate a key for redis + pub fn generate_redis_key(&self) -> String { + format!("epkey_{}", self.get_string_repr()) + } +} 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..3ad64729fb73 --- /dev/null +++ b/crates/common_utils/src/id_type/relay.rs @@ -0,0 +1,13 @@ +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_try_from_cow_str_id_type!(RelayId, "relay_id"); +crate::impl_generate_id_id_type!(RelayId, "relay"); +crate::impl_serializable_secret_id_type!(RelayId); +crate::impl_queryable_id_type!(RelayId); +crate::impl_to_sql_from_sql_id_type!(RelayId); + +crate::impl_debug_id_type!(RelayId); diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index 0d39841fe09b..9e71ca76733f 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -1399,6 +1399,15 @@ pub struct BrowserInformation { /// User-agent of the browser pub user_agent: Option, + + /// The os type of the client device + pub os_type: Option, + + /// The os version of the client device + pub os_version: Option, + + /// The device model of the client + pub device_model: Option, } #[cfg(feature = "v2")] diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index 1af99f110208..985a6d7b547f 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -198,6 +198,7 @@ pub struct ConnectorConfig { pub gpayments: Option, pub helcim: Option, // pub inespay: Option, + pub jpmorgan: Option, pub klarna: Option, pub mifinity: Option, pub mollie: Option, @@ -370,6 +371,7 @@ impl ConnectorConfig { Connector::Gpayments => Ok(connector_data.gpayments), Connector::Helcim => Ok(connector_data.helcim), // Connector::Inespay => Ok(connector_data.inespay), + Connector::Jpmorgan => Ok(connector_data.jpmorgan), Connector::Klarna => Ok(connector_data.klarna), Connector::Mifinity => Ok(connector_data.mifinity), Connector::Mollie => Ok(connector_data.mollie), diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index fcf0dca100cb..f49f6b311f6e 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -1731,6 +1731,47 @@ api_key="Client Secret" api_secret="Certificates" key2="Certificate Key" +[jpmorgan] +[[jpmorgan.credit]] + payment_method_type = "American Express" +[[jpmorgan.credit]] + payment_method_type = "ChaseNet" +[[jpmorgan.credit]] + payment_method_type = "Diners Club" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "JCB" +[[jpmorgan.credit]] + payment_method_type = "Mastercard" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "UnionPay" +[[jpmorgan.credit]] + payment_method_type = "Visa" + [[jpmorgan.debit]] + payment_method_type = "American Express" +[[jpmorgan.debit]] + payment_method_type = "ChaseNet" +[[jpmorgan.debit]] + payment_method_type = "Diners Club" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "JCB" +[[jpmorgan.debit]] + payment_method_type = "Mastercard" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "UnionPay" +[[jpmorgan.debit]] + payment_method_type = "Visa" +[jpmorgan.connector_auth.BodyKey] +api_key="Client ID" +key1="Client Secret" + [klarna] [[klarna.pay_later]] payment_method_type = "klarna" diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 443d3fd72ac7..7216d4f7890b 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -1443,6 +1443,46 @@ api_key="Client Secret" api_secret="Certificates" key2="Certificate Key" +[jpmorgan] +[[jpmorgan.credit]] + payment_method_type = "American Express" +[[jpmorgan.credit]] + payment_method_type = "ChaseNet" +[[jpmorgan.credit]] + payment_method_type = "Diners Club" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "JCB" +[[jpmorgan.credit]] + payment_method_type = "Mastercard" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "UnionPay" +[[jpmorgan.credit]] + payment_method_type = "Visa" + [[jpmorgan.debit]] + payment_method_type = "American Express" +[[jpmorgan.debit]] + payment_method_type = "ChaseNet" +[[jpmorgan.debit]] + payment_method_type = "Diners Club" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "JCB" +[[jpmorgan.debit]] + payment_method_type = "Mastercard" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "UnionPay" +[[jpmorgan.debit]] + payment_method_type = "Visa" +[jpmorgan.connector_auth.BodyKey] +api_key="Access Token" + [klarna] [[klarna.pay_later]] payment_method_type = "klarna" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 4d3f6ede1403..4c6ee7566f31 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -1681,6 +1681,46 @@ api_key="Client Secret" api_secret="Certificates" key2="Certificate Key" +[jpmorgan] +[[jpmorgan.credit]] + payment_method_type = "American Express" +[[jpmorgan.credit]] + payment_method_type = "ChaseNet" +[[jpmorgan.credit]] + payment_method_type = "Diners Club" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "JCB" +[[jpmorgan.credit]] + payment_method_type = "Mastercard" +[[jpmorgan.credit]] + payment_method_type = "Discover" +[[jpmorgan.credit]] + payment_method_type = "UnionPay" +[[jpmorgan.credit]] + payment_method_type = "Visa" + [[jpmorgan.debit]] + payment_method_type = "American Express" +[[jpmorgan.debit]] + payment_method_type = "ChaseNet" +[[jpmorgan.debit]] + payment_method_type = "Diners Club" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "JCB" +[[jpmorgan.debit]] + payment_method_type = "Mastercard" +[[jpmorgan.debit]] + payment_method_type = "Discover" +[[jpmorgan.debit]] + payment_method_type = "UnionPay" +[[jpmorgan.debit]] + payment_method_type = "Visa" +[jpmorgan.connector_auth.BodyKey] +api_key="Access Token" + [klarna] [[klarna.pay_later]] payment_method_type = "klarna" diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 19f3bf0a3823..ec6e91a2ecb0 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -17,7 +17,8 @@ pub mod diesel_exports { DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, DbPaymentSource as PaymentSource, DbPaymentType as PaymentType, DbPayoutStatus as PayoutStatus, DbPayoutType as PayoutType, DbProcessTrackerStatus as ProcessTrackerStatus, DbReconStatus as ReconStatus, - DbRefundStatus as RefundStatus, DbRefundType as RefundType, + DbRefundStatus as RefundStatus, DbRefundType as RefundType, DbRelayStatus as RelayStatus, + DbRelayType as RelayType, DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, DbRoleScope as RoleScope, DbRoutingAlgorithmKind as RoutingAlgorithmKind, DbScaExemptionType as ScaExemptionType, diff --git a/crates/diesel_models/src/ephemeral_key.rs b/crates/diesel_models/src/ephemeral_key.rs index d398ecdf784a..c7fc103ed09b 100644 --- a/crates/diesel_models/src/ephemeral_key.rs +++ b/crates/diesel_models/src/ephemeral_key.rs @@ -1,3 +1,33 @@ +#[cfg(feature = "v2")] +use masking::{PeekInterface, Secret}; +#[cfg(feature = "v2")] +pub struct EphemeralKeyTypeNew { + pub id: common_utils::id_type::EphemeralKeyId, + pub merchant_id: common_utils::id_type::MerchantId, + pub customer_id: common_utils::id_type::GlobalCustomerId, + pub secret: Secret, + pub resource_type: ResourceType, +} + +#[cfg(feature = "v2")] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct EphemeralKeyType { + pub id: common_utils::id_type::EphemeralKeyId, + pub merchant_id: common_utils::id_type::MerchantId, + pub customer_id: common_utils::id_type::GlobalCustomerId, + pub resource_type: ResourceType, + pub created_at: time::PrimitiveDateTime, + pub expires: time::PrimitiveDateTime, + pub secret: Secret, +} + +#[cfg(feature = "v2")] +impl EphemeralKeyType { + pub fn generate_secret_key(&self) -> String { + format!("epkey_{}", self.secret.peek()) + } +} + pub struct EphemeralKeyNew { pub id: String, pub merchant_id: common_utils::id_type::MerchantId, @@ -20,3 +50,21 @@ impl common_utils::events::ApiEventMetric for EphemeralKey { Some(common_utils::events::ApiEventsType::Miscellaneous) } } + +#[derive( + Clone, + Copy, + Debug, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, + PartialEq, + Eq, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum ResourceType { + Payment, + PaymentMethod, +} diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index cc3dc1361545..1369368a8099 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -39,6 +39,7 @@ pub mod payouts; pub mod process_tracker; pub mod query; pub mod refund; +pub mod relay; pub mod reverse_lookup; pub mod role; pub mod routing_algorithm; 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 57549ed201f2..8b834ee5d827 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -369,6 +369,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 { @@ -514,8 +531,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/payment_method.rs b/crates/diesel_models/src/payment_method.rs index 8ae34a6f89cf..76613984363d 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -290,6 +290,7 @@ pub enum PaymentMethodUpdate { network_token_requestor_reference_id: Option, network_token_locker_id: Option, network_token_payment_method_data: Option, + locker_fingerprint_id: Option, }, ConnectorMandateDetailsUpdate { connector_mandate_details: Option, @@ -324,6 +325,7 @@ pub struct PaymentMethodUpdateInternal { network_token_requestor_reference_id: Option, network_token_locker_id: Option, network_token_payment_method_data: Option, + locker_fingerprint_id: Option, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -343,6 +345,7 @@ impl PaymentMethodUpdateInternal { network_token_requestor_reference_id, network_token_locker_id, network_token_payment_method_data, + locker_fingerprint_id, } = self; PaymentMethod { @@ -361,7 +364,7 @@ impl PaymentMethodUpdateInternal { client_secret: source.client_secret, payment_method_billing_address: source.payment_method_billing_address, updated_by: updated_by.or(source.updated_by), - locker_fingerprint_id: source.locker_fingerprint_id, + locker_fingerprint_id: locker_fingerprint_id.or(source.locker_fingerprint_id), payment_method_type_v2: payment_method_type_v2.or(source.payment_method_type_v2), payment_method_subtype: payment_method_subtype.or(source.payment_method_subtype), id: source.id, @@ -710,6 +713,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, PaymentMethodUpdate::LastUsedUpdate { last_used_at } => Self { payment_method_data: None, @@ -725,6 +729,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, PaymentMethodUpdate::UpdatePaymentMethodDataAndLastUsed { payment_method_data, @@ -744,6 +749,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, PaymentMethodUpdate::NetworkTransactionIdAndStatusUpdate { network_transaction_id, @@ -762,6 +768,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, PaymentMethodUpdate::StatusUpdate { status } => Self { payment_method_data: None, @@ -777,6 +784,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, PaymentMethodUpdate::AdditionalDataUpdate { payment_method_data, @@ -787,6 +795,7 @@ impl From for PaymentMethodUpdateInternal { network_token_requestor_reference_id, network_token_locker_id, network_token_payment_method_data, + locker_fingerprint_id, } => Self { payment_method_data, last_used_at: None, @@ -801,6 +810,7 @@ impl From for PaymentMethodUpdateInternal { network_token_requestor_reference_id, network_token_locker_id, network_token_payment_method_data, + locker_fingerprint_id, }, PaymentMethodUpdate::ConnectorMandateDetailsUpdate { connector_mandate_details, @@ -818,6 +828,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id: None, network_token_requestor_reference_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: None, }, } } diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index ab044b5c6e69..8eb0a44f5dd7 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -34,6 +34,7 @@ pub mod payout_attempt; pub mod payouts; pub mod process_tracker; pub mod refund; +pub mod relay; pub mod reverse_lookup; pub mod role; pub mod routing_algorithm; diff --git a/crates/diesel_models/src/query/relay.rs b/crates/diesel_models/src/query/relay.rs new file mode 100644 index 000000000000..28ede3cd61a8 --- /dev/null +++ b/crates/diesel_models/src/query/relay.rs @@ -0,0 +1,38 @@ +use diesel::{associations::HasTable, ExpressionMethods}; + +use super::generics; +use crate::{ + errors, + relay::{Relay, RelayNew, RelayUpdateInternal}, + schema::relay::dsl, + PgPooledConn, StorageResult, +}; + +impl RelayNew { + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl Relay { + pub async fn update( + self, + conn: &PgPooledConn, + relay: RelayUpdateInternal, + ) -> StorageResult { + match generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >(conn, dsl::id.eq(self.id.to_owned()), relay) + .await + { + Err(error) => match error.current_context() { + errors::DatabaseError::NoFieldsToUpdate => Ok(self), + _ => Err(error), + }, + result => result, + } + } +} diff --git a/crates/diesel_models/src/relay.rs b/crates/diesel_models/src/relay.rs new file mode 100644 index 000000000000..24a6ab8b034a --- /dev/null +++ b/crates/diesel_models/src/relay.rs @@ -0,0 +1,77 @@ +use common_utils::pii; +use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; +use time::PrimitiveDateTime; + +use crate::{enums as storage_enums, schema::relay}; + +#[derive( + Clone, + Debug, + Eq, + Identifiable, + Queryable, + Selectable, + PartialEq, + serde::Serialize, + serde::Deserialize, +)] +#[diesel(table_name = relay)] +pub struct Relay { + pub id: common_utils::id_type::RelayId, + pub connector_resource_id: String, + pub connector_id: common_utils::id_type::MerchantConnectorAccountId, + pub profile_id: common_utils::id_type::ProfileId, + pub merchant_id: common_utils::id_type::MerchantId, + pub relay_type: storage_enums::RelayType, + pub request_data: Option, + pub status: storage_enums::RelayStatus, + pub connector_reference_id: Option, + pub error_code: Option, + pub error_message: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: PrimitiveDateTime, + pub response_data: Option, +} + +#[derive( + Clone, + Debug, + Eq, + PartialEq, + Insertable, + router_derive::DebugAsDisplay, + serde::Serialize, + serde::Deserialize, + router_derive::Setter, +)] +#[diesel(table_name = relay)] +pub struct RelayNew { + pub id: common_utils::id_type::RelayId, + pub connector_resource_id: String, + pub connector_id: common_utils::id_type::MerchantConnectorAccountId, + pub profile_id: common_utils::id_type::ProfileId, + pub merchant_id: common_utils::id_type::MerchantId, + pub relay_type: storage_enums::RelayType, + pub request_data: Option, + pub status: storage_enums::RelayStatus, + pub connector_reference_id: Option, + pub error_code: Option, + pub error_message: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: PrimitiveDateTime, + pub response_data: Option, +} + +#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] +#[table_name = "relay"] +pub struct RelayUpdateInternal { + pub connector_reference_id: Option, + pub status: Option, + pub error_code: Option, + pub error_message: Option, + pub modified_at: PrimitiveDateTime, +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 61a8a7e19b9e..366c917d2d11 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1235,6 +1235,35 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + relay (id) { + #[max_length = 64] + id -> Varchar, + #[max_length = 128] + connector_resource_id -> Varchar, + #[max_length = 64] + connector_id -> Varchar, + #[max_length = 64] + profile_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + relay_type -> RelayType, + request_data -> Nullable, + status -> RelayStatus, + #[max_length = 128] + connector_reference_id -> Nullable, + #[max_length = 64] + error_code -> Nullable, + error_message -> Nullable, + created_at -> Timestamp, + modified_at -> Timestamp, + response_data -> Nullable, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -1479,6 +1508,7 @@ diesel::allow_tables_to_appear_in_same_query!( payouts, process_tracker, refund, + relay, reverse_lookup, roles, routing_algorithm, diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 6569c0353654..fcfe05e5731c 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1181,6 +1181,35 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + relay (id) { + #[max_length = 64] + id -> Varchar, + #[max_length = 128] + connector_resource_id -> Varchar, + #[max_length = 64] + connector_id -> Varchar, + #[max_length = 64] + profile_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + relay_type -> RelayType, + request_data -> Nullable, + status -> RelayStatus, + #[max_length = 128] + connector_reference_id -> Nullable, + #[max_length = 64] + error_code -> Nullable, + error_message -> Nullable, + created_at -> Timestamp, + modified_at -> Timestamp, + response_data -> Nullable, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -1427,6 +1456,7 @@ diesel::allow_tables_to_appear_in_same_query!( payouts, process_tracker, refund, + relay, reverse_lookup, roles, routing_algorithm, diff --git a/crates/external_services/src/grpc_client.rs b/crates/external_services/src/grpc_client.rs index 8981a1094d69..404685025ed3 100644 --- a/crates/external_services/src/grpc_client.rs +++ b/crates/external_services/src/grpc_client.rs @@ -6,6 +6,8 @@ pub mod dynamic_routing; pub mod health_check_client; use std::{fmt::Debug, sync::Arc}; +#[cfg(feature = "dynamic_routing")] +use common_utils::consts; #[cfg(feature = "dynamic_routing")] use dynamic_routing::{DynamicRoutingClientConfig, RoutingStrategy}; #[cfg(feature = "dynamic_routing")] @@ -16,6 +18,8 @@ use http_body_util::combinators::UnsyncBoxBody; use hyper::body::Bytes; #[cfg(feature = "dynamic_routing")] use hyper_util::client::legacy::connect::HttpConnector; +#[cfg(feature = "dynamic_routing")] +use router_env::logger; use serde; #[cfg(feature = "dynamic_routing")] use tonic::Status; @@ -76,3 +80,51 @@ impl GrpcClientSettings { }) } } + +/// Contains grpc headers +#[derive(Debug)] +pub struct GrpcHeaders { + /// Tenant id + pub tenant_id: String, + /// Request id + pub request_id: Option, +} + +#[cfg(feature = "dynamic_routing")] +/// Trait to add necessary headers to the tonic Request +pub(crate) trait AddHeaders { + /// Add necessary header fields to the tonic Request + fn add_headers_to_grpc_request(&mut self, headers: GrpcHeaders); +} + +#[cfg(feature = "dynamic_routing")] +impl AddHeaders for tonic::Request { + #[track_caller] + fn add_headers_to_grpc_request(&mut self, headers: GrpcHeaders) { + headers.tenant_id + .parse() + .map(|tenant_id| { + self + .metadata_mut() + .append(consts::TENANT_HEADER, tenant_id) + }) + .inspect_err( + |err| logger::warn!(header_parse_error=?err,"invalid {} received",consts::TENANT_HEADER), + ) + .ok(); + + headers.request_id.map(|request_id| { + request_id + .parse() + .map(|request_id| { + self + .metadata_mut() + .append(consts::X_REQUEST_ID, request_id) + }) + .inspect_err( + |err| logger::warn!(header_parse_error=?err,"invalid {} received",consts::X_REQUEST_ID), + ) + .ok(); + }); + } +} diff --git a/crates/external_services/src/grpc_client/dynamic_routing/elimination_rate_client.rs b/crates/external_services/src/grpc_client/dynamic_routing/elimination_rate_client.rs index 6587b7941f4c..bc5ce4997270 100644 --- a/crates/external_services/src/grpc_client/dynamic_routing/elimination_rate_client.rs +++ b/crates/external_services/src/grpc_client/dynamic_routing/elimination_rate_client.rs @@ -21,6 +21,7 @@ pub mod elimination_rate { } use super::{Client, DynamicRoutingError, DynamicRoutingResult}; +use crate::grpc_client::{AddHeaders, GrpcHeaders}; /// The trait Elimination Based Routing would have the functions required to support performance, calculation and invalidation bucket #[async_trait::async_trait] @@ -32,6 +33,7 @@ pub trait EliminationBasedRouting: dyn_clone::DynClone + Send + Sync { params: String, labels: Vec, configs: Option, + headers: GrpcHeaders, ) -> DynamicRoutingResult; /// To update the bucket size and ttl for list of connectors with its respective bucket name async fn update_elimination_bucket_config( @@ -40,11 +42,13 @@ pub trait EliminationBasedRouting: dyn_clone::DynClone + Send + Sync { params: String, report: Vec, config: Option, + headers: GrpcHeaders, ) -> DynamicRoutingResult; /// To invalidate the previous id's bucket async fn invalidate_elimination_bucket( &self, id: String, + headers: GrpcHeaders, ) -> DynamicRoutingResult; } @@ -56,6 +60,7 @@ impl EliminationBasedRouting for EliminationAnalyserClient { params: String, label_input: Vec, configs: Option, + headers: GrpcHeaders, ) -> DynamicRoutingResult { let labels = label_input .into_iter() @@ -64,13 +69,15 @@ impl EliminationBasedRouting for EliminationAnalyserClient { let config = configs.map(ForeignTryFrom::foreign_try_from).transpose()?; - let request = tonic::Request::new(EliminationRequest { + let mut request = tonic::Request::new(EliminationRequest { id, params, labels, config, }); + request.add_headers_to_grpc_request(headers); + let response = self .clone() .get_elimination_status(request) @@ -89,6 +96,7 @@ impl EliminationBasedRouting for EliminationAnalyserClient { params: String, report: Vec, configs: Option, + headers: GrpcHeaders, ) -> DynamicRoutingResult { let config = configs.map(ForeignTryFrom::foreign_try_from).transpose()?; @@ -102,13 +110,15 @@ impl EliminationBasedRouting for EliminationAnalyserClient { }) .collect::>(); - let request = tonic::Request::new(UpdateEliminationBucketRequest { + let mut request = tonic::Request::new(UpdateEliminationBucketRequest { id, params, labels_with_bucket_name, config, }); + request.add_headers_to_grpc_request(headers); + let response = self .clone() .update_elimination_bucket(request) @@ -122,8 +132,11 @@ impl EliminationBasedRouting for EliminationAnalyserClient { async fn invalidate_elimination_bucket( &self, id: String, + headers: GrpcHeaders, ) -> DynamicRoutingResult { - let request = tonic::Request::new(InvalidateBucketRequest { id }); + let mut request = tonic::Request::new(InvalidateBucketRequest { id }); + + request.add_headers_to_grpc_request(headers); let response = self .clone() diff --git a/crates/external_services/src/grpc_client/dynamic_routing/success_rate_client.rs b/crates/external_services/src/grpc_client/dynamic_routing/success_rate_client.rs index f6d3efb88769..3cf06ab63beb 100644 --- a/crates/external_services/src/grpc_client/dynamic_routing/success_rate_client.rs +++ b/crates/external_services/src/grpc_client/dynamic_routing/success_rate_client.rs @@ -21,6 +21,7 @@ pub mod success_rate { tonic::include_proto!("success_rate"); } use super::{Client, DynamicRoutingError, DynamicRoutingResult}; +use crate::grpc_client::{AddHeaders, GrpcHeaders}; /// The trait Success Based Dynamic Routing would have the functions required to support the calculation and updation window #[async_trait::async_trait] pub trait SuccessBasedDynamicRouting: dyn_clone::DynClone + Send + Sync { @@ -31,6 +32,7 @@ pub trait SuccessBasedDynamicRouting: dyn_clone::DynClone + Send + Sync { success_rate_based_config: SuccessBasedRoutingConfig, params: String, label_input: Vec, + headers: GrpcHeaders, ) -> DynamicRoutingResult; /// To update the success rate with the given label async fn update_success_rate( @@ -39,11 +41,13 @@ pub trait SuccessBasedDynamicRouting: dyn_clone::DynClone + Send + Sync { success_rate_based_config: SuccessBasedRoutingConfig, params: String, response: Vec, + headers: GrpcHeaders, ) -> DynamicRoutingResult; /// To invalidates the success rate routing keys async fn invalidate_success_rate_routing_keys( &self, id: String, + headers: GrpcHeaders, ) -> DynamicRoutingResult; } @@ -55,6 +59,7 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { success_rate_based_config: SuccessBasedRoutingConfig, params: String, label_input: Vec, + headers: GrpcHeaders, ) -> DynamicRoutingResult { let labels = label_input .into_iter() @@ -66,13 +71,15 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { .map(ForeignTryFrom::foreign_try_from) .transpose()?; - let request = tonic::Request::new(CalSuccessRateRequest { + let mut request = tonic::Request::new(CalSuccessRateRequest { id, params, labels, config, }); + request.add_headers_to_grpc_request(headers); + let response = self .clone() .fetch_success_rate(request) @@ -91,6 +98,7 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { success_rate_based_config: SuccessBasedRoutingConfig, params: String, label_input: Vec, + headers: GrpcHeaders, ) -> DynamicRoutingResult { let config = success_rate_based_config .config @@ -105,13 +113,15 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { }) .collect(); - let request = tonic::Request::new(UpdateSuccessRateWindowRequest { + let mut request = tonic::Request::new(UpdateSuccessRateWindowRequest { id, params, labels_with_status, config, }); + request.add_headers_to_grpc_request(headers); + let response = self .clone() .update_success_rate_window(request) @@ -126,8 +136,11 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { async fn invalidate_success_rate_routing_keys( &self, id: String, + headers: GrpcHeaders, ) -> DynamicRoutingResult { - let request = tonic::Request::new(InvalidateWindowsRequest { id }); + let mut request = tonic::Request::new(InvalidateWindowsRequest { id }); + + request.add_headers_to_grpc_request(headers); let response = self .clone() diff --git a/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs b/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs index e8ab479c978c..075a7b070d6d 100644 --- a/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs @@ -1,5 +1,5 @@ use common_enums::enums; -use common_utils::{errors::ParsingError, request::Method}; +use common_utils::{errors::ParsingError, pii::IpAddress, request::Method}; use error_stack::ResultExt; use hyperswitch_domain_models::{ payment_method_data::{PaymentMethodData, WalletData}, @@ -21,7 +21,7 @@ use uuid::Uuid; use crate::{ types::{RefundsResponseRouterData, ResponseRouterData}, - utils::{self, CardData as _}, + utils::{self, BrowserInformationData, CardData as _, PaymentsAuthorizeRequestData}, }; pub struct AirwallexAuthType { @@ -124,6 +124,40 @@ pub struct AirwallexPaymentsRequest { payment_method: AirwallexPaymentMethod, payment_method_options: Option, return_url: Option, + device_data: DeviceData, +} + +#[derive(Debug, Serialize)] +pub struct DeviceData { + accept_header: String, + browser: Browser, + ip_address: Secret, + language: String, + mobile: Option, + screen_color_depth: u8, + screen_height: u32, + screen_width: u32, + timezone: String, +} + +#[derive(Debug, Serialize)] +pub struct Browser { + java_enabled: bool, + javascript_enabled: bool, + user_agent: String, +} + +#[derive(Debug, Serialize)] +pub struct Location { + lat: String, + lon: String, +} + +#[derive(Debug, Serialize)] +pub struct Mobile { + device_model: Option, + os_type: Option, + os_version: Option, } #[derive(Debug, Serialize)] @@ -242,16 +276,55 @@ impl TryFrom<&AirwallexRouterData<&types::PaymentsAuthorizeRouterData>> )) } }?; + let device_data = get_device_data(item.router_data)?; Ok(Self { request_id: Uuid::new_v4().to_string(), payment_method, payment_method_options, return_url: request.complete_authorize_url.clone(), + device_data, }) } } +fn get_device_data( + item: &types::PaymentsAuthorizeRouterData, +) -> Result> { + let info = item.request.get_browser_info()?; + let browser = Browser { + java_enabled: info.get_java_enabled()?, + javascript_enabled: info.get_java_script_enabled()?, + user_agent: info.get_user_agent()?, + }; + let mobile = { + let device_model = info.get_device_model().ok(); + let os_type = info.get_os_type().ok(); + let os_version = info.get_os_version().ok(); + + if device_model.is_some() || os_type.is_some() || os_version.is_some() { + Some(Mobile { + device_model, + os_type, + os_version, + }) + } else { + None + } + }; + Ok(DeviceData { + accept_header: info.get_accept_header()?, + browser, + ip_address: info.get_ip_address()?, + mobile, + screen_color_depth: info.get_color_depth()?, + screen_height: info.get_screen_height()?, + screen_width: info.get_screen_width()?, + timezone: info.get_time_zone()?.to_string(), + language: info.get_language()?, + }) +} + fn get_wallet_details( wallet_data: &WalletData, ) -> Result { diff --git a/crates/hyperswitch_connectors/src/connectors/jpmorgan.rs b/crates/hyperswitch_connectors/src/connectors/jpmorgan.rs index 9039a7e22189..2689e1a62d0f 100644 --- a/crates/hyperswitch_connectors/src/connectors/jpmorgan.rs +++ b/crates/hyperswitch_connectors/src/connectors/jpmorgan.rs @@ -1,14 +1,15 @@ pub mod transformers; - +use base64::Engine; +use common_enums::enums; use common_utils::{ errors::CustomResult, ext_traits::BytesExt, request::{Method, Request, RequestBuilder, RequestContent}, - types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, + types::{AmountConvertor, MinorUnit, MinorUnitForConnector}, }; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ - router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, + router_data::{AccessToken, ErrorResponse, RouterData}, router_flow_types::{ access_token_auth::AccessTokenAuth, payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void}, @@ -21,36 +22,39 @@ use hyperswitch_domain_models::{ }, router_response_types::{PaymentsResponseData, RefundsResponseData}, types::{ - PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, - RefundSyncRouterData, RefundsRouterData, + PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, + PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData, }, }; -// use hyperswitch_interfaces::{ api::{ self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorSpecifications, ConnectorValidation, }, configs::Connectors, - errors, + consts, errors, events::connector_api_logs::ConnectorEvent, - types::{self, Response}, + types::{self, RefreshTokenType, Response}, webhooks, }; -use masking::{ExposeInterface, Mask}; -use transformers as jpmorgan; +use masking::{Mask, Maskable, PeekInterface}; +use transformers::{self as jpmorgan, JpmorganErrorResponse}; -use crate::{constants::headers, types::ResponseRouterData, utils}; +use crate::{ + constants::headers, + types::{RefreshTokenRouterData, ResponseRouterData}, + utils, +}; #[derive(Clone)] pub struct Jpmorgan { - amount_converter: &'static (dyn AmountConvertor + Sync), + amount_converter: &'static (dyn AmountConvertor + Sync), } impl Jpmorgan { pub fn new() -> &'static Self { &Self { - amount_converter: &StringMinorUnitForConnector, + amount_converter: &MinorUnitForConnector, } } } @@ -82,14 +86,38 @@ where &self, req: &RouterData, _connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - let mut header = vec![( + ) -> CustomResult)>, errors::ConnectorError> { + let mut headers = vec![( headers::CONTENT_TYPE.to_string(), self.get_content_type().to_string().into(), )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); - Ok(header) + let auth_header = ( + headers::AUTHORIZATION.to_string(), + format!( + "Bearer {}", + req.access_token + .clone() + .ok_or(errors::ConnectorError::FailedToObtainAuthType)? + .token + .peek() + ) + .into_masked(), + ); + let request_id = ( + headers::REQUEST_ID.to_string(), + req.connector_request_reference_id + .clone() + .to_string() + .into_masked(), + ); + let merchant_id = ( + headers::MERCHANT_ID.to_string(), + req.merchant_id.get_string_repr().to_string().into_masked(), + ); + headers.push(auth_header); + headers.push(request_id); + headers.push(merchant_id); + Ok(headers) } } @@ -99,11 +127,7 @@ impl ConnectorCommon for Jpmorgan { } fn get_currency_unit(&self) -> api::CurrencyUnit { - api::CurrencyUnit::Base - //todo!() - // TODO! Check connector documentation, on which unit they are processing the currency. - // If the connector accepts amount in lower unit ( i.e cents for USD) then return api::CurrencyUnit::Minor, - // if connector accepts amount in base unit (i.e dollars for USD) then return api::CurrencyUnit::Base + api::CurrencyUnit::Minor } fn common_get_content_type(&self) -> &'static str { @@ -114,36 +138,29 @@ impl ConnectorCommon for Jpmorgan { connectors.jpmorgan.base_url.as_ref() } - fn get_auth_header( - &self, - auth_type: &ConnectorAuthType, - ) -> CustomResult)>, errors::ConnectorError> { - let auth = jpmorgan::JpmorganAuthType::try_from(auth_type) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - Ok(vec![( - headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), - )]) - } - fn build_error_response( &self, res: Response, event_builder: Option<&mut ConnectorEvent>, ) -> CustomResult { - let response: jpmorgan::JpmorganErrorResponse = res + let response: JpmorganErrorResponse = res .response .parse_struct("JpmorganErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); + event_builder.map(|i| i.set_response_body(&response)); + + let response_message = response + .response_message + .as_ref() + .map_or_else(|| consts::NO_ERROR_MESSAGE.to_string(), ToString::to_string); Ok(ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, + code: response.response_code, + message: response_message.clone(), + reason: Some(response_message), attempt_status: None, connector_transaction_id: None, }) @@ -151,14 +168,151 @@ impl ConnectorCommon for Jpmorgan { } impl ConnectorValidation for Jpmorgan { - //TODO: implement functions when support enabled + fn validate_connector_against_payment_request( + &self, + capture_method: Option, + _payment_method: enums::PaymentMethod, + _pmt: Option, + ) -> CustomResult<(), errors::ConnectorError> { + let capture_method = capture_method.unwrap_or_default(); + match capture_method { + enums::CaptureMethod::Automatic | enums::CaptureMethod::Manual => Ok(()), + enums::CaptureMethod::ManualMultiple + | enums::CaptureMethod::Scheduled + | enums::CaptureMethod::SequentialAutomatic => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Jpmorgan"), + ))? + } + } + } + + fn validate_psync_reference_id( + &self, + data: &PaymentsSyncData, + _is_three_ds: bool, + _status: enums::AttemptStatus, + _connector_meta_data: Option, + ) -> CustomResult<(), errors::ConnectorError> { + if data.encoded_data.is_some() + || data + .connector_transaction_id + .get_connector_transaction_id() + .is_ok() + { + return Ok(()); + } + Err(errors::ConnectorError::MissingConnectorTransactionID.into()) + } } impl ConnectorIntegration for Jpmorgan { //TODO: implement sessions flow } -impl ConnectorIntegration for Jpmorgan {} +impl ConnectorIntegration for Jpmorgan { + fn get_headers( + &self, + req: &RefreshTokenRouterData, + _connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let client_id = req.request.app_id.clone(); + + let client_secret = req.request.id.clone(); + + let creds = format!( + "{}:{}", + client_id.peek(), + client_secret.unwrap_or_default().peek() + ); + let encoded_creds = common_utils::consts::BASE64_ENGINE.encode(creds); + + let auth_string = format!("Basic {}", encoded_creds); + Ok(vec![ + ( + headers::CONTENT_TYPE.to_string(), + RefreshTokenType::get_content_type(self).to_string().into(), + ), + ( + headers::AUTHORIZATION.to_string(), + auth_string.into_masked(), + ), + ]) + } + + fn get_content_type(&self) -> &'static str { + "application/x-www-form-urlencoded" + } + + fn get_url( + &self, + _req: &RefreshTokenRouterData, + connectors: &Connectors, + ) -> CustomResult { + Ok(format!( + "{}/am/oauth2/alpha/access_token", + connectors + .jpmorgan + .secondary_base_url + .as_ref() + .ok_or(errors::ConnectorError::FailedToObtainIntegrationUrl)? + )) + } + + fn get_request_body( + &self, + req: &RefreshTokenRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let connector_req = jpmorgan::JpmorganAuthUpdateRequest::try_from(req)?; + Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &RefreshTokenRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .attach_default_headers() + .headers(RefreshTokenType::get_headers(self, req, connectors)?) + .url(&RefreshTokenType::get_url(self, req, connectors)?) + .set_body(RefreshTokenType::get_request_body(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &RefreshTokenRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: jpmorgan::JpmorganAuthUpdateResponse = res + .response + .parse_struct("jpmorgan JpmorganAuthUpdateResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} impl ConnectorIntegration for Jpmorgan @@ -170,7 +324,7 @@ impl ConnectorIntegration CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } @@ -181,9 +335,9 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}/payments", self.base_url(connectors))) } fn get_request_body( @@ -191,7 +345,7 @@ impl ConnectorIntegration CustomResult { - let amount = utils::convert_amount( + let amount: MinorUnit = utils::convert_amount( self.amount_converter, req.request.minor_amount, req.request.currency, @@ -252,12 +406,102 @@ impl ConnectorIntegration for Jpmorgan { + fn get_headers( + &self, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, + ) -> CustomResult { + let tid = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}/payments/{}/captures", + self.base_url(connectors), + tid + )) + } + + fn get_request_body( + &self, + req: &PaymentsCaptureRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let amount: MinorUnit = utils::convert_amount( + self.amount_converter, + req.request.minor_amount_to_capture, + req.request.currency, + )?; + + let connector_router_data = jpmorgan::JpmorganRouterData::from((amount, req)); + let connector_req = jpmorgan::JpmorganCaptureRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsCaptureRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: jpmorgan::JpmorganPaymentsResponse = res + .response + .parse_struct("Jpmorgan PaymentsCaptureResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + impl ConnectorIntegration for Jpmorgan { fn get_headers( &self, req: &PaymentsSyncRouterData, connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } @@ -267,10 +511,15 @@ impl ConnectorIntegration for Jpm fn get_url( &self, - _req: &PaymentsSyncRouterData, - _connectors: &Connectors, + req: &PaymentsSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let tid = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!("{}/payments/{}", self.base_url(connectors), tid)) } fn build_request( @@ -316,12 +565,12 @@ impl ConnectorIntegration for Jpm } } -impl ConnectorIntegration for Jpmorgan { +impl ConnectorIntegration for Jpmorgan { fn get_headers( &self, - req: &PaymentsCaptureRouterData, + req: &PaymentsCancelRouterData, connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } @@ -331,34 +580,41 @@ impl ConnectorIntegration fo fn get_url( &self, - _req: &PaymentsCaptureRouterData, - _connectors: &Connectors, + req: &PaymentsCancelRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let tid = req.request.connector_transaction_id.clone(); + Ok(format!("{}/payments/{}", self.base_url(connectors), tid)) } fn get_request_body( &self, - _req: &PaymentsCaptureRouterData, + req: &PaymentsCancelRouterData, _connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let amount: MinorUnit = utils::convert_amount( + self.amount_converter, + req.request.minor_amount.unwrap_or_default(), + req.request.currency.unwrap_or_default(), + )?; + + let connector_router_data = jpmorgan::JpmorganRouterData::from((amount, req)); + let connector_req = jpmorgan::JpmorganCancelRequest::try_from(connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, - req: &PaymentsCaptureRouterData, + req: &PaymentsCancelRouterData, connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( RequestBuilder::new() - .method(Method::Post) - .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .method(Method::Patch) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) .attach_default_headers() - .headers(types::PaymentsCaptureType::get_headers( - self, req, connectors, - )?) - .set_body(types::PaymentsCaptureType::get_request_body( + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .set_body(types::PaymentsVoidType::get_request_body( self, req, connectors, )?) .build(), @@ -367,14 +623,15 @@ impl ConnectorIntegration fo fn handle_response( &self, - data: &PaymentsCaptureRouterData, + data: &PaymentsCancelRouterData, event_builder: Option<&mut ConnectorEvent>, res: Response, - ) -> CustomResult { - let response: jpmorgan::JpmorganPaymentsResponse = res + ) -> CustomResult { + let response: jpmorgan::JpmorganCancelResponse = res .response - .parse_struct("Jpmorgan PaymentsCaptureResponse") + .parse_struct("JpmrorganPaymentsVoidResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); RouterData::try_from(ResponseRouterData { @@ -393,14 +650,12 @@ impl ConnectorIntegration fo } } -impl ConnectorIntegration for Jpmorgan {} - impl ConnectorIntegration for Jpmorgan { fn get_headers( &self, req: &RefundsRouterData, connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } @@ -413,7 +668,7 @@ impl ConnectorIntegration for Jpmorga _req: &RefundsRouterData, _connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Err(errors::ConnectorError::NotImplemented("Refunds".to_string()).into()) } fn get_request_body( @@ -437,18 +692,19 @@ impl ConnectorIntegration for Jpmorga req: &RefundsRouterData, connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { - let request = RequestBuilder::new() - .method(Method::Post) - .url(&types::RefundExecuteType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::RefundExecuteType::get_headers( - self, req, connectors, - )?) - .set_body(types::RefundExecuteType::get_request_body( - self, req, connectors, - )?) - .build(); - Ok(Some(request)) + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .set_body(types::RefundExecuteType::get_request_body( + self, req, connectors, + )?) + .build(), + )) } fn handle_response( @@ -457,9 +713,9 @@ impl ConnectorIntegration for Jpmorga event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: jpmorgan::RefundResponse = res + let response: jpmorgan::JpmorganRefundResponse = res .response - .parse_struct("jpmorgan RefundResponse") + .parse_struct("JpmorganRefundResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -484,22 +740,21 @@ impl ConnectorIntegration for Jpmorgan &self, req: &RefundSyncRouterData, connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { + ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) } fn get_content_type(&self) -> &'static str { self.common_get_content_type() } - fn get_url( &self, - _req: &RefundSyncRouterData, - _connectors: &Connectors, + req: &RefundSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let tid = req.request.connector_transaction_id.clone(); + Ok(format!("{}/refunds/{}", self.base_url(connectors), tid)) } - fn build_request( &self, req: &RefundSyncRouterData, @@ -524,7 +779,7 @@ impl ConnectorIntegration for Jpmorgan event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: jpmorgan::RefundResponse = res + let response: jpmorgan::JpmorganRefundSyncResponse = res .response .parse_struct("jpmorgan RefundSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; diff --git a/crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs b/crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs index 90b058697051..78c1b5eefb5f 100644 --- a/crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/jpmorgan/transformers.rs @@ -1,12 +1,15 @@ -use common_enums::enums; -use common_utils::types::StringMinorUnit; +use common_enums::enums::CaptureMethod; +use common_utils::types::MinorUnit; use hyperswitch_domain_models::{ payment_method_data::PaymentMethodData, - router_data::{ConnectorAuthType, RouterData}, + router_data::{AccessToken, ConnectorAuthType, RouterData}, router_flow_types::refunds::{Execute, RSync}, - router_request_types::ResponseId, + router_request_types::{PaymentsCancelData, ResponseId}, router_response_types::{PaymentsResponseData, RefundsResponseData}, - types::{PaymentsAuthorizeRouterData, RefundsRouterData}, + types::{ + PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, + RefreshTokenRouterData, RefundsRouterData, + }, }; use hyperswitch_interfaces::errors; use masking::Secret; @@ -14,18 +17,17 @@ use serde::{Deserialize, Serialize}; use crate::{ types::{RefundsResponseRouterData, ResponseRouterData}, - utils::PaymentsAuthorizeRequestData, + utils::{ + get_unimplemented_payment_method_error_message, CardData, RouterData as OtherRouterData, + }, }; - -//TODO: Fill the struct with respective fields pub struct JpmorganRouterData { - pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub amount: MinorUnit, pub router_data: T, } -impl From<(StringMinorUnit, T)> for JpmorganRouterData { - fn from((amount, item): (StringMinorUnit, T)) -> Self { - //Todo : use utils to convert the amount to the type of amount that a connector accepts +impl From<(MinorUnit, T)> for JpmorganRouterData { + fn from((amount, item): (MinorUnit, T)) -> Self { Self { amount, router_data: item, @@ -33,20 +35,102 @@ impl From<(StringMinorUnit, T)> for JpmorganRouterData { } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, PartialEq)] +#[derive(Debug, Clone, Serialize)] +pub struct JpmorganAuthUpdateRequest { + pub grant_type: String, + pub scope: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JpmorganAuthUpdateResponse { + pub access_token: Secret, + pub scope: String, + pub token_type: String, + pub expires_in: i64, +} + +impl TryFrom<&RefreshTokenRouterData> for JpmorganAuthUpdateRequest { + type Error = error_stack::Report; + fn try_from(_item: &RefreshTokenRouterData) -> Result { + Ok(Self { + grant_type: String::from("client_credentials"), + scope: String::from("jpm:payments:sandbox"), + }) + } +} + +impl TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(AccessToken { + token: item.response.access_token, + expires: item.response.expires_in, + }), + ..item.data + }) + } +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct JpmorganPaymentsRequest { - amount: StringMinorUnit, - card: JpmorganCard, + capture_method: CapMethod, + amount: MinorUnit, + currency: common_enums::Currency, + merchant: JpmorganMerchant, + payment_method_type: JpmorganPaymentMethodType, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct JpmorganCard { - number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, + account_number: Secret, + expiry: Expiry, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganPaymentMethodType { + card: JpmorganCard, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Expiry { + month: Secret, + year: Secret, +} + +#[derive(Serialize, Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganMerchantSoftware { + company_name: Secret, + product_name: Secret, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganMerchant { + merchant_software: JpmorganMerchantSoftware, +} + +fn map_capture_method( + capture_method: CaptureMethod, +) -> Result> { + match capture_method { + CaptureMethod::Automatic => Ok(CapMethod::Now), + CaptureMethod::Manual => Ok(CapMethod::Manual), + CaptureMethod::Scheduled + | CaptureMethod::ManualMultiple + | CaptureMethod::SequentialAutomatic => { + Err(errors::ConnectorError::NotImplemented("Capture Method".to_string()).into()) + } + } } impl TryFrom<&JpmorganRouterData<&PaymentsAuthorizeRouterData>> for JpmorganPaymentsRequest { @@ -56,84 +140,226 @@ impl TryFrom<&JpmorganRouterData<&PaymentsAuthorizeRouterData>> for JpmorganPaym ) -> Result { match item.router_data.request.payment_method_data.clone() { PaymentMethodData::Card(req_card) => { + if item.router_data.is_three_ds() { + return Err(errors::ConnectorError::NotSupported { + message: "3DS payments".to_string(), + connector: "Jpmorgan", + } + .into()); + } + + let capture_method = + map_capture_method(item.router_data.request.capture_method.unwrap_or_default()); + + let merchant_software = JpmorganMerchantSoftware { + company_name: String::from("JPMC").into(), + product_name: String::from("Hyperswitch").into(), + }; + + let merchant = JpmorganMerchant { merchant_software }; + + let expiry: Expiry = Expiry { + month: req_card.card_exp_month.clone(), + year: req_card.get_expiry_year_4_digit(), + }; + + let account_number = Secret::new(req_card.card_number.to_string()); + let card = JpmorganCard { - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.router_data.request.is_auto_capture()?, + account_number, + expiry, }; + + let payment_method_type = JpmorganPaymentMethodType { card }; + Ok(Self { - amount: item.amount.clone(), - card, + capture_method: capture_method?, + currency: item.router_data.request.currency, + amount: item.amount, + merchant, + payment_method_type, }) } - _ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()), + PaymentMethodData::CardDetailsForNetworkTransactionId(_) + | PaymentMethodData::CardRedirect(_) + | PaymentMethodData::Wallet(_) + | PaymentMethodData::PayLater(_) + | PaymentMethodData::BankRedirect(_) + | PaymentMethodData::BankDebit(_) + | PaymentMethodData::BankTransfer(_) + | PaymentMethodData::Crypto(_) + | PaymentMethodData::MandatePayment + | PaymentMethodData::Reward + | PaymentMethodData::RealTimePayment(_) + | PaymentMethodData::MobilePayment(_) + | PaymentMethodData::Upi(_) + | PaymentMethodData::Voucher(_) + | PaymentMethodData::GiftCard(_) + | PaymentMethodData::OpenBanking(_) + | PaymentMethodData::CardToken(_) + | PaymentMethodData::NetworkToken(_) => Err(errors::ConnectorError::NotImplemented( + get_unimplemented_payment_method_error_message("jpmorgan"), + ) + .into()), } } } -//TODO: Fill the struct with respective fields -// Auth Struct +//JP Morgan uses access token only due to which we aren't reading the fields in this struct +#[derive(Debug)] pub struct JpmorganAuthType { - pub(super) api_key: Secret, + pub(super) _api_key: Secret, + pub(super) _key1: Secret, } impl TryFrom<&ConnectorAuthType> for JpmorganAuthType { type Error = error_stack::Report; fn try_from(auth_type: &ConnectorAuthType) -> Result { match auth_type { - ConnectorAuthType::HeaderKey { api_key } => Ok(Self { - api_key: api_key.to_owned(), + ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { + _api_key: api_key.to_owned(), + _key1: key1.to_owned(), }), _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), } } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum JpmorganPaymentStatus { - Succeeded, - Failed, - #[default] - Processing, + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub enum JpmorganTransactionStatus { + Success, + Denied, + Error, } -impl From for common_enums::AttemptStatus { - fn from(item: JpmorganPaymentStatus) -> Self { - match item { - JpmorganPaymentStatus::Succeeded => Self::Charged, - JpmorganPaymentStatus::Failed => Self::Failure, - JpmorganPaymentStatus::Processing => Self::Authorizing, - } - } +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "UPPERCASE")] +pub enum JpmorganTransactionState { + Closed, + Authorized, + Voided, + #[default] + Pending, + Declined, + Error, } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Default, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub struct JpmorganPaymentsResponse { - status: JpmorganPaymentStatus, - id: String, + transaction_id: String, + request_id: String, + transaction_state: JpmorganTransactionState, + response_status: String, + response_code: String, + response_message: String, + payment_method_type: PaymentMethodType, + capture_method: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Merchant { + merchant_id: Option, + merchant_software: MerchantSoftware, + merchant_category_code: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantSoftware { + company_name: Secret, + product_name: Secret, + version: Option>, +} + +#[derive(Default, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentMethodType { + card: Option, +} + +#[derive(Default, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Card { + expiry: Option, + card_type: Option>, + card_type_name: Option>, + masked_account_number: Option>, + card_type_indicators: Option, + network_response: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NetworkResponse { + address_verification_result: Option>, + address_verification_result_code: Option>, + card_verification_result_code: Option>, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExpiryResponse { + month: Option>, + year: Option>, +} + +#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CardTypeIndicators { + issuance_country_code: Option>, + is_durbin_regulated: Option, + card_product_types: Secret>, +} + +pub fn attempt_status_from_transaction_state( + transaction_state: JpmorganTransactionState, +) -> common_enums::AttemptStatus { + match transaction_state { + JpmorganTransactionState::Authorized => common_enums::AttemptStatus::Authorized, + JpmorganTransactionState::Closed => common_enums::AttemptStatus::Charged, + JpmorganTransactionState::Declined | JpmorganTransactionState::Error => { + common_enums::AttemptStatus::Failure + } + JpmorganTransactionState::Pending => common_enums::AttemptStatus::Pending, + JpmorganTransactionState::Voided => common_enums::AttemptStatus::Voided, + } } impl TryFrom> for RouterData { type Error = error_stack::Report; + fn try_from( item: ResponseRouterData, ) -> Result { + let transaction_state = match item.response.transaction_state { + JpmorganTransactionState::Closed => match item.response.capture_method { + Some(CapMethod::Now) => JpmorganTransactionState::Closed, + _ => JpmorganTransactionState::Authorized, + }, + JpmorganTransactionState::Authorized => JpmorganTransactionState::Authorized, + JpmorganTransactionState::Voided => JpmorganTransactionState::Voided, + JpmorganTransactionState::Pending => JpmorganTransactionState::Pending, + JpmorganTransactionState::Declined => JpmorganTransactionState::Declined, + JpmorganTransactionState::Error => JpmorganTransactionState::Error, + }; + let status = attempt_status_from_transaction_state(transaction_state); + Ok(Self { - status: common_enums::AttemptStatus::from(item.response.status), + status, response: Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(item.response.id), + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), redirection_data: Box::new(None), mandate_reference: Box::new(None), connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(item.response.transaction_id.clone()), incremental_authorization_allowed: None, charge_id: None, }), @@ -142,24 +368,179 @@ impl TryFrom, + amount: MinorUnit, + currency: Option, } -impl TryFrom<&JpmorganRouterData<&RefundsRouterData>> for JpmorganRefundRequest { +#[derive(Debug, Default, Copy, Serialize, Deserialize, Clone)] +#[serde(rename_all = "UPPERCASE")] +pub enum CapMethod { + #[default] + Now, + Delayed, + Manual, +} + +impl TryFrom<&JpmorganRouterData<&PaymentsCaptureRouterData>> for JpmorganCaptureRequest { + type Error = error_stack::Report; + fn try_from( + item: &JpmorganRouterData<&PaymentsCaptureRouterData>, + ) -> Result { + let capture_method = Some(map_capture_method( + item.router_data.request.capture_method.unwrap_or_default(), + )?); + Ok(Self { + capture_method, + amount: item.amount, + currency: Some(item.router_data.request.currency), + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganCaptureResponse { + pub transaction_id: String, + pub request_id: String, + pub transaction_state: JpmorganTransactionState, + pub response_status: JpmorganTransactionStatus, + pub response_code: String, + pub response_message: String, + pub payment_method_type: PaymentMethodTypeCapRes, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentMethodTypeCapRes { + pub card: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CardCapRes { + pub card_type: Option>, + pub card_type_name: Option>, + unmasked_account_number: Option>, +} + +impl TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + let status = attempt_status_from_transaction_state(item.response.transaction_state); + Ok(Self { + status, + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.transaction_id.clone()), + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganPSyncResponse { + transaction_id: String, + transaction_state: JpmorganTransactionState, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum JpmorganResponseStatus { + Success, + Denied, + Error, +} + +impl + TryFrom> + for RouterData +{ type Error = error_stack::Report; - fn try_from(item: &JpmorganRouterData<&RefundsRouterData>) -> Result { + fn try_from( + item: ResponseRouterData, + ) -> Result { + let status = attempt_status_from_transaction_state(item.response.transaction_state); Ok(Self { - amount: item.amount.to_owned(), + status, + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.transaction_id.clone()), + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data }) } } -// Type definition for Refund Response +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct TransactionData { + payment_type: Option>, + status_code: Secret, + txn_secret: Option>, + tid: Option>, + test_mode: Option>, + status: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganRefundRequest { + pub merchant: MerchantRefundReq, + pub amount: MinorUnit, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantRefundReq { + pub merchant_software: MerchantSoftware, +} + +impl TryFrom<&JpmorganRouterData<&RefundsRouterData>> for JpmorganRefundRequest { + type Error = error_stack::Report; + fn try_from(_item: &JpmorganRouterData<&RefundsRouterData>) -> Result { + Err(errors::ConnectorError::NotImplemented("Refunds".to_string()).into()) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganRefundResponse { + pub transaction_id: Option, + pub request_id: String, + pub transaction_state: JpmorganTransactionState, + pub amount: MinorUnit, + pub currency: common_enums::Currency, + pub response_status: JpmorganResponseStatus, + pub response_code: String, + pub response_message: String, + pub transaction_reference_id: Option, + pub remaining_refundable_amount: Option, +} #[allow(dead_code)] #[derive(Debug, Serialize, Default, Deserialize, Clone)] @@ -170,59 +551,192 @@ pub enum RefundStatus { Processing, } -impl From for enums::RefundStatus { +impl From for common_enums::RefundStatus { fn from(item: RefundStatus) -> Self { match item { RefundStatus::Succeeded => Self::Success, RefundStatus::Failed => Self::Failure, RefundStatus::Processing => Self::Pending, - //TODO: Review mapping } } } -//TODO: Fill the struct with respective fields #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct RefundResponse { id: String, status: RefundStatus, } -impl TryFrom> for RefundsRouterData { +pub fn refund_status_from_transaction_state( + transaction_state: JpmorganTransactionState, +) -> common_enums::RefundStatus { + match transaction_state { + JpmorganTransactionState::Voided | JpmorganTransactionState::Closed => { + common_enums::RefundStatus::Success + } + JpmorganTransactionState::Declined | JpmorganTransactionState::Error => { + common_enums::RefundStatus::Failure + } + JpmorganTransactionState::Pending | JpmorganTransactionState::Authorized => { + common_enums::RefundStatus::Pending + } + } +} + +impl TryFrom> + for RefundsRouterData +{ type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + connector_refund_id: item + .response + .transaction_id + .clone() + .ok_or(errors::ConnectorError::ResponseHandlingFailed)?, + refund_status: refund_status_from_transaction_state( + item.response.transaction_state, + ), }), ..item.data }) } } -impl TryFrom> for RefundsRouterData { +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganRefundSyncResponse { + transaction_id: String, + request_id: String, + transaction_state: JpmorganTransactionState, + amount: MinorUnit, + currency: common_enums::Currency, + response_status: JpmorganResponseStatus, + response_code: String, +} + +impl TryFrom> + for RefundsRouterData +{ type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: RefundsResponseRouterData, ) -> Result { Ok(Self { response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + connector_refund_id: item.response.transaction_id.clone(), + refund_status: refund_status_from_transaction_state( + item.response.transaction_state, + ), + }), + ..item.data + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganCancelRequest { + pub amount: Option, + pub is_void: Option, + pub reversal_reason: Option, +} + +impl TryFrom> for JpmorganCancelRequest { + type Error = error_stack::Report; + fn try_from(item: JpmorganRouterData<&PaymentsCancelRouterData>) -> Result { + Ok(Self { + amount: item.router_data.request.amount, + is_void: Some(true), + reversal_reason: item.router_data.request.cancellation_reason.clone(), + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganCancelResponse { + transaction_id: String, + request_id: String, + response_status: JpmorganResponseStatus, + response_code: String, + response_message: String, + payment_method_type: JpmorganPaymentMethodTypeCancelResponse, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganPaymentMethodTypeCancelResponse { + pub card: CardCancelResponse, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CardCancelResponse { + pub card_type: Secret, + pub card_type_name: Secret, +} + +impl + TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + F, + JpmorganCancelResponse, + PaymentsCancelData, + PaymentsResponseData, + >, + ) -> Result { + let status = match item.response.response_status { + JpmorganResponseStatus::Success => common_enums::AttemptStatus::Voided, + JpmorganResponseStatus::Denied | JpmorganResponseStatus::Error => { + common_enums::AttemptStatus::Failure + } + }; + Ok(Self { + status, + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + item.response.transaction_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(item.response.transaction_id.clone()), + incremental_authorization_allowed: None, + charge_id: None, }), ..item.data }) } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganValidationErrors { + pub code: Option, + pub message: Option, + pub entity: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct JpmorganErrorInformation { + pub code: Option, + pub message: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct JpmorganErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, - pub reason: Option, + pub response_status: JpmorganTransactionStatus, + pub response_code: String, + pub response_message: Option, } diff --git a/crates/hyperswitch_connectors/src/constants.rs b/crates/hyperswitch_connectors/src/constants.rs index 9ca4ee9591fb..3d99f8d167ff 100644 --- a/crates/hyperswitch_connectors/src/constants.rs +++ b/crates/hyperswitch_connectors/src/constants.rs @@ -10,6 +10,7 @@ pub(crate) mod headers { pub(crate) const IDEMPOTENCY_KEY: &str = "Idempotency-Key"; pub(crate) const MESSAGE_SIGNATURE: &str = "Message-Signature"; pub(crate) const MERCHANT_ID: &str = "Merchant-ID"; + pub(crate) const REQUEST_ID: &str = "request-id"; pub(crate) const NONCE: &str = "nonce"; pub(crate) const TIMESTAMP: &str = "Timestamp"; pub(crate) const TOKEN: &str = "token"; diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index cf32d966e056..d1205dd8bb89 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1759,6 +1759,9 @@ pub trait BrowserInformationData { fn get_java_enabled(&self) -> Result; fn get_java_script_enabled(&self) -> Result; fn get_ip_address(&self) -> Result, Error>; + fn get_os_type(&self) -> Result; + fn get_os_version(&self) -> Result; + fn get_device_model(&self) -> Result; } impl BrowserInformationData for BrowserInformation { @@ -1807,6 +1810,21 @@ impl BrowserInformationData for BrowserInformation { self.java_script_enabled .ok_or_else(missing_field_err("browser_info.java_script_enabled")) } + fn get_os_type(&self) -> Result { + self.os_type + .clone() + .ok_or_else(missing_field_err("browser_info.os_type")) + } + fn get_os_version(&self) -> Result { + self.os_version + .clone() + .ok_or_else(missing_field_err("browser_info.os_version")) + } + fn get_device_model(&self) -> Result { + self.device_model + .clone() + .ok_or_else(missing_field_err("browser_info.device_model")) + } } pub fn get_header_key_value<'a>( diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index fe152e876ccf..7a170f918efc 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -17,6 +17,7 @@ pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; pub mod refunds; +pub mod relay; pub mod router_data; pub mod router_data_v2; pub mod router_flow_types; diff --git a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs index 51c75113dcda..baec0e59f8a4 100644 --- a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs +++ b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs @@ -1,12 +1,11 @@ #[cfg(feature = "v2")] use api_models::admin; -#[cfg(feature = "v2")] -use common_utils::ext_traits::ValueExt; use common_utils::{ crypto::Encryptable, date_time, encryption::Encryption, errors::{CustomResult, ValidationError}, + ext_traits::ValueExt, id_type, pii, type_name, types::keymanager::{Identifier, KeyManagerState, ToEncryptable}, }; @@ -21,9 +20,10 @@ use serde_json::Value; use super::behaviour; #[cfg(feature = "v2")] use crate::errors::api_error_response::ApiErrorResponse; -#[cfg(feature = "v2")] -use crate::router_data; -use crate::type_encryption::{crypto_operation, CryptoOperation}; +use crate::{ + router_data, + type_encryption::{crypto_operation, CryptoOperation}, +}; #[cfg(feature = "v1")] #[derive(Clone, Debug, router_derive::ToEncryption)] @@ -62,6 +62,19 @@ impl MerchantConnectorAccount { pub fn get_id(&self) -> id_type::MerchantConnectorAccountId { self.merchant_connector_id.clone() } + + pub fn get_connector_account_details( + &self, + ) -> error_stack::Result + { + self.connector_account_details + .get_inner() + .clone() + .parse_value("ConnectorAuthType") + } + pub fn get_connector_test_mode(&self) -> Option { + self.test_mode + } } #[cfg(feature = "v2")] @@ -134,6 +147,9 @@ impl MerchantConnectorAccount { .clone() .parse_value("ConnectorAuthType") } + pub fn get_connector_test_mode(&self) -> Option { + todo!() + } } #[cfg(feature = "v1")] diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 0243c2549204..1c88b83d087e 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -29,7 +29,10 @@ pub mod payment_intent; use common_enums as storage_enums; #[cfg(feature = "v2")] -use diesel_models::types::{FeatureMetadata, OrderDetailsWithAmount}; +use diesel_models::{ + ephemeral_key, + types::{FeatureMetadata, OrderDetailsWithAmount}, +}; use self::payment_attempt::PaymentAttempt; #[cfg(feature = "v1")] @@ -226,7 +229,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, @@ -235,7 +238,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 { @@ -599,6 +602,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 4fd861acb65c..cbd3a8137ca9 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/relay.rs b/crates/hyperswitch_domain_models/src/relay.rs new file mode 100644 index 000000000000..959ac8e7f612 --- /dev/null +++ b/crates/hyperswitch_domain_models/src/relay.rs @@ -0,0 +1,275 @@ +use common_enums::enums; +use common_utils::{ + self, + errors::{CustomResult, ValidationError}, + id_type::{self, GenerateId}, + pii, + types::{keymanager, MinorUnit}, +}; +use diesel_models::relay::RelayUpdateInternal; +use error_stack::ResultExt; +use masking::{ExposeInterface, Secret}; +use serde::{self, Deserialize, Serialize}; +use time::PrimitiveDateTime; + +use crate::{router_data::ErrorResponse, router_response_types}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Relay { + pub id: id_type::RelayId, + pub connector_resource_id: String, + pub connector_id: id_type::MerchantConnectorAccountId, + pub profile_id: id_type::ProfileId, + pub merchant_id: id_type::MerchantId, + pub relay_type: enums::RelayType, + pub request_data: Option, + pub status: enums::RelayStatus, + pub connector_reference_id: Option, + pub error_code: Option, + pub error_message: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: PrimitiveDateTime, + pub response_data: Option, +} + +impl Relay { + pub fn new( + relay_request: &api_models::relay::RelayRequest, + merchant_id: &id_type::MerchantId, + profile_id: &id_type::ProfileId, + ) -> Self { + let relay_id = id_type::RelayId::generate(); + Self { + id: relay_id.clone(), + connector_resource_id: relay_request.connector_resource_id.clone(), + connector_id: relay_request.connector_id.clone(), + profile_id: profile_id.clone(), + merchant_id: merchant_id.clone(), + relay_type: common_enums::RelayType::Refund, + request_data: relay_request.data.clone().map(From::from), + status: common_enums::RelayStatus::Created, + connector_reference_id: None, + error_code: None, + error_message: None, + created_at: common_utils::date_time::now(), + modified_at: common_utils::date_time::now(), + response_data: None, + } + } +} + +impl From for RelayData { + fn from(relay: api_models::relay::RelayData) -> Self { + match relay { + api_models::relay::RelayData::Refund(relay_refund_request) => { + Self::Refund(RelayRefundData { + amount: relay_refund_request.amount, + currency: relay_refund_request.currency, + reason: relay_refund_request.reason, + }) + } + } + } +} + +impl RelayUpdate { + pub fn from( + response: Result, + ) -> Self { + match response { + Err(error) => Self::ErrorUpdate { + error_code: error.code, + error_message: error.message, + status: common_enums::RelayStatus::Failure, + }, + Ok(response) => Self::StatusUpdate { + connector_reference_id: Some(response.connector_refund_id), + status: common_enums::RelayStatus::from(response.refund_status), + }, + } + } +} + +impl From for api_models::relay::RelayResponse { + fn from(value: Relay) -> Self { + let error = value + .error_code + .zip(value.error_message) + .map( + |(error_code, error_message)| api_models::relay::RelayError { + code: error_code, + message: error_message, + }, + ); + + let data = value.request_data.map(|relay_data| match relay_data { + RelayData::Refund(relay_refund_request) => { + api_models::relay::RelayData::Refund(api_models::relay::RelayRefundRequest { + amount: relay_refund_request.amount, + currency: relay_refund_request.currency, + reason: relay_refund_request.reason, + }) + } + }); + Self { + id: value.id, + status: value.status, + error, + connector_resource_id: value.connector_resource_id, + connector_id: value.connector_id, + profile_id: value.profile_id, + relay_type: value.relay_type, + data, + connector_reference_id: value.connector_reference_id, + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case", untagged)] +pub enum RelayData { + Refund(RelayRefundData), +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RelayRefundData { + pub amount: MinorUnit, + pub currency: enums::Currency, + pub reason: Option, +} + +#[derive(Debug)] +pub enum RelayUpdate { + ErrorUpdate { + error_code: String, + error_message: String, + status: enums::RelayStatus, + }, + StatusUpdate { + connector_reference_id: Option, + status: common_enums::RelayStatus, + }, +} + +impl From for RelayUpdateInternal { + fn from(value: RelayUpdate) -> Self { + match value { + RelayUpdate::ErrorUpdate { + error_code, + error_message, + status, + } => Self { + error_code: Some(error_code), + error_message: Some(error_message), + connector_reference_id: None, + status: Some(status), + modified_at: common_utils::date_time::now(), + }, + RelayUpdate::StatusUpdate { + connector_reference_id, + status, + } => Self { + connector_reference_id, + status: Some(status), + error_code: None, + error_message: None, + modified_at: common_utils::date_time::now(), + }, + } + } +} + +#[async_trait::async_trait] +impl super::behaviour::Conversion for Relay { + type DstType = diesel_models::relay::Relay; + type NewDstType = diesel_models::relay::RelayNew; + + async fn convert(self) -> CustomResult { + Ok(diesel_models::relay::Relay { + id: self.id, + connector_resource_id: self.connector_resource_id, + connector_id: self.connector_id, + profile_id: self.profile_id, + merchant_id: self.merchant_id, + relay_type: self.relay_type, + request_data: self + .request_data + .map(|data| { + serde_json::to_value(data).change_context(ValidationError::InvalidValue { + message: "Failed while decrypting business profile data".to_string(), + }) + }) + .transpose()? + .map(Secret::new), + status: self.status, + connector_reference_id: self.connector_reference_id, + error_code: self.error_code, + error_message: self.error_message, + created_at: self.created_at, + modified_at: self.modified_at, + response_data: self.response_data, + }) + } + + async fn convert_back( + _state: &keymanager::KeyManagerState, + item: Self::DstType, + _key: &Secret>, + _key_manager_identifier: keymanager::Identifier, + ) -> CustomResult { + Ok(Self { + id: item.id, + connector_resource_id: item.connector_resource_id, + connector_id: item.connector_id, + profile_id: item.profile_id, + merchant_id: item.merchant_id, + relay_type: enums::RelayType::Refund, + request_data: item + .request_data + .map(|data| { + serde_json::from_value(data.expose()).change_context( + ValidationError::InvalidValue { + message: "Failed while decrypting business profile data".to_string(), + }, + ) + }) + .transpose()?, + status: item.status, + connector_reference_id: item.connector_reference_id, + error_code: item.error_code, + error_message: item.error_message, + created_at: item.created_at, + modified_at: item.modified_at, + response_data: item.response_data, + }) + } + + async fn construct_new(self) -> CustomResult { + Ok(diesel_models::relay::RelayNew { + id: self.id, + connector_resource_id: self.connector_resource_id, + connector_id: self.connector_id, + profile_id: self.profile_id, + merchant_id: self.merchant_id, + relay_type: self.relay_type, + request_data: self + .request_data + .map(|data| { + serde_json::to_value(data).change_context(ValidationError::InvalidValue { + message: "Failed while decrypting business profile data".to_string(), + }) + }) + .transpose()? + .map(Secret::new), + status: self.status, + connector_reference_id: self.connector_reference_id, + error_code: self.error_code, + error_message: self.error_message, + created_at: self.created_at, + modified_at: self.modified_at, + response_data: self.response_data, + }) + } +} 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/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index 55624af9f728..aa9fb2b5f1f0 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -486,6 +486,9 @@ pub struct BrowserInformation { pub ip_address: Option, pub accept_header: Option, pub user_agent: Option, + pub os_type: Option, + pub os_version: Option, + pub device_model: Option, } #[derive(Debug, Clone, Default, Serialize)] diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index c9e30c72c08e..2f068b5609b8 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, @@ -522,6 +526,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::RelayResponse, + api_models::enums::RelayType, + api_models::relay::RelayData, + api_models::relay::RelayRefundRequest, + api_models::enums::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 75c5ba3bfac2..42375a1c7646 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -645,7 +645,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::feature_matrix::ConnectorFeatureMatrixResponse, api_models::feature_matrix::SupportedPaymentMethod, 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/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/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index c20cc1aeb3aa..3006a73a3ee4 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -398,6 +398,9 @@ impl TryFrom<&TrustpayRouterData<&types::PaymentsAuthorizeRouterData>> for Trust accept_header: Some(browser_info.accept_header.unwrap_or("*".to_string())), user_agent: browser_info.user_agent, ip_address: browser_info.ip_address, + os_type: None, + os_version: None, + device_model: None, }; let params = get_mandatory_fields(item.router_data)?; let amount = item.amount.to_owned(); diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 823f0a4bf04f..e2784b968ad1 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -171,19 +171,19 @@ pub const DEFAULT_SDK_LAYOUT: &str = "tabs"; /// Vault Add request url #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub const ADD_VAULT_REQUEST_URL: &str = "/vault/add"; +pub const ADD_VAULT_REQUEST_URL: &str = "/api/v2/vault/add"; /// Vault Get Fingerprint request url #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub const VAULT_FINGERPRINT_REQUEST_URL: &str = "/fingerprint"; +pub const VAULT_FINGERPRINT_REQUEST_URL: &str = "/api/v2/vault/fingerprint"; /// Vault Retrieve request url #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub const VAULT_RETRIEVE_REQUEST_URL: &str = "/vault/retrieve"; +pub const VAULT_RETRIEVE_REQUEST_URL: &str = "/api/v2/vault/retrieve"; /// Vault Delete request url #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub const VAULT_DELETE_REQUEST_URL: &str = "/vault/delete"; +pub const VAULT_DELETE_REQUEST_URL: &str = "/api/v2/vault/delete"; /// Vault Header content type #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index f6fe1d85b5de..c22cecc20f9f 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -54,3 +54,5 @@ pub mod verify_connector; pub mod webhooks; pub mod unified_authentication_service; + +pub mod relay; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 8e6d74c83753..ccb563b06102 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1401,6 +1401,10 @@ impl ConnectorAuthTypeAndMetadataValidation<'_> { itaubank::transformers::ItaubankAuthType::try_from(self.auth_type)?; Ok(()) } + api_enums::Connector::Jpmorgan => { + jpmorgan::transformers::JpmorganAuthType::try_from(self.auth_type)?; + Ok(()) + } api_enums::Connector::Klarna => { klarna::transformers::KlarnaAuthType::try_from(self.auth_type)?; klarna::transformers::KlarnaConnectorMetadataObject::try_from( diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 61dc70ac4799..6cdadf07320c 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -62,7 +62,7 @@ use crate::{ types::{ api::{self, payment_methods::PaymentMethodCreateExt}, payment_methods as pm_types, - storage::PaymentMethodListContext, + storage::{ephemeral_key, PaymentMethodListContext}, }, utils::ext_traits::OptionExt, }; @@ -851,21 +851,23 @@ pub async fn create_payment_method( let db = &*state.store; let merchant_id = merchant_account.get_id(); let customer_id = req.customer_id.to_owned(); + let key_manager_state = &(state).into(); db.find_customer_by_global_id( - &(state.into()), + key_manager_state, &customer_id, merchant_account.get_id(), key_store, merchant_account.storage_scheme, ) .await - .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; - let key_manager_state = state.into(); + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) + .attach_printable("Customer not found for the payment method")?; + let payment_method_billing_address: Option>> = req .billing .clone() - .async_map(|billing| cards::create_encrypted_data(&key_manager_state, key_store, billing)) + .async_map(|billing| cards::create_encrypted_data(key_manager_state, key_store, billing)) .await .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) @@ -877,7 +879,7 @@ pub async fn create_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; - let payment_method = create_payment_method_for_intent( + let (payment_method, ephemeral_key) = create_payment_method_for_intent( state, req.metadata.clone(), &customer_id, @@ -902,14 +904,15 @@ pub async fn create_payment_method( .await; let response = match vaulting_result { - Ok(resp) => { + Ok((vaulting_resp, fingerprint_id)) => { let pm_update = create_pm_additional_data_update( &payment_method_data, state, key_store, - Some(resp.vault_id.get_string_repr().clone()), + Some(vaulting_resp.vault_id.get_string_repr().clone()), Some(req.payment_method_type), Some(req.payment_method_subtype), + Some(fingerprint_id), ) .await .attach_printable("Unable to create Payment method data")?; @@ -926,7 +929,10 @@ pub async fn create_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; - let resp = pm_transforms::generate_payment_method_response(&payment_method)?; + let resp = pm_transforms::generate_payment_method_response( + &payment_method, + Some(ephemeral_key), + )?; Ok(resp) } @@ -964,21 +970,23 @@ pub async fn payment_method_intent_create( let db = &*state.store; let merchant_id = merchant_account.get_id(); let customer_id = req.customer_id.to_owned(); + let key_manager_state = &(state).into(); db.find_customer_by_global_id( - &(state.into()), + key_manager_state, &customer_id, merchant_account.get_id(), key_store, merchant_account.storage_scheme, ) .await - .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; - let key_manager_state = state.into(); + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) + .attach_printable("Customer not found for the payment method")?; + let payment_method_billing_address: Option>> = req .billing .clone() - .async_map(|billing| cards::create_encrypted_data(&key_manager_state, key_store, billing)) + .async_map(|billing| cards::create_encrypted_data(key_manager_state, key_store, billing)) .await .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) @@ -991,7 +999,7 @@ pub async fn payment_method_intent_create( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; - let payment_method = create_payment_method_for_intent( + let (payment_method, ephemeral_key) = create_payment_method_for_intent( state, req.metadata.clone(), &customer_id, @@ -1004,7 +1012,8 @@ pub async fn payment_method_intent_create( .await .attach_printable("Failed to add Payment method to DB")?; - let resp = pm_transforms::generate_payment_method_response(&payment_method)?; + let resp = + pm_transforms::generate_payment_method_response(&payment_method, Some(ephemeral_key))?; Ok(services::ApplicationResponse::Json(resp)) } @@ -1018,10 +1027,10 @@ pub async fn payment_method_intent_confirm( key_store: &domain::MerchantKeyStore, pm_id: String, ) -> RouterResponse { + let key_manager_state = &(state).into(); req.validate()?; let db = &*state.store; - let client_secret = req.client_secret.clone(); let pm_id = id_type::GlobalPaymentMethodId::generate_from_string(pm_id) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; @@ -1037,11 +1046,6 @@ pub async fn payment_method_intent_confirm( .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) .attach_printable("Unable to find payment method")?; - when( - cards::authenticate_pm_client_secret_and_check_expiry(&client_secret, &payment_method)?, - || Err(errors::ApiErrorResponse::ClientSecretExpired), - )?; - when( payment_method.status != enums::PaymentMethodStatus::AwaitingData, || { @@ -1054,7 +1058,7 @@ pub async fn payment_method_intent_confirm( let customer_id = payment_method.customer_id.to_owned(); db.find_customer_by_global_id( - &(state.into()), + key_manager_state, &customer_id, merchant_account.get_id(), key_store, @@ -1075,14 +1079,15 @@ pub async fn payment_method_intent_confirm( .await; let response = match vaulting_result { - Ok(resp) => { + Ok((vaulting_resp, fingerprint_id)) => { let pm_update = create_pm_additional_data_update( &payment_method_data, state, key_store, - Some(resp.vault_id.get_string_repr().clone()), + Some(vaulting_resp.vault_id.get_string_repr().clone()), Some(req.payment_method_type), Some(req.payment_method_subtype), + Some(fingerprint_id), ) .await .attach_printable("Unable to create Payment method data")?; @@ -1099,7 +1104,7 @@ pub async fn payment_method_intent_confirm( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; - let resp = pm_transforms::generate_payment_method_response(&payment_method)?; + let resp = pm_transforms::generate_payment_method_response(&payment_method, None)?; Ok(resp) } @@ -1153,7 +1158,6 @@ pub async fn create_payment_method_in_db( card_scheme: Option, ) -> errors::CustomResult { let db = &*state.store; - let client_secret = pm_types::PaymentMethodClientSecret::generate(&payment_method_id); let current_time = common_utils::date_time::now(); let response = db @@ -1170,7 +1174,7 @@ pub async fn create_payment_method_in_db( payment_method_data, connector_mandate_details, customer_acceptance, - client_secret: Some(client_secret), + client_secret: None, status: status.unwrap_or(enums::PaymentMethodStatus::Active), network_transaction_id: network_transaction_id.to_owned(), created_at: current_time, @@ -1205,9 +1209,18 @@ pub async fn create_payment_method_for_intent( key_store: &domain::MerchantKeyStore, storage_scheme: enums::MerchantStorageScheme, payment_method_billing_address: crypto::OptionalEncryptableValue, -) -> errors::CustomResult { +) -> errors::CustomResult<(domain::PaymentMethod, Secret), errors::ApiErrorResponse> { let db = &*state.store; - let client_secret = pm_types::PaymentMethodClientSecret::generate(&payment_method_id); + let ephemeral_key = payment_helpers::create_ephemeral_key( + state, + customer_id, + merchant_id, + ephemeral_key::ResourceType::PaymentMethod, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to create ephemeral_key")?; + let current_time = common_utils::date_time::now(); let response = db @@ -1224,7 +1237,7 @@ pub async fn create_payment_method_for_intent( payment_method_data: None, connector_mandate_details: None, customer_acceptance: None, - client_secret: Some(client_secret), + client_secret: None, status: enums::PaymentMethodStatus::AwaitingData, network_transaction_id: None, created_at: current_time, @@ -1244,7 +1257,7 @@ pub async fn create_payment_method_for_intent( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to add payment method in db")?; - Ok(response) + Ok((response, ephemeral_key.secret)) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -1255,15 +1268,16 @@ pub async fn create_pm_additional_data_update( vault_id: Option, payment_method_type: Option, payment_method_subtype: Option, + vault_fingerprint_id: Option, ) -> RouterResult { let card = match pmd { pm_types::PaymentMethodVaultingData::Card(card) => { api::PaymentMethodsData::Card(card.clone().into()) } }; - let key_manager_state = state.into(); + let key_manager_state = &(state).into(); let pmd: Encryptable> = - cards::create_encrypted_data(&key_manager_state, key_store, card) + cards::create_encrypted_data(key_manager_state, key_store, card) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to encrypt Payment method data")?; @@ -1277,6 +1291,7 @@ pub async fn create_pm_additional_data_update( network_token_requestor_reference_id: None, network_token_locker_id: None, network_token_payment_method_data: None, + locker_fingerprint_id: vault_fingerprint_id, }; Ok(pm_update) @@ -1290,7 +1305,7 @@ pub async fn vault_payment_method( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, existing_vault_id: Option, -) -> RouterResult { +) -> RouterResult<(pm_types::AddVaultResponse, String)> { let db = &*state.store; // get fingerprint_id from vault @@ -1301,17 +1316,16 @@ pub async fn vault_payment_method( // throw back error if payment method is duplicated when( - Some( - db.find_payment_method_by_fingerprint_id( - &(state.into()), - key_store, - &fingerprint_id_from_vault, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to find payment method by fingerprint_id")?, + db.find_payment_method_by_fingerprint_id( + &(state.into()), + key_store, + &fingerprint_id_from_vault, ) - .is_some(), + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to find payment method by fingerprint_id") + .inspect_err(|e| logger::error!("Vault Fingerprint_id error: {:?}", e)) + .is_ok(), || { Err(report!(errors::ApiErrorResponse::DuplicatePaymentMethod) .attach_printable("Cannot vault duplicate payment method")) @@ -1324,7 +1338,7 @@ pub async fn vault_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to add payment method in vault")?; - Ok(resp_from_vault) + Ok((resp_from_vault, fingerprint_id_from_vault)) } #[cfg(all( @@ -1754,6 +1768,11 @@ pub async fn retrieve_payment_method( .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + when( + payment_method.status == enums::PaymentMethodStatus::Inactive, + || Err(errors::ApiErrorResponse::PaymentMethodNotFound), + )?; + let pmd = payment_method .payment_method_data .clone() @@ -1774,7 +1793,7 @@ pub async fn retrieve_payment_method( created: Some(payment_method.created_at), recurring_enabled: false, last_used_at: Some(payment_method.last_used_at), - client_secret: payment_method.client_secret.clone(), + ephemeral_key: None, payment_method_data: pmd, }; @@ -1822,16 +1841,12 @@ pub async fn update_payment_method( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to retrieve payment method from vault")? - .data - .expose() - .parse_struct("PaymentMethodVaultingData") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to parse PaymentMethodVaultingData")?; + .data; let vault_request_data = pm_transforms::generate_pm_vaulting_req_from_update_request(pmd, req.payment_method_data); - let vaulting_response = vault_payment_method( + let (vaulting_response, fingerprint_id) = vault_payment_method( &state, &vault_request_data, &merchant_account, @@ -1848,6 +1863,7 @@ pub async fn update_payment_method( Some(vaulting_response.vault_id.get_string_repr().clone()), payment_method.get_payment_method_type(), payment_method.get_payment_method_subtype(), + Some(fingerprint_id), ) .await .attach_printable("Unable to create Payment method data")?; @@ -1864,7 +1880,7 @@ pub async fn update_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; - let response = pm_transforms::generate_payment_method_response(&payment_method)?; + let response = pm_transforms::generate_payment_method_response(&payment_method, None)?; // Add a PT task to handle payment_method delete from vault @@ -1896,6 +1912,11 @@ pub async fn delete_payment_method( .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; + when( + payment_method.status == enums::PaymentMethodStatus::Inactive, + || Err(errors::ApiErrorResponse::PaymentMethodNotFound), + )?; + let vault_id = payment_method .locker_id .clone() diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index c3fbfd8afbff..ce95096a0fb3 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -551,6 +551,7 @@ pub fn generate_pm_vaulting_req_from_update_request( #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub fn generate_payment_method_response( pm: &domain::PaymentMethod, + ephemeral_key: Option>, ) -> errors::RouterResult { let pmd = pm .payment_method_data @@ -572,7 +573,7 @@ pub fn generate_payment_method_response( created: Some(pm.created_at), recurring_enabled: false, last_used_at: Some(pm.last_used_at), - client_secret: pm.client_secret.clone(), + ephemeral_key, payment_method_data: pmd, }; diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index fcff711f6c9a..65ef16cb4496 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -1186,6 +1186,7 @@ async fn create_vault_request( jwekey: &settings::Jwekey, locker: &settings::Locker, payload: Vec, + tenant_id: id_type::TenantId, ) -> CustomResult { let private_key = jwekey.vault_private_key.peek().as_bytes(); @@ -1206,6 +1207,10 @@ async fn create_vault_request( headers::CONTENT_TYPE, consts::VAULT_HEADER_CONTENT_TYPE.into(), ); + request.add_header( + headers::X_TENANT_ID, + tenant_id.get_string_repr().to_owned().into(), + ); request.set_body(request::RequestContent::Json(Box::new(jwe_payload))); Ok(request) } @@ -1219,7 +1224,9 @@ pub async fn call_to_vault( let locker = &state.conf.locker; let jwekey = state.conf.jwekey.get_inner(); - let request = create_vault_request::(jwekey, locker, payload).await?; + let request = + create_vault_request::(jwekey, locker, payload, state.tenant.tenant_id.to_owned()) + .await?; let response = services::call_connector_api(state, request, V::get_vaulting_flow_name()) .await .change_context(errors::VaultError::VaultAPIError); diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index f758dc0b0e55..e68bd09ba462 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, @@ -1519,8 +1519,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, @@ -1534,13 +1538,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( @@ -1569,12 +1574,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, @@ -6640,7 +6641,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(), }, ))) @@ -7800,3 +7801,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/helpers.rs b/crates/router/src/core/payments/helpers.rs index 78cfdda33b49..c243686a76cd 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,11 +1,15 @@ use std::{borrow::Cow, str::FromStr}; +#[cfg(feature = "v2")] +use api_models::ephemeral_key::EphemeralKeyResponse; use api_models::{ mandates::RecurringDetails, payments::{additional_info as payment_additional_types, RequestSurchargeDetails}, }; use base64::Engine; use common_enums::ConnectorType; +#[cfg(feature = "v2")] +use common_utils::id_type::GenerateId; use common_utils::{ crypto::Encryptable, ext_traits::{AsyncExt, ByteSliceExt, Encode, ValueExt}, @@ -40,6 +44,8 @@ use openssl::{ pkey::PKey, symm::{decrypt_aead, Cipher}, }; +#[cfg(feature = "v2")] +use redis_interface::errors::RedisError; use router_env::{instrument, logger, tracing}; use uuid::Uuid; use x509_parser::parse_x509_certificate; @@ -48,8 +54,6 @@ use super::{ operations::{BoxedOperation, Operation, PaymentResponse}, CustomerDetails, PaymentData, }; -#[cfg(feature = "v2")] -use crate::core::admin as core_admin; use crate::{ configs::settings::{ConnectorRequestReferenceIdConfig, TempLockerEnableConfig}, connector, @@ -84,6 +88,8 @@ use crate::{ OptionExt, StringExt, }, }; +#[cfg(feature = "v2")] +use crate::{core::admin as core_admin, headers}; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use crate::{ core::payment_methods::cards::create_encrypted_data, types::storage::CustomerUpdate::Update, @@ -3021,6 +3027,7 @@ pub fn make_merchant_url_with_response( Ok(merchant_url_with_response.to_string()) } +#[cfg(feature = "v1")] pub async fn make_ephemeral_key( state: SessionState, customer_id: id_type::CustomerId, @@ -3043,6 +3050,80 @@ pub async fn make_ephemeral_key( Ok(services::ApplicationResponse::Json(ek)) } +#[cfg(feature = "v2")] +pub async fn make_ephemeral_key( + state: SessionState, + customer_id: id_type::GlobalCustomerId, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + headers: &actix_web::http::header::HeaderMap, +) -> errors::RouterResponse { + let db = &state.store; + let key_manager_state = &((&state).into()); + db.find_customer_by_global_id( + key_manager_state, + &customer_id, + merchant_account.get_id(), + &key_store, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + + let resource_type = services::authentication::get_header_value_by_key( + headers::X_RESOURCE_TYPE.to_string(), + headers, + )? + .map(ephemeral_key::ResourceType::from_str) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: format!("`{}` header is invalid", headers::X_RESOURCE_TYPE), + })? + .get_required_value("ResourceType") + .attach_printable("Failed to convert ResourceType from string")?; + + let ephemeral_key = create_ephemeral_key( + &state, + &customer_id, + merchant_account.get_id(), + resource_type, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to create ephemeral key")?; + + let response = EphemeralKeyResponse::foreign_from(ephemeral_key); + Ok(services::ApplicationResponse::Json(response)) +} + +#[cfg(feature = "v2")] +pub async fn create_ephemeral_key( + state: &SessionState, + customer_id: &id_type::GlobalCustomerId, + merchant_id: &id_type::MerchantId, + resource_type: ephemeral_key::ResourceType, +) -> RouterResult { + use common_utils::generate_time_ordered_id; + + let store = &state.store; + let id = id_type::EphemeralKeyId::generate(); + let secret = masking::Secret::new(generate_time_ordered_id("epk")); + let ephemeral_key = ephemeral_key::EphemeralKeyTypeNew { + id, + customer_id: customer_id.to_owned(), + merchant_id: merchant_id.to_owned(), + secret, + resource_type, + }; + let ephemeral_key = store + .create_ephemeral_key(ephemeral_key, state.conf.eph_key.validity) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to create ephemeral key")?; + Ok(ephemeral_key) +} + +#[cfg(feature = "v1")] pub async fn delete_ephemeral_key( state: SessionState, ek_id: String, @@ -3056,6 +3137,29 @@ pub async fn delete_ephemeral_key( Ok(services::ApplicationResponse::Json(ek)) } +#[cfg(feature = "v2")] +pub async fn delete_ephemeral_key( + state: SessionState, + ephemeral_key_id: String, +) -> errors::RouterResponse { + let db = state.store.as_ref(); + let ephemeral_key = db + .delete_ephemeral_key(&ephemeral_key_id) + .await + .map_err(|err| match err.current_context() { + errors::StorageError::ValueNotFound(_) => { + err.change_context(errors::ApiErrorResponse::GenericNotFoundError { + message: "Ephemeral Key not found".to_string(), + }) + } + _ => err.change_context(errors::ApiErrorResponse::InternalServerError), + }) + .attach_printable("Unable to delete ephemeral key")?; + + let response = EphemeralKeyResponse::foreign_from(ephemeral_key); + Ok(services::ApplicationResponse::Json(response)) +} + pub fn make_pg_redirect_response( payment_id: id_type::PaymentId, response: &api::PaymentsResponse, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index a7e60274d135..38911bb2d6df 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; @@ -439,7 +442,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 92c8240d187c..403ee5443148 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 { @@ -133,7 +118,7 @@ impl GetTracker, PaymentsRetriev merchant_account: &domain::MerchantAccount, _profile: &domain::Profile, key_store: &domain::MerchantKeyStore, - header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, ) -> RouterResult>> { let db = &*state.store; let key_manager_state = &state.into(); @@ -231,12 +216,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, @@ -248,12 +233,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 => { @@ -294,15 +279,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/routing.rs b/crates/router/src/core/payments/routing.rs index 746b6b7fb409..6d3c0973283c 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -1343,6 +1343,7 @@ pub async fn perform_success_based_routing( success_based_routing_configs, success_based_routing_config_params, routable_connectors, + state.get_grpc_headers(), ) .await .change_context(errors::RoutingError::SuccessRateCalculationError) diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 1e39831af174..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,14 +1439,17 @@ 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, @@ -1261,70 +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, @@ -2622,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, @@ -2841,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, + }) } } @@ -3360,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, @@ -3478,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, @@ -3640,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): ( @@ -3651,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, } } @@ -3672,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): ( @@ -3684,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() @@ -3695,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/relay.rs b/crates/router/src/core/relay.rs new file mode 100644 index 000000000000..812709269d83 --- /dev/null +++ b/crates/router/src/core/relay.rs @@ -0,0 +1,177 @@ +use api_models::relay as relay_models; +use common_utils::{self, ext_traits::OptionExt, id_type}; +use error_stack::ResultExt; + +use super::errors::{self, ConnectorErrorExt, RouterResponse, RouterResult, StorageErrorExt}; +use crate::{ + core::payments, + routes::SessionState, + services, + types::{ + api::{self}, + domain, + }, +}; + +pub mod utils; + +pub async fn relay( + state: SessionState, + merchant_account: domain::MerchantAccount, + profile_id_optional: Option, + key_store: domain::MerchantKeyStore, + req: relay_models::RelayRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + let key_manager_state = &(&state).into(); + let merchant_id = merchant_account.get_id(); + let connector_id = &req.connector_id; + + let profile_id_from_auth_layer = profile_id_optional + .get_required_value("ProfileId") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "profile id", + })?; + + let profile = db + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + merchant_id, + &profile_id_from_auth_layer, + ) + .await + .change_context(errors::ApiErrorResponse::ProfileNotFound { + id: profile_id_from_auth_layer.get_string_repr().to_owned(), + })?; + + #[cfg(feature = "v1")] + let connector_account = db + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + key_manager_state, + merchant_id, + connector_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: connector_id.get_string_repr().to_string(), + })?; + + #[cfg(feature = "v2")] + let connector_account = db + .find_merchant_connector_account_by_id(key_manager_state, connector_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: connector_id.get_string_repr().to_string(), + })?; + + validate_relay_refund_request(&req).attach_printable("Invalid relay refund request")?; + + let relay_domain = + hyperswitch_domain_models::relay::Relay::new(&req, merchant_id, profile.get_id()); + + let relay_record = db + .insert_relay(key_manager_state, &key_store, relay_domain) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to insert a relay record in db")?; + + let relay_response = match req.relay_type { + common_enums::RelayType::Refund => { + Box::pin(relay_refund( + &state, + merchant_account, + connector_account, + &relay_record, + )) + .await? + } + }; + + let relay_update_record = db + .update_relay(key_manager_state, &key_store, relay_record, relay_response) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let response = relay_models::RelayResponse::from(relay_update_record); + + Ok(hyperswitch_domain_models::api::ApplicationResponse::Json( + response, + )) +} + +pub async fn relay_refund( + state: &SessionState, + merchant_account: domain::MerchantAccount, + connector_account: domain::MerchantConnectorAccount, + relay_record: &hyperswitch_domain_models::relay::Relay, +) -> RouterResult { + let connector_id = &relay_record.connector_id; + + let merchant_id = merchant_account.get_id(); + + let connector_data = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &connector_account.connector_name, + api::GetToken::Connector, + Some(connector_id.clone()), + )?; + + let connector_integration: services::BoxedRefundConnectorIntegrationInterface< + api::Execute, + hyperswitch_domain_models::router_request_types::RefundsData, + hyperswitch_domain_models::router_response_types::RefundsResponseData, + > = connector_data.connector.get_connector_integration(); + + let router_data = utils::construct_relay_refund_router_data( + state, + &connector_account.connector_name, + merchant_id, + &connector_account, + relay_record, + ) + .await?; + + let router_data_res = services::execute_connector_processing_step( + state, + connector_integration, + &router_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await + .to_refund_failed_response()?; + + let relay_response = + hyperswitch_domain_models::relay::RelayUpdate::from(router_data_res.response); + + Ok(relay_response) +} + +// validate relay request +pub fn validate_relay_refund_request( + relay_request: &relay_models::RelayRequest, +) -> RouterResult<()> { + match (relay_request.relay_type, &relay_request.data) { + (common_enums::RelayType::Refund, Some(relay_models::RelayData::Refund(ref_data))) => { + validate_relay_refund_data(ref_data) + } + (common_enums::RelayType::Refund, None) => { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Relay data is required for refund relay".to_string(), + })? + } + } +} + +pub fn validate_relay_refund_data( + refund_data: &relay_models::RelayRefundRequest, +) -> RouterResult<()> { + if refund_data.amount.get_amount_as_i64() <= 0 { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Amount should be greater than 0".to_string(), + })? + } + Ok(()) +} diff --git a/crates/router/src/core/relay/utils.rs b/crates/router/src/core/relay/utils.rs new file mode 100644 index 000000000000..0753be1ec435 --- /dev/null +++ b/crates/router/src/core/relay/utils.rs @@ -0,0 +1,139 @@ +use std::str::FromStr; + +use common_utils::{ext_traits::OptionExt, id_type}; +use error_stack::ResultExt; +use hyperswitch_domain_models::{router_data::ErrorResponse, types}; + +use crate::{ + core::payments, + db::{ + domain, + errors::{self, RouterResult}, + }, + routes::SessionState, +}; + +const IRRELEVANT_PAYMENT_INTENT_ID: &str = "irrelevant_payment_intent_id"; + +const IRRELEVANT_PAYMENT_ATTEMPT_ID: &str = "irrelevant_payment_attempt_id"; + +pub async fn construct_relay_refund_router_data<'a, F>( + state: &'a SessionState, + connector_name: &str, + merchant_id: &id_type::MerchantId, + connector_account: &domain::MerchantConnectorAccount, + relay_record: &hyperswitch_domain_models::relay::Relay, +) -> RouterResult> { + let connector_auth_type = connector_account + .get_connector_account_details() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while parsing value for ConnectorAuthType")?; + + let webhook_url = Some(payments::helpers::create_webhook_url( + &state.base_url.clone(), + merchant_id, + connector_name, + )); + + let supported_connector = &state + .conf + .multiple_api_version_supported_connectors + .supported_connectors; + + let connector_enum = api_models::enums::Connector::from_str(connector_name) + .change_context(errors::ConnectorError::InvalidConnectorName) + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "connector", + }) + .attach_printable_lazy(|| format!("unable to parse connector name {connector_name:?}"))?; + + let connector_api_version = if supported_connector.contains(&connector_enum) { + state + .store + .find_config_by_key(&format!("connector_api_version_{connector_name}")) + .await + .map(|value| value.config) + .ok() + } else { + None + }; + + let hyperswitch_domain_models::relay::RelayData::Refund(relay_refund_data) = relay_record + .request_data + .clone() + .get_required_value("refund relay data") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to obtain relay data to construct relay refund data")?; + + let relay_id_string = relay_record.id.get_string_repr().to_string(); + + let router_data = hyperswitch_domain_models::router_data::RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_id.clone(), + customer_id: None, + connector: connector_name.to_string(), + payment_id: IRRELEVANT_PAYMENT_INTENT_ID.to_string(), + attempt_id: IRRELEVANT_PAYMENT_ATTEMPT_ID.to_string(), + status: common_enums::AttemptStatus::Charged, + payment_method: common_enums::PaymentMethod::default(), + connector_auth_type, + description: None, + return_url: None, + address: hyperswitch_domain_models::payment_address::PaymentAddress::default(), + auth_type: common_enums::AuthenticationType::default(), + connector_meta_data: None, + connector_wallets_details: None, + amount_captured: None, + payment_method_status: None, + minor_amount_captured: None, + request: hyperswitch_domain_models::router_request_types::RefundsData { + refund_id: relay_id_string.clone(), + connector_transaction_id: relay_record.connector_resource_id.clone(), + refund_amount: relay_refund_data.amount.get_amount_as_i64(), + minor_refund_amount: relay_refund_data.amount, + currency: relay_refund_data.currency, + payment_amount: relay_refund_data.amount.get_amount_as_i64(), + minor_payment_amount: relay_refund_data.amount, + webhook_url, + connector_metadata: None, + reason: relay_refund_data.reason, + connector_refund_id: relay_record.connector_reference_id.clone(), + browser_info: None, + split_refunds: None, + integrity_object: None, + refund_status: common_enums::RefundStatus::from(relay_record.status), + }, + + response: Err(ErrorResponse::default()), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + recurring_mandate_payment_data: None, + preprocessing_id: None, + connector_request_reference_id: relay_id_string.clone(), + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + test_mode: connector_account.get_connector_test_mode(), + payment_method_balance: None, + connector_api_version, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + frm_metadata: None, + refund_id: Some(relay_id_string), + dispute_id: None, + connector_response: None, + integrity_check: Ok(()), + additional_merchant_data: None, + header_payload: None, + connector_mandate_request_reference_id: None, + authentication_id: None, + psd2_sca_exemption_type: None, + }; + + Ok(router_data) +} diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 36bcb233d02a..5fc54fe96e52 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -1408,7 +1408,10 @@ pub async fn success_based_routing_update_configs( .as_ref() .async_map(|sr_client| async { sr_client - .invalidate_success_rate_routing_keys(prefix_of_dynamic_routing_keys) + .invalidate_success_rate_routing_keys( + prefix_of_dynamic_routing_keys, + state.get_grpc_headers(), + ) .await .change_context(errors::ApiErrorResponse::GenericNotFoundError { message: "Failed to invalidate the routing keys".to_string(), diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index 2abb5e64cd4b..489c3122a8e9 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -719,6 +719,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( success_based_routing_configs.clone(), success_based_routing_config_params.clone(), routable_connectors.clone(), + state.get_grpc_headers(), ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -855,6 +856,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( }, payment_status_attribute == common_enums::AttemptStatus::Charged, )], + state.get_grpc_headers(), ) .await .change_context(errors::ApiErrorResponse::InternalServerError) 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/db.rs b/crates/router/src/db.rs index fbb75b6af603..eef84f6ff983 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -30,6 +30,7 @@ pub mod organization; pub mod payment_link; pub mod payment_method; pub mod refund; +pub mod relay; pub mod reverse_lookup; pub mod role; pub mod routing_algorithm; @@ -131,6 +132,7 @@ pub trait StorageInterface: + user_authentication_method::UserAuthenticationMethodInterface + authentication::AuthenticationInterface + generic_link::GenericLinkInterface + + relay::RelayInterface + user::theme::ThemeInterface + 'static { diff --git a/crates/router/src/db/ephemeral_key.rs b/crates/router/src/db/ephemeral_key.rs index a1d43e83b871..a77995e4e58e 100644 --- a/crates/router/src/db/ephemeral_key.rs +++ b/crates/router/src/db/ephemeral_key.rs @@ -1,5 +1,9 @@ +#[cfg(feature = "v2")] +use common_utils::id_type; use time::ext::NumericalDuration; +#[cfg(feature = "v2")] +use crate::types::storage::ephemeral_key::{EphemeralKeyType, EphemeralKeyTypeNew, ResourceType}; use crate::{ core::errors::{self, CustomResult}, db::MockDb, @@ -8,30 +12,64 @@ use crate::{ #[async_trait::async_trait] pub trait EphemeralKeyInterface { + #[cfg(feature = "v1")] async fn create_ephemeral_key( &self, _ek: EphemeralKeyNew, _validity: i64, ) -> CustomResult; + + #[cfg(feature = "v2")] + async fn create_ephemeral_key( + &self, + _ek: EphemeralKeyTypeNew, + _validity: i64, + ) -> CustomResult; + + #[cfg(feature = "v1")] async fn get_ephemeral_key( &self, _key: &str, ) -> CustomResult; + + #[cfg(feature = "v2")] + async fn get_ephemeral_key( + &self, + _key: &str, + ) -> CustomResult; + + #[cfg(feature = "v1")] async fn delete_ephemeral_key( &self, _id: &str, ) -> CustomResult; + + #[cfg(feature = "v2")] + async fn delete_ephemeral_key( + &self, + _id: &str, + ) -> CustomResult; } mod storage { use common_utils::date_time; + #[cfg(feature = "v2")] + use common_utils::id_type; use error_stack::ResultExt; + #[cfg(feature = "v2")] + use masking::PeekInterface; + #[cfg(feature = "v2")] + use redis_interface::errors::RedisError; use redis_interface::HsetnxReply; use router_env::{instrument, tracing}; use storage_impl::redis::kv_store::RedisConnInterface; use time::ext::NumericalDuration; use super::EphemeralKeyInterface; + #[cfg(feature = "v2")] + use crate::types::storage::ephemeral_key::{ + EphemeralKeyType, EphemeralKeyTypeNew, ResourceType, + }; use crate::{ core::errors::{self, CustomResult}, services::Store, @@ -40,6 +78,7 @@ mod storage { #[async_trait::async_trait] impl EphemeralKeyInterface for Store { + #[cfg(feature = "v1")] #[instrument(skip_all)] async fn create_ephemeral_key( &self, @@ -94,6 +133,68 @@ mod storage { Err(er) => Err(er).change_context(errors::StorageError::KVError), } } + + #[cfg(feature = "v2")] + #[instrument(skip_all)] + async fn create_ephemeral_key( + &self, + new: EphemeralKeyTypeNew, + validity: i64, + ) -> CustomResult { + let created_at = date_time::now(); + let expires = created_at.saturating_add(validity.hours()); + let id_key = new.id.generate_redis_key(); + + let created_ephemeral_key = EphemeralKeyType { + id: new.id, + created_at, + expires, + customer_id: new.customer_id, + merchant_id: new.merchant_id, + secret: new.secret, + resource_type: new.resource_type, + }; + let secret_key = created_ephemeral_key.generate_secret_key(); + + match self + .get_redis_conn() + .map_err(Into::::into)? + .serialize_and_set_multiple_hash_field_if_not_exist( + &[ + (&secret_key, &created_ephemeral_key), + (&id_key, &created_ephemeral_key), + ], + "ephkey", + None, + ) + .await + { + Ok(v) if v.contains(&HsetnxReply::KeyNotSet) => { + Err(errors::StorageError::DuplicateValue { + entity: "ephemeral key", + key: None, + } + .into()) + } + Ok(_) => { + let expire_at = expires.assume_utc().unix_timestamp(); + self.get_redis_conn() + .map_err(Into::::into)? + .set_expire_at(&secret_key, expire_at) + .await + .change_context(errors::StorageError::KVError)?; + self.get_redis_conn() + .map_err(Into::::into)? + .set_expire_at(&id_key, expire_at) + .await + .change_context(errors::StorageError::KVError)?; + Ok(created_ephemeral_key) + } + Err(er) => Err(er).change_context(errors::StorageError::KVError), + } + } + + #[cfg(feature = "v1")] #[instrument(skip_all)] async fn get_ephemeral_key( &self, @@ -106,6 +207,22 @@ mod storage { .await .change_context(errors::StorageError::KVError) } + + #[cfg(feature = "v2")] + #[instrument(skip_all)] + async fn get_ephemeral_key( + &self, + key: &str, + ) -> CustomResult { + let key = format!("epkey_{key}"); + self.get_redis_conn() + .map_err(Into::::into)? + .get_hash_field_and_deserialize(&key, "ephkey", "EphemeralKeyType") + .await + .change_context(errors::StorageError::KVError) + } + + #[cfg(feature = "v1")] async fn delete_ephemeral_key( &self, id: &str, @@ -125,11 +242,45 @@ mod storage { .change_context(errors::StorageError::KVError)?; Ok(ek) } + + #[cfg(feature = "v2")] + async fn delete_ephemeral_key( + &self, + id: &str, + ) -> CustomResult { + let ephemeral_key = self.get_ephemeral_key(id).await?; + let redis_id_key = ephemeral_key.id.generate_redis_key(); + let secret_key = ephemeral_key.generate_secret_key(); + + self.get_redis_conn() + .map_err(Into::::into)? + .delete_key(&redis_id_key) + .await + .map_err(|err| match err.current_context() { + RedisError::NotFound => { + err.change_context(errors::StorageError::ValueNotFound(redis_id_key)) + } + _ => err.change_context(errors::StorageError::KVError), + })?; + + self.get_redis_conn() + .map_err(Into::::into)? + .delete_key(&secret_key) + .await + .map_err(|err| match err.current_context() { + RedisError::NotFound => { + err.change_context(errors::StorageError::ValueNotFound(secret_key)) + } + _ => err.change_context(errors::StorageError::KVError), + })?; + Ok(ephemeral_key) + } } } #[async_trait::async_trait] impl EphemeralKeyInterface for MockDb { + #[cfg(feature = "v1")] async fn create_ephemeral_key( &self, ek: EphemeralKeyNew, @@ -150,6 +301,17 @@ impl EphemeralKeyInterface for MockDb { ephemeral_keys.push(ephemeral_key.clone()); Ok(ephemeral_key) } + + #[cfg(feature = "v2")] + async fn create_ephemeral_key( + &self, + ek: EphemeralKeyTypeNew, + validity: i64, + ) -> CustomResult { + todo!() + } + + #[cfg(feature = "v1")] async fn get_ephemeral_key( &self, key: &str, @@ -167,6 +329,16 @@ impl EphemeralKeyInterface for MockDb { ), } } + + #[cfg(feature = "v2")] + async fn get_ephemeral_key( + &self, + key: &str, + ) -> CustomResult { + todo!() + } + + #[cfg(feature = "v1")] async fn delete_ephemeral_key( &self, id: &str, @@ -181,4 +353,12 @@ impl EphemeralKeyInterface for MockDb { ); } } + + #[cfg(feature = "v2")] + async fn delete_ephemeral_key( + &self, + id: &str, + ) -> CustomResult { + todo!() + } } diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index f49173b35496..4155bbf3ff9a 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -6,6 +6,8 @@ use common_utils::{ id_type, types::{keymanager::KeyManagerState, theme::ThemeLineage}, }; +#[cfg(feature = "v2")] +use diesel_models::ephemeral_key::{EphemeralKeyType, EphemeralKeyTypeNew}; use diesel_models::{ enums, enums::ProcessTrackerStatus, @@ -646,6 +648,7 @@ impl DisputeInterface for KafkaStore { #[async_trait::async_trait] impl EphemeralKeyInterface for KafkaStore { + #[cfg(feature = "v1")] async fn create_ephemeral_key( &self, ek: EphemeralKeyNew, @@ -653,18 +656,47 @@ impl EphemeralKeyInterface for KafkaStore { ) -> CustomResult { self.diesel_store.create_ephemeral_key(ek, validity).await } + + #[cfg(feature = "v2")] + async fn create_ephemeral_key( + &self, + ek: EphemeralKeyTypeNew, + validity: i64, + ) -> CustomResult { + self.diesel_store.create_ephemeral_key(ek, validity).await + } + + #[cfg(feature = "v1")] async fn get_ephemeral_key( &self, key: &str, ) -> CustomResult { self.diesel_store.get_ephemeral_key(key).await } + + #[cfg(feature = "v2")] + async fn get_ephemeral_key( + &self, + key: &str, + ) -> CustomResult { + self.diesel_store.get_ephemeral_key(key).await + } + + #[cfg(feature = "v1")] async fn delete_ephemeral_key( &self, id: &str, ) -> CustomResult { self.diesel_store.delete_ephemeral_key(id).await } + + #[cfg(feature = "v2")] + async fn delete_ephemeral_key( + &self, + id: &str, + ) -> CustomResult { + self.diesel_store.delete_ephemeral_key(id).await + } } #[async_trait::async_trait] diff --git a/crates/router/src/db/relay.rs b/crates/router/src/db/relay.rs new file mode 100644 index 000000000000..e869165aa339 --- /dev/null +++ b/crates/router/src/db/relay.rs @@ -0,0 +1,135 @@ +use common_utils::types::keymanager::KeyManagerState; +use diesel_models; +use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::behaviour::{Conversion, ReverseConversion}; +use storage_impl::MockDb; + +use super::domain; +use crate::{ + connection, + core::errors::{self, CustomResult}, + db::kafka_store::KafkaStore, + services::Store, +}; + +#[async_trait::async_trait] +pub trait RelayInterface { + async fn insert_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + new: hyperswitch_domain_models::relay::Relay, + ) -> CustomResult; + + async fn update_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + current_state: hyperswitch_domain_models::relay::Relay, + relay_update: hyperswitch_domain_models::relay::RelayUpdate, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl RelayInterface for Store { + async fn insert_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + new: hyperswitch_domain_models::relay::Relay, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + new.construct_new() + .await + .change_context(errors::StorageError::EncryptionError)? + .insert(&conn) + .await + .map_err(|error| report!(errors::StorageError::from(error)))? + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + } + + async fn update_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + current_state: hyperswitch_domain_models::relay::Relay, + relay_update: hyperswitch_domain_models::relay::RelayUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + Conversion::convert(current_state) + .await + .change_context(errors::StorageError::EncryptionError)? + .update( + &conn, + diesel_models::relay::RelayUpdateInternal::from(relay_update), + ) + .await + .map_err(|error| report!(errors::StorageError::from(error)))? + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + } +} + +#[async_trait::async_trait] +impl RelayInterface for MockDb { + async fn insert_relay( + &self, + _key_manager_state: &KeyManagerState, + _merchant_key_store: &domain::MerchantKeyStore, + _new: hyperswitch_domain_models::relay::Relay, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn update_relay( + &self, + _key_manager_state: &KeyManagerState, + _merchant_key_store: &domain::MerchantKeyStore, + _current_state: hyperswitch_domain_models::relay::Relay, + _relay_update: hyperswitch_domain_models::relay::RelayUpdate, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } +} + +#[async_trait::async_trait] +impl RelayInterface for KafkaStore { + async fn insert_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + new: hyperswitch_domain_models::relay::Relay, + ) -> CustomResult { + self.diesel_store + .insert_relay(key_manager_state, merchant_key_store, new) + .await + } + + async fn update_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + current_state: hyperswitch_domain_models::relay::Relay, + relay_update: hyperswitch_domain_models::relay::RelayUpdate, + ) -> CustomResult { + self.diesel_store + .update_relay( + key_manager_state, + merchant_key_store, + current_state, + relay_update, + ) + .await + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 58b1c4447fc4..3dda2a97294b 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -90,6 +90,7 @@ pub mod headers { pub const X_REDIRECT_URI: &str = "x-redirect-uri"; pub const X_TENANT_ID: &str = "x-tenant-id"; pub const X_CLIENT_SECRET: &str = "X-Client-Secret"; + pub const X_RESOURCE_TYPE: &str = "X-Resource-Type"; } pub mod pii { @@ -140,7 +141,8 @@ pub fn mk_app( .service(routes::Customers::server(state.clone())) .service(routes::Configs::server(state.clone())) .service(routes::MerchantConnectorAccount::server(state.clone())) - .service(routes::Webhooks::server(state.clone())); + .service(routes::Webhooks::server(state.clone())) + .service(routes::Relay::server(state.clone())); #[cfg(feature = "oltp")] { @@ -155,15 +157,17 @@ pub fn mk_app( } } + #[cfg(all(feature = "oltp", any(feature = "v1", feature = "v2"),))] + { + server_app = server_app.service(routes::EphemeralKey::server(state.clone())) + } #[cfg(all( feature = "oltp", any(feature = "v1", feature = "v2"), not(feature = "customer_v2") ))] { - server_app = server_app - .service(routes::EphemeralKey::server(state.clone())) - .service(routes::Poll::server(state.clone())) + server_app = server_app.service(routes::Poll::server(state.clone())) } #[cfg(feature = "olap")] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 6d1406c78ed4..462861d331f4 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -59,6 +59,8 @@ pub mod verify_connector; pub mod webhook_events; pub mod webhooks; +pub mod relay; + #[cfg(feature = "dummy_connector")] pub use self::app::DummyConnector; #[cfg(all(feature = "olap", feature = "recon", feature = "v1"))] @@ -67,7 +69,7 @@ pub use self::app::{ ApiKeys, AppState, ApplePayCertificatesMigration, Cache, Cards, Configs, ConnectorOnboarding, Customers, Disputes, EphemeralKey, FeatureMatrix, Files, Forex, Gsm, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Poll, - Profile, ProfileNew, Refunds, SessionState, User, Webhooks, + Profile, ProfileNew, Refunds, Relay, SessionState, User, Webhooks, }; #[cfg(feature = "olap")] pub use self::app::{Blocklist, Organization, Routing, Verify, WebhookEvents}; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index d63bace196ae..4a02340db6e6 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -12,7 +12,10 @@ use common_utils::id_type; use external_services::email::{ no_email::NoEmailClient, ses::AwsSes, smtp::SmtpServer, EmailClientConfigs, EmailService, }; -use external_services::{file_storage::FileStorageInterface, grpc_client::GrpcClients}; +use external_services::{ + file_storage::FileStorageInterface, + grpc_client::{GrpcClients, GrpcHeaders}, +}; use hyperswitch_interfaces::{ encryption_interface::EncryptionManagementInterface, secrets_interface::secret_state::{RawSecret, SecuredSecret}, @@ -27,11 +30,7 @@ use self::settings::Tenant; use super::currency; #[cfg(feature = "dummy_connector")] use super::dummy_connector::*; -#[cfg(all( - any(feature = "v1", feature = "v2"), - not(feature = "customer_v2"), - feature = "oltp" -))] +#[cfg(all(any(feature = "v1", feature = "v2"), feature = "oltp"))] use super::ephemeral_key::*; #[cfg(any(feature = "olap", feature = "oltp"))] use super::payment_methods::*; @@ -55,7 +54,7 @@ use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_ve use super::webhooks::*; use super::{ admin, api_keys, cache::*, connector_onboarding, disputes, files, gsm, health::*, profiles, - user, user_role, + relay, user, user_role, }; #[cfg(feature = "v1")] use super::{apple_pay_certificates_migration, blocklist, payment_link, webhook_events}; @@ -125,6 +124,12 @@ impl SessionState { event_context: events::EventContext::new(self.event_handler.clone()), } } + pub fn get_grpc_headers(&self) -> GrpcHeaders { + GrpcHeaders { + tenant_id: self.tenant.tenant_id.get_string_repr().to_string(), + request_id: self.request_id.map(|req_id| (*req_id).to_string()), + } + } } pub trait SessionStateInfo { @@ -577,6 +582,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)), ), ); @@ -584,6 +592,17 @@ impl Payments { } } +pub struct Relay; + +#[cfg(feature = "oltp")] +impl Relay { + pub fn server(state: AppState) -> Scope { + web::scope("/relay") + .app_data(web::Data::new(state)) + .service(web::resource("").route(web::post().to(relay::relay))) + } +} + #[cfg(feature = "v1")] impl Payments { pub fn server(state: AppState) -> Scope { @@ -1143,8 +1162,11 @@ impl PaymentMethods { web::resource("/{id}/update-saved-payment-method") .route(web::patch().to(payment_method_update_api)), ) - .service(web::resource("/{id}").route(web::get().to(payment_method_retrieve_api))) - .service(web::resource("/{id}").route(web::delete().to(payment_method_delete_api))); + .service( + web::resource("/{id}") + .route(web::get().to(payment_method_retrieve_api)) + .route(web::delete().to(payment_method_delete_api)), + ); route } @@ -1418,6 +1440,16 @@ impl EphemeralKey { } } +#[cfg(feature = "v2")] +impl EphemeralKey { + pub fn server(config: AppState) -> Scope { + web::scope("/v2/ephemeral-keys") + .app_data(web::Data::new(config)) + .service(web::resource("").route(web::post().to(ephemeral_key_create))) + .service(web::resource("/{id}").route(web::delete().to(ephemeral_key_delete))) + } +} + pub struct Mandates; #[cfg(all(any(feature = "olap", feature = "oltp"), feature = "v1"))] diff --git a/crates/router/src/routes/ephemeral_key.rs b/crates/router/src/routes/ephemeral_key.rs index e6a33c59192e..0330482e81a9 100644 --- a/crates/router/src/routes/ephemeral_key.rs +++ b/crates/router/src/routes/ephemeral_key.rs @@ -1,11 +1,7 @@ -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use actix_web::{web, HttpRequest, HttpResponse}; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use router_env::{instrument, tracing, Flow}; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use super::AppState; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use crate::{ core::{api_locking, payments::helpers}, services::{api, authentication as auth}, @@ -38,7 +34,35 @@ pub async fn ephemeral_key_create( .await } -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::EphemeralKeyCreate))] +pub async fn ephemeral_key_create( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::EphemeralKeyCreate; + let payload = json_payload.into_inner(); + api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: auth::AuthenticationData, payload, _| { + helpers::make_ephemeral_key( + state, + payload.customer_id.to_owned(), + auth.merchant_account, + auth.key_store, + req.headers(), + ) + }, + &auth::HeaderAuth(auth::ApiKeyAuth), + api_locking::LockAction::NotApplicable, + ) + .await +} + #[instrument(skip_all, fields(flow = ?Flow::EphemeralKeyDelete))] pub async fn ephemeral_key_delete( state: web::Data, diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 2a76bbcd435e..25f712f89808 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -37,6 +37,7 @@ pub enum ApiIdentifier { Recon, Poll, ApplePayCertificatesMigration, + Relay, Documentation, } @@ -165,6 +166,7 @@ impl From for ApiIdentifier { | Flow::RefundsFilters | Flow::RefundsAggregate | Flow::RefundsManualUpdate => Self::Refunds, + Flow::Relay => Self::Relay, Flow::FrmFulfillment | Flow::IncomingWebhookReceive diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index c27a25677af2..729a35aa8b7a 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -141,8 +141,8 @@ pub async fn confirm_payment_method_intent_api( let pm_id = path.into_inner(); let payload = json_payload.into_inner(); - let (auth, _) = match auth::check_client_secret_and_get_auth(req.headers(), &payload) { - Ok((auth, _auth_flow)) => (auth, _auth_flow), + let auth = match auth::is_ephemeral_or_publishible_auth(req.headers()) { + Ok(auth) => auth, Err(e) => return api::log_and_return_error_response(e), }; @@ -150,7 +150,6 @@ pub async fn confirm_payment_method_intent_api( id: pm_id.clone(), payment_method_type: payload.payment_method_type, payment_method_subtype: payload.payment_method_subtype, - client_secret: payload.client_secret.clone(), customer_id: payload.customer_id.to_owned(), payment_method_data: payload.payment_method_data.clone(), }; @@ -191,6 +190,11 @@ pub async fn payment_method_update_api( let payment_method_id = path.into_inner(); let payload = json_payload.into_inner(); + let auth = match auth::is_ephemeral_or_publishible_auth(req.headers()) { + Ok(auth) => auth, + Err(e) => return api::log_and_return_error_response(e), + }; + Box::pin(api::server_wrap( flow, state, @@ -205,7 +209,7 @@ pub async fn payment_method_update_api( auth.key_store, ) }, - &auth::HeaderAuth(auth::ApiKeyAuth), + &*auth, api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 2893cb8fa044..a9005644bebb 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -2447,3 +2447,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/routes/payments/helpers.rs b/crates/router/src/routes/payments/helpers.rs index 5b0ef815b289..529475a3584d 100644 --- a/crates/router/src/routes/payments/helpers.rs +++ b/crates/router/src/routes/payments/helpers.rs @@ -31,6 +31,9 @@ pub fn populate_ip_into_browser_info( accept_header: None, user_agent: None, ip_address: None, + os_type: None, + os_version: None, + device_model: None, }); let ip_address = req diff --git a/crates/router/src/routes/relay.rs b/crates/router/src/routes/relay.rs new file mode 100644 index 000000000000..13c92eb19a6e --- /dev/null +++ b/crates/router/src/routes/relay.rs @@ -0,0 +1,40 @@ +use actix_web::{web, Responder}; +use router_env::{instrument, tracing, Flow}; + +use crate::{ + self as app, + core::{api_locking, relay}, + services::{api, authentication as auth}, +}; + +#[instrument(skip_all, fields(flow = ?Flow::Relay))] +#[cfg(feature = "oltp")] +pub async fn relay( + state: web::Data, + req: actix_web::HttpRequest, + payload: web::Json, +) -> impl Responder { + let flow = Flow::Relay; + let payload = payload.into_inner(); + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: auth::AuthenticationData, req, _| { + relay::relay( + state, + auth.merchant_account, + #[cfg(feature = "v1")] + auth.profile_id, + #[cfg(feature = "v2")] + Some(auth.profile.get_id().clone()), + auth.key_store, + req, + ) + }, + &auth::HeaderAuth(auth::ApiKeyAuth), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index e8435243ff4d..e5412c20fe29 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use actix_web::http::header::HeaderMap; #[cfg(all( any(feature = "v2", feature = "v1"), @@ -11,7 +13,11 @@ use api_models::payouts; use api_models::{payment_methods::PaymentMethodListRequest, payments}; use async_trait::async_trait; use common_enums::TokenPurpose; +#[cfg(feature = "v2")] +use common_utils::fp_utils; use common_utils::{date_time, id_type}; +#[cfg(feature = "v2")] +use diesel_models::ephemeral_key; use error_stack::{report, ResultExt}; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use masking::PeekInterface; @@ -538,6 +544,15 @@ where .change_context(errors::ApiErrorResponse::Unauthorized) .attach_printable("Failed to fetch merchant key store for the merchant id")?; + let profile_id = + get_header_value_by_key(headers::X_PROFILE_ID.to_string(), request_headers)? + .map(id_type::ProfileId::from_str) + .transpose() + .change_context(errors::ValidationError::IncorrectValueProvided { + field_name: "X-Profile-Id", + }) + .change_context(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state .store() .find_merchant_account_by_merchant_id( @@ -551,7 +566,7 @@ where let auth = AuthenticationData { merchant_account: merchant, key_store, - profile_id: None, + profile_id, }; Ok(( auth.clone(), @@ -1329,6 +1344,7 @@ where #[derive(Debug)] pub struct EphemeralKeyAuth; +#[cfg(feature = "v1")] #[async_trait] impl AuthenticateAndFetch for EphemeralKeyAuth where @@ -1352,6 +1368,45 @@ where .await } } + +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for EphemeralKeyAuth +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let api_key = + get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?; + let ephemeral_key = state + .store() + .get_ephemeral_key(api_key) + .await + .change_context(errors::ApiErrorResponse::Unauthorized)?; + + let resource_type = HeaderMapStruct::new(request_headers) + .get_mandatory_header_value_by_key(headers::X_RESOURCE_TYPE) + .and_then(|val| { + ephemeral_key::ResourceType::from_str(val).change_context( + errors::ApiErrorResponse::InvalidRequestData { + message: format!("`{}` header is invalid", headers::X_RESOURCE_TYPE), + }, + ) + })?; + + fp_utils::when(resource_type != ephemeral_key.resource_type, || { + Err(errors::ApiErrorResponse::Unauthorized) + })?; + + MerchantIdAuth(ephemeral_key.merchant_id) + .authenticate_and_fetch(request_headers, state) + .await + } +} #[derive(Debug)] pub struct MerchantIdAuth(pub id_type::MerchantId); @@ -2938,13 +2993,6 @@ impl ClientSecretFetch for PaymentMethodCreate { } } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -impl ClientSecretFetch for PaymentMethodIntentConfirm { - fn get_client_secret(&self) -> Option<&String> { - Some(&self.client_secret) - } -} - impl ClientSecretFetch for api_models::cards_info::CardsInfoRequest { fn get_client_secret(&self) -> Option<&String> { self.client_secret.as_ref() @@ -2969,6 +3017,7 @@ impl ClientSecretFetch for api_models::pm_auth::ExchangeTokenCreateRequest { } } +#[cfg(feature = "v1")] impl ClientSecretFetch for api_models::payment_methods::PaymentMethodUpdate { fn get_client_secret(&self) -> Option<&String> { self.client_secret.as_ref() @@ -3059,6 +3108,20 @@ where } } +pub fn is_ephemeral_or_publishible_auth( + headers: &HeaderMap, +) -> RouterResult>> { + let api_key = get_api_key(headers)?; + + if api_key.starts_with("epk") { + Ok(Box::new(EphemeralKeyAuth)) + } else if api_key.starts_with("pk_") { + Ok(Box::new(HeaderAuth(PublishableKeyAuth))) + } else { + Ok(Box::new(HeaderAuth(ApiKeyAuth))) + } +} + pub fn is_ephemeral_auth( headers: &HeaderMap, ) -> RouterResult>> { @@ -3109,7 +3172,7 @@ pub fn get_header_value_by_key(key: String, headers: &HeaderMap) -> RouterResult }) .transpose() } -pub fn get_id_type_by_key_from_headers( +pub fn get_id_type_by_key_from_headers( key: String, headers: &HeaderMap, ) -> RouterResult> { 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/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 20a075e8ceda..b0c3d2d21df8 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -445,9 +445,11 @@ impl ConnectorData { // Ok(ConnectorEnum::Old(Box::new(connector::Inespay::new()))) // } enums::Connector::Itaubank => { - //enums::Connector::Jpmorgan => Ok(ConnectorEnum::Old(Box::new(connector::Jpmorgan))), Ok(ConnectorEnum::Old(Box::new(connector::Itaubank::new()))) } + enums::Connector::Jpmorgan => { + Ok(ConnectorEnum::Old(Box::new(connector::Jpmorgan::new()))) + } enums::Connector::Klarna => { Ok(ConnectorEnum::Old(Box::new(connector::Klarna::new()))) } diff --git a/crates/router/src/types/payment_methods.rs b/crates/router/src/types/payment_methods.rs index 1a6b9053dcb9..c40d6fedfe73 100644 --- a/crates/router/src/types/payment_methods.rs +++ b/crates/router/src/types/payment_methods.rs @@ -126,16 +126,6 @@ impl VaultingDataInterface for PaymentMethodVaultingData { } } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub struct PaymentMethodClientSecret; - -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -impl PaymentMethodClientSecret { - pub fn generate(payment_method_id: &common_utils::id_type::GlobalPaymentMethodId) -> String { - todo!() - } -} - #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub struct SavedPMLPaymentsInfo { pub payment_intent: storage::PaymentIntent, @@ -155,7 +145,7 @@ pub struct VaultRetrieveRequest { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct VaultRetrieveResponse { - pub data: Secret, + pub data: PaymentMethodVaultingData, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] diff --git a/crates/router/src/types/storage/ephemeral_key.rs b/crates/router/src/types/storage/ephemeral_key.rs index 9927e16b3fad..c4b8e2ba701a 100644 --- a/crates/router/src/types/storage/ephemeral_key.rs +++ b/crates/router/src/types/storage/ephemeral_key.rs @@ -1 +1,18 @@ pub use diesel_models::ephemeral_key::{EphemeralKey, EphemeralKeyNew}; +#[cfg(feature = "v2")] +pub use diesel_models::ephemeral_key::{EphemeralKeyType, EphemeralKeyTypeNew, ResourceType}; + +#[cfg(feature = "v2")] +use crate::types::transformers::ForeignFrom; +#[cfg(feature = "v2")] +impl ForeignFrom for api_models::ephemeral_key::EphemeralKeyResponse { + fn foreign_from(from: EphemeralKeyType) -> Self { + Self { + customer_id: from.customer_id, + created_at: from.created_at, + expires: from.expires, + secret: from.secret, + id: from.id, + } + } +} diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 1947a2a2dee1..1d9a86f0aff9 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -256,7 +256,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Iatapay => Self::Iatapay, // api_enums::Connector::Inespay => Self::Inespay, api_enums::Connector::Itaubank => Self::Itaubank, - //api_enums::Connector::Jpmorgan => Self::Jpmorgan, + api_enums::Connector::Jpmorgan => Self::Jpmorgan, api_enums::Connector::Klarna => Self::Klarna, api_enums::Connector::Mifinity => Self::Mifinity, api_enums::Connector::Mollie => Self::Mollie, diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index f2f849731d52..10db11bb3b0c 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -294,7 +294,8 @@ api_key="API Key" api_key="API Key" [jpmorgan] -api_key="API Key" +api_key="Client ID" +key1 ="Client Secret" [elavon] api_key="API Key" diff --git a/crates/router/tests/connectors/trustpay.rs b/crates/router/tests/connectors/trustpay.rs index 308bf5135855..2e5ea6d50f1a 100644 --- a/crates/router/tests/connectors/trustpay.rs +++ b/crates/router/tests/connectors/trustpay.rs @@ -49,6 +49,9 @@ fn get_default_browser_info() -> BrowserInformation { accept_header: Some("*".to_string()), user_agent: Some("none".to_string()), ip_address: None, + os_type: None, + os_version: None, + device_model: None, } } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 489b55a227b3..361cda63a9f9 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -1013,6 +1013,9 @@ impl Default for BrowserInfoType { java_enabled: Some(true), java_script_enabled: Some(true), ip_address: Some("127.0.0.1".parse().unwrap()), + device_model: Some("Apple IPHONE 7".to_string()), + os_type: Some("IOS or ANDROID".to_string()), + os_version: Some("IOS 14.5".to_string()), }; Self(data) } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index b7ad19f958b0..041d4c9f6148 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -531,6 +531,8 @@ pub enum Flow { PaymentStartRedirection, /// Volume split on the routing type VolumeSplitOnRoutingType, + /// Relay flow + Relay, } /// Trait for providing generic behaviour to flow metric diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index 37cc341c4540..7b6a1ba6b7a0 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -51,7 +51,7 @@ pub struct ConnectorAuthentication { pub iatapay: Option, pub inespay: Option, pub itaubank: Option, - pub jpmorgan: Option, + pub jpmorgan: Option, pub mifinity: Option, pub mollie: Option, pub multisafepay: Option, diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Jpmorgan.js b/cypress-tests/cypress/e2e/PaymentUtils/Jpmorgan.js new file mode 100644 index 000000000000..36e53ffd995f --- /dev/null +++ b/cypress-tests/cypress/e2e/PaymentUtils/Jpmorgan.js @@ -0,0 +1,225 @@ +const successfulNo3DSCardDetails = { + card_number: "6011016011016011", + card_exp_month: "10", + card_exp_year: "2027", + card_holder_name: "John Doe", + card_cvc: "123", +}; + +export const connectorDetails = { + card_pm: { + PaymentIntent: { + Request: { + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_payment_method", + }, + }, + }, + "3DSManualCapture": { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 501, + body: { + error: { + type: "invalid_request", + message: "3DS payments is not supported by Jpmorgan", + code: "IR_00", + }, + }, + }, + }, + + "3DSAutoCapture": { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 501, + body: { + error: { + type: "invalid_request", + message: "Three_ds payments is not supported by Jpmorgan", + code: "IR_00", + }, + }, + }, + }, + No3DSManualCapture: { + Request: { + currency: "USD", + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "requires_capture", + }, + }, + }, + No3DSAutoCapture: { + Request: { + currency: "USD", + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 200, + body: { + status: "succeeded", + }, + }, + }, + Capture: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 200, + body: { + status: "succeeded", + amount: 6500, + amount_capturable: 0, + amount_received: 6500, + }, + }, + }, + PartialCapture: { + Request: {}, + Response: { + status: 200, + body: { + status: "partially_captured", + amount: 6500, + amount_capturable: 0, + amount_received: 100, + }, + }, + }, + Refund: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 501, + body: { + type: "invalid_request", + message: "Refunds is not implemented", + code: "IR_00", + }, + }, + }, + manualPaymentRefund: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 501, + body: { + type: "invalid_request", + message: "Refunds is not implemented", + code: "IR_00", + }, + }, + }, + manualPaymentPartialRefund: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 501, + body: { + type: "invalid_request", + message: "Refunds is not implemented", + code: "IR_00", + }, + }, + }, + PartialRefund: { + Configs: { + TRIGGER_SKIP: true, + }, + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + }, + Response: { + status: 501, + body: { + type: "invalid_request", + message: "Refunds is not implemented", + code: "IR_00", + }, + }, + }, + SyncRefund: { + Configs: { + TRIGGER_SKIP: true, + }, + Response: { + status: 404, + body: { + type: "invalid_request", + message: "Refund does not exist in our records.", + code: "HE_02", + }, + }, + }, + }, +}; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js index e432c37717bf..3b4c9e5decf0 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Utils.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Utils.js @@ -15,6 +15,7 @@ import { connectorDetails as fiservemeaConnectorDetails } from "./Fiservemea.js" import { connectorDetails as fiuuConnectorDetails } from "./Fiuu.js"; import { connectorDetails as iatapayConnectorDetails } from "./Iatapay.js"; import { connectorDetails as itaubankConnectorDetails } from "./ItauBank.js"; +import { connectorDetails as jpmorganConnectorDetails } from "./Jpmorgan.js"; import { connectorDetails as nexixpayConnectorDetails } from "./Nexixpay.js"; import { connectorDetails as nmiConnectorDetails } from "./Nmi.js"; import { connectorDetails as noonConnectorDetails } from "./Noon.js"; @@ -36,6 +37,7 @@ const connectorDetails = { fiservemea: fiservemeaConnectorDetails, iatapay: iatapayConnectorDetails, itaubank: itaubankConnectorDetails, + jpmorgan: jpmorganConnectorDetails, nexixpay: nexixpayConnectorDetails, nmi: nmiConnectorDetails, novalnet: novalnetConnectorDetails, diff --git a/cypress-tests/cypress/e2e/PayoutUtils/AdyenPlatform.js b/cypress-tests/cypress/e2e/PayoutUtils/AdyenPlatform.js index 75b45aac2046..0048daf45b88 100644 --- a/cypress-tests/cypress/e2e/PayoutUtils/AdyenPlatform.js +++ b/cypress-tests/cypress/e2e/PayoutUtils/AdyenPlatform.js @@ -71,7 +71,7 @@ export const connectorDetails = { Response: { status: 200, body: { - status: "success", + status: "initiated", payout_type: "bank", }, }, @@ -101,7 +101,7 @@ export const connectorDetails = { Response: { status: 200, body: { - status: "success", + status: "initiated", payout_type: "bank", }, }, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 6c9ca772170b..64d0526d1476 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -120,6 +120,7 @@ iatapay.base_url = "https://sandbox.iata-pay.iata.org/api/v1" inespay.base_url = "https://apiflow.inespay.com/san/v21" itaubank.base_url = "https://sandbox.devportal.itau.com.br/" jpmorgan.base_url = "https://api-mock.payments.jpmorgan.com/api/v2" +jpmorgan.secondary_base_url="https://id.payments.jpmorgan.com" klarna.base_url = "https://api{{klarna_region}}.playground.klarna.com/" mifinity.base_url = "https://demo.mifinity.com/" mollie.base_url = "https://api.mollie.com/v2/" @@ -331,19 +332,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-17-141811_add_relay_table/down.sql b/migrations/2024-12-17-141811_add_relay_table/down.sql new file mode 100644 index 000000000000..47b6682c6791 --- /dev/null +++ b/migrations/2024-12-17-141811_add_relay_table/down.sql @@ -0,0 +1,7 @@ +-- This file should undo anything in `up.sql` +DROP TABLE relay; + +DROP TYPE IF EXISTS "RelayStatus"; + +DROP TYPE IF EXISTS "RelayType"; + diff --git a/migrations/2024-12-17-141811_add_relay_table/up.sql b/migrations/2024-12-17-141811_add_relay_table/up.sql new file mode 100644 index 000000000000..e9f0017bd049 --- /dev/null +++ b/migrations/2024-12-17-141811_add_relay_table/up.sql @@ -0,0 +1,22 @@ +-- Your SQL goes here +CREATE TYPE "RelayStatus" AS ENUM ('created', 'pending', 'failure', 'success'); + +CREATE TYPE "RelayType" AS ENUM ('refund'); + +CREATE TABLE relay ( + id VARCHAR(64) PRIMARY KEY, + connector_resource_id VARCHAR(128) NOT NULL, + connector_id VARCHAR(64) NOT NULL, + profile_id VARCHAR(64) NOT NULL, + merchant_id VARCHAR(64) NOT NULL, + relay_type "RelayType" NOT NULL, + request_data JSONB DEFAULT NULL, + status "RelayStatus" NOT NULL, + connector_reference_id VARCHAR(128), + error_code VARCHAR(64), + error_message TEXT, + created_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + modified_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + response_data JSONB DEFAULT NULL +); +