diff --git a/api-reference-v2/api-reference/payments/payments--create-and-confirm-intent.mdx b/api-reference-v2/api-reference/payments/payments--create-and-confirm-intent.mdx new file mode 100644 index 0000000000..1259bd33dc --- /dev/null +++ b/api-reference-v2/api-reference/payments/payments--create-and-confirm-intent.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /v2/payments +--- diff --git a/api-reference-v2/mint.json b/api-reference-v2/mint.json index d47cf7aee6..11e716d630 100644 --- a/api-reference-v2/mint.json +++ b/api-reference-v2/mint.json @@ -42,7 +42,8 @@ "api-reference/payments/payments--session-token", "api-reference/payments/payments--payment-methods-list", "api-reference/payments/payments--confirm-intent", - "api-reference/payments/payments--get" + "api-reference/payments/payments--get", + "api-reference/payments/payments--create-and-confirm-intent" ] }, { diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index bc593adb45..ebb93fd8a1 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -1886,9 +1886,7 @@ "schema": { "type": "string" }, - "example": { - "X-Profile-Id": "pro_abcdefghijklmnop" - } + "example": "pro_abcdefghijklmnop" } ], "requestBody": { @@ -1959,9 +1957,7 @@ "schema": { "type": "string" }, - "example": { - "X-Profile-Id": "pro_abcdefghijklmnop" - } + "example": "pro_abcdefghijklmnop" }, { "name": "X-Client-Secret", @@ -1994,6 +1990,7 @@ "card_number": "4242424242424242" } }, + "payment_method_subtype": "credit", "payment_method_type": "card" } } @@ -2074,6 +2071,79 @@ ] } }, + "/v2/payments": { + "post": { + "tags": [ + "Payments" + ], + "summary": "Payments - Create and Confirm Intent", + "description": "**Creates and confirms a payment intent object when the amount and payment method information are passed.**\n\nYou will require the 'API - Key' from the Hyperswitch dashboard to make the call.", + "operationId": "Create and Confirm Payment Intent", + "parameters": [ + { + "name": "X-Profile-Id", + "in": "header", + "description": "Profile ID associated to the payment intent", + "required": true, + "schema": { + "type": "string" + }, + "example": "pro_abcdefghijklmnop" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsRequest" + }, + "examples": { + "Create and confirm the payment intent with amount and card details": { + "value": { + "amount_details": { + "currency": "USD", + "order_amount": 6540 + }, + "payment_method_data": { + "card": { + "card_cvc": "123", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_number": "4242424242424242" + } + }, + "payment_method_subtype": "credit", + "payment_method_type": "card" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Payment created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/v2/payments/{payment_id}/create-external-sdk-tokens": { "post": { "tags": [ @@ -2151,9 +2221,7 @@ "schema": { "type": "string" }, - "example": { - "X-Profile-Id": "pro_abcdefghijklmnop" - } + "example": "pro_abcdefghijklmnop" }, { "name": "X-Client-Secret", @@ -2296,9 +2364,7 @@ "schema": { "type": "string" }, - "example": { - "X-Profile-Id": "pro_abcdefghijklmnop" - } + "example": "pro_abcdefghijklmnop" } ], "responses": { @@ -6819,6 +6885,20 @@ "active" ] }, + "ConnectorTokenDetails": { + "type": "object", + "description": "Token information that can be used to initiate transactions by the merchant.", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string", + "description": "A token that can be used to make payments directly with the connector.", + "example": "pm_9UhMqBMEOooRIvJFFdeW" + } + } + }, "ConnectorType": { "type": "string", "description": "Type of the Connector for the financial use case. Could range from Payments to Accounting to Banking.", @@ -13198,26 +13278,6 @@ }, "additionalProperties": false }, - "PaymentListResponse": { - "type": "object", - "required": [ - "size", - "data" - ], - "properties": { - "size": { - "type": "integer", - "description": "The number of payments included in the list", - "minimum": 0 - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentsResponse" - } - } - } - }, "PaymentMethod": { "type": "string", "description": "Indicates the type of payment method. Eg: 'card', 'wallet', etc.", @@ -14633,6 +14693,14 @@ "example": "993672945374576J", "nullable": true }, + "connector_token_details": { + "allOf": [ + { + "$ref": "#/components/schemas/ConnectorTokenDetails" + } + ], + "nullable": true + }, "merchant_connector_id": { "type": "string", "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment" @@ -14847,545 +14915,6 @@ }, "additionalProperties": false }, - "PaymentsCreateResponseOpenApi": { - "type": "object", - "required": [ - "payment_id", - "merchant_id", - "status", - "amount", - "net_amount", - "amount_capturable", - "currency", - "payment_method", - "attempt_count" - ], - "properties": { - "payment_id": { - "type": "string", - "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", - "example": "pay_mbabizu24mvu3mela5njyhpit4", - "maxLength": 30, - "minLength": 30 - }, - "merchant_id": { - "type": "string", - "description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request", - "example": "merchant_1668273825", - "maxLength": 255 - }, - "status": { - "allOf": [ - { - "$ref": "#/components/schemas/IntentStatus" - } - ], - "default": "requires_confirmation" - }, - "amount": { - "type": "integer", - "format": "int64", - "description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,", - "example": 6540 - }, - "net_amount": { - "type": "integer", - "format": "int64", - "description": "The payment net amount. net_amount = amount + surcharge_details.surcharge_amount + surcharge_details.tax_amount + shipping_cost + order_tax_amount,\nIf no surcharge_details, shipping_cost, order_tax_amount, net_amount = amount", - "example": 6540 - }, - "shipping_cost": { - "type": "integer", - "format": "int64", - "description": "The shipping cost for the payment.", - "example": 6540, - "nullable": true - }, - "amount_capturable": { - "type": "integer", - "format": "int64", - "description": "The maximum amount that could be captured from the payment", - "example": 6540, - "minimum": 100 - }, - "amount_received": { - "type": "integer", - "format": "int64", - "description": "The amount which is already captured from the payment, this helps in the cases where merchants can't capture all capturable amount at once.", - "example": 6540, - "nullable": true - }, - "connector": { - "type": "string", - "description": "The connector used for the payment", - "example": "stripe", - "nullable": true - }, - "client_secret": { - "type": "string", - "description": "It's a token used for client side verification.", - "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo", - "nullable": true - }, - "created": { - "type": "string", - "format": "date-time", - "description": "Time when the payment was created", - "example": "2022-09-10T10:11:12Z", - "nullable": true - }, - "currency": { - "$ref": "#/components/schemas/Currency" - }, - "customer_id": { - "type": "string", - "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.\nThis field will be deprecated soon. Please refer to `customer.id`", - "deprecated": true, - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "nullable": true, - "maxLength": 64, - "minLength": 1 - }, - "description": { - "type": "string", - "description": "A description of the payment", - "example": "It's my first payment request", - "nullable": true - }, - "refunds": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RefundResponse" - }, - "description": "List of refunds that happened on this intent, as same payment intent can have multiple refund requests depending on the nature of order", - "nullable": true - }, - "disputes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DisputeResponsePaymentsRetrieve" - }, - "description": "List of disputes that happened on this intent", - "nullable": true - }, - "attempts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentAttemptResponse" - }, - "description": "List of attempts that happened on this intent", - "nullable": true - }, - "captures": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaptureResponse" - }, - "description": "List of captures done on latest attempt", - "nullable": true - }, - "mandate_id": { - "type": "string", - "description": "A unique identifier to link the payment to a mandate, can be used instead of payment_method_data, in case of setting up recurring payments", - "example": "mandate_iwer89rnjef349dni3", - "nullable": true, - "maxLength": 255 - }, - "mandate_data": { - "allOf": [ - { - "$ref": "#/components/schemas/MandateData" - } - ], - "nullable": true - }, - "setup_future_usage": { - "allOf": [ - { - "$ref": "#/components/schemas/FutureUsage" - } - ], - "nullable": true - }, - "off_session": { - "type": "boolean", - "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with confirm=true.", - "example": true, - "nullable": true - }, - "capture_method": { - "allOf": [ - { - "$ref": "#/components/schemas/CaptureMethod" - } - ], - "nullable": true - }, - "payment_method": { - "$ref": "#/components/schemas/PaymentMethod" - }, - "payment_method_data": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodDataResponseWithBilling" - } - ], - "nullable": true - }, - "payment_token": { - "type": "string", - "description": "Provide a reference to a stored payment method", - "example": "187282ab-40ef-47a9-9206-5099ba31e432", - "nullable": true - }, - "shipping": { - "allOf": [ - { - "$ref": "#/components/schemas/Address" - } - ], - "nullable": true - }, - "billing": { - "allOf": [ - { - "$ref": "#/components/schemas/Address" - } - ], - "nullable": true - }, - "order_details": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrderDetailsWithAmount" - }, - "description": "Information about the product , quantity and amount for connectors. (e.g. Klarna)", - "example": "[{\n \"product_name\": \"gillete creme\",\n \"quantity\": 15,\n \"amount\" : 900\n }]", - "nullable": true - }, - "email": { - "type": "string", - "description": "description: The customer's email address\nThis field will be deprecated soon. Please refer to `customer.email` object", - "deprecated": true, - "example": "johntest@test.com", - "nullable": true, - "maxLength": 255 - }, - "name": { - "type": "string", - "description": "description: The customer's name\nThis field will be deprecated soon. Please refer to `customer.name` object", - "deprecated": true, - "example": "John Test", - "nullable": true, - "maxLength": 255 - }, - "phone": { - "type": "string", - "description": "The customer's phone number\nThis field will be deprecated soon. Please refer to `customer.phone` object", - "deprecated": true, - "example": "9123456789", - "nullable": true, - "maxLength": 255 - }, - "return_url": { - "type": "string", - "description": "The URL to redirect after the completion of the operation", - "example": "https://hyperswitch.io", - "nullable": true - }, - "authentication_type": { - "allOf": [ - { - "$ref": "#/components/schemas/AuthenticationType" - } - ], - "default": "three_ds", - "nullable": true - }, - "statement_descriptor_name": { - "type": "string", - "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", - "example": "Hyperswitch Router", - "nullable": true, - "maxLength": 255 - }, - "statement_descriptor_suffix": { - "type": "string", - "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 255 characters for the concatenated descriptor.", - "example": "Payment for shoes purchase", - "nullable": true, - "maxLength": 255 - }, - "next_action": { - "allOf": [ - { - "$ref": "#/components/schemas/NextActionData" - } - ], - "nullable": true - }, - "cancellation_reason": { - "type": "string", - "description": "If the payment was cancelled the reason will be provided here", - "nullable": true - }, - "error_code": { - "type": "string", - "description": "If there was an error while calling the connectors the code is received here", - "example": "E0001", - "nullable": true - }, - "error_message": { - "type": "string", - "description": "If there was an error while calling the connector the error message is received here", - "example": "Failed while verifying the card", - "nullable": true - }, - "payment_experience": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentExperience" - } - ], - "nullable": true - }, - "payment_method_type": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodType" - } - ], - "nullable": true - }, - "connector_label": { - "type": "string", - "description": "The connector used for this payment along with the country and business details", - "example": "stripe_US_food", - "nullable": true - }, - "business_country": { - "allOf": [ - { - "$ref": "#/components/schemas/CountryAlpha2" - } - ], - "nullable": true - }, - "business_label": { - "type": "string", - "description": "The business label of merchant for this payment", - "nullable": true - }, - "business_sub_label": { - "type": "string", - "description": "The business_sub_label for this payment", - "nullable": true - }, - "allowed_payment_method_types": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "description": "Allowed Payment Method Types for a given PaymentIntent", - "nullable": true - }, - "ephemeral_key": { - "allOf": [ - { - "$ref": "#/components/schemas/EphemeralKeyCreateResponse" - } - ], - "nullable": true - }, - "manual_retry_allowed": { - "type": "boolean", - "description": "If true the payment can be retried with same or different payment method which means the confirm call can be made again.", - "nullable": true - }, - "connector_transaction_id": { - "type": "string", - "description": "A unique identifier for a payment provided by the connector", - "example": "993672945374576J", - "nullable": true - }, - "frm_message": { - "allOf": [ - { - "$ref": "#/components/schemas/FrmMessage" - } - ], - "nullable": true - }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - }, - "connector_metadata": { - "allOf": [ - { - "$ref": "#/components/schemas/ConnectorMetadata" - } - ], - "nullable": true - }, - "feature_metadata": { - "allOf": [ - { - "$ref": "#/components/schemas/FeatureMetadata" - } - ], - "nullable": true - }, - "reference_id": { - "type": "string", - "description": "reference(Identifier) to the payment at connector side", - "example": "993672945374576J", - "nullable": true - }, - "payment_link": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentLinkResponse" - } - ], - "nullable": true - }, - "profile_id": { - "type": "string", - "description": "The business profile that is associated with this payment", - "nullable": true - }, - "surcharge_details": { - "allOf": [ - { - "$ref": "#/components/schemas/RequestSurchargeDetails" - } - ], - "nullable": true - }, - "attempt_count": { - "type": "integer", - "format": "int32", - "description": "Total number of attempts associated with this payment" - }, - "merchant_decision": { - "type": "string", - "description": "Denotes the action(approve or reject) taken by merchant in case of manual review. Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment", - "nullable": true - }, - "merchant_connector_id": { - "type": "string", - "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", - "nullable": true - }, - "incremental_authorization_allowed": { - "type": "boolean", - "description": "If true, incremental authorization can be performed on this payment, in case the funds authorized initially fall short.", - "nullable": true - }, - "authorization_count": { - "type": "integer", - "format": "int32", - "description": "Total number of authorizations happened in an incremental_authorization payment", - "nullable": true - }, - "incremental_authorizations": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IncrementalAuthorizationResponse" - }, - "description": "List of incremental authorizations happened to the payment", - "nullable": true - }, - "external_authentication_details": { - "allOf": [ - { - "$ref": "#/components/schemas/ExternalAuthenticationDetailsResponse" - } - ], - "nullable": true - }, - "external_3ds_authentication_attempted": { - "type": "boolean", - "description": "Flag indicating if external 3ds authentication is made or not", - "nullable": true - }, - "expires_on": { - "type": "string", - "format": "date-time", - "description": "Date Time for expiry of the payment", - "example": "2022-09-10T10:11:12Z", - "nullable": true - }, - "fingerprint": { - "type": "string", - "description": "Payment Fingerprint, to identify a particular card.\nIt is a 20 character long alphanumeric code.", - "nullable": true - }, - "browser_info": { - "allOf": [ - { - "$ref": "#/components/schemas/BrowserInformation" - } - ], - "nullable": true - }, - "payment_method_id": { - "type": "string", - "description": "Identifier for Payment Method used for the payment", - "nullable": true - }, - "payment_method_status": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodStatus" - } - ], - "nullable": true - }, - "updated": { - "type": "string", - "format": "date-time", - "description": "Date time at which payment was updated", - "example": "2022-09-10T10:11:12Z", - "nullable": true - }, - "split_payments": { - "allOf": [ - { - "$ref": "#/components/schemas/SplitPaymentsResponse" - } - ], - "nullable": true - }, - "frm_metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. FRM Metadata is useful for storing additional, structured information on an object related to FRM.", - "nullable": true - }, - "merchant_order_reference_id": { - "type": "string", - "description": "Merchant's identifier for the payment/invoice. This will be sent to the connector\nif the connector provides support to accept multiple reference ids.\nIn case the connector supports only one reference id, Hyperswitch's Payment ID will be sent as reference.", - "example": "Custom_Order_id_123", - "nullable": true, - "maxLength": 255 - }, - "order_tax_amount": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "connector_mandate_id": { - "type": "string", - "description": "Connector Identifier for the payment method", - "nullable": true - } - } - }, "PaymentsDynamicTaxCalculationRequest": { "type": "object", "required": [ @@ -15722,185 +15251,30 @@ }, "additionalProperties": false }, - "PaymentsResponse": { + "PaymentsRequest": { "type": "object", "required": [ - "payment_id", - "merchant_id", - "status", - "amount", - "net_amount", - "amount_capturable", - "currency", - "payment_method", - "attempt_count" + "amount_details", + "customer_id", + "payment_method_data", + "payment_method_type", + "payment_method_subtype" ], "properties": { - "payment_id": { + "amount_details": { + "$ref": "#/components/schemas/AmountDetails" + }, + "merchant_reference_id": { "type": "string", "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", "example": "pay_mbabizu24mvu3mela5njyhpit4", + "nullable": true, "maxLength": 30, "minLength": 30 }, - "merchant_id": { + "routing_algorithm_id": { "type": "string", - "description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request", - "example": "merchant_1668273825", - "maxLength": 255 - }, - "status": { - "allOf": [ - { - "$ref": "#/components/schemas/IntentStatus" - } - ], - "default": "requires_confirmation" - }, - "amount": { - "type": "integer", - "format": "int64", - "description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,", - "example": 6540 - }, - "net_amount": { - "type": "integer", - "format": "int64", - "description": "The payment net amount. net_amount = amount + surcharge_details.surcharge_amount + surcharge_details.tax_amount + shipping_cost + order_tax_amount,\nIf no surcharge_details, shipping_cost, order_tax_amount, net_amount = amount", - "example": 6540 - }, - "shipping_cost": { - "type": "integer", - "format": "int64", - "description": "The shipping cost for the payment.", - "example": 6540, - "nullable": true - }, - "amount_capturable": { - "type": "integer", - "format": "int64", - "description": "The maximum amount that could be captured from the payment", - "example": 6540, - "minimum": 100 - }, - "amount_received": { - "type": "integer", - "format": "int64", - "description": "The amount which is already captured from the payment, this helps in the cases where merchants can't capture all capturable amount at once.", - "example": 6540, - "nullable": true - }, - "connector": { - "type": "string", - "description": "The connector used for the payment", - "example": "stripe", - "nullable": true - }, - "client_secret": { - "type": "string", - "description": "It's a token used for client side verification.", - "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo", - "nullable": true - }, - "created": { - "type": "string", - "format": "date-time", - "description": "Time when the payment was created", - "example": "2022-09-10T10:11:12Z", - "nullable": true - }, - "currency": { - "$ref": "#/components/schemas/Currency" - }, - "customer_id": { - "type": "string", - "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.\nThis field will be deprecated soon. Please refer to `customer.id`", - "deprecated": true, - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "nullable": true, - "maxLength": 64, - "minLength": 1 - }, - "customer": { - "allOf": [ - { - "$ref": "#/components/schemas/CustomerDetailsResponse" - } - ], - "nullable": true - }, - "description": { - "type": "string", - "description": "A description of the payment", - "example": "It's my first payment request", - "nullable": true - }, - "refunds": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RefundResponse" - }, - "description": "List of refunds that happened on this intent, as same payment intent can have multiple refund requests depending on the nature of order", - "nullable": true - }, - "disputes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DisputeResponsePaymentsRetrieve" - }, - "description": "List of disputes that happened on this intent", - "nullable": true - }, - "attempts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentAttemptResponse" - }, - "description": "List of attempts that happened on this intent", - "nullable": true - }, - "captures": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaptureResponse" - }, - "description": "List of captures done on latest attempt", - "nullable": true - }, - "mandate_id": { - "type": "string", - "description": "A unique identifier to link the payment to a mandate, can be used instead of payment_method_data, in case of setting up recurring payments", - "example": "mandate_iwer89rnjef349dni3", - "nullable": true, - "maxLength": 255 - }, - "mandate_data": { - "allOf": [ - { - "$ref": "#/components/schemas/MandateData" - } - ], - "nullable": true - }, - "setup_future_usage": { - "allOf": [ - { - "$ref": "#/components/schemas/FutureUsage" - } - ], - "nullable": true - }, - "off_session": { - "type": "boolean", - "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with confirm=true.", - "example": true, - "nullable": true - }, - "capture_on": { - "type": "string", - "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the payment should be captured.\nProviding this field will automatically set `capture` to true", - "example": "2022-09-10T10:11:12Z", + "description": "The routing algorithm id to be used for the payment", "nullable": true }, "capture_method": { @@ -15911,29 +15285,13 @@ ], "nullable": true }, - "payment_method": { - "$ref": "#/components/schemas/PaymentMethod" - }, - "payment_method_data": { + "authentication_type": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodDataResponseWithBilling" - } - ], - "nullable": true - }, - "payment_token": { - "type": "string", - "description": "Provide a reference to a stored payment method", - "example": "187282ab-40ef-47a9-9206-5099ba31e432", - "nullable": true - }, - "shipping": { - "allOf": [ - { - "$ref": "#/components/schemas/Address" + "$ref": "#/components/schemas/AuthenticationType" } ], + "default": "no_three_ds", "nullable": true }, "billing": { @@ -15944,141 +15302,71 @@ ], "nullable": true }, + "shipping": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], + "nullable": true + }, + "customer_id": { + "type": "string", + "description": "The identifier for the customer", + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", + "maxLength": 64, + "minLength": 32 + }, + "customer_present": { + "allOf": [ + { + "$ref": "#/components/schemas/PresenceOfCustomerDuringPayment" + } + ], + "nullable": true + }, + "description": { + "type": "string", + "description": "A description for the payment", + "example": "It's my first payment request", + "nullable": true + }, + "return_url": { + "type": "string", + "description": "The URL to which you want the user to be redirected after the completion of the payment operation", + "example": "https://hyperswitch.io", + "nullable": true + }, + "setup_future_usage": { + "allOf": [ + { + "$ref": "#/components/schemas/FutureUsage" + } + ], + "nullable": true + }, + "apply_mit_exemption": { + "allOf": [ + { + "$ref": "#/components/schemas/MitExemptionRequest" + } + ], + "nullable": true + }, + "statement_descriptor": { + "type": "string", + "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", + "example": "Hyperswitch Router", + "nullable": true, + "maxLength": 22 + }, "order_details": { "type": "array", "items": { "$ref": "#/components/schemas/OrderDetailsWithAmount" }, - "description": "Information about the product , quantity and amount for connectors. (e.g. Klarna)", - "example": "[{\n \"product_name\": \"gillete creme\",\n \"quantity\": 15,\n \"amount\" : 900\n }]", - "nullable": true - }, - "email": { - "type": "string", - "description": "description: The customer's email address\nThis field will be deprecated soon. Please refer to `customer.email` object", - "deprecated": true, - "example": "johntest@test.com", - "nullable": true, - "maxLength": 255 - }, - "name": { - "type": "string", - "description": "description: The customer's name\nThis field will be deprecated soon. Please refer to `customer.name` object", - "deprecated": true, - "example": "John Test", - "nullable": true, - "maxLength": 255 - }, - "phone": { - "type": "string", - "description": "The customer's phone number\nThis field will be deprecated soon. Please refer to `customer.phone` object", - "deprecated": true, - "example": "9123456789", - "nullable": true, - "maxLength": 255 - }, - "return_url": { - "type": "string", - "description": "The URL to redirect after the completion of the operation", - "example": "https://hyperswitch.io", - "nullable": true - }, - "authentication_type": { - "allOf": [ - { - "$ref": "#/components/schemas/AuthenticationType" - } - ], - "default": "three_ds", - "nullable": true - }, - "statement_descriptor_name": { - "type": "string", - "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", - "example": "Hyperswitch Router", - "nullable": true, - "maxLength": 255 - }, - "statement_descriptor_suffix": { - "type": "string", - "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 255 characters for the concatenated descriptor.", - "example": "Payment for shoes purchase", - "nullable": true, - "maxLength": 255 - }, - "next_action": { - "allOf": [ - { - "$ref": "#/components/schemas/NextActionData" - } - ], - "nullable": true - }, - "cancellation_reason": { - "type": "string", - "description": "If the payment was cancelled the reason will be provided here", - "nullable": true - }, - "error_code": { - "type": "string", - "description": "If there was an error while calling the connectors the code is received here", - "example": "E0001", - "nullable": true - }, - "error_message": { - "type": "string", - "description": "If there was an error while calling the connector the error message is received here", - "example": "Failed while verifying the card", - "nullable": true - }, - "unified_code": { - "type": "string", - "description": "error code unified across the connectors is received here if there was an error while calling connector", - "nullable": true - }, - "unified_message": { - "type": "string", - "description": "error message unified across the connectors is received here if there was an error while calling connector", - "nullable": true - }, - "payment_experience": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentExperience" - } - ], - "nullable": true - }, - "payment_method_type": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodType" - } - ], - "nullable": true - }, - "connector_label": { - "type": "string", - "description": "The connector used for this payment along with the country and business details", - "example": "stripe_US_food", - "nullable": true - }, - "business_country": { - "allOf": [ - { - "$ref": "#/components/schemas/CountryAlpha2" - } - ], - "nullable": true - }, - "business_label": { - "type": "string", - "description": "The business label of merchant for this payment", - "nullable": true - }, - "business_sub_label": { - "type": "string", - "description": "The business_sub_label for this payment", + "description": "Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount", + "example": "[{\n \"product_name\": \"Apple iPhone 16\",\n \"quantity\": 1,\n \"amount\" : 69000\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", "nullable": true }, "allowed_payment_method_types": { @@ -16086,39 +15374,12 @@ "items": { "$ref": "#/components/schemas/PaymentMethodType" }, - "description": "Allowed Payment Method Types for a given PaymentIntent", - "nullable": true - }, - "ephemeral_key": { - "allOf": [ - { - "$ref": "#/components/schemas/EphemeralKeyCreateResponse" - } - ], - "nullable": true - }, - "manual_retry_allowed": { - "type": "boolean", - "description": "If true the payment can be retried with same or different payment method which means the confirm call can be made again.", - "nullable": true - }, - "connector_transaction_id": { - "type": "string", - "description": "A unique identifier for a payment provided by the connector", - "example": "993672945374576J", - "nullable": true - }, - "frm_message": { - "allOf": [ - { - "$ref": "#/components/schemas/FrmMessage" - } - ], + "description": "Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent", "nullable": true }, "metadata": { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true }, "connector_metadata": { @@ -16137,90 +15398,66 @@ ], "nullable": true }, - "reference_id": { - "type": "string", - "description": "reference(Identifier) to the payment at connector side", - "example": "993672945374576J", - "nullable": true - }, - "payment_link": { + "payment_link_enabled": { "allOf": [ { - "$ref": "#/components/schemas/PaymentLinkResponse" + "$ref": "#/components/schemas/EnablePaymentLinkRequest" } ], "nullable": true }, - "profile_id": { - "type": "string", - "description": "The business profile that is associated with this payment", - "nullable": true - }, - "surcharge_details": { + "payment_link_config": { "allOf": [ { - "$ref": "#/components/schemas/RequestSurchargeDetails" + "$ref": "#/components/schemas/PaymentLinkConfigRequest" } ], "nullable": true }, - "attempt_count": { + "request_incremental_authorization": { + "allOf": [ + { + "$ref": "#/components/schemas/RequestIncrementalAuthorization" + } + ], + "nullable": true + }, + "session_expiry": { "type": "integer", "format": "int32", - "description": "Total number of attempts associated with this payment" + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds, if not sent it will be taken from profile config\n(900) for 15 mins", + "example": 900, + "nullable": true, + "minimum": 0 }, - "merchant_decision": { - "type": "string", - "description": "Denotes the action(approve or reject) taken by merchant in case of manual review. Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment", + "frm_metadata": { + "type": "object", + "description": "Additional data related to some frm(Fraud Risk Management) connectors", "nullable": true }, - "merchant_connector_id": { - "type": "string", - "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", - "nullable": true - }, - "incremental_authorization_allowed": { - "type": "boolean", - "description": "If true, incremental authorization can be performed on this payment, in case the funds authorized initially fall short.", - "nullable": true - }, - "authorization_count": { - "type": "integer", - "format": "int32", - "description": "Total number of authorizations happened in an incremental_authorization payment", - "nullable": true - }, - "incremental_authorizations": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IncrementalAuthorizationResponse" - }, - "description": "List of incremental authorizations happened to the payment", - "nullable": true - }, - "external_authentication_details": { + "request_external_three_ds_authentication": { "allOf": [ { - "$ref": "#/components/schemas/ExternalAuthenticationDetailsResponse" + "$ref": "#/components/schemas/External3dsAuthenticationRequest" } ], "nullable": true }, - "external_3ds_authentication_attempted": { - "type": "boolean", - "description": "Flag indicating if external 3ds authentication is made or not", - "nullable": true + "payment_method_data": { + "$ref": "#/components/schemas/PaymentMethodDataRequest" }, - "expires_on": { - "type": "string", - "format": "date-time", - "description": "Date Time for expiry of the payment", - "example": "2022-09-10T10:11:12Z", - "nullable": true + "payment_method_type": { + "$ref": "#/components/schemas/PaymentMethod" }, - "fingerprint": { - "type": "string", - "description": "Payment Fingerprint, to identify a particular card.\nIt is a 20 character long alphanumeric code.", + "payment_method_subtype": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "customer_acceptance": { + "allOf": [ + { + "$ref": "#/components/schemas/CustomerAcceptance" + } + ], "nullable": true }, "browser_info": { @@ -16230,58 +15467,120 @@ } ], "nullable": true - }, - "payment_method_id": { + } + }, + "additionalProperties": false + }, + "PaymentsResponse": { + "type": "object", + "required": [ + "id", + "status", + "amount", + "customer_id", + "connector", + "client_secret", + "created", + "payment_method_type", + "payment_method_subtype", + "merchant_connector_id" + ], + "properties": { + "id": { "type": "string", - "description": "Identifier for Payment Method used for the payment", - "nullable": true + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", + "example": "12345_pay_01926c58bc6e77c09e809964e72af8c8", + "maxLength": 64, + "minLength": 32 }, - "payment_method_status": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodStatus" - } - ], - "nullable": true + "status": { + "$ref": "#/components/schemas/IntentStatus" }, - "updated": { + "amount": { + "$ref": "#/components/schemas/PaymentAmountDetailsResponse" + }, + "customer_id": { + "type": "string", + "description": "The identifier for the customer", + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", + "maxLength": 64, + "minLength": 32 + }, + "connector": { + "type": "string", + "description": "The connector used for the payment", + "example": "stripe" + }, + "client_secret": { + "type": "string", + "description": "It's a token used for client side verification." + }, + "created": { "type": "string", "format": "date-time", - "description": "Date time at which payment was updated", - "example": "2022-09-10T10:11:12Z", - "nullable": true + "description": "Time when the payment was created", + "example": "2022-09-10T10:11:12Z" }, - "split_payments": { + "payment_method_data": { "allOf": [ { - "$ref": "#/components/schemas/SplitPaymentsResponse" + "$ref": "#/components/schemas/PaymentMethodDataResponseWithBilling" } ], "nullable": true }, - "frm_metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. FRM Metadata is useful for storing additional, structured information on an object related to FRM.", - "nullable": true + "payment_method_type": { + "$ref": "#/components/schemas/PaymentMethod" }, - "merchant_order_reference_id": { - "type": "string", - "description": "Merchant's identifier for the payment/invoice. This will be sent to the connector\nif the connector provides support to accept multiple reference ids.\nIn case the connector supports only one reference id, Hyperswitch's Payment ID will be sent as reference.", - "example": "Custom_Order_id_123", - "nullable": true, - "maxLength": 255 + "payment_method_subtype": { + "$ref": "#/components/schemas/PaymentMethodType" }, - "order_tax_amount": { + "next_action": { "allOf": [ { - "$ref": "#/components/schemas/MinorUnit" + "$ref": "#/components/schemas/NextActionData" } ], "nullable": true }, - "connector_mandate_id": { + "connector_transaction_id": { "type": "string", - "description": "Connector Identifier for the payment method", + "description": "A unique identifier for a payment provided by the connector", + "example": "993672945374576J", + "nullable": true + }, + "connector_reference_id": { + "type": "string", + "description": "reference(Identifier) to the payment at connector side", + "example": "993672945374576J", + "nullable": true + }, + "connector_token_details": { + "allOf": [ + { + "$ref": "#/components/schemas/ConnectorTokenDetails" + } + ], + "nullable": true + }, + "merchant_connector_id": { + "type": "string", + "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment" + }, + "browser_info": { + "allOf": [ + { + "$ref": "#/components/schemas/BrowserInformation" + } + ], + "nullable": true + }, + "error": { + "allOf": [ + { + "$ref": "#/components/schemas/ErrorDetails" + } + ], "nullable": true } } diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index e6de6190b8..c365445e10 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -3,13 +3,15 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; #[cfg(feature = "v2")] use super::{ PaymentStartRedirectionRequest, PaymentsConfirmIntentResponse, PaymentsCreateIntentRequest, - PaymentsGetIntentRequest, PaymentsIntentResponse, + PaymentsGetIntentRequest, PaymentsIntentResponse, PaymentsRequest, }; #[cfg(all( any(feature = "v2", feature = "v1"), not(feature = "payment_methods_v2") ))] use crate::payment_methods::CustomerPaymentMethodsListResponse; +#[cfg(feature = "v1")] +use crate::payments::{PaymentListResponse, PaymentListResponseV2}; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use crate::{events, payment_methods::CustomerPaymentMethodsListResponse}; use crate::{ @@ -23,14 +25,14 @@ use crate::{ payments::{ self, ExtendedCardInfoResponse, PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, PaymentListFiltersV2, - PaymentListResponse, PaymentListResponseV2, PaymentsAggregateResponse, - PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, - PaymentsCompleteAuthorizeRequest, PaymentsDynamicTaxCalculationRequest, - PaymentsDynamicTaxCalculationResponse, PaymentsExternalAuthenticationRequest, - PaymentsExternalAuthenticationResponse, PaymentsIncrementalAuthorizationRequest, - PaymentsManualUpdateRequest, PaymentsManualUpdateResponse, - PaymentsPostSessionTokensRequest, PaymentsPostSessionTokensResponse, PaymentsRejectRequest, - PaymentsResponse, PaymentsRetrieveRequest, PaymentsSessionResponse, PaymentsStartRequest, + PaymentsAggregateResponse, PaymentsApproveRequest, PaymentsCancelRequest, + PaymentsCaptureRequest, PaymentsCompleteAuthorizeRequest, + PaymentsDynamicTaxCalculationRequest, PaymentsDynamicTaxCalculationResponse, + PaymentsExternalAuthenticationRequest, PaymentsExternalAuthenticationResponse, + PaymentsIncrementalAuthorizationRequest, PaymentsManualUpdateRequest, + PaymentsManualUpdateResponse, PaymentsPostSessionTokensRequest, + PaymentsPostSessionTokensResponse, PaymentsRejectRequest, PaymentsResponse, + PaymentsRetrieveRequest, PaymentsSessionResponse, PaymentsStartRequest, RedirectionResponse, }, }; @@ -150,6 +152,22 @@ impl ApiEventMetric for PaymentsCreateIntentRequest { } } +#[cfg(feature = "v2")] +impl ApiEventMetric for PaymentsRequest { + fn get_api_event_type(&self) -> Option { + None + } +} + +#[cfg(feature = "v2")] +impl ApiEventMetric for PaymentsResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.id.clone(), + }) + } +} + #[cfg(feature = "v2")] impl ApiEventMetric for PaymentsGetIntentRequest { fn get_api_event_type(&self) -> Option { @@ -355,12 +373,14 @@ impl ApiEventMetric for PaymentListConstraints { } } +#[cfg(feature = "v1")] impl ApiEventMetric for PaymentListResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::ResourceListAPI) } } +#[cfg(feature = "v1")] impl ApiEventMetric for PaymentListResponseV2 { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::ResourceListAPI) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 3a6fe000af..4a1fa8b408 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -4414,6 +4414,7 @@ pub struct ReceiverDetails { amount_remaining: Option, } +#[cfg(feature = "v1")] #[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema, router_derive::PolymorphicSchema)] #[generate_schemas(PaymentsCreateResponseOpenApi)] pub struct PaymentsResponse { @@ -4787,6 +4788,271 @@ pub struct PaymentsConfirmIntentRequest { pub browser_info: Option, } +// This struct contains the union of fields in `PaymentsCreateIntentRequest` and +// `PaymentsConfirmIntentRequest` +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ToSchema)] +#[serde(deny_unknown_fields)] +#[cfg(feature = "v2")] +pub struct PaymentsRequest { + /// The amount details for the payment + pub amount_details: AmountDetails, + + /// Unique identifier for the payment. This ensures idempotency for multiple payments + /// that have been done by a single merchant. + #[schema( + value_type = Option, + min_length = 30, + max_length = 30, + example = "pay_mbabizu24mvu3mela5njyhpit4" + )] + pub merchant_reference_id: Option, + + /// The routing algorithm id to be used for the payment + #[schema(value_type = Option)] + pub routing_algorithm_id: Option, + + #[schema(value_type = Option, example = "automatic")] + pub capture_method: Option, + + #[schema(value_type = Option, example = "no_three_ds", default = "no_three_ds")] + pub authentication_type: Option, + + /// The billing details of the payment. This address will be used for invoicing. + pub billing: Option
, + + /// The shipping address for the payment + pub shipping: Option
, + + /// The identifier for the customer + #[schema( + min_length = 32, + max_length = 64, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", + value_type = String + )] + pub customer_id: Option, + + /// Set to `present` to indicate that the customer is in your checkout flow during this payment, and therefore is able to authenticate. This parameter should be `absent` when merchant's doing merchant initiated payments and customer is not present while doing the payment. + #[schema(example = "present", value_type = Option)] + pub customer_present: Option, + + /// A description for the payment + #[schema(example = "It's my first payment request", value_type = Option)] + pub description: Option, + + /// The URL to which you want the user to be redirected after the completion of the payment operation + #[schema(value_type = Option, example = "https://hyperswitch.io")] + pub return_url: Option, + + #[schema(value_type = Option, example = "off_session")] + pub setup_future_usage: Option, + + /// Apply MIT exemption for a payment + #[schema(value_type = Option)] + pub apply_mit_exemption: Option, + + /// For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters. + #[schema(max_length = 22, example = "Hyperswitch Router", value_type = Option)] + pub statement_descriptor: Option, + + /// Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount + #[schema(value_type = Option>, example = r#"[{ + "product_name": "Apple iPhone 16", + "quantity": 1, + "amount" : 69000 + "product_img_link" : "https://dummy-img-link.com" + }]"#)] + pub order_details: Option>, + + /// Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent + #[schema(value_type = Option>)] + pub allowed_payment_method_types: Option>, + + /// Metadata is useful for storing additional, unstructured information on an object. + #[schema(value_type = Option, example = r#"{ "udf1": "some-value", "udf2": "some-value" }"#)] + pub metadata: Option, + + /// Some connectors like Apple pay, Airwallex and Noon might require some additional information, find specific details in the child attributes below. + pub connector_metadata: Option, + + /// Additional data that might be required by hyperswitch based on the requested features by the merchants. + pub feature_metadata: Option, + + /// Whether to generate the payment link for this payment or not (if applicable) + #[schema(value_type = Option)] + pub payment_link_enabled: Option, + + /// Configure a custom payment link for the particular payment + #[schema(value_type = Option)] + pub payment_link_config: Option, + + ///Request an incremental authorization, i.e., increase the authorized amount on a confirmed payment before you capture it. + #[schema(value_type = Option)] + pub request_incremental_authorization: Option, + + ///Will be used to expire client secret after certain amount of time to be supplied in seconds, if not sent it will be taken from profile config + ///(900) for 15 mins + #[schema(example = 900)] + pub session_expiry: Option, + + /// Additional data related to some frm(Fraud Risk Management) connectors + #[schema(value_type = Option, example = r#"{ "coverage_request" : "fraud", "fulfillment_method" : "delivery" }"#)] + pub frm_metadata: Option, + + /// Whether to perform external authentication (if applicable) + #[schema(value_type = Option)] + pub request_external_three_ds_authentication: + Option, + + /// The payment instrument data to be used for the payment + pub payment_method_data: PaymentMethodDataRequest, + + /// The payment method type to be used for the payment. This should match with the `payment_method_data` provided + #[schema(value_type = PaymentMethod, example = "card")] + pub payment_method_type: api_enums::PaymentMethod, + + /// The payment method subtype to be used for the payment. This should match with the `payment_method_data` provided + #[schema(value_type = PaymentMethodType, example = "apple_pay")] + pub payment_method_subtype: api_enums::PaymentMethodType, + + /// This "CustomerAcceptance" object is passed during Payments-Confirm request, it enlists the type, time, and mode of acceptance properties related to an acceptance done by the customer. The customer_acceptance sub object is usually passed by the SDK or client. + #[schema(value_type = Option)] + pub customer_acceptance: Option, + + /// Additional details required by 3DS 2.0 + #[schema(value_type = Option)] + pub browser_info: Option, +} + +#[cfg(feature = "v2")] +impl From<&PaymentsRequest> for PaymentsCreateIntentRequest { + fn from(request: &PaymentsRequest) -> Self { + Self { + amount_details: request.amount_details.clone(), + merchant_reference_id: request.merchant_reference_id.clone(), + routing_algorithm_id: request.routing_algorithm_id.clone(), + capture_method: request.capture_method, + authentication_type: request.authentication_type, + billing: request.billing.clone(), + shipping: request.shipping.clone(), + customer_id: request.customer_id.clone(), + customer_present: request.customer_present.clone(), + description: request.description.clone(), + return_url: request.return_url.clone(), + setup_future_usage: request.setup_future_usage, + apply_mit_exemption: request.apply_mit_exemption.clone(), + statement_descriptor: request.statement_descriptor.clone(), + order_details: request.order_details.clone(), + allowed_payment_method_types: request.allowed_payment_method_types.clone(), + metadata: request.metadata.clone(), + connector_metadata: request.connector_metadata.clone(), + feature_metadata: request.feature_metadata.clone(), + payment_link_enabled: request.payment_link_enabled.clone(), + payment_link_config: request.payment_link_config.clone(), + request_incremental_authorization: request.request_incremental_authorization, + session_expiry: request.session_expiry, + frm_metadata: request.frm_metadata.clone(), + request_external_three_ds_authentication: request + .request_external_three_ds_authentication + .clone(), + } + } +} + +#[cfg(feature = "v2")] +impl From<&PaymentsRequest> for PaymentsConfirmIntentRequest { + fn from(request: &PaymentsRequest) -> Self { + Self { + return_url: request.return_url.clone(), + payment_method_data: request.payment_method_data.clone(), + payment_method_type: request.payment_method_type, + payment_method_subtype: request.payment_method_subtype, + shipping: request.shipping.clone(), + customer_acceptance: request.customer_acceptance.clone(), + browser_info: request.browser_info.clone(), + } + } +} + +#[cfg(feature = "v2")] +#[derive(Debug, serde::Serialize, ToSchema)] +pub struct PaymentsResponse { + /// Unique identifier for the payment. This ensures idempotency for multiple payments + /// that have been done by a single merchant. + #[schema( + min_length = 32, + max_length = 64, + example = "12345_pay_01926c58bc6e77c09e809964e72af8c8", + value_type = String, + )] + pub id: id_type::GlobalPaymentId, + + #[schema(value_type = IntentStatus, example = "success")] + pub status: api_enums::IntentStatus, + + /// Amount related information for this payment and attempt + pub amount: PaymentAmountDetailsResponse, + + /// The identifier for the customer + #[schema( + min_length = 32, + max_length = 64, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", + value_type = String + )] + pub customer_id: Option, + + /// The connector used for the payment + #[schema(example = "stripe")] + pub connector: String, + + /// It's a token used for client side verification. + #[schema(value_type = String)] + pub client_secret: common_utils::types::ClientSecret, + + /// Time when the payment was created + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created: PrimitiveDateTime, + + /// The payment method information provided for making a payment + #[schema(value_type = Option)] + #[serde(serialize_with = "serialize_payment_method_data_response")] + pub payment_method_data: Option, + + /// The payment method type for this payment attempt + #[schema(value_type = PaymentMethod, example = "wallet")] + pub payment_method_type: api_enums::PaymentMethod, + + #[schema(value_type = PaymentMethodType, example = "apple_pay")] + pub payment_method_subtype: api_enums::PaymentMethodType, + + /// Additional information required for redirection + pub next_action: Option, + + /// A unique identifier for a payment provided by the connector + #[schema(value_type = Option, example = "993672945374576J")] + pub connector_transaction_id: Option, + + /// reference(Identifier) to the payment at connector side + #[schema(value_type = Option, example = "993672945374576J")] + pub connector_reference_id: Option, + + /// Connector token information that can be used to make payments directly by the merchant. + pub connector_token_details: Option, + + /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment + #[schema(value_type = String)] + pub merchant_connector_id: id_type::MerchantConnectorAccountId, + + /// The browser information used for this payment + #[schema(value_type = Option)] + pub browser_info: Option, + + /// Error details for the payment if any + pub error: Option, +} + // Serialize is implemented because, this will be serialized in the api events. // Usually request types should not have serialize implemented. // @@ -4885,6 +5151,9 @@ pub struct PaymentsConfirmIntentResponse { #[schema(value_type = Option, example = "993672945374576J")] pub connector_reference_id: Option, + /// Connector token information that can be used to make payments directly by the merchant. + pub connector_token_details: Option, + /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment #[schema(value_type = String)] pub merchant_connector_id: id_type::MerchantConnectorAccountId, @@ -4897,6 +5166,15 @@ pub struct PaymentsConfirmIntentResponse { pub error: Option, } +/// Token information that can be used to initiate transactions by the merchant. +#[cfg(feature = "v2")] +#[derive(Debug, Serialize, ToSchema)] +pub struct ConnectorTokenDetails { + /// A token that can be used to make payments directly with the connector. + #[schema(example = "pm_9UhMqBMEOooRIvJFFdeW")] + pub token: String, +} + // TODO: have a separate response for detailed, summarized /// Response for Payment Intent Confirm #[cfg(feature = "v2")] @@ -5104,6 +5382,7 @@ pub struct PaymentListConstraints { pub created_gte: Option, } +#[cfg(feature = "v1")] #[derive(Clone, Debug, serde::Serialize, ToSchema)] pub struct PaymentListResponse { /// The number of payments included in the list @@ -5130,6 +5409,7 @@ pub struct IncrementalAuthorizationResponse { pub previously_authorized_amount: MinorUnit, } +#[cfg(feature = "v1")] #[derive(Clone, Debug, serde::Serialize)] pub struct PaymentListResponseV2 { /// The number of payments included in the list for given constraints diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index dbd867c9e5..a8fdf882e7 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -90,10 +90,10 @@ pub struct PaymentAttempt { pub payment_method_billing_address: Option, pub redirection_data: Option, pub connector_payment_data: Option, + pub connector_token_details: Option, pub id: id_type::GlobalAttemptId, pub shipping_cost: Option, pub order_tax_amount: Option, - pub connector_mandate_detail: Option, pub card_discovery: Option, } @@ -216,6 +216,26 @@ impl ConnectorTransactionIdTrait for PaymentAttempt { } } +#[cfg(feature = "v2")] +#[derive( + Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize, diesel::AsExpression, +)] +#[diesel(sql_type = diesel::sql_types::Jsonb)] +pub struct ConnectorTokenDetails { + pub connector_mandate_id: Option, + pub connector_mandate_request_reference_id: Option, +} + +#[cfg(feature = "v2")] +common_utils::impl_to_sql_from_sql_json!(ConnectorTokenDetails); + +#[cfg(feature = "v2")] +impl ConnectorTokenDetails { + pub fn get_connector_mandate_request_reference_id(&self) -> Option { + self.connector_mandate_request_reference_id.clone() + } +} + #[derive(Clone, Debug, Eq, PartialEq, Queryable, Serialize, Deserialize)] pub struct PaymentListFilters { pub connector: Vec, @@ -279,7 +299,7 @@ pub struct PaymentAttemptNew { pub payment_method_type_v2: storage_enums::PaymentMethod, pub payment_method_subtype: storage_enums::PaymentMethodType, pub id: id_type::GlobalAttemptId, - pub connector_mandate_detail: Option, + pub connector_token_details: Option, pub card_discovery: Option, } @@ -796,6 +816,7 @@ pub struct PaymentAttemptUpdateInternal { // client_version: Option, // customer_acceptance: Option, // card_network: Option, + pub connector_token_details: Option, } #[cfg(feature = "v1")] diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 07a76c13ae..9b1c3aa7d4 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -872,11 +872,11 @@ diesel::table! { redirection_data -> Nullable, #[max_length = 512] connector_payment_data -> Nullable, + connector_token_details -> Nullable, #[max_length = 64] id -> Varchar, shipping_cost -> Nullable, order_tax_amount -> Nullable, - connector_mandate_detail -> Nullable, card_discovery -> Nullable, } } diff --git a/crates/hyperswitch_domain_models/src/consts.rs b/crates/hyperswitch_domain_models/src/consts.rs index 3808e615c0..4a87c35b5c 100644 --- a/crates/hyperswitch_domain_models/src/consts.rs +++ b/crates/hyperswitch_domain_models/src/consts.rs @@ -5,3 +5,6 @@ pub const API_VERSION: common_enums::ApiVersion = common_enums::ApiVersion::V1; #[cfg(all(feature = "v2", feature = "customer_v2"))] pub const API_VERSION: common_enums::ApiVersion = common_enums::ApiVersion::V2; + +/// Length of the unique reference ID generated for connector mandate requests +pub const CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH: usize = 18; diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index 74804e658a..65455b3b19 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -33,9 +33,13 @@ use time::PrimitiveDateTime; #[cfg(all(feature = "v1", feature = "olap"))] use super::PaymentIntent; #[cfg(feature = "v2")] -use crate::type_encryption::{crypto_operation, CryptoOperation}; -#[cfg(feature = "v2")] -use crate::{address::Address, merchant_key_store::MerchantKeyStore, router_response_types}; +use crate::{ + address::Address, + consts, + merchant_key_store::MerchantKeyStore, + router_response_types, + type_encryption::{crypto_operation, CryptoOperation}, +}; use crate::{ behaviour, errors, mandates::{MandateDataType, MandateDetails}, @@ -400,8 +404,8 @@ pub struct PaymentAttempt { pub payment_method_billing_address: Option>, /// The global identifier for the payment attempt pub id: id_type::GlobalAttemptId, - /// The connector mandate details which are stored temporarily - pub connector_mandate_detail: Option, + /// Connector token information that can be used to make payments directly by the merchant. + pub connector_token_details: Option, /// Indicates the method by which a card is discovered during a payment pub card_discovery: Option, } @@ -520,7 +524,12 @@ impl PaymentAttempt { external_reference_id: None, payment_method_billing_address, error: None, - connector_mandate_detail: None, + connector_token_details: Some(diesel_models::ConnectorTokenDetails { + connector_mandate_id: None, + connector_mandate_request_reference_id: Some(common_utils::generate_id_with_len( + consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH, + )), + }), id, card_discovery: None, }) @@ -1413,6 +1422,18 @@ impl PaymentAttemptUpdate { } } +#[cfg(feature = "v2")] +#[derive(Debug, Clone, Serialize)] +pub struct ConfirmIntentResponseUpdate { + pub status: storage_enums::AttemptStatus, + pub connector_payment_id: Option, + pub updated_by: String, + pub redirection_data: Option, + pub connector_metadata: Option, + pub amount_capturable: Option, + pub connector_token_details: Option, +} + #[cfg(feature = "v2")] #[derive(Debug, Clone, Serialize)] pub enum PaymentAttemptUpdate { @@ -1424,14 +1445,7 @@ pub enum PaymentAttemptUpdate { merchant_connector_id: id_type::MerchantConnectorAccountId, }, /// Update the payment attempt on confirming the intent, after calling the connector on success response - ConfirmIntentResponse { - status: storage_enums::AttemptStatus, - connector_payment_id: Option, - updated_by: String, - redirection_data: Option, - connector_metadata: Option, - amount_capturable: Option, - }, + ConfirmIntentResponse(Box), /// Update the payment attempt after force syncing with the connector SyncUpdate { status: storage_enums::AttemptStatus, @@ -1791,7 +1805,7 @@ impl behaviour::Conversion for PaymentAttempt { payment_method_id, payment_method_billing_address, connector, - connector_mandate_detail, + connector_token_details, card_discovery, } = self; @@ -1869,7 +1883,7 @@ impl behaviour::Conversion for PaymentAttempt { tax_on_surcharge, payment_method_billing_address: payment_method_billing_address.map(Encryption::from), connector_payment_data, - connector_mandate_detail, + connector_token_details, card_discovery, }) } @@ -1981,7 +1995,7 @@ impl behaviour::Conversion for PaymentAttempt { external_reference_id: storage_model.external_reference_id, connector: storage_model.connector, payment_method_billing_address, - connector_mandate_detail: storage_model.connector_mandate_detail, + connector_token_details: storage_model.connector_token_details, card_discovery: storage_model.card_discovery, }) } @@ -2066,7 +2080,7 @@ impl behaviour::Conversion for PaymentAttempt { payment_method_subtype: self.payment_method_subtype, payment_method_type_v2: self.payment_method_type, id: self.id, - connector_mandate_detail: self.connector_mandate_detail, + connector_token_details: self.connector_token_details, card_discovery: self.card_discovery, }) } @@ -2098,6 +2112,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector_metadata: None, amount_capturable: None, amount_to_capture: None, + connector_token_details: None, }, PaymentAttemptUpdate::ErrorUpdate { status, @@ -2122,33 +2137,39 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector_metadata: None, amount_capturable, amount_to_capture: None, + connector_token_details: None, }, - PaymentAttemptUpdate::ConfirmIntentResponse { - status, - connector_payment_id, - updated_by, - redirection_data, - connector_metadata, - amount_capturable, - } => Self { - status: Some(status), - amount_capturable, - error_message: None, - error_code: None, - modified_at: common_utils::date_time::now(), - browser_info: None, - error_reason: None, - updated_by, - merchant_connector_id: None, - unified_code: None, - unified_message: None, - connector_payment_id, - connector: None, - redirection_data: redirection_data - .map(diesel_models::payment_attempt::RedirectForm::from), - connector_metadata, - amount_to_capture: None, - }, + PaymentAttemptUpdate::ConfirmIntentResponse(confirm_intent_response_update) => { + let ConfirmIntentResponseUpdate { + status, + connector_payment_id, + updated_by, + redirection_data, + connector_metadata, + amount_capturable, + connector_token_details, + } = *confirm_intent_response_update; + Self { + status: Some(status), + amount_capturable, + error_message: None, + error_code: None, + modified_at: common_utils::date_time::now(), + browser_info: None, + error_reason: None, + updated_by, + merchant_connector_id: None, + unified_code: None, + unified_message: None, + connector_payment_id, + connector: None, + redirection_data: redirection_data + .map(diesel_models::payment_attempt::RedirectForm::from), + connector_metadata, + amount_to_capture: None, + connector_token_details, + } + } PaymentAttemptUpdate::SyncUpdate { status, amount_capturable, @@ -2170,6 +2191,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal redirection_data: None, connector_metadata: None, amount_to_capture: None, + connector_token_details: None, }, PaymentAttemptUpdate::CaptureUpdate { status, @@ -2192,6 +2214,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector: None, redirection_data: None, connector_metadata: None, + connector_token_details: None, }, PaymentAttemptUpdate::PreCaptureUpdate { amount_to_capture, @@ -2213,6 +2236,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal status: None, connector_metadata: None, amount_capturable: None, + connector_token_details: None, }, } } diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index 322b4aa452..1ad7d2dcbd 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -10,6 +10,14 @@ use error_stack::ResultExt; use masking::{ExposeInterface, Secret}; use crate::{payment_address::PaymentAddress, payment_method_data, payments}; +#[cfg(feature = "v2")] +use crate::{ + payments::{ + payment_attempt::{ErrorDetails, PaymentAttemptUpdate}, + payment_intent::PaymentIntentUpdate, + }, + router_flow_types, router_request_types, router_response_types, +}; #[derive(Debug, Clone)] pub struct RouterData { @@ -416,15 +424,6 @@ impl ErrorResponse { } } -#[cfg(feature = "v2")] -use crate::{ - payments::{ - payment_attempt::{ErrorDetails, PaymentAttemptUpdate}, - payment_intent::PaymentIntentUpdate, - }, - router_flow_types, router_request_types, router_response_types, -}; - /// Get updatable trakcer objects of payment intent and payment attempt #[cfg(feature = "v2")] pub trait TrackerPostUpdateObjects { @@ -518,14 +517,27 @@ impl | router_request_types::ResponseId::EncodedData(id) => Some(id.to_owned()), }; - PaymentAttemptUpdate::ConfirmIntentResponse { - status: attempt_status, - connector_payment_id, - updated_by: storage_scheme.to_string(), - redirection_data: *redirection_data.clone(), - amount_capturable, - connector_metadata: connector_metadata.clone().map(Secret::new), - } + PaymentAttemptUpdate::ConfirmIntentResponse(Box::new( + payments::payment_attempt::ConfirmIntentResponseUpdate { + status: attempt_status, + connector_payment_id, + updated_by: storage_scheme.to_string(), + redirection_data: *redirection_data.clone(), + amount_capturable, + connector_metadata: connector_metadata.clone().map(Secret::new), + connector_token_details: response_router_data + .get_updated_connector_token_details( + payment_data + .payment_attempt + .connector_token_details + .as_ref() + .and_then(|token_details| { + token_details + .get_connector_mandate_request_reference_id() + }), + ), + }, + )) } router_response_types::PaymentsResponseData::MultipleCaptureResponse { .. } => { todo!() @@ -1092,3 +1104,222 @@ impl } } } + +#[cfg(feature = "v2")] +impl + TrackerPostUpdateObjects< + router_flow_types::SetupMandate, + router_request_types::SetupMandateRequestData, + payments::PaymentConfirmData, + > + for RouterData< + router_flow_types::SetupMandate, + router_request_types::SetupMandateRequestData, + router_response_types::PaymentsResponseData, + > +{ + fn get_payment_intent_update( + &self, + payment_data: &payments::PaymentConfirmData, + storage_scheme: common_enums::MerchantStorageScheme, + ) -> PaymentIntentUpdate { + let amount_captured = self.get_captured_amount(payment_data); + match self.response { + Ok(ref _response) => PaymentIntentUpdate::ConfirmIntentPostUpdate { + status: common_enums::IntentStatus::from( + self.get_attempt_status_for_db_update(payment_data), + ), + amount_captured, + updated_by: storage_scheme.to_string(), + }, + Err(ref error) => PaymentIntentUpdate::ConfirmIntentPostUpdate { + status: error + .attempt_status + .map(common_enums::IntentStatus::from) + .unwrap_or(common_enums::IntentStatus::Failed), + amount_captured, + updated_by: storage_scheme.to_string(), + }, + } + } + + fn get_payment_attempt_update( + &self, + payment_data: &payments::PaymentConfirmData, + storage_scheme: common_enums::MerchantStorageScheme, + ) -> PaymentAttemptUpdate { + let amount_capturable = self.get_amount_capturable(payment_data); + + match self.response { + Ok(ref response_router_data) => match response_router_data { + router_response_types::PaymentsResponseData::TransactionResponse { + resource_id, + redirection_data, + mandate_reference, + connector_metadata, + network_txn_id, + connector_response_reference_id, + incremental_authorization_allowed, + charge_id, + } => { + let attempt_status = self.get_attempt_status_for_db_update(payment_data); + + let connector_payment_id = match resource_id { + router_request_types::ResponseId::NoResponseId => None, + router_request_types::ResponseId::ConnectorTransactionId(id) + | router_request_types::ResponseId::EncodedData(id) => Some(id.to_owned()), + }; + + PaymentAttemptUpdate::ConfirmIntentResponse(Box::new( + payments::payment_attempt::ConfirmIntentResponseUpdate { + status: attempt_status, + connector_payment_id, + updated_by: storage_scheme.to_string(), + redirection_data: *redirection_data.clone(), + amount_capturable, + connector_metadata: connector_metadata.clone().map(Secret::new), + connector_token_details: response_router_data + .get_updated_connector_token_details( + payment_data + .payment_attempt + .connector_token_details + .as_ref() + .and_then(|token_details| { + token_details + .get_connector_mandate_request_reference_id() + }), + ), + }, + )) + } + router_response_types::PaymentsResponseData::MultipleCaptureResponse { .. } => { + todo!() + } + router_response_types::PaymentsResponseData::SessionResponse { .. } => todo!(), + router_response_types::PaymentsResponseData::SessionTokenResponse { .. } => todo!(), + router_response_types::PaymentsResponseData::TransactionUnresolvedResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::TokenizationResponse { .. } => todo!(), + router_response_types::PaymentsResponseData::ConnectorCustomerResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::ThreeDSEnrollmentResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::PreProcessingResponse { .. } => { + todo!() + } + router_response_types::PaymentsResponseData::IncrementalAuthorizationResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::PostProcessingResponse { .. } => { + todo!() + } + router_response_types::PaymentsResponseData::SessionUpdateResponse { .. } => { + todo!() + } + }, + Err(ref error_response) => { + let ErrorResponse { + code, + message, + reason, + status_code: _, + attempt_status, + connector_transaction_id, + } = error_response.clone(); + let attempt_status = attempt_status.unwrap_or(self.status); + + let error_details = ErrorDetails { + code, + message, + reason, + unified_code: None, + unified_message: None, + }; + + PaymentAttemptUpdate::ErrorUpdate { + status: attempt_status, + error: error_details, + amount_capturable, + connector_payment_id: connector_transaction_id, + updated_by: storage_scheme.to_string(), + } + } + } + } + + fn get_attempt_status_for_db_update( + &self, + _payment_data: &payments::PaymentConfirmData, + ) -> common_enums::AttemptStatus { + // For this step, consider whatever status was given by the connector module + // We do not need to check for amount captured or amount capturable here because we are authorizing the whole amount + self.status + } + + fn get_amount_capturable( + &self, + payment_data: &payments::PaymentConfirmData, + ) -> Option { + // Based on the status of the response, we can determine the amount capturable + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is already succeeded / failed we cannot capture any more amount + // So set the amount capturable to zero + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Cancelled => Some(MinorUnit::zero()), + // For these statuses, update the capturable amount when it reaches terminal / capturable state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + // If status is requires capture, get the total amount that can be captured + // This is in cases where the capture method will be `manual` or `manual_multiple` + // We do not need to handle the case where amount_to_capture is provided here as it cannot be passed in authroize flow + common_enums::IntentStatus::RequiresCapture => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // Invalid statues for this flow, after doing authorization this state is invalid + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } + + fn get_captured_amount( + &self, + payment_data: &payments::PaymentConfirmData, + ) -> Option { + // Based on the status of the response, we can determine the amount that was captured + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is succeeded then we have captured the whole amount + // we need not check for `amount_to_capture` here because passing `amount_to_capture` when authorizing is not supported + common_enums::IntentStatus::Succeeded => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // No amount is captured + common_enums::IntentStatus::Cancelled | common_enums::IntentStatus::Failed => { + Some(MinorUnit::zero()) + } + // For these statuses, update the amount captured when it reaches terminal state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + // No amount has been captured yet + common_enums::IntentStatus::RequiresCapture => Some(MinorUnit::zero()), + // Invalid statues for this flow + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } +} diff --git a/crates/hyperswitch_domain_models/src/router_response_types.rs b/crates/hyperswitch_domain_models/src/router_response_types.rs index 1a1488031f..4d15d715ff 100644 --- a/crates/hyperswitch_domain_models/src/router_response_types.rs +++ b/crates/hyperswitch_domain_models/src/router_response_types.rs @@ -149,6 +149,7 @@ impl PaymentsResponseData { .into()), } } + pub fn merge_transaction_responses( auth_response: &Self, capture_response: &Self, @@ -206,6 +207,31 @@ impl PaymentsResponseData { .into()), } } + + #[cfg(feature = "v2")] + pub fn get_updated_connector_token_details( + &self, + original_connector_mandate_request_reference_id: Option, + ) -> Option { + if let Self::TransactionResponse { + mandate_reference, .. + } = self + { + mandate_reference.clone().map(|mandate_ref| { + let connector_mandate_id = mandate_ref.connector_mandate_id; + let connector_mandate_request_reference_id = mandate_ref + .connector_mandate_request_reference_id + .or(original_connector_mandate_request_reference_id); + + diesel_models::ConnectorTokenDetails { + connector_mandate_id, + connector_mandate_request_reference_id, + } + }) + } else { + None + } + } } #[derive(Debug, Clone)] diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 701fb22d1a..1078d8080c 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -124,6 +124,7 @@ Never share your secret api keys. Keep them guarded and secure. routes::payments::payments_update_intent, routes::payments::payments_confirm_intent, routes::payments::payment_status, + routes::payments::payments_create_and_confirm_intent, routes::payments::payments_connector_session, routes::payments::list_payment_methods, @@ -359,8 +360,9 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::CardRedirectData, api_models::payments::CardToken, api_models::payments::CustomerAcceptance, + api_models::payments::ConnectorTokenDetails, + api_models::payments::PaymentsRequest, api_models::payments::PaymentsResponse, - api_models::payments::PaymentsCreateResponseOpenApi, api_models::payments::PaymentRetrieveBody, api_models::payments::PaymentsRetrieveRequest, api_models::payments::PaymentsCaptureRequest, @@ -424,7 +426,6 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::SamsungPayTokenData, api_models::payments::PaymentsCancelRequest, api_models::payments::PaymentListConstraints, - api_models::payments::PaymentListResponse, api_models::payments::CashappQr, api_models::payments::BankTransferData, api_models::payments::BankTransferNextStepsData, diff --git a/crates/openapi/src/routes/payment_method.rs b/crates/openapi/src/routes/payment_method.rs index afc9f4a173..4c0c201e29 100644 --- a/crates/openapi/src/routes/payment_method.rs +++ b/crates/openapi/src/routes/payment_method.rs @@ -334,7 +334,7 @@ pub async fn payment_method_delete_api() {} ( "X-Profile-Id" = String, Header, description = "Profile ID associated to the payment method intent", - example = json!({"X-Profile-Id": "pro_abcdefghijklmnop"}) + example = "pro_abcdefghijklmnop" ), ), responses( diff --git a/crates/openapi/src/routes/payments.rs b/crates/openapi/src/routes/payments.rs index 33bb95618e..ceccd53c4d 100644 --- a/crates/openapi/src/routes/payments.rs +++ b/crates/openapi/src/routes/payments.rs @@ -659,7 +659,7 @@ pub fn payments_get_intent() {} ( "X-Profile-Id" = String, Header, description = "Profile ID associated to the payment intent", - example = json!({"X-Profile-Id": "pro_abcdefghijklmnop"}) + example = "pro_abcdefghijklmnop" ), ), request_body( @@ -695,7 +695,7 @@ pub fn payments_update_intent() {} ( "X-Profile-Id" = String, Header, description = "Profile ID associated to the payment intent", - example = json!({"X-Profile-Id": "pro_abcdefghijklmnop"}) + example = "pro_abcdefghijklmnop" ), ( "X-Client-Secret" = String, Header, @@ -710,6 +710,7 @@ pub fn payments_update_intent() {} "Confirm the payment intent with card details" = ( value = json!({ "payment_method_type": "card", + "payment_method_subtype": "credit", "payment_method_data": { "card": { "card_number": "4242424242424242", @@ -756,6 +757,57 @@ pub fn payments_confirm_intent() {} #[cfg(feature = "v2")] pub fn payment_status() {} +/// Payments - Create and Confirm Intent +/// +/// **Creates and confirms a payment intent object when the amount and payment method information are passed.** +/// +/// You will require the 'API - Key' from the Hyperswitch dashboard to make the call. +#[utoipa::path( + post, + path = "/v2/payments", + params ( + ( + "X-Profile-Id" = String, Header, + description = "Profile ID associated to the payment intent", + example = "pro_abcdefghijklmnop" + ) + ), + request_body( + content = PaymentsRequest, + examples( + ( + "Create and confirm the payment intent with amount and card details" = ( + value = json!({ + "amount_details": { + "order_amount": 6540, + "currency": "USD" + }, + "payment_method_type": "card", + "payment_method_subtype": "credit", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + }) + ) + ), + ), + ), + responses( + (status = 200, description = "Payment created", body = PaymentsResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Payments", + operation_id = "Create and Confirm Payment Intent", + security(("api_key" = [])), +)] +pub fn payments_create_and_confirm_intent() {} + #[derive(utoipa::ToSchema)] #[schema(rename_all = "lowercase")] pub(crate) enum ForceSync { @@ -777,7 +829,7 @@ pub(crate) enum ForceSync { ( "X-Profile-Id" = String, Header, description = "Profile ID associated to the payment intent", - example = json!({"X-Profile-Id": "pro_abcdefghijklmnop"}) + example = "pro_abcdefghijklmnop" ), ( "X-Client-Secret" = String, Header, diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 9e00bcd6bc..585e894adf 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -6,7 +6,9 @@ pub mod user_role; use std::collections::HashSet; use common_utils::consts; +pub use hyperswitch_domain_models::consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH; pub use hyperswitch_interfaces::consts::{NO_ERROR_CODE, NO_ERROR_MESSAGE}; + // ID generation pub(crate) const ID_LENGTH: usize = 20; pub(crate) const MAX_ID_LENGTH: usize = 64; @@ -136,9 +138,6 @@ pub const DEFAULT_UNIFIED_ERROR_MESSAGE: &str = "Something went wrong"; // Recon's feature tag pub const RECON_FEATURE_TAG: &str = "RECONCILIATION AND SETTLEMENT"; -// Length of the unique reference ID generated for connector mandate requests -pub const CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH: usize = 18; - /// Default allowed domains for payment links pub const DEFAULT_ALLOWED_DOMAINS: Option> = None; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 8e31ab7975..47d2024834 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1610,6 +1610,192 @@ where ) } +#[allow(clippy::too_many_arguments)] +#[cfg(feature = "v2")] +pub(crate) async fn payments_create_and_confirm_intent( + state: SessionState, + req_state: ReqState, + merchant_account: domain::MerchantAccount, + profile: domain::Profile, + key_store: domain::MerchantKeyStore, + request: payments_api::PaymentsRequest, + payment_id: id_type::GlobalPaymentId, + mut header_payload: HeaderPayload, + platform_merchant_account: Option, +) -> RouterResponse { + use actix_http::body::MessageBody; + use common_utils::ext_traits::BytesExt; + use hyperswitch_domain_models::{ + payments::{PaymentConfirmData, PaymentIntentData}, + router_flow_types::{Authorize, PaymentCreateIntent, SetupMandate}, + }; + + let payload = payments_api::PaymentsCreateIntentRequest::from(&request); + + let create_intent_response = Box::pin(payments_intent_core::< + PaymentCreateIntent, + payments_api::PaymentsIntentResponse, + _, + _, + PaymentIntentData, + >( + state.clone(), + req_state.clone(), + merchant_account.clone(), + profile.clone(), + key_store.clone(), + operations::PaymentIntentCreate, + payload, + payment_id.clone(), + header_payload.clone(), + platform_merchant_account, + )) + .await?; + + logger::info!(?create_intent_response); + let create_intent_response = handle_payments_intent_response(create_intent_response)?; + + // Adding client secret to ensure client secret validation passes during confirm intent step + header_payload.client_secret = Some(create_intent_response.client_secret.clone()); + + let payload = payments_api::PaymentsConfirmIntentRequest::from(&request); + + let confirm_intent_response = decide_authorize_or_setup_intent_flow( + state, + req_state, + merchant_account, + profile, + key_store, + &create_intent_response, + payload, + payment_id, + header_payload, + ) + .await?; + + logger::info!(?confirm_intent_response); + let confirm_intent_response = handle_payments_intent_response(confirm_intent_response)?; + + construct_payments_response(create_intent_response, confirm_intent_response) +} + +#[cfg(feature = "v2")] +#[inline] +fn handle_payments_intent_response( + response: hyperswitch_domain_models::api::ApplicationResponse, +) -> CustomResult { + match response { + hyperswitch_domain_models::api::ApplicationResponse::Json(body) + | hyperswitch_domain_models::api::ApplicationResponse::JsonWithHeaders((body, _)) => { + Ok(body) + } + hyperswitch_domain_models::api::ApplicationResponse::StatusOk + | hyperswitch_domain_models::api::ApplicationResponse::TextPlain(_) + | hyperswitch_domain_models::api::ApplicationResponse::JsonForRedirection(_) + | hyperswitch_domain_models::api::ApplicationResponse::Form(_) + | hyperswitch_domain_models::api::ApplicationResponse::PaymentLinkForm(_) + | hyperswitch_domain_models::api::ApplicationResponse::FileData(_) + | hyperswitch_domain_models::api::ApplicationResponse::GenericLinkForm(_) => { + Err(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unexpected response from payment intent core") + } + } +} + +#[cfg(feature = "v2")] +#[inline] +fn construct_payments_response( + create_intent_response: payments_api::PaymentsIntentResponse, + confirm_intent_response: payments_api::PaymentsConfirmIntentResponse, +) -> RouterResponse { + let response = payments_api::PaymentsResponse { + id: confirm_intent_response.id, + status: confirm_intent_response.status, + amount: confirm_intent_response.amount, + customer_id: confirm_intent_response.customer_id, + connector: confirm_intent_response.connector, + client_secret: confirm_intent_response.client_secret, + created: confirm_intent_response.created, + payment_method_data: confirm_intent_response.payment_method_data, + payment_method_type: confirm_intent_response.payment_method_type, + payment_method_subtype: confirm_intent_response.payment_method_subtype, + next_action: confirm_intent_response.next_action, + connector_transaction_id: confirm_intent_response.connector_transaction_id, + connector_reference_id: confirm_intent_response.connector_reference_id, + connector_token_details: confirm_intent_response.connector_token_details, + merchant_connector_id: confirm_intent_response.merchant_connector_id, + browser_info: confirm_intent_response.browser_info, + error: confirm_intent_response.error, + }; + + Ok(hyperswitch_domain_models::api::ApplicationResponse::Json( + response, + )) +} + +#[cfg(feature = "v2")] +#[allow(clippy::too_many_arguments)] +async fn decide_authorize_or_setup_intent_flow( + state: SessionState, + req_state: ReqState, + merchant_account: domain::MerchantAccount, + profile: domain::Profile, + key_store: domain::MerchantKeyStore, + create_intent_response: &payments_api::PaymentsIntentResponse, + confirm_intent_request: payments_api::PaymentsConfirmIntentRequest, + payment_id: id_type::GlobalPaymentId, + header_payload: HeaderPayload, +) -> RouterResponse { + use hyperswitch_domain_models::{ + payments::PaymentConfirmData, + router_flow_types::{Authorize, SetupMandate}, + }; + + if create_intent_response.amount_details.order_amount == MinorUnit::zero() { + Box::pin(payments_core::< + SetupMandate, + api_models::payments::PaymentsConfirmIntentResponse, + _, + _, + _, + PaymentConfirmData, + >( + state, + req_state, + merchant_account, + profile, + key_store, + operations::PaymentIntentConfirm, + confirm_intent_request, + payment_id, + CallConnectorAction::Trigger, + header_payload, + )) + .await + } else { + Box::pin(payments_core::< + Authorize, + api_models::payments::PaymentsConfirmIntentResponse, + _, + _, + _, + PaymentConfirmData, + >( + state, + req_state, + merchant_account, + profile, + key_store, + operations::PaymentIntentConfirm, + confirm_intent_request, + payment_id, + CallConnectorAction::Trigger, + header_payload, + )) + .await + } +} + fn is_start_pay(operation: &Op) -> bool { format!("{operation:?}").eq("PaymentStart") } diff --git a/crates/router/src/core/payments/flows/setup_mandate_flow.rs b/crates/router/src/core/payments/flows/setup_mandate_flow.rs index ee8ff78bda..52014730ee 100644 --- a/crates/router/src/core/payments/flows/setup_mandate_flow.rs +++ b/crates/router/src/core/payments/flows/setup_mandate_flow.rs @@ -15,6 +15,7 @@ use crate::{ types::{self, api, domain}, }; +#[cfg(feature = "v1")] #[async_trait] impl ConstructFlowSpecificData< @@ -23,7 +24,6 @@ impl types::PaymentsResponseData, > for PaymentData { - #[cfg(feature = "v1")] async fn construct_router_data<'a>( &self, state: &SessionState, @@ -52,7 +52,27 @@ impl .await } - #[cfg(feature = "v2")] + async fn get_merchant_recipient_data<'a>( + &self, + _state: &SessionState, + _merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _merchant_connector_account: &helpers::MerchantConnectorAccountType, + _connector: &api::ConnectorData, + ) -> RouterResult> { + Ok(None) + } +} + +#[cfg(feature = "v2")] +#[async_trait] +impl + ConstructFlowSpecificData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for hyperswitch_domain_models::payments::PaymentConfirmData +{ async fn construct_router_data<'a>( &self, state: &SessionState, @@ -64,7 +84,20 @@ impl merchant_recipient_data: Option, header_payload: Option, ) -> RouterResult { - todo!() + Box::pin( + transformers::construct_payment_router_data_for_setup_mandate( + state, + self.clone(), + connector_id, + merchant_account, + key_store, + customer, + merchant_connector_account, + merchant_recipient_data, + header_payload, + ), + ) + .await } async fn get_merchant_recipient_data<'a>( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 09fb371442..b0df22f3d0 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3163,6 +3163,7 @@ pub async fn delete_ephemeral_key( Ok(services::ApplicationResponse::Json(response)) } +#[cfg(feature = "v1")] pub fn make_pg_redirect_response( payment_id: id_type::PaymentId, response: &api::PaymentsResponse, diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 9b1c2ea3be..5f3b515c96 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -777,7 +777,7 @@ impl GetTracker, api::PaymentsRequest> None, // update_history None, // mandate_metadata Some(common_utils::generate_id_with_len( - consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH.to_owned(), + consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH, )), // connector_mandate_request_reference_id )), ); diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 6ace8c3018..dd72bfff30 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -560,7 +560,7 @@ impl GetTracker, api::PaymentsRequest> None, // update_history None, // mandate_metadata Some(common_utils::generate_id_with_len( - consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH.to_owned(), + consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH, )), // connector_mandate_request_reference_id ), )); diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 07342ffb4e..748b71469b 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -2423,6 +2423,95 @@ impl PostUpdateTracker, types::PaymentsSyncDat } } +#[cfg(feature = "v2")] +impl Operation for &PaymentResponse { + type Data = PaymentConfirmData; + fn to_post_update_tracker( + &self, + ) -> RouterResult< + &(dyn PostUpdateTracker + Send + Sync), + > { + Ok(*self) + } +} + +#[cfg(feature = "v2")] +impl Operation for PaymentResponse { + type Data = PaymentConfirmData; + fn to_post_update_tracker( + &self, + ) -> RouterResult< + &(dyn PostUpdateTracker + Send + Sync), + > { + Ok(self) + } +} + +#[cfg(feature = "v2")] +#[async_trait] +impl PostUpdateTracker, types::SetupMandateRequestData> + for PaymentResponse +{ + async fn update_tracker<'b>( + &'b self, + state: &'b SessionState, + mut payment_data: PaymentConfirmData, + response: types::RouterData, + key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, + ) -> RouterResult> + where + F: 'b + Send + Sync, + types::RouterData: + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects< + F, + types::SetupMandateRequestData, + PaymentConfirmData, + >, + { + use hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; + + let db = &*state.store; + let key_manager_state = &state.into(); + + let response_router_data = response; + + let payment_intent_update = + response_router_data.get_payment_intent_update(&payment_data, storage_scheme); + let payment_attempt_update = + response_router_data.get_payment_attempt_update(&payment_data, storage_scheme); + + let updated_payment_intent = db + .update_payment_intent( + key_manager_state, + payment_data.payment_intent, + payment_intent_update, + key_store, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to update payment intent")?; + + let updated_payment_attempt = db + .update_payment_attempt( + key_manager_state, + key_store, + payment_data.payment_attempt, + payment_attempt_update, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to update payment attempt")?; + + payment_data.payment_intent = updated_payment_intent; + payment_data.payment_attempt = updated_payment_attempt; + + Ok(payment_data) + } +} + #[cfg(feature = "v1")] fn update_connector_mandate_details_for_the_flow( connector_mandate_id: Option, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 80c10a294e..1e9adb0a06 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -305,7 +305,7 @@ pub async fn construct_payment_router_data_for_authorize<'a>( }; let connector_mandate_request_reference_id = payment_data .payment_attempt - .connector_mandate_detail + .connector_token_details .as_ref() .and_then(|detail| detail.get_connector_mandate_request_reference_id()); @@ -428,7 +428,7 @@ pub async fn construct_payment_router_data_for_capture<'a>( let connector_mandate_request_reference_id = payment_data .payment_attempt - .connector_mandate_detail + .connector_token_details .as_ref() .and_then(|detail| detail.get_connector_mandate_request_reference_id()); @@ -831,6 +831,198 @@ pub async fn construct_payment_router_data_for_sdk_session<'a>( Ok(router_data) } +#[cfg(feature = "v2")] +#[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +pub async fn construct_payment_router_data_for_setup_mandate<'a>( + state: &'a SessionState, + payment_data: hyperswitch_domain_models::payments::PaymentConfirmData, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &'a Option, + merchant_connector_account: &domain::MerchantConnectorAccount, + _merchant_recipient_data: Option, + header_payload: Option, +) -> RouterResult { + fp_utils::when(merchant_connector_account.is_disabled(), || { + Err(errors::ApiErrorResponse::MerchantConnectorAccountDisabled) + })?; + + let auth_type = merchant_connector_account + .get_connector_account_details() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while parsing value for ConnectorAuthType")?; + + // TODO: Take Globalid and convert to connector reference id + let customer_id = customer + .to_owned() + .map(|customer| common_utils::id_type::CustomerId::try_from(customer.id.clone())) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Invalid global customer generated, not able to convert to reference id", + )?; + + let payment_method = payment_data.payment_attempt.payment_method_type; + + let router_base_url = &state.base_url; + let attempt = &payment_data.payment_attempt; + + let complete_authorize_url = Some(helpers::create_complete_authorize_url( + router_base_url, + attempt, + connector_id, + )); + + let webhook_url = Some(helpers::create_webhook_url( + router_base_url, + &attempt.merchant_id, + merchant_connector_account.get_id().get_string_repr(), + )); + + let router_return_url = payment_data + .payment_intent + .create_finish_redirection_url(router_base_url, &merchant_account.publishable_key) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to construct finish redirection url")? + .to_string(); + + let connector_request_reference_id = payment_data + .payment_intent + .merchant_reference_id + .map(|id| id.get_string_repr().to_owned()) + .unwrap_or(payment_data.payment_attempt.id.get_string_repr().to_owned()); + + let email = customer + .as_ref() + .and_then(|customer| customer.email.clone()) + .map(pii::Email::from); + + let browser_info = payment_data + .payment_attempt + .browser_info + .clone() + .map(types::BrowserInformation::from); + + // TODO: few fields are repeated in both routerdata and request + let request = types::SetupMandateRequestData { + currency: payment_data.payment_intent.amount_details.currency, + payment_method_data: payment_data + .payment_method_data + .get_required_value("payment_method_data")?, + amount: Some( + payment_data + .payment_attempt + .amount_details + .get_net_amount() + .get_amount_as_i64(), + ), + confirm: true, + statement_descriptor_suffix: None, + customer_acceptance: None, + mandate_id: None, + setup_future_usage: Some(payment_data.payment_intent.setup_future_usage), + off_session: None, + setup_mandate_details: None, + router_return_url: Some(router_return_url.clone()), + webhook_url, + browser_info, + email, + customer_name: None, + return_url: Some(router_return_url), + payment_method_type: Some(payment_data.payment_attempt.payment_method_subtype), + request_incremental_authorization: matches!( + payment_data + .payment_intent + .request_incremental_authorization, + RequestIncrementalAuthorization::True | RequestIncrementalAuthorization::Default + ), + metadata: payment_data.payment_intent.metadata, + minor_amount: Some(payment_data.payment_attempt.amount_details.get_net_amount()), + shipping_cost: payment_data.payment_intent.amount_details.shipping_cost, + }; + let connector_mandate_request_reference_id = payment_data + .payment_attempt + .connector_token_details + .as_ref() + .and_then(|detail| detail.get_connector_mandate_request_reference_id()); + + // TODO: evaluate the fields in router data, if they are required or not + let router_data = types::RouterData { + flow: PhantomData, + merchant_id: merchant_account.get_id().clone(), + tenant_id: state.tenant.tenant_id.clone(), + // TODO: evaluate why we need customer id at the connector level. We already have connector customer id. + customer_id, + connector: connector_id.to_owned(), + // TODO: evaluate why we need payment id at the connector level. We already have connector reference id + payment_id: payment_data + .payment_attempt + .payment_id + .get_string_repr() + .to_owned(), + // TODO: evaluate why we need attempt id at the connector level. We already have connector reference id + attempt_id: payment_data + .payment_attempt + .get_id() + .get_string_repr() + .to_owned(), + status: payment_data.payment_attempt.status, + payment_method, + connector_auth_type: auth_type, + description: payment_data + .payment_intent + .description + .as_ref() + .map(|description| description.get_string_repr()) + .map(ToOwned::to_owned), + // TODO: Create unified address + address: payment_data.payment_address.clone(), + auth_type: payment_data.payment_attempt.authentication_type, + connector_meta_data: None, + connector_wallets_details: None, + request, + response: Err(hyperswitch_domain_models::router_data::ErrorResponse::default()), + amount_captured: None, + minor_amount_captured: None, + access_token: None, + session_token: None, + reference_id: None, + payment_method_status: None, + payment_method_token: None, + connector_customer: None, + recurring_mandate_payment_data: None, + // TODO: This has to be generated as the reference id based on the connector configuration + // Some connectros might not accept accept the global id. This has to be done when generating the reference id + connector_request_reference_id, + preprocessing_id: payment_data.payment_attempt.preprocessing_step_id, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + // TODO: take this based on the env + test_mode: Some(true), + payment_method_balance: None, + connector_api_version: None, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, + connector_response: None, + integrity_check: Ok(()), + additional_merchant_data: None, + header_payload, + connector_mandate_request_reference_id, + authentication_id: None, + psd2_sca_exemption_type: None, + }; + + Ok(router_data) +} + #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] @@ -1433,6 +1625,10 @@ where .as_ref() .map(|_| api_models::payments::NextActionData::RedirectToUrl { redirect_to_url }); + let connector_token_details = payment_attempt + .connector_token_details + .and_then(Option::::foreign_from); + let response = api_models::payments::PaymentsConfirmIntentResponse { id: payment_intent.id.clone(), status: payment_intent.status, @@ -1447,6 +1643,7 @@ where next_action, connector_transaction_id: payment_attempt.connector_payment_id.clone(), connector_reference_id: None, + connector_token_details, merchant_connector_id, browser_info: None, error, @@ -4146,6 +4343,7 @@ impl ForeignFrom for ConnectorMandateReferenc ) } } + impl ForeignFrom for DieselConnectorMandateReferenceId { fn foreign_from(value: ConnectorMandateReferenceId) -> Self { Self { @@ -4158,6 +4356,17 @@ impl ForeignFrom for DieselConnectorMandateReferenc } } +#[cfg(feature = "v2")] +impl ForeignFrom + for Option +{ + fn foreign_from(value: diesel_models::ConnectorTokenDetails) -> Self { + value + .connector_mandate_id + .map(|mandate_id| api_models::payments::ConnectorTokenDetails { token: mandate_id }) + } +} + impl ForeignFrom<(Self, Option<&api_models::payments::AdditionalPaymentData>)> for Option { diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 22bc38b3d6..71cc3dceeb 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1419,6 +1419,7 @@ pub fn get_external_authentication_request_poll_id( payment_id.get_external_authentication_request_poll_id() } +#[cfg(feature = "v1")] pub fn get_html_redirect_response_for_external_authentication( return_url_with_query_params: String, payment_response: &api_models::payments::PaymentsResponse, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index fffb3eab09..17ab056e85 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -551,9 +551,15 @@ pub struct Payments; impl Payments { pub fn server(state: AppState) -> Scope { let mut route = web::scope("/v2/payments").app_data(web::Data::new(state)); - route = route.service( - web::resource("/create-intent").route(web::post().to(payments::payments_create_intent)), - ); + route = route + .service( + web::resource("") + .route(web::post().to(payments::payments_create_and_confirm_intent)), + ) + .service( + web::resource("/create-intent") + .route(web::post().to(payments::payments_create_intent)), + ); route = route.service( web::scope("/{payment_id}") diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 6550778619..895ba4b8af 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -146,6 +146,7 @@ impl From for ApiIdentifier { | Flow::PaymentsGetIntent | Flow::PaymentsPostSessionTokens | Flow::PaymentsUpdateIntent + | Flow::PaymentsCreateAndConfirmIntent | Flow::PaymentStartRedirection => Self::Payments, Flow::PayoutsCreate diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index c88f61f7c8..d461599b6f 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -217,6 +217,57 @@ pub async fn payments_get_intent( .await } +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsCreateAndConfirmIntent, payment_id))] +pub async fn payments_create_and_confirm_intent( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let flow = Flow::PaymentsCreateAndConfirmIntent; + let header_payload = match HeaderPayload::foreign_try_from(req.headers()) { + Ok(headers) => headers, + Err(err) => { + return api::log_and_return_error_response(err); + } + }; + + let global_payment_id = + common_utils::id_type::GlobalPaymentId::generate(&state.conf.cell_information.id); + + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, request, req_state| { + payments::payments_create_and_confirm_intent( + state, + req_state, + auth.merchant_account, + auth.profile, + auth.key_store, + request, + global_payment_id.clone(), + header_payload.clone(), + auth.platform_merchant_account, + ) + }, + match env::which() { + env::Env::Production => &auth::HeaderAuth(auth::ApiKeyAuth), + _ => auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuth { + permission: Permission::ProfilePaymentWrite, + }, + req.headers(), + ), + }, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(feature = "v2")] #[instrument(skip_all, fields(flow = ?Flow::PaymentsUpdateIntent, payment_id))] pub async fn payments_update_intent( diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 0a56ff83f9..8095907eca 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -592,7 +592,6 @@ async fn handle_response( match status_code { 200..=202 | 302 | 204 => { - logger::debug!(response=?response); // If needed add log line // logger:: error!( error_parsing_response=?err); let response = response diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index c11d54fc29..4ad2bd4998 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -543,6 +543,7 @@ pub struct RedirectPaymentFlowResponse { pub profile: domain::Profile, } +#[cfg(feature = "v1")] #[derive(Clone, Debug)] pub struct AuthenticatePaymentFlowResponse { pub payments_response: api_models::payments::PaymentsResponse, diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index 6ec0fe701e..58159fc568 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -1,8 +1,9 @@ #[cfg(feature = "v1")] -pub use api_models::payments::PaymentsRequest; +pub use api_models::payments::{PaymentListResponse, PaymentListResponseV2}; #[cfg(feature = "v2")] pub use api_models::payments::{ - PaymentsCreateIntentRequest, PaymentsIntentResponse, PaymentsUpdateIntentRequest, + PaymentsConfirmIntentRequest, PaymentsCreateIntentRequest, PaymentsIntentResponse, + PaymentsUpdateIntentRequest, }; pub use api_models::{ feature_matrix::{ @@ -14,18 +15,18 @@ pub use api_models::{ MandateTransactionType, MandateType, MandateValidationFields, NextActionType, OnlineMandate, OpenBankingSessionToken, PayLaterData, PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, - PaymentListFiltersV2, PaymentListResponse, PaymentListResponseV2, PaymentMethodData, - PaymentMethodDataRequest, PaymentMethodDataResponse, PaymentOp, PaymentRetrieveBody, + PaymentListFiltersV2, PaymentMethodData, PaymentMethodDataRequest, + PaymentMethodDataResponse, PaymentOp, PaymentRetrieveBody, PaymentRetrieveBodyWithCredentials, PaymentsAggregateResponse, PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsCompleteAuthorizeRequest, PaymentsDynamicTaxCalculationRequest, PaymentsDynamicTaxCalculationResponse, PaymentsExternalAuthenticationRequest, PaymentsIncrementalAuthorizationRequest, PaymentsManualUpdateRequest, PaymentsPostSessionTokensRequest, PaymentsPostSessionTokensResponse, PaymentsRedirectRequest, PaymentsRedirectionResponse, - PaymentsRejectRequest, PaymentsResponse, PaymentsResponseForm, PaymentsRetrieveRequest, - PaymentsSessionRequest, PaymentsSessionResponse, PaymentsStartRequest, PgRedirectResponse, - PhoneDetails, RedirectionResponse, SessionToken, UrlDetails, VerifyRequest, VerifyResponse, - WalletData, + PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsResponseForm, + PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, + PaymentsStartRequest, PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, + UrlDetails, VerifyRequest, VerifyResponse, WalletData, }, }; use error_stack::ResultExt; diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 935efa3c78..a267d24af1 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -172,6 +172,10 @@ pub enum Flow { PaymentsGetIntent, /// Payments Update Intent flow PaymentsUpdateIntent, + /// Payments confirm intent flow + PaymentsConfirmIntent, + /// Payments create and confirm intent flow + PaymentsCreateAndConfirmIntent, #[cfg(feature = "payouts")] /// Payouts create flow PayoutsCreate, @@ -525,8 +529,6 @@ pub enum Flow { PaymentsManualUpdate, /// Dynamic Tax Calcultion SessionUpdateTaxCalculation, - /// Payments confirm intent - PaymentsConfirmIntent, /// Payments post session tokens flow PaymentsPostSessionTokens, /// Payments start redirection flow diff --git a/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql b/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql index a4616016ab..08e4b0a356 100644 --- a/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql +++ b/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql @@ -42,7 +42,8 @@ ALTER TABLE payment_attempt DROP COLUMN payment_method_type_v2, DROP COLUMN tax_on_surcharge, DROP COLUMN payment_method_billing_address, DROP COLUMN redirection_data, - DROP COLUMN connector_payment_data; + DROP COLUMN connector_payment_data, + DROP COLUMN connector_token_details; ALTER TABLE merchant_connector_account ALTER COLUMN payment_methods_enabled TYPE JSON [ ]; diff --git a/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql b/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql index faebb36cdf..3926a02afa 100644 --- a/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql +++ b/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql @@ -51,7 +51,8 @@ ADD COLUMN payment_method_type_v2 VARCHAR, ADD COLUMN tax_on_surcharge BIGINT, ADD COLUMN payment_method_billing_address BYTEA, ADD COLUMN redirection_data JSONB, - ADD COLUMN connector_payment_data VARCHAR(512); + ADD COLUMN connector_payment_data VARCHAR(512), + ADD COLUMN connector_token_details JSONB; -- Change the type of the column from JSON to JSONB ALTER TABLE merchant_connector_account diff --git a/v2_migrations/2024-10-08-081847_drop_v1_columns/down.sql b/v2_migrations/2024-11-08-081847_drop_v1_columns/down.sql similarity index 97% rename from v2_migrations/2024-10-08-081847_drop_v1_columns/down.sql rename to v2_migrations/2024-11-08-081847_drop_v1_columns/down.sql index 64cbd2233e..2dcd3a16a6 100644 --- a/v2_migrations/2024-10-08-081847_drop_v1_columns/down.sql +++ b/v2_migrations/2024-11-08-081847_drop_v1_columns/down.sql @@ -88,7 +88,8 @@ ADD COLUMN IF NOT EXISTS attempt_id VARCHAR(64) NOT NULL, ADD COLUMN straight_through_algorithm JSONB, ADD COLUMN confirm BOOLEAN, ADD COLUMN authentication_data JSONB, - ADD COLUMN payment_method_billing_address_id VARCHAR(64); + ADD COLUMN payment_method_billing_address_id VARCHAR(64), + ADD COLUMN connector_mandate_detail JSONB; -- Create the index which was dropped because of dropping the column CREATE INDEX payment_attempt_connector_transaction_id_merchant_id_index ON payment_attempt (connector_transaction_id, merchant_id); diff --git a/v2_migrations/2024-10-08-081847_drop_v1_columns/up.sql b/v2_migrations/2024-11-08-081847_drop_v1_columns/up.sql similarity index 97% rename from v2_migrations/2024-10-08-081847_drop_v1_columns/up.sql rename to v2_migrations/2024-11-08-081847_drop_v1_columns/up.sql index 276bea10bd..65eb29c474 100644 --- a/v2_migrations/2024-10-08-081847_drop_v1_columns/up.sql +++ b/v2_migrations/2024-11-08-081847_drop_v1_columns/up.sql @@ -86,4 +86,5 @@ ALTER TABLE payment_attempt DROP COLUMN attempt_id, DROP COLUMN straight_through_algorithm, DROP COLUMN confirm, DROP COLUMN authentication_data, - DROP COLUMN payment_method_billing_address_id; + DROP COLUMN payment_method_billing_address_id, + DROP COLUMN connector_mandate_detail;