diff --git a/api-reference-v2/api-reference/customers/customers--list-saved-payment-methods.mdx b/api-reference-v2/api-reference/customers/customers--list-saved-payment-methods.mdx new file mode 100644 index 0000000000..ef5a27f960 --- /dev/null +++ b/api-reference-v2/api-reference/customers/customers--list-saved-payment-methods.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v2/customers/{id}/saved-payment-methods +--- \ No newline at end of file diff --git a/api-reference-v2/api-reference/payment-method-session/payment-method-session--create.mdx b/api-reference-v2/api-reference/payment-method-session/payment-method-session--create.mdx new file mode 100644 index 0000000000..b75390a47d --- /dev/null +++ b/api-reference-v2/api-reference/payment-method-session/payment-method-session--create.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /v2/payment-method-session +--- diff --git a/api-reference-v2/api-reference/payment-method-session/payment-method-session--list-payment-methods.mdx b/api-reference-v2/api-reference/payment-method-session/payment-method-session--list-payment-methods.mdx new file mode 100644 index 0000000000..bbbaf01c29 --- /dev/null +++ b/api-reference-v2/api-reference/payment-method-session/payment-method-session--list-payment-methods.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v2/payment-method-session/:id/list-payment-methods +--- \ No newline at end of file diff --git a/api-reference-v2/api-reference/payment-method-session/payment-method-session--retrieve.mdx b/api-reference-v2/api-reference/payment-method-session/payment-method-session--retrieve.mdx new file mode 100644 index 0000000000..3f0ab4a746 --- /dev/null +++ b/api-reference-v2/api-reference/payment-method-session/payment-method-session--retrieve.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v2/payment-method-session/:id +--- \ No newline at end of file diff --git a/api-reference-v2/api-reference/payment-method-session/payment-method-session--update-a-saved-payment-method.mdx b/api-reference-v2/api-reference/payment-method-session/payment-method-session--update-a-saved-payment-method.mdx new file mode 100644 index 0000000000..fab85e2a41 --- /dev/null +++ b/api-reference-v2/api-reference/payment-method-session/payment-method-session--update-a-saved-payment-method.mdx @@ -0,0 +1,3 @@ +--- +openapi: put /v2/payment-method-session/:id/update-saved-payment-method +--- diff --git a/api-reference-v2/api-reference/payment-methods/payment-methods--payment-methods-list.mdx b/api-reference-v2/api-reference/payment-methods/payment-methods--payment-methods-list.mdx new file mode 100644 index 0000000000..b7f8edccff --- /dev/null +++ b/api-reference-v2/api-reference/payment-methods/payment-methods--payment-methods-list.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v2/payment-methods/{id}/list-enabled-payment-methods +--- \ No newline at end of file diff --git a/api-reference-v2/mint.json b/api-reference-v2/mint.json index 11e716d630..8fd3b64230 100644 --- a/api-reference-v2/mint.json +++ b/api-reference-v2/mint.json @@ -55,7 +55,17 @@ "api-reference/payment-methods/payment-method--confirm-intent", "api-reference/payment-methods/payment-method--update", "api-reference/payment-methods/payment-method--retrieve", - "api-reference/payment-methods/payment-method--delete" + "api-reference/payment-methods/payment-method--delete", + "api-reference/payment-methods/list-saved-payment-methods-for-a-customer" + ] + }, + { + "group": "Payment Method Session", + "pages": [ + "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" ] }, { @@ -122,7 +132,8 @@ "api-reference/customers/customers--retrieve", "api-reference/customers/customers--update", "api-reference/customers/customers--delete", - "api-reference/customers/customers--list" + "api-reference/customers/customers--list", + "api-reference/customers/customers--list-saved-payment-methods" ] }, { diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 96294f56a9..ba795d4059 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -2338,58 +2338,6 @@ ] } }, - "/v2/payment-methods/{id}/list-enabled-payment-methods": { - "get": { - "tags": [ - "Payment Methods" - ], - "summary": "Payment Methods - Payment Methods List", - "description": "List the payment methods eligible for a payment method.", - "operationId": "List Payment methods for a Payment Method Intent", - "parameters": [ - { - "name": "id", - "in": "path", - "description": "The global payment method id", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "X-Profile-Id", - "in": "header", - "description": "Profile ID associated to the payment method intent", - "required": true, - "schema": { - "type": "string" - }, - "example": "pro_abcdefghijklmnop" - } - ], - "responses": { - "200": { - "description": "Get the payment methods", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PaymentMethodListResponseForPayments" - } - } - } - }, - "404": { - "description": "No payment method found with the given id" - } - }, - "security": [ - { - "api_key": [], - "ephemeral_key": [] - } - ] - } - }, "/v2/payment-methods/{id}/confirm-intent": { "post": { "tags": [ @@ -2550,6 +2498,198 @@ ] } }, + "/v2/payment-method-session": { + "post": { + "tags": [ + "Payment Method Session" + ], + "summary": "Payment Method Session - Create", + "description": "Create a payment method session for a customer\nThis is used to list the saved payment methods for the customer\nThe customer can also add a new payment method using this session", + "operationId": "Create a payment method session", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodSessionRequest" + }, + "examples": { + "Create a payment method session with customer_id": { + "value": { + "customer_id": "12345_cus_abcdefghijklmnopqrstuvwxyz" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Create the payment method session", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodsSessionResponse" + } + } + } + }, + "400": { + "description": "The request is invalid" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/v2/payment-method-session/:id": { + "get": { + "tags": [ + "Payment Method Session" + ], + "summary": "Payment Method Session - Retrieve", + "description": "Retrieve the payment method session", + "operationId": "Retrieve the payment method session", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The unique identifier for the Payment Method Session", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The payment method session is retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodsSessionResponse" + } + } + } + }, + "404": { + "description": "The request is invalid" + } + }, + "security": [ + { + "ephemeral_key": [] + } + ] + } + }, + "/v2/payment-method-session/:id/list-payment-methods": { + "get": { + "tags": [ + "Payment Method Session" + ], + "summary": "Payment Method Session - List Payment Methods", + "description": "List payment methods for the given payment method session.\nThis endpoint lists the enabled payment methods for the profile and the saved payment methods of the customer.", + "operationId": "List Payment methods for a Payment Method Session", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The unique identifier for the Payment Method Session", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The payment method session is retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodListResponse" + } + } + } + }, + "404": { + "description": "The request is invalid" + } + }, + "security": [ + { + "ephemeral_key": [] + } + ] + } + }, + "/v2/payment-method-session/:id/update-saved-payment-method": { + "put": { + "tags": [ + "Payment Method Session" + ], + "summary": "Payment Method Session - Update a saved payment method", + "description": "Update a saved payment method from the given payment method session.", + "operationId": "Update a saved payment method", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The unique identifier for the Payment Method Session", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodSessionUpdateSavedPaymentMethod" + }, + "examples": { + "Update the card holder name": { + "value": { + "payment_method_data": { + "card": { + "card_holder_name": "Narayan Bhat" + } + }, + "payment_method_id": "12345_pm_0194b1ecabc172e28aeb71f70a4daba3" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "The payment method has been updated successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentMethodResponse" + } + } + } + }, + "404": { + "description": "The request is invalid" + } + }, + "security": [ + { + "ephemeral_key": [] + } + ] + } + }, "/v2/refunds": { "post": { "tags": [ @@ -6709,6 +6849,42 @@ } } }, + "ClientSecretResponse": { + "type": "object", + "description": "client_secret for the resource_id mentioned", + "required": [ + "id", + "resource_id", + "created_at", + "expires", + "secret" + ], + "properties": { + "id": { + "type": "string", + "description": "Client Secret id", + "maxLength": 32, + "minLength": 1 + }, + "resource_id": { + "$ref": "#/components/schemas/ResourceId" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "time at which this client secret was created" + }, + "expires": { + "type": "string", + "format": "date-time", + "description": "time at which this client secret would expire" + }, + "secret": { + "type": "string", + "description": "client secret" + } + } + }, "Comparison": { "type": "object", "description": "Represents a single comparison condition.", @@ -7779,25 +7955,20 @@ "CustomerPaymentMethod": { "type": "object", "required": [ - "payment_method_id", + "id", "customer_id", "payment_method_type", + "payment_method_subtype", "recurring_enabled", "created", "requires_cvv", "is_default" ], "properties": { - "payment_token": { + "id": { "type": "string", - "description": "Token for payment method in temporary card locker which gets refreshed often", - "example": "7ebf443f-a050-4067-84e5-e6f6d4800aef", - "nullable": true - }, - "payment_method_id": { - "type": "string", - "description": "The unique identifier of the customer.", - "example": "pm_iouuy468iyuowqs" + "description": "The unique identifier of the payment method.", + "example": "12345_pm_01926c58bc6e77c09e809964e72af8c8" }, "customer_id": { "type": "string", @@ -7810,12 +7981,7 @@ "$ref": "#/components/schemas/PaymentMethod" }, "payment_method_subtype": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodType" - } - ], - "nullable": true + "$ref": "#/components/schemas/PaymentMethodType" }, "recurring_enabled": { "type": "boolean", @@ -7844,14 +8010,6 @@ "description": "A timestamp (ISO 8601 code) that determines when the payment method was created", "example": "2023-01-18T11:04:09.922Z" }, - "surcharge_details": { - "allOf": [ - { - "$ref": "#/components/schemas/SurchargeDetailsResponse" - } - ], - "nullable": true - }, "requires_cvv": { "type": "boolean", "description": "Whether this payment method requires CVV to be collected", @@ -7861,8 +8019,7 @@ "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 + "example": "2024-02-24T11:04:09.922Z" }, "is_default": { "type": "boolean", @@ -7891,11 +8048,6 @@ "$ref": "#/components/schemas/CustomerPaymentMethod" }, "description": "List of payment methods for customer" - }, - "is_guest_customer": { - "type": "boolean", - "description": "Returns whether a customer id is not tied to a payment intent (only when the request is made against a client secret)", - "nullable": true } } }, @@ -8059,7 +8211,7 @@ "default_payment_method_id": { "type": "string", "description": "The identifier for the default payment method.", - "example": "pm_djh2837dwduh890123", + "example": "12345_pm_01926c58bc6e77c09e809964e72af8c8", "nullable": true, "maxLength": 64 } @@ -8137,7 +8289,7 @@ "default_payment_method_id": { "type": "string", "description": "The unique identifier of the payment method", - "example": "card_rGK4Vi5iSW70MY7J2mIg", + "example": "12345_pm_01926c58bc6e77c09e809964e72af8c8", "nullable": true } }, @@ -8492,39 +8644,6 @@ } } }, - "EphemeralKeyCreateResponse": { - "type": "object", - "description": "ephemeral_key for the customer_id mentioned", - "required": [ - "customer_id", - "created_at", - "expires", - "secret" - ], - "properties": { - "customer_id": { - "type": "string", - "description": "customer_id to which this ephemeral key belongs to", - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "maxLength": 64, - "minLength": 1 - }, - "created_at": { - "type": "integer", - "format": "int64", - "description": "time at which this ephemeral key was created" - }, - "expires": { - "type": "integer", - "format": "int64", - "description": "time at which this ephemeral key would expire" - }, - "secret": { - "type": "string", - "description": "ephemeral key" - } - } - }, "ErrorCategory": { "type": "string", "enum": [ @@ -11712,6 +11831,26 @@ } } }, + "NetworkTokenization": { + "type": "object", + "description": "The network tokenization configuration for creating the payment method session", + "required": [ + "enable" + ], + "properties": { + "enable": { + "$ref": "#/components/schemas/NetworkTokenizationToggle" + } + } + }, + "NetworkTokenizationToggle": { + "type": "string", + "description": "The network tokenization toggle, whether to enable or skip the network tokenization", + "enum": [ + "Enable", + "Skip" + ] + }, "NetworkTransactionIdAndCardDetails": { "type": "object", "required": [ @@ -12394,7 +12533,7 @@ ] }, "object": { - "$ref": "#/components/schemas/PaymentsResponse" + "$ref": "#/components/schemas/PaymentsRetrieveResponse" } } }, @@ -14247,7 +14386,8 @@ "PaymentMethodListResponse": { "type": "object", "required": [ - "payment_methods_enabled" + "payment_methods_enabled", + "customer_payment_methods" ], "properties": { "payment_methods_enabled": { @@ -14256,6 +14396,13 @@ "$ref": "#/components/schemas/ResponsePaymentMethodTypes" }, "description": "The list of payment methods that are enabled for the business profile" + }, + "customer_payment_methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerPaymentMethod" + }, + "description": "The list of saved payment methods of the customer" } } }, @@ -14338,11 +14485,6 @@ "example": "2024-02-24T11:04:09.922Z", "nullable": true }, - "ephemeral_key": { - "type": "string", - "description": "For Client based calls", - "nullable": true - }, "payment_method_data": { "allOf": [ { @@ -14368,6 +14510,72 @@ } ] }, + "PaymentMethodSessionRequest": { + "type": "object", + "required": [ + "customer_id" + ], + "properties": { + "customer_id": { + "type": "string", + "description": "The customer id for which the payment methods session is to be created", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44" + }, + "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_in": { + "type": "integer", + "format": "int32", + "description": "The time (seconds ) when the session will expire\nIf not provided, the session will expire in 15 minutes", + "default": 900, + "example": 900, + "nullable": true, + "minimum": 0 + } + } + }, + "PaymentMethodSessionUpdateSavedPaymentMethod": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentMethodUpdate" + }, + { + "type": "object", + "required": [ + "payment_method_id" + ], + "properties": { + "payment_method_id": { + "type": "string", + "description": "The payment method id of the payment method to be updated", + "example": "12345_pm_01926c58bc6e77c09e809964e72af8c8" + } + } + } + ] + }, "PaymentMethodSpecificFeatures": { "oneOf": [ { @@ -14569,6 +14777,60 @@ }, "additionalProperties": false }, + "PaymentMethodsSessionResponse": { + "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" + } + } + }, "PaymentProcessingDetails": { "type": "object", "required": [ @@ -17858,6 +18120,23 @@ } } }, + "PspTokenization": { + "type": "object", + "description": "The Payment Service Provider Configuration for payment methods that are created using the payment method session", + "required": [ + "tokenization_type", + "connector_id" + ], + "properties": { + "tokenization_type": { + "$ref": "#/components/schemas/TokenizationType" + }, + "connector_id": { + "type": "string", + "description": "The merchant connector id to be used for tokenization" + } + } + }, "RealTimePaymentData": { "oneOf": [ { @@ -18567,6 +18846,21 @@ } } }, + "ResourceId": { + "oneOf": [ + { + "type": "object", + "required": [ + "customer" + ], + "properties": { + "customer": { + "type": "string" + } + } + } + ] + }, "ResponsePaymentMethodTypes": { "allOf": [ { @@ -20480,6 +20774,14 @@ } } }, + "TokenizationType": { + "type": "string", + "description": "The type of tokenization to use for the payment method", + "enum": [ + "single_use", + "multi_use" + ] + }, "TouchNGoRedirection": { "type": "object" }, diff --git a/config/development.toml b/config/development.toml index fc7a9427d8..a7e33511f1 100644 --- a/config/development.toml +++ b/config/development.toml @@ -69,8 +69,8 @@ common_merchant_identifier = "COMMON MERCHANT IDENTIFIER" applepay_endpoint = "DOMAIN SPECIFIC ENDPOINT" [locker] -host = "" -host_rs = "" +host = "http://127.0.0.1:3000" +host_rs = "http://127.0.0.1:3000" mock_locker = true basilisk_host = "" locker_enabled = true @@ -83,9 +83,57 @@ fallback_api_key = "" redis_lock_timeout = 100 [jwekey] -vault_encryption_key = "" -rust_locker_encryption_key = "" -vault_private_key = "" +vault_encryption_key = """ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwa6siKaSYqD1o4J3AbHq +Km8oVTvep7GoN/C45qY60C7DO72H1O7Ujt6ZsSiK83EyI0CaUg3ORPS3ayobFNmu +zR366ckK8GIf3BG7sVI6u/9751z4OvBHZMM9JFWa7Bx/RCPQ8aeM+iJoqf9auuQm +3NCTlfaZJif45pShswR+xuZTR/bqnsOSP/MFROI9ch0NE7KRogy0tvrZe21lP24i +Ro2LJJG+bYshxBddhxQf2ryJ85+/Trxdu16PunodGzCl6EMT3bvb4ZC41i15omqU +aXXV1Z1wYUhlsO0jyd1bVvjyuE/KE1TbBS0gfR/RkacODmmE2zEdZ0EyyiXwqkmc +oQIDAQAB +-----END PUBLIC KEY----- +""" +rust_locker_encryption_key = """ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwa6siKaSYqD1o4J3AbHq +Km8oVTvep7GoN/C45qY60C7DO72H1O7Ujt6ZsSiK83EyI0CaUg3ORPS3ayobFNmu +zR366ckK8GIf3BG7sVI6u/9751z4OvBHZMM9JFWa7Bx/RCPQ8aeM+iJoqf9auuQm +3NCTlfaZJif45pShswR+xuZTR/bqnsOSP/MFROI9ch0NE7KRogy0tvrZe21lP24i +Ro2LJJG+bYshxBddhxQf2ryJ85+/Trxdu16PunodGzCl6EMT3bvb4ZC41i15omqU +aXXV1Z1wYUhlsO0jyd1bVvjyuE/KE1TbBS0gfR/RkacODmmE2zEdZ0EyyiXwqkmc +oQIDAQAB +-----END PUBLIC KEY----- +""" +vault_private_key = """ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA5Z/K0JWds8iHhWCa+rj0rhOQX1nVs/ArQ1D0vh3UlSPR2vZU +TrkdP7i3amv4d2XDC+3+5/YWExTkpxqnfl1T9J37leN2guAARed6oYoTDEP/OoKt +nUrKK2xk/+V5DNOWcRiSpcCrJOOIEoACOlPIrXQSg16KDZQb0QTMntnsiPIJDbsO +GcdKytRAcNaokiKLnia1v13N3bk6dSplrj1YzawslZfgqD0eov4FjzBMoA19yNtl +VLLf6kOkLcFQjTKXJLP1tLflLUBPTg8fm9wgAPK2BjMQ2AMkUxx0ubbtw/9CeJ+b +FWrqGnEhlvfDMlyAV77sAiIdQ4mXs3TLcLb/AQIDAQABAoIBAGNekD1N0e5AZG1S +zh6cNb6zVrH8xV9WGtLJ0PAJJrrXwnQYT4m10DOIM0+Jo/+/ePXLq5kkRI9DZmPu +Q/eKWc+tInfN9LZUS6n0r3wCrZWMQ4JFlO5RtEWwZdDbtFPZqOwObz/treKL2JHw +9YXaRijR50UUf3e61YLRqd9AfX0RNuuG8H+WgU3Gwuh5TwRnljM3JGaDPHsf7HLS +tNkqJuByp26FEbOLTokZDbHN0sy7/6hufxnIS9AK4vp8y9mZAjghG26Rbg/H71mp +Z+Q6P1y7xdgAKbhq7usG3/o4Y1e9wnghHvFS7DPwGekHQH2+LsYNEYOir1iRjXxH +GUXOhfUCgYEA+cR9jONQFco8Zp6z0wdGlBeYoUHVMjThQWEblWL2j4RG/qQd/y0j +uhVeU0/PmkYK2mgcjrh/pgDTtPymc+QuxBi+lexs89ppuJIAgMvLUiJT67SBHguP +l4+oL9U78KGh7PfJpMKH+Pk5yc1xucAevk0wWmr5Tz2vKRDavFTPV+MCgYEA61qg +Y7yN0cDtxtqlZdMC8BJPFCQ1+s3uB0wetAY3BEKjfYc2O/4sMbixXzt5PkZqZy96 +QBUBxhcM/rIolpM3nrgN7h1nmJdk9ljCTjWoTJ6fDk8BUh8+0GrVhTbe7xZ+bFUN +UioIqvfapr/q/k7Ah2mCBE04wTZFry9fndrH2ssCgYEAh1T2Cj6oiAX6UEgxe2h3 +z4oxgz6efAO3AavSPFFQ81Zi+VqHflpA/3TQlSerfxXwj4LV5mcFkzbjfy9eKXE7 +/bjCm41tQ3vWyNEjQKYr1qcO/aniRBtThHWsVa6eObX6fOGN+p4E+txfeX693j3A +6q/8QSGxUERGAmRFgMIbTq0CgYAmuTeQkXKII4U75be3BDwEgg6u0rJq/L0ASF74 +4djlg41g1wFuZ4if+bJ9Z8ywGWfiaGZl6s7q59oEgg25kKljHQd1uTLVYXuEKOB3 +e86gJK0o7ojaGTf9lMZi779IeVv9uRTDAxWAA93e987TXuPAo/R3frkq2SIoC9Rg +paGidwKBgBqYd/iOJWsUZ8cWEhSE1Huu5rDEpjra8JPXHqQdILirxt1iCA5aEQou +BdDGaDr8sepJbGtjwTyiG8gEaX1DD+KsF2+dQRQdQfcYC40n8fKkvpFwrKjDj1ac +VuY3OeNxi+dC2r7HppP3O/MJ4gX/RJJfSrcaGP8/Ke1W5+jE97Qy +-----END RSA PRIVATE KEY----- +""" tunnel_private_key = "" [connectors.supported] diff --git a/crates/api_models/src/customers.rs b/crates/api_models/src/customers.rs index fe4ba7d968..d78564ae53 100644 --- a/crates/api_models/src/customers.rs +++ b/crates/api_models/src/customers.rs @@ -213,8 +213,8 @@ pub struct CustomerResponse { #[schema(value_type = Option,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, /// The identifier for the default payment method. - #[schema(max_length = 64, example = "pm_djh2837dwduh890123")] - pub default_payment_method_id: Option, + #[schema(value_type = Option, max_length = 64, example = "12345_pm_01926c58bc6e77c09e809964e72af8c8")] + pub default_payment_method_id: Option, } #[cfg(all(feature = "v2", feature = "customer_v2"))] @@ -340,8 +340,8 @@ pub struct CustomerUpdateRequest { #[schema(value_type = Option,example = json!({ "city": "NY", "unit": "245" }))] pub metadata: Option, /// The unique identifier of the payment method - #[schema(example = "card_rGK4Vi5iSW70MY7J2mIg")] - pub default_payment_method_id: Option, + #[schema(value_type = Option, example = "12345_pm_01926c58bc6e77c09e809964e72af8c8")] + pub default_payment_method_id: Option, } #[cfg(all(feature = "v2", feature = "customer_v2"))] diff --git a/crates/api_models/src/ephemeral_key.rs b/crates/api_models/src/ephemeral_key.rs index fa61642e35..9f569b91f7 100644 --- a/crates/api_models/src/ephemeral_key.rs +++ b/crates/api_models/src/ephemeral_key.rs @@ -19,51 +19,69 @@ pub struct EphemeralKeyCreateRequest { } #[cfg(feature = "v2")] -/// Information required to create an ephemeral key. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] -pub struct EphemeralKeyCreateRequest { - /// Customer ID for which an ephemeral key must be created - #[schema( - min_length = 32, - max_length = 64, - value_type = String, - example = "12345_cus_01926c58bc6e77c09e809964e72af8c8" - )] - pub customer_id: id_type::GlobalCustomerId, +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum ResourceId { + #[schema(value_type = String)] + Customer(id_type::GlobalCustomerId), } #[cfg(feature = "v2")] -/// ephemeral_key for the customer_id mentioned +/// Information required to create a client secret. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct ClientSecretCreateRequest { + /// Resource ID for which a client secret must be created + pub resource_id: ResourceId, +} + +#[cfg(feature = "v2")] +/// client_secret for the resource_id mentioned #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq, ToSchema)] -pub struct EphemeralKeyResponse { - /// Ephemeral key id +pub struct ClientSecretResponse { + /// Client Secret id #[schema(value_type = String, max_length = 32, min_length = 1)] - pub id: id_type::EphemeralKeyId, - /// customer_id to which this ephemeral key belongs to - #[schema(value_type = String, max_length = 64, min_length = 32, example = "12345_cus_01926c58bc6e77c09e809964e72af8c8")] - pub customer_id: id_type::GlobalCustomerId, - /// time at which this ephemeral key was created + pub id: id_type::ClientSecretId, + /// resource_id to which this client secret belongs to + #[schema(value_type = ResourceId)] + pub resource_id: ResourceId, + /// time at which this client secret was created pub created_at: time::PrimitiveDateTime, - /// time at which this ephemeral key would expire + /// time at which this client secret would expire pub expires: time::PrimitiveDateTime, #[schema(value_type=String)] - /// ephemeral key + /// client secret pub secret: Secret, } +#[cfg(feature = "v1")] impl common_utils::events::ApiEventMetric for EphemeralKeyCreateRequest { fn get_api_event_type(&self) -> Option { Some(common_utils::events::ApiEventsType::Miscellaneous) } } -#[cfg(feature = "v2")] -impl common_utils::events::ApiEventMetric for EphemeralKeyResponse { +#[cfg(feature = "v1")] +impl common_utils::events::ApiEventMetric for EphemeralKeyCreateResponse { fn get_api_event_type(&self) -> Option { Some(common_utils::events::ApiEventsType::Miscellaneous) } } +#[cfg(feature = "v2")] +impl common_utils::events::ApiEventMetric for ClientSecretCreateRequest { + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::Miscellaneous) + } +} + +#[cfg(feature = "v2")] +impl common_utils::events::ApiEventMetric for ClientSecretResponse { + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::Miscellaneous) + } +} + +#[cfg(feature = "v1")] /// ephemeral_key for the customer_id mentioned #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq, ToSchema)] pub struct EphemeralKeyCreateResponse { diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 27cbbd614d..2124ff1aff 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -179,6 +179,16 @@ impl ApiEventMetric for DisputesMetricsResponse { Some(ApiEventsType::Miscellaneous) } } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl ApiEventMetric for PaymentMethodIntentConfirmInternal { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::PaymentMethod { + payment_method_id: self.id.clone(), + payment_method_type: Some(self.request.payment_method_type), + payment_method_subtype: Some(self.request.payment_method_subtype), + }) + } +} #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] impl ApiEventMetric for PaymentMethodIntentCreate { @@ -192,3 +202,15 @@ impl ApiEventMetric for DisputeListFilters { Some(ApiEventsType::ResourceListAPI) } } + +#[cfg(feature = "v2")] +impl ApiEventMetric for PaymentMethodSessionRequest {} + +#[cfg(feature = "v2")] +impl ApiEventMetric for PaymentMethodsSessionResponse { + 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 c365445e10..4e67292728 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -160,7 +160,7 @@ impl ApiEventMetric for PaymentsRequest { } #[cfg(feature = "v2")] -impl ApiEventMetric for PaymentsResponse { +impl ApiEventMetric for payments::PaymentsResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Payment { payment_id: self.id.clone(), diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index d15fdd7443..ba18cbba50 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -194,6 +194,20 @@ impl PaymentMethodIntentConfirm { } } +/// This struct is used internally only +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct PaymentMethodIntentConfirmInternal { + pub id: id_type::GlobalPaymentMethodId, + pub request: PaymentMethodIntentConfirm, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl From for PaymentMethodIntentConfirm { + fn from(item: PaymentMethodIntentConfirmInternal) -> Self { + item.request + } +} #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] /// This struct is only used by and internal api to migrate payment method pub struct PaymentMethodMigrate { @@ -871,10 +885,6 @@ pub struct PaymentMethodResponse { #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub last_used_at: Option, - /// For Client based calls - #[schema(value_type=Option)] - pub ephemeral_key: Option>, - pub payment_method_data: Option, } @@ -1105,6 +1115,9 @@ impl From for payments::AdditionalCardInfo { pub struct PaymentMethodListResponse { /// The list of payment methods that are enabled for the business profile pub payment_methods_enabled: Vec, + + /// The list of saved payment methods of the customer + pub customer_payment_methods: Vec, } #[cfg(all( @@ -1797,8 +1810,6 @@ pub struct CustomerPaymentMethodsListResponse { pub struct CustomerPaymentMethodsListResponse { /// List of payment methods for customer pub customer_payment_methods: Vec, - /// Returns whether a customer id is not tied to a payment intent (only when the request is made against a client secret) - pub is_guest_customer: Option, } #[cfg(all( @@ -1844,12 +1855,9 @@ pub struct CustomerDefaultPaymentMethodResponse { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, Clone, serde::Serialize, ToSchema)] pub struct CustomerPaymentMethod { - /// Token for payment method in temporary card locker which gets refreshed often - #[schema(example = "7ebf443f-a050-4067-84e5-e6f6d4800aef")] - pub payment_token: Option, - /// The unique identifier of the customer. - #[schema(example = "pm_iouuy468iyuowqs")] - pub payment_method_id: String, + /// The unique identifier of the payment method. + #[schema(value_type = String, example = "12345_pm_01926c58bc6e77c09e809964e72af8c8")] + pub id: id_type::GlobalPaymentMethodId, /// The unique identifier of the customer. #[schema( @@ -1865,8 +1873,8 @@ pub struct CustomerPaymentMethod { pub payment_method_type: api_enums::PaymentMethod, /// This is a sub-category of payment method. - #[schema(value_type = Option,example = "credit_card")] - pub payment_method_subtype: Option, + #[schema(value_type = PaymentMethodType,example = "credit")] + pub payment_method_subtype: api_enums::PaymentMethodType, /// Indicates whether the payment method is eligible for recurring payments #[schema(example = true)] @@ -1884,17 +1892,15 @@ pub struct CustomerPaymentMethod { #[serde(with = "common_utils::custom_serde::iso8601")] pub created: time::PrimitiveDateTime, - /// Surcharge details for this saved card - pub surcharge_details: Option, - /// Whether this payment method requires CVV to be collected #[schema(example = true)] pub requires_cvv: bool, /// 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, + #[schema(value_type = PrimitiveDateTime,example = "2024-02-24T11:04:09.922Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601")] + pub last_used_at: time::PrimitiveDateTime, + /// Indicates if the payment method has been set to default or not #[schema(example = true)] pub is_default: bool, @@ -2299,6 +2305,7 @@ type PaymentMethodMigrationResponseType = ( Result, PaymentMethodRecord, ); + #[cfg(all( any(feature = "v2", feature = "v1"), not(feature = "payment_methods_v2") @@ -2464,29 +2471,72 @@ impl From<(PaymentMethodRecord, id_type::MerchantId)> for customers::CustomerReq } } -// #[cfg(feature = "v2")] -// impl From for customers::CustomerRequest { -// fn from(record: PaymentMethodRecord) -> Self { -// Self { -// merchant_reference_id: Some(record.customer_id), -// name: record.name.unwrap(), -// email: record.email.unwrap(), -// phone: record.phone, -// description: None, -// phone_country_code: record.phone_country_code, -// default_billing_address: Some(payments::AddressDetails { -// city: Some(record.billing_address_city), -// country: record.billing_address_country, -// line1: Some(record.billing_address_line1), -// line2: record.billing_address_line2, -// state: Some(record.billing_address_state), -// line3: record.billing_address_line3, -// zip: Some(record.billing_address_zip), -// first_name: Some(record.billing_address_first_name), -// last_name: Some(record.billing_address_last_name), -// }), -// default_shipping_address: None, -// metadata: None, -// } -// } -// } +#[cfg(feature = "v2")] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct PaymentMethodSessionRequest { + /// The customer id for which the payment methods session is to be created + #[schema(value_type = String, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] + pub customer_id: id_type::GlobalCustomerId, + + /// 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, + + /// The time (seconds ) when the session will expire + /// If not provided, the session will expire in 15 minutes + #[schema(example = 900, default = 900)] + pub expires_in: Option, +} + +#[cfg(feature = "v2")] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct PaymentMethodSessionUpdateSavedPaymentMethod { + /// The payment method id of the payment method to be updated + #[schema(value_type = String, example = "12345_pm_01926c58bc6e77c09e809964e72af8c8")] + pub payment_method_id: id_type::GlobalPaymentMethodId, + + /// The update request for the payment method update + #[serde(flatten)] + pub payment_method_update_request: PaymentMethodUpdate, +} + +#[cfg(feature = "v2")] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct PaymentMethodsSessionResponse { + #[schema(value_type = String, example = "12345_pms_01926c58bc6e77c09e809964e72af8c8")] + pub id: id_type::GlobalPaymentMethodSessionId, + + /// The customer id for which the payment methods session is to be created + #[schema(value_type = String, example = "12345_cus_01926c58bc6e77c09e809964e72af8c8")] + pub customer_id: id_type::GlobalCustomerId, + + /// 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, + + /// The iso timestamp when the session will expire + /// Trying to retrieve the session or any operations on the session after this time will result in an error + #[schema(value_type = PrimitiveDateTime, example = "2023-01-18T11:04:09.922Z")] + #[serde(with = "common_utils::custom_serde::iso8601")] + pub expires_at: time::PrimitiveDateTime, + + /// Client Secret + #[schema(value_type = String)] + pub client_secret: masking::Secret, +} diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 295b1979f1..820992025c 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -27,12 +27,13 @@ use time::{Date, PrimitiveDateTime}; use url::Url; use utoipa::ToSchema; +#[cfg(feature = "v1")] +use crate::ephemeral_key::EphemeralKeyCreateResponse; #[cfg(feature = "v2")] use crate::payment_methods; use crate::{ admin::{self, MerchantConnectorInfo}, disputes, enums as api_enums, - ephemeral_key::EphemeralKeyCreateResponse, mandates::RecurringDetails, refunds, }; diff --git a/crates/api_models/src/webhooks.rs b/crates/api_models/src/webhooks.rs index f99e3a450b..ddefea542c 100644 --- a/crates/api_models/src/webhooks.rs +++ b/crates/api_models/src/webhooks.rs @@ -285,7 +285,7 @@ pub enum OutgoingWebhookContent { #[serde(tag = "type", content = "object", rename_all = "snake_case")] #[cfg(feature = "v2")] pub enum OutgoingWebhookContent { - #[schema(value_type = PaymentsResponse, title = "PaymentsResponse")] + #[schema(value_type = PaymentsRetrieveResponse, title = "PaymentsResponse")] PaymentDetails(Box), #[schema(value_type = RefundResponse, title = "RefundResponse")] RefundDetails(Box), diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index c4ee48ed5a..2a5219f4de 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -3687,6 +3687,39 @@ pub enum FeatureStatus { Supported, } +/// The type of tokenization to use for the payment method +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + ToSchema, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum TokenizationType { + /// Create a single use token for the given payment method + /// The user might have to go through additional factor authentication when using the single use token if required by the payment method + SingleUse, + /// Create a multi use token for the given payment method + /// User will have to complete the additional factor authentication only once when creating the multi use token + /// This will create a mandate at the connector which can be used for recurring payments + MultiUse, +} + +/// The network tokenization toggle, whether to enable or skip the network tokenization +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] +pub enum NetworkTokenizationToggle { + /// Enable network tokenization for the payment method + Enable, + /// Skip network tokenization for the payment method + Skip, +} + #[derive(Clone, Copy, Debug, Deserialize, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum GooglePayAuthMethod { diff --git a/crates/common_types/src/payment_methods.rs b/crates/common_types/src/payment_methods.rs index 702d8c0e70..9e80a029a2 100644 --- a/crates/common_types/src/payment_methods.rs +++ b/crates/common_types/src/payment_methods.rs @@ -124,3 +124,23 @@ where } common_utils::impl_to_sql_from_sql_json!(PaymentMethodsEnabled); + +/// The network tokenization configuration for creating the payment method session +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct NetworkTokenization { + /// Enable the network tokenization for payment methods that are created using the payment method session + #[schema(value_type = NetworkTokenizationToggle)] + pub enable: common_enums::NetworkTokenizationToggle, +} + +/// The Payment Service Provider Configuration for payment methods that are created using the payment method session +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct PspTokenization { + /// The tokenization type to be applied for the payment method + #[schema(value_type = TokenizationType)] + 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, +} diff --git a/crates/common_utils/src/crypto.rs b/crates/common_utils/src/crypto.rs index e8c2b2d5a0..52fc0daacf 100644 --- a/crates/common_utils/src/crypto.rs +++ b/crates/common_utils/src/crypto.rs @@ -484,12 +484,21 @@ impl Encryptable { F: FnOnce(T) -> CustomResult, U: Clone, { - // Option::map(self, f) let inner = self.inner; let encrypted = self.encrypted; let inner = f(inner)?; Ok(Encryptable { inner, encrypted }) } + + /// consume self and modify the inner value + pub fn map(self, f: impl FnOnce(T) -> U) -> Encryptable { + let encrypted_data = self.encrypted; + let masked_data = f(self.inner); + Encryptable { + inner: masked_data, + encrypted: encrypted_data, + } + } } impl Deref for Encryptable> { diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index bae525f456..2e90d646e9 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -112,8 +112,13 @@ pub enum ApiEventsType { poll_id: String, }, Analytics, - EphemeralKey { - key_id: id_type::EphemeralKeyId, + #[cfg(feature = "v2")] + ClientSecret { + key_id: id_type::ClientSecretId, + }, + #[cfg(feature = "v2")] + PaymentMethodSession { + payment_method_session_id: id_type::GlobalPaymentMethodSessionId, }, } diff --git a/crates/common_utils/src/id_type.rs b/crates/common_utils/src/id_type.rs index ea90c00121..30b6a3888d 100644 --- a/crates/common_utils/src/id_type.rs +++ b/crates/common_utils/src/id_type.rs @@ -2,8 +2,8 @@ //! The id type can be used to create specific id types with custom behaviour mod api_key; +mod client_secret; mod customer; -mod ephemeral_key; #[cfg(feature = "v2")] mod global_id; mod merchant; @@ -32,14 +32,14 @@ use thiserror::Error; pub use self::global_id::{ customer::GlobalCustomerId, payment::{GlobalAttemptId, GlobalPaymentId}, - payment_methods::GlobalPaymentMethodId, + payment_methods::{GlobalPaymentMethodId, GlobalPaymentMethodSessionId}, refunds::GlobalRefundId, CellId, }; pub use self::{ api_key::ApiKeyId, + client_secret::ClientSecretId, customer::CustomerId, - ephemeral_key::EphemeralKeyId, merchant::MerchantId, merchant_connector_account::MerchantConnectorAccountId, organization::OrganizationId, diff --git a/crates/common_utils/src/id_type/client_secret.rs b/crates/common_utils/src/id_type/client_secret.rs new file mode 100644 index 0000000000..b9d7f2e09e --- /dev/null +++ b/crates/common_utils/src/id_type/client_secret.rs @@ -0,0 +1,32 @@ +crate::id_type!( + ClientSecretId, + "A type for key_id that can be used for Ephemeral key IDs" +); +crate::impl_id_type_methods!(ClientSecretId, "key_id"); + +// This is to display the `ClientSecretId` as ClientSecretId(abcd) +crate::impl_debug_id_type!(ClientSecretId); +crate::impl_try_from_cow_str_id_type!(ClientSecretId, "key_id"); + +crate::impl_generate_id_id_type!(ClientSecretId, "csi"); +crate::impl_serializable_secret_id_type!(ClientSecretId); +crate::impl_queryable_id_type!(ClientSecretId); +crate::impl_to_sql_from_sql_id_type!(ClientSecretId); + +#[cfg(feature = "v2")] +impl crate::events::ApiEventMetric for ClientSecretId { + fn get_api_event_type(&self) -> Option { + Some(crate::events::ApiEventsType::ClientSecret { + key_id: self.clone(), + }) + } +} + +crate::impl_default_id_type!(ClientSecretId, "key"); + +impl ClientSecretId { + /// Generate a key for redis + pub fn generate_redis_key(&self) -> String { + format!("cs_{}", self.get_string_repr()) + } +} diff --git a/crates/common_utils/src/id_type/ephemeral_key.rs b/crates/common_utils/src/id_type/ephemeral_key.rs deleted file mode 100644 index 071980fc6a..0000000000 --- a/crates/common_utils/src/id_type/ephemeral_key.rs +++ /dev/null @@ -1,31 +0,0 @@ -crate::id_type!( - EphemeralKeyId, - "A type for key_id that can be used for Ephemeral key IDs" -); -crate::impl_id_type_methods!(EphemeralKeyId, "key_id"); - -// This is to display the `EphemeralKeyId` as EphemeralKeyId(abcd) -crate::impl_debug_id_type!(EphemeralKeyId); -crate::impl_try_from_cow_str_id_type!(EphemeralKeyId, "key_id"); - -crate::impl_generate_id_id_type!(EphemeralKeyId, "eki"); -crate::impl_serializable_secret_id_type!(EphemeralKeyId); -crate::impl_queryable_id_type!(EphemeralKeyId); -crate::impl_to_sql_from_sql_id_type!(EphemeralKeyId); - -impl crate::events::ApiEventMetric for EphemeralKeyId { - fn get_api_event_type(&self) -> Option { - Some(crate::events::ApiEventsType::EphemeralKey { - key_id: self.clone(), - }) - } -} - -crate::impl_default_id_type!(EphemeralKeyId, "key"); - -impl EphemeralKeyId { - /// Generate a key for redis - pub fn generate_redis_key(&self) -> String { - format!("epkey_{}", self.get_string_repr()) - } -} diff --git a/crates/common_utils/src/id_type/global_id.rs b/crates/common_utils/src/id_type/global_id.rs index 1ad1bd9608..1e376dfe4d 100644 --- a/crates/common_utils/src/id_type/global_id.rs +++ b/crates/common_utils/src/id_type/global_id.rs @@ -5,7 +5,6 @@ pub(super) mod refunds; use diesel::{backend::Backend, deserialize::FromSql, serialize::ToSql, sql_types}; use error_stack::ResultExt; -use serde_json::error; use thiserror::Error; use crate::{ @@ -27,6 +26,7 @@ pub(crate) enum GlobalEntity { Attempt, PaymentMethod, Refund, + PaymentMethodSession, } impl GlobalEntity { @@ -37,6 +37,7 @@ impl GlobalEntity { Self::PaymentMethod => "pm", Self::Attempt => "att", Self::Refund => "ref", + Self::PaymentMethodSession => "pms", } } } @@ -204,8 +205,8 @@ mod global_id_tests { let cell_id = CellId::from_str(cell_id_string).unwrap(); let global_id = GlobalId::generate(&cell_id, entity); - /// Generate a regex for globalid - /// Eg - 12abc_cus_abcdefghijklmnopqrstuvwxyz1234567890 + // Generate a regex for globalid + // Eg - 12abc_cus_abcdefghijklmnopqrstuvwxyz1234567890 let regex = regex::Regex::new(r"[a-z0-9]{5}_cus_[a-z0-9]{32}").unwrap(); assert!(regex.is_match(&global_id.0 .0 .0)); diff --git a/crates/common_utils/src/id_type/global_id/payment_methods.rs b/crates/common_utils/src/id_type/global_id/payment_methods.rs index a4fec5e836..b3b314e7fc 100644 --- a/crates/common_utils/src/id_type/global_id/payment_methods.rs +++ b/crates/common_utils/src/id_type/global_id/payment_methods.rs @@ -19,12 +19,61 @@ use crate::{ #[diesel(sql_type = diesel::sql_types::Text)] pub struct GlobalPaymentMethodId(GlobalId); +/// A global id that can be used to identify a payment method session +#[derive( + Debug, + Clone, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + diesel::expression::AsExpression, +)] +#[diesel(sql_type = diesel::sql_types::Text)] +pub struct GlobalPaymentMethodSessionId(GlobalId); + #[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)] pub enum GlobalPaymentMethodIdError { #[error("Failed to construct GlobalPaymentMethodId")] ConstructionError, } +#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)] +pub enum GlobalPaymentMethodSessionIdError { + #[error("Failed to construct GlobalPaymentMethodSessionId")] + ConstructionError, +} + +impl GlobalPaymentMethodSessionId { + /// Create a new GlobalPaymentMethodSessionId from cell id information + pub fn generate( + cell_id: &CellId, + ) -> error_stack::Result { + let global_id = GlobalId::generate(cell_id, GlobalEntity::PaymentMethodSession); + Ok(Self(global_id)) + } + + /// Get the string representation of the id + pub fn get_string_repr(&self) -> &str { + self.0.get_string_repr() + } + + /// Construct a redis key from the id to be stored in redis + pub fn get_redis_key(&self) -> String { + format!("payment_method_session:{}", self.get_string_repr()) + } +} + +#[cfg(feature = "v2")] +impl crate::events::ApiEventMetric for GlobalPaymentMethodSessionId { + fn get_api_event_type(&self) -> Option { + Some(crate::events::ApiEventsType::PaymentMethodSession { + payment_method_session_id: self.clone(), + }) + } +} + impl crate::events::ApiEventMetric for GlobalPaymentMethodId { fn get_api_event_type(&self) -> Option { Some( @@ -89,3 +138,38 @@ where Ok(Self(global_id)) } } + +impl diesel::Queryable for GlobalPaymentMethodSessionId +where + DB: diesel::backend::Backend, + Self: diesel::deserialize::FromSql, +{ + type Row = Self; + fn build(row: Self::Row) -> diesel::deserialize::Result { + Ok(row) + } +} + +impl diesel::serialize::ToSql for GlobalPaymentMethodSessionId +where + DB: diesel::backend::Backend, + GlobalId: diesel::serialize::ToSql, +{ + fn to_sql<'b>( + &'b self, + out: &mut diesel::serialize::Output<'b, '_, DB>, + ) -> diesel::serialize::Result { + self.0.to_sql(out) + } +} + +impl diesel::deserialize::FromSql for GlobalPaymentMethodSessionId +where + DB: diesel::backend::Backend, + GlobalId: diesel::deserialize::FromSql, +{ + fn from_sql(value: DB::RawValue<'_>) -> diesel::deserialize::Result { + let global_id = GlobalId::from_sql(value)?; + Ok(Self(global_id)) + } +} diff --git a/crates/common_utils/src/types/authentication.rs b/crates/common_utils/src/types/authentication.rs index 2df81b2d3d..331f1e2a65 100644 --- a/crates/common_utils/src/types/authentication.rs +++ b/crates/common_utils/src/types/authentication.rs @@ -35,3 +35,28 @@ pub enum AuthInfo { profile_ids: Vec, }, } + +/// Enum for different resource types supported in client secret +#[cfg(feature = "v2")] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ResourceId { + /// Global Payment ID (Not exposed in api_models version of enum) + Payment(id_type::GlobalPaymentId), + /// Global Customer ID + Customer(id_type::GlobalCustomerId), + /// Global Payment Methods Session ID + PaymentMethodSession(id_type::GlobalPaymentMethodSessionId), +} + +#[cfg(feature = "v2")] +impl ResourceId { + /// Get string representation of enclosed ID type + pub fn to_str(&self) -> &str { + match self { + Self::Payment(id) => id.get_string_repr(), + Self::Customer(id) => id.get_string_repr(), + Self::PaymentMethodSession(id) => id.get_string_repr(), + } + } +} diff --git a/crates/diesel_models/src/customers.rs b/crates/diesel_models/src/customers.rs index 7bd7d8368a..dc39059c13 100644 --- a/crates/diesel_models/src/customers.rs +++ b/crates/diesel_models/src/customers.rs @@ -84,7 +84,7 @@ pub struct CustomerNew { pub metadata: Option, pub connector_customer: Option, pub modified_at: PrimitiveDateTime, - pub default_payment_method_id: Option, + pub default_payment_method_id: Option, pub updated_by: Option, pub version: ApiVersion, pub merchant_reference_id: Option, @@ -166,7 +166,7 @@ pub struct Customer { pub metadata: Option, pub connector_customer: Option, pub modified_at: PrimitiveDateTime, - pub default_payment_method_id: Option, + pub default_payment_method_id: Option, pub updated_by: Option, pub version: ApiVersion, pub merchant_reference_id: Option, @@ -243,7 +243,7 @@ pub struct CustomerUpdateInternal { pub metadata: Option, pub modified_at: PrimitiveDateTime, pub connector_customer: Option, - pub default_payment_method_id: Option>, + pub default_payment_method_id: Option>, pub updated_by: Option, pub default_billing_address: Option, pub default_shipping_address: Option, diff --git a/crates/diesel_models/src/ephemeral_key.rs b/crates/diesel_models/src/ephemeral_key.rs index c7fc103ed0..ede9f61b03 100644 --- a/crates/diesel_models/src/ephemeral_key.rs +++ b/crates/diesel_models/src/ephemeral_key.rs @@ -1,30 +1,29 @@ #[cfg(feature = "v2")] use masking::{PeekInterface, Secret}; + #[cfg(feature = "v2")] -pub struct EphemeralKeyTypeNew { - pub id: common_utils::id_type::EphemeralKeyId, +pub struct ClientSecretTypeNew { + pub id: common_utils::id_type::ClientSecretId, pub merchant_id: common_utils::id_type::MerchantId, - pub customer_id: common_utils::id_type::GlobalCustomerId, pub secret: Secret, - pub resource_type: ResourceType, + pub resource_id: common_utils::types::authentication::ResourceId, } #[cfg(feature = "v2")] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct EphemeralKeyType { - pub id: common_utils::id_type::EphemeralKeyId, +pub struct ClientSecretType { + pub id: common_utils::id_type::ClientSecretId, pub merchant_id: common_utils::id_type::MerchantId, - pub customer_id: common_utils::id_type::GlobalCustomerId, - pub resource_type: ResourceType, + pub resource_id: common_utils::types::authentication::ResourceId, pub created_at: time::PrimitiveDateTime, pub expires: time::PrimitiveDateTime, pub secret: Secret, } #[cfg(feature = "v2")] -impl EphemeralKeyType { +impl ClientSecretType { pub fn generate_secret_key(&self) -> String { - format!("epkey_{}", self.secret.peek()) + format!("cs_{}", self.secret.peek()) } } @@ -50,21 +49,3 @@ impl common_utils::events::ApiEventMetric for EphemeralKey { Some(common_utils::events::ApiEventsType::Miscellaneous) } } - -#[derive( - Clone, - Copy, - Debug, - serde::Serialize, - serde::Deserialize, - strum::Display, - strum::EnumString, - PartialEq, - Eq, -)] -#[serde(rename_all = "snake_case")] -#[strum(serialize_all = "snake_case")] -pub enum ResourceType { - Payment, - PaymentMethod, -} diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 1908edb8ad..d9163b02aa 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -47,6 +47,9 @@ pub mod routing_algorithm; pub mod types; pub mod unified_translations; +#[cfg(feature = "v2")] +pub mod payment_methods_session; + #[allow(unused_qualifications)] pub mod schema; #[allow(unused_qualifications)] diff --git a/crates/diesel_models/src/payment_methods_session.rs b/crates/diesel_models/src/payment_methods_session.rs new file mode 100644 index 0000000000..dad3e995e0 --- /dev/null +++ b/crates/diesel_models/src/payment_methods_session.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "v2")] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct PaymentMethodsSession { + 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, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub expires_at: time::PrimitiveDateTime, +} diff --git a/crates/hyperswitch_connectors/src/connectors/deutschebank.rs b/crates/hyperswitch_connectors/src/connectors/deutschebank.rs index 0b92cb2538..b481c3e808 100644 --- a/crates/hyperswitch_connectors/src/connectors/deutschebank.rs +++ b/crates/hyperswitch_connectors/src/connectors/deutschebank.rs @@ -1058,6 +1058,25 @@ lazy_static! { } ); + deutschebank_supported_payment_methods.add( + enums::PaymentMethod::Card, + enums::PaymentMethodType::Debit, + PaymentMethodDetails{ + mandates: enums::FeatureStatus::NotSupported, + refunds: enums::FeatureStatus::Supported, + supported_capture_methods: supported_capture_methods.clone(), + specific_features: Some( + api_models::feature_matrix::PaymentMethodSpecificFeatures::Card({ + api_models::feature_matrix::CardSpecificFeatures { + three_ds: common_enums::FeatureStatus::Supported, + no_three_ds: common_enums::FeatureStatus::NotSupported, + supported_card_networks: supported_card_network.clone(), + } + }), + ), + } + ); + deutschebank_supported_payment_methods }; diff --git a/crates/hyperswitch_domain_models/src/customer.rs b/crates/hyperswitch_domain_models/src/customer.rs index b503326f84..58103c08fa 100644 --- a/crates/hyperswitch_domain_models/src/customer.rs +++ b/crates/hyperswitch_domain_models/src/customer.rs @@ -58,7 +58,7 @@ pub struct Customer { pub metadata: Option, pub connector_customer: Option, pub modified_at: PrimitiveDateTime, - pub default_payment_method_id: Option, + pub default_payment_method_id: Option, pub updated_by: Option, pub merchant_reference_id: Option, pub default_billing_address: Option, @@ -313,14 +313,14 @@ pub enum CustomerUpdate { connector_customer: Box>, default_billing_address: Option, default_shipping_address: Option, - default_payment_method_id: Option>, + default_payment_method_id: Option>, status: Option, }, ConnectorCustomer { connector_customer: Option, }, UpdateDefaultPaymentMethod { - default_payment_method_id: Option>, + default_payment_method_id: Option>, }, } diff --git a/crates/hyperswitch_domain_models/src/payment_methods.rs b/crates/hyperswitch_domain_models/src/payment_methods.rs index 71ad9ee927..c8683919f7 100644 --- a/crates/hyperswitch_domain_models/src/payment_methods.rs +++ b/crates/hyperswitch_domain_models/src/payment_methods.rs @@ -1,5 +1,7 @@ +#[cfg(feature = "v2")] +use api_models::payment_methods::PaymentMethodsData; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -use common_utils::crypto::Encryptable; +use common_utils::{crypto::Encryptable, encryption::Encryption, types::keymanager::ToEncryptable}; use common_utils::{ crypto::OptionalEncryptableValue, errors::{CustomResult, ParsingError, ValidationError}, @@ -9,10 +11,15 @@ use common_utils::{ use diesel_models::enums as storage_enums; use error_stack::ResultExt; use masking::{PeekInterface, Secret}; +// specific imports because of using the macro +#[cfg(feature = "v2")] +use rustc_hash::FxHashMap; +#[cfg(feature = "v2")] +use serde_json::Value; use time::PrimitiveDateTime; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -use crate::type_encryption::OptionalEncryptableJsonType; +use crate::{address::Address, type_encryption::OptionalEncryptableJsonType}; use crate::{ mandates::{CommonMandateReference, PaymentsMandateReference}, type_encryption::{crypto_operation, AsyncLift, CryptoOperation}, @@ -75,16 +82,22 @@ pub struct PaymentMethod { } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, router_derive::ToEncryption)] pub struct PaymentMethod { + /// The identifier for the payment method. Using this recurring payments can be made + pub id: common_utils::id_type::GlobalPaymentMethodId, + + /// The customer id against which the payment method is saved pub customer_id: common_utils::id_type::GlobalCustomerId, + + /// The merchant id against which the payment method is saved pub merchant_id: common_utils::id_type::MerchantId, pub created_at: PrimitiveDateTime, pub last_modified: PrimitiveDateTime, pub payment_method_type: Option, pub payment_method_subtype: Option, - pub payment_method_data: - OptionalEncryptableJsonType, + #[encrypt(ty = Value)] + pub payment_method_data: Option>, pub locker_id: Option, pub last_used_at: PrimitiveDateTime, pub connector_mandate_details: Option, @@ -92,14 +105,15 @@ pub struct PaymentMethod { pub status: storage_enums::PaymentMethodStatus, pub network_transaction_id: Option, pub client_secret: Option, - pub payment_method_billing_address: OptionalEncryptableValue, + #[encrypt(ty = Value)] + pub payment_method_billing_address: Option>, pub updated_by: Option, pub locker_fingerprint_id: Option, - pub id: common_utils::id_type::GlobalPaymentMethodId, pub version: common_enums::ApiVersion, pub network_token_requestor_reference_id: Option, pub network_token_locker_id: Option, - pub network_token_payment_method_data: OptionalEncryptableValue, + #[encrypt(ty = Value)] + pub network_token_payment_method_data: Option>, } impl PaymentMethod { @@ -431,76 +445,88 @@ impl super::behaviour::Conversion for PaymentMethod { async fn convert_back( state: &keymanager::KeyManagerState, - item: Self::DstType, + storage_model: Self::DstType, key: &Secret>, key_manager_identifier: keymanager::Identifier, ) -> CustomResult where Self: Sized, { + use common_utils::ext_traits::ValueExt; + use masking::ExposeInterface; + async { + let decrypted_data = crypto_operation( + state, + type_name!(Self::DstType), + CryptoOperation::BatchDecrypt(EncryptedPaymentMethod::to_encryptable( + EncryptedPaymentMethod { + payment_method_data: storage_model.payment_method_data, + payment_method_billing_address: storage_model + .payment_method_billing_address, + network_token_payment_method_data: storage_model + .network_token_payment_method_data, + }, + )), + key_manager_identifier, + key.peek(), + ) + .await + .and_then(|val| val.try_into_batchoperation())?; + + let data = EncryptedPaymentMethod::from_encryptable(decrypted_data) + .change_context(common_utils::errors::CryptoError::DecodingFailed) + .attach_printable("Invalid batch operation data")?; + + let payment_method_billing_address = data + .payment_method_billing_address + .map(|billing| { + billing.deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(common_utils::errors::CryptoError::DecodingFailed) + .attach_printable("Error while deserializing Address")?; + + let payment_method_data = data + .payment_method_data + .map(|payment_method_data| { + payment_method_data + .deserialize_inner_value(|value| value.parse_value("Payment Method Data")) + }) + .transpose() + .change_context(common_utils::errors::CryptoError::DecodingFailed) + .attach_printable("Error while deserializing Payment Method Data")?; + + let network_token_payment_method_data = + data.network_token_payment_method_data + .map(|network_token_payment_method_data| { + network_token_payment_method_data.map(|value| value.expose()) + }); + Ok::>(Self { - customer_id: item.customer_id, - merchant_id: item.merchant_id, - id: item.id, - created_at: item.created_at, - last_modified: item.last_modified, - payment_method_type: item.payment_method_type_v2, - payment_method_subtype: item.payment_method_subtype, - payment_method_data: item - .payment_method_data - .async_lift(|inner| async { - crypto_operation( - state, - type_name!(Self::DstType), - CryptoOperation::DecryptOptional(inner), - key_manager_identifier.clone(), - key.peek(), - ) - .await - .and_then(|val| val.try_into_optionaloperation()) - }) - .await?, - locker_id: item.locker_id.map(VaultId::generate), - last_used_at: item.last_used_at, - connector_mandate_details: item.connector_mandate_details.map(|cmd| cmd.into()), - customer_acceptance: item.customer_acceptance, - status: item.status, - network_transaction_id: item.network_transaction_id, - client_secret: item.client_secret, - payment_method_billing_address: item - .payment_method_billing_address - .async_lift(|inner| async { - crypto_operation( - state, - type_name!(Self::DstType), - CryptoOperation::DecryptOptional(inner), - key_manager_identifier.clone(), - key.peek(), - ) - .await - .and_then(|val| val.try_into_optionaloperation()) - }) - .await?, - updated_by: item.updated_by, - locker_fingerprint_id: item.locker_fingerprint_id, - version: item.version, - network_token_requestor_reference_id: item.network_token_requestor_reference_id, - network_token_locker_id: item.network_token_locker_id, - network_token_payment_method_data: item - .network_token_payment_method_data - .async_lift(|inner| async { - crypto_operation( - state, - type_name!(Self::DstType), - CryptoOperation::DecryptOptional(inner), - key_manager_identifier.clone(), - key.peek(), - ) - .await - .and_then(|val| val.try_into_optionaloperation()) - }) - .await?, + customer_id: storage_model.customer_id, + merchant_id: storage_model.merchant_id, + id: storage_model.id, + created_at: storage_model.created_at, + last_modified: storage_model.last_modified, + payment_method_type: storage_model.payment_method_type_v2, + payment_method_subtype: storage_model.payment_method_subtype, + payment_method_data, + locker_id: storage_model.locker_id.map(VaultId::generate), + last_used_at: storage_model.last_used_at, + connector_mandate_details: storage_model.connector_mandate_details.map(From::from), + customer_acceptance: storage_model.customer_acceptance, + status: storage_model.status, + network_transaction_id: storage_model.network_transaction_id, + client_secret: storage_model.client_secret, + payment_method_billing_address, + updated_by: storage_model.updated_by, + locker_fingerprint_id: storage_model.locker_fingerprint_id, + version: storage_model.version, + network_token_requestor_reference_id: storage_model + .network_token_requestor_reference_id, + network_token_locker_id: storage_model.network_token_locker_id, + network_token_payment_method_data, }) } .await @@ -541,6 +567,100 @@ impl super::behaviour::Conversion for PaymentMethod { } } +#[cfg(feature = "v2")] +#[derive(Clone, Debug, router_derive::ToEncryption)] +pub struct PaymentMethodsSession { + pub id: common_utils::id_type::GlobalPaymentMethodSessionId, + pub customer_id: common_utils::id_type::GlobalCustomerId, + #[encrypt(ty = Value)] + pub billing: Option>, + pub psp_tokenization: Option, + pub network_tokenization: Option, + pub expires_at: PrimitiveDateTime, +} + +#[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; + 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, + expires_at: self.expires_at, + }) + } + + async fn convert_back( + state: &keymanager::KeyManagerState, + storage_model: Self::DstType, + key: &Secret>, + key_manager_identifier: keymanager::Identifier, + ) -> CustomResult + where + Self: Sized, + { + use common_utils::ext_traits::ValueExt; + + async { + let decrypted_data = crypto_operation( + state, + type_name!(Self::DstType), + CryptoOperation::BatchDecrypt(EncryptedPaymentMethodsSession::to_encryptable( + EncryptedPaymentMethodsSession { + billing: storage_model.billing, + }, + )), + key_manager_identifier, + key.peek(), + ) + .await + .and_then(|val| val.try_into_batchoperation())?; + + let data = EncryptedPaymentMethodsSession::from_encryptable(decrypted_data) + .change_context(common_utils::errors::CryptoError::DecodingFailed) + .attach_printable("Invalid batch operation data")?; + + let billing = data + .billing + .map(|billing| { + billing.deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(common_utils::errors::CryptoError::DecodingFailed) + .attach_printable("Error while deserializing Address")?; + + Ok::>(Self { + id: storage_model.id, + customer_id: storage_model.customer_id, + billing, + psp_tokenization: storage_model.psp_tokenization, + network_tokenization: storage_model.network_tokeinzation, + expires_at: storage_model.expires_at, + }) + } + .await + .change_context(ValidationError::InvalidValue { + message: "Failed while decrypting payment method data".to_string(), + }) + } + + async fn construct_new(self) -> CustomResult { + Ok(Self::NewDstType { + 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, + expires_at: self.expires_at, + }) + } +} + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index a4af7b1e13..cc6f1b0e41 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -131,12 +131,16 @@ Never share your secret api keys. Keep them guarded and secure. //Routes for payment methods routes::payment_method::create_payment_method_api, routes::payment_method::create_payment_method_intent_api, - routes::payment_method::list_payment_methods, routes::payment_method::confirm_payment_method_intent_api, routes::payment_method::payment_method_update_api, routes::payment_method::payment_method_retrieve_api, routes::payment_method::payment_method_delete_api, - // routes::payment_method::list_customer_payment_method_api, + + //Routes for payment method session + routes::payment_method::payment_method_session_create, + 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 for refunds routes::refunds::refunds_create, @@ -164,6 +168,8 @@ Never share your secret api keys. Keep them guarded and secure. common_types::refunds::StripeSplitRefundRequest, common_utils::types::ChargeRefunds, common_types::payment_methods::PaymentMethodsEnabled, + common_types::payment_methods::PspTokenization, + common_types::payment_methods::NetworkTokenization, common_types::refunds::SplitRefund, common_types::payments::ConnectorChargeResponseData, common_types::payments::StripeChargeResponseData, @@ -193,6 +199,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::customers::CustomerRequest, api_models::customers::CustomerUpdateRequest, api_models::customers::CustomerDeleteResponse, + api_models::ephemeral_key::ResourceId, api_models::payment_methods::PaymentMethodCreate, api_models::payment_methods::PaymentMethodIntentCreate, api_models::payment_methods::PaymentMethodIntentConfirm, @@ -493,6 +500,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::PaymentMethodCollectLinkRequest, 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::refunds::RefundListRequest, api_models::refunds::RefundListResponse, @@ -502,7 +511,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::mandates::MandateCardDetails, api_models::mandates::RecurringDetails, api_models::mandates::ProcessorPaymentToken, - api_models::ephemeral_key::EphemeralKeyCreateResponse, + api_models::ephemeral_key::ClientSecretResponse, api_models::payments::CustomerDetails, api_models::payments::GiftCardData, api_models::payments::GiftCardDetails, @@ -655,7 +664,10 @@ Never share your secret api keys. Keep them guarded and secure. api_models::feature_matrix::PaymentMethodSpecificFeatures, api_models::feature_matrix::CardSpecificFeatures, api_models::feature_matrix::SupportedPaymentMethod, + api_models::payment_methods::PaymentMethodSessionUpdateSavedPaymentMethod, common_utils::types::BrowserInformation, + api_models::enums::TokenizationType, + api_models::enums::NetworkTokenizationToggle, api_models::payments::PaymentAmountDetailsResponse, routes::payments::ForceSync, )), diff --git a/crates/openapi/src/routes/payment_method.rs b/crates/openapi/src/routes/payment_method.rs index 4c0c201e29..3c1f66e6e7 100644 --- a/crates/openapi/src/routes/payment_method.rs +++ b/crates/openapi/src/routes/payment_method.rs @@ -322,27 +322,104 @@ pub async fn payment_method_update_api() {} #[cfg(feature = "v2")] pub async fn payment_method_delete_api() {} -/// Payment Methods - Payment Methods List +/// Payment Method Session - Create /// -/// List the payment methods eligible for a payment method. +/// Create a payment method session for a customer +/// This is used to list the saved payment methods for the customer +/// The customer can also add a new payment method using this session +#[cfg(feature = "v2")] +#[utoipa::path( + post, + path = "/v2/payment-method-session", + request_body( + content = PaymentMethodSessionRequest, + examples (( "Create a payment method session with customer_id" = ( + value =json!( { + "customer_id": "12345_cus_abcdefghijklmnopqrstuvwxyz" + }) + ))) + ), + responses( + (status = 200, description = "Create the payment method session", body = PaymentMethodsSessionResponse), + (status = 400, description = "The request is invalid") + ), + tag = "Payment Method Session", + operation_id = "Create a payment method session", + security(("api_key" = [])) +)] +pub fn payment_method_session_create() {} + +/// Payment Method Session - Retrieve +/// +/// Retrieve the payment method session #[cfg(feature = "v2")] #[utoipa::path( get, - path = "/v2/payment-methods/{id}/list-enabled-payment-methods", - params( - ("id" = String, Path, description = "The global payment method id"), - ( - "X-Profile-Id" = String, Header, - description = "Profile ID associated to the payment method intent", - example = "pro_abcdefghijklmnop" - ), + path = "/v2/payment-method-session/:id", + params ( + ("id" = String, Path, description = "The unique identifier for the Payment Method Session"), ), responses( - (status = 200, description = "Get the payment methods", body = PaymentMethodListResponseForPayments), - (status = 404, description = "No payment method found with the given id") + (status = 200, description = "The payment method session is retrieved successfully", body = PaymentMethodsSessionResponse), + (status = 404, description = "The request is invalid") ), - tag = "Payment Methods", - operation_id = "List Payment methods for a Payment Method Intent", - security(("api_key" = [], "ephemeral_key" = [])) + tag = "Payment Method Session", + operation_id = "Retrieve the payment method session", + security(("ephemeral_key" = [])) )] -pub fn list_payment_methods() {} +pub fn payment_method_session_retrieve() {} + +/// Payment Method Session - List Payment Methods +/// +/// List payment methods for the given payment method session. +/// This endpoint lists the enabled payment methods for the profile and the saved payment methods of the customer. +#[cfg(feature = "v2")] +#[utoipa::path( + get, + path = "/v2/payment-method-session/:id/list-payment-methods", + params ( + ("id" = String, Path, description = "The unique identifier for the Payment Method Session"), + ), + responses( + (status = 200, description = "The payment method session is retrieved successfully", body = PaymentMethodListResponse), + (status = 404, description = "The request is invalid") + ), + tag = "Payment Method Session", + operation_id = "List Payment methods for a Payment Method Session", + security(("ephemeral_key" = [])) +)] +pub fn payment_method_session_list_payment_methods() {} + +/// Payment Method Session - Update a saved payment method +/// +/// Update a saved payment method from the given payment method session. +#[cfg(feature = "v2")] +#[utoipa::path( + put, + path = "/v2/payment-method-session/:id/update-saved-payment-method", + params ( + ("id" = String, Path, description = "The unique identifier for the Payment Method Session"), + ), + request_body( + content = PaymentMethodSessionUpdateSavedPaymentMethod, + examples(( "Update the card holder name" = ( + value =json!( { + "payment_method_id": "12345_pm_0194b1ecabc172e28aeb71f70a4daba3", + "payment_method_data": { + "card": { + "card_holder_name": "Narayan Bhat" + } + } + } + ) + ))) + ), + responses( + (status = 200, description = "The payment method has been updated successfully", body = PaymentMethodResponse), + (status = 404, description = "The request is invalid") + ), + tag = "Payment Method Session", + operation_id = "Update a saved payment method", + security(("ephemeral_key" = [])) +)] +pub fn payment_method_session_update_saved_payment_method() {} diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 4d50644958..05d08f37dd 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -215,5 +215,8 @@ pub const AUTHENTICATION_SERVICE_ELIGIBLE_CONFIG: &str = /// Refund flow identifier used for performing GSM operations pub const REFUND_FLOW_STR: &str = "refund_flow"; +/// Default payment method session expiry +pub const DEFAULT_PAYMENT_METHOD_SESSION_EXPIRY: u32 = 15 * 60; // 15 minutes + /// Authorize flow identifier used for performing GSM operations pub const AUTHORIZE_FLOW_STR: &str = "Authorize"; diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index d9f0db4c2a..5bdb0f0819 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -67,8 +67,10 @@ use crate::{ services::encryption, types::{ api::{self, payment_methods::PaymentMethodCreateExt}, + domain::types as domain_types, payment_methods as pm_types, storage::{ephemeral_key, PaymentMethodListContext}, + transformers::{ForeignFrom, ForeignTryFrom}, }, utils::ext_traits::OptionExt, }; @@ -852,6 +854,8 @@ pub async fn create_payment_method( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, ) -> RouterResponse { + use common_utils::ext_traits::ValueExt; + req.validate()?; let db = &*state.store; @@ -870,14 +874,20 @@ pub async fn create_payment_method( .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) .attach_printable("Customer not found for the payment method")?; - let payment_method_billing_address: Option>> = req + let payment_method_billing_address = req .billing .clone() .async_map(|billing| cards::create_encrypted_data(key_manager_state, key_store, billing)) .await .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to encrypt Payment method billing address")?; + .attach_printable("Unable to encrypt Payment method billing address")? + .map(|encoded_address| { + encoded_address.deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to parse Payment method billing address")?; // create pm let payment_method_id = @@ -885,7 +895,7 @@ pub async fn create_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; - let (payment_method, ephemeral_key) = create_payment_method_for_intent( + let payment_method = create_payment_method_for_intent( state, req.metadata.clone(), &customer_id, @@ -935,10 +945,7 @@ pub async fn create_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; - let resp = pm_transforms::generate_payment_method_response( - &payment_method, - Some(ephemeral_key), - )?; + let resp = pm_transforms::generate_payment_method_response(&payment_method)?; Ok(resp) } @@ -989,14 +996,20 @@ pub async fn payment_method_intent_create( .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound) .attach_printable("Customer not found for the payment method")?; - let payment_method_billing_address: Option>> = req + let payment_method_billing_address = req .billing .clone() .async_map(|billing| cards::create_encrypted_data(key_manager_state, key_store, billing)) .await .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to encrypt Payment method billing address")?; + .attach_printable("Unable to encrypt Payment method billing address")? + .map(|encoded_address| { + encoded_address.deserialize_inner_value(|value| value.parse_value("Address")) + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to parse Payment method billing address")?; // create pm entry @@ -1005,7 +1018,7 @@ pub async fn payment_method_intent_create( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Unable to generate GlobalPaymentMethodId")?; - let (payment_method, ephemeral_key) = create_payment_method_for_intent( + let payment_method = create_payment_method_for_intent( state, req.metadata.clone(), &customer_id, @@ -1018,122 +1031,11 @@ pub async fn payment_method_intent_create( .await .attach_printable("Failed to add Payment method to DB")?; - let resp = - pm_transforms::generate_payment_method_response(&payment_method, Some(ephemeral_key))?; + let resp = pm_transforms::generate_payment_method_response(&payment_method)?; Ok(services::ApplicationResponse::Json(resp)) } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -#[instrument(skip_all)] -pub async fn payment_method_intent_confirm( - state: &SessionState, - req: api::PaymentMethodIntentConfirm, - merchant_account: &domain::MerchantAccount, - key_store: &domain::MerchantKeyStore, - pm_id: id_type::GlobalPaymentMethodId, -) -> RouterResponse { - let key_manager_state = &(state).into(); - req.validate()?; - - let db = &*state.store; - - let payment_method = db - .find_payment_method( - &(state.into()), - key_store, - &pm_id, - merchant_account.storage_scheme, - ) - .await - .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) - .attach_printable("Unable to find payment method")?; - - when( - payment_method.status != enums::PaymentMethodStatus::AwaitingData, - || { - Err(errors::ApiErrorResponse::InvalidRequestData { - message: "Invalid pm_id provided: This Payment method cannot be confirmed" - .to_string(), - }) - }, - )?; - - let customer_id = payment_method.customer_id.to_owned(); - db.find_customer_by_global_id( - key_manager_state, - &customer_id, - merchant_account.get_id(), - key_store, - merchant_account.storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; - - let payment_method_data = pm_types::PaymentMethodVaultingData::from(req.payment_method_data); - - let vaulting_result = vault_payment_method( - state, - &payment_method_data, - merchant_account, - key_store, - None, - ) - .await; - - let response = match vaulting_result { - Ok((vaulting_resp, fingerprint_id)) => { - let pm_update = create_pm_additional_data_update( - &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), - ) - .await - .attach_printable("Unable to create Payment method data")?; - - let payment_method = db - .update_payment_method( - &(state.into()), - key_store, - payment_method, - pm_update, - merchant_account.storage_scheme, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to update payment method in db")?; - - let resp = pm_transforms::generate_payment_method_response(&payment_method, None)?; - - Ok(resp) - } - Err(e) => { - let pm_update = storage::PaymentMethodUpdate::StatusUpdate { - status: Some(enums::PaymentMethodStatus::Inactive), - }; - - db.update_payment_method( - &(state.into()), - key_store, - payment_method, - pm_update, - merchant_account.storage_scheme, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to update payment method in db")?; - - Err(e) - } - }?; - - Ok(services::ApplicationResponse::Json(response)) -} - #[cfg(feature = "v2")] trait PerformFilteringOnEnabledPaymentMethods { fn perform_filtering(self) -> FilteredPaymentMethodsEnabled; @@ -1148,6 +1050,55 @@ impl PerformFilteringOnEnabledPaymentMethods } } +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all)] +pub async fn list_payment_methods_for_session( + state: SessionState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + profile: domain::Profile, + payment_method_session_id: id_type::GlobalPaymentMethodSessionId, +) -> RouterResponse { + let key_manager_state = &(&state).into(); + + let db = &*state.store; + + let payment_method_session = db + .get_payment_methods_session(key_manager_state, &key_store, &payment_method_session_id) + .await + .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) + .attach_printable("Unable to find payment method")?; + + let payment_connector_accounts = db + .list_enabled_connector_accounts_by_profile_id( + key_manager_state, + profile.get_id(), + &key_store, + common_enums::ConnectorType::PaymentProcessor, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error when fetching merchant connector accounts")?; + + let customer_payment_methods = list_customer_payment_method_core( + &state, + &merchant_account, + &key_store, + &payment_method_session.customer_id, + ) + .await?; + + let response = + hyperswitch_domain_models::merchant_connector_account::FlattenedPaymentMethodsEnabled::from_payment_connectors_list(payment_connector_accounts) + .perform_filtering() + .get_required_fields(RequiredFieldsInput::new(state.conf.required_fields.clone())) + .generate_response(customer_payment_methods.customer_payment_methods); + + Ok(hyperswitch_domain_models::api::ApplicationResponse::Json( + response, + )) +} + #[cfg(feature = "v2")] /// Container for the inputs required for the required fields struct RequiredFieldsInput { @@ -1262,7 +1213,10 @@ struct RequiredFieldsForEnabledPaymentMethodTypes(Vec payment_methods::PaymentMethodListResponse { + fn generate_response( + self, + customer_payment_methods: Vec, + ) -> payment_methods::PaymentMethodListResponse { let response_payment_methods = self .0 .into_iter() @@ -1278,121 +1232,11 @@ impl RequiredFieldsForEnabledPaymentMethodTypes { payment_methods::PaymentMethodListResponse { payment_methods_enabled: response_payment_methods, + customer_payment_methods, } } } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -#[instrument(skip_all)] -pub async fn list_payment_methods_enabled( - state: SessionState, - merchant_account: domain::MerchantAccount, - key_store: domain::MerchantKeyStore, - profile: domain::Profile, - payment_method_id: id_type::GlobalPaymentMethodId, -) -> RouterResponse { - let key_manager_state = &(&state).into(); - - let db = &*state.store; - - db.find_payment_method( - key_manager_state, - &key_store, - &payment_method_id, - merchant_account.storage_scheme, - ) - .await - .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) - .attach_printable("Unable to find payment method")?; - - let payment_connector_accounts = db - .list_enabled_connector_accounts_by_profile_id( - key_manager_state, - profile.get_id(), - &key_store, - common_enums::ConnectorType::PaymentProcessor, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("error when fetching merchant connector accounts")?; - - let response = - hyperswitch_domain_models::merchant_connector_account::FlattenedPaymentMethodsEnabled::from_payment_connectors_list(payment_connector_accounts) - .perform_filtering() - .get_required_fields(RequiredFieldsInput::new(state.conf.required_fields.clone())) - .generate_response(); - - Ok(hyperswitch_domain_models::api::ApplicationResponse::Json( - response, - )) -} - -#[cfg(all( - feature = "v2", - feature = "payment_methods_v2", - feature = "customer_v2" -))] -#[instrument(skip_all)] -#[allow(clippy::too_many_arguments)] -pub async fn create_payment_method_in_db( - state: &SessionState, - req: &api::PaymentMethodCreate, - customer_id: &id_type::GlobalCustomerId, - payment_method_id: id_type::GlobalPaymentMethodId, - locker_id: Option, - merchant_id: &id_type::MerchantId, - customer_acceptance: Option, - payment_method_data: domain::types::OptionalEncryptableJsonType< - api::payment_methods::PaymentMethodsData, - >, - key_store: &domain::MerchantKeyStore, - connector_mandate_details: Option, - status: Option, - network_transaction_id: Option, - storage_scheme: enums::MerchantStorageScheme, - payment_method_billing_address: crypto::OptionalEncryptableValue, - card_scheme: Option, -) -> errors::CustomResult { - let db = &*state.store; - let current_time = common_utils::date_time::now(); - - let response = db - .insert_payment_method( - &state.into(), - key_store, - domain::PaymentMethod { - customer_id: customer_id.to_owned(), - merchant_id: merchant_id.to_owned(), - id: payment_method_id, - locker_id, - payment_method_type: Some(req.payment_method_type), - payment_method_subtype: Some(req.payment_method_subtype), - payment_method_data, - connector_mandate_details, - customer_acceptance, - client_secret: None, - status: status.unwrap_or(enums::PaymentMethodStatus::Active), - network_transaction_id: network_transaction_id.to_owned(), - created_at: current_time, - last_modified: current_time, - last_used_at: current_time, - payment_method_billing_address, - updated_by: None, - version: domain::consts::API_VERSION, - locker_fingerprint_id: None, - network_token_locker_id: None, - network_token_payment_method_data: None, - network_token_requestor_reference_id: None, - }, - storage_scheme, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to add payment method in db")?; - - Ok(response) -} - #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] @@ -1404,18 +1248,11 @@ pub async fn create_payment_method_for_intent( merchant_id: &id_type::MerchantId, key_store: &domain::MerchantKeyStore, storage_scheme: enums::MerchantStorageScheme, - payment_method_billing_address: crypto::OptionalEncryptableValue, -) -> errors::CustomResult<(domain::PaymentMethod, Secret), errors::ApiErrorResponse> { + payment_method_billing_address: Option< + Encryptable, + >, +) -> errors::CustomResult { let db = &*state.store; - let ephemeral_key = payment_helpers::create_ephemeral_key( - state, - customer_id, - merchant_id, - ephemeral_key::ResourceType::PaymentMethod, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to create ephemeral_key")?; let current_time = common_utils::date_time::now(); @@ -1453,7 +1290,7 @@ pub async fn create_payment_method_for_intent( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to add payment method in db")?; - Ok((response, ephemeral_key.secret)) + Ok(response) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -1537,12 +1374,14 @@ pub async fn vault_payment_method( Ok((resp_from_vault, fingerprint_id_from_vault)) } +// TODO: check if this function will be used for listing the customer payment methods for payments +#[allow(unused)] #[cfg(all( feature = "v2", feature = "payment_methods_v2", feature = "customer_v2" ))] -async fn get_pm_list_context( +fn get_pm_list_context( payment_method_type: enums::PaymentMethod, payment_method: &domain::PaymentMethod, is_payment_associated: bool, @@ -1550,7 +1389,7 @@ async fn get_pm_list_context( let payment_method_data = payment_method .payment_method_data .clone() - .map(|payment_method_data| payment_method_data.into_inner().expose().into_inner()); + .map(|payment_method_data| payment_method_data.into_inner()); let payment_method_retrieval_context = match payment_method_data { Some(payment_methods::PaymentMethodsData::Card(card)) => { @@ -1629,316 +1468,39 @@ async fn get_pm_list_context( feature = "payment_methods_v2", feature = "customer_v2" ))] -pub async fn list_customer_payment_method_util( - state: SessionState, - merchant_account: domain::MerchantAccount, - profile: domain::Profile, - key_store: domain::MerchantKeyStore, - req: Option, - customer_id: Option, - is_payment_associated: bool, -) -> RouterResponse { - let limit = req.as_ref().and_then(|pml_req| pml_req.limit); - - let (customer_id, payment_intent) = if is_payment_associated { - let cloned_secret = req.and_then(|r| r.client_secret.clone()); - let payment_intent = payment_helpers::verify_payment_intent_time_and_client_secret( - &state, - &merchant_account, - &key_store, - cloned_secret, - ) - .await?; - - ( - payment_intent - .as_ref() - .and_then(|pi| pi.customer_id.clone()), - payment_intent, - ) - } else { - (customer_id, None) - }; - - let resp = if let Some(cust) = customer_id { - Box::pin(list_customer_payment_method( - &state, - &merchant_account, - profile, - key_store, - payment_intent, - &cust, - limit, - is_payment_associated, - )) - .await? - } else { - let response = api::CustomerPaymentMethodsListResponse { - customer_payment_methods: Vec::new(), - is_guest_customer: Some(true), - }; - services::ApplicationResponse::Json(response) - }; - - Ok(resp) -} - -#[allow(clippy::too_many_arguments)] -#[cfg(all( - feature = "v2", - feature = "payment_methods_v2", - feature = "customer_v2" -))] -pub async fn list_customer_payment_method( +pub async fn list_customer_payment_method_core( state: &SessionState, merchant_account: &domain::MerchantAccount, - profile: domain::Profile, - key_store: domain::MerchantKeyStore, - payment_intent: Option, + key_store: &domain::MerchantKeyStore, customer_id: &id_type::GlobalCustomerId, - limit: Option, - is_payment_associated: bool, -) -> RouterResponse { +) -> RouterResult { let db = &*state.store; let key_manager_state = &(state).into(); - let customer = db - .find_customer_by_global_id( - key_manager_state, - customer_id, - merchant_account.get_id(), - &key_store, - merchant_account.storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; - - let payments_info = payment_intent - .async_map(|pi| { - pm_types::SavedPMLPaymentsInfo::form_payments_info( - pi, - merchant_account, - profile, - db, - key_manager_state, - &key_store, - ) - }) - .await - .transpose()?; - let saved_payment_methods = db .find_payment_method_by_global_customer_id_merchant_id_status( key_manager_state, - &key_store, + key_store, customer_id, merchant_account.get_id(), common_enums::PaymentMethodStatus::Active, - limit, + None, merchant_account.storage_scheme, ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentMethodNotFound)?; - let mut filtered_saved_payment_methods_ctx = Vec::new(); - for payment_method in saved_payment_methods.into_iter() { - let payment_method_type = payment_method - .get_payment_method_type() - .get_required_value("payment_method")?; - let parent_payment_method_token = - is_payment_associated.then(|| generate_id(consts::ID_LENGTH, "token")); - - let pm_list_context = - get_pm_list_context(payment_method_type, &payment_method, is_payment_associated) - .await?; - - if let Some(ctx) = pm_list_context { - filtered_saved_payment_methods_ctx.push(( - ctx, - parent_payment_method_token, - payment_method, - )); - } - } - - let merchant_connector_accounts = if filtered_saved_payment_methods_ctx.iter().any( - |(_pm_list_context, _parent_payment_method_token, pm)| { - pm.connector_mandate_details.is_some() - }, - ) { - db.find_merchant_connector_account_by_merchant_id_and_disabled_list( - key_manager_state, - merchant_account.get_id(), - true, - &key_store, - ) - .await - .change_context(errors::ApiErrorResponse::MerchantAccountNotFound)? - } else { - Vec::new() - }; - let merchant_connector_accounts = - domain::MerchantConnectorAccounts::new(merchant_connector_accounts); - - let pm_list_futures = filtered_saved_payment_methods_ctx + let customer_payment_methods = saved_payment_methods .into_iter() - .map(|ctx| { - generate_saved_pm_response( - state, - &key_store, - merchant_account, - &merchant_connector_accounts, - ctx, - &customer, - payments_info.as_ref(), - ) - }) - .collect::>(); + .map(ForeignTryFrom::foreign_try_from) + .collect::, _>>() + .change_context(errors::ApiErrorResponse::InternalServerError)?; - let customer_pms = futures::future::join_all(pm_list_futures) - .await - .into_iter() - .collect::, _>>() - .attach_printable("Failed to obtain customer payment methods")?; - - let mut response = api::CustomerPaymentMethodsListResponse { - customer_payment_methods: customer_pms, - is_guest_customer: is_payment_associated.then_some(false), //to return this key only when the request is tied to a payment intent + let response = api::CustomerPaymentMethodsListResponse { + customer_payment_methods, }; - /* - TODO: Implement surcharge for v2 - if is_payment_associated { - Box::pin(cards::perform_surcharge_ops( - payments_info.as_ref().map(|pi| pi.payment_intent.clone()), - state, - merchant_account, - key_store, - payments_info.map(|pi| pi.profile), - &mut response, - )) - .await?; - } - */ - - Ok(services::ApplicationResponse::Json(response)) -} - -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -async fn generate_saved_pm_response( - state: &SessionState, - key_store: &domain::MerchantKeyStore, - merchant_account: &domain::MerchantAccount, - merchant_connector_accounts: &domain::MerchantConnectorAccounts, - (pm_list_context, parent_payment_method_token, pm): ( - PaymentMethodListContext, - Option, - domain::PaymentMethod, - ), - customer: &domain::Customer, - payment_info: Option<&pm_types::SavedPMLPaymentsInfo>, -) -> Result> { - let payment_method_type = pm - .get_payment_method_type() - .get_required_value("payment_method_type")?; - - let bank_details = if payment_method_type == enums::PaymentMethod::BankDebit { - cards::get_masked_bank_details(&pm) - .await - .unwrap_or_else(|err| { - logger::error!(error=?err); - None - }) - } else { - None - }; - - let payment_method_billing = pm - .payment_method_billing_address - .clone() - .map(|decrypted_data| decrypted_data.into_inner().expose()) - .map(|decrypted_value| decrypted_value.parse_value("payment_method_billing_address")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("unable to parse payment method billing address details")?; - - let (is_connector_agnostic_mit_enabled, requires_cvv, off_session_payment_flag, profile_id) = - payment_info - .map(|pi| { - ( - pi.is_connector_agnostic_mit_enabled, - pi.collect_cvv_during_payment, - pi.off_session_payment_flag, - Some(pi.profile.get_id().to_owned()), - ) - }) - .unwrap_or((false, false, false, Default::default())); - - let mca_enabled = cards::get_mca_status( - state, - key_store, - profile_id, - merchant_account.get_id(), - is_connector_agnostic_mit_enabled, - pm.connector_mandate_details.as_ref(), - pm.network_transaction_id.as_ref(), - merchant_connector_accounts, - ) - .await; - - let requires_cvv = if is_connector_agnostic_mit_enabled { - requires_cvv - && !(off_session_payment_flag - && (pm.connector_mandate_details.is_some() || pm.network_transaction_id.is_some())) - } else { - requires_cvv && !(off_session_payment_flag && pm.connector_mandate_details.is_some()) - }; - - let pmd = match &pm_list_context { - PaymentMethodListContext::Card { card_details, .. } => { - Some(api::PaymentMethodListData::Card(card_details.clone())) - } - #[cfg(feature = "payouts")] - PaymentMethodListContext::BankTransfer { - bank_transfer_details, - .. - } => Some(api::PaymentMethodListData::Bank( - bank_transfer_details.clone(), - )), - PaymentMethodListContext::Bank { .. } | PaymentMethodListContext::TemporaryToken { .. } => { - None - } - }; - - let pma = api::CustomerPaymentMethod { - payment_token: parent_payment_method_token.clone(), - payment_method_id: pm.get_id().get_string_repr().to_owned(), - customer_id: pm.customer_id.to_owned(), - payment_method_type, - payment_method_subtype: pm.get_payment_method_subtype(), - payment_method_data: pmd, - recurring_enabled: mca_enabled, - created: pm.created_at, - bank: bank_details, - surcharge_details: None, - requires_cvv: requires_cvv - && !(off_session_payment_flag && pm.connector_mandate_details.is_some()), - last_used_at: Some(pm.last_used_at), - is_default: customer - .default_payment_method_id - .as_ref() - .is_some_and(|payment_method_id| payment_method_id == pm.get_id().get_string_repr()), - billing: payment_method_billing, - }; - - payment_info - .async_map(|pi| { - pi.perform_payment_ops(state, parent_payment_method_token, &pma, pm_list_context) - }) - .await - .transpose()?; - - Ok(pma) + Ok(response) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -1972,7 +1534,7 @@ pub async fn retrieve_payment_method( let pmd = payment_method .payment_method_data .clone() - .map(|x| x.into_inner().expose().into_inner()) + .map(|x| x.into_inner()) .and_then(|pmd| match pmd { api::PaymentMethodsData::Card(card) => { Some(api::PaymentMethodResponseData::Card(card.into())) @@ -1989,7 +1551,6 @@ pub async fn retrieve_payment_method( created: Some(payment_method.created_at), recurring_enabled: false, last_used_at: Some(payment_method.last_used_at), - ephemeral_key: None, payment_method_data: pmd, }; @@ -2001,21 +1562,33 @@ pub async fn retrieve_payment_method( pub async fn update_payment_method( state: SessionState, merchant_account: domain::MerchantAccount, - req: api::PaymentMethodUpdate, - payment_method_id: &str, key_store: domain::MerchantKeyStore, + req: api::PaymentMethodUpdate, + payment_method_id: &id_type::GlobalPaymentMethodId, ) -> RouterResponse { - let db = state.store.as_ref(); + let response = + update_payment_method_core(state, merchant_account, key_store, req, payment_method_id) + .await?; - let pm_id = id_type::GlobalPaymentMethodId::generate_from_string(payment_method_id.to_string()) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to generate GlobalPaymentMethodId")?; + Ok(services::ApplicationResponse::Json(response)) +} + +#[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, + payment_method_id: &id_type::GlobalPaymentMethodId, +) -> RouterResult { + let db = state.store.as_ref(); let payment_method = db .find_payment_method( &((&state).into()), &key_store, - &pm_id, + payment_method_id, merchant_account.storage_scheme, ) .await @@ -2076,11 +1649,11 @@ pub async fn update_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update payment method in db")?; - let response = pm_transforms::generate_payment_method_response(&payment_method, None)?; + let response = pm_transforms::generate_payment_method_response(&payment_method)?; // Add a PT task to handle payment_method delete from vault - Ok(services::ApplicationResponse::Json(response)) + Ok(response) } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -2135,6 +1708,7 @@ pub async fn delete_payment_method( let pm_update = storage::PaymentMethodUpdate::StatusUpdate { status: Some(enums::PaymentMethodStatus::Inactive), }; + db.update_payment_method( &((&state).into()), &key_store, @@ -2156,6 +1730,216 @@ pub async fn delete_payment_method( Ok(services::ApplicationResponse::Json(response)) } +#[cfg(feature = "v2")] +#[async_trait::async_trait] +trait EncryptableData { + type Output; + async fn encrypt_data( + &self, + key_manager_state: &common_utils::types::keymanager::KeyManagerState, + key_store: &domain::MerchantKeyStore, + ) -> RouterResult; +} + +#[cfg(feature = "v2")] +#[async_trait::async_trait] +impl EncryptableData for payment_methods::PaymentMethodSessionRequest { + type Output = hyperswitch_domain_models::payment_methods::DecryptedPaymentMethodsSession; + + 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::PaymentMethodsSession), + domain_types::CryptoOperation::BatchEncrypt( + hyperswitch_domain_models::payment_methods::FromRequestEncryptablePaymentMethodsSession::to_encryptable( + hyperswitch_domain_models::payment_methods::FromRequestEncryptablePaymentMethodsSession { + 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::FromRequestEncryptablePaymentMethodsSession::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")] +pub async fn payment_methods_session_create( + state: SessionState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + request: payment_methods::PaymentMethodSessionRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + let key_manager_state = &(&state).into(); + + db.find_customer_by_global_id( + key_manager_state, + &request.customer_id, + merchant_account.get_id(), + &key_store, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + + let payment_methods_session_id = + id_type::GlobalPaymentMethodSessionId::generate(&state.conf.cell_information.id) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to generate GlobalPaymentMethodSessionId")?; + + 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")?; + + // If not passed in the request, use the default value from constants + let expires_in = request + .expires_in + .unwrap_or(consts::DEFAULT_PAYMENT_METHOD_SESSION_EXPIRY) + .into(); + + let expires_at = common_utils::date_time::now().saturating_add(Duration::seconds(expires_in)); + + let client_secret = payment_helpers::create_client_secret( + &state, + merchant_account.get_id(), + util_types::authentication::ResourceId::PaymentMethodSession( + payment_methods_session_id.clone(), + ), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to create client secret")?; + + let payment_method_session_domain_model = + hyperswitch_domain_models::payment_methods::PaymentMethodsSession { + id: payment_methods_session_id, + customer_id: request.customer_id, + billing, + psp_tokenization: request.psp_tokenization, + network_tokenization: request.network_tokenization, + expires_at, + }; + + db.insert_payment_methods_session( + key_manager_state, + &key_store, + payment_method_session_domain_model.clone(), + expires_in, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to insert payment methods session in db")?; + + let response = payment_methods::PaymentMethodsSessionResponse::foreign_from(( + payment_method_session_domain_model, + client_secret.secret, + )); + + 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 { + let db = state.store.as_ref(); + let key_manager_state = &(&state).into(); + + let payment_method_session_domain_model = 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 response = payment_methods::PaymentMethodsSessionResponse::foreign_from(( + payment_method_session_domain_model, + Secret::new("CLIENT_SECRET_REDACTED".to_string()), + )); + + Ok(services::ApplicationResponse::Json(response)) +} + +#[cfg(feature = "v2")] +pub async fn payment_methods_session_update_payment_method( + state: SessionState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + payment_method_session_id: id_type::GlobalPaymentMethodSessionId, + request: payment_methods::PaymentMethodSessionUpdateSavedPaymentMethod, +) -> RouterResponse { + let db = state.store.as_ref(); + let key_manager_state = &(&state).into(); + + // Validate if the session still exists + 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_update_request = request.payment_method_update_request; + + let updated_payment_method = update_payment_method_core( + state, + merchant_account, + key_store, + payment_method_update_request, + &request.payment_method_id, + ) + .await + .attach_printable("Failed to update saved payment method")?; + + Ok(services::ApplicationResponse::Json(updated_payment_method)) +} + #[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/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 1f4fb6904f..1f37cc157e 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -63,6 +63,7 @@ use strum::IntoEnumIterator; not(feature = "payment_methods_v2") ))] use super::migration; +#[cfg(feature = "v1")] use super::surcharge_decision_configs::{ perform_surcharge_decision_management_for_payment_method_list, perform_surcharge_decision_management_for_saved_cards, @@ -5434,10 +5435,7 @@ pub async fn get_masked_bank_details( .transpose()?; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] - let payment_method_data = pm - .payment_method_data - .clone() - .map(|x| x.into_inner().expose().into_inner()); + let payment_method_data = pm.payment_method_data.clone().map(|x| x.into_inner()); match payment_method_data { Some(pmd) => match pmd { diff --git a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs index 6a2b3198e9..7c8558ff8c 100644 --- a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -367,82 +367,83 @@ pub async fn perform_surcharge_decision_management_for_saved_cards( Ok(surcharge_metadata) } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -pub async fn perform_surcharge_decision_management_for_saved_cards( - state: &SessionState, - algorithm_ref: routing::RoutingAlgorithmRef, - payment_attempt: &storage::PaymentAttempt, - payment_intent: &storage::PaymentIntent, - customer_payment_method_list: &mut [api_models::payment_methods::CustomerPaymentMethod], -) -> ConditionalConfigResult { - // let mut surcharge_metadata = types::SurchargeMetadata::new(payment_attempt.id.clone()); - let mut surcharge_metadata = todo!(); +// TODO: uncomment and resolve compiler error when required +// #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +// pub async fn perform_surcharge_decision_management_for_saved_cards( +// state: &SessionState, +// algorithm_ref: routing::RoutingAlgorithmRef, +// payment_attempt: &storage::PaymentAttempt, +// payment_intent: &storage::PaymentIntent, +// customer_payment_method_list: &mut [api_models::payment_methods::CustomerPaymentMethod], +// ) -> ConditionalConfigResult { +// // let mut surcharge_metadata = types::SurchargeMetadata::new(payment_attempt.id.clone()); +// let mut surcharge_metadata = todo!(); - let surcharge_source = match ( - payment_attempt.get_surcharge_details(), - algorithm_ref.surcharge_config_algo_id, - ) { - (Some(request_surcharge_details), _) => { - SurchargeSource::Predetermined(request_surcharge_details) - } - (None, Some(algorithm_id)) => { - let cached_algo = ensure_algorithm_cached( - &*state.store, - &payment_attempt.merchant_id, - algorithm_id.as_str(), - ) - .await?; +// let surcharge_source = match ( +// payment_attempt.get_surcharge_details(), +// algorithm_ref.surcharge_config_algo_id, +// ) { +// (Some(request_surcharge_details), _) => { +// SurchargeSource::Predetermined(request_surcharge_details) +// } +// (None, Some(algorithm_id)) => { +// let cached_algo = ensure_algorithm_cached( +// &*state.store, +// &payment_attempt.merchant_id, +// algorithm_id.as_str(), +// ) +// .await?; - SurchargeSource::Generate(cached_algo) - } - (None, None) => return Ok(surcharge_metadata), - }; - let surcharge_source_log_message = match &surcharge_source { - SurchargeSource::Generate(_) => "Surcharge was calculated through surcharge rules", - SurchargeSource::Predetermined(_) => "Surcharge was sent in payment create request", - }; - logger::debug!(customer_saved_card_list_surcharge_source = surcharge_source_log_message); - let mut backend_input = make_dsl_input_for_surcharge(payment_attempt, payment_intent, None) - .change_context(ConfigError::InputConstructionError)?; +// SurchargeSource::Generate(cached_algo) +// } +// (None, None) => return Ok(surcharge_metadata), +// }; +// let surcharge_source_log_message = match &surcharge_source { +// SurchargeSource::Generate(_) => "Surcharge was calculated through surcharge rules", +// SurchargeSource::Predetermined(_) => "Surcharge was sent in payment create request", +// }; +// logger::debug!(customer_saved_card_list_surcharge_source = surcharge_source_log_message); +// let mut backend_input = make_dsl_input_for_surcharge(payment_attempt, payment_intent, None) +// .change_context(ConfigError::InputConstructionError)?; - for customer_payment_method in customer_payment_method_list.iter_mut() { - let payment_token = customer_payment_method - .payment_token - .clone() - .get_required_value("payment_token") - .change_context(ConfigError::InputConstructionError)?; +// for customer_payment_method in customer_payment_method_list.iter_mut() { +// let payment_token = customer_payment_method +// .payment_token +// .clone() +// .get_required_value("payment_token") +// .change_context(ConfigError::InputConstructionError)?; - backend_input.payment_method.payment_method = - Some(customer_payment_method.payment_method_type); - backend_input.payment_method.payment_method_type = - customer_payment_method.payment_method_subtype; +// backend_input.payment_method.payment_method = +// Some(customer_payment_method.payment_method_type); +// backend_input.payment_method.payment_method_type = +// customer_payment_method.payment_method_subtype; - let card_network = match customer_payment_method.payment_method_data.as_ref() { - Some(api_models::payment_methods::PaymentMethodListData::Card(card)) => { - card.card_network.clone() - } - _ => None, - }; - backend_input.payment_method.card_network = card_network; +// let card_network = match customer_payment_method.payment_method_data.as_ref() { +// Some(api_models::payment_methods::PaymentMethodListData::Card(card)) => { +// card.card_network.clone() +// } +// _ => None, +// }; +// backend_input.payment_method.card_network = card_network; - let surcharge_details = surcharge_source - .generate_surcharge_details_and_populate_surcharge_metadata( - &backend_input, - payment_attempt, - ( - &mut surcharge_metadata, - types::SurchargeKey::Token(payment_token), - ), - )?; - customer_payment_method.surcharge_details = surcharge_details - .map(|surcharge_details| { - SurchargeDetailsResponse::foreign_try_from((&surcharge_details, payment_attempt)) - .change_context(ConfigError::DslParsingError) - }) - .transpose()?; - } - Ok(surcharge_metadata) -} +// let surcharge_details = surcharge_source +// .generate_surcharge_details_and_populate_surcharge_metadata( +// &backend_input, +// payment_attempt, +// ( +// &mut surcharge_metadata, +// types::SurchargeKey::Token(payment_token), +// ), +// )?; +// customer_payment_method.surcharge_details = surcharge_details +// .map(|surcharge_details| { +// SurchargeDetailsResponse::foreign_try_from((&surcharge_details, payment_attempt)) +// .change_context(ConfigError::DslParsingError) +// }) +// .transpose()?; +// } +// Ok(surcharge_metadata) +// } #[cfg(feature = "v2")] fn get_surcharge_details_from_surcharge_output( diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 4a43eef1ed..c4d3eafe46 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -16,7 +16,7 @@ use router_env::tracing_actix_web::RequestId; use serde::{Deserialize, Serialize}; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -use crate::types::payment_methods as pm_types; +use crate::types::{payment_methods as pm_types, transformers}; use crate::{ configs::settings, core::errors::{self, CustomResult}, @@ -551,12 +551,11 @@ pub fn generate_pm_vaulting_req_from_update_request( #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub fn generate_payment_method_response( pm: &domain::PaymentMethod, - ephemeral_key: Option>, ) -> errors::RouterResult { let pmd = pm .payment_method_data .clone() - .map(|data| data.into_inner().expose().into_inner()) + .map(|data| data.into_inner()) .and_then(|data| match data { api::PaymentMethodsData::Card(card) => { Some(api::PaymentMethodResponseData::Card(card.into())) @@ -573,7 +572,6 @@ pub fn generate_payment_method_response( created: Some(pm.created_at), recurring_enabled: false, last_used_at: Some(pm.last_used_at), - ephemeral_key, payment_method_data: pmd, }; @@ -911,3 +909,93 @@ pub fn mk_card_value2( .change_context(errors::VaultError::FetchCardFailed)?; Ok(value2_req) } + +#[cfg(feature = "v2")] +impl transformers::ForeignTryFrom for api::CustomerPaymentMethod { + type Error = error_stack::Report; + + fn foreign_try_from(item: domain::PaymentMethod) -> Result { + // For payment methods that are active we should always have the payment method subtype + let payment_method_subtype = + item.payment_method_subtype + .ok_or(errors::ValidationError::MissingRequiredField { + field_name: "payment_method_subtype".to_string(), + })?; + + // For payment methods that are active we should always have the payment method type + let payment_method_type = + item.payment_method_type + .ok_or(errors::ValidationError::MissingRequiredField { + field_name: "payment_method_type".to_string(), + })?; + + let payment_method_data = item + .payment_method_data + .map(|payment_method_data| payment_method_data.into_inner()) + .map(|payment_method_data| match payment_method_data { + api_models::payment_methods::PaymentMethodsData::Card( + card_details_payment_method, + ) => { + let card_details = api::CardDetailFromLocker::from(card_details_payment_method); + api_models::payment_methods::PaymentMethodListData::Card(card_details) + } + api_models::payment_methods::PaymentMethodsData::BankDetails(..) => todo!(), + api_models::payment_methods::PaymentMethodsData::WalletDetails(..) => { + todo!() + } + }); + + let payment_method_billing = item + .payment_method_billing_address + .clone() + .map(|billing| billing.into_inner()) + .map(From::from); + + // TODO: check how we can get this field + let recurring_enabled = true; + + Ok(Self { + id: item.id, + customer_id: item.customer_id, + payment_method_type, + payment_method_subtype, + created: item.created_at, + last_used_at: item.last_used_at, + recurring_enabled, + payment_method_data, + bank: None, + requires_cvv: true, + is_default: false, + billing: payment_method_billing, + }) + } +} + +#[cfg(feature = "v2")] +impl + transformers::ForeignFrom<( + hyperswitch_domain_models::payment_methods::PaymentMethodsSession, + Secret, + )> for api_models::payment_methods::PaymentMethodsSessionResponse +{ + fn foreign_from( + item: ( + hyperswitch_domain_models::payment_methods::PaymentMethodsSession, + Secret, + ), + ) -> Self { + let (session, client_secret) = 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, + } + } +} diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index cfa57a3565..28d843e235 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,7 +1,7 @@ use std::{borrow::Cow, collections::HashSet, str::FromStr}; #[cfg(feature = "v2")] -use api_models::ephemeral_key::EphemeralKeyResponse; +use api_models::ephemeral_key::ClientSecretResponse; use api_models::{ mandates::RecurringDetails, payments::{additional_info as payment_additional_types, RequestSurchargeDetails}, @@ -3055,76 +3055,70 @@ pub async fn make_ephemeral_key( } #[cfg(feature = "v2")] -pub async fn make_ephemeral_key( +pub async fn make_client_secret( state: SessionState, - customer_id: id_type::GlobalCustomerId, + resource_id: api_models::ephemeral_key::ResourceId, merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, headers: &actix_web::http::header::HeaderMap, -) -> errors::RouterResponse { +) -> errors::RouterResponse { let db = &state.store; let key_manager_state = &((&state).into()); - db.find_customer_by_global_id( - key_manager_state, - &customer_id, - merchant_account.get_id(), - &key_store, - merchant_account.storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; - let resource_type = services::authentication::get_header_value_by_key( - headers::X_RESOURCE_TYPE.to_string(), - headers, - )? - .map(ephemeral_key::ResourceType::from_str) - .transpose() - .change_context(errors::ApiErrorResponse::InvalidRequestData { - message: format!("`{}` header is invalid", headers::X_RESOURCE_TYPE), - })? - .get_required_value("ResourceType") - .attach_printable("Failed to convert ResourceType from string")?; + match &resource_id { + api_models::ephemeral_key::ResourceId::Customer(global_customer_id) => { + db.find_customer_by_global_id( + key_manager_state, + global_customer_id, + merchant_account.get_id(), + &key_store, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + } + } - let ephemeral_key = create_ephemeral_key( - &state, - &customer_id, - merchant_account.get_id(), - resource_type, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to create ephemeral key")?; + let resource_id = match resource_id { + api_models::ephemeral_key::ResourceId::Customer(global_customer_id) => { + common_utils::types::authentication::ResourceId::Customer(global_customer_id) + } + }; - let response = EphemeralKeyResponse::foreign_from(ephemeral_key); + let client_secret = create_client_secret(&state, merchant_account.get_id(), resource_id) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to create client secret")?; + + let response = ClientSecretResponse::foreign_try_from(client_secret) + .attach_printable("Only customer is supported as resource_id in response")?; Ok(services::ApplicationResponse::Json(response)) } #[cfg(feature = "v2")] -pub async fn create_ephemeral_key( +pub async fn create_client_secret( state: &SessionState, - customer_id: &id_type::GlobalCustomerId, merchant_id: &id_type::MerchantId, - resource_type: ephemeral_key::ResourceType, -) -> RouterResult { + resource_id: common_utils::types::authentication::ResourceId, +) -> RouterResult { use common_utils::generate_time_ordered_id; let store = &state.store; - let id = id_type::EphemeralKeyId::generate(); - let secret = masking::Secret::new(generate_time_ordered_id("epk")); - let ephemeral_key = ephemeral_key::EphemeralKeyTypeNew { + let id = id_type::ClientSecretId::generate(); + let secret = masking::Secret::new(generate_time_ordered_id("cs")); + + let client_secret = ephemeral_key::ClientSecretTypeNew { id, - customer_id: customer_id.to_owned(), merchant_id: merchant_id.to_owned(), secret, - resource_type, + resource_id, }; - let ephemeral_key = store - .create_ephemeral_key(ephemeral_key, state.conf.eph_key.validity) + let client_secret = store + .create_client_secret(client_secret, state.conf.eph_key.validity) .await .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to create ephemeral key")?; - Ok(ephemeral_key) + .attach_printable("Unable to create client secret")?; + Ok(client_secret) } #[cfg(feature = "v1")] @@ -3142,13 +3136,13 @@ pub async fn delete_ephemeral_key( } #[cfg(feature = "v2")] -pub async fn delete_ephemeral_key( +pub async fn delete_client_secret( state: SessionState, ephemeral_key_id: String, -) -> errors::RouterResponse { +) -> errors::RouterResponse { let db = state.store.as_ref(); let ephemeral_key = db - .delete_ephemeral_key(&ephemeral_key_id) + .delete_client_secret(&ephemeral_key_id) .await .map_err(|err| match err.current_context() { errors::StorageError::ValueNotFound(_) => { @@ -3160,7 +3154,8 @@ pub async fn delete_ephemeral_key( }) .attach_printable("Unable to delete ephemeral key")?; - let response = EphemeralKeyResponse::foreign_from(ephemeral_key); + let response = ClientSecretResponse::foreign_try_from(ephemeral_key) + .attach_printable("Only customer is supported as resource_id in response")?; Ok(services::ApplicationResponse::Json(response)) } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 5371afca93..1f6e03a0de 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1929,7 +1929,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, @@ -2751,6 +2751,7 @@ impl ForeignFrom<(storage::PaymentIntent, storage::PaymentAttempt)> for api::Pay } } +#[cfg(feature = "v1")] impl ForeignFrom for api::ephemeral_key::EphemeralKeyCreateResponse { fn foreign_from(from: ephemeral_key::EphemeralKey) -> Self { Self { diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index 9b6396013f..a08f80354b 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -2,11 +2,9 @@ //! //! Functions that are used to perform the retrieval of merchant's //! routing dict, configs, defaults -use std::fmt::Debug; #[cfg(all(feature = "dynamic_routing", feature = "v1"))] use std::str::FromStr; -#[cfg(any(feature = "dynamic_routing", feature = "v1"))] -use std::sync::Arc; +use std::{fmt::Debug, sync::Arc}; use api_models::routing as routing_types; #[cfg(all(feature = "dynamic_routing", feature = "v1"))] diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index d852319e38..17da4902ba 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -30,6 +30,7 @@ pub mod merchant_key_store; pub mod organization; pub mod payment_link; pub mod payment_method; +pub mod payment_method_session; pub mod refund; pub mod relay; pub mod reverse_lookup; @@ -40,6 +41,7 @@ pub mod user; pub mod user_authentication_method; pub mod user_key_store; pub mod user_role; + use common_utils::id_type; use diesel_models::{ fraud_check::{FraudCheck, FraudCheckUpdate}, @@ -97,6 +99,7 @@ pub trait StorageInterface: + dashboard_metadata::DashboardMetadataInterface + dispute::DisputeInterface + ephemeral_key::EphemeralKeyInterface + + ephemeral_key::ClientSecretInterface + events::EventInterface + file::FileMetadataInterface + FraudCheckInterface @@ -135,6 +138,7 @@ pub trait StorageInterface: + generic_link::GenericLinkInterface + relay::RelayInterface + user::theme::ThemeInterface + + payment_method_session::PaymentMethodsSessionInterface + 'static { fn get_scheduler_db(&self) -> Box; diff --git a/crates/router/src/db/ephemeral_key.rs b/crates/router/src/db/ephemeral_key.rs index b3e33cd777..2799d1c9cc 100644 --- a/crates/router/src/db/ephemeral_key.rs +++ b/crates/router/src/db/ephemeral_key.rs @@ -3,7 +3,7 @@ use common_utils::id_type; use time::ext::NumericalDuration; #[cfg(feature = "v2")] -use crate::types::storage::ephemeral_key::{EphemeralKeyType, EphemeralKeyTypeNew, ResourceType}; +use crate::types::storage::ephemeral_key::{ClientSecretType, ClientSecretTypeNew}; use crate::{ core::errors::{self, CustomResult}, db::MockDb, @@ -19,36 +19,39 @@ pub trait EphemeralKeyInterface { _validity: i64, ) -> CustomResult; - #[cfg(feature = "v2")] - async fn create_ephemeral_key( + #[cfg(feature = "v1")] + async fn get_ephemeral_key( &self, - _ek: EphemeralKeyTypeNew, + _key: &str, + ) -> CustomResult; + + #[cfg(feature = "v1")] + async fn delete_ephemeral_key( + &self, + _id: &str, + ) -> CustomResult; +} + +#[async_trait::async_trait] +pub trait ClientSecretInterface { + #[cfg(feature = "v2")] + async fn create_client_secret( + &self, + _ek: ClientSecretTypeNew, _validity: i64, - ) -> CustomResult; - - #[cfg(feature = "v1")] - async fn get_ephemeral_key( - &self, - _key: &str, - ) -> CustomResult; + ) -> CustomResult; #[cfg(feature = "v2")] - async fn get_ephemeral_key( + async fn get_client_secret( &self, _key: &str, - ) -> CustomResult; - - #[cfg(feature = "v1")] - async fn delete_ephemeral_key( - &self, - _id: &str, - ) -> CustomResult; + ) -> CustomResult; #[cfg(feature = "v2")] - async fn delete_ephemeral_key( + async fn delete_client_secret( &self, _id: &str, - ) -> CustomResult; + ) -> CustomResult; } mod storage { @@ -65,11 +68,9 @@ mod storage { use storage_impl::redis::kv_store::RedisConnInterface; use time::ext::NumericalDuration; - use super::EphemeralKeyInterface; + use super::{ClientSecretInterface, EphemeralKeyInterface}; #[cfg(feature = "v2")] - use crate::types::storage::ephemeral_key::{ - EphemeralKeyType, EphemeralKeyTypeNew, ResourceType, - }; + use crate::types::storage::ephemeral_key::{ClientSecretType, ClientSecretTypeNew}; use crate::{ core::errors::{self, CustomResult}, services::Store, @@ -137,66 +138,6 @@ mod storage { } } - #[cfg(feature = "v2")] - #[instrument(skip_all)] - async fn create_ephemeral_key( - &self, - new: EphemeralKeyTypeNew, - validity: i64, - ) -> CustomResult { - let created_at = date_time::now(); - let expires = created_at.saturating_add(validity.hours()); - let id_key = new.id.generate_redis_key(); - - let created_ephemeral_key = EphemeralKeyType { - id: new.id, - created_at, - expires, - customer_id: new.customer_id, - merchant_id: new.merchant_id, - secret: new.secret, - resource_type: new.resource_type, - }; - let secret_key = created_ephemeral_key.generate_secret_key(); - - match self - .get_redis_conn() - .map_err(Into::::into)? - .serialize_and_set_multiple_hash_field_if_not_exist( - &[ - (&secret_key.as_str().into(), &created_ephemeral_key), - (&id_key.as_str().into(), &created_ephemeral_key), - ], - "ephkey", - None, - ) - .await - { - Ok(v) if v.contains(&HsetnxReply::KeyNotSet) => { - Err(errors::StorageError::DuplicateValue { - entity: "ephemeral key", - key: None, - } - .into()) - } - Ok(_) => { - let expire_at = expires.assume_utc().unix_timestamp(); - self.get_redis_conn() - .map_err(Into::::into)? - .set_expire_at(&secret_key.into(), expire_at) - .await - .change_context(errors::StorageError::KVError)?; - self.get_redis_conn() - .map_err(Into::::into)? - .set_expire_at(&id_key.into(), expire_at) - .await - .change_context(errors::StorageError::KVError)?; - Ok(created_ephemeral_key) - } - Err(er) => Err(er).change_context(errors::StorageError::KVError), - } - } - #[cfg(feature = "v1")] #[instrument(skip_all)] async fn get_ephemeral_key( @@ -211,20 +152,6 @@ mod storage { .change_context(errors::StorageError::KVError) } - #[cfg(feature = "v2")] - #[instrument(skip_all)] - async fn get_ephemeral_key( - &self, - key: &str, - ) -> CustomResult { - let key = format!("epkey_{key}"); - self.get_redis_conn() - .map_err(Into::::into)? - .get_hash_field_and_deserialize(&key.into(), "ephkey", "EphemeralKeyType") - .await - .change_context(errors::StorageError::KVError) - } - #[cfg(feature = "v1")] async fn delete_ephemeral_key( &self, @@ -245,15 +172,91 @@ mod storage { .change_context(errors::StorageError::KVError)?; Ok(ek) } + } + + #[async_trait::async_trait] + impl ClientSecretInterface for Store { + #[cfg(feature = "v2")] + #[instrument(skip_all)] + async fn create_client_secret( + &self, + new: ClientSecretTypeNew, + validity: i64, + ) -> CustomResult { + let created_at = date_time::now(); + let expires = created_at.saturating_add(validity.hours()); + let id_key = new.id.generate_redis_key(); + + let created_client_secret = ClientSecretType { + id: new.id, + created_at, + expires, + merchant_id: new.merchant_id, + secret: new.secret, + resource_id: new.resource_id, + }; + let secret_key = created_client_secret.generate_secret_key(); + + match self + .get_redis_conn() + .map_err(Into::::into)? + .serialize_and_set_multiple_hash_field_if_not_exist( + &[ + (&secret_key.as_str().into(), &created_client_secret), + (&id_key.as_str().into(), &created_client_secret), + ], + "csh", + None, + ) + .await + { + Ok(v) if v.contains(&HsetnxReply::KeyNotSet) => { + Err(errors::StorageError::DuplicateValue { + entity: "ephemeral key", + key: None, + } + .into()) + } + Ok(_) => { + let expire_at = expires.assume_utc().unix_timestamp(); + self.get_redis_conn() + .map_err(Into::::into)? + .set_expire_at(&secret_key.into(), expire_at) + .await + .change_context(errors::StorageError::KVError)?; + self.get_redis_conn() + .map_err(Into::::into)? + .set_expire_at(&id_key.into(), expire_at) + .await + .change_context(errors::StorageError::KVError)?; + Ok(created_client_secret) + } + Err(er) => Err(er).change_context(errors::StorageError::KVError), + } + } #[cfg(feature = "v2")] - async fn delete_ephemeral_key( + #[instrument(skip_all)] + async fn get_client_secret( + &self, + key: &str, + ) -> CustomResult { + let key = format!("cs_{key}"); + self.get_redis_conn() + .map_err(Into::::into)? + .get_hash_field_and_deserialize(&key.into(), "csh", "ClientSecretType") + .await + .change_context(errors::StorageError::KVError) + } + + #[cfg(feature = "v2")] + async fn delete_client_secret( &self, id: &str, - ) -> CustomResult { - let ephemeral_key = self.get_ephemeral_key(id).await?; - let redis_id_key = ephemeral_key.id.generate_redis_key(); - let secret_key = ephemeral_key.generate_secret_key(); + ) -> CustomResult { + let client_secret = self.get_client_secret(id).await?; + let redis_id_key = client_secret.id.generate_redis_key(); + let secret_key = client_secret.generate_secret_key(); self.get_redis_conn() .map_err(Into::::into)? @@ -276,7 +279,7 @@ mod storage { } _ => err.change_context(errors::StorageError::KVError), })?; - Ok(ephemeral_key) + Ok(client_secret) } } } @@ -305,15 +308,6 @@ impl EphemeralKeyInterface for MockDb { Ok(ephemeral_key) } - #[cfg(feature = "v2")] - async fn create_ephemeral_key( - &self, - ek: EphemeralKeyTypeNew, - validity: i64, - ) -> CustomResult { - todo!() - } - #[cfg(feature = "v1")] async fn get_ephemeral_key( &self, @@ -333,14 +327,6 @@ impl EphemeralKeyInterface for MockDb { } } - #[cfg(feature = "v2")] - async fn get_ephemeral_key( - &self, - key: &str, - ) -> CustomResult { - todo!() - } - #[cfg(feature = "v1")] async fn delete_ephemeral_key( &self, @@ -356,12 +342,32 @@ impl EphemeralKeyInterface for MockDb { ); } } +} + +#[async_trait::async_trait] +impl ClientSecretInterface for MockDb { + #[cfg(feature = "v2")] + async fn create_client_secret( + &self, + new: ClientSecretTypeNew, + validity: i64, + ) -> CustomResult { + todo!() + } #[cfg(feature = "v2")] - async fn delete_ephemeral_key( + async fn get_client_secret( + &self, + key: &str, + ) -> CustomResult { + todo!() + } + + #[cfg(feature = "v2")] + async fn delete_client_secret( &self, id: &str, - ) -> CustomResult { + ) -> CustomResult { todo!() } } diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 3622695c9f..2dc7191863 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -7,7 +7,7 @@ use common_utils::{ types::{keymanager::KeyManagerState, theme::ThemeLineage}, }; #[cfg(feature = "v2")] -use diesel_models::ephemeral_key::{EphemeralKeyType, EphemeralKeyTypeNew}; +use diesel_models::ephemeral_key::{ClientSecretType, ClientSecretTypeNew}; use diesel_models::{ enums, enums::ProcessTrackerStatus, @@ -39,6 +39,7 @@ use time::PrimitiveDateTime; use super::{ dashboard_metadata::DashboardMetadataInterface, + ephemeral_key::ClientSecretInterface, role::RoleInterface, user::{sample_data::BatchSampleDataInterface, theme::ThemeInterface, UserInterface}, user_authentication_method::UserAuthenticationMethodInterface, @@ -50,6 +51,7 @@ use crate::services::kafka::payout::KafkaPayout; use crate::{ core::errors::{self, ProcessTrackerError}, db::{ + self, address::AddressInterface, api_keys::ApiKeyInterface, authentication::AuthenticationInterface, @@ -658,45 +660,48 @@ impl EphemeralKeyInterface for KafkaStore { self.diesel_store.create_ephemeral_key(ek, validity).await } - #[cfg(feature = "v2")] - async fn create_ephemeral_key( + #[cfg(feature = "v1")] + async fn get_ephemeral_key( &self, - ek: EphemeralKeyTypeNew, + key: &str, + ) -> CustomResult { + self.diesel_store.get_ephemeral_key(key).await + } + + #[cfg(feature = "v1")] + async fn delete_ephemeral_key( + &self, + id: &str, + ) -> CustomResult { + self.diesel_store.delete_ephemeral_key(id).await + } +} + +#[async_trait::async_trait] +impl ClientSecretInterface for KafkaStore { + #[cfg(feature = "v2")] + async fn create_client_secret( + &self, + ek: ClientSecretTypeNew, validity: i64, - ) -> CustomResult { - self.diesel_store.create_ephemeral_key(ek, validity).await - } - - #[cfg(feature = "v1")] - async fn get_ephemeral_key( - &self, - key: &str, - ) -> CustomResult { - self.diesel_store.get_ephemeral_key(key).await + ) -> CustomResult { + self.diesel_store.create_client_secret(ek, validity).await } #[cfg(feature = "v2")] - async fn get_ephemeral_key( + async fn get_client_secret( &self, key: &str, - ) -> CustomResult { - self.diesel_store.get_ephemeral_key(key).await - } - - #[cfg(feature = "v1")] - async fn delete_ephemeral_key( - &self, - id: &str, - ) -> CustomResult { - self.diesel_store.delete_ephemeral_key(id).await + ) -> CustomResult { + self.diesel_store.get_client_secret(key).await } #[cfg(feature = "v2")] - async fn delete_ephemeral_key( + async fn delete_client_secret( &self, id: &str, - ) -> CustomResult { - self.diesel_store.delete_ephemeral_key(id).await + ) -> CustomResult { + self.diesel_store.delete_client_secret(id).await } } @@ -3922,6 +3927,40 @@ impl ThemeInterface for KafkaStore { } } +#[async_trait::async_trait] +#[cfg(feature = "v2")] +impl db::payment_method_session::PaymentMethodsSessionInterface for KafkaStore { + async fn insert_payment_methods_session( + &self, + state: &KeyManagerState, + key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore, + payment_methods_session: hyperswitch_domain_models::payment_methods::PaymentMethodsSession, + validity: i64, + ) -> CustomResult<(), errors::StorageError> { + self.diesel_store + .insert_payment_methods_session(state, key_store, payment_methods_session, validity) + .await + } + + async fn get_payment_methods_session( + &self, + state: &KeyManagerState, + key_store: &hyperswitch_domain_models::merchant_key_store::MerchantKeyStore, + id: &id_type::GlobalPaymentMethodSessionId, + ) -> CustomResult< + hyperswitch_domain_models::payment_methods::PaymentMethodsSession, + errors::StorageError, + > { + self.diesel_store + .get_payment_methods_session(state, key_store, id) + .await + } +} + +#[async_trait::async_trait] +#[cfg(feature = "v1")] +impl db::payment_method_session::PaymentMethodsSessionInterface for KafkaStore {} + #[async_trait::async_trait] impl CallbackMapperInterface for KafkaStore { #[instrument(skip_all)] diff --git a/crates/router/src/db/payment_method_session.rs b/crates/router/src/db/payment_method_session.rs new file mode 100644 index 0000000000..a7e500ac52 --- /dev/null +++ b/crates/router/src/db/payment_method_session.rs @@ -0,0 +1,137 @@ +#[cfg(feature = "v2")] +use crate::core::errors::{self, CustomResult}; +use crate::db::MockDb; + +#[cfg(feature = "v2")] +#[async_trait::async_trait] +pub trait PaymentMethodsSessionInterface { + async fn insert_payment_methods_session( + &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, + validity: i64, + ) -> CustomResult<(), 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, + errors::StorageError, + >; +} + +#[cfg(feature = "v1")] +pub trait PaymentMethodsSessionInterface {} + +#[cfg(feature = "v1")] +impl PaymentMethodsSessionInterface for crate::services::Store {} + +#[cfg(feature = "v2")] +mod storage { + use error_stack::ResultExt; + use hyperswitch_domain_models::behaviour::{Conversion, ReverseConversion}; + use router_env::{instrument, tracing}; + use storage_impl::redis::kv_store::RedisConnInterface; + + use super::PaymentMethodsSessionInterface; + use crate::{ + core::errors::{self, CustomResult}, + services::Store, + }; + + #[async_trait::async_trait] + impl PaymentMethodsSessionInterface for Store { + #[instrument(skip_all)] + async fn insert_payment_methods_session( + &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, + validity_in_seconds: i64, + ) -> CustomResult<(), errors::StorageError> { + let redis_key = payment_methods_session.id.get_redis_key(); + + let db_model = payment_methods_session + .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_with_expiry(&redis_key.into(), db_model, validity_in_seconds) + .await + .change_context(errors::StorageError::KVError) + .attach_printable("Failed to insert payment methods session to redis") + } + + #[instrument(skip_all)] + 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, + errors::StorageError, + > { + let redis_key = id.get_redis_key(); + + let redis_connection = self + .get_redis_conn() + .map_err(Into::::into)?; + + let db_model = redis_connection + .get_and_deserialize_key::(&redis_key.into(), "PaymentMethodsSession") + .await + .change_context(errors::StorageError::KVError)?; + + 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 { + async fn insert_payment_methods_session( + &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, + validity_in_seconds: i64, + ) -> CustomResult<(), errors::StorageError> { + Err(errors::StorageError::MockDbError)? + } + + #[cfg(feature = "v2")] + 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, + errors::StorageError, + > { + Err(errors::StorageError::MockDbError)? + } +} + +#[cfg(feature = "v1")] +#[async_trait::async_trait] +impl PaymentMethodsSessionInterface for MockDb {} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 9febfa5e46..9b43d7f2b1 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -90,8 +90,8 @@ pub mod headers { pub const X_REDIRECT_URI: &str = "x-redirect-uri"; pub const X_TENANT_ID: &str = "x-tenant-id"; pub const X_CLIENT_SECRET: &str = "X-Client-Secret"; + pub const X_CUSTOMER_ID: &str = "X-Customer-Id"; pub const X_CONNECTED_MERCHANT_ID: &str = "x-connected-merchant-id"; - pub const X_RESOURCE_TYPE: &str = "X-Resource-Type"; } pub mod pii { @@ -151,6 +151,11 @@ pub fn mk_app( server_app = server_app.service(routes::PaymentMethods::server(state.clone())); } + #[cfg(all(feature = "v2", feature = "oltp"))] + { + server_app = server_app.service(routes::PaymentMethodsSession::server(state.clone())); + } + #[cfg(feature = "v1")] { server_app = server_app diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 22f5983e4c..80906570d7 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -63,6 +63,8 @@ pub mod relay; #[cfg(feature = "dummy_connector")] pub use self::app::DummyConnector; +#[cfg(feature = "v2")] +pub use self::app::PaymentMethodsSession; #[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 e4da0119fd..c5e1f8a108 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1016,13 +1016,6 @@ impl Customers { .route(web::delete().to(customers::customers_delete)), ) } - #[cfg(all(feature = "oltp", feature = "v2", feature = "payment_methods_v2"))] - { - route = route.service( - web::resource("/{customer_id}/saved-payment-methods") - .route(web::get().to(payment_methods::list_customer_payment_method_api)), - ); - } route } } @@ -1182,14 +1175,9 @@ impl PaymentMethods { .route(web::get().to(payment_methods::payment_method_retrieve_api)) .route(web::delete().to(payment_methods::payment_method_delete_api)), ) - .service( - web::resource("/list-enabled-payment-methods") - .route(web::get().to(payment_methods::list_payment_methods_enabled)), - ) - .service( - web::resource("/confirm-intent") - .route(web::post().to(payment_methods::confirm_payment_method_intent_api)), - ) + .service(web::resource("/list-enabled-payment-methods").route( + web::get().to(payment_methods::payment_method_session_list_payment_methods), + )) .service( web::resource("/update-saved-payment-method") .route(web::put().to(payment_methods::payment_method_update_api)), @@ -1266,6 +1254,40 @@ impl PaymentMethods { } } +#[cfg(all(feature = "v2", feature = "oltp"))] +pub struct PaymentMethodsSession; + +#[cfg(all(feature = "v2", feature = "oltp"))] +impl PaymentMethodsSession { + pub fn server(state: AppState) -> Scope { + let mut route = web::scope("/v2/payment-methods-session").app_data(web::Data::new(state)); + route = route.service( + web::resource("") + .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( + web::put().to( + payment_methods::payment_method_session_update_saved_payment_method, + ), + ), + ), + ); + + route + } +} + #[cfg(all(feature = "olap", feature = "recon", feature = "v1"))] pub struct Recon; @@ -1483,10 +1505,10 @@ impl EphemeralKey { #[cfg(feature = "v2")] impl EphemeralKey { pub fn server(config: AppState) -> Scope { - web::scope("/v2/ephemeral-keys") + web::scope("/v2/client-secret") .app_data(web::Data::new(config)) - .service(web::resource("").route(web::post().to(ephemeral_key_create))) - .service(web::resource("/{id}").route(web::delete().to(ephemeral_key_delete))) + .service(web::resource("").route(web::post().to(client_secret_create))) + .service(web::resource("/{id}").route(web::delete().to(client_secret_delete))) } } diff --git a/crates/router/src/routes/customers.rs b/crates/router/src/routes/customers.rs index 09b6983d2a..20f53c17aa 100644 --- a/crates/router/src/routes/customers.rs +++ b/crates/router/src/routes/customers.rs @@ -85,19 +85,21 @@ pub async fn customers_retrieve( req: HttpRequest, path: web::Path, ) -> HttpResponse { + use crate::services::authentication::api_or_client_auth; + let flow = Flow::CustomersRetrieve; let id = path.into_inner(); + let v2_client_auth = auth::V2ClientAuth( + common_utils::types::authentication::ResourceId::Customer(id.clone()), + ); let auth = if auth::is_jwt_auth(req.headers()) { - Box::new(auth::JWTAuth { + &auth::JWTAuth { permission: Permission::MerchantCustomerRead, - }) - } else { - match auth::is_ephemeral_auth(req.headers()) { - Ok(auth) => auth, - Err(err) => return api::log_and_return_error_response(err), } + } else { + api_or_client_auth(&auth::V2ApiKeyAuth, &v2_client_auth, req.headers()) }; Box::pin(api::server_wrap( @@ -108,7 +110,7 @@ pub async fn customers_retrieve( |state, auth: auth::AuthenticationData, id, _| { retrieve_customer(state, auth.merchant_account, auth.key_store, id) }, - &*auth, + auth, api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/routes/ephemeral_key.rs b/crates/router/src/routes/ephemeral_key.rs index 0330482e81..92b5d667da 100644 --- a/crates/router/src/routes/ephemeral_key.rs +++ b/crates/router/src/routes/ephemeral_key.rs @@ -7,7 +7,7 @@ use crate::{ services::{api, authentication as auth}, }; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +#[cfg(all(feature = "v1", not(feature = "customer_v2")))] #[instrument(skip_all, fields(flow = ?Flow::EphemeralKeyCreate))] pub async fn ephemeral_key_create( state: web::Data, @@ -34,35 +34,7 @@ pub async fn ephemeral_key_create( .await } -#[cfg(feature = "v2")] -#[instrument(skip_all, fields(flow = ?Flow::EphemeralKeyCreate))] -pub async fn ephemeral_key_create( - state: web::Data, - req: HttpRequest, - json_payload: web::Json, -) -> HttpResponse { - let flow = Flow::EphemeralKeyCreate; - let payload = json_payload.into_inner(); - api::server_wrap( - flow, - state, - &req, - payload, - |state, auth: auth::AuthenticationData, payload, _| { - helpers::make_ephemeral_key( - state, - payload.customer_id.to_owned(), - auth.merchant_account, - auth.key_store, - req.headers(), - ) - }, - &auth::HeaderAuth(auth::ApiKeyAuth), - api_locking::LockAction::NotApplicable, - ) - .await -} - +#[cfg(feature = "v1")] #[instrument(skip_all, fields(flow = ?Flow::EphemeralKeyDelete))] pub async fn ephemeral_key_delete( state: web::Data, @@ -82,3 +54,53 @@ pub async fn ephemeral_key_delete( ) .await } + +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::EphemeralKeyCreate))] +pub async fn client_secret_create( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::EphemeralKeyCreate; + let payload = json_payload.into_inner(); + api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: auth::AuthenticationData, payload, _| { + helpers::make_client_secret( + state, + payload.resource_id.to_owned(), + auth.merchant_account, + auth.key_store, + req.headers(), + ) + }, + &auth::HeaderAuth(auth::ApiKeyAuth), + api_locking::LockAction::NotApplicable, + ) + .await +} + +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::EphemeralKeyDelete))] +pub async fn client_secret_delete( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> HttpResponse { + let flow = Flow::EphemeralKeyDelete; + let payload = path.into_inner(); + api::server_wrap( + flow, + state, + &req, + payload, + |state, _: auth::AuthenticationData, req, _| helpers::delete_client_secret(state, req), + &auth::HeaderAuth(auth::ApiKeyAuth), + api_locking::LockAction::NotApplicable, + ) + .await +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index b09f92364b..ce9dda97c9 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -39,6 +39,7 @@ pub enum ApiIdentifier { ApplePayCertificatesMigration, Relay, Documentation, + PaymentMethodsSession, } impl From for ApiIdentifier { @@ -309,6 +310,10 @@ impl From for ApiIdentifier { Flow::RetrievePollStatus => Self::Poll, Flow::FeatureMatrix => Self::Documentation, + + Flow::PaymentMethodSessionCreate + | Flow::PaymentMethodSessionRetrieve + | Flow::PaymentMethodSessionUpdateSavedPaymentMethod => Self::PaymentMethodsSession, } } } diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 5f9fe729ee..86f1874ab0 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -11,12 +11,6 @@ use hyperswitch_domain_models::merchant_key_store::MerchantKeyStore; use router_env::{instrument, logger, tracing, Flow}; use super::app::{AppState, SessionState}; -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -use crate::core::payment_methods::{ - create_payment_method, delete_payment_method, list_customer_payment_method_util, - payment_method_intent_confirm, payment_method_intent_create, retrieve_payment_method, - update_payment_method, -}; use crate::{ core::{ api_locking, @@ -86,7 +80,7 @@ pub async fn create_payment_method_api( &req, json_payload.into_inner(), |state, auth: auth::AuthenticationData, req, _| async move { - Box::pin(create_payment_method( + Box::pin(payment_methods_routes::create_payment_method( &state, req, &auth.merchant_account, @@ -115,7 +109,7 @@ pub async fn create_payment_method_intent_api( &req, json_payload.into_inner(), |state, auth: auth::AuthenticationData, req, _| async move { - Box::pin(payment_method_intent_create( + Box::pin(payment_methods_routes::payment_method_intent_create( &state, req, &auth.merchant_account, @@ -123,7 +117,7 @@ pub async fn create_payment_method_intent_api( )) .await }, - &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::V2ApiKeyAuth, api_locking::LockAction::NotApplicable, )) .await @@ -134,21 +128,13 @@ pub async fn create_payment_method_intent_api( #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct PaymentMethodIntentConfirmInternal { pub id: id_type::GlobalPaymentMethodId, - pub payment_method_type: common_enums::PaymentMethod, - pub payment_method_subtype: common_enums::PaymentMethodType, - pub customer_id: Option, - pub payment_method_data: payment_methods::PaymentMethodCreateData, + pub request: payment_methods::PaymentMethodIntentConfirm, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] impl From for payment_methods::PaymentMethodIntentConfirm { fn from(item: PaymentMethodIntentConfirmInternal) -> Self { - Self { - payment_method_type: item.payment_method_type, - payment_method_subtype: item.payment_method_subtype, - customer_id: item.customer_id, - payment_method_data: item.payment_method_data.clone(), - } + item.request } } @@ -157,128 +143,39 @@ impl common_utils::events::ApiEventMetric for PaymentMethodIntentConfirmInternal fn get_api_event_type(&self) -> Option { Some(common_utils::events::ApiEventsType::PaymentMethod { payment_method_id: self.id.clone(), - payment_method_type: Some(self.payment_method_type), - payment_method_subtype: Some(self.payment_method_subtype), + payment_method_type: Some(self.request.payment_method_type), + payment_method_subtype: Some(self.request.payment_method_subtype), }) } } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -#[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsCreate))] -pub async fn confirm_payment_method_intent_api( - state: web::Data, - req: HttpRequest, - json_payload: web::Json, - path: web::Path, -) -> HttpResponse { - let flow = Flow::PaymentMethodsCreate; - let pm_id = path.into_inner(); - let payload = json_payload.into_inner(); - - let auth = match auth::is_ephemeral_or_publishible_auth(req.headers()) { - Ok(auth) => auth, - Err(e) => return api::log_and_return_error_response(e), - }; - - let inner_payload = PaymentMethodIntentConfirmInternal { - id: pm_id.to_owned(), - payment_method_type: payload.payment_method_type, - payment_method_subtype: payload.payment_method_subtype, - customer_id: payload.customer_id.to_owned(), - payment_method_data: payload.payment_method_data.clone(), - }; - - Box::pin(api::server_wrap( - flow, - state, - &req, - inner_payload, - |state, auth: auth::AuthenticationData, req, _| { - let pm_id = pm_id.clone(); - async move { - Box::pin(payment_method_intent_confirm( - &state, - req.into(), - &auth.merchant_account, - &auth.key_store, - pm_id, - )) - .await - } - }, - &*auth, - api_locking::LockAction::NotApplicable, - )) - .await -} - -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -#[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsList))] -pub async fn list_payment_methods_enabled( - state: web::Data, - req: HttpRequest, - path: web::Path, -) -> HttpResponse { - let flow = Flow::PaymentMethodsList; - let payment_method_id = path.into_inner(); - - let auth = match auth::is_ephemeral_or_publishible_auth(req.headers()) { - Ok(auth) => auth, - Err(e) => return api::log_and_return_error_response(e), - }; - - Box::pin(api::server_wrap( - flow, - state, - &req, - payment_method_id, - |state, auth: auth::AuthenticationData, payment_method_id, _| { - payment_methods_routes::list_payment_methods_enabled( - state, - auth.merchant_account, - auth.key_store, - auth.profile, - payment_method_id, - ) - }, - &*auth, - api_locking::LockAction::NotApplicable, - )) - .await -} - #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsUpdate))] pub async fn payment_method_update_api( state: web::Data, req: HttpRequest, - path: web::Path, + path: web::Path, json_payload: web::Json, ) -> HttpResponse { let flow = Flow::PaymentMethodsUpdate; let payment_method_id = path.into_inner(); let payload = json_payload.into_inner(); - let auth = match auth::is_ephemeral_or_publishible_auth(req.headers()) { - Ok(auth) => auth, - Err(e) => return api::log_and_return_error_response(e), - }; - Box::pin(api::server_wrap( flow, state, &req, payload, |state, auth: auth::AuthenticationData, req, _| { - update_payment_method( + payment_methods_routes::update_payment_method( state, auth.merchant_account, + auth.key_store, req, &payment_method_id, - auth.key_store, ) }, - &*auth, + &auth::V2ApiKeyAuth, api_locking::LockAction::NotApplicable, )) .await @@ -303,7 +200,12 @@ pub async fn payment_method_retrieve_api( &req, payload, |state, auth: auth::AuthenticationData, pm, _| { - retrieve_payment_method(state, pm, auth.key_store, auth.merchant_account) + payment_methods_routes::retrieve_payment_method( + state, + pm, + auth.key_store, + auth.merchant_account, + ) }, &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, @@ -330,7 +232,12 @@ pub async fn payment_method_delete_api( &req, payload, |state, auth: auth::AuthenticationData, pm, _| { - delete_payment_method(state, pm, auth.key_store, auth.merchant_account) + payment_methods_routes::delete_payment_method( + state, + pm, + auth.key_store, + auth.merchant_account, + ) }, &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, @@ -580,49 +487,6 @@ pub async fn list_customer_payment_method_api( .await } -#[cfg(all( - feature = "v2", - feature = "payment_methods_v2", - feature = "customer_v2" -))] -#[instrument(skip_all, fields(flow = ?Flow::CustomerPaymentMethodsList))] -pub async fn list_customer_payment_method_api( - state: web::Data, - customer_id: web::Path, - req: HttpRequest, - query_payload: web::Query, -) -> HttpResponse { - let flow = Flow::CustomerPaymentMethodsList; - let payload = query_payload.into_inner(); - let customer_id = customer_id.into_inner(); - - let ephemeral_or_api_auth = match auth::is_ephemeral_auth(req.headers()) { - Ok(auth) => auth, - Err(err) => return api::log_and_return_error_response(err), - }; - - Box::pin(api::server_wrap( - flow, - state, - &req, - payload, - |state, auth: auth::AuthenticationData, req, _| { - list_customer_payment_method_util( - state, - auth.merchant_account, - auth.profile, - auth.key_store, - Some(req), - Some(customer_id.clone()), - false, - ) - }, - &*ephemeral_or_api_auth, - api_locking::LockAction::NotApplicable, - )) - .await -} - #[cfg(all( any(feature = "v2", feature = "v1"), not(feature = "payment_methods_v2"), @@ -1022,3 +886,167 @@ impl ParentPaymentMethodToken { } } } + +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::PaymentMethodSessionCreate))] +pub async fn payment_methods_session_create( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::PaymentMethodSessionCreate; + let payload = json_payload.into_inner(); + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: auth::AuthenticationData, request, _| async move { + payment_methods_routes::payment_methods_session_create( + state, + auth.merchant_account, + auth.key_store, + request, + ) + .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( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> HttpResponse { + let flow = Flow::PaymentMethodSessionRetrieve; + let payment_method_session_id = path.into_inner(); + + Box::pin(api::server_wrap( + flow, + state, + &req, + payment_method_session_id.clone(), + |state, auth: auth::AuthenticationData, payment_method_session_id, _| async move { + payment_methods_routes::payment_methods_session_retrieve( + state, + auth.merchant_account, + auth.key_store, + payment_method_session_id, + ) + .await + }, + auth::api_or_client_auth( + &auth::V2ApiKeyAuth, + &auth::V2ClientAuth( + common_utils::types::authentication::ResourceId::PaymentMethodSession( + payment_method_session_id, + ), + ), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentMethodsList))] +pub async fn payment_method_session_list_payment_methods( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> HttpResponse { + let flow = Flow::PaymentMethodsList; + let payment_method_session_id = path.into_inner(); + + Box::pin(api::server_wrap( + flow, + state, + &req, + payment_method_session_id.clone(), + |state, auth: auth::AuthenticationData, payment_method_session_id, _| { + payment_methods_routes::list_payment_methods_for_session( + state, + auth.merchant_account, + auth.key_store, + auth.profile, + payment_method_session_id, + ) + }, + &auth::V2ClientAuth( + common_utils::types::authentication::ResourceId::PaymentMethodSession( + payment_method_session_id, + ), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "v2")] +#[derive(Clone, Debug, serde::Serialize)] +struct PaymentMethodsSessionGenericRequest { + payment_method_session_id: id_type::GlobalPaymentMethodSessionId, + #[serde(flatten)] + request: T, +} + +#[cfg(feature = "v2")] +impl common_utils::events::ApiEventMetric + for PaymentMethodsSessionGenericRequest +{ + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::PaymentMethodSession { + payment_method_session_id: self.payment_method_session_id.clone(), + }) + } +} + +#[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( + state: web::Data, + req: HttpRequest, + path: web::Path, + json_payload: web::Json< + api_models::payment_methods::PaymentMethodSessionUpdateSavedPaymentMethod, + >, +) -> HttpResponse { + let flow = Flow::PaymentMethodSessionUpdateSavedPaymentMethod; + 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, _| { + payment_methods_routes::payment_methods_session_update_payment_method( + state, + auth.merchant_account, + auth.key_store, + request.payment_method_session_id, + request.request, + ) + }, + &auth::V2ClientAuth( + common_utils::types::authentication::ResourceId::PaymentMethodSession( + payment_method_session_id, + ), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 99800b5551..edf127ac30 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -20,6 +20,8 @@ use common_utils::{date_time, id_type}; use diesel_models::ephemeral_key; use error_stack::{report, ResultExt}; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +#[cfg(feature = "v2")] +use masking::ExposeInterface; use masking::PeekInterface; use router_env::logger; use serde::Serialize; @@ -1286,6 +1288,17 @@ impl<'a> HeaderMapStruct<'a> { }) } + pub fn get_auth_string_from_header(&self) -> RouterResult<&str> { + self.headers + .get(headers::AUTHORIZATION) + .get_required_value(headers::AUTHORIZATION)? + .to_str() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: headers::AUTHORIZATION, + }) + .attach_printable("Failed to convert authorization header to string") + } + pub fn get_id_type_from_header_if_present(&self, key: &str) -> RouterResult> where T: TryFrom< @@ -1510,44 +1523,6 @@ where } } -#[cfg(feature = "v2")] -#[async_trait] -impl AuthenticateAndFetch for EphemeralKeyAuth -where - A: SessionStateInfo + Sync, -{ - async fn authenticate_and_fetch( - &self, - request_headers: &HeaderMap, - state: &A, - ) -> RouterResult<(AuthenticationData, AuthenticationType)> { - let api_key = - get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?; - let ephemeral_key = state - .store() - .get_ephemeral_key(api_key) - .await - .change_context(errors::ApiErrorResponse::Unauthorized)?; - - let resource_type = HeaderMapStruct::new(request_headers) - .get_mandatory_header_value_by_key(headers::X_RESOURCE_TYPE) - .and_then(|val| { - ephemeral_key::ResourceType::from_str(val).change_context( - errors::ApiErrorResponse::InvalidRequestData { - message: format!("`{}` header is invalid", headers::X_RESOURCE_TYPE), - }, - ) - })?; - - fp_utils::when(resource_type != ephemeral_key.resource_type, || { - Err(errors::ApiErrorResponse::Unauthorized) - })?; - - MerchantIdAuth(ephemeral_key.merchant_id) - .authenticate_and_fetch(request_headers, state) - .await - } -} #[derive(Debug)] pub struct MerchantIdAuth(pub id_type::MerchantId); @@ -1778,6 +1753,249 @@ where } } +/// Take api-key from `Authorization` header +#[cfg(feature = "v2")] +#[derive(Debug)] +pub struct V2ApiKeyAuth; + +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for V2ApiKeyAuth +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let header_map_struct = HeaderMapStruct::new(request_headers); + let auth_string = header_map_struct.get_auth_string_from_header()?; + + let api_key = auth_string + .split(',') + .find_map(|part| part.trim().strip_prefix("api-key=")) + .ok_or_else(|| { + report!(errors::ApiErrorResponse::Unauthorized) + .attach_printable("Unable to parse api_key") + })?; + if api_key.is_empty() { + return Err(errors::ApiErrorResponse::Unauthorized) + .attach_printable("API key is empty"); + } + + let profile_id = HeaderMapStruct::new(request_headers) + .get_id_type_from_header::(headers::X_PROFILE_ID)?; + + let api_key = api_keys::PlaintextApiKey::from(api_key); + let hash_key = { + let config = state.conf(); + config.api_keys.get_inner().get_hash_key()? + }; + let hashed_api_key = api_key.keyed_hash(hash_key.peek()); + + let stored_api_key = state + .store() + .find_api_key_by_hash_optional(hashed_api_key.into()) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) // If retrieve failed + .attach_printable("Failed to retrieve API key")? + .ok_or(report!(errors::ApiErrorResponse::Unauthorized)) // If retrieve returned `None` + .attach_printable("Merchant not authenticated")?; + + if stored_api_key + .expires_at + .map(|expires_at| expires_at < date_time::now()) + .unwrap_or(false) + { + return Err(report!(errors::ApiErrorResponse::Unauthorized)) + .attach_printable("API key has expired"); + } + + let key_manager_state = &(&state.session_state()).into(); + + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &stored_api_key.merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .change_context(errors::ApiErrorResponse::Unauthorized) + .attach_printable("Failed to fetch merchant key store for the merchant id")?; + + let merchant = state + .store() + .find_merchant_account_by_merchant_id( + key_manager_state, + &stored_api_key.merchant_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + // Get connected merchant account if API call is done by Platform merchant account on behalf of connected merchant account + let (merchant, platform_merchant_account) = if state.conf().platform.enabled { + get_platform_merchant_account(state, request_headers, merchant).await? + } else { + (merchant, None) + }; + + let key_store = if platform_merchant_account.is_some() { + state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + merchant.get_id(), + &state.store().get_master_key().to_vec().into(), + ) + .await + .change_context(errors::ApiErrorResponse::Unauthorized) + .attach_printable("Failed to fetch merchant key store for the merchant id")? + } else { + key_store + }; + + let profile = state + .store() + .find_business_profile_by_profile_id(key_manager_state, &key_store, &profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + + let auth = AuthenticationData { + merchant_account: merchant, + platform_merchant_account, + key_store, + profile, + }; + Ok(( + auth.clone(), + AuthenticationType::ApiKey { + merchant_id: auth.merchant_account.get_id().clone(), + key_id: stored_api_key.key_id, + }, + )) + } +} + +#[cfg(feature = "v2")] +#[derive(Debug)] +pub struct V2ClientAuth(pub common_utils::types::authentication::ResourceId); + +#[cfg(feature = "v2")] +#[async_trait] +impl AuthenticateAndFetch for V2ClientAuth +where + A: SessionStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + let header_map_struct = HeaderMapStruct::new(request_headers); + let auth_string = header_map_struct.get_auth_string_from_header()?; + + let publishable_key = auth_string + .split(',') + .find_map(|part| part.trim().strip_prefix("publishable-key=")) + .ok_or_else(|| { + report!(errors::ApiErrorResponse::Unauthorized) + .attach_printable("Unable to parse publishable_key") + })?; + + let client_secret = auth_string + .split(',') + .find_map(|part| part.trim().strip_prefix("client-secret=")) + .ok_or_else(|| { + report!(errors::ApiErrorResponse::Unauthorized) + .attach_printable("Unable to parse client_secret") + })?; + + let key_manager_state: &common_utils::types::keymanager::KeyManagerState = + &(&state.session_state()).into(); + + let db_client_secret: diesel_models::ClientSecretType = state + .store() + .get_client_secret(client_secret) + .await + .change_context(errors::ApiErrorResponse::Unauthorized) + .attach_printable("Invalid ephemeral_key")?; + + let profile_id = + get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? + .get_required_value(headers::X_PROFILE_ID)?; + + match db_client_secret.resource_id { + common_utils::types::authentication::ResourceId::Payment(global_payment_id) => { + return Err(errors::ApiErrorResponse::Unauthorized.into()) + } + common_utils::types::authentication::ResourceId::Customer(global_customer_id) => { + if global_customer_id.get_string_repr() != self.0.to_str() { + return Err(errors::ApiErrorResponse::Unauthorized.into()); + } + } + common_utils::types::authentication::ResourceId::PaymentMethodSession( + global_payment_method_session_id, + ) => { + if global_payment_method_session_id.get_string_repr() != self.0.to_str() { + return Err(errors::ApiErrorResponse::Unauthorized.into()); + } + } + }; + + let (merchant_account, key_store) = state + .store() + .find_merchant_account_by_publishable_key(key_manager_state, publishable_key) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + let merchant_id = merchant_account.get_id().clone(); + + if db_client_secret.merchant_id != merchant_id { + return Err(errors::ApiErrorResponse::Unauthorized.into()); + } + let profile = state + .store() + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + &merchant_id, + &profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + Ok(( + AuthenticationData { + merchant_account, + key_store, + profile, + platform_merchant_account: None, + }, + AuthenticationType::PublishableKey { merchant_id }, + )) + } +} + +#[cfg(feature = "v2")] +pub fn api_or_client_auth<'a, T, A>( + api_auth: &'a dyn AuthenticateAndFetch, + client_auth: &'a dyn AuthenticateAndFetch, + headers: &HeaderMap, +) -> &'a dyn AuthenticateAndFetch +where +{ + if let Ok(val) = HeaderMapStruct::new(headers).get_auth_string_from_header() { + if val.trim().starts_with("api-key=") { + api_auth + } else { + client_auth + } + } else { + api_auth + } +} + #[derive(Debug)] pub struct PublishableKeyAuth; @@ -3282,20 +3500,7 @@ where } } -pub fn is_ephemeral_or_publishible_auth( - headers: &HeaderMap, -) -> RouterResult>> { - let api_key = get_api_key(headers)?; - - if api_key.starts_with("epk") { - Ok(Box::new(EphemeralKeyAuth)) - } else if api_key.starts_with("pk_") { - Ok(Box::new(HeaderAuth(PublishableKeyAuth))) - } else { - Ok(Box::new(HeaderAuth(ApiKeyAuth))) - } -} - +#[cfg(feature = "v1")] pub fn is_ephemeral_auth( headers: &HeaderMap, ) -> RouterResult>> { @@ -3309,10 +3514,13 @@ pub fn is_ephemeral_auth( } pub fn is_jwt_auth(headers: &HeaderMap) -> bool { - headers.get(headers::AUTHORIZATION).is_some() - || get_cookie_from_header(headers) + let header_map_struct = HeaderMapStruct::new(headers); + match header_map_struct.get_auth_string_from_header() { + Ok(auth_str) => auth_str.starts_with("Bearer"), + Err(_) => get_cookie_from_header(headers) .and_then(cookies::get_jwt_from_cookies) - .is_ok() + .is_ok(), + } } pub async fn decode_jwt(token: &str, state: &impl SessionStateInfo) -> RouterResult diff --git a/crates/router/src/types/storage/ephemeral_key.rs b/crates/router/src/types/storage/ephemeral_key.rs index c4b8e2ba70..46bf185194 100644 --- a/crates/router/src/types/storage/ephemeral_key.rs +++ b/crates/router/src/types/storage/ephemeral_key.rs @@ -1,18 +1,33 @@ -pub use diesel_models::ephemeral_key::{EphemeralKey, EphemeralKeyNew}; #[cfg(feature = "v2")] -pub use diesel_models::ephemeral_key::{EphemeralKeyType, EphemeralKeyTypeNew, ResourceType}; +pub use diesel_models::ephemeral_key::{ClientSecretType, ClientSecretTypeNew}; +pub use diesel_models::ephemeral_key::{EphemeralKey, EphemeralKeyNew}; #[cfg(feature = "v2")] -use crate::types::transformers::ForeignFrom; +use crate::db::errors; #[cfg(feature = "v2")] -impl ForeignFrom for api_models::ephemeral_key::EphemeralKeyResponse { - fn foreign_from(from: EphemeralKeyType) -> Self { - Self { - customer_id: from.customer_id, - created_at: from.created_at, - expires: from.expires, - secret: from.secret, - id: from.id, +use crate::types::transformers::ForeignTryFrom; +#[cfg(feature = "v2")] +impl ForeignTryFrom for api_models::ephemeral_key::ClientSecretResponse { + type Error = errors::ApiErrorResponse; + fn foreign_try_from(from: ClientSecretType) -> Result { + match from.resource_id { + common_utils::types::authentication::ResourceId::Payment(global_payment_id) => { + Err(errors::ApiErrorResponse::InternalServerError) + } + common_utils::types::authentication::ResourceId::PaymentMethodSession( + global_payment_id, + ) => Err(errors::ApiErrorResponse::InternalServerError), + common_utils::types::authentication::ResourceId::Customer(global_customer_id) => { + Ok(Self { + resource_id: api_models::ephemeral_key::ResourceId::Customer( + global_customer_id.clone(), + ), + created_at: from.created_at, + expires: from.expires, + secret: from.secret, + id: from.id, + }) + } } } } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index acd6e59431..b88effea34 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -543,6 +543,12 @@ pub enum Flow { RelayRetrieve, /// Incoming Relay Webhook Receive IncomingRelayWebhookReceive, + /// Payment Method Session Create + PaymentMethodSessionCreate, + /// Payment Method Session Retrieve + PaymentMethodSessionRetrieve, + /// Update a saved payment method using the payment methods session + PaymentMethodSessionUpdateSavedPaymentMethod, } /// Trait for providing generic behaviour to flow metric