diff --git a/api-reference-v2/api-reference/payment-method-session/payment-method-session--confirm-a-payment-method-session.mdx b/api-reference-v2/api-reference/payment-method-session/payment-method-session--confirm-a-payment-method-session.mdx new file mode 100644 index 0000000000..d115fc7237 --- /dev/null +++ b/api-reference-v2/api-reference/payment-method-session/payment-method-session--confirm-a-payment-method-session.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /v2/payment-method-session/:id/confirm +--- \ No newline at end of file diff --git a/api-reference-v2/mint.json b/api-reference-v2/mint.json index d935ec84a6..2b0997326d 100644 --- a/api-reference-v2/mint.json +++ b/api-reference-v2/mint.json @@ -66,7 +66,8 @@ "api-reference/payment-method-session/payment-method-session--create", "api-reference/payment-method-session/payment-method-session--retrieve", "api-reference/payment-method-session/payment-method-session--list-payment-methods", - "api-reference/payment-method-session/payment-method-session--update-a-saved-payment-method" + "api-reference/payment-method-session/payment-method-session--update-a-saved-payment-method", + "api-reference/payment-method-session/payment-method-session--confirm-a-payment-method-session" ] }, { diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 082a554dd9..1ec27163f3 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -2005,7 +2005,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsConfirmIntentResponse" + "$ref": "#/components/schemas/PaymentsResponse" } } } @@ -2055,7 +2055,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentsRetrieveResponse" + "$ref": "#/components/schemas/PaymentsResponse" } } } @@ -2905,7 +2905,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentMethodsSessionResponse" + "$ref": "#/components/schemas/PaymentMethodSessionResponse" } } } @@ -2946,7 +2946,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PaymentMethodsSessionResponse" + "$ref": "#/components/schemas/PaymentMethodSessionResponse" } } } @@ -3066,6 +3066,83 @@ ] } }, + "/v2/payment-method-session/:id/confirm": { + "post": { + "tags": [ + "Payment Method Session" + ], + "summary": "Payment Method Session - Confirm a payment method session", + "description": "**Confirms a payment method session object with the payment method data**", + "operationId": "Confirm the payment method session", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The unique identifier for the Payment Method Session", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-Profile-Id", + "in": "header", + "description": "Profile ID associated to the payment intent", + "required": true, + "schema": { + "type": "string" + }, + "example": "pro_abcdefghijklmnop" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodSessionConfirmRequest" + }, + "examples": { + "Confirm the payment method session with card details": { + "value": { + "payment_method_data": { + "card": { + "card_cvc": "123", + "card_exp_month": "10", + "card_exp_year": "25", + "card_number": "4242424242424242" + } + }, + "payment_method_subtype": "credit", + "payment_method_type": "card" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Payment Method created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "publishable_key": [] + } + ] + } + }, "/v2/refunds": { "post": { "tags": [ @@ -6735,7 +6812,8 @@ "card_exp_month", "card_exp_year", "card_holder_name", - "card_issuing_country" + "card_issuing_country", + "card_cvc" ], "properties": { "card_number": { @@ -6787,6 +6865,11 @@ } ], "nullable": true + }, + "card_cvc": { + "type": "string", + "description": "The CVC number for the card\nThis is optional in case the card needs to be vaulted", + "example": "242" } }, "additionalProperties": false @@ -7637,6 +7720,11 @@ "type": "string", "description": "A token that can be used to make payments directly with the connector.", "example": "pm_9UhMqBMEOooRIvJFFdeW" + }, + "connector_token_request_reference_id": { + "type": "string", + "description": "The reference id sent to the connector when creating the token", + "nullable": true } } }, @@ -13013,7 +13101,7 @@ ] }, "object": { - "$ref": "#/components/schemas/PaymentsRetrieveResponse" + "$ref": "#/components/schemas/PaymentsResponse" } } }, @@ -14240,6 +14328,14 @@ ], "nullable": true }, + "psp_tokenization": { + "allOf": [ + { + "$ref": "#/components/schemas/PspTokenization" + } + ], + "nullable": true + }, "network_tokenization": { "allOf": [ { @@ -14988,6 +15084,7 @@ "last_used_at": { "type": "string", "format": "date-time", + "description": "A timestamp (ISO 8601 code) that determines when the payment method was last used", "example": "2024-02-24T11:04:09.922Z", "nullable": true }, @@ -14998,6 +15095,14 @@ } ], "nullable": true + }, + "connector_tokens": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConnectorTokenDetails" + }, + "description": "The connector token details if available", + "nullable": true } } }, @@ -15016,6 +15121,30 @@ } ] }, + "PaymentMethodSessionConfirmRequest": { + "type": "object", + "required": [ + "payment_method_type", + "payment_method_subtype", + "payment_method_data" + ], + "properties": { + "payment_method_type": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_subtype": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "payment_method_data": { + "$ref": "#/components/schemas/PaymentMethodDataRequest" + }, + "return_url": { + "type": "string", + "description": "The return url to which the customer should be redirected to after adding the payment method", + "nullable": true + } + } + }, "PaymentMethodSessionRequest": { "type": "object", "required": [ @@ -15035,6 +15164,11 @@ ], "nullable": true }, + "return_url": { + "type": "string", + "description": "The return url to which the customer should be redirected to after adding the payment method", + "nullable": true + }, "psp_tokenization": { "allOf": [ { @@ -15062,6 +15196,73 @@ } } }, + "PaymentMethodSessionResponse": { + "type": "object", + "required": [ + "id", + "customer_id", + "expires_at", + "client_secret" + ], + "properties": { + "id": { + "type": "string", + "example": "12345_pms_01926c58bc6e77c09e809964e72af8c8" + }, + "customer_id": { + "type": "string", + "description": "The customer id for which the payment methods session is to be created", + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8" + }, + "billing": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], + "nullable": true + }, + "psp_tokenization": { + "allOf": [ + { + "$ref": "#/components/schemas/PspTokenization" + } + ], + "nullable": true + }, + "network_tokenization": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTokenization" + } + ], + "nullable": true + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "The iso timestamp when the session will expire\nTrying to retrieve the session or any operations on the session after this time will result in an error", + "example": "2023-01-18T11:04:09.922Z" + }, + "client_secret": { + "type": "string", + "description": "Client Secret" + }, + "return_url": { + "type": "string", + "description": "The return url to which the user should be redirected to", + "nullable": true + }, + "associated_payment": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentsResponse" + } + ], + "nullable": true + } + } + }, "PaymentMethodSessionUpdateSavedPaymentMethod": { "allOf": [ { @@ -15237,12 +15438,22 @@ }, "PaymentMethodUpdate": { "type": "object", - "required": [ - "payment_method_data" - ], "properties": { "payment_method_data": { - "$ref": "#/components/schemas/PaymentMethodUpdateData" + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodUpdateData" + } + ], + "nullable": true + }, + "connector_token_details": { + "allOf": [ + { + "$ref": "#/components/schemas/ConnectorTokenDetails" + } + ], + "nullable": true } }, "additionalProperties": false @@ -15337,6 +15548,35 @@ } } }, + "PaymentMethodsSessionUpdateRequest": { + "type": "object", + "properties": { + "billing": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], + "nullable": true + }, + "psp_tokenization": { + "allOf": [ + { + "$ref": "#/components/schemas/PspTokenization" + } + ], + "nullable": true + }, + "network_tokenization": { + "allOf": [ + { + "$ref": "#/components/schemas/NetworkTokenization" + } + ], + "nullable": true + } + } + }, "PaymentProcessingDetails": { "type": "object", "required": [ @@ -15574,137 +15814,15 @@ } ], "nullable": true + }, + "payment_method_id": { + "type": "string", + "description": "The payment_method_id to be associated with the payment", + "nullable": true } }, "additionalProperties": false }, - "PaymentsConfirmIntentResponse": { - "type": "object", - "description": "Response for Payment Intent Confirm", - "required": [ - "id", - "status", - "amount", - "customer_id", - "connector", - "client_secret", - "created", - "payment_method_type", - "payment_method_subtype", - "merchant_connector_id", - "applied_authentication_type" - ], - "properties": { - "id": { - "type": "string", - "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", - "example": "12345_pay_01926c58bc6e77c09e809964e72af8c8", - "maxLength": 64, - "minLength": 32 - }, - "status": { - "$ref": "#/components/schemas/IntentStatus" - }, - "amount": { - "$ref": "#/components/schemas/PaymentAmountDetailsResponse" - }, - "customer_id": { - "type": "string", - "description": "The identifier for the customer", - "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", - "maxLength": 64, - "minLength": 32 - }, - "connector": { - "type": "string", - "description": "The connector used for the payment", - "example": "stripe" - }, - "client_secret": { - "type": "string", - "description": "It's a token used for client side verification." - }, - "created": { - "type": "string", - "format": "date-time", - "description": "Time when the payment was created", - "example": "2022-09-10T10:11:12Z" - }, - "payment_method_data": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodDataResponseWithBilling" - } - ], - "nullable": true - }, - "payment_method_type": { - "$ref": "#/components/schemas/PaymentMethod" - }, - "payment_method_subtype": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "next_action": { - "allOf": [ - { - "$ref": "#/components/schemas/NextActionData" - } - ], - "nullable": true - }, - "connector_transaction_id": { - "type": "string", - "description": "A unique identifier for a payment provided by the connector", - "example": "993672945374576J", - "nullable": true - }, - "connector_reference_id": { - "type": "string", - "description": "reference(Identifier) to the payment at connector side", - "example": "993672945374576J", - "nullable": true - }, - "connector_token_details": { - "allOf": [ - { - "$ref": "#/components/schemas/ConnectorTokenDetails" - } - ], - "nullable": true - }, - "merchant_connector_id": { - "type": "string", - "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment" - }, - "browser_info": { - "allOf": [ - { - "$ref": "#/components/schemas/BrowserInformation" - } - ], - "nullable": true - }, - "error": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorDetails" - } - ], - "nullable": true - }, - "authentication_type": { - "allOf": [ - { - "$ref": "#/components/schemas/AuthenticationType" - } - ], - "nullable": true - }, - "applied_authentication_type": { - "$ref": "#/components/schemas/AuthenticationType" - } - } - }, "PaymentsCreateIntentRequest": { "type": "object", "required": [ @@ -16659,146 +16777,18 @@ } ], "nullable": true + }, + "payment_method_id": { + "type": "string", + "description": "The payment_method_id to be associated with the payment", + "nullable": true } }, "additionalProperties": false }, "PaymentsResponse": { "type": "object", - "required": [ - "id", - "status", - "amount", - "customer_id", - "connector", - "client_secret", - "created", - "payment_method_type", - "payment_method_subtype", - "merchant_connector_id" - ], - "properties": { - "id": { - "type": "string", - "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", - "example": "12345_pay_01926c58bc6e77c09e809964e72af8c8", - "maxLength": 64, - "minLength": 32 - }, - "status": { - "$ref": "#/components/schemas/IntentStatus" - }, - "amount": { - "$ref": "#/components/schemas/PaymentAmountDetailsResponse" - }, - "customer_id": { - "type": "string", - "description": "The identifier for the customer", - "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", - "maxLength": 64, - "minLength": 32 - }, - "connector": { - "type": "string", - "description": "The connector used for the payment", - "example": "stripe" - }, - "client_secret": { - "type": "string", - "description": "It's a token used for client side verification." - }, - "created": { - "type": "string", - "format": "date-time", - "description": "Time when the payment was created", - "example": "2022-09-10T10:11:12Z" - }, - "payment_method_data": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodDataResponseWithBilling" - } - ], - "nullable": true - }, - "payment_method_type": { - "$ref": "#/components/schemas/PaymentMethod" - }, - "payment_method_subtype": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "next_action": { - "allOf": [ - { - "$ref": "#/components/schemas/NextActionData" - } - ], - "nullable": true - }, - "connector_transaction_id": { - "type": "string", - "description": "A unique identifier for a payment provided by the connector", - "example": "993672945374576J", - "nullable": true - }, - "connector_reference_id": { - "type": "string", - "description": "reference(Identifier) to the payment at connector side", - "example": "993672945374576J", - "nullable": true - }, - "connector_token_details": { - "allOf": [ - { - "$ref": "#/components/schemas/ConnectorTokenDetails" - } - ], - "nullable": true - }, - "merchant_connector_id": { - "type": "string", - "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment" - }, - "browser_info": { - "allOf": [ - { - "$ref": "#/components/schemas/BrowserInformation" - } - ], - "nullable": true - }, - "error": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorDetails" - } - ], - "nullable": true - } - } - }, - "PaymentsRetrieveRequest": { - "type": "object", - "description": "Request for Payment Status", - "properties": { - "force_sync": { - "type": "boolean", - "description": "A boolean used to indicate if the payment status should be fetched from the connector\nIf this is set to true, the status will be fetched from the connector" - }, - "expand_attempts": { - "type": "boolean", - "description": "A boolean used to indicate if all the attempts needs to be fetched for the intent.\nIf this is set to true, attempts list will be available in the response." - }, - "param": { - "type": "string", - "description": "These are the query params that are sent in case of redirect response.\nThese can be ingested by the connector to take necessary actions.", - "nullable": true - } - } - }, - "PaymentsRetrieveResponse": { - "type": "object", - "description": "Response for Payment Intent Confirm", + "description": "Response for Payment Intent Confirm\nFew fields should be expandable, we need not return these in the normal response\nBut when explictly requested for expanded objects, these can be returned\nFor example\nshipping, billing, customer, payment_method", "required": [ "id", "status", @@ -16924,6 +16914,69 @@ }, "description": "List of payment attempts associated with payment intent", "nullable": true + }, + "connector_token_details": { + "allOf": [ + { + "$ref": "#/components/schemas/ConnectorTokenDetails" + } + ], + "nullable": true + }, + "payment_method_id": { + "type": "string", + "description": "The payment_method_id associated with the payment", + "nullable": true + }, + "next_action": { + "allOf": [ + { + "$ref": "#/components/schemas/NextActionData" + } + ], + "nullable": true + }, + "return_url": { + "type": "string", + "description": "The url to which user must be redirected to after completion of the purchase", + "nullable": true + }, + "authentication_type": { + "allOf": [ + { + "$ref": "#/components/schemas/AuthenticationType" + } + ], + "default": "no_three_ds", + "nullable": true + }, + "authentication_type_applied": { + "allOf": [ + { + "$ref": "#/components/schemas/AuthenticationType" + } + ], + "default": "no_three_ds", + "nullable": true + } + } + }, + "PaymentsRetrieveRequest": { + "type": "object", + "description": "Request for Payment Status", + "properties": { + "force_sync": { + "type": "boolean", + "description": "A boolean used to indicate if the payment status should be fetched from the connector\nIf this is set to true, the status will be fetched from the connector" + }, + "expand_attempts": { + "type": "boolean", + "description": "A boolean used to indicate if all the attempts needs to be fetched for the intent.\nIf this is set to true, attempts list will be available in the response." + }, + "param": { + "type": "string", + "description": "These are the query params that are sent in case of redirect response.\nThese can be ingested by the connector to take necessary actions.", + "nullable": true } } }, @@ -18912,8 +18965,7 @@ "type": "object", "description": "The Payment Service Provider Configuration for payment methods that are created using the payment method session", "required": [ - "tokenization_type", - "connector_id" + "tokenization_type" ], "properties": { "tokenization_type": { @@ -18921,7 +18973,8 @@ }, "connector_id": { "type": "string", - "description": "The merchant connector id to be used for tokenization" + "description": "The merchant connector id to be used for tokenization", + "nullable": true } } }, diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 9a2a0ca839..bf7544f7c0 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -208,7 +208,10 @@ impl ApiEventMetric for DisputeListFilters { impl ApiEventMetric for PaymentMethodSessionRequest {} #[cfg(feature = "v2")] -impl ApiEventMetric for PaymentMethodsSessionResponse { +impl ApiEventMetric for PaymentMethodsSessionUpdateRequest {} + +#[cfg(feature = "v2")] +impl ApiEventMetric for PaymentMethodSessionResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::PaymentMethodSession { payment_method_session_id: self.id.clone(), diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index ccda5fd402..044b548b98 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -2,8 +2,8 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; #[cfg(feature = "v2")] use super::{ - PaymentStartRedirectionRequest, PaymentsConfirmIntentResponse, PaymentsCreateIntentRequest, - PaymentsGetIntentRequest, PaymentsIntentResponse, PaymentsRequest, + PaymentStartRedirectionRequest, PaymentsCreateIntentRequest, PaymentsGetIntentRequest, + PaymentsIntentResponse, PaymentsRequest, }; #[cfg(all( any(feature = "v2", feature = "v1"), @@ -31,7 +31,7 @@ use crate::{ PaymentsExternalAuthenticationResponse, PaymentsIncrementalAuthorizationRequest, PaymentsManualUpdateRequest, PaymentsManualUpdateResponse, PaymentsPostSessionTokensRequest, PaymentsPostSessionTokensResponse, PaymentsRejectRequest, - PaymentsResponse, PaymentsRetrieveRequest, PaymentsSessionResponse, PaymentsStartRequest, + PaymentsRetrieveRequest, PaymentsSessionResponse, PaymentsStartRequest, RedirectionResponse, }, }; @@ -158,15 +158,6 @@ impl ApiEventMetric for PaymentsRequest { } } -#[cfg(feature = "v2")] -impl ApiEventMetric for payments::PaymentsResponse { - fn get_api_event_type(&self) -> Option { - Some(ApiEventsType::Payment { - payment_id: self.id.clone(), - }) - } -} - #[cfg(feature = "v2")] impl ApiEventMetric for PaymentsGetIntentRequest { fn get_api_event_type(&self) -> Option { @@ -186,16 +177,7 @@ impl ApiEventMetric for PaymentsIntentResponse { } #[cfg(feature = "v2")] -impl ApiEventMetric for PaymentsConfirmIntentResponse { - fn get_api_event_type(&self) -> Option { - Some(ApiEventsType::Payment { - payment_id: self.id.clone(), - }) - } -} - -#[cfg(feature = "v2")] -impl ApiEventMetric for super::PaymentsRetrieveResponse { +impl ApiEventMetric for payments::PaymentsResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Payment { payment_id: self.id.clone(), @@ -204,7 +186,7 @@ impl ApiEventMetric for super::PaymentsRetrieveResponse { } #[cfg(feature = "v1")] -impl ApiEventMetric for PaymentsResponse { +impl ApiEventMetric for payments::PaymentsResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Payment { payment_id: self.payment_id.clone(), diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 66e3e2d3ce..ccc055a793 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -136,6 +136,10 @@ pub struct PaymentMethodCreate { #[schema(value_type = Option
)] pub billing: Option, + /// The tokenization type to be applied + #[schema(value_type = Option)] + pub psp_tokenization: Option, + /// The network tokenization configuration if applicable #[schema(value_type = Option)] pub network_tokenization: Option, @@ -268,7 +272,7 @@ pub struct PaymentMethodMigrate { pub network_transaction_id: Option, } -#[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema)] +#[derive(Debug, serde::Serialize, ToSchema)] pub struct PaymentMethodMigrateResponse { //payment method response when payment method entry is created pub payment_method_response: PaymentMethodResponse, @@ -474,8 +478,11 @@ pub struct PaymentMethodUpdate { #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] pub struct PaymentMethodUpdate { - /// payment method data to be passed - pub payment_method_data: PaymentMethodUpdateData, + /// Payment method details to be updated for the payment_method + pub payment_method_data: Option, + + /// The connector token details to be updated for the payment_method + pub connector_token_details: Option, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -567,6 +574,11 @@ pub enum CardType { Debit, } +// We cannot use the card struct that we have for payments for the following reason +// The card struct used for payments has card_cvc as mandatory +// but when vaulting the card, we do not need cvc to be collected from the user +// This is because, the vaulted payment method can be used for future transactions in the presence of the customer +// when the customer is on_session again, the cvc can be collected from the customer #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] #[serde(deny_unknown_fields)] @@ -604,6 +616,11 @@ pub struct CardDetail { /// Card Type pub card_type: Option, + + /// The CVC number for the card + /// This is optional in case the card needs to be vaulted + #[schema(value_type = String, example = "242")] + pub card_cvc: Option>, } #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -778,6 +795,7 @@ impl CardDetailUpdate { card_network: None, card_issuer: None, card_type: None, + card_cvc: None, } } } @@ -858,6 +876,37 @@ pub struct PaymentMethodResponse { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, serde::Deserialize, serde::Serialize, ToSchema, Clone)] +pub struct ConnectorTokenDetails { + /// The unique identifier of the connector account through which the token was generated + #[schema(value_type = String, example = "mca_")] + pub connector_id: id_type::MerchantConnectorAccountId, + + #[schema(value_type = TokenizationType)] + pub token_type: common_enums::TokenizationType, + + /// The status of connector token if it is active or inactive + #[schema(value_type = ConnectorTokenStatus)] + pub status: common_enums::ConnectorTokenStatus, + + /// The reference id of the connector token + /// This is the reference that was passed to connector when creating the token + pub connector_token_request_reference_id: Option, + + pub original_payment_authorized_amount: Option, + + /// The currency of the original payment authorized amount + #[schema(value_type = Currency)] + pub original_payment_authorized_currency: Option, + + /// Metadata associated with the connector token + pub metadata: Option, + + /// The value of the connector token. This token can be used to make merchant initiated payments ( MIT ), directly with the connector. + pub token: String, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, serde::Serialize, ToSchema, Clone)] pub struct PaymentMethodResponse { /// The unique identifier of the Payment method #[schema(value_type = String, example = "12345_pm_01926c58bc6e77c09e809964e72af8c8")] @@ -893,11 +942,16 @@ pub struct PaymentMethodResponse { #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub created: Option, + /// A timestamp (ISO 8601 code) that determines when the payment method was last used #[schema(value_type = Option, example = "2024-02-24T11:04:09.922Z")] #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub last_used_at: Option, + /// The payment method details related to the payment method pub payment_method_data: Option, + + /// The connector token details if available + pub connector_tokens: Option>, } #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] @@ -2501,6 +2555,10 @@ pub struct PaymentMethodSessionRequest { #[schema(value_type = Option
)] pub billing: Option, + /// The return url to which the customer should be redirected to after adding the payment method + #[schema(value_type = Option)] + pub return_url: Option, + /// The tokenization type to be applied #[schema(value_type = Option)] pub psp_tokenization: Option, @@ -2515,6 +2573,22 @@ pub struct PaymentMethodSessionRequest { pub expires_in: Option, } +#[cfg(feature = "v2")] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct PaymentMethodsSessionUpdateRequest { + /// The billing address details of the customer. This will also be used for any new payment methods added during the session + #[schema(value_type = Option
)] + pub billing: Option, + + /// The tokenization type to be applied + #[schema(value_type = Option)] + pub psp_tokenization: Option, + + /// The network tokenization configuration if applicable + #[schema(value_type = Option)] + pub network_tokenization: Option, +} + #[cfg(feature = "v2")] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] pub struct PaymentMethodSessionUpdateSavedPaymentMethod { @@ -2529,7 +2603,27 @@ pub struct PaymentMethodSessionUpdateSavedPaymentMethod { #[cfg(feature = "v2")] #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] -pub struct PaymentMethodsSessionResponse { +pub struct PaymentMethodSessionConfirmRequest { + /// The payment method type + #[schema(value_type = PaymentMethod, example = "card")] + pub payment_method_type: common_enums::PaymentMethod, + + /// The payment method subtype + #[schema(value_type = PaymentMethodType, example = "credit")] + pub payment_method_subtype: common_enums::PaymentMethodType, + + /// The payment instrument data to be used for the payment + #[schema(value_type = PaymentMethodDataRequest)] + pub payment_method_data: payments::PaymentMethodDataRequest, + + /// The return url to which the customer should be redirected to after adding the payment method + #[schema(value_type = Option)] + pub return_url: Option, +} + +#[cfg(feature = "v2")] +#[derive(Debug, serde::Serialize, ToSchema)] +pub struct PaymentMethodSessionResponse { #[schema(value_type = String, example = "12345_pms_01926c58bc6e77c09e809964e72af8c8")] pub id: id_type::GlobalPaymentMethodSessionId, @@ -2558,4 +2652,26 @@ pub struct PaymentMethodsSessionResponse { /// Client Secret #[schema(value_type = String)] pub client_secret: masking::Secret, + + /// The return url to which the user should be redirected to + #[schema(value_type = Option)] + pub return_url: Option, + + /// The next action details for the payment method session + #[schema(value_type = Option)] + pub next_action: Option, + + /// The customer authenticaion details for the payment method + /// This refers to either the payment / external authentication details + pub authentication_details: Option, + + /// The payment method that was created using this payment method session + pub associated_payment_methods: Option>, +} + +#[cfg(feature = "v2")] +#[derive(Debug, serde::Serialize, ToSchema, Clone)] +pub struct AuthenticationDetails { + pub status: common_enums::IntentStatus, + pub error: Option, } diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 22e845c5e0..8f14dc8d86 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -586,6 +586,22 @@ pub struct AmountDetails { tax_on_surcharge: Option, } +#[cfg(feature = "v2")] +impl AmountDetails { + pub fn new_for_zero_auth_payment(currency: common_enums::Currency) -> Self { + Self { + order_amount: Amount::Zero, + currency, + shipping_cost: None, + order_tax_amount: None, + skip_external_tax_calculation: common_enums::TaxCalculationOverride::Skip, + skip_surcharge_calculation: common_enums::SurchargeCalculationOverride::Skip, + surcharge_amount: None, + tax_on_surcharge: None, + } + } +} + #[cfg(feature = "v2")] #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] pub struct AmountDetailsUpdate { @@ -1871,6 +1887,43 @@ pub struct Card { pub nick_name: Option>, } +#[cfg(feature = "v2")] +impl TryFrom for Card { + type Error = error_stack::Report; + + fn try_from(value: payment_methods::CardDetail) -> Result { + use common_utils::ext_traits::OptionExt; + + let payment_methods::CardDetail { + card_number, + card_exp_month, + card_exp_year, + card_holder_name, + nick_name, + card_network, + card_issuer, + card_cvc, + .. + } = value; + + let card_cvc = card_cvc.get_required_value("card_cvc")?; + + Ok(Self { + card_number, + card_exp_month, + card_exp_year, + card_holder_name, + card_cvc, + card_issuer, + card_network, + card_type: None, + card_issuing_country: None, + bank_code: None, + nick_name, + }) + } +} + #[derive(Default, Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct ExtendedCardInfo { /// The card number @@ -5097,6 +5150,10 @@ pub struct PaymentsConfirmIntentRequest { /// Additional details required by 3DS 2.0 #[schema(value_type = Option)] pub browser_info: Option, + + /// The payment_method_id to be associated with the payment + #[schema(value_type = Option)] + pub payment_method_id: Option, } // This struct contains the union of fields in `PaymentsCreateIntentRequest` and @@ -5233,6 +5290,10 @@ pub struct PaymentsRequest { /// Additional details required by 3DS 2.0 #[schema(value_type = Option)] pub browser_info: Option, + + /// The payment_method_id to be associated with the payment + #[schema(value_type = Option)] + pub payment_method_id: Option, } #[cfg(feature = "v2")] @@ -5281,89 +5342,11 @@ impl From<&PaymentsRequest> for PaymentsConfirmIntentRequest { shipping: request.shipping.clone(), customer_acceptance: request.customer_acceptance.clone(), browser_info: request.browser_info.clone(), + payment_method_id: request.payment_method_id.clone(), } } } -#[cfg(feature = "v2")] -#[derive(Debug, serde::Serialize, ToSchema)] -pub struct PaymentsResponse { - /// Unique identifier for the payment. This ensures idempotency for multiple payments - /// that have been done by a single merchant. - #[schema( - min_length = 32, - max_length = 64, - example = "12345_pay_01926c58bc6e77c09e809964e72af8c8", - value_type = String, - )] - pub id: id_type::GlobalPaymentId, - - #[schema(value_type = IntentStatus, example = "success")] - pub status: api_enums::IntentStatus, - - /// Amount related information for this payment and attempt - pub amount: PaymentAmountDetailsResponse, - - /// The identifier for the customer - #[schema( - min_length = 32, - max_length = 64, - example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", - value_type = String - )] - pub customer_id: Option, - - /// The connector used for the payment - #[schema(example = "stripe")] - pub connector: String, - - /// It's a token used for client side verification. - #[schema(value_type = String)] - pub client_secret: common_utils::types::ClientSecret, - - /// Time when the payment was created - #[schema(example = "2022-09-10T10:11:12Z")] - #[serde(with = "common_utils::custom_serde::iso8601")] - pub created: PrimitiveDateTime, - - /// The payment method information provided for making a payment - #[schema(value_type = Option)] - #[serde(serialize_with = "serialize_payment_method_data_response")] - pub payment_method_data: Option, - - /// The payment method type for this payment attempt - #[schema(value_type = PaymentMethod, example = "wallet")] - pub payment_method_type: api_enums::PaymentMethod, - - #[schema(value_type = PaymentMethodType, example = "apple_pay")] - pub payment_method_subtype: api_enums::PaymentMethodType, - - /// Additional information required for redirection - pub next_action: Option, - - /// A unique identifier for a payment provided by the connector - #[schema(value_type = Option, example = "993672945374576J")] - pub connector_transaction_id: Option, - - /// reference(Identifier) to the payment at connector side - #[schema(value_type = Option, example = "993672945374576J")] - pub connector_reference_id: Option, - - /// Connector token information that can be used to make payments directly by the merchant. - pub connector_token_details: Option, - - /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment - #[schema(value_type = String)] - pub merchant_connector_id: id_type::MerchantConnectorAccountId, - - /// The browser information used for this payment - #[schema(value_type = Option)] - pub browser_info: Option, - - /// Error details for the payment if any - pub error: Option, -} - // Serialize is implemented because, this will be serialized in the api events. // Usually request types should not have serialize implemented. // @@ -5400,94 +5383,6 @@ pub struct ErrorDetails { pub unified_message: Option, } -/// Response for Payment Intent Confirm -#[cfg(feature = "v2")] -#[derive(Debug, serde::Serialize, ToSchema)] -pub struct PaymentsConfirmIntentResponse { - /// Unique identifier for the payment. This ensures idempotency for multiple payments - /// that have been done by a single merchant. - #[schema( - min_length = 32, - max_length = 64, - example = "12345_pay_01926c58bc6e77c09e809964e72af8c8", - value_type = String, - )] - pub id: id_type::GlobalPaymentId, - - #[schema(value_type = IntentStatus, example = "success")] - pub status: api_enums::IntentStatus, - - /// Amount related information for this payment and attempt - pub amount: PaymentAmountDetailsResponse, - - /// The identifier for the customer - #[schema( - min_length = 32, - max_length = 64, - example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", - value_type = String - )] - pub customer_id: Option, - - /// The connector used for the payment - #[schema(example = "stripe")] - pub connector: String, - - /// It's a token used for client side verification. - #[schema(value_type = String)] - pub client_secret: common_utils::types::ClientSecret, - - /// Time when the payment was created - #[schema(example = "2022-09-10T10:11:12Z")] - #[serde(with = "common_utils::custom_serde::iso8601")] - pub created: PrimitiveDateTime, - - /// The payment method information provided for making a payment - #[schema(value_type = Option)] - #[serde(serialize_with = "serialize_payment_method_data_response")] - pub payment_method_data: Option, - - /// The payment method type for this payment attempt - #[schema(value_type = PaymentMethod, example = "wallet")] - pub payment_method_type: api_enums::PaymentMethod, - - #[schema(value_type = PaymentMethodType, example = "apple_pay")] - pub payment_method_subtype: api_enums::PaymentMethodType, - - /// Additional information required for redirection - pub next_action: Option, - - /// A unique identifier for a payment provided by the connector - #[schema(value_type = Option, example = "993672945374576J")] - pub connector_transaction_id: Option, - - /// reference(Identifier) to the payment at connector side - #[schema(value_type = Option, example = "993672945374576J")] - pub connector_reference_id: Option, - - /// Connector token information that can be used to make payments directly by the merchant. - pub connector_token_details: Option, - - /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment - #[schema(value_type = String)] - pub merchant_connector_id: id_type::MerchantConnectorAccountId, - - /// The browser information used for this payment - #[schema(value_type = Option)] - pub browser_info: Option, - - /// Error details for the payment if any - pub error: Option, - - /// The transaction authentication can be set to undergo payer authentication. By default, the authentication will be marked as NO_THREE_DS - #[schema(value_type = Option, example = "no_three_ds")] - pub authentication_type: Option, - - /// The authentication type applied for the payment - #[schema(value_type = AuthenticationType, example = "no_three_ds")] - pub applied_authentication_type: api_enums::AuthenticationType, -} - /// Token information that can be used to initiate transactions by the merchant. #[cfg(feature = "v2")] #[derive(Debug, Serialize, ToSchema)] @@ -5495,13 +5390,19 @@ pub struct ConnectorTokenDetails { /// A token that can be used to make payments directly with the connector. #[schema(example = "pm_9UhMqBMEOooRIvJFFdeW")] pub token: String, + + /// The reference id sent to the connector when creating the token + pub connector_token_request_reference_id: Option, } -// TODO: have a separate response for detailed, summarized /// Response for Payment Intent Confirm +/// Few fields should be expandable, we need not return these in the normal response +/// But when explictly requested for expanded objects, these can be returned +/// For example +/// shipping, billing, customer, payment_method #[cfg(feature = "v2")] -#[derive(Debug, serde::Serialize, Clone, ToSchema)] -pub struct PaymentsRetrieveResponse { +#[derive(Debug, serde::Serialize, ToSchema)] +pub struct PaymentsResponse { /// Unique identifier for the payment. This ensures idempotency for multiple payments /// that have been done by a single merchant. #[schema( @@ -5579,10 +5480,33 @@ pub struct PaymentsRetrieveResponse { /// List of payment attempts associated with payment intent pub attempts: Option>, + + /// Connector token information that can be used to make payments directly by the merchant. + pub connector_token_details: Option, + + /// The payment_method_id associated with the payment + #[schema(value_type = Option)] + pub payment_method_id: Option, + + /// Additional information required for redirection + pub next_action: Option, + + /// The url to which user must be redirected to after completion of the purchase + #[schema(value_type = Option)] + pub return_url: Option, + + /// The authentication type that was requested for this order + #[schema(value_type = Option, example = "no_three_ds", default = "no_three_ds")] + pub authentication_type: Option, + + /// The authentication type that was appliced for this order + /// This depeneds on the 3DS rules configured, If not a default authentication type will be applied + #[schema(value_type = Option, example = "no_three_ds", default = "no_three_ds")] + pub authentication_type_applied: Option, } #[cfg(feature = "v2")] -impl PaymentsRetrieveResponse { +impl PaymentsResponse { pub fn find_attempt_in_attempts_list_using_connector_transaction_id( self, connector_transaction_id: &common_utils::types::ConnectorTransactionId, diff --git a/crates/api_models/src/webhooks.rs b/crates/api_models/src/webhooks.rs index c3e96c47c6..d5404d57aa 100644 --- a/crates/api_models/src/webhooks.rs +++ b/crates/api_models/src/webhooks.rs @@ -266,7 +266,7 @@ pub struct IncomingWebhookDetails { pub resource_object: Vec, } -#[derive(Debug, Clone, Serialize, ToSchema)] +#[derive(Debug, Serialize, ToSchema)] pub struct OutgoingWebhook { /// The merchant id of the merchant #[schema(value_type = String)] @@ -304,12 +304,12 @@ pub enum OutgoingWebhookContent { PayoutDetails(Box), } -#[derive(Debug, Clone, Serialize, ToSchema)] +#[derive(Debug, Serialize, ToSchema)] #[serde(tag = "type", content = "object", rename_all = "snake_case")] #[cfg(feature = "v2")] pub enum OutgoingWebhookContent { - #[schema(value_type = PaymentsRetrieveResponse, title = "PaymentsResponse")] - PaymentDetails(Box), + #[schema(value_type = PaymentsResponse, title = "PaymentsResponse")] + PaymentDetails(Box), #[schema(value_type = RefundResponse, title = "RefundResponse")] RefundDetails(Box), #[schema(value_type = DisputeResponse, title = "DisputeResponse")] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 660474e91e..27a4e82066 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -7379,6 +7379,19 @@ pub enum ConnectorMandateStatus { Inactive, } +/// Connector Mandate Status +#[derive( + Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, strum::Display, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum ConnectorTokenStatus { + /// Indicates that the connector mandate is active and can be used for payments. + Active, + /// Indicates that the connector mandate is not active and hence cannot be used for payments. + Inactive, +} + #[derive( Clone, Copy, diff --git a/crates/common_types/src/payment_methods.rs b/crates/common_types/src/payment_methods.rs index c6b492a103..79cf34e110 100644 --- a/crates/common_types/src/payment_methods.rs +++ b/crates/common_types/src/payment_methods.rs @@ -162,6 +162,6 @@ pub struct PspTokenization { pub tokenization_type: common_enums::TokenizationType, /// The merchant connector id to be used for tokenization - #[schema(value_type = String)] - pub connector_id: common_utils::id_type::MerchantConnectorAccountId, + #[schema(value_type = Option)] + pub connector_id: Option, } diff --git a/crates/common_utils/src/macros.rs b/crates/common_utils/src/macros.rs index fe1289acba..339cf236c7 100644 --- a/crates/common_utils/src/macros.rs +++ b/crates/common_utils/src/macros.rs @@ -157,8 +157,10 @@ mod id_type { serde::Serialize, serde::Deserialize, diesel::expression::AsExpression, + utoipa::ToSchema, )] #[diesel(sql_type = $diesel_type)] + #[schema(value_type = String)] pub struct $type($crate::id_type::LengthId<$max_length, $min_length>); }; ($type:ident, $doc:literal) => { diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 9409be9416..2ac3179a71 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -236,7 +236,7 @@ impl ConnectorTransactionIdTrait for PaymentAttempt { #[diesel(sql_type = diesel::sql_types::Jsonb)] pub struct ConnectorTokenDetails { pub connector_mandate_id: Option, - pub connector_mandate_request_reference_id: Option, + pub connector_token_request_reference_id: Option, } #[cfg(feature = "v2")] @@ -244,8 +244,8 @@ common_utils::impl_to_sql_from_sql_json!(ConnectorTokenDetails); #[cfg(feature = "v2")] impl ConnectorTokenDetails { - pub fn get_connector_mandate_request_reference_id(&self) -> Option { - self.connector_mandate_request_reference_id.clone() + pub fn get_connector_token_request_reference_id(&self) -> Option { + self.connector_token_request_reference_id.clone() } } diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index e8c6e1737b..47ac152d9e 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -286,7 +286,7 @@ pub enum PaymentMethodUpdate { StatusUpdate { status: Option, }, - AdditionalDataUpdate { + GenericUpdate { payment_method_data: Option, status: Option, locker_id: Option, @@ -296,6 +296,7 @@ pub enum PaymentMethodUpdate { network_token_locker_id: Option, network_token_payment_method_data: Option, locker_fingerprint_id: Option, + connector_mandate_details: Option, }, ConnectorMandateDetailsUpdate { connector_mandate_details: Option, @@ -791,7 +792,7 @@ impl From for PaymentMethodUpdateInternal { network_token_payment_method_data: None, locker_fingerprint_id: None, }, - PaymentMethodUpdate::AdditionalDataUpdate { + PaymentMethodUpdate::GenericUpdate { payment_method_data, status, locker_id, @@ -801,6 +802,7 @@ impl From for PaymentMethodUpdateInternal { network_token_locker_id, network_token_payment_method_data, locker_fingerprint_id, + connector_mandate_details, } => Self { payment_method_data, last_used_at: None, @@ -808,7 +810,7 @@ impl From for PaymentMethodUpdateInternal { status, locker_id, payment_method_type_v2, - connector_mandate_details: None, + connector_mandate_details, updated_by: None, payment_method_subtype, last_modified: common_utils::date_time::now(), @@ -943,22 +945,24 @@ pub struct PaymentsMandateReferenceRecord { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct PaymentsMandateReferenceRecord { - pub connector_mandate_id: String, +pub struct ConnectorTokenReferenceRecord { + pub connector_token: String, pub payment_method_subtype: Option, - pub original_payment_authorized_amount: Option, + pub original_payment_authorized_amount: Option, pub original_payment_authorized_currency: Option, - pub mandate_metadata: Option, - pub connector_mandate_status: Option, - pub connector_mandate_request_reference_id: Option, + pub metadata: Option, + pub connector_token_status: common_enums::ConnectorTokenStatus, + pub connector_token_request_reference_id: Option, } +#[cfg(feature = "v1")] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, diesel::AsExpression)] #[diesel(sql_type = diesel::sql_types::Jsonb)] pub struct PaymentsMandateReference( pub HashMap, ); +#[cfg(feature = "v1")] impl std::ops::Deref for PaymentsMandateReference { type Target = HashMap; @@ -968,14 +972,43 @@ impl std::ops::Deref for PaymentsMandateReference { } } +#[cfg(feature = "v1")] impl std::ops::DerefMut for PaymentsMandateReference { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } +#[cfg(feature = "v2")] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, diesel::AsExpression)] +#[diesel(sql_type = diesel::sql_types::Jsonb)] +pub struct PaymentsTokenReference( + pub HashMap, +); + +#[cfg(feature = "v2")] +impl std::ops::Deref for PaymentsTokenReference { + type Target = + HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(feature = "v2")] +impl std::ops::DerefMut for PaymentsTokenReference { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[cfg(feature = "v1")] common_utils::impl_to_sql_from_sql_json!(PaymentsMandateReference); +#[cfg(feature = "v2")] +common_utils::impl_to_sql_from_sql_json!(PaymentsTokenReference); + #[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct PayoutsMandateReferenceRecord { pub transfer_method_id: Option, @@ -1002,6 +1035,7 @@ impl std::ops::DerefMut for PayoutsMandateReference { } } +#[cfg(feature = "v1")] #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, diesel::AsExpression)] #[diesel(sql_type = diesel::sql_types::Jsonb)] pub struct CommonMandateReference { @@ -1009,6 +1043,14 @@ pub struct CommonMandateReference { pub payouts: Option, } +#[cfg(feature = "v2")] +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, diesel::AsExpression)] +#[diesel(sql_type = diesel::sql_types::Jsonb)] +pub struct CommonMandateReference { + pub payments: Option, + pub payouts: Option, +} + impl CommonMandateReference { pub fn get_mandate_details_value(&self) -> CustomResult { let mut payments = self @@ -1031,6 +1073,25 @@ impl CommonMandateReference { Ok(payments) } + + #[cfg(feature = "v2")] + /// Insert a new payment token reference for the given connector_id + pub fn insert_payment_token_reference_record( + &mut self, + connector_id: &common_utils::id_type::MerchantConnectorAccountId, + record: ConnectorTokenReferenceRecord, + ) { + match self.payments { + Some(ref mut payments_reference) => { + payments_reference.insert(connector_id.clone(), record); + } + None => { + let mut payments_reference = HashMap::new(); + payments_reference.insert(connector_id.clone(), record); + self.payments = Some(PaymentsTokenReference(payments_reference)); + } + } + } } impl diesel::serialize::ToSql for CommonMandateReference { @@ -1047,6 +1108,7 @@ impl diesel::serialize::ToSql for Comm } } +#[cfg(feature = "v1")] impl diesel::deserialize::FromSql for CommonMandateReference where @@ -1095,6 +1157,56 @@ where } } +#[cfg(feature = "v2")] +impl diesel::deserialize::FromSql + for CommonMandateReference +where + serde_json::Value: diesel::deserialize::FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result { + let value = >::from_sql(bytes)?; + + let payments_data = value + .clone() + .as_object_mut() + .map(|obj| { + obj.remove("payouts"); + + serde_json::from_value::(serde_json::Value::Object( + obj.clone(), + )) + .inspect_err(|err| { + router_env::logger::error!("Failed to parse payments data: {}", err); + }) + .change_context(ParsingError::StructParseFailure( + "Failed to parse payments data", + )) + }) + .transpose()?; + + let payouts_data = serde_json::from_value::>(value) + .inspect_err(|err| { + router_env::logger::error!("Failed to parse payouts data: {}", err); + }) + .change_context(ParsingError::StructParseFailure( + "Failed to parse payouts data", + )) + .map(|optional_common_mandate_details| { + optional_common_mandate_details + .and_then(|common_mandate_details| common_mandate_details.payouts) + })?; + + Ok(Self { + payments: payments_data, + payouts: payouts_data, + }) + } +} + +#[cfg(feature = "v1")] impl From for CommonMandateReference { fn from(payment_reference: PaymentsMandateReference) -> Self { Self { diff --git a/crates/diesel_models/src/payment_methods_session.rs b/crates/diesel_models/src/payment_methods_session.rs index dad3e995e0..250fd44bdb 100644 --- a/crates/diesel_models/src/payment_methods_session.rs +++ b/crates/diesel_models/src/payment_methods_session.rs @@ -1,11 +1,50 @@ #[cfg(feature = "v2")] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct PaymentMethodsSession { +pub struct PaymentMethodSession { pub id: common_utils::id_type::GlobalPaymentMethodSessionId, pub customer_id: common_utils::id_type::GlobalCustomerId, pub billing: Option, pub psp_tokenization: Option, - pub network_tokeinzation: Option, + pub network_tokenization: Option, + pub return_url: Option, #[serde(with = "common_utils::custom_serde::iso8601")] pub expires_at: time::PrimitiveDateTime, + pub associated_payment_methods: Option>, + pub associated_payment: Option, +} + +#[cfg(feature = "v2")] +impl PaymentMethodSession { + pub fn apply_changeset(self, update_session: PaymentMethodsSessionUpdateInternal) -> Self { + let Self { + id, + customer_id, + billing, + psp_tokenization, + network_tokenization, + expires_at, + return_url, + associated_payment_methods, + associated_payment, + } = self; + + Self { + id, + customer_id, + billing: update_session.billing.or(billing), + psp_tokenization: update_session.psp_tokenization.or(psp_tokenization), + network_tokenization: update_session.network_tokenization.or(network_tokenization), + expires_at, + return_url, + associated_payment_methods, + associated_payment, + } + } +} + +#[cfg(feature = "v2")] +pub struct PaymentMethodsSessionUpdateInternal { + pub billing: Option, + pub psp_tokenization: Option, + pub network_tokenization: Option, } diff --git a/crates/hyperswitch_domain_models/src/api.rs b/crates/hyperswitch_domain_models/src/api.rs index d8a4f998e1..571371dc63 100644 --- a/crates/hyperswitch_domain_models/src/api.rs +++ b/crates/hyperswitch_domain_models/src/api.rs @@ -20,6 +20,28 @@ pub enum ApplicationResponse { GenericLinkForm(Box), } +impl ApplicationResponse { + /// Get the json response from response + #[inline] + pub fn get_json_body( + self, + ) -> common_utils::errors::CustomResult { + match self { + Self::Json(body) | Self::JsonWithHeaders((body, _)) => Ok(body), + Self::TextPlain(_) + | Self::JsonForRedirection(_) + | Self::Form(_) + | Self::PaymentLinkForm(_) + | Self::FileData(_) + | Self::GenericLinkForm(_) + | Self::StatusOk => Err(common_utils::errors::ValidationError::InvalidValue { + message: "expected either Json or JsonWithHeaders Response".to_string(), + } + .into()), + } + } +} + impl ApiEventMetric for ApplicationResponse { fn get_api_event_type(&self) -> Option { match self { diff --git a/crates/hyperswitch_domain_models/src/mandates.rs b/crates/hyperswitch_domain_models/src/mandates.rs index 6ab7a59089..27ede3ba18 100644 --- a/crates/hyperswitch_domain_models/src/mandates.rs +++ b/crates/hyperswitch_domain_models/src/mandates.rs @@ -279,14 +279,14 @@ pub struct PaymentsMandateReferenceRecord { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct PaymentsMandateReferenceRecord { - pub connector_mandate_id: String, +pub struct ConnectorTokenReferenceRecord { + pub connector_token: String, pub payment_method_subtype: Option, - pub original_payment_authorized_amount: Option, + pub original_payment_authorized_amount: Option, pub original_payment_authorized_currency: Option, - pub mandate_metadata: Option, - pub connector_mandate_status: Option, - pub connector_mandate_request_reference_id: Option, + pub metadata: Option, + pub connector_token_status: common_enums::ConnectorTokenStatus, + pub connector_token_request_reference_id: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -314,11 +314,19 @@ impl std::ops::DerefMut for PayoutsMandateReference { } } +#[cfg(feature = "v2")] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentsTokenReference( + pub HashMap, +); + +#[cfg(feature = "v1")] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentsMandateReference( pub HashMap, ); +#[cfg(feature = "v1")] impl std::ops::Deref for PaymentsMandateReference { type Target = HashMap; @@ -328,18 +336,44 @@ impl std::ops::Deref for PaymentsMandateReference { } } +#[cfg(feature = "v1")] impl std::ops::DerefMut for PaymentsMandateReference { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } +#[cfg(feature = "v2")] +impl std::ops::Deref for PaymentsTokenReference { + type Target = + HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(feature = "v2")] +impl std::ops::DerefMut for PaymentsTokenReference { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[cfg(feature = "v1")] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct CommonMandateReference { pub payments: Option, pub payouts: Option, } +#[cfg(feature = "v2")] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct CommonMandateReference { + pub payments: Option, + pub payouts: Option, +} + impl CommonMandateReference { pub fn get_mandate_details_value(&self) -> CustomResult { let mut payments = self @@ -362,6 +396,25 @@ impl CommonMandateReference { Ok(payments) } + + #[cfg(feature = "v2")] + /// Insert a new payment token reference for the given connector_id + pub fn insert_payment_token_reference_record( + &mut self, + connector_id: &common_utils::id_type::MerchantConnectorAccountId, + record: ConnectorTokenReferenceRecord, + ) { + match self.payments { + Some(ref mut payments_reference) => { + payments_reference.insert(connector_id.clone(), record); + } + None => { + let mut payments_reference = HashMap::new(); + payments_reference.insert(connector_id.clone(), record); + self.payments = Some(PaymentsTokenReference(payments_reference)); + } + } + } } impl From for CommonMandateReference { @@ -406,6 +459,7 @@ impl From for diesel_models::PayoutsMandateReference { } } +#[cfg(feature = "v1")] impl From for PaymentsMandateReference { fn from(value: diesel_models::PaymentsMandateReference) -> Self { Self( @@ -418,6 +472,7 @@ impl From for PaymentsMandateReference } } +#[cfg(feature = "v1")] impl From for diesel_models::PaymentsMandateReference { fn from(value: PaymentsMandateReference) -> Self { Self( @@ -430,6 +485,32 @@ impl From for diesel_models::PaymentsMandateReference } } +#[cfg(feature = "v2")] +impl From for PaymentsTokenReference { + fn from(value: diesel_models::PaymentsTokenReference) -> Self { + Self( + value + .0 + .into_iter() + .map(|(key, record)| (key, record.into())) + .collect(), + ) + } +} + +#[cfg(feature = "v2")] +impl From for diesel_models::PaymentsTokenReference { + fn from(value: PaymentsTokenReference) -> Self { + Self( + value + .0 + .into_iter() + .map(|(key, record)| (key, record.into())) + .collect(), + ) + } +} + impl From for PayoutsMandateReferenceRecord { fn from(value: diesel_models::PayoutsMandateReferenceRecord) -> Self { Self { @@ -446,10 +527,31 @@ impl From for diesel_models::PayoutsMandateRefere } } -#[cfg(all( - any(feature = "v1", feature = "v2"), - not(feature = "payment_methods_v2") -))] +#[cfg(feature = "v2")] +impl From for ConnectorTokenReferenceRecord { + fn from(value: diesel_models::ConnectorTokenReferenceRecord) -> Self { + let diesel_models::ConnectorTokenReferenceRecord { + connector_token, + payment_method_subtype, + original_payment_authorized_amount, + original_payment_authorized_currency, + metadata, + connector_token_status, + connector_token_request_reference_id, + } = value; + Self { + connector_token, + payment_method_subtype, + original_payment_authorized_amount, + original_payment_authorized_currency, + metadata, + connector_token_status, + connector_token_request_reference_id, + } + } +} + +#[cfg(feature = "v1")] impl From for PaymentsMandateReferenceRecord { fn from(value: diesel_models::PaymentsMandateReferenceRecord) -> Self { Self { @@ -464,10 +566,31 @@ impl From for PaymentsMandateRefe } } -#[cfg(all( - any(feature = "v1", feature = "v2"), - not(feature = "payment_methods_v2") -))] +#[cfg(feature = "v2")] +impl From for diesel_models::ConnectorTokenReferenceRecord { + fn from(value: ConnectorTokenReferenceRecord) -> Self { + let ConnectorTokenReferenceRecord { + connector_token, + payment_method_subtype, + original_payment_authorized_amount, + original_payment_authorized_currency, + metadata, + connector_token_status, + connector_token_request_reference_id, + } = value; + Self { + connector_token, + payment_method_subtype, + original_payment_authorized_amount, + original_payment_authorized_currency, + metadata, + connector_token_status, + connector_token_request_reference_id, + } + } +} + +#[cfg(feature = "v1")] impl From for diesel_models::PaymentsMandateReferenceRecord { fn from(value: PaymentsMandateReferenceRecord) -> Self { Self { @@ -481,33 +604,3 @@ impl From for diesel_models::PaymentsMandateRefe } } } - -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -impl From for PaymentsMandateReferenceRecord { - fn from(value: diesel_models::PaymentsMandateReferenceRecord) -> Self { - Self { - connector_mandate_id: value.connector_mandate_id, - payment_method_subtype: value.payment_method_subtype, - original_payment_authorized_amount: value.original_payment_authorized_amount, - original_payment_authorized_currency: value.original_payment_authorized_currency, - mandate_metadata: value.mandate_metadata, - connector_mandate_status: value.connector_mandate_status, - connector_mandate_request_reference_id: value.connector_mandate_request_reference_id, - } - } -} - -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -impl From for diesel_models::PaymentsMandateReferenceRecord { - fn from(value: PaymentsMandateReferenceRecord) -> Self { - Self { - connector_mandate_id: value.connector_mandate_id, - payment_method_subtype: value.payment_method_subtype, - original_payment_authorized_amount: value.original_payment_authorized_amount, - original_payment_authorized_currency: value.original_payment_authorized_currency, - mandate_metadata: value.mandate_metadata, - connector_mandate_status: value.connector_mandate_status, - connector_mandate_request_reference_id: value.connector_mandate_request_reference_id, - } - } -} diff --git a/crates/hyperswitch_domain_models/src/merchant_account.rs b/crates/hyperswitch_domain_models/src/merchant_account.rs index c42b000551..e010aeffeb 100644 --- a/crates/hyperswitch_domain_models/src/merchant_account.rs +++ b/crates/hyperswitch_domain_models/src/merchant_account.rs @@ -244,14 +244,13 @@ pub enum MerchantAccountUpdate { } #[cfg(feature = "v2")] -#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone)] pub enum MerchantAccountUpdate { Update { merchant_name: OptionalEncryptableName, merchant_details: OptionalEncryptableValue, publishable_key: Option, - metadata: Option, + metadata: Option>, }, StorageSchemeUpdate { storage_scheme: MerchantStorageScheme, @@ -477,7 +476,7 @@ impl From for MerchantAccountUpdateInternal { merchant_name: merchant_name.map(Encryption::from), merchant_details: merchant_details.map(Encryption::from), publishable_key, - metadata, + metadata: metadata.map(|metadata| *metadata), modified_at: now, storage_scheme: None, organization_id: None, diff --git a/crates/hyperswitch_domain_models/src/payment_methods.rs b/crates/hyperswitch_domain_models/src/payment_methods.rs index c8683919f7..5257015d74 100644 --- a/crates/hyperswitch_domain_models/src/payment_methods.rs +++ b/crates/hyperswitch_domain_models/src/payment_methods.rs @@ -21,7 +21,7 @@ use time::PrimitiveDateTime; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use crate::{address::Address, type_encryption::OptionalEncryptableJsonType}; use crate::{ - mandates::{CommonMandateReference, PaymentsMandateReference}, + mandates::{self, CommonMandateReference}, type_encryption::{crypto_operation, AsyncLift, CryptoOperation}, }; @@ -169,11 +169,10 @@ impl PaymentMethod { .as_object_mut() .map(|obj| obj.remove("payouts")); - serde_json::from_value::(mandate_details).inspect_err( - |err| { + serde_json::from_value::(mandate_details) + .inspect_err(|err| { router_env::logger::error!("Failed to parse payments data: {:?}", err); - }, - ) + }) }) .transpose() .map_err(|err| { @@ -218,6 +217,19 @@ impl PaymentMethod { }) } } + + #[cfg(feature = "v2")] + pub fn set_payment_method_type(&mut self, payment_method_type: common_enums::PaymentMethod) { + self.payment_method_type = Some(payment_method_type); + } + + #[cfg(feature = "v2")] + pub fn set_payment_method_subtype( + &mut self, + payment_method_subtype: common_enums::PaymentMethodType, + ) { + self.payment_method_subtype = Some(payment_method_subtype); + } } #[cfg(all( @@ -569,29 +581,35 @@ impl super::behaviour::Conversion for PaymentMethod { #[cfg(feature = "v2")] #[derive(Clone, Debug, router_derive::ToEncryption)] -pub struct PaymentMethodsSession { +pub struct PaymentMethodSession { pub id: common_utils::id_type::GlobalPaymentMethodSessionId, pub customer_id: common_utils::id_type::GlobalCustomerId, #[encrypt(ty = Value)] pub billing: Option>, + pub return_url: Option, pub psp_tokenization: Option, pub network_tokenization: Option, pub expires_at: PrimitiveDateTime, + pub associated_payment_methods: Option>, + pub associated_payment: Option, } #[cfg(feature = "v2")] #[async_trait::async_trait] -impl super::behaviour::Conversion for PaymentMethodsSession { - type DstType = diesel_models::payment_methods_session::PaymentMethodsSession; - type NewDstType = diesel_models::payment_methods_session::PaymentMethodsSession; +impl super::behaviour::Conversion for PaymentMethodSession { + type DstType = diesel_models::payment_methods_session::PaymentMethodSession; + type NewDstType = diesel_models::payment_methods_session::PaymentMethodSession; async fn convert(self) -> CustomResult { Ok(Self::DstType { id: self.id, customer_id: self.customer_id, billing: self.billing.map(|val| val.into()), psp_tokenization: self.psp_tokenization, - network_tokeinzation: self.network_tokenization, + network_tokenization: self.network_tokenization, expires_at: self.expires_at, + associated_payment_methods: self.associated_payment_methods, + associated_payment: self.associated_payment, + return_url: self.return_url, }) } @@ -610,8 +628,8 @@ impl super::behaviour::Conversion for PaymentMethodsSession { let decrypted_data = crypto_operation( state, type_name!(Self::DstType), - CryptoOperation::BatchDecrypt(EncryptedPaymentMethodsSession::to_encryptable( - EncryptedPaymentMethodsSession { + CryptoOperation::BatchDecrypt(EncryptedPaymentMethodSession::to_encryptable( + EncryptedPaymentMethodSession { billing: storage_model.billing, }, )), @@ -621,7 +639,7 @@ impl super::behaviour::Conversion for PaymentMethodsSession { .await .and_then(|val| val.try_into_batchoperation())?; - let data = EncryptedPaymentMethodsSession::from_encryptable(decrypted_data) + let data = EncryptedPaymentMethodSession::from_encryptable(decrypted_data) .change_context(common_utils::errors::CryptoError::DecodingFailed) .attach_printable("Invalid batch operation data")?; @@ -639,8 +657,11 @@ impl super::behaviour::Conversion for PaymentMethodsSession { customer_id: storage_model.customer_id, billing, psp_tokenization: storage_model.psp_tokenization, - network_tokenization: storage_model.network_tokeinzation, + network_tokenization: storage_model.network_tokenization, expires_at: storage_model.expires_at, + associated_payment_methods: storage_model.associated_payment_methods, + associated_payment: storage_model.associated_payment, + return_url: storage_model.return_url, }) } .await @@ -655,12 +676,76 @@ impl super::behaviour::Conversion for PaymentMethodsSession { customer_id: self.customer_id, billing: self.billing.map(|val| val.into()), psp_tokenization: self.psp_tokenization, - network_tokeinzation: self.network_tokenization, + network_tokenization: self.network_tokenization, expires_at: self.expires_at, + associated_payment_methods: self.associated_payment_methods, + associated_payment: self.associated_payment, + return_url: self.return_url, }) } } +#[cfg(feature = "v2")] +pub enum PaymentMethodsSessionUpdateEnum { + GeneralUpdate { + billing: Option>, + psp_tokenization: Option, + network_tokenization: Option, + }, +} + +#[cfg(feature = "v2")] +impl From for PaymentMethodsSessionUpdateInternal { + fn from(update: PaymentMethodsSessionUpdateEnum) -> Self { + match update { + PaymentMethodsSessionUpdateEnum::GeneralUpdate { + billing, + psp_tokenization, + network_tokenization, + } => Self { + billing, + psp_tokenization, + network_tokenization, + }, + } + } +} + +#[cfg(feature = "v2")] +impl PaymentMethodSession { + pub fn apply_changeset(self, update_session: PaymentMethodsSessionUpdateInternal) -> Self { + let Self { + id, + customer_id, + billing, + psp_tokenization, + network_tokenization, + expires_at, + return_url, + associated_payment_methods, + associated_payment, + } = self; + Self { + id, + customer_id, + billing: update_session.billing.or(billing), + psp_tokenization: update_session.psp_tokenization.or(psp_tokenization), + network_tokenization: update_session.network_tokenization.or(network_tokenization), + expires_at, + return_url, + associated_payment_methods, + associated_payment, + } + } +} + +#[cfg(feature = "v2")] +pub struct PaymentMethodsSessionUpdateInternal { + pub billing: Option>, + pub psp_tokenization: Option, + pub network_tokenization: Option, +} + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index f512711987..bce9868270 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -552,7 +552,7 @@ impl PaymentAttempt { profile_id: payment_intent.profile_id.clone(), organization_id: payment_intent.organization_id.clone(), payment_method_type: request.payment_method_type, - payment_method_id: None, + payment_method_id: request.payment_method_id.clone(), connector_payment_id: None, payment_method_subtype: request.payment_method_subtype, authentication_applied: None, @@ -561,7 +561,7 @@ impl PaymentAttempt { error: None, connector_token_details: Some(diesel_models::ConnectorTokenDetails { connector_mandate_id: None, - connector_mandate_request_reference_id: Some(common_utils::generate_id_with_len( + connector_token_request_reference_id: Some(common_utils::generate_id_with_len( consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH, )), }), diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index e3a8264217..5c41541dee 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -535,8 +535,7 @@ impl .connector_token_details .as_ref() .and_then(|token_details| { - token_details - .get_connector_mandate_request_reference_id() + token_details.get_connector_token_request_reference_id() }), ), }, @@ -1188,8 +1187,7 @@ impl .connector_token_details .as_ref() .and_then(|token_details| { - token_details - .get_connector_mandate_request_reference_id() + token_details.get_connector_token_request_reference_id() }), ), }, diff --git a/crates/hyperswitch_domain_models/src/router_response_types.rs b/crates/hyperswitch_domain_models/src/router_response_types.rs index 86589ad680..3009d5521d 100644 --- a/crates/hyperswitch_domain_models/src/router_response_types.rs +++ b/crates/hyperswitch_domain_models/src/router_response_types.rs @@ -225,7 +225,7 @@ impl PaymentsResponseData { diesel_models::ConnectorTokenDetails { connector_mandate_id, - connector_mandate_request_reference_id, + connector_token_request_reference_id: connector_mandate_request_reference_id, } }) } else { diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index ac432fab96..0c84f67cb4 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -143,6 +143,7 @@ Never share your secret api keys. Keep them guarded and secure. routes::payment_method::payment_method_session_retrieve, routes::payment_method::payment_method_session_list_payment_methods, routes::payment_method::payment_method_session_update_saved_payment_method, + routes::payment_method::payment_method_session_confirm, //Routes for refunds routes::refunds::refunds_create, @@ -504,7 +505,6 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::ExternalAuthenticationDetailsResponse, api_models::payments::ExtendedCardInfo, api_models::payments::PaymentsConfirmIntentRequest, - api_models::payments::PaymentsConfirmIntentResponse, api_models::payments::AmountDetailsResponse, api_models::payments::BankCodeResponse, api_models::payments::Order, @@ -521,8 +521,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::PaymentMethodCollectLinkResponse, api_models::payment_methods::PaymentMethodSubtypeSpecificData, api_models::payment_methods::PaymentMethodSessionRequest, - api_models::payment_methods::PaymentMethodsSessionResponse, - api_models::payments::PaymentsRetrieveResponse, + api_models::payment_methods::PaymentMethodSessionResponse, + api_models::payment_methods::PaymentMethodsSessionUpdateRequest, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, api_models::payments::AmountFilter, @@ -689,6 +689,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::enums::TokenizationType, api_models::enums::NetworkTokenizationToggle, api_models::payments::PaymentAmountDetailsResponse, + api_models::payment_methods::PaymentMethodSessionConfirmRequest, + api_models::payment_methods::PaymentMethodSessionResponse, routes::payments::ForceSync, )), modifiers(&SecurityAddon) diff --git a/crates/openapi/src/routes/payment_method.rs b/crates/openapi/src/routes/payment_method.rs index 1531832239..2a309848a7 100644 --- a/crates/openapi/src/routes/payment_method.rs +++ b/crates/openapi/src/routes/payment_method.rs @@ -360,7 +360,7 @@ pub async fn list_customer_payment_method_api() {} ))) ), responses( - (status = 200, description = "Create the payment method session", body = PaymentMethodsSessionResponse), + (status = 200, description = "Create the payment method session", body = PaymentMethodSessionResponse), (status = 400, description = "The request is invalid") ), tag = "Payment Method Session", @@ -380,7 +380,7 @@ pub fn payment_method_session_create() {} ("id" = String, Path, description = "The unique identifier for the Payment Method Session"), ), responses( - (status = 200, description = "The payment method session is retrieved successfully", body = PaymentMethodsSessionResponse), + (status = 200, description = "The payment method session is retrieved successfully", body = PaymentMethodSessionResponse), (status = 404, description = "The request is invalid") ), tag = "Payment Method Session", @@ -443,3 +443,48 @@ pub fn payment_method_session_list_payment_methods() {} security(("ephemeral_key" = [])) )] pub fn payment_method_session_update_saved_payment_method() {} + +/// Payment Method Session - Confirm a payment method session +/// +/// **Confirms a payment method session object with the payment method data** +#[utoipa::path( + post, + path = "/v2/payment-method-session/:id/confirm", + params (("id" = String, Path, description = "The unique identifier for the Payment Method Session"), + ( + "X-Profile-Id" = String, Header, + description = "Profile ID associated to the payment intent", + example = "pro_abcdefghijklmnop" + ) + ), + request_body( + content = PaymentMethodSessionConfirmRequest, + examples( + ( + "Confirm the payment method session with card details" = ( + value = json!({ + "payment_method_type": "card", + "payment_method_subtype": "credit", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_cvc": "123" + } + }, + }) + ) + ), + ), + ), + responses( + (status = 200, description = "Payment Method created", body = PaymentMethodResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Payment Method Session", + operation_id = "Confirm the payment method session", + security(("publishable_key" = [])), +)] +#[cfg(feature = "v2")] +pub fn payment_method_session_confirm() {} diff --git a/crates/openapi/src/routes/payments.rs b/crates/openapi/src/routes/payments.rs index 633e0685e6..aa6d666fec 100644 --- a/crates/openapi/src/routes/payments.rs +++ b/crates/openapi/src/routes/payments.rs @@ -727,7 +727,7 @@ pub fn payments_update_intent() {} ), ), responses( - (status = 200, description = "Payment created", body = PaymentsConfirmIntentResponse), + (status = 200, description = "Payment created", body = PaymentsResponse), (status = 400, description = "Missing Mandatory fields") ), tag = "Payments", @@ -748,7 +748,7 @@ pub fn payments_confirm_intent() {} ("force_sync" = ForceSync, Query, description = "A boolean to indicate whether to force sync the payment status. Value can be true or false") ), responses( - (status = 200, description = "Gets the payment with final status", body = PaymentsRetrieveResponse), + (status = 200, description = "Gets the payment with final status", body = PaymentsResponse), (status = 404, description = "No payment found with the given id") ), tag = "Payments", diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 2ad07d0d22..5fb7115035 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1038,7 +1038,7 @@ impl MerchantAccountUpdateBridge for api::MerchantAccountUpdate { .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to encrypt merchant details")?, - metadata, + metadata: metadata.map(Box::new), publishable_key: None, }) } diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index ffb864ea07..9f0fd70522 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -28,12 +28,10 @@ use common_utils::ext_traits::Encode; use common_utils::{consts::DEFAULT_LOCALE, id_type}; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use common_utils::{ - crypto::{self, Encryptable}, - ext_traits::{AsyncExt, Encode, StringExt, ValueExt}, + crypto::Encryptable, + ext_traits::{AsyncExt, Encode, ValueExt}, fp_utils::when, - generate_id, - request::RequestContent, - types as util_types, + generate_id, types as util_types, }; use diesel_models::{ enums, GenericLinkNew, PaymentMethodCollectLink, PaymentMethodCollectLinkData, @@ -61,9 +59,9 @@ use super::{ #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use crate::{ configs::settings, - core::{payment_methods::transformers as pm_transforms, utils as core_utils}, + core::{payment_methods::transformers as pm_transforms, payments as payments_core}, headers, logger, - routes::payment_methods as pm_routes, + routes::{self, payment_methods as pm_routes}, services::encryption, types::{ api::{self, payment_methods::PaymentMethodCreateExt}, @@ -685,76 +683,58 @@ pub async fn retrieve_payment_method_with_token( #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all)] -pub(crate) async fn get_payment_method_create_request( - payment_method_data: Option<&domain::PaymentMethodData>, - payment_method_type: Option, - payment_method_subtype: Option, - customer_id: &Option, - billing_name: Option>, +pub(crate) fn get_payment_method_create_request( + payment_method_data: &api_models::payments::PaymentMethodData, + payment_method_type: storage_enums::PaymentMethod, + payment_method_subtype: storage_enums::PaymentMethodType, + customer_id: id_type::GlobalCustomerId, + billing_address: Option<&api_models::payments::Address>, + payment_method_session: Option<&domain::payment_methods::PaymentMethodSession>, ) -> RouterResult { match payment_method_data { - Some(pm_data) => match payment_method_type { - Some(payment_method_type) => match pm_data { - domain::PaymentMethodData::Card(card) => { - let card_detail = payment_methods::CardDetail { - card_number: card.card_number.clone(), - card_exp_month: card.card_exp_month.clone(), - card_exp_year: card.card_exp_year.clone(), - card_holder_name: billing_name, - nick_name: card.nick_name.clone(), - card_issuing_country: card - .card_issuing_country - .as_ref() - .map(|c| api_enums::CountryAlpha2::from_str(c)) - .transpose() - .ok() - .flatten(), - card_network: card.card_network.clone(), - card_issuer: card.card_issuer.clone(), - card_type: card - .card_type - .as_ref() - .map(|c| payment_methods::CardType::from_str(c)) - .transpose() - .ok() - .flatten(), - }; - let payment_method_request = payment_methods::PaymentMethodCreate { - payment_method_type, - payment_method_subtype: payment_method_subtype - .get_required_value("payment_method_subtype") - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "payment_method_data", - })?, - metadata: None, - customer_id: customer_id - .clone() - .get_required_value("customer_id") - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "customer_id", - })?, - payment_method_data: payment_methods::PaymentMethodCreateData::Card( - card_detail, - ), - billing: None, - network_tokenization: None, - }; - Ok(payment_method_request) - } - _ => Err(report!(errors::ApiErrorResponse::MissingRequiredField { - field_name: "payment_method_data" - }) - .attach_printable("Payment method data is incorrect")), - }, - None => Err(report!(errors::ApiErrorResponse::MissingRequiredField { - field_name: "payment_method_type" - }) - .attach_printable("PaymentMethodType Required")), - }, - None => Err(report!(errors::ApiErrorResponse::MissingRequiredField { - field_name: "payment_method_data" + api_models::payments::PaymentMethodData::Card(card) => { + let card_detail = payment_methods::CardDetail { + card_number: card.card_number.clone(), + card_exp_month: card.card_exp_month.clone(), + card_exp_year: card.card_exp_year.clone(), + card_holder_name: card.card_holder_name.clone(), + nick_name: card.nick_name.clone(), + card_issuing_country: card + .card_issuing_country + .as_ref() + .map(|c| api_enums::CountryAlpha2::from_str(c)) + .transpose() + .ok() + .flatten(), + card_network: card.card_network.clone(), + card_issuer: card.card_issuer.clone(), + card_type: card + .card_type + .as_ref() + .map(|c| payment_methods::CardType::from_str(c)) + .transpose() + .ok() + .flatten(), + card_cvc: Some(card.card_cvc.clone()), + }; + let payment_method_request = payment_methods::PaymentMethodCreate { + payment_method_type, + payment_method_subtype, + metadata: None, + customer_id: customer_id.clone(), + payment_method_data: payment_methods::PaymentMethodCreateData::Card(card_detail), + billing: billing_address.map(ToOwned::to_owned), + psp_tokenization: payment_method_session + .and_then(|pm_session| pm_session.psp_tokenization.clone()), + network_tokenization: payment_method_session + .and_then(|pm_session| pm_session.network_tokenization.clone()), + }; + Ok(payment_method_request) + } + _ => Err(report!(errors::ApiErrorResponse::UnprocessableEntity { + message: "only card payment methods are supported for tokenization".to_string() }) - .attach_printable("PaymentMethodData required Or Card is already saved")), + .attach_printable("Payment method data is incorrect")), } } @@ -851,11 +831,35 @@ pub(crate) async fn get_payment_method_create_request( #[instrument(skip_all)] pub async fn create_payment_method( state: &SessionState, + request_state: &routes::app::ReqState, req: api::PaymentMethodCreate, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, profile: &domain::Profile, ) -> RouterResponse { + let response = create_payment_method_core( + state, + request_state, + req, + merchant_account, + key_store, + profile, + ) + .await?; + + Ok(services::ApplicationResponse::Json(response)) +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all)] +pub async fn create_payment_method_core( + state: &SessionState, + _request_state: &routes::app::ReqState, + req: api::PaymentMethodCreate, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + profile: &domain::Profile, +) -> RouterResult { use common_utils::ext_traits::ValueExt; req.validate()?; @@ -891,13 +895,12 @@ pub async fn create_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to parse Payment method billing address")?; - // create pm let payment_method_id = id_type::GlobalPaymentMethodId::generate(&state.conf.cell_information.id) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; - let payment_method = create_payment_method_for_intent( + let mut payment_method = create_payment_method_for_intent( state, req.metadata.clone(), &customer_id, @@ -910,7 +913,8 @@ pub async fn create_payment_method( .await .attach_printable("Failed to add Payment method to DB")?; - let payment_method_data = pm_types::PaymentMethodVaultingData::from(req.payment_method_data); + let payment_method_data = + pm_types::PaymentMethodVaultingData::from(req.payment_method_data.clone()); let payment_method_data = populate_bin_details_for_payment_method(state, &payment_method_data).await; @@ -930,7 +934,7 @@ pub async fn create_payment_method( &payment_method_data, merchant_account, key_store, - req.network_tokenization, + req.network_tokenization.clone(), profile.is_network_tokenization_enabled, &customer_id, ) @@ -944,16 +948,19 @@ pub async fn create_payment_method( }) .ok(); - let response = match vaulting_result { + let (response, payment_method) = match vaulting_result { Ok((vaulting_resp, fingerprint_id)) => { + payment_method.set_payment_method_type(req.payment_method_type); + payment_method.set_payment_method_subtype(req.payment_method_subtype); + let pm_update = create_pm_additional_data_update( - &payment_method_data, + Some(&payment_method_data), state, key_store, Some(vaulting_resp.vault_id.get_string_repr().clone()), - Some(req.payment_method_type), - Some(req.payment_method_subtype), Some(fingerprint_id), + &payment_method, + None, network_tokenization_resp, ) .await @@ -973,7 +980,7 @@ pub async fn create_payment_method( let resp = pm_transforms::generate_payment_method_response(&payment_method)?; - Ok(resp) + Ok((resp, payment_method)) } Err(e) => { let pm_update = storage::PaymentMethodUpdate::StatusUpdate { @@ -995,7 +1002,7 @@ pub async fn create_payment_method( } }?; - Ok(services::ApplicationResponse::Json(response)) + Ok(response) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -1130,6 +1137,7 @@ pub async fn populate_bin_details_for_payment_method( .ok() .flatten() }), + card_cvc: card.card_cvc.clone(), }) } } @@ -1475,48 +1483,104 @@ pub async fn create_payment_method_for_intent( Ok(response) } -#[allow(clippy::too_many_arguments)] +#[cfg(feature = "v2")] +/// Update the connector_mandate_details of the payment method with +/// new token details for the payment +fn create_connector_token_details_update( + token_details: payment_methods::ConnectorTokenDetails, + payment_method: &domain::PaymentMethod, +) -> hyperswitch_domain_models::mandates::CommonMandateReference { + let connector_id = token_details.connector_id.clone(); + + let reference_record = + hyperswitch_domain_models::mandates::ConnectorTokenReferenceRecord::foreign_from( + token_details, + ); + + let connector_token_details = payment_method.connector_mandate_details.clone(); + + match connector_token_details { + Some(mut connector_mandate_reference) => { + connector_mandate_reference + .insert_payment_token_reference_record(&connector_id, reference_record); + + connector_mandate_reference + } + None => { + let reference_record_hash_map = + std::collections::HashMap::from([(connector_id, reference_record)]); + let payments_mandate_reference = + hyperswitch_domain_models::mandates::PaymentsTokenReference( + reference_record_hash_map, + ); + hyperswitch_domain_models::mandates::CommonMandateReference { + payments: Some(payments_mandate_reference), + payouts: None, + } + } + } +} + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[allow(clippy::too_many_arguments)] pub async fn create_pm_additional_data_update( - pmd: &pm_types::PaymentMethodVaultingData, + payment_method_vaulting_data: Option<&pm_types::PaymentMethodVaultingData>, state: &SessionState, key_store: &domain::MerchantKeyStore, vault_id: Option, - payment_method_type: Option, - payment_method_subtype: Option, vault_fingerprint_id: Option, + payment_method: &domain::PaymentMethod, + connector_token_details: Option, nt_data: Option, ) -> RouterResult { - let card = match pmd { - pm_types::PaymentMethodVaultingData::Card(card) => { - payment_method_data::PaymentMethodsData::Card( - payment_method_data::CardDetailsPaymentMethod::from(card.clone()), - ) - } - pm_types::PaymentMethodVaultingData::NetworkToken(network_token) => { - payment_method_data::PaymentMethodsData::NetworkToken( - payment_method_data::NetworkTokenDetailsPaymentMethod::from(network_token.clone()), - ) - } - }; - let key_manager_state = &(state).into(); - let pmd: Encryptable> = - cards::create_encrypted_data(key_manager_state, key_store, card) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to encrypt Payment method data")?; + let encrypted_payment_method_data = payment_method_vaulting_data + .map( + |payment_method_vaulting_data| match payment_method_vaulting_data { + pm_types::PaymentMethodVaultingData::Card(card) => { + payment_method_data::PaymentMethodsData::Card( + payment_method_data::CardDetailsPaymentMethod::from(card.clone()), + ) + } + pm_types::PaymentMethodVaultingData::NetworkToken(network_token) => { + payment_method_data::PaymentMethodsData::NetworkToken( + payment_method_data::NetworkTokenDetailsPaymentMethod::from( + network_token.clone(), + ), + ) + } + }, + ) + .async_map(|payment_method_details| async { + let key_manager_state = &(state).into(); - let pm_update = storage::PaymentMethodUpdate::AdditionalDataUpdate { + cards::create_encrypted_data(key_manager_state, key_store, payment_method_details) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to encrypt Payment method data") + }) + .await + .transpose()? + .map(From::from); + + let connector_mandate_details_update = connector_token_details + .map(|connector_token| { + create_connector_token_details_update(connector_token, payment_method) + }) + .map(From::from); + + let pm_update = storage::PaymentMethodUpdate::GenericUpdate { status: Some(enums::PaymentMethodStatus::Active), locker_id: vault_id, - payment_method_type_v2: payment_method_type, - payment_method_subtype, - payment_method_data: Some(pmd.into()), + // Payment method type remains the same, only card details are updated + payment_method_type_v2: None, + payment_method_subtype: None, + payment_method_data: encrypted_payment_method_data, network_token_requestor_reference_id: nt_data .clone() .map(|data| data.network_token_requestor_reference_id), network_token_locker_id: nt_data.clone().map(|data| data.network_token_locker_id), network_token_payment_method_data: nt_data.map(|data| data.network_token_pmd.into()), + connector_mandate_details: connector_mandate_details_update, locker_fingerprint_id: vault_fingerprint_id, }; @@ -1717,35 +1781,8 @@ 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() - .map(|x| x.into_inner()) - .and_then(|pmd| match pmd { - api::PaymentMethodsData::Card(card) => { - Some(api::PaymentMethodResponseData::Card(card.into())) - } - _ => None, - }); - - let resp = api::PaymentMethodResponse { - merchant_id: payment_method.merchant_id.to_owned(), - customer_id: payment_method.customer_id.to_owned(), - id: payment_method.id.to_owned(), - payment_method_type: payment_method.get_payment_method_type(), - payment_method_subtype: payment_method.get_payment_method_subtype(), - created: Some(payment_method.created_at), - recurring_enabled: false, - last_used_at: Some(payment_method.last_used_at), - payment_method_data: pmd, - }; - - Ok(services::ApplicationResponse::Json(resp)) + transformers::generate_payment_method_response(&payment_method) + .map(services::ApplicationResponse::Json) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -1757,9 +1794,14 @@ pub async fn update_payment_method( req: api::PaymentMethodUpdate, payment_method_id: &id_type::GlobalPaymentMethodId, ) -> RouterResponse { - let response = - update_payment_method_core(state, merchant_account, key_store, req, payment_method_id) - .await?; + let response = update_payment_method_core( + &state, + &merchant_account, + &key_store, + req, + payment_method_id, + ) + .await?; Ok(services::ApplicationResponse::Json(response)) } @@ -1767,18 +1809,18 @@ pub async fn update_payment_method( #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all)] pub async fn update_payment_method_core( - state: SessionState, - merchant_account: domain::MerchantAccount, - key_store: domain::MerchantKeyStore, - req: api::PaymentMethodUpdate, + state: &SessionState, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + request: api::PaymentMethodUpdate, payment_method_id: &id_type::GlobalPaymentMethodId, ) -> RouterResult { let db = state.store.as_ref(); let payment_method = db .find_payment_method( - &((&state).into()), - &key_store, + &((state).into()), + key_store, payment_method_id, merchant_account.storage_scheme, ) @@ -1796,35 +1838,50 @@ pub async fn update_payment_method_core( }, )?; - let pmd: pm_types::PaymentMethodVaultingData = - vault::retrieve_payment_method_from_vault(&state, &merchant_account, &payment_method) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to retrieve payment method from vault")? - .data; + let pmd = vault::retrieve_payment_method_from_vault(state, merchant_account, &payment_method) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to retrieve payment method from vault")? + .data; - let vault_request_data = - pm_transforms::generate_pm_vaulting_req_from_update_request(pmd, req.payment_method_data); + let vault_request_data = request.payment_method_data.map(|payment_method_data| { + pm_transforms::generate_pm_vaulting_req_from_update_request(pmd, payment_method_data) + }); - let (vaulting_response, fingerprint_id) = vault_payment_method( - &state, - &vault_request_data, - &merchant_account, - &key_store, - current_vault_id, // using current vault_id for now, will have to refactor this - &payment_method.customer_id, - ) // to generate new one on each vaulting later on - .await - .attach_printable("Failed to add payment method in vault")?; + let (vaulting_response, fingerprint_id) = match vault_request_data { + // cannot use async map because of problems related to lifetimes + // to overcome this, we will have to use a move closure and add some clones + Some(ref vault_request_data) => { + Some( + vault_payment_method( + state, + vault_request_data, + merchant_account, + key_store, + // using current vault_id for now, + // will have to refactor this to generate new one on each vaulting later on + current_vault_id, + &payment_method.customer_id, + ) + .await + .attach_printable("Failed to add payment method in vault")?, + ) + } + None => None, + } + .unzip(); + + let vault_id = vaulting_response + .map(|vaulting_response| vaulting_response.vault_id.get_string_repr().clone()); let pm_update = create_pm_additional_data_update( - &vault_request_data, - &state, - &key_store, - Some(vaulting_response.vault_id.get_string_repr().clone()), - payment_method.get_payment_method_type(), - payment_method.get_payment_method_subtype(), - Some(fingerprint_id), + vault_request_data.as_ref(), + state, + key_store, + vault_id, + fingerprint_id, + &payment_method, + request.connector_token_details, None, ) .await @@ -1832,8 +1889,8 @@ pub async fn update_payment_method_core( let payment_method = db .update_payment_method( - &((&state).into()), - &key_store, + &((state).into()), + key_store, payment_method, pm_update, merchant_account.storage_scheme, @@ -1937,7 +1994,7 @@ trait EncryptableData { #[cfg(feature = "v2")] #[async_trait::async_trait] impl EncryptableData for payment_methods::PaymentMethodSessionRequest { - type Output = hyperswitch_domain_models::payment_methods::DecryptedPaymentMethodsSession; + type Output = hyperswitch_domain_models::payment_methods::DecryptedPaymentMethodSession; async fn encrypt_data( &self, @@ -1957,10 +2014,10 @@ impl EncryptableData for payment_methods::PaymentMethodSessionRequest { let batch_encrypted_data = domain_types::crypto_operation( key_manager_state, - common_utils::type_name!(hyperswitch_domain_models::payment_methods::PaymentMethodsSession), + common_utils::type_name!(hyperswitch_domain_models::payment_methods::PaymentMethodSession), domain_types::CryptoOperation::BatchEncrypt( - hyperswitch_domain_models::payment_methods::FromRequestEncryptablePaymentMethodsSession::to_encryptable( - hyperswitch_domain_models::payment_methods::FromRequestEncryptablePaymentMethodsSession { + hyperswitch_domain_models::payment_methods::FromRequestEncryptablePaymentMethodSession::to_encryptable( + hyperswitch_domain_models::payment_methods::FromRequestEncryptablePaymentMethodSession { billing: encrypted_billing_address, }, ), @@ -1974,7 +2031,57 @@ impl EncryptableData for payment_methods::PaymentMethodSessionRequest { .attach_printable("Failed while encrypting payment methods session details".to_string())?; let encrypted_data = - hyperswitch_domain_models::payment_methods::FromRequestEncryptablePaymentMethodsSession::from_encryptable( + hyperswitch_domain_models::payment_methods::FromRequestEncryptablePaymentMethodSession::from_encryptable( + batch_encrypted_data, + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encrypting payment methods session detailss")?; + + Ok(encrypted_data) + } +} + +#[cfg(feature = "v2")] +#[async_trait::async_trait] +impl EncryptableData for payment_methods::PaymentMethodsSessionUpdateRequest { + type Output = hyperswitch_domain_models::payment_methods::DecryptedPaymentMethodSession; + + async fn encrypt_data( + &self, + key_manager_state: &common_utils::types::keymanager::KeyManagerState, + key_store: &domain::MerchantKeyStore, + ) -> RouterResult { + use common_utils::types::keymanager::ToEncryptable; + + let encrypted_billing_address = self + .billing + .clone() + .map(|address| address.encode_to_value()) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode billing address")? + .map(Secret::new); + + let batch_encrypted_data = domain_types::crypto_operation( + key_manager_state, + common_utils::type_name!(hyperswitch_domain_models::payment_methods::PaymentMethodSession), + domain_types::CryptoOperation::BatchEncrypt( + hyperswitch_domain_models::payment_methods::FromRequestEncryptablePaymentMethodSession::to_encryptable( + hyperswitch_domain_models::payment_methods::FromRequestEncryptablePaymentMethodSession { + billing: encrypted_billing_address, + }, + ), + ), + common_utils::types::keymanager::Identifier::Merchant(key_store.merchant_id.clone()), + key_store.key.peek(), + ) + .await + .and_then(|val| val.try_into_batchoperation()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while encrypting payment methods session details".to_string())?; + + let encrypted_data = + hyperswitch_domain_models::payment_methods::FromRequestEncryptablePaymentMethodSession::from_encryptable( batch_encrypted_data, ) .change_context(errors::ApiErrorResponse::InternalServerError) @@ -1990,7 +2097,7 @@ pub async fn payment_methods_session_create( merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, request: payment_methods::PaymentMethodSessionRequest, -) -> RouterResponse { +) -> RouterResponse { let db = state.store.as_ref(); let key_manager_state = &(&state).into(); @@ -2046,13 +2153,16 @@ pub async fn payment_methods_session_create( .attach_printable("Unable to create client secret")?; let payment_method_session_domain_model = - hyperswitch_domain_models::payment_methods::PaymentMethodsSession { + hyperswitch_domain_models::payment_methods::PaymentMethodSession { id: payment_methods_session_id, customer_id: request.customer_id, billing, psp_tokenization: request.psp_tokenization, network_tokenization: request.network_tokenization, expires_at, + return_url: request.return_url, + associated_payment_methods: None, + associated_payment: None, }; db.insert_payment_methods_session( @@ -2065,21 +2175,85 @@ pub async fn payment_methods_session_create( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to insert payment methods session in db")?; - let response = payment_methods::PaymentMethodsSessionResponse::foreign_from(( + let response = transformers::generate_payment_method_session_response( payment_method_session_domain_model, client_secret.secret, - )); + None, + ); Ok(services::ApplicationResponse::Json(response)) } +#[cfg(feature = "v2")] +pub async fn payment_methods_session_update( + state: SessionState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + payment_method_session_id: id_type::GlobalPaymentMethodSessionId, + request: payment_methods::PaymentMethodsSessionUpdateRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + let key_manager_state = &(&state).into(); + + let existing_payment_method_session_state = db + .get_payment_methods_session(key_manager_state, &key_store, &payment_method_session_id) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "payment methods session does not exist or has expired".to_string(), + }) + .attach_printable("Failed to retrieve payment methods session from db")?; + + let encrypted_data = request + .encrypt_data(key_manager_state, &key_store) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encrypt payment methods session data")?; + + let billing = encrypted_data + .billing + .as_ref() + .map(|data| { + data.clone() + .deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to decode billing address")?; + + let payment_method_session_domain_model = + hyperswitch_domain_models::payment_methods::PaymentMethodsSessionUpdateEnum::GeneralUpdate{ + billing, + psp_tokenization: request.psp_tokenization, + network_tokenization: request.network_tokenization, + }; + + let update_state_change = db + .update_payment_method_session( + key_manager_state, + &key_store, + &payment_method_session_id, + payment_method_session_domain_model, + existing_payment_method_session_state.clone(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update payment methods session in db")?; + + let response = transformers::generate_payment_method_session_response( + update_state_change, + Secret::new("CLIENT_SECRET_REDACTED".to_string()), + None, // TODO: send associated payments response based on the expandable param + ); + + Ok(services::ApplicationResponse::Json(response)) +} #[cfg(feature = "v2")] pub async fn payment_methods_session_retrieve( state: SessionState, _merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, payment_method_session_id: id_type::GlobalPaymentMethodSessionId, -) -> RouterResponse { +) -> RouterResponse { let db = state.store.as_ref(); let key_manager_state = &(&state).into(); @@ -2091,10 +2265,11 @@ pub async fn payment_methods_session_retrieve( }) .attach_printable("Failed to retrieve payment methods session from db")?; - let response = payment_methods::PaymentMethodsSessionResponse::foreign_from(( + let response = transformers::generate_payment_method_session_response( payment_method_session_domain_model, Secret::new("CLIENT_SECRET_REDACTED".to_string()), - )); + None, // TODO: send associated payments response based on the expandable param + ); Ok(services::ApplicationResponse::Json(response)) } @@ -2121,9 +2296,9 @@ pub async fn payment_methods_session_update_payment_method( let payment_method_update_request = request.payment_method_update_request; let updated_payment_method = update_payment_method_core( - state, - merchant_account, - key_store, + &state, + &merchant_account, + &key_store, payment_method_update_request, &request.payment_method_id, ) @@ -2133,6 +2308,186 @@ pub async fn payment_methods_session_update_payment_method( Ok(services::ApplicationResponse::Json(updated_payment_method)) } +#[cfg(feature = "v2")] +fn construct_zero_auth_payments_request( + confirm_request: &payment_methods::PaymentMethodSessionConfirmRequest, + payment_method_session: &hyperswitch_domain_models::payment_methods::PaymentMethodSession, + payment_method: &payment_methods::PaymentMethodResponse, +) -> RouterResult { + use api_models::payments; + + Ok(payments::PaymentsRequest { + amount_details: payments::AmountDetails::new_for_zero_auth_payment( + common_enums::Currency::USD, + ), + payment_method_data: confirm_request.payment_method_data.clone(), + payment_method_type: confirm_request.payment_method_type, + payment_method_subtype: confirm_request.payment_method_subtype, + customer_id: Some(payment_method_session.customer_id.clone()), + customer_present: Some(enums::PresenceOfCustomerDuringPayment::Present), + setup_future_usage: Some(common_enums::FutureUsage::OffSession), + payment_method_id: Some(payment_method.id.clone()), + merchant_reference_id: None, + routing_algorithm_id: None, + capture_method: None, + authentication_type: None, + // We have already passed payment method billing address + billing: None, + shipping: None, + description: None, + return_url: payment_method_session.return_url.clone(), + 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_enabled: None, + payment_link_config: None, + request_incremental_authorization: None, + session_expiry: None, + frm_metadata: None, + request_external_three_ds_authentication: None, + customer_acceptance: None, + browser_info: None, + }) +} + +#[cfg(feature = "v2")] +async fn create_zero_auth_payment( + state: SessionState, + req_state: routes::app::ReqState, + merchant_account: domain::MerchantAccount, + profile: domain::Profile, + key_store: domain::MerchantKeyStore, + request: api_models::payments::PaymentsRequest, +) -> RouterResult { + let response = Box::pin(payments_core::payments_create_and_confirm_intent( + state, + req_state, + merchant_account, + profile, + key_store, + request, + hyperswitch_domain_models::payments::HeaderPayload::default(), + None, + )) + .await?; + + logger::info!(associated_payments_response=?response); + + response + .get_json_body() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unexpected response from payments core") +} + +#[cfg(feature = "v2")] +pub async fn payment_methods_session_confirm( + state: SessionState, + req_state: routes::app::ReqState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + profile: domain::Profile, + payment_method_session_id: id_type::GlobalPaymentMethodSessionId, + request: payment_methods::PaymentMethodSessionConfirmRequest, +) -> RouterResponse { + let db: &dyn StorageInterface = state.store.as_ref(); + let key_manager_state = &(&state).into(); + + // Validate if the session still exists + let payment_method_session = db + .get_payment_methods_session(key_manager_state, &key_store, &payment_method_session_id) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "payment methods session does not exist or has expired".to_string(), + }) + .attach_printable("Failed to retrieve payment methods session from db")?; + + let payment_method_session_billing = payment_method_session + .billing + .clone() + .map(|billing| billing.into_inner()) + .map(From::from); + + // Unify the billing address that we receive from the session and from the confirm request + let unified_billing_address = request + .payment_method_data + .billing + .clone() + .map(|payment_method_billing| { + payment_method_billing.unify_address(payment_method_session_billing.as_ref()) + }) + .or_else(|| payment_method_session_billing.clone()); + + let create_payment_method_request = get_payment_method_create_request( + request + .payment_method_data + .payment_method_data + .as_ref() + .get_required_value("payment_method_data")?, + request.payment_method_type, + request.payment_method_subtype, + payment_method_session.customer_id.clone(), + unified_billing_address.as_ref(), + Some(&payment_method_session), + ) + .attach_printable("Failed to create payment method request")?; + + let payment_method = create_payment_method_core( + &state, + &req_state, + create_payment_method_request, + &merchant_account, + &key_store, + &profile, + ) + .await?; + + let payments_response = match &payment_method_session.psp_tokenization { + Some(common_types::payment_methods::PspTokenization { + tokenization_type: common_enums::TokenizationType::MultiUse, + .. + }) => { + let zero_auth_request = construct_zero_auth_payments_request( + &request, + &payment_method_session, + &payment_method, + )?; + let payments_response = Box::pin(create_zero_auth_payment( + state.clone(), + req_state, + merchant_account.clone(), + profile.clone(), + key_store.clone(), + zero_auth_request, + )) + .await?; + + Some(payments_response) + } + Some(common_types::payment_methods::PspTokenization { + tokenization_type: common_enums::TokenizationType::SingleUse, + .. + }) => { + todo!("single use tokenization are not implemented") + } + None => None, + }; + + //TODO: update the payment method session with the payment id and payment method id + let payment_method_session_response = transformers::generate_payment_method_session_response( + payment_method_session, + Secret::new("CLIENT_SECRET_REDACTED".to_string()), + payments_response, + ); + + Ok(services::ApplicationResponse::Json( + payment_method_session_response, + )) +} + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] impl pm_types::SavedPMLPaymentsInfo { pub async fn form_payments_info( diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 947ae97b11..233dade651 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -1,9 +1,4 @@ -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -use std::str::FromStr; - use api_models::{enums as api_enums, payment_methods::Card}; -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -use common_utils::ext_traits::ValueExt; use common_utils::{ ext_traits::{Encode, StringExt}, id_type, @@ -544,6 +539,7 @@ pub fn generate_pm_vaulting_req_from_update_request( card_type: card_create.card_type, card_holder_name: update_card.card_holder_name, nick_name: update_card.nick_name, + card_cvc: None, }), _ => todo!(), //todo! - since support for network tokenization is not added PaymentMethodUpdateData. should be handled later. } @@ -551,9 +547,9 @@ 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, + payment_method: &domain::PaymentMethod, ) -> errors::RouterResult { - let pmd = pm + let pmd = payment_method .payment_method_data .clone() .map(|data| data.into_inner()) @@ -564,16 +560,29 @@ pub fn generate_payment_method_response( _ => None, }); + let connector_tokens = payment_method + .connector_mandate_details + .as_ref() + .and_then(|connector_token_details| connector_token_details.payments.clone()) + .map(|payment_token_details| payment_token_details.0) + .map(|payment_token_details| { + payment_token_details + .into_iter() + .map(transformers::ForeignFrom::foreign_from) + .collect::>() + }); + let resp = api::PaymentMethodResponse { - merchant_id: pm.merchant_id.to_owned(), - customer_id: pm.customer_id.to_owned(), - id: pm.id.to_owned(), - payment_method_type: pm.get_payment_method_type(), - payment_method_subtype: pm.get_payment_method_subtype(), - created: Some(pm.created_at), + merchant_id: payment_method.merchant_id.to_owned(), + customer_id: payment_method.customer_id.to_owned(), + id: payment_method.id.to_owned(), + payment_method_type: payment_method.get_payment_method_type(), + payment_method_subtype: payment_method.get_payment_method_subtype(), + created: Some(payment_method.created_at), recurring_enabled: false, - last_used_at: Some(pm.last_used_at), + last_used_at: Some(payment_method.last_used_at), payment_method_data: pmd, + connector_tokens, }; Ok(resp) @@ -973,30 +982,102 @@ impl transformers::ForeignTryFrom for api::CustomerPaymen } #[cfg(feature = "v2")] -impl - transformers::ForeignFrom<( - hyperswitch_domain_models::payment_methods::PaymentMethodsSession, - Secret, - )> for api_models::payment_methods::PaymentMethodsSessionResponse +pub fn generate_payment_method_session_response( + payment_method_session: hyperswitch_domain_models::payment_methods::PaymentMethodSession, + client_secret: Secret, + associated_payment: Option, +) -> api_models::payment_methods::PaymentMethodSessionResponse { + let next_action = associated_payment + .as_ref() + .and_then(|payment| payment.next_action.clone()); + + let authentication_details = + associated_payment.map( + |payment| api_models::payment_methods::AuthenticationDetails { + status: payment.status, + error: payment.error, + }, + ); + + api_models::payment_methods::PaymentMethodSessionResponse { + id: payment_method_session.id, + customer_id: payment_method_session.customer_id, + billing: payment_method_session + .billing + .map(|address| address.into_inner()) + .map(From::from), + psp_tokenization: payment_method_session.psp_tokenization, + network_tokenization: payment_method_session.network_tokenization, + expires_at: payment_method_session.expires_at, + client_secret, + next_action, + return_url: payment_method_session.return_url, + associated_payment_methods: payment_method_session.associated_payment_methods, + authentication_details, + } +} + +#[cfg(feature = "v2")] +impl transformers::ForeignFrom + for hyperswitch_domain_models::mandates::ConnectorTokenReferenceRecord { - fn foreign_from( - item: ( - hyperswitch_domain_models::payment_methods::PaymentMethodsSession, - Secret, - ), - ) -> Self { - let (session, client_secret) = item; + fn foreign_from(item: api_models::payment_methods::ConnectorTokenDetails) -> Self { + let api_models::payment_methods::ConnectorTokenDetails { + status, + connector_token_request_reference_id, + original_payment_authorized_amount, + original_payment_authorized_currency, + metadata, + token, + .. + } = item; + Self { - id: session.id, - customer_id: session.customer_id, - billing: session - .billing - .map(|address| address.into_inner()) - .map(From::from), - psp_tokenization: session.psp_tokenization, - network_tokenization: session.network_tokenization, - expires_at: session.expires_at, - client_secret, + connector_token: token, + // TODO: check why do we need this field + payment_method_subtype: None, + original_payment_authorized_amount, + original_payment_authorized_currency, + metadata, + connector_token_status: status, + connector_token_request_reference_id, + } + } +} + +#[cfg(feature = "v2")] +impl + transformers::ForeignFrom<( + id_type::MerchantConnectorAccountId, + hyperswitch_domain_models::mandates::ConnectorTokenReferenceRecord, + )> for api_models::payment_methods::ConnectorTokenDetails +{ + fn foreign_from( + (connector_id, mandate_reference_record): ( + id_type::MerchantConnectorAccountId, + hyperswitch_domain_models::mandates::ConnectorTokenReferenceRecord, + ), + ) -> Self { + let hyperswitch_domain_models::mandates::ConnectorTokenReferenceRecord { + connector_token_request_reference_id, + original_payment_authorized_amount, + original_payment_authorized_currency, + metadata, + connector_token, + connector_token_status, + .. + } = mandate_reference_record; + + Self { + connector_id, + status: connector_token_status, + connector_token_request_reference_id, + original_payment_authorized_amount, + original_payment_authorized_currency, + metadata, + token: connector_token, + // Token that is derived from payments mandate reference will always be multi use token + token_type: common_enums::TokenizationType::MultiUse, } } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index da8096ce2b..7ea847ce65 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -131,7 +131,7 @@ pub async fn payments_operation_core( req_state: ReqState, merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, - profile: domain::Profile, + profile: &domain::Profile, operation: Op, req: Req, get_tracker_response: operations::GetTrackerResponse, @@ -178,7 +178,7 @@ where operation .to_domain()? - .run_decision_manager(state, &mut payment_data, &profile) + .run_decision_manager(state, &mut payment_data, profile) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to run decision manager")?; @@ -187,7 +187,7 @@ where .to_domain()? .perform_routing( &merchant_account, - &profile, + profile, state, &mut payment_data, &key_store, @@ -212,13 +212,25 @@ where None, #[cfg(not(feature = "frm"))] None, - &profile, + profile, false, ) .await?; let payments_response_operation = Box::new(PaymentResponse); + payments_response_operation + .to_post_update_tracker()? + .save_pm_and_mandate( + state, + &router_data, + &merchant_account, + &key_store, + &mut payment_data, + profile, + ) + .await?; + payments_response_operation .to_post_update_tracker()? .update_tracker( @@ -1720,7 +1732,7 @@ where req_state, merchant_account.clone(), key_store, - profile, + &profile, operation.clone(), req, get_tracker_response, @@ -1735,6 +1747,7 @@ where external_latency, header_payload.x_hs_latency, &merchant_account, + &profile, ) } @@ -1747,17 +1760,15 @@ pub(crate) async fn payments_create_and_confirm_intent( profile: domain::Profile, key_store: domain::MerchantKeyStore, request: payments_api::PaymentsRequest, - payment_id: id_type::GlobalPaymentId, mut header_payload: HeaderPayload, platform_merchant_account: Option, ) -> RouterResponse { - use actix_http::body::MessageBody; - use common_utils::ext_traits::BytesExt; use hyperswitch_domain_models::{ - payments::{PaymentConfirmData, PaymentIntentData}, - router_flow_types::{Authorize, PaymentCreateIntent, SetupMandate}, + payments::PaymentIntentData, router_flow_types::PaymentCreateIntent, }; + let payment_id = id_type::GlobalPaymentId::generate(&state.conf.cell_information.id); + let payload = payments_api::PaymentsCreateIntentRequest::from(&request); let create_intent_response = Box::pin(payments_intent_core::< @@ -1781,7 +1792,11 @@ pub(crate) async fn payments_create_and_confirm_intent( .await?; logger::info!(?create_intent_response); - let create_intent_response = handle_payments_intent_response(create_intent_response)?; + + let create_intent_response = create_intent_response + .get_json_body() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unexpected response from payments core")?; // Adding client secret to ensure client secret validation passes during confirm intent step header_payload.client_secret = Some(create_intent_response.client_secret.clone()); @@ -1802,63 +1817,8 @@ pub(crate) async fn payments_create_and_confirm_intent( .await?; logger::info!(?confirm_intent_response); - let confirm_intent_response = handle_payments_intent_response(confirm_intent_response)?; - construct_payments_response(create_intent_response, confirm_intent_response) -} - -#[cfg(feature = "v2")] -#[inline] -pub fn handle_payments_intent_response( - response: hyperswitch_domain_models::api::ApplicationResponse, -) -> CustomResult { - match response { - hyperswitch_domain_models::api::ApplicationResponse::Json(body) - | hyperswitch_domain_models::api::ApplicationResponse::JsonWithHeaders((body, _)) => { - Ok(body) - } - hyperswitch_domain_models::api::ApplicationResponse::StatusOk - | hyperswitch_domain_models::api::ApplicationResponse::TextPlain(_) - | hyperswitch_domain_models::api::ApplicationResponse::JsonForRedirection(_) - | hyperswitch_domain_models::api::ApplicationResponse::Form(_) - | hyperswitch_domain_models::api::ApplicationResponse::PaymentLinkForm(_) - | hyperswitch_domain_models::api::ApplicationResponse::FileData(_) - | hyperswitch_domain_models::api::ApplicationResponse::GenericLinkForm(_) => { - Err(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unexpected response from payment intent core") - } - } -} - -#[cfg(feature = "v2")] -#[inline] -fn construct_payments_response( - create_intent_response: payments_api::PaymentsIntentResponse, - confirm_intent_response: payments_api::PaymentsConfirmIntentResponse, -) -> RouterResponse { - let response = payments_api::PaymentsResponse { - id: confirm_intent_response.id, - status: confirm_intent_response.status, - amount: confirm_intent_response.amount, - customer_id: confirm_intent_response.customer_id, - connector: confirm_intent_response.connector, - client_secret: confirm_intent_response.client_secret, - created: confirm_intent_response.created, - payment_method_data: confirm_intent_response.payment_method_data, - payment_method_type: confirm_intent_response.payment_method_type, - payment_method_subtype: confirm_intent_response.payment_method_subtype, - next_action: confirm_intent_response.next_action, - connector_transaction_id: confirm_intent_response.connector_transaction_id, - connector_reference_id: confirm_intent_response.connector_reference_id, - connector_token_details: confirm_intent_response.connector_token_details, - merchant_connector_id: confirm_intent_response.merchant_connector_id, - browser_info: confirm_intent_response.browser_info, - error: confirm_intent_response.error, - }; - - Ok(hyperswitch_domain_models::api::ApplicationResponse::Json( - response, - )) + Ok(confirm_intent_response) } #[cfg(feature = "v2")] @@ -1873,7 +1833,7 @@ async fn decide_authorize_or_setup_intent_flow( confirm_intent_request: payments_api::PaymentsConfirmIntentRequest, payment_id: id_type::GlobalPaymentId, header_payload: HeaderPayload, -) -> RouterResponse { +) -> RouterResponse { use hyperswitch_domain_models::{ payments::PaymentConfirmData, router_flow_types::{Authorize, SetupMandate}, @@ -1882,7 +1842,7 @@ async fn decide_authorize_or_setup_intent_flow( if create_intent_response.amount_details.order_amount == MinorUnit::zero() { Box::pin(payments_core::< SetupMandate, - api_models::payments::PaymentsConfirmIntentResponse, + api_models::payments::PaymentsResponse, _, _, _, @@ -1903,7 +1863,7 @@ async fn decide_authorize_or_setup_intent_flow( } else { Box::pin(payments_core::< Authorize, - api_models::payments::PaymentsConfirmIntentResponse, + api_models::payments::PaymentsResponse, _, _, _, @@ -2458,7 +2418,7 @@ impl PaymentRedirectFlow for PaymentRedirectSync { req_state, merchant_account, merchant_key_store.clone(), - profile.clone(), + &profile, operation, payment_sync_request, get_tracker_response, diff --git a/crates/router/src/core/payments/operations/payment_confirm_intent.rs b/crates/router/src/core/payments/operations/payment_confirm_intent.rs index 79ebc2d8a3..0f42948e3f 100644 --- a/crates/router/src/core/payments/operations/payment_confirm_intent.rs +++ b/crates/router/src/core/payments/operations/payment_confirm_intent.rs @@ -1,15 +1,10 @@ -use api_models::{ - admin::ExtendedCardInfoConfig, - enums::FrmSuggestion, - payments::{ExtendedCardInfo, GetAddressFromPaymentMethodData, PaymentsConfirmIntentRequest}, -}; +use api_models::{enums::FrmSuggestion, payments::PaymentsConfirmIntentRequest}; use async_trait::async_trait; use common_utils::{ext_traits::Encode, types::keymanager::ToEncryptable}; use error_stack::ResultExt; use hyperswitch_domain_models::payments::PaymentConfirmData; use masking::PeekInterface; use router_env::{instrument, tracing}; -use tracing_futures::Instrument; use super::{Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; use crate::{ diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index f543c9aa1a..1c69852a10 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -26,6 +26,8 @@ use tracing_futures::Instrument; use super::{Operation, OperationSessionSetters, PostUpdateTracker}; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] use crate::core::routing::helpers as routing_helpers; +#[cfg(feature = "v2")] +use crate::utils::OptionExt; use crate::{ connector::utils::PaymentResponseRouterData, consts, @@ -2539,6 +2541,108 @@ impl PostUpdateTracker, types::SetupMandateRe Ok(payment_data) } + + async fn save_pm_and_mandate<'b>( + &self, + state: &SessionState, + router_data: &types::RouterData< + F, + types::SetupMandateRequestData, + types::PaymentsResponseData, + >, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + payment_data: &mut PaymentConfirmData, + _business_profile: &domain::Profile, + ) -> CustomResult<(), errors::ApiErrorResponse> + where + F: 'b + Clone + Send + Sync, + { + // If we received a payment_method_id from connector in the router data response + // Then we either update the payment method or create a new payment method + // The case for updating the payment method is when the payment is created from the payment method service + + let Ok(payments_response) = &router_data.response else { + // In case there was an error response from the connector + // We do not take any action related to the payment method + return Ok(()); + }; + + let connector_request_reference_id = payment_data + .payment_attempt + .connector_token_details + .as_ref() + .and_then(|token_details| token_details.get_connector_token_request_reference_id()); + + let connector_token = + payments_response.get_updated_connector_token_details(connector_request_reference_id); + + let payment_method_id = payment_data.payment_attempt.payment_method_id.clone(); + + // TODO: check what all conditions we will need to see if card need to be saved + match ( + connector_token + .as_ref() + .and_then(|connector_token| connector_token.connector_mandate_id.clone()), + payment_method_id, + ) { + (Some(token), Some(payment_method_id)) => { + if !matches!( + router_data.status, + enums::AttemptStatus::Charged | enums::AttemptStatus::Authorized + ) { + return Ok(()); + } + let connector_id = payment_data + .payment_attempt + .merchant_connector_id + .clone() + .get_required_value("merchant_connector_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("missing connector id")?; + + let net_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + let currency = payment_data.payment_intent.amount_details.currency; + + let connector_token_details_for_payment_method_update = + api_models::payment_methods::ConnectorTokenDetails { + connector_id, + status: common_enums::ConnectorTokenStatus::Active, + connector_token_request_reference_id: connector_token + .and_then(|details| details.connector_token_request_reference_id), + original_payment_authorized_amount: Some(net_amount), + original_payment_authorized_currency: Some(currency), + metadata: None, + token, + token_type: common_enums::TokenizationType::MultiUse, + }; + + let payment_method_update_request = + api_models::payment_methods::PaymentMethodUpdate { + payment_method_data: None, + connector_token_details: Some( + connector_token_details_for_payment_method_update, + ), + }; + + payment_methods::update_payment_method_core( + state, + merchant_account, + key_store, + payment_method_update_request, + &payment_method_id, + ) + .await + .attach_printable("Failed to update payment method")?; + } + (Some(_), None) => { + // TODO: create a new payment method + } + (None, Some(_)) | (None, None) => {} + } + + Ok(()) + } } #[cfg(feature = "v1")] diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index b4e4db3d16..3baf72901e 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -15,6 +15,7 @@ use common_utils::{ id_type, pii, }; use error_stack::{report, ResultExt}; +#[cfg(feature = "v1")] use hyperswitch_domain_models::mandates::{ CommonMandateReference, PaymentsMandateReference, PaymentsMandateReferenceRecord, }; diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 661bd73b50..ff4e6fe56a 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -313,7 +313,7 @@ pub async fn construct_payment_router_data_for_authorize<'a>( .payment_attempt .connector_token_details .as_ref() - .and_then(|detail| detail.get_connector_mandate_request_reference_id()); + .and_then(|detail| detail.get_connector_token_request_reference_id()); // TODO: evaluate the fields in router data, if they are required or not let router_data = types::RouterData { @@ -436,7 +436,7 @@ pub async fn construct_payment_router_data_for_capture<'a>( .payment_attempt .connector_token_details .as_ref() - .and_then(|detail| detail.get_connector_mandate_request_reference_id()); + .and_then(|detail| detail.get_connector_token_request_reference_id()); let connector = api::ConnectorData::get_connector_by_name( &state.conf.connectors, @@ -962,7 +962,7 @@ pub async fn construct_payment_router_data_for_setup_mandate<'a>( .payment_attempt .connector_token_details .as_ref() - .and_then(|detail| detail.get_connector_mandate_request_reference_id()); + .and_then(|detail| detail.get_connector_token_request_reference_id()); // TODO: evaluate the fields in router data, if they are required or not let router_data = types::RouterData { @@ -1286,6 +1286,7 @@ where external_latency: Option, is_latency_header_enabled: Option, merchant_account: &domain::MerchantAccount, + profile: &domain::Profile, ) -> RouterResponse; } @@ -1302,6 +1303,7 @@ where external_latency: Option, is_latency_header_enabled: Option, merchant_account: &domain::MerchantAccount, + profile: &domain::Profile, ) -> RouterResponse { let payment_intent = &self.payment_intent; let payment_attempt = &self.payment_attempt; @@ -1579,7 +1581,7 @@ where } #[cfg(feature = "v2")] -impl GenerateResponse +impl GenerateResponse for hyperswitch_domain_models::payments::PaymentConfirmData where F: Clone, @@ -1591,7 +1593,8 @@ where external_latency: Option, is_latency_header_enabled: Option, merchant_account: &domain::MerchantAccount, - ) -> RouterResponse { + profile: &domain::Profile, + ) -> RouterResponse { let payment_intent = self.payment_intent; let payment_attempt = self.payment_attempt; @@ -1645,26 +1648,36 @@ where .connector_token_details .and_then(Option::::foreign_from); - let response = api_models::payments::PaymentsConfirmIntentResponse { + let return_url = payment_intent + .return_url + .clone() + .or(profile.return_url.clone()); + + let response = api_models::payments::PaymentsResponse { id: payment_intent.id.clone(), status: payment_intent.status, amount, customer_id: payment_intent.customer_id.clone(), - connector, + connector: Some(connector), client_secret: payment_intent.client_secret.clone(), created: payment_intent.created_at, payment_method_data, - payment_method_type: payment_attempt.payment_method_type, - payment_method_subtype: payment_attempt.payment_method_subtype, + payment_method_type: Some(payment_attempt.payment_method_type), + payment_method_subtype: Some(payment_attempt.payment_method_subtype), next_action, connector_transaction_id: payment_attempt.connector_payment_id.clone(), connector_reference_id: None, connector_token_details, - merchant_connector_id, + merchant_connector_id: Some(merchant_connector_id), browser_info: None, error, + return_url, authentication_type: payment_intent.authentication_type, - applied_authentication_type: payment_attempt.authentication_type, + authentication_type_applied: Some(payment_attempt.authentication_type), + payment_method_id: payment_attempt.payment_method_id, + attempts: None, + billing: None, //TODO: add this + shipping: None, //TODO: add this }; Ok(services::ApplicationResponse::JsonWithHeaders(( @@ -1675,7 +1688,7 @@ where } #[cfg(feature = "v2")] -impl GenerateResponse +impl GenerateResponse for hyperswitch_domain_models::payments::PaymentStatusData where F: Clone, @@ -1687,7 +1700,8 @@ where external_latency: Option, is_latency_header_enabled: Option, merchant_account: &domain::MerchantAccount, - ) -> RouterResponse { + profile: &domain::Profile, + ) -> RouterResponse { let payment_intent = self.payment_intent; let optional_payment_attempt = self.payment_attempt.as_ref(); @@ -1723,7 +1737,15 @@ where .map(From::from), }); - let response = api_models::payments::PaymentsRetrieveResponse { + let connector_token_details = self + .payment_attempt + .as_ref() + .and_then(|attempt| attempt.connector_token_details.clone()) + .and_then(Option::::foreign_from); + + let return_url = payment_intent.return_url.or(profile.return_url.clone()); + + let response = api_models::payments::PaymentsResponse { id: payment_intent.id.clone(), status: payment_intent.status, amount, @@ -1753,8 +1775,20 @@ where connector_reference_id: None, merchant_connector_id, browser_info: None, + connector_token_details, + payment_method_id: self + .payment_attempt + .as_ref() + .and_then(|attempt| attempt.payment_method_id.clone()), error, + authentication_type_applied: self + .payment_attempt + .as_ref() + .and_then(|attempt| attempt.authentication_applied), + authentication_type: payment_intent.authentication_type, + next_action: None, attempts, + return_url, }; Ok(services::ApplicationResponse::JsonWithHeaders(( @@ -1948,7 +1982,7 @@ pub fn payments_to_payments_response( _connector_http_status_code: Option, _external_latency: Option, _is_latency_header_enabled: Option, -) -> RouterResponse +) -> RouterResponse where Op: Debug, D: OperationSessionGetters, @@ -4502,9 +4536,14 @@ impl ForeignFrom for Option { fn foreign_from(value: diesel_models::ConnectorTokenDetails) -> Self { - value - .connector_mandate_id - .map(|mandate_id| api_models::payments::ConnectorTokenDetails { token: mandate_id }) + let connector_token_request_reference_id = + value.connector_token_request_reference_id.clone(); + value.connector_mandate_id.clone().map(|mandate_id| { + api_models::payments::ConnectorTokenDetails { + token: mandate_id, + connector_token_request_reference_id, + } + }) } } diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index 392cfd4382..450714b2b8 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -26,10 +26,6 @@ use diesel_models::{ generic_link::{GenericLinkNew, PayoutLink}, CommonMandateReference, PayoutsMandateReference, PayoutsMandateReferenceRecord, }; -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -use diesel_models::{ - PaymentsMandateReference, PaymentsMandateReferenceRecord as PaymentsMandateReferenceRecordV2, -}; use error_stack::{report, ResultExt}; #[cfg(feature = "olap")] use futures::future::join_all; diff --git a/crates/router/src/core/webhooks/incoming_v2.rs b/crates/router/src/core/webhooks/incoming_v2.rs index c564b40583..f67eb5c15c 100644 --- a/crates/router/src/core/webhooks/incoming_v2.rs +++ b/crates/router/src/core/webhooks/incoming_v2.rs @@ -447,7 +447,7 @@ async fn payments_incoming_webhook_flow( req_state, merchant_account.clone(), key_store.clone(), - profile, + &profile, payments::operations::PaymentGet, api::PaymentsRetrieveRequest { force_sync: true, @@ -466,6 +466,7 @@ async fn payments_incoming_webhook_flow( external_latency, None, &merchant_account, + &profile, ); lock_action diff --git a/crates/router/src/core/webhooks/recovery_incoming.rs b/crates/router/src/core/webhooks/recovery_incoming.rs index 3634acfab9..b87311c0be 100644 --- a/crates/router/src/core/webhooks/recovery_incoming.rs +++ b/crates/router/src/core/webhooks/recovery_incoming.rs @@ -8,7 +8,7 @@ use router_env::{instrument, tracing}; use crate::{ core::{ errors::{self, CustomResult}, - payments::{self, operations}, + payments, }, routes::{app::ReqState, SessionState}, services::{self, connector_integration_interface}, @@ -215,8 +215,11 @@ impl RevenueRecoveryInvoice { )) .await .change_context(errors::RevenueRecoveryError::PaymentIntentCreateFailed)?; - let response = payments::handle_payments_intent_response(create_intent_response) - .change_context(errors::RevenueRecoveryError::PaymentIntentCreateFailed)?; + + let response = create_intent_response + .get_json_body() + .change_context(errors::RevenueRecoveryError::PaymentIntentCreateFailed) + .attach_printable("expected json response")?; Ok(revenue_recovery::RecoveryPaymentIntent { payment_id: response.id, @@ -239,7 +242,7 @@ impl RevenueRecoveryAttempt { { let attempt_response = Box::pin(payments::payments_core::< hyperswitch_domain_models::router_flow_types::payments::PSync, - api_models::payments::PaymentsRetrieveResponse, + api_models::payments::PaymentsResponse, _, _, _, diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index a8189468b1..ac5db83b3f 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -4009,7 +4009,7 @@ impl db::payment_method_session::PaymentMethodsSessionInterface for KafkaStore { &self, state: &KeyManagerState, key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore, - payment_methods_session: hyperswitch_domain_models::payment_methods::PaymentMethodsSession, + payment_methods_session: hyperswitch_domain_models::payment_methods::PaymentMethodSession, validity: i64, ) -> CustomResult<(), errors::StorageError> { self.diesel_store @@ -4023,13 +4023,35 @@ impl db::payment_method_session::PaymentMethodsSessionInterface for KafkaStore { key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore, id: &id_type::GlobalPaymentMethodSessionId, ) -> CustomResult< - hyperswitch_domain_models::payment_methods::PaymentMethodsSession, + hyperswitch_domain_models::payment_methods::PaymentMethodSession, errors::StorageError, > { self.diesel_store .get_payment_methods_session(state, key_store, id) .await } + + async fn update_payment_method_session( + &self, + state: &KeyManagerState, + key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore, + id: &id_type::GlobalPaymentMethodSessionId, + payment_methods_session: hyperswitch_domain_models::payment_methods::PaymentMethodsSessionUpdateEnum, + current_session: hyperswitch_domain_models::payment_methods::PaymentMethodSession, + ) -> CustomResult< + hyperswitch_domain_models::payment_methods::PaymentMethodSession, + errors::StorageError, + > { + self.diesel_store + .update_payment_method_session( + state, + key_store, + id, + payment_methods_session, + current_session, + ) + .await + } } #[async_trait::async_trait] diff --git a/crates/router/src/db/payment_method_session.rs b/crates/router/src/db/payment_method_session.rs index a7e500ac52..5dd00e3690 100644 --- a/crates/router/src/db/payment_method_session.rs +++ b/crates/router/src/db/payment_method_session.rs @@ -9,17 +9,29 @@ pub trait PaymentMethodsSessionInterface { &self, state: &common_utils::types::keymanager::KeyManagerState, key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore, - payment_methods_session: hyperswitch_domain_models::payment_methods::PaymentMethodsSession, + payment_methods_session: hyperswitch_domain_models::payment_methods::PaymentMethodSession, validity: i64, ) -> CustomResult<(), errors::StorageError>; + async fn update_payment_method_session( + &self, + state: &common_utils::types::keymanager::KeyManagerState, + key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore, + id: &common_utils::id_type::GlobalPaymentMethodSessionId, + payment_methods_session: hyperswitch_domain_models::payment_methods::PaymentMethodsSessionUpdateEnum, + current_session: hyperswitch_domain_models::payment_methods::PaymentMethodSession, + ) -> CustomResult< + hyperswitch_domain_models::payment_methods::PaymentMethodSession, + errors::StorageError, + >; + async fn get_payment_methods_session( &self, state: &common_utils::types::keymanager::KeyManagerState, key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore, id: &common_utils::id_type::GlobalPaymentMethodSessionId, ) -> CustomResult< - hyperswitch_domain_models::payment_methods::PaymentMethodsSession, + hyperswitch_domain_models::payment_methods::PaymentMethodSession, errors::StorageError, >; } @@ -50,7 +62,7 @@ mod storage { &self, _state: &common_utils::types::keymanager::KeyManagerState, _key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore, - payment_methods_session: hyperswitch_domain_models::payment_methods::PaymentMethodsSession, + payment_methods_session: hyperswitch_domain_models::payment_methods::PaymentMethodSession, validity_in_seconds: i64, ) -> CustomResult<(), errors::StorageError> { let redis_key = payment_methods_session.id.get_redis_key(); @@ -78,7 +90,7 @@ mod storage { key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore, id: &common_utils::id_type::GlobalPaymentMethodSessionId, ) -> CustomResult< - hyperswitch_domain_models::payment_methods::PaymentMethodsSession, + hyperswitch_domain_models::payment_methods::PaymentMethodSession, errors::StorageError, > { let redis_key = id.get_redis_key(); @@ -88,7 +100,7 @@ mod storage { .map_err(Into::::into)?; let db_model = redis_connection - .get_and_deserialize_key::(&redis_key.into(), "PaymentMethodsSession") + .get_and_deserialize_key::(&redis_key.into(), "PaymentMethodSession") .await .change_context(errors::StorageError::KVError)?; @@ -102,9 +114,52 @@ mod storage { .change_context(errors::StorageError::DecryptionError) .attach_printable("Failed to decrypt payment methods session") } + + #[instrument(skip_all)] + async fn update_payment_method_session( + &self, + state: &common_utils::types::keymanager::KeyManagerState, + key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore, + session_id: &common_utils::id_type::GlobalPaymentMethodSessionId, + update_request: hyperswitch_domain_models::payment_methods::PaymentMethodsSessionUpdateEnum, + current_session: hyperswitch_domain_models::payment_methods::PaymentMethodSession, + ) -> CustomResult< + hyperswitch_domain_models::payment_methods::PaymentMethodSession, + errors::StorageError, + > { + let redis_key = session_id.get_redis_key(); + + let internal_obj = hyperswitch_domain_models::payment_methods::PaymentMethodsSessionUpdateInternal::from(update_request); + + let update_state = current_session.apply_changeset(internal_obj); + + let db_model = update_state + .construct_new() + .await + .change_context(errors::StorageError::EncryptionError)?; + + let redis_connection = self + .get_redis_conn() + .map_err(Into::::into)?; + + redis_connection + .serialize_and_set_key_without_modifying_ttl(&redis_key.into(), db_model.clone()) + .await + .change_context(errors::StorageError::KVError) + .attach_printable("Failed to insert payment methods session to redis"); + + let key_manager_identifier = common_utils::types::keymanager::Identifier::Merchant( + key_store.merchant_id.clone(), + ); + + db_model + .convert(state, &key_store.key, key_manager_identifier) + .await + .change_context(errors::StorageError::DecryptionError) + .attach_printable("Failed to decrypt payment methods session") + } } } - #[cfg(feature = "v2")] #[async_trait::async_trait] impl PaymentMethodsSessionInterface for MockDb { @@ -112,12 +167,26 @@ impl PaymentMethodsSessionInterface for MockDb { &self, state: &common_utils::types::keymanager::KeyManagerState, key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore, - payment_methods_session: hyperswitch_domain_models::payment_methods::PaymentMethodsSession, + payment_methods_session: hyperswitch_domain_models::payment_methods::PaymentMethodSession, validity_in_seconds: i64, ) -> CustomResult<(), errors::StorageError> { Err(errors::StorageError::MockDbError)? } + async fn update_payment_method_session( + &self, + state: &common_utils::types::keymanager::KeyManagerState, + key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore, + id: &common_utils::id_type::GlobalPaymentMethodSessionId, + payment_methods_session: hyperswitch_domain_models::payment_methods::PaymentMethodsSessionUpdateEnum, + current_session: hyperswitch_domain_models::payment_methods::PaymentMethodSession, + ) -> CustomResult< + hyperswitch_domain_models::payment_methods::PaymentMethodSession, + errors::StorageError, + > { + Err(errors::StorageError::MockDbError)? + } + #[cfg(feature = "v2")] async fn get_payment_methods_session( &self, @@ -125,7 +194,7 @@ impl PaymentMethodsSessionInterface for MockDb { key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore, id: &common_utils::id_type::GlobalPaymentMethodSessionId, ) -> CustomResult< - hyperswitch_domain_models::payment_methods::PaymentMethodsSession, + hyperswitch_domain_models::payment_methods::PaymentMethodSession, errors::StorageError, > { Err(errors::StorageError::MockDbError)? diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 18e8e7cdcc..349fb91efc 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -154,7 +154,7 @@ pub fn mk_app( #[cfg(all(feature = "v2", feature = "oltp"))] { - server_app = server_app.service(routes::PaymentMethodsSession::server(state.clone())); + server_app = server_app.service(routes::PaymentMethodSession::server(state.clone())); } #[cfg(feature = "v1")] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 0b50661166..78d2b508a4 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -68,7 +68,7 @@ pub mod relay; #[cfg(feature = "dummy_connector")] pub use self::app::DummyConnector; #[cfg(feature = "v2")] -pub use self::app::PaymentMethodsSession; +pub use self::app::PaymentMethodSession; #[cfg(all(feature = "olap", feature = "recon", feature = "v1"))] pub use self::app::Recon; pub use self::app::{ diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index dfcded6a2f..df4b9977b9 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1287,10 +1287,10 @@ impl PaymentMethods { } #[cfg(all(feature = "v2", feature = "oltp"))] -pub struct PaymentMethodsSession; +pub struct PaymentMethodSession; #[cfg(all(feature = "v2", feature = "oltp"))] -impl PaymentMethodsSession { +impl PaymentMethodSession { pub fn server(state: AppState) -> Scope { let mut route = web::scope("/v2/payment-methods-session").app_data(web::Data::new(state)); route = route.service( @@ -1298,23 +1298,27 @@ impl PaymentMethodsSession { .route(web::post().to(payment_methods::payment_methods_session_create)), ); - route = route.service( - web::scope("/{payment_method_session_id}") - .service( - web::resource("") - .route(web::get().to(payment_methods::payment_methods_session_retrieve)), - ) - .service(web::resource("/list-payment-methods").route( - web::get().to(payment_methods::payment_method_session_list_payment_methods), - )) - .service( - web::resource("/update-saved-payment-method").route( + route = + route.service( + web::scope("/{payment_method_session_id}") + .service( + web::resource("") + .route(web::get().to(payment_methods::payment_methods_session_retrieve)) + .route(web::put().to(payment_methods::payment_methods_session_update)), + ) + .service(web::resource("/list-payment-methods").route( + web::get().to(payment_methods::payment_method_session_list_payment_methods), + )) + .service( + web::resource("/confirm") + .route(web::post().to(payment_methods::payment_method_session_confirm)), + ) + .service(web::resource("/update-saved-payment-method").route( web::put().to( payment_methods::payment_method_session_update_saved_payment_method, ), - ), - ), - ); + )), + ); route } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index f48a7970fd..fd490c6fe4 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -40,7 +40,7 @@ pub enum ApiIdentifier { Relay, Documentation, Hypersense, - PaymentMethodsSession, + PaymentMethodSession, } impl From for ApiIdentifier { @@ -319,7 +319,9 @@ impl From for ApiIdentifier { Flow::PaymentMethodSessionCreate | Flow::PaymentMethodSessionRetrieve - | Flow::PaymentMethodSessionUpdateSavedPaymentMethod => Self::PaymentMethodsSession, + | Flow::PaymentMethodSessionConfirm + | Flow::PaymentMethodSessionUpdateSavedPaymentMethod + | Flow::PaymentMethodSessionUpdate => Self::PaymentMethodSession, } } } diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 98be9d6a26..8d17a41664 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -79,9 +79,10 @@ pub async fn create_payment_method_api( state, &req, json_payload.into_inner(), - |state, auth: auth::AuthenticationData, req, _| async move { + |state, auth: auth::AuthenticationData, req, req_state| async move { Box::pin(payment_methods_routes::create_payment_method( &state, + &req_state, req, &auth.merchant_account, &auth.key_store, @@ -956,6 +957,41 @@ pub async fn payment_methods_session_create( .await } +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::PaymentMethodSessionUpdate))] +pub async fn payment_methods_session_update( + state: web::Data, + req: HttpRequest, + path: web::Path, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::PaymentMethodSessionUpdate; + let payment_method_session_id = path.into_inner(); + let payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: auth::AuthenticationData, req, _| { + let value = payment_method_session_id.clone(); + async move { + payment_methods_routes::payment_methods_session_update( + state, + auth.merchant_account, + auth.key_store, + value.clone(), + req, + ) + .await + } + }, + &auth::V2ApiKeyAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(feature = "v2")] #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodSessionRetrieve))] pub async fn payment_methods_session_retrieve( @@ -1047,6 +1083,49 @@ impl common_utils::events::ApiEventMetric } } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentMethodSessionConfirm))] +pub async fn payment_method_session_confirm( + state: web::Data, + req: HttpRequest, + path: web::Path, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::PaymentMethodSessionConfirm; + let payload = json_payload.into_inner(); + let payment_method_session_id = path.into_inner(); + + let request = PaymentMethodsSessionGenericRequest { + payment_method_session_id: payment_method_session_id.clone(), + request: payload, + }; + + Box::pin(api::server_wrap( + flow, + state, + &req, + request, + |state, auth: auth::AuthenticationData, request, req_state| { + payment_methods_routes::payment_methods_session_confirm( + state, + req_state, + auth.merchant_account, + auth.key_store, + auth.profile, + request.payment_method_session_id, + request.request, + ) + }, + &auth::V2ClientAuth( + common_utils::types::authentication::ResourceId::PaymentMethodSession( + payment_method_session_id, + ), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodSessionUpdateSavedPaymentMethod))] pub async fn payment_method_session_update_saved_payment_method( diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index f24a0e6374..e4ced3ac0b 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -232,9 +232,6 @@ pub async fn payments_create_and_confirm_intent( } }; - let global_payment_id = - common_utils::id_type::GlobalPaymentId::generate(&state.conf.cell_information.id); - Box::pin(api::server_wrap( flow, state, @@ -248,7 +245,6 @@ pub async fn payments_create_and_confirm_intent( auth.profile, auth.key_store, request, - global_payment_id.clone(), header_payload.clone(), auth.platform_merchant_account, ) @@ -2454,7 +2450,7 @@ pub async fn payment_confirm_intent( Box::pin(payments::payments_core::< api_types::Authorize, - api_models::payments::PaymentsConfirmIntentResponse, + api_models::payments::PaymentsResponse, _, _, _, @@ -2524,7 +2520,7 @@ pub async fn payment_status( Box::pin(payments::payments_core::< api_types::PSync, - api_models::payments::PaymentsRetrieveResponse, + api_models::payments::PaymentsResponse, _, _, _, diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 6021ee70d9..b4655b6b77 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -555,8 +555,12 @@ pub enum Flow { PaymentMethodSessionCreate, /// Payment Method Session Retrieve PaymentMethodSessionRetrieve, + // Payment Method Session Update + PaymentMethodSessionUpdate, /// Update a saved payment method using the payment methods session PaymentMethodSessionUpdateSavedPaymentMethod, + /// Confirm a payment method session with payment method data + PaymentMethodSessionConfirm, } /// Trait for providing generic behaviour to flow metric diff --git a/crates/storage_impl/src/errors.rs b/crates/storage_impl/src/errors.rs index 10d7f4dc8e..d75911d593 100644 --- a/crates/storage_impl/src/errors.rs +++ b/crates/storage_impl/src/errors.rs @@ -111,6 +111,7 @@ impl StorageError { match self { Self::DatabaseError(err) => matches!(err.current_context(), DatabaseError::NotFound), Self::ValueNotFound(_) => true, + Self::RedisError(err) => matches!(err.current_context(), RedisError::NotFound), _ => false, } }