From 7c561d57767001e755fc9abfc32352ffdc9aacea Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 19:01:09 +0000 Subject: [PATCH 01/21] test(postman): update postman collection files --- .../stripe.postman_collection.json | 406 +++++++++++++++++- 1 file changed, 404 insertions(+), 2 deletions(-) diff --git a/postman/collection-json/stripe.postman_collection.json b/postman/collection-json/stripe.postman_collection.json index 85017bf2764d..7ae8061862d4 100644 --- a/postman/collection-json/stripe.postman_collection.json +++ b/postman/collection-json/stripe.postman_collection.json @@ -1836,6 +1836,15 @@ { "name": "Payment Connector - Create", "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + }, { "listen": "test", "script": { @@ -1924,7 +1933,7 @@ "language": "json" } }, - "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_name\":\"stripe\",\"business_country\":\"US\",\"business_label\":\"default\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}_invalid_values\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" }, "url": { "raw": "{{baseUrl}}/account/:account_id/connectors", @@ -2016,6 +2025,299 @@ " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", " );", "}", + "", + "// Response body should have value \"failed\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'failed'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"failed\");", + " },", + " );", + "}", + "", + "// Response body should have an error message", + "if (jsonData?.error_message) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'error_message' is not 'null'\",", + " function () {", + " pm.expect(jsonData.error_message).is.not.null;", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"amount\":6540,\"currency\":\"USD\",\"confirm\":true,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":6540,\"customer_id\":\"StripeCustomer\",\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+1\",\"description\":\"Its my first payment request\",\"authentication_type\":\"no_three_ds\",\"return_url\":\"https://duck.com\",\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_data\":{\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"joseph Doe\",\"card_cvc\":\"123\"}},\"billing\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"joseph\",\"last_name\":\"Doe\"},\"phone\":{\"number\":\"8056594427\",\"country_code\":\"+91\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"},\"routing\":{\"type\":\"single\",\"data\":\"stripe\"}}" + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "payments" + ] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" + }, + "response": [] + }, + { + "name": "Payment Connector - Update", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Status code is 2xx\",", + " function () {", + " pm.response.to.be.success;", + " },", + ");", + "", + "// Validate if response header has matching content-type", + "pm.test(", + " \"[POST]::/account/:account_id/connectors/:connector_id - Content-Type is application/json\",", + " function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + " },", + ");", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id", + "if (jsonData?.merchant_connector_id) {", + " pm.collectionVariables.set(", + " \"merchant_connector_id\",", + " jsonData.merchant_connector_id,", + " );", + " console.log(", + " \"- use {{merchant_connector_id}} as collection variable for value\",", + " jsonData.merchant_connector_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"connector_type\":\"fiz_operations\",\"connector_account_details\":{\"auth_type\":\"HeaderKey\",\"api_key\":\"{{connector_api_key}}\"},\"test_mode\":false,\"disabled\":false,\"payment_methods_enabled\":[{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"affirm\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"afterpay_clearpay\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"redirect_to_url\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"pay_later\",\"payment_method_types\":[{\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true,\"payment_experience\":\"invoke_sdk_client\",\"payment_method_type\":\"klarna\"}]},{\"payment_method\":\"bank_redirect\",\"payment_method_types\":[{\"payment_method_type\":\"ideal\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"giropay\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sofort\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"eps\",\"payment_experience\":null,\"card_networks\":null,\"accepted_currencies\":null,\"accepted_countries\":null,\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_debit\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"becs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"bank_transfer\",\"payment_method_types\":[{\"payment_method_type\":\"ach\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"bacs\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true},{\"payment_method_type\":\"sepa\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"credit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"card\",\"payment_method_types\":[{\"payment_method_type\":\"debit\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"apple_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]},{\"payment_method\":\"wallet\",\"payment_method_types\":[{\"payment_method_type\":\"google_pay\",\"payment_experience\":\"invoke_sdk_client\",\"minimum_amount\":1,\"maximum_amount\":68607706,\"recurring_enabled\":true,\"installment_payment_enabled\":true}]}],\"metadata\":{\"google_pay\":{\"allowed_payment_methods\":[{\"type\":\"CARD\",\"parameters\":{\"allowed_auth_methods\":[\"PAN_ONLY\",\"CRYPTOGRAM_3DS\"],\"allowed_card_networks\":[\"AMEX\",\"DISCOVER\",\"INTERAC\",\"JCB\",\"MASTERCARD\",\"VISA\"]},\"tokenization_specification\":{\"type\":\"PAYMENT_GATEWAY\",\"parameters\":{\"gateway\":\"example\",\"gateway_merchant_id\":\"{{gateway_merchant_id}}\"}}}],\"merchant_info\":{\"merchant_name\":\"Narayan Bhat\"}},\"apple_pay\":{\"session_token_data\":{\"initiative\":\"web\",\"certificate\":\"{{certificate}}\",\"display_name\":\"applepay\",\"certificate_keys\":\"{{certificate_keys}}\",\"initiative_context\":\"hyperswitch-sdk-test.netlify.app\",\"merchant_identifier\":\"merchant.com.stripe.sang\"},\"payment_request_data\":{\"label\":\"applepay pvt.ltd\",\"supported_networks\":[\"visa\",\"masterCard\",\"amex\",\"discover\"],\"merchant_capabilities\":[\"supports3DS\"]}}}}" + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors/:connector_id", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "account", + ":account_id", + "connectors", + ":connector_id" + ], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}" + }, + { + "key": "connector_id", + "value": "{{merchant_connector_id}}" + } + ] + }, + "description": "To update an existing Payment Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc" + }, + "response": [] + }, + { + "name": "Payments - Create-copy", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/payments - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/payments - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/payments - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id", + "if (jsonData?.payment_id) {", + " pm.collectionVariables.set(\"payment_id\", jsonData.payment_id);", + " console.log(", + " \"- use {{payment_id}} as collection variable for value\",", + " jsonData.payment_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id", + "if (jsonData?.mandate_id) {", + " pm.collectionVariables.set(\"mandate_id\", jsonData.mandate_id);", + " console.log(", + " \"- use {{mandate_id}} as collection variable for value\",", + " jsonData.mandate_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.\",", + " );", + "}", + "", + "// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret", + "if (jsonData?.client_secret) {", + " pm.collectionVariables.set(\"client_secret\", jsonData.client_secret);", + " console.log(", + " \"- use {{client_secret}} as collection variable for value\",", + " jsonData.client_secret,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.\",", + " );", + "}", + "", + "// Response body should have value \"succeeded\" for \"status\"", + "if (jsonData?.status) {", + " pm.test(", + " \"[POST]::/payments - Content check if value for 'status' matches 'succeeded'\",", + " function () {", + " pm.expect(jsonData.status).to.eql(\"succeeded\");", + " },", + " );", + "}", + "", + "// Response body should have \"connector_transaction_id\"", + "pm.test(", + " \"[POST]::/payments - Content check if 'connector_transaction_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.connector_transaction_id !== \"undefined\").to.be", + " .true;", + " },", + ");", "" ], "type": "text/javascript" @@ -4264,6 +4566,106 @@ { "name": "PaymentMethods", "item": [ + { + "name": "P_Create Customer", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Validate status 2xx", + "pm.test(\"[POST]::/customers - Status code is 2xx\", function () {", + " pm.response.to.be.success;", + "});", + "", + "// Validate if response header has matching content-type", + "pm.test(\"[POST]::/customers - Content-Type is application/json\", function () {", + " pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(", + " \"application/json\",", + " );", + "});", + "", + "// Validate if response has JSON Body", + "pm.test(\"[POST]::/customers - Response has JSON Body\", function () {", + " pm.response.to.have.jsonBody();", + "});", + "", + "// Set response object as internal variable", + "let jsonData = {};", + "try {", + " jsonData = pm.response.json();", + "} catch (e) {}", + "", + "// Response body should have \"customer_id\"", + "pm.test(", + " \"[POST]::/customers - Content check if 'customer_id' exists\",", + " function () {", + " pm.expect(typeof jsonData.customer_id !== \"undefined\").to.be.true;", + " },", + ");", + "", + "// Response body should have a minimum length of \"1\" for \"customer_id\"", + "if (jsonData?.customer_id) {", + " pm.test(", + " \"[POST]::/customers - Content check if value of 'customer_id' has a minimum length of '1'\",", + " function () {", + " pm.expect(jsonData.customer_id.length).is.at.least(1);", + " },", + " );", + "}", + "", + "// pm.collectionVariables - Set customer_id as variable for jsonData.customer_id", + "if (jsonData?.customer_id) {", + " pm.collectionVariables.set(\"customer_id\", jsonData.customer_id);", + " console.log(", + " \"- use {{customer_id}} as collection variable for value\",", + " jsonData.customer_id,", + " );", + "} else {", + " console.log(", + " \"INFO - Unable to assign variable {{customer_id}}, as jsonData.customer_id is undefined.\",", + " );", + "}", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw": "{\"email\":\"guest@example.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"First customer\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" + }, + "url": { + "raw": "{{baseUrl}}/customers", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "customers" + ] + }, + "description": "Create a customer object and store the customer details to be reused for future payments. Incase the customer already exists in the system, this API will respond with the customer details." + }, + "response": [] + }, { "name": "PaymentMethods - Create", "event": [ @@ -4341,7 +4743,7 @@ "language": "json" } }, - "raw": "{\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_issuer\":\"Visa\",\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"John Doe\"},\"customer_id\":\"cus_mnewerunwiuwiwqw\",\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" + "raw": "{\"payment_method\":\"card\",\"payment_method_type\":\"credit\",\"payment_method_issuer\":\"Visa\",\"card\":{\"card_number\":\"4242424242424242\",\"card_exp_month\":\"10\",\"card_exp_year\":\"25\",\"card_holder_name\":\"John Doe\"},\"customer_id\":\"{{customer_id}}\",\"metadata\":{\"city\":\"NY\",\"unit\":\"245\"}}" }, "url": { "raw": "{{baseUrl}}/payment_methods", From b5b3625efdd70c619e9fc206721c4bedf8af30a4 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 19:01:09 +0000 Subject: [PATCH 02/21] chore(version): v1.56.0 --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a3ef270b471..ddb0f6f983aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,35 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.56.0 (2023-10-11) + +### Features + +- **connector:** + - [Volt] Template generation ([#2480](https://github.com/juspay/hyperswitch/pull/2480)) ([`ee321bb`](https://github.com/juspay/hyperswitch/commit/ee321bb82686559643d8c2725b0491997af717b2)) + - [NexiNets] Update connector_response_reference_id as reference to merchant ([#2537](https://github.com/juspay/hyperswitch/pull/2537)) ([`2f6c00a`](https://github.com/juspay/hyperswitch/commit/2f6c00a1fd853876333608a7d1fa6b488c3001d3)) + - [Authorizedotnet] use connector_response_reference_id as reference to merchant ([#2497](https://github.com/juspay/hyperswitch/pull/2497)) ([`62638c4`](https://github.com/juspay/hyperswitch/commit/62638c4230bfd149c43c2805cbad0ce9be5386b3)) +- **router:** Change temp locker config as enable only ([#2522](https://github.com/juspay/hyperswitch/pull/2522)) ([`7acf101`](https://github.com/juspay/hyperswitch/commit/7acf10101435ab97d93490e19eaac5373d34f531)) + +### Refactors + +- Delete requires cvv config when merchant account is deleted ([#2525](https://github.com/juspay/hyperswitch/pull/2525)) ([`b968552`](https://github.com/juspay/hyperswitch/commit/b9685521735956659c50bc2e1c15b08cb9952aee)) + +### Testing + +- **postman:** + - Add proper `customer_id` in payment method create api ([#2548](https://github.com/juspay/hyperswitch/pull/2548)) ([`7994a12`](https://github.com/juspay/hyperswitch/commit/7994a1259c5852ba4ebabb906bef963c6cf81bc9)) + - Update postman collection files ([`7c561d5`](https://github.com/juspay/hyperswitch/commit/7c561d57767001e755fc9abfc32352ffdc9aacea)) + +### Miscellaneous Tasks + +- **CODEOWNERS:** Update CODEOWNERS ([#2541](https://github.com/juspay/hyperswitch/pull/2541)) ([`d9fb5d4`](https://github.com/juspay/hyperswitch/commit/d9fb5d4a52f44809ab4a1576a99e97b4c8b8c41b)) + +**Full Changelog:** [`v1.55.0...v1.56.0`](https://github.com/juspay/hyperswitch/compare/v1.55.0...v1.56.0) + +- - - + + ## 1.55.0 (2023-10-10) ### Features From 550377a6c3943d9fec4ca6a8be5a5f3aafe109ab Mon Sep 17 00:00:00 2001 From: Rutam Prita Mishra Date: Thu, 12 Oct 2023 02:38:54 +0530 Subject: [PATCH 03/21] feat(connector): [Tsys] Use `connector_response_reference_id` as reference to the connector (#2546) Co-authored-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com> --- crates/router/src/connector/tsys/transformers.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/router/src/connector/tsys/transformers.rs b/crates/router/src/connector/tsys/transformers.rs index 5aa4574c91e7..d8516c8293bd 100644 --- a/crates/router/src/connector/tsys/transformers.rs +++ b/crates/router/src/connector/tsys/transformers.rs @@ -192,12 +192,14 @@ fn get_error_response( fn get_payments_response(connector_response: TsysResponse) -> types::PaymentsResponseData { types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(connector_response.transaction_id), + resource_id: types::ResponseId::ConnectorTransactionId( + connector_response.transaction_id.clone(), + ), redirection_data: None, mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some(connector_response.transaction_id), } } @@ -215,7 +217,12 @@ fn get_payments_sync_response( mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: Some( + connector_response + .transaction_details + .transaction_id + .clone(), + ), } } From b78109bc93433e0886b0b8656231899df84da8cf Mon Sep 17 00:00:00 2001 From: Suraj <105744567+Suraj3240@users.noreply.github.com> Date: Thu, 12 Oct 2023 11:25:31 +0530 Subject: [PATCH 04/21] refactor(connector): [Worldpay] Currency Unit Conversion (#2436) Co-authored-by: Suraj --- crates/router/src/connector/worldpay.rs | 12 +++- .../src/connector/worldpay/transformers.rs | 66 ++++++++++++++++--- package.json | 2 +- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/crates/router/src/connector/worldpay.rs b/crates/router/src/connector/worldpay.rs index 0c69cd981bb3..60a5fc830453 100644 --- a/crates/router/src/connector/worldpay.rs +++ b/crates/router/src/connector/worldpay.rs @@ -56,6 +56,10 @@ impl ConnectorCommon for Worldpay { "worldpay" } + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Minor + } + fn common_get_content_type(&self) -> &'static str { "application/vnd.worldpay.payments-v6+json" } @@ -428,7 +432,13 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { - let connector_request = WorldpayPaymentsRequest::try_from(req)?; + let connector_router_data = worldpay::WorldpayRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.amount, + req, + ))?; + let connector_request = WorldpayPaymentsRequest::try_from(&connector_router_data)?; let worldpay_payment_request = types::RequestBody::log_and_get_request_body( &connector_request, ext_traits::Encode::::encode_to_string_of_json, diff --git a/crates/router/src/connector/worldpay/transformers.rs b/crates/router/src/connector/worldpay/transformers.rs index 3d467f4198f7..aabe27fc4eb1 100644 --- a/crates/router/src/connector/worldpay/transformers.rs +++ b/crates/router/src/connector/worldpay/transformers.rs @@ -3,15 +3,44 @@ use common_utils::errors::CustomResult; use diesel_models::enums; use error_stack::{IntoReport, ResultExt}; use masking::{PeekInterface, Secret}; +use serde::Serialize; use super::{requests::*, response::*}; use crate::{ connector::utils, consts, core::errors, - types::{self, api}, + types::{self, api, PaymentsAuthorizeData, PaymentsResponseData}, }; +#[derive(Debug, Serialize)] +pub struct WorldpayRouterData { + amount: i64, + router_data: T, +} +impl + TryFrom<( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + )> for WorldpayRouterData +{ + type Error = error_stack::Report; + fn try_from( + (_currency_unit, _currency, amount, item): ( + &types::api::CurrencyUnit, + types::storage::enums::Currency, + i64, + T, + ), + ) -> Result { + Ok(Self { + amount, + router_data: item, + }) + } +} fn fetch_payment_instrument( payment_method: api::PaymentMethodData, ) -> CustomResult { @@ -100,29 +129,48 @@ fn fetch_payment_instrument( } } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for WorldpayPaymentsRequest { +impl + TryFrom< + &WorldpayRouterData< + &types::RouterData< + types::api::payments::Authorize, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + >, + > for WorldpayPaymentsRequest +{ type Error = error_stack::Report; - fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + + fn try_from( + item: &WorldpayRouterData< + &types::RouterData< + types::api::payments::Authorize, + PaymentsAuthorizeData, + PaymentsResponseData, + >, + >, + ) -> Result { Ok(Self { instruction: Instruction { value: PaymentValue { - amount: item.request.amount, - currency: item.request.currency.to_string(), + amount: item.amount, + currency: item.router_data.request.currency.to_string(), }, narrative: InstructionNarrative { - line1: item.merchant_id.clone().replace('_', "-"), + line1: item.router_data.merchant_id.clone().replace('_', "-"), ..Default::default() }, payment_instrument: fetch_payment_instrument( - item.request.payment_method_data.clone(), + item.router_data.request.payment_method_data.clone(), )?, debt_repayment: None, }, merchant: Merchant { - entity: item.attempt_id.clone().replace('_', "-"), + entity: item.router_data.attempt_id.clone().replace('_', "-"), ..Default::default() }, - transaction_reference: item.attempt_id.clone(), + transaction_reference: item.router_data.attempt_id.clone(), channel: None, customer: None, }) diff --git a/package.json b/package.json index 0766efdcc17a..45d2be3153b1 100644 --- a/package.json +++ b/package.json @@ -6,4 +6,4 @@ "devDependencies": { "newman": "git+ssh://git@github.com:knutties/newman.git#7106e194c15d49d066fa09d9a2f18b2238f3dba8" } -} +} \ No newline at end of file From 81cb8da4d47fe2a75330d39c665bb259faa35b00 Mon Sep 17 00:00:00 2001 From: Sahal Saad Date: Thu, 12 Oct 2023 13:57:50 +0800 Subject: [PATCH 05/21] feat(connector): [Cybersource] Use connector_request_reference_id as reference to the connector (#2512) Co-authored-by: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> --- .../src/connector/cybersource/transformers.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 3558f7528414..5a3060f99ebd 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -21,6 +21,7 @@ pub struct CybersourcePaymentsRequest { processing_information: ProcessingInformation, payment_information: PaymentInformation, order_information: OrderInformationWithBill, + client_reference_information: ClientReferenceInformation, } #[derive(Default, Debug, Serialize, Eq, PartialEq)] @@ -150,10 +151,15 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for CybersourcePaymentsRequest capture_options: None, }; + let client_reference_information = ClientReferenceInformation { + code: Some(item.connector_request_reference_id.clone()), + }; + Ok(Self { processing_information, payment_information, order_information, + client_reference_information, }) } _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), @@ -179,6 +185,9 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for CybersourcePaymentsRequest { }, ..Default::default() }, + client_reference_information: ClientReferenceInformation { + code: Some(value.connector_request_reference_id.clone()), + }, ..Default::default() }) } @@ -195,6 +204,9 @@ impl TryFrom<&types::RefundExecuteRouterData> for CybersourcePaymentsRequest { }, ..Default::default() }, + client_reference_information: ClientReferenceInformation { + code: Some(value.connector_request_reference_id.clone()), + }, ..Default::default() }) } @@ -278,7 +290,7 @@ pub struct CybersourcePaymentsResponse { client_reference_information: Option, } -#[derive(Default, Debug, Clone, Deserialize, Eq, PartialEq)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ClientReferenceInformation { code: Option, From 9f446bc1742c06a7fab3d92128ba4e7d3be80ea6 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:12:51 +0530 Subject: [PATCH 06/21] feat(router): add kv implementation for update address in update payments flow (#2542) --- crates/diesel_models/src/address.rs | 2 +- crates/diesel_models/src/kv.rs | 9 +- crates/diesel_models/src/query/address.rs | 26 ++++ crates/drainer/src/lib.rs | 5 + crates/router/src/core/payments/helpers.rs | 23 +++- crates/router/src/db/address.rs | 140 +++++++++++++++++++++ crates/router/src/types/domain/address.rs | 2 +- crates/router/src/types/storage/kv.rs | 4 +- 8 files changed, 203 insertions(+), 8 deletions(-) diff --git a/crates/diesel_models/src/address.rs b/crates/diesel_models/src/address.rs index 028d878f7da9..569df3f551ca 100644 --- a/crates/diesel_models/src/address.rs +++ b/crates/diesel_models/src/address.rs @@ -49,7 +49,7 @@ pub struct Address { pub payment_id: Option, } -#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] +#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay, Serialize, Deserialize)] #[diesel(table_name = address)] pub struct AddressUpdateInternal { pub city: Option, diff --git a/crates/diesel_models/src/kv.rs b/crates/diesel_models/src/kv.rs index 93d045bd71a0..ed886c783f97 100644 --- a/crates/diesel_models/src/kv.rs +++ b/crates/diesel_models/src/kv.rs @@ -2,7 +2,7 @@ use error_stack::{IntoReport, ResultExt}; use serde::{Deserialize, Serialize}; use crate::{ - address::AddressNew, + address::{Address, AddressNew, AddressUpdateInternal}, connector_response::{ConnectorResponse, ConnectorResponseNew, ConnectorResponseUpdate}, errors, payment_attempt::{PaymentAttempt, PaymentAttemptNew, PaymentAttemptUpdate}, @@ -54,6 +54,7 @@ pub enum Updateable { PaymentAttemptUpdate(PaymentAttemptUpdateMems), RefundUpdate(RefundUpdateMems), ConnectorResponseUpdate(ConnectorResponseUpdateMems), + AddressUpdate(Box), } #[derive(Debug, Serialize, Deserialize)] @@ -62,6 +63,12 @@ pub struct ConnectorResponseUpdateMems { pub update_data: ConnectorResponseUpdate, } +#[derive(Debug, Serialize, Deserialize)] +pub struct AddressUpdateMems { + pub orig: Address, + pub update_data: AddressUpdateInternal, +} + #[derive(Debug, Serialize, Deserialize)] pub struct PaymentIntentUpdateMems { pub orig: PaymentIntent, diff --git a/crates/diesel_models/src/query/address.rs b/crates/diesel_models/src/query/address.rs index 9a4f20942ccd..ada476229909 100644 --- a/crates/diesel_models/src/query/address.rs +++ b/crates/diesel_models/src/query/address.rs @@ -56,6 +56,32 @@ impl Address { } } + #[instrument(skip(conn))] + pub async fn update( + self, + conn: &PgPooledConn, + address_update_internal: AddressUpdateInternal, + ) -> StorageResult { + match generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + dsl::address_id.eq(self.address_id.clone()), + address_update_internal, + ) + .await + { + Err(error) => match error.current_context() { + errors::DatabaseError::NoFieldsToUpdate => Ok(self), + _ => Err(error), + }, + result => result, + } + } + #[instrument(skip(conn))] pub async fn delete_by_address_id( conn: &PgPooledConn, diff --git a/crates/drainer/src/lib.rs b/crates/drainer/src/lib.rs index e09c56540572..632fb4a0c146 100644 --- a/crates/drainer/src/lib.rs +++ b/crates/drainer/src/lib.rs @@ -271,6 +271,11 @@ async fn drainer( update_op, connector_response ), + kv::Updateable::AddressUpdate(a) => macro_util::handle_resp!( + a.orig.update(&conn, a.update_data).await, + update_op, + address + ), } }) .await; diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 143b9923958c..a4cb7b2b9e65 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -181,10 +181,27 @@ pub async fn create_or_update_address_for_payment_by_request( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while encrypting address")?; + let address = db + .find_address_by_merchant_id_payment_id_address_id( + merchant_id, + payment_id, + id, + merchant_key_store, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while fetching address")?; Some( - db.update_address(id.to_owned(), address_update, merchant_key_store) - .await - .to_not_found_response(errors::ApiErrorResponse::AddressNotFound)?, + db.update_address_for_payments( + address, + address_update, + payment_id.to_string(), + merchant_key_store, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::AddressNotFound)?, ) } None => Some( diff --git a/crates/router/src/db/address.rs b/crates/router/src/db/address.rs index dda6838c9739..0b7bffda2f82 100644 --- a/crates/router/src/db/address.rs +++ b/crates/router/src/db/address.rs @@ -28,6 +28,15 @@ where key_store: &domain::MerchantKeyStore, ) -> CustomResult; + async fn update_address_for_payments( + &self, + this: domain::Address, + address: domain::AddressUpdate, + payment_id: String, + key_store: &domain::MerchantKeyStore, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult; + async fn find_address_by_address_id( &self, address_id: &str, @@ -155,6 +164,32 @@ mod storage { .await } + async fn update_address_for_payments( + &self, + this: domain::Address, + address_update: domain::AddressUpdate, + _payment_id: String, + key_store: &domain::MerchantKeyStore, + _storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + let address = Conversion::convert(this) + .await + .change_context(errors::StorageError::EncryptionError)?; + address + .update(&conn, address_update.into()) + .await + .map_err(Into::into) + .into_report() + .async_and_then(|address| async { + address + .convert(key_store.key.get_inner()) + .await + .change_context(errors::StorageError::DecryptionError) + }) + .await + } + async fn insert_address_for_payments( &self, _payment_id: &str, @@ -241,6 +276,7 @@ mod storage { mod storage { use common_utils::ext_traits::AsyncExt; use data_models::MerchantStorageScheme; + use diesel_models::AddressUpdateInternal; use error_stack::{IntoReport, ResultExt}; use redis_interface::HsetnxReply; use router_env::{instrument, tracing}; @@ -348,6 +384,79 @@ mod storage { .await } + async fn update_address_for_payments( + &self, + this: domain::Address, + address_update: domain::AddressUpdate, + payment_id: String, + key_store: &domain::MerchantKeyStore, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + let address = Conversion::convert(this) + .await + .change_context(errors::StorageError::EncryptionError)?; + match storage_scheme { + MerchantStorageScheme::PostgresOnly => { + address + .update(&conn, address_update.into()) + .await + .map_err(Into::into) + .into_report() + .async_and_then(|address| async { + address + .convert(key_store.key.get_inner()) + .await + .change_context(errors::StorageError::DecryptionError) + }) + .await + } + MerchantStorageScheme::RedisKv => { + let key = format!("mid_{}_pid_{}", address.merchant_id.clone(), payment_id); + let field = format!("add_{}", address.address_id); + let updated_address = AddressUpdateInternal::from(address_update.clone()) + .create_address(address.clone()); + let redis_value = serde_json::to_string(&updated_address) + .into_report() + .change_context(errors::StorageError::KVError)?; + kv_wrapper::<(), _, _>( + self, + KvOperation::Hset::((&field, redis_value)), + &key, + ) + .await + .change_context(errors::StorageError::KVError)? + .try_into_hset() + .change_context(errors::StorageError::KVError)?; + + let redis_entry = kv::TypedSql { + op: kv::DBOperation::Update { + updatable: kv::Updateable::AddressUpdate(Box::new( + kv::AddressUpdateMems { + orig: address, + update_data: address_update.into(), + }, + )), + }, + }; + + self.push_to_drainer_stream::( + redis_entry, + PartitionKey::MerchantIdPaymentId { + merchant_id: &updated_address.merchant_id, + payment_id: &payment_id, + }, + ) + .await + .change_context(errors::StorageError::KVError)?; + updated_address + .convert(key_store.key.get_inner()) + .await + .change_context(errors::StorageError::DecryptionError) + } + } + } + async fn insert_address_for_payments( &self, payment_id: &str, @@ -584,6 +693,37 @@ impl AddressInterface for MockDb { } } + async fn update_address_for_payments( + &self, + this: domain::Address, + address_update: domain::AddressUpdate, + _payment_id: String, + key_store: &domain::MerchantKeyStore, + _storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + match self + .addresses + .lock() + .await + .iter_mut() + .find(|address| address.address_id == this.address_id) + .map(|a| { + let address_updated = + AddressUpdateInternal::from(address_update).create_address(a.clone()); + *a = address_updated.clone(); + address_updated + }) { + Some(address_updated) => address_updated + .convert(key_store.key.get_inner()) + .await + .change_context(errors::StorageError::DecryptionError), + None => Err(errors::StorageError::ValueNotFound( + "cannot find address to update".to_string(), + ) + .into()), + } + } + async fn insert_address_for_payments( &self, _payment_id: &str, diff --git a/crates/router/src/types/domain/address.rs b/crates/router/src/types/domain/address.rs index fadd83b34e09..bd9d034e8c98 100644 --- a/crates/router/src/types/domain/address.rs +++ b/crates/router/src/types/domain/address.rs @@ -125,7 +125,7 @@ impl behaviour::Conversion for Address { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum AddressUpdate { Update { city: Option, diff --git a/crates/router/src/types/storage/kv.rs b/crates/router/src/types/storage/kv.rs index 51c9eb2b42e9..2afc73e6637d 100644 --- a/crates/router/src/types/storage/kv.rs +++ b/crates/router/src/types/storage/kv.rs @@ -1,4 +1,4 @@ pub use diesel_models::kv::{ - ConnectorResponseUpdateMems, DBOperation, Insertable, PaymentAttemptUpdateMems, - PaymentIntentUpdateMems, RefundUpdateMems, TypedSql, Updateable, + AddressUpdateMems, ConnectorResponseUpdateMems, DBOperation, Insertable, + PaymentAttemptUpdateMems, PaymentIntentUpdateMems, RefundUpdateMems, TypedSql, Updateable, }; From 9ef60e425d0cbe764ce66c65c8c09b1992cbe99f Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:22:48 +0530 Subject: [PATCH 07/21] fix(connector): [noon] sync with reference_id (#2544) --- crates/router/src/connector/noon.rs | 20 +++++++++++++------ .../router/src/connector/noon/transformers.rs | 18 +++++++++++++---- .../router/src/core/payments/transformers.rs | 5 +++++ crates/router/src/types.rs | 3 +++ crates/router/tests/connectors/bambora.rs | 2 ++ crates/router/tests/connectors/forte.rs | 1 + crates/router/tests/connectors/nexinets.rs | 1 + crates/router/tests/connectors/paypal.rs | 2 ++ crates/router/tests/connectors/utils.rs | 1 + crates/router/tests/connectors/zen.rs | 2 ++ 10 files changed, 45 insertions(+), 10 deletions(-) diff --git a/crates/router/src/connector/noon.rs b/crates/router/src/connector/noon.rs index 5bd7f2495408..c2637ba93b2a 100644 --- a/crates/router/src/connector/noon.rs +++ b/crates/router/src/connector/noon.rs @@ -9,7 +9,6 @@ use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as noon; -use super::utils::PaymentsSyncRequestData; use crate::{ configs::settings, connector::utils as connector_utils, @@ -276,10 +275,19 @@ impl ConnectorIntegration CustomResult { - let connector_transaction_id = req.request.get_connector_transaction_id()?; + //Added as a fix for past payments before the given timestamp we can reconcile using payment_id + let cutoff_timestamp: i64 = 1697023800; + + let reference_id = if req.request.payment_attempt_created_at_as_utc < cutoff_timestamp { + req.payment_id.clone() + } else { + req.attempt_id.clone() + }; + Ok(format!( - "{}payment/v1/order/{connector_transaction_id}", - self.base_url(connectors) + "{}payment/v1/order/getbyreference/{}", + self.base_url(connectors), + reference_id )) } @@ -569,9 +577,9 @@ impl ConnectorIntegration CustomResult { Ok(format!( - "{}payment/v1/order/{}", + "{}payment/v1/order/getbyreference/{}", self.base_url(connectors), - req.request.connector_transaction_id + req.attempt_id )) } diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index 5300525b7cbd..749c8c8dea7b 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -4,8 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::{ connector::utils::{ - self as conn_utils, CardData, PaymentsAuthorizeRequestData, RefundsRequestData, RouterData, - WalletData, + self as conn_utils, CardData, PaymentsAuthorizeRequestData, RouterData, WalletData, }, core::errors, services, @@ -438,6 +437,7 @@ impl pub struct NoonActionTransaction { amount: String, currency: diesel_models::enums::Currency, + transaction_reference: Option, } #[derive(Debug, Serialize)] @@ -466,6 +466,7 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for NoonPaymentsActionRequest { item.request.currency, )?, currency: item.request.currency, + transaction_reference: None, }; Ok(Self { api_operation: NoonApiOperations::Capture, @@ -507,6 +508,7 @@ impl TryFrom<&types::RefundsRouterData> for NoonPaymentsActionRequest { item.request.currency, )?, currency: item.request.currency, + transaction_reference: Some(item.request.refund_id.clone()), }; Ok(Self { api_operation: NoonApiOperations::Refund, @@ -570,9 +572,11 @@ impl TryFrom> } #[derive(Default, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct NoonRefundResponseTransactions { id: String, status: RefundStatus, + transaction_reference: Option, } #[derive(Default, Debug, Deserialize)] @@ -592,13 +596,19 @@ impl TryFrom> fn try_from( item: types::RefundsResponseRouterData, ) -> Result { - let connector_refund_id = item.data.request.get_connector_refund_id()?; let noon_transaction: &NoonRefundResponseTransactions = item .response .result .transactions .iter() - .find(|transaction| transaction.id == connector_refund_id) + .find(|transaction| { + transaction + .transaction_reference + .clone() + .map_or(false, |transaction_instance| { + transaction_instance == item.data.request.refund_id + }) + }) .ok_or(errors::ConnectorError::ResponseHandlingFailed)?; Ok(Self { diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index e1ea8a063592..8526f48a590c 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1050,6 +1050,11 @@ impl TryFrom> for types::PaymentsSyncData ), None => types::SyncRequestType::SinglePaymentSync, }, + payment_attempt_created_at_as_utc: payment_data + .payment_attempt + .created_at + .assume_utc() + .unix_timestamp(), }) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 560d706a8403..cb8fe8fa7979 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -476,6 +476,9 @@ pub struct PaymentsSyncData { pub connector_meta: Option, pub sync_type: SyncRequestType, pub mandate_id: Option, + //This is being added as a temporary fix, will be deprecated before or by v1.65.0 + // #2628 + pub payment_attempt_created_at_as_utc: i64, } #[derive(Debug, Default, Clone)] diff --git a/crates/router/tests/connectors/bambora.rs b/crates/router/tests/connectors/bambora.rs index 5638214202b4..73e75b713645 100644 --- a/crates/router/tests/connectors/bambora.rs +++ b/crates/router/tests/connectors/bambora.rs @@ -108,6 +108,7 @@ async fn should_sync_authorized_payment() { capture_method: Some(diesel_models::enums::CaptureMethod::Manual), sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta: None, + payment_attempt_created_at_as_utc: 0, }), None, ) @@ -222,6 +223,7 @@ async fn should_sync_auto_captured_payment() { capture_method: Some(enums::CaptureMethod::Automatic), sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta: None, + payment_attempt_created_at_as_utc: 0, }), None, ) diff --git a/crates/router/tests/connectors/forte.rs b/crates/router/tests/connectors/forte.rs index 703b8900b69b..8cce94dc85ae 100644 --- a/crates/router/tests/connectors/forte.rs +++ b/crates/router/tests/connectors/forte.rs @@ -153,6 +153,7 @@ async fn should_sync_authorized_payment() { sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta: None, mandate_id: None, + payment_attempt_created_at_as_utc: 0, }), get_default_payment_info(), ) diff --git a/crates/router/tests/connectors/nexinets.rs b/crates/router/tests/connectors/nexinets.rs index f9b8affb0626..5c95b1f41a9d 100644 --- a/crates/router/tests/connectors/nexinets.rs +++ b/crates/router/tests/connectors/nexinets.rs @@ -123,6 +123,7 @@ async fn should_sync_authorized_payment() { sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta, mandate_id: None, + payment_attempt_created_at_as_utc: 0, }), None, ) diff --git a/crates/router/tests/connectors/paypal.rs b/crates/router/tests/connectors/paypal.rs index e9df65566930..5fe37c374a68 100644 --- a/crates/router/tests/connectors/paypal.rs +++ b/crates/router/tests/connectors/paypal.rs @@ -140,6 +140,7 @@ async fn should_sync_authorized_payment() { capture_method: None, sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta, + payment_attempt_created_at_as_utc: 0, }), get_default_payment_info(), ) @@ -336,6 +337,7 @@ async fn should_sync_auto_captured_payment() { capture_method: Some(enums::CaptureMethod::Automatic), sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta, + payment_attempt_created_at_as_utc: 0, }), get_default_payment_info(), ) diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index f890eec192f2..81f4f6bef29d 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -942,6 +942,7 @@ impl Default for PaymentSyncType { capture_method: None, sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta: None, + payment_attempt_created_at_as_utc: 0, }; Self(data) } diff --git a/crates/router/tests/connectors/zen.rs b/crates/router/tests/connectors/zen.rs index ca71fd0c6221..a994c290dd9a 100644 --- a/crates/router/tests/connectors/zen.rs +++ b/crates/router/tests/connectors/zen.rs @@ -102,6 +102,7 @@ async fn should_sync_authorized_payment() { sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta: None, mandate_id: None, + payment_attempt_created_at_as_utc: 0, }), None, ) @@ -216,6 +217,7 @@ async fn should_sync_auto_captured_payment() { sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta: None, mandate_id: None, + payment_attempt_created_at_as_utc: 0, }), None, ) From ef647b7ab942707a06971b6545c81168f28cb94c Mon Sep 17 00:00:00 2001 From: SomeYoGuy <59766344+SoumyoPurkayastha@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:30:44 +0530 Subject: [PATCH 08/21] feat(connector): [Iatapay] use connector_response_reference_id as reference to connector (#2524) Co-authored-by: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Co-authored-by: Arjun Karthik --- crates/router/src/connector/iatapay/transformers.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/router/src/connector/iatapay/transformers.rs b/crates/router/src/connector/iatapay/transformers.rs index 39dfd61b9a3e..9d4ecdff197f 100644 --- a/crates/router/src/connector/iatapay/transformers.rs +++ b/crates/router/src/connector/iatapay/transformers.rs @@ -212,10 +212,14 @@ impl item: types::ResponseRouterData, ) -> Result { let form_fields = HashMap::new(); - let id = match item.response.iata_payment_id { + let id = match item.response.iata_payment_id.clone() { Some(s) => types::ResponseId::ConnectorTransactionId(s), None => types::ResponseId::NoResponseId, }; + let connector_response_reference_id = item + .response + .merchant_payment_id + .or(item.response.iata_payment_id); Ok(Self { status: enums::AttemptStatus::from(item.response.status), response: item.response.checkout_methods.map_or( @@ -225,7 +229,7 @@ impl mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: connector_response_reference_id.clone(), }), |checkout_methods| { Ok(types::PaymentsResponseData::TransactionResponse { @@ -238,7 +242,7 @@ impl mandate_reference: None, connector_metadata: None, network_txn_id: None, - connector_response_reference_id: None, + connector_response_reference_id: connector_response_reference_id.clone(), }) }, ), From 1ee11849d4a60afbf3d05103cb491a11e905b811 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Thu, 12 Oct 2023 13:06:09 +0530 Subject: [PATCH 09/21] fix: percentage float inconsistency problem and api models changes to support surcharge feature (#2550) --- crates/api_models/Cargo.toml | 1 + crates/api_models/src/lib.rs | 1 - crates/api_models/src/payment_methods.rs | 46 ++++- crates/api_models/src/payments.rs | 162 ++++++++++++++++++ crates/common_utils/src/lib.rs | 1 + .../{api_models => common_utils}/src/types.rs | 62 ++++--- .../tests/percentage.rs | 78 +++++++-- .../src/payments/payment_attempt.rs | 4 + crates/diesel_models/src/payment_attempt.rs | 14 ++ .../router/src/core/payment_methods/cards.rs | 1 + crates/router/src/core/payments.rs | 27 ++- .../payments/operations/payment_approve.rs | 1 + .../payments/operations/payment_cancel.rs | 1 + .../payments/operations/payment_capture.rs | 1 + .../operations/payment_complete_authorize.rs | 1 + .../payments/operations/payment_confirm.rs | 1 + .../payments/operations/payment_create.rs | 1 + .../operations/payment_method_validate.rs | 1 + .../payments/operations/payment_reject.rs | 1 + .../payments/operations/payment_session.rs | 1 + .../core/payments/operations/payment_start.rs | 1 + .../payments/operations/payment_status.rs | 1 + .../payments/operations/payment_update.rs | 1 + .../router/src/core/payments/transformers.rs | 2 + crates/router/src/types.rs | 3 + crates/router/tests/connectors/aci.rs | 1 + crates/router/tests/connectors/adyen.rs | 1 + crates/router/tests/connectors/bitpay.rs | 1 + crates/router/tests/connectors/cashtocode.rs | 1 + crates/router/tests/connectors/coinbase.rs | 1 + crates/router/tests/connectors/cryptopay.rs | 1 + crates/router/tests/connectors/opennode.rs | 1 + crates/router/tests/connectors/utils.rs | 1 + crates/router/tests/connectors/worldline.rs | 1 + .../src/payments/payment_attempt.rs | 14 ++ 35 files changed, 390 insertions(+), 47 deletions(-) rename crates/{api_models => common_utils}/src/types.rs (52%) rename crates/{api_models => common_utils}/tests/percentage.rs (60%) diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index 662129204c29..ce61d30d36f5 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -21,6 +21,7 @@ mime = "0.3.17" reqwest = { version = "0.11.18", optional = true } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" +serde_with = "3.0.0" strum = { version = "0.24.1", features = ["derive"] } time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } url = { version = "2.4.0", features = ["serde"] } diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index caff4e1780c7..29ad9be051b6 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -16,6 +16,5 @@ pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; pub mod refunds; -pub mod types; pub mod verifications; pub mod webhooks; diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index eb15f0e3bc3a..9d3983e73a1f 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -3,8 +3,10 @@ use std::collections::HashMap; use cards::CardNumber; use common_utils::{ consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, crypto::OptionalEncryptableName, pii, + types::Percentage, }; use serde::de; +use serde_with::serde_as; use utoipa::ToSchema; #[cfg(feature = "payouts")] @@ -12,7 +14,6 @@ use crate::payouts; use crate::{ admin, enums as api_enums, payments::{self, BankCodeResponse}, - types::Percentage, }; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -251,12 +252,28 @@ pub struct PaymentExperienceTypes { pub eligible_connectors: Vec, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, PartialEq, Eq)] +#[derive(Debug, Clone, serde::Serialize, ToSchema, PartialEq)] pub struct CardNetworkTypes { /// The card network enabled #[schema(value_type = Option, example = "Visa")] pub card_network: api_enums::CardNetwork, + /// surcharge details for this card network + #[schema(example = r#" + { + "surcharge": { + "type": "rate", + "value": { + "percentage": 2.5 + } + }, + "tax_on_surcharge": { + "percentage": 1.5 + } + } + "#)] + pub surcharge_details: Option, + /// The list of eligible connectors for a given card network #[schema(example = json!(["stripe", "adyen"]))] pub eligible_connectors: Vec, @@ -304,18 +321,31 @@ pub struct ResponsePaymentMethodTypes { } } "#)] - pub surcharge_details: Option, + pub surcharge_details: Option, } -#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] -pub struct SurchargeDetails { +pub struct SurchargeDetailsResponse { /// surcharge value - surcharge: Surcharge, + pub surcharge: Surcharge, /// tax on surcharge value - tax_on_surcharge: Option>, + pub tax_on_surcharge: Option>, + /// surcharge amount for this payment + pub surcharge_amount: i64, + /// tax on surcharge amount for this payment + pub tax_on_surcharge_amount: i64, + /// sum of original amount, + pub final_amount: i64, +} + +#[serde_as] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct SurchargeMetadata { + #[serde_as(as = "HashMap<_, _>")] + pub surcharge_results: HashMap, } -#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] #[serde(rename_all = "snake_case", tag = "type", content = "value")] pub enum Surcharge { /// Fixed Surcharge value diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 01edef87a67a..08646a4a0b3c 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -795,6 +795,168 @@ pub enum PaymentMethodData { GiftCard(Box), } +pub trait GetPaymentMethodType { + fn get_payment_method_type(&self) -> api_enums::PaymentMethodType; +} + +impl GetPaymentMethodType for CardRedirectData { + fn get_payment_method_type(&self) -> api_enums::PaymentMethodType { + match self { + Self::Knet {} => api_enums::PaymentMethodType::Knet, + Self::Benefit {} => api_enums::PaymentMethodType::Benefit, + Self::MomoAtm {} => api_enums::PaymentMethodType::MomoAtm, + } + } +} + +impl GetPaymentMethodType for WalletData { + fn get_payment_method_type(&self) -> api_enums::PaymentMethodType { + match self { + Self::AliPayQr(_) | Self::AliPayRedirect(_) => api_enums::PaymentMethodType::AliPay, + Self::AliPayHkRedirect(_) => api_enums::PaymentMethodType::AliPayHk, + Self::MomoRedirect(_) => api_enums::PaymentMethodType::Momo, + Self::KakaoPayRedirect(_) => api_enums::PaymentMethodType::KakaoPay, + Self::GoPayRedirect(_) => api_enums::PaymentMethodType::GoPay, + Self::GcashRedirect(_) => api_enums::PaymentMethodType::Gcash, + Self::ApplePay(_) | Self::ApplePayRedirect(_) | Self::ApplePayThirdPartySdk(_) => { + api_enums::PaymentMethodType::ApplePay + } + Self::DanaRedirect {} => api_enums::PaymentMethodType::Dana, + Self::GooglePay(_) | Self::GooglePayRedirect(_) | Self::GooglePayThirdPartySdk(_) => { + api_enums::PaymentMethodType::GooglePay + } + Self::MbWayRedirect(_) => api_enums::PaymentMethodType::MbWay, + Self::MobilePayRedirect(_) => api_enums::PaymentMethodType::MobilePay, + Self::PaypalRedirect(_) | Self::PaypalSdk(_) => api_enums::PaymentMethodType::Paypal, + Self::SamsungPay(_) => api_enums::PaymentMethodType::SamsungPay, + Self::TwintRedirect {} => api_enums::PaymentMethodType::Twint, + Self::VippsRedirect {} => api_enums::PaymentMethodType::Vipps, + Self::TouchNGoRedirect(_) => api_enums::PaymentMethodType::TouchNGo, + Self::WeChatPayRedirect(_) | Self::WeChatPayQr(_) => { + api_enums::PaymentMethodType::WeChatPay + } + Self::CashappQr(_) => api_enums::PaymentMethodType::Cashapp, + Self::SwishQr(_) => api_enums::PaymentMethodType::Swish, + } + } +} + +impl GetPaymentMethodType for PayLaterData { + fn get_payment_method_type(&self) -> api_enums::PaymentMethodType { + match self { + Self::KlarnaRedirect { .. } => api_enums::PaymentMethodType::Klarna, + Self::KlarnaSdk { .. } => api_enums::PaymentMethodType::Klarna, + Self::AffirmRedirect {} => api_enums::PaymentMethodType::Affirm, + Self::AfterpayClearpayRedirect { .. } => api_enums::PaymentMethodType::AfterpayClearpay, + Self::PayBrightRedirect {} => api_enums::PaymentMethodType::PayBright, + Self::WalleyRedirect {} => api_enums::PaymentMethodType::Walley, + Self::AlmaRedirect {} => api_enums::PaymentMethodType::Alma, + Self::AtomeRedirect {} => api_enums::PaymentMethodType::Atome, + } + } +} + +impl GetPaymentMethodType for BankRedirectData { + fn get_payment_method_type(&self) -> api_enums::PaymentMethodType { + match self { + Self::BancontactCard { .. } => api_enums::PaymentMethodType::BancontactCard, + Self::Bizum {} => api_enums::PaymentMethodType::Bizum, + Self::Blik { .. } => api_enums::PaymentMethodType::Blik, + Self::Eps { .. } => api_enums::PaymentMethodType::Eps, + Self::Giropay { .. } => api_enums::PaymentMethodType::Giropay, + Self::Ideal { .. } => api_enums::PaymentMethodType::Ideal, + Self::Interac { .. } => api_enums::PaymentMethodType::Interac, + Self::OnlineBankingCzechRepublic { .. } => { + api_enums::PaymentMethodType::OnlineBankingCzechRepublic + } + Self::OnlineBankingFinland { .. } => api_enums::PaymentMethodType::OnlineBankingFinland, + Self::OnlineBankingPoland { .. } => api_enums::PaymentMethodType::OnlineBankingPoland, + Self::OnlineBankingSlovakia { .. } => { + api_enums::PaymentMethodType::OnlineBankingSlovakia + } + Self::OpenBankingUk { .. } => api_enums::PaymentMethodType::OpenBankingUk, + Self::Przelewy24 { .. } => api_enums::PaymentMethodType::Przelewy24, + Self::Sofort { .. } => api_enums::PaymentMethodType::Sofort, + Self::Trustly { .. } => api_enums::PaymentMethodType::Trustly, + Self::OnlineBankingFpx { .. } => api_enums::PaymentMethodType::OnlineBankingFpx, + Self::OnlineBankingThailand { .. } => { + api_enums::PaymentMethodType::OnlineBankingThailand + } + } + } +} + +impl GetPaymentMethodType for BankDebitData { + fn get_payment_method_type(&self) -> api_enums::PaymentMethodType { + match self { + Self::AchBankDebit { .. } => api_enums::PaymentMethodType::Ach, + Self::SepaBankDebit { .. } => api_enums::PaymentMethodType::Sepa, + Self::BecsBankDebit { .. } => api_enums::PaymentMethodType::Becs, + Self::BacsBankDebit { .. } => api_enums::PaymentMethodType::Bacs, + } + } +} + +impl GetPaymentMethodType for BankTransferData { + fn get_payment_method_type(&self) -> api_enums::PaymentMethodType { + match self { + Self::AchBankTransfer { .. } => api_enums::PaymentMethodType::Ach, + Self::SepaBankTransfer { .. } => api_enums::PaymentMethodType::Sepa, + Self::BacsBankTransfer { .. } => api_enums::PaymentMethodType::Bacs, + Self::MultibancoBankTransfer { .. } => api_enums::PaymentMethodType::Multibanco, + Self::PermataBankTransfer { .. } => api_enums::PaymentMethodType::PermataBankTransfer, + Self::BcaBankTransfer { .. } => api_enums::PaymentMethodType::BcaBankTransfer, + Self::BniVaBankTransfer { .. } => api_enums::PaymentMethodType::BniVa, + Self::BriVaBankTransfer { .. } => api_enums::PaymentMethodType::BriVa, + Self::CimbVaBankTransfer { .. } => api_enums::PaymentMethodType::CimbVa, + Self::DanamonVaBankTransfer { .. } => api_enums::PaymentMethodType::DanamonVa, + Self::MandiriVaBankTransfer { .. } => api_enums::PaymentMethodType::MandiriVa, + Self::Pix {} => api_enums::PaymentMethodType::Pix, + Self::Pse {} => api_enums::PaymentMethodType::Pse, + } + } +} + +impl GetPaymentMethodType for CryptoData { + fn get_payment_method_type(&self) -> api_enums::PaymentMethodType { + api_enums::PaymentMethodType::CryptoCurrency + } +} + +impl GetPaymentMethodType for UpiData { + fn get_payment_method_type(&self) -> api_enums::PaymentMethodType { + api_enums::PaymentMethodType::UpiCollect + } +} +impl GetPaymentMethodType for VoucherData { + fn get_payment_method_type(&self) -> api_enums::PaymentMethodType { + match self { + Self::Boleto(_) => api_enums::PaymentMethodType::Boleto, + Self::Efecty => api_enums::PaymentMethodType::Efecty, + Self::PagoEfectivo => api_enums::PaymentMethodType::PagoEfectivo, + Self::RedCompra => api_enums::PaymentMethodType::RedCompra, + Self::RedPagos => api_enums::PaymentMethodType::RedPagos, + Self::Alfamart(_) => api_enums::PaymentMethodType::Alfamart, + Self::Indomaret(_) => api_enums::PaymentMethodType::Indomaret, + Self::Oxxo => api_enums::PaymentMethodType::Oxxo, + Self::SevenEleven(_) => api_enums::PaymentMethodType::SevenEleven, + Self::Lawson(_) => api_enums::PaymentMethodType::Lawson, + Self::MiniStop(_) => api_enums::PaymentMethodType::MiniStop, + Self::FamilyMart(_) => api_enums::PaymentMethodType::FamilyMart, + Self::Seicomart(_) => api_enums::PaymentMethodType::Seicomart, + Self::PayEasy(_) => api_enums::PaymentMethodType::PayEasy, + } + } +} +impl GetPaymentMethodType for GiftCardData { + fn get_payment_method_type(&self) -> api_enums::PaymentMethodType { + match self { + Self::Givex(_) => api_enums::PaymentMethodType::Givex, + Self::PaySafeCard {} => api_enums::PaymentMethodType::PaySafeCard, + } + } +} + #[derive(serde::Deserialize, serde::Serialize, Debug, Clone, ToSchema, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub enum GiftCardData { diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index 01c9c80fceca..ca6bba480063 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -13,6 +13,7 @@ pub mod pii; pub mod request; #[cfg(feature = "signals")] pub mod signals; +pub mod types; pub mod validation; /// Date-time utilities. diff --git a/crates/api_models/src/types.rs b/crates/common_utils/src/types.rs similarity index 52% rename from crates/api_models/src/types.rs rename to crates/common_utils/src/types.rs index bd594ba62766..d745334a21ea 100644 --- a/crates/api_models/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -1,46 +1,68 @@ -use common_utils::errors::{ApiModelsError, CustomResult}; -use error_stack::ResultExt; +//! Types that can be used in other crates +use error_stack::{IntoReport, ResultExt}; use serde::{de::Visitor, Deserialize, Deserializer}; -use utoipa::ToSchema; -#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, ToSchema)] +use crate::errors::{ApiModelsError, CustomResult}; + +/// Represents Percentage Value between 0 and 100 both inclusive +#[derive(Clone, Default, Debug, PartialEq, serde::Serialize)] pub struct Percentage { // this value will range from 0 to 100, decimal length defined by precision macro /// Percentage value ranging between 0 and 100 - #[schema(example = 2.5)] percentage: f32, } fn get_invalid_percentage_error_message(precision: u8) -> String { format!( - "value should be between 0 to 100 and precise to only upto {} decimal digits", + "value should be a float between 0 to 100 and precise to only upto {} decimal digits", precision ) } impl Percentage { - pub fn from_float(value: f32) -> CustomResult { - if Self::is_valid_value(value) { - Ok(Self { percentage: value }) + /// construct percentage using a string representation of float value + pub fn from_string(value: String) -> CustomResult { + if Self::is_valid_string_value(&value)? { + Ok(Self { + percentage: value + .parse() + .into_report() + .change_context(ApiModelsError::InvalidPercentageValue)?, + }) } else { Err(ApiModelsError::InvalidPercentageValue.into()) .attach_printable(get_invalid_percentage_error_message(PRECISION)) } } + /// function to get percentage value pub fn get_percentage(&self) -> f32 { self.percentage } - fn is_valid_value(value: f32) -> bool { - Self::is_valid_range(value) && Self::is_valid_precision_length(value) + fn is_valid_string_value(value: &str) -> CustomResult { + let float_value = Self::is_valid_float_string(value)?; + Ok(Self::is_valid_range(float_value) && Self::is_valid_precision_length(value)) + } + fn is_valid_float_string(value: &str) -> CustomResult { + value + .parse() + .into_report() + .change_context(ApiModelsError::InvalidPercentageValue) } fn is_valid_range(value: f32) -> bool { (0.0..=100.0).contains(&value) } - fn is_valid_precision_length(value: f32) -> bool { - let multiplier = f32::powf(10.0, PRECISION.into()); - let multiplied_value = value * multiplier; - // if fraction part is 0, then the percentage value is valid - multiplied_value.fract() == 0.0 + fn is_valid_precision_length(value: &str) -> bool { + if value.contains('.') { + // if string has '.' then take the decimal part and verify precision length + match value.split('.').last() { + Some(decimal_part) => decimal_part.trim_end_matches('0').len() <= PRECISION.into(), + // will never be None + None => false, + } + } else { + // if there is no '.' then it is a whole number with no decimal part. So return true + true + } } } @@ -62,17 +84,17 @@ impl<'de, const PRECISION: u8> Visitor<'de> for PercentageVisitor { if percentage_value.is_some() { return Err(serde::de::Error::duplicate_field("percentage")); } - percentage_value = Some(map.next_value::()?); + percentage_value = Some(map.next_value::()?); } else { // Ignore unknown fields let _: serde::de::IgnoredAny = map.next_value()?; } } if let Some(value) = percentage_value { - let str_value = value.to_string(); - Ok(Percentage::from_float(value).map_err(|_| { + let string_value = value.to_string(); + Ok(Percentage::from_string(string_value.clone()).map_err(|_| { serde::de::Error::invalid_value( - serde::de::Unexpected::Other(&format!("percentage value `{}`", str_value)), + serde::de::Unexpected::Other(&format!("percentage value {}", string_value)), &&*get_invalid_percentage_error_message(PRECISION), ) })?) diff --git a/crates/api_models/tests/percentage.rs b/crates/common_utils/tests/percentage.rs similarity index 60% rename from crates/api_models/tests/percentage.rs rename to crates/common_utils/tests/percentage.rs index 3e137a2592d1..95c112523376 100644 --- a/crates/api_models/tests/percentage.rs +++ b/crates/common_utils/tests/percentage.rs @@ -1,12 +1,11 @@ #![allow(clippy::panic_in_result_fn)] -use api_models::types::Percentage; -use common_utils::errors::ApiModelsError; +use common_utils::{errors::ApiModelsError, types::Percentage}; const PRECISION_2: u8 = 2; const PRECISION_0: u8 = 0; #[test] fn invalid_range_more_than_100() -> Result<(), Box> { - let percentage = Percentage::::from_float(100.01); + let percentage = Percentage::::from_string("100.01".to_string()); assert!(percentage.is_err()); if let Err(err) = percentage { assert_eq!( @@ -18,7 +17,7 @@ fn invalid_range_more_than_100() -> Result<(), Box Result<(), Box> { - let percentage = Percentage::::from_float(-0.01); + let percentage = Percentage::::from_string("-0.01".to_string()); assert!(percentage.is_err()); if let Err(err) = percentage { assert_eq!( @@ -28,21 +27,35 @@ fn invalid_range_less_than_0() -> Result<(), Box Result<(), Box> { + let percentage = Percentage::::from_string("-0.01ed".to_string()); + assert!(percentage.is_err()); + if let Err(err) = percentage { + assert_eq!( + *err.current_context(), + ApiModelsError::InvalidPercentageValue + ) + } + Ok(()) +} + #[test] fn valid_range() -> Result<(), Box> { - let percentage = Percentage::::from_float(2.22); + let percentage = Percentage::::from_string("2.22".to_string()); assert!(percentage.is_ok()); if let Ok(percentage) = percentage { assert_eq!(percentage.get_percentage(), 2.22) } - let percentage = Percentage::::from_float(0.0); + let percentage = Percentage::::from_string("0.05".to_string()); assert!(percentage.is_ok()); if let Ok(percentage) = percentage { - assert_eq!(percentage.get_percentage(), 0.0) + assert_eq!(percentage.get_percentage(), 0.05) } - let percentage = Percentage::::from_float(100.0); + let percentage = Percentage::::from_string("100.0".to_string()); assert!(percentage.is_ok()); if let Ok(percentage) = percentage { assert_eq!(percentage.get_percentage(), 100.0) @@ -51,19 +64,19 @@ fn valid_range() -> Result<(), Box> { } #[test] fn valid_precision() -> Result<(), Box> { - let percentage = Percentage::::from_float(2.2); + let percentage = Percentage::::from_string("2.2".to_string()); assert!(percentage.is_ok()); if let Ok(percentage) = percentage { assert_eq!(percentage.get_percentage(), 2.2) } - let percentage = Percentage::::from_float(2.20000); + let percentage = Percentage::::from_string("2.20000".to_string()); assert!(percentage.is_ok()); if let Ok(percentage) = percentage { assert_eq!(percentage.get_percentage(), 2.2) } - let percentage = Percentage::::from_float(2.0); + let percentage = Percentage::::from_string("2.0".to_string()); assert!(percentage.is_ok()); if let Ok(percentage) = percentage { assert_eq!(percentage.get_percentage(), 2.0) @@ -74,7 +87,7 @@ fn valid_precision() -> Result<(), Box> { #[test] fn invalid_precision() -> Result<(), Box> { - let percentage = Percentage::::from_float(2.221); + let percentage = Percentage::::from_string("2.221".to_string()); assert!(percentage.is_err()); if let Err(err) = percentage { assert_eq!( @@ -87,15 +100,47 @@ fn invalid_precision() -> Result<(), Box> { #[test] fn deserialization_test_ok() -> Result<(), Box> { + let mut decimal = 0; + let mut integer = 0; + // check for all percentage values from 0 to 100 + while integer <= 100 { + let json_string = format!( + r#" + {{ + "percentage" : {}.{} + }} + "#, + integer, decimal + ); + let percentage = serde_json::from_str::>(&json_string); + assert!(percentage.is_ok()); + if let Ok(percentage) = percentage { + assert_eq!( + percentage.get_percentage(), + format!("{}.{}", integer, decimal) + .parse::() + .unwrap_or_default() + ) + } + if integer == 100 { + break; + } + decimal += 1; + if decimal == 100 { + decimal = 0; + integer += 1; + } + } + let json_string = r#" { - "percentage" : 12.4 + "percentage" : 18.7 } "#; let percentage = serde_json::from_str::>(json_string); assert!(percentage.is_ok()); if let Ok(percentage) = percentage { - assert_eq!(percentage.get_percentage(), 12.4) + assert_eq!(percentage.get_percentage(), 18.7) } let json_string = r#" @@ -122,7 +167,7 @@ fn deserialization_test_err() -> Result<(), Box>(json_string); assert!(percentage.is_err()); if let Err(err) = percentage { - assert_eq!(err.to_string(), "invalid value: percentage value `12.4`, expected value should be between 0 to 100 and precise to only upto 0 decimal digits at line 4 column 9".to_string()) + assert_eq!(err.to_string(), "invalid value: percentage value 12.4, expected value should be a float between 0 to 100 and precise to only upto 0 decimal digits at line 4 column 9".to_string()) } // invalid percentage value @@ -134,7 +179,7 @@ fn deserialization_test_err() -> Result<(), Box>(json_string); assert!(percentage.is_err()); if let Err(err) = percentage { - assert_eq!(err.to_string(), "invalid value: percentage value `123.42`, expected value should be between 0 to 100 and precise to only upto 2 decimal digits at line 4 column 9".to_string()) + assert_eq!(err.to_string(), "invalid value: percentage value 123.42, expected value should be a float between 0 to 100 and precise to only upto 2 decimal digits at line 4 column 9".to_string()) } // missing percentage field @@ -146,7 +191,6 @@ fn deserialization_test_err() -> Result<(), Box>(json_string); assert!(percentage.is_err()); if let Err(err) = percentage { - dbg!(err.to_string()); assert_eq!( err.to_string(), "missing field `percentage` at line 4 column 9".to_string() diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index a372a77f0643..29a83a309973 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -294,6 +294,10 @@ pub enum PaymentAttemptUpdate { MultipleCaptureCountUpdate { multiple_capture_count: i16, }, + SurchargeAmountUpdate { + surcharge_amount: Option, + tax_amount: Option, + }, AmountToCaptureUpdate { status: storage_enums::AttemptStatus, amount_capturable: i64, diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index ebda08d759eb..97f45a868307 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -214,6 +214,10 @@ pub enum PaymentAttemptUpdate { status: storage_enums::AttemptStatus, amount_capturable: i64, }, + SurchargeAmountUpdate { + surcharge_amount: Option, + tax_amount: Option, + }, PreprocessingUpdate { status: storage_enums::AttemptStatus, payment_method_id: Option>, @@ -257,6 +261,8 @@ pub struct PaymentAttemptUpdateInternal { capture_method: Option, connector_response_reference_id: Option, multiple_capture_count: Option, + surcharge_amount: Option, + tax_amount: Option, amount_capturable: Option, surcharge_metadata: Option, } @@ -507,6 +513,14 @@ impl From for PaymentAttemptUpdateInternal { surcharge_metadata, ..Default::default() }, + PaymentAttemptUpdate::SurchargeAmountUpdate { + surcharge_amount, + tax_amount, + } => Self { + surcharge_amount, + tax_amount, + ..Default::default() + }, } } } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index deccf98d83e5..88ea12388db5 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1172,6 +1172,7 @@ pub async fn list_payment_methods( card_network_types.push(CardNetworkTypes { card_network: card_network_type.0.clone(), eligible_connectors: card_network_type.1.clone(), + surcharge_details: None, }) } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index d65e53c95ba9..14b97fec42e4 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -9,7 +9,11 @@ pub mod types; use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant}; -use api_models::{enums, payments::HeaderPayload}; +use api_models::{ + enums, + payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}, + payments::HeaderPayload, +}; use common_utils::{ext_traits::AsyncExt, pii}; use data_models::mandates::MandateData; use diesel_models::{ephemeral_key, fraud_check::FraudCheck}; @@ -814,6 +818,18 @@ where { let call_connectors_start_time = Instant::now(); let mut join_handlers = Vec::with_capacity(connectors.len()); + let surcharge_metadata = payment_data + .payment_attempt + .surcharge_metadata + .as_ref() + .map(|surcharge_metadata_value| { + surcharge_metadata_value + .clone() + .parse_value::("SurchargeMetadata") + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to Deserialize SurchargeMetadata")?; for session_connector_data in connectors.iter() { let connector_id = session_connector_data.connector.connector.id(); @@ -827,6 +843,14 @@ where false, ) .await?; + payment_data.surcharge_details = + surcharge_metadata.as_ref().and_then(|surcharge_metadata| { + let payment_method_type = session_connector_data.payment_method_type; + surcharge_metadata + .surcharge_results + .get(&payment_method_type.to_string()) + .cloned() + }); let router_data = payment_data .construct_router_data( @@ -1460,6 +1484,7 @@ where pub recurring_mandate_payment_data: Option, pub ephemeral_key: Option, pub redirect_response: Option, + pub surcharge_details: Option, pub frm_message: Option, } diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index d7b3d743b959..edb6478efec0 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -251,6 +251,7 @@ impl ephemeral_key: None, multiple_capture_data: None, redirect_response, + surcharge_details: None, frm_message: frm_response.ok(), }, Some(CustomerDetails { diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index 72006946c207..d92d86e79239 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -171,6 +171,7 @@ impl ephemeral_key: None, multiple_capture_data: None, redirect_response: None, + surcharge_details: None, frm_message: None, }, None, diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index bd64ebac6322..eb88265bfc88 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -229,6 +229,7 @@ impl ephemeral_key: None, multiple_capture_data, redirect_response: None, + surcharge_details: None, frm_message: None, }, None, diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 506a9b4421cd..029652902248 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -246,6 +246,7 @@ impl ephemeral_key: None, multiple_capture_data: None, redirect_response, + surcharge_details: None, frm_message: None, }, Some(CustomerDetails { diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 761264e4798e..a40de370b368 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -359,6 +359,7 @@ impl ephemeral_key: None, multiple_capture_data: None, redirect_response: None, + surcharge_details: None, frm_message: None, }, Some(customer_details), diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 479b0e2cceea..0be92418183f 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -294,6 +294,7 @@ impl ephemeral_key, multiple_capture_data: None, redirect_response: None, + surcharge_details: None, frm_message: None, }, Some(customer_details), diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs index 0ff49279f3c8..1b33f2dddb81 100644 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -195,6 +195,7 @@ impl ephemeral_key: None, multiple_capture_data: None, redirect_response: None, + surcharge_details: None, frm_message: None, }, Some(payments::CustomerDetails { diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index c9a24b8fb840..2e9bd3eb9ed3 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -157,6 +157,7 @@ impl ephemeral_key: None, multiple_capture_data: None, redirect_response: None, + surcharge_details: None, frm_message: frm_response.ok(), }, None, diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 261275296f14..cc91482440ba 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -191,6 +191,7 @@ impl ephemeral_key: None, multiple_capture_data: None, redirect_response: None, + surcharge_details: None, frm_message: None, }, Some(customer_details), diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 07015810039d..4979b7902c85 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -166,6 +166,7 @@ impl ephemeral_key: None, multiple_capture_data: None, redirect_response: None, + surcharge_details: None, frm_message: None, }, Some(customer_details), diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 6e0ef2f4bac7..c2762a966631 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -402,6 +402,7 @@ async fn get_tracker_for_sync< ephemeral_key: None, multiple_capture_data, redirect_response: None, + surcharge_details: None, frm_message: frm_response.ok(), }, None, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 9e0ef76d3e7f..1c7b432c3276 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -345,6 +345,7 @@ impl ephemeral_key: None, multiple_capture_data: None, redirect_response: None, + surcharge_details: None, frm_message: None, }, Some(customer_details), diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 8526f48a590c..d19460127b17 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1024,6 +1024,7 @@ impl TryFrom> for types::PaymentsAuthoriz webhook_url, complete_authorize_url, customer_id: None, + surcharge_details: payment_data.surcharge_details, }) } } @@ -1185,6 +1186,7 @@ impl TryFrom> for types::PaymentsSessionD billing_address.address.and_then(|address| address.country) }), order_details, + surcharge_details: payment_data.surcharge_details, }) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index cb8fe8fa7979..faa393fd7513 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -379,6 +379,7 @@ pub struct PaymentsAuthorizeData { pub related_transaction_id: Option, pub payment_experience: Option, pub payment_method_type: Option, + pub surcharge_details: Option, pub customer_id: Option, } @@ -514,6 +515,7 @@ pub struct PaymentsSessionData { pub amount: i64, pub currency: storage_enums::Currency, pub country: Option, + pub surcharge_details: Option, pub order_details: Option>, } @@ -1075,6 +1077,7 @@ impl From<&SetupMandateRouterData> for PaymentsAuthorizeData { payment_experience: None, payment_method_type: None, customer_id: None, + surcharge_details: None, } } } diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index 63d2ee364fa6..cba0640e79a0 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -68,6 +68,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { webhook_url: None, complete_authorize_url: None, customer_id: None, + surcharge_details: None, }, response: Err(types::ErrorResponse::default()), payment_method_id: None, diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index 06f048bb7461..2ee6c4912e7c 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -153,6 +153,7 @@ impl AdyenTest { webhook_url: None, complete_authorize_url: None, customer_id: None, + surcharge_details: None, }) } } diff --git a/crates/router/tests/connectors/bitpay.rs b/crates/router/tests/connectors/bitpay.rs index 1eaf1580f0a7..2f6db7a9e850 100644 --- a/crates/router/tests/connectors/bitpay.rs +++ b/crates/router/tests/connectors/bitpay.rs @@ -90,6 +90,7 @@ fn payment_method_details() -> Option { complete_authorize_url: None, capture_method: None, customer_id: None, + surcharge_details: None, }) } diff --git a/crates/router/tests/connectors/cashtocode.rs b/crates/router/tests/connectors/cashtocode.rs index 141aee4ff49b..fdb1b94a7149 100644 --- a/crates/router/tests/connectors/cashtocode.rs +++ b/crates/router/tests/connectors/cashtocode.rs @@ -65,6 +65,7 @@ impl CashtocodeTest { webhook_url: None, complete_authorize_url: None, customer_id: Some("John Doe".to_owned()), + surcharge_details: None, }) } diff --git a/crates/router/tests/connectors/coinbase.rs b/crates/router/tests/connectors/coinbase.rs index dc677c6e31a6..cc8cc774a144 100644 --- a/crates/router/tests/connectors/coinbase.rs +++ b/crates/router/tests/connectors/coinbase.rs @@ -92,6 +92,7 @@ fn payment_method_details() -> Option { complete_authorize_url: None, capture_method: None, customer_id: None, + surcharge_details: None, }) } diff --git a/crates/router/tests/connectors/cryptopay.rs b/crates/router/tests/connectors/cryptopay.rs index d86284159954..48313e5d1a12 100644 --- a/crates/router/tests/connectors/cryptopay.rs +++ b/crates/router/tests/connectors/cryptopay.rs @@ -90,6 +90,7 @@ fn payment_method_details() -> Option { complete_authorize_url: None, capture_method: None, customer_id: None, + surcharge_details: None, }) } diff --git a/crates/router/tests/connectors/opennode.rs b/crates/router/tests/connectors/opennode.rs index 79fadf3e130d..a46fc20604a5 100644 --- a/crates/router/tests/connectors/opennode.rs +++ b/crates/router/tests/connectors/opennode.rs @@ -91,6 +91,7 @@ fn payment_method_details() -> Option { complete_authorize_url: None, capture_method: None, customer_id: None, + surcharge_details: None, }) } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 81f4f6bef29d..7cd2e16fd361 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -886,6 +886,7 @@ impl Default for PaymentAuthorizeType { complete_authorize_url: None, webhook_url: None, customer_id: None, + surcharge_details: None, }; Self(data) } diff --git a/crates/router/tests/connectors/worldline.rs b/crates/router/tests/connectors/worldline.rs index 11de3be8a2c6..b9ea1364fdb1 100644 --- a/crates/router/tests/connectors/worldline.rs +++ b/crates/router/tests/connectors/worldline.rs @@ -100,6 +100,7 @@ impl WorldlineTest { webhook_url: None, complete_authorize_url: None, customer_id: None, + surcharge_details: None, }) } } diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 4764ce68a314..b9b850a0aaa5 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1307,6 +1307,13 @@ impl DataModelExt for PaymentAttemptUpdate { Self::SurchargeMetadataUpdate { surcharge_metadata } => { DieselPaymentAttemptUpdate::SurchargeMetadataUpdate { surcharge_metadata } } + Self::SurchargeAmountUpdate { + surcharge_amount, + tax_amount, + } => DieselPaymentAttemptUpdate::SurchargeAmountUpdate { + surcharge_amount, + tax_amount, + }, } } @@ -1500,6 +1507,13 @@ impl DataModelExt for PaymentAttemptUpdate { DieselPaymentAttemptUpdate::SurchargeMetadataUpdate { surcharge_metadata } => { Self::SurchargeMetadataUpdate { surcharge_metadata } } + DieselPaymentAttemptUpdate::SurchargeAmountUpdate { + surcharge_amount, + tax_amount, + } => Self::SurchargeAmountUpdate { + surcharge_amount, + tax_amount, + }, } } } From 7eabd24a4da6f82fd30f8a4be739962538654214 Mon Sep 17 00:00:00 2001 From: Kashif <46213975+kashif-m@users.noreply.github.com> Date: Thu, 12 Oct 2023 13:14:23 +0530 Subject: [PATCH 10/21] fix: consume profile_id throughout payouts flow (#2501) Co-authored-by: Kashif --- crates/api_models/src/payouts.rs | 7 +++++ crates/router/src/core/payouts.rs | 4 ++- crates/router/src/core/utils.rs | 40 +++++++++++-------------- crates/router/src/types.rs | 1 - crates/router/tests/connectors/utils.rs | 5 ---- openapi/openapi_spec.json | 10 +++++++ 6 files changed, 38 insertions(+), 29 deletions(-) diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index 8fbed6bf68de..5cc5e5118166 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -146,6 +146,10 @@ pub struct PayoutCreateRequest { /// Provide a reference to a stored payment method #[schema(example = "187282ab-40ef-47a9-9206-5099ba31e432")] pub payout_token: Option, + + /// The business profile to use for this payment, if not passed the default business profile + /// associated with the merchant account will be used. + pub profile_id: Option, } #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] @@ -376,6 +380,9 @@ pub struct PayoutCreateResponse { /// If there was an error while calling the connectors the code is received here #[schema(value_type = String, example = "E0001")] pub error_code: Option, + + /// The business profile that is associated with this payment + pub profile_id: Option, } #[derive(Default, Debug, Clone, Deserialize, ToSchema)] diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index bf438a15a45b..ddb2a017e35a 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -618,7 +618,7 @@ pub async fn create_recipient( let merchant_id = merchant_account.merchant_id.to_owned(); let updated_customer = storage::CustomerUpdate::ConnectorCustomer { connector_customer: Some( - serde_json::json!({"id": recipient_create_data.connector_payout_id}), + serde_json::json!({connector_label: recipient_create_data.connector_payout_id}), ), }; payout_data.customer_details = Some( @@ -1116,6 +1116,7 @@ pub async fn response_handler( status: payout_attempt.status.to_owned(), error_message: payout_attempt.error_message.to_owned(), error_code: payout_attempt.error_code, + profile_id: payout_attempt.profile_id, }; Ok(services::ApplicationResponse::Json(response)) } @@ -1244,6 +1245,7 @@ pub async fn payout_create_db_entries( .set_payout_token(req.payout_token.to_owned()) .set_created_at(Some(common_utils::date_time::now())) .set_last_modified_at(Some(common_utils::date_time::now())) + .set_profile_id(req.profile_id.to_owned()) .to_owned(); let payout_attempt = db .insert_payout_attempt(payout_attempt_req) diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 908bd1438762..7790e2ac5b78 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -40,31 +40,32 @@ pub async fn get_mca_for_payout<'a>( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, payout_data: &PayoutData, -) -> RouterResult { +) -> RouterResult<(helpers::MerchantConnectorAccountType, String)> { let payout_attempt = &payout_data.payout_attempt; + let profile_id = get_profile_id_from_business_details( + payout_attempt.business_country, + payout_attempt.business_label.as_ref(), + merchant_account, + payout_attempt.profile_id.as_ref(), + &*state.store, + false, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payout_attempt")?; match payout_data.merchant_connector_account.to_owned() { - Some(mca) => Ok(mca), + Some(mca) => Ok((mca, profile_id)), None => { - let profile_id = payout_attempt - .profile_id - .as_ref() - .ok_or(errors::ApiErrorResponse::MissingRequiredField { - field_name: "business_profile", - }) - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("profile_id is not set in payment_intent")?; - let merchant_connector_account = helpers::get_merchant_connector_account( state, merchant_account.merchant_id.as_str(), None, key_store, - profile_id, + &profile_id, connector_id, ) .await?; - Ok(merchant_connector_account) + Ok((merchant_connector_account, profile_id)) } } } @@ -79,12 +80,7 @@ pub async fn construct_payout_router_data<'a, F>( _request: &api_models::payouts::PayoutRequest, payout_data: &mut PayoutData, ) -> RouterResult> { - let (business_country, _) = helpers::get_business_details( - payout_data.payout_attempt.business_country, - payout_data.payout_attempt.business_label.as_ref(), - merchant_account, - )?; - let merchant_connector_account = get_mca_for_payout( + let (merchant_connector_account, profile_id) = get_mca_for_payout( state, connector_id, merchant_account, @@ -130,10 +126,11 @@ pub async fn construct_payout_router_data<'a, F>( let payouts = &payout_data.payouts; let payout_attempt = &payout_data.payout_attempt; let customer_details = &payout_data.customer_details; + let connector_label = format!("{profile_id}_{}", payout_attempt.connector); let connector_customer_id = customer_details .as_ref() .and_then(|c| c.connector_customer.as_ref()) - .and_then(|cc| cc.get("id")) + .and_then(|cc| cc.get(connector_label)) .and_then(|id| serde_json::from_value::(id.to_owned()).ok()); let router_data = types::RouterData { flow: PhantomData, @@ -161,7 +158,6 @@ pub async fn construct_payout_router_data<'a, F>( source_currency: payouts.source_currency, entity_type: payouts.entity_type.to_owned(), payout_type: payouts.payout_type, - country_code: business_country, customer_details: customer_details .to_owned() .map(|c| payments::CustomerDetails { diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index faa393fd7513..c3da5564b55e 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -335,7 +335,6 @@ pub struct PayoutsData { pub source_currency: storage_enums::Currency, pub payout_type: storage_enums::PayoutType, pub entity_type: storage_enums::PayoutEntityType, - pub country_code: storage_enums::CountryAlpha2, pub customer_details: Option, } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 7cd2e16fd361..f88848d37922 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -442,11 +442,6 @@ pub trait ConnectorActions: Connector { }), entity_type: enums::PayoutEntityType::Individual, payout_type, - country_code: payment_info - .to_owned() - .map_or(enums::CountryAlpha2::NL, |pi| { - pi.country.map_or(enums::CountryAlpha2::NL, |c| c) - }), customer_details: Some(payments::CustomerDetails { customer_id: core_utils::get_or_generate_id("customer_id", &None, "cust_").ok(), name: Some(Secret::new("John Doe".to_string())), diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 56632a68c39b..27b73f1c097d 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -9847,6 +9847,11 @@ "description": "Provide a reference to a stored payment method", "example": "187282ab-40ef-47a9-9206-5099ba31e432", "nullable": true + }, + "profile_id": { + "type": "string", + "description": "The business profile to use for this payment, if not passed the default business profile\nassociated with the merchant account will be used.", + "nullable": true } } }, @@ -9996,6 +10001,11 @@ "type": "string", "description": "If there was an error while calling the connectors the code is received here", "example": "E0001" + }, + "profile_id": { + "type": "string", + "description": "The business profile that is associated with this payment", + "nullable": true } } }, From c2ad2002c0e6d673f62ec4c72c8fd98b07a05c0b Mon Sep 17 00:00:00 2001 From: Azanul Haque <42029519+Azanul@users.noreply.github.com> Date: Thu, 12 Oct 2023 13:23:06 +0530 Subject: [PATCH 11/21] feat(connector): [ACI] Use connector_request_reference_id as reference to the connector (#2549) Signed-off-by: Azanul --- crates/router/src/connector/aci/transformers.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index 30ed9f5d8dba..7d30c80c49c9 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -203,7 +203,9 @@ impl bank_account_iban: None, billing_country: Some(country.to_owned()), merchant_customer_id: Some(Secret::new(item.get_customer_id()?)), - merchant_transaction_id: Some(Secret::new(item.payment_id.clone())), + merchant_transaction_id: Some(Secret::new( + item.connector_request_reference_id.clone(), + )), customer_email: None, })) } From 13be4d36eac3d1e17d8ad9b3f3ef8993547f548b Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Thu, 12 Oct 2023 13:26:20 +0530 Subject: [PATCH 12/21] revert: fix(connector): [noon] sync with reference_id (#2556) --- crates/router/src/connector/noon.rs | 20 ++++++------------- .../router/src/connector/noon/transformers.rs | 18 ++++------------- .../router/src/core/payments/transformers.rs | 5 ----- crates/router/src/types.rs | 3 --- crates/router/tests/connectors/bambora.rs | 2 -- crates/router/tests/connectors/forte.rs | 1 - crates/router/tests/connectors/nexinets.rs | 1 - crates/router/tests/connectors/paypal.rs | 2 -- crates/router/tests/connectors/utils.rs | 1 - crates/router/tests/connectors/zen.rs | 2 -- 10 files changed, 10 insertions(+), 45 deletions(-) diff --git a/crates/router/src/connector/noon.rs b/crates/router/src/connector/noon.rs index c2637ba93b2a..5bd7f2495408 100644 --- a/crates/router/src/connector/noon.rs +++ b/crates/router/src/connector/noon.rs @@ -9,6 +9,7 @@ use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as noon; +use super::utils::PaymentsSyncRequestData; use crate::{ configs::settings, connector::utils as connector_utils, @@ -275,19 +276,10 @@ impl ConnectorIntegration CustomResult { - //Added as a fix for past payments before the given timestamp we can reconcile using payment_id - let cutoff_timestamp: i64 = 1697023800; - - let reference_id = if req.request.payment_attempt_created_at_as_utc < cutoff_timestamp { - req.payment_id.clone() - } else { - req.attempt_id.clone() - }; - + let connector_transaction_id = req.request.get_connector_transaction_id()?; Ok(format!( - "{}payment/v1/order/getbyreference/{}", - self.base_url(connectors), - reference_id + "{}payment/v1/order/{connector_transaction_id}", + self.base_url(connectors) )) } @@ -577,9 +569,9 @@ impl ConnectorIntegration CustomResult { Ok(format!( - "{}payment/v1/order/getbyreference/{}", + "{}payment/v1/order/{}", self.base_url(connectors), - req.attempt_id + req.request.connector_transaction_id )) } diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index 749c8c8dea7b..5300525b7cbd 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -4,7 +4,8 @@ use serde::{Deserialize, Serialize}; use crate::{ connector::utils::{ - self as conn_utils, CardData, PaymentsAuthorizeRequestData, RouterData, WalletData, + self as conn_utils, CardData, PaymentsAuthorizeRequestData, RefundsRequestData, RouterData, + WalletData, }, core::errors, services, @@ -437,7 +438,6 @@ impl pub struct NoonActionTransaction { amount: String, currency: diesel_models::enums::Currency, - transaction_reference: Option, } #[derive(Debug, Serialize)] @@ -466,7 +466,6 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for NoonPaymentsActionRequest { item.request.currency, )?, currency: item.request.currency, - transaction_reference: None, }; Ok(Self { api_operation: NoonApiOperations::Capture, @@ -508,7 +507,6 @@ impl TryFrom<&types::RefundsRouterData> for NoonPaymentsActionRequest { item.request.currency, )?, currency: item.request.currency, - transaction_reference: Some(item.request.refund_id.clone()), }; Ok(Self { api_operation: NoonApiOperations::Refund, @@ -572,11 +570,9 @@ impl TryFrom> } #[derive(Default, Debug, Deserialize)] -#[serde(rename_all = "camelCase")] pub struct NoonRefundResponseTransactions { id: String, status: RefundStatus, - transaction_reference: Option, } #[derive(Default, Debug, Deserialize)] @@ -596,19 +592,13 @@ impl TryFrom> fn try_from( item: types::RefundsResponseRouterData, ) -> Result { + let connector_refund_id = item.data.request.get_connector_refund_id()?; let noon_transaction: &NoonRefundResponseTransactions = item .response .result .transactions .iter() - .find(|transaction| { - transaction - .transaction_reference - .clone() - .map_or(false, |transaction_instance| { - transaction_instance == item.data.request.refund_id - }) - }) + .find(|transaction| transaction.id == connector_refund_id) .ok_or(errors::ConnectorError::ResponseHandlingFailed)?; Ok(Self { diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index d19460127b17..f93b2e9ff4aa 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1051,11 +1051,6 @@ impl TryFrom> for types::PaymentsSyncData ), None => types::SyncRequestType::SinglePaymentSync, }, - payment_attempt_created_at_as_utc: payment_data - .payment_attempt - .created_at - .assume_utc() - .unix_timestamp(), }) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index c3da5564b55e..48e3697c6ba9 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -476,9 +476,6 @@ pub struct PaymentsSyncData { pub connector_meta: Option, pub sync_type: SyncRequestType, pub mandate_id: Option, - //This is being added as a temporary fix, will be deprecated before or by v1.65.0 - // #2628 - pub payment_attempt_created_at_as_utc: i64, } #[derive(Debug, Default, Clone)] diff --git a/crates/router/tests/connectors/bambora.rs b/crates/router/tests/connectors/bambora.rs index 73e75b713645..5638214202b4 100644 --- a/crates/router/tests/connectors/bambora.rs +++ b/crates/router/tests/connectors/bambora.rs @@ -108,7 +108,6 @@ async fn should_sync_authorized_payment() { capture_method: Some(diesel_models::enums::CaptureMethod::Manual), sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta: None, - payment_attempt_created_at_as_utc: 0, }), None, ) @@ -223,7 +222,6 @@ async fn should_sync_auto_captured_payment() { capture_method: Some(enums::CaptureMethod::Automatic), sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta: None, - payment_attempt_created_at_as_utc: 0, }), None, ) diff --git a/crates/router/tests/connectors/forte.rs b/crates/router/tests/connectors/forte.rs index 8cce94dc85ae..703b8900b69b 100644 --- a/crates/router/tests/connectors/forte.rs +++ b/crates/router/tests/connectors/forte.rs @@ -153,7 +153,6 @@ async fn should_sync_authorized_payment() { sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta: None, mandate_id: None, - payment_attempt_created_at_as_utc: 0, }), get_default_payment_info(), ) diff --git a/crates/router/tests/connectors/nexinets.rs b/crates/router/tests/connectors/nexinets.rs index 5c95b1f41a9d..f9b8affb0626 100644 --- a/crates/router/tests/connectors/nexinets.rs +++ b/crates/router/tests/connectors/nexinets.rs @@ -123,7 +123,6 @@ async fn should_sync_authorized_payment() { sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta, mandate_id: None, - payment_attempt_created_at_as_utc: 0, }), None, ) diff --git a/crates/router/tests/connectors/paypal.rs b/crates/router/tests/connectors/paypal.rs index 5fe37c374a68..e9df65566930 100644 --- a/crates/router/tests/connectors/paypal.rs +++ b/crates/router/tests/connectors/paypal.rs @@ -140,7 +140,6 @@ async fn should_sync_authorized_payment() { capture_method: None, sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta, - payment_attempt_created_at_as_utc: 0, }), get_default_payment_info(), ) @@ -337,7 +336,6 @@ async fn should_sync_auto_captured_payment() { capture_method: Some(enums::CaptureMethod::Automatic), sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta, - payment_attempt_created_at_as_utc: 0, }), get_default_payment_info(), ) diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index f88848d37922..7d600d98d3e4 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -938,7 +938,6 @@ impl Default for PaymentSyncType { capture_method: None, sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta: None, - payment_attempt_created_at_as_utc: 0, }; Self(data) } diff --git a/crates/router/tests/connectors/zen.rs b/crates/router/tests/connectors/zen.rs index a994c290dd9a..ca71fd0c6221 100644 --- a/crates/router/tests/connectors/zen.rs +++ b/crates/router/tests/connectors/zen.rs @@ -102,7 +102,6 @@ async fn should_sync_authorized_payment() { sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta: None, mandate_id: None, - payment_attempt_created_at_as_utc: 0, }), None, ) @@ -217,7 +216,6 @@ async fn should_sync_auto_captured_payment() { sync_type: types::SyncRequestType::SinglePaymentSync, connector_meta: None, mandate_id: None, - payment_attempt_created_at_as_utc: 0, }), None, ) From 46f14191ab7e036539ef3fd58acd9376b6b6b63c Mon Sep 17 00:00:00 2001 From: Kashif <46213975+kashif-m@users.noreply.github.com> Date: Thu, 12 Oct 2023 14:37:18 +0530 Subject: [PATCH 13/21] =?UTF-8?q?fix:=20parse=20allowed=5Fpayment=5Fmethod?= =?UTF-8?q?=5Ftypes=20only=20if=20there=20is=20some=20value=20p=E2=80=A6?= =?UTF-8?q?=20(#2161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kashif --- crates/router/src/core/payment_methods/cards.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 88ea12388db5..ee5f34938f99 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1351,9 +1351,11 @@ pub async fn filter_payment_methods( payment_intent .allowed_payment_method_types .clone() - .parse_value("Vec") - .map_err(|error| logger::error!(%error, "Failed to deserialize PaymentIntent allowed_payment_method_types")) - .ok() + .map(|val| val.parse_value("Vec")) + .transpose() + .unwrap_or_else(|error| { + logger::error!(%error, "Failed to deserialize PaymentIntent allowed_payment_method_types"); None + }) }); for payment_method_type_info in payment_methods_enabled From 642085dc745f87b4edd2f7a744c31b8979b23cfa Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Thu, 12 Oct 2023 14:39:10 +0530 Subject: [PATCH 14/21] feat(router): Add payment link support (#2105) Co-authored-by: Sahkal Poddar Co-authored-by: Kashif <46213975+kashif-m@users.noreply.github.com> Co-authored-by: Kashif Co-authored-by: Bernard Eugine <114725419+bernard-eugine@users.noreply.github.com> --- Cargo.lock | 224 +++ config/config.example.toml | 4 + config/development.toml | 3 + crates/api_models/src/payments.rs | 44 + .../src/payments/payment_intent.rs | 2 + crates/diesel_models/src/lib.rs | 1 + crates/diesel_models/src/payment_intent.rs | 2 + crates/diesel_models/src/payment_link.rs | 50 + crates/diesel_models/src/query.rs | 1 + .../diesel_models/src/query/payment_link.rs | 29 + crates/diesel_models/src/schema.rs | 24 + crates/router/Cargo.toml | 1 + .../router/src/compatibility/stripe/errors.rs | 4 + crates/router/src/compatibility/wrap.rs | 14 + crates/router/src/configs/settings.rs | 6 + crates/router/src/core.rs | 1 + .../src/core/errors/api_error_response.rs | 2 + crates/router/src/core/errors/transformers.rs | 3 + crates/router/src/core/payment_link.rs | 172 +++ .../src/core/payment_link/payment_link.html | 1310 +++++++++++++++++ crates/router/src/core/payments.rs | 1 + crates/router/src/core/payments/helpers.rs | 61 +- .../payments/operations/payment_approve.rs | 1 + .../payments/operations/payment_cancel.rs | 1 + .../payments/operations/payment_capture.rs | 1 + .../operations/payment_complete_authorize.rs | 1 + .../payments/operations/payment_confirm.rs | 10 +- .../payments/operations/payment_create.rs | 75 +- .../operations/payment_method_validate.rs | 1 + .../payments/operations/payment_reject.rs | 1 + .../payments/operations/payment_session.rs | 11 +- .../core/payments/operations/payment_start.rs | 10 +- .../payments/operations/payment_status.rs | 10 +- .../payments/operations/payment_update.rs | 10 +- .../router/src/core/payments/transformers.rs | 4 + crates/router/src/db.rs | 2 + crates/router/src/db/payment_link.rs | 66 + crates/router/src/lib.rs | 3 +- crates/router/src/openapi.rs | 9 +- crates/router/src/routes.rs | 5 +- crates/router/src/routes/app.rs | 18 +- crates/router/src/routes/lock_utils.rs | 3 + crates/router/src/routes/metrics/request.rs | 1 + crates/router/src/routes/payment_link.rs | 82 ++ crates/router/src/services/api.rs | 56 +- crates/router/src/services/authentication.rs | 6 + crates/router/src/types/storage.rs | 6 +- .../router/src/types/storage/payment_link.rs | 1 + crates/router/src/types/transformers.rs | 16 + crates/router_env/src/logger/types.rs | 4 + crates/storage_impl/src/mock_db.rs | 2 + .../src/mock_db/payment_intent.rs | 1 + .../src/payments/payment_intent.rs | 7 +- .../down.sql | 2 + .../2023-09-08-101302_add_payment_link/up.sql | 13 + .../down.sql | 2 + .../up.sql | 2 + openapi/openapi_spec.json | 186 +++ 58 files changed, 2565 insertions(+), 23 deletions(-) create mode 100644 crates/diesel_models/src/payment_link.rs create mode 100644 crates/diesel_models/src/query/payment_link.rs create mode 100644 crates/router/src/core/payment_link.rs create mode 100644 crates/router/src/core/payment_link/payment_link.html create mode 100644 crates/router/src/db/payment_link.rs create mode 100644 crates/router/src/routes/payment_link.rs create mode 100644 crates/router/src/types/storage/payment_link.rs create mode 100644 migrations/2023-09-08-101302_add_payment_link/down.sql create mode 100644 migrations/2023-09-08-101302_add_payment_link/up.sql create mode 100644 migrations/2023-09-08-114828_add_payment_link_id_in_payment_intent/down.sql create mode 100644 migrations/2023-09-08-114828_add_payment_link_id_in_payment_intent/up.sql diff --git a/Cargo.lock b/Cargo.lock index b8290efee027..e843210faa0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,6 +392,7 @@ dependencies = [ "router_derive", "serde", "serde_json", + "serde_with", "strum 0.24.1", "thiserror", "time 0.3.22", @@ -1202,6 +1203,16 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.13.0" @@ -1358,6 +1369,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "chrono-tz" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1369bc6b9e9a7dfdae2055f6ec151fe9c554a9d23d357c0237cee2e25eaabb7" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2f5ebdc942f57ed96d560a6d1a459bae5851102a25d5bf89dc04ae453e31ecf" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "clap" version = "4.3.4" @@ -1786,6 +1819,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "deunicode" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95203a6a50906215a502507c0f879a0ce7ff205a6111e2db2a5ef8e4bb92e43" + [[package]] name = "diesel" version = "2.1.0" @@ -2407,6 +2446,30 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "globset" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" +dependencies = [ + "aho-corasick", + "bstr", + "fnv", + "log", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + [[package]] name = "h2" version = "0.3.19" @@ -2541,6 +2604,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "humantime" version = "1.3.0" @@ -2653,6 +2725,23 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +dependencies = [ + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + [[package]] name = "image" version = "0.23.14" @@ -3450,6 +3539,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae" +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + [[package]] name = "paste" version = "1.0.12" @@ -3521,6 +3619,44 @@ dependencies = [ "sha2", ] +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "phonenumber" version = "0.3.3+8.13.9" @@ -4144,6 +4280,7 @@ dependencies = [ "signal-hook-tokio", "storage_impl", "strum 0.24.1", + "tera", "test_utils", "thirtyfour", "thiserror", @@ -4720,6 +4857,12 @@ dependencies = [ "time 0.3.22", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "skeptic" version = "0.13.7" @@ -4744,6 +4887,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slug" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" +dependencies = [ + "deunicode", +] + [[package]] name = "smallvec" version = "1.10.0" @@ -4926,6 +5078,28 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "tera" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -5477,6 +5651,56 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicase" version = "2.6.0" diff --git a/config/config.example.toml b/config/config.example.toml index bee61f2f6156..e97b73d87c6b 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -431,3 +431,7 @@ apple_pay_ppc = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE" #Payment apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" #Private key generate by Elliptic-curve prime256v1 curve apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" #Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key generate by RSA:2048 algorithm + + +[payment_link] +sdk_url = "http://localhost:9090/dist/HyperLoader.js" \ No newline at end of file diff --git a/config/development.toml b/config/development.toml index 64bdd45f1c31..a29cc9ec5be2 100644 --- a/config/development.toml +++ b/config/development.toml @@ -441,6 +441,9 @@ apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" +[payment_link] +sdk_url = "http://localhost:9090/dist/HyperLoader.js" + [lock_settings] redis_lock_expiry_seconds = 180 # 3 * 60 seconds delay_between_retries_in_milliseconds = 500 \ No newline at end of file diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 08646a4a0b3c..3888d6d1d21c 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -294,6 +294,8 @@ pub struct PaymentsRequest { /// additional data that might be required by hyperswitch pub feature_metadata: Option, + /// payment link object required for generating the payment_link + pub payment_link_object: Option, /// The business profile to use for this payment, if not passed the default business profile /// associated with the merchant account will be used. @@ -2079,6 +2081,7 @@ pub struct PaymentsResponse { #[schema(value_type = Option, example = "993672945374576J")] pub reference_id: Option, + pub payment_link: Option, /// The business profile that is associated with this payment pub profile_id: Option, @@ -3066,3 +3069,44 @@ mod tests { ) } } + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, ToSchema)] +pub struct PaymentLinkObject { + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub link_expiry: Option, + pub merchant_custom_domain_name: Option, +} + +#[derive(Default, Debug, serde::Deserialize, Clone, ToSchema)] +pub struct RetrievePaymentLinkRequest { + pub client_secret: Option, +} + +#[derive(Clone, Debug, serde::Serialize, PartialEq, ToSchema)] +pub struct PaymentLinkResponse { + pub link: String, + pub payment_link_id: String, +} + +#[derive(Clone, Debug, serde::Serialize, ToSchema)] +pub struct RetrievePaymentLinkResponse { + pub payment_link_id: String, + pub payment_id: String, + pub merchant_id: String, + pub link_to_pay: String, + pub amount: i64, + #[schema(value_type = Option, example = "USD")] + pub currency: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub last_modified_at: PrimitiveDateTime, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub link_expiry: Option, +} + +#[derive(Clone, Debug, serde::Deserialize, ToSchema)] +pub struct PaymentLinkInitiateRequest { + pub merchant_id: String, + pub payment_id: String, +} diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index b3bf8af8c36b..3d8f52d00060 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -104,6 +104,7 @@ pub struct PaymentIntent { // 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 pub merchant_decision: Option, + pub payment_link_id: Option, pub payment_confirm_source: Option, } @@ -143,6 +144,7 @@ pub struct PaymentIntentNew { pub attempt_count: i16, pub profile_id: Option, pub merchant_decision: Option, + pub payment_link_id: Option, pub payment_confirm_source: Option, } diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 474ac984e69c..3de35d73f822 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -25,6 +25,7 @@ pub mod merchant_connector_account; pub mod merchant_key_store; pub mod payment_attempt; pub mod payment_intent; +pub mod payment_link; pub mod payment_method; pub mod payout_attempt; pub mod payouts; diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 1212c9d52424..1669cd7e7798 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -46,6 +46,7 @@ pub struct PaymentIntent { // 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 pub merchant_decision: Option, + pub payment_link_id: Option, pub payment_confirm_source: Option, } @@ -97,6 +98,7 @@ pub struct PaymentIntentNew { pub attempt_count: i16, pub profile_id: Option, pub merchant_decision: Option, + pub payment_link_id: Option, pub payment_confirm_source: Option, } diff --git a/crates/diesel_models/src/payment_link.rs b/crates/diesel_models/src/payment_link.rs new file mode 100644 index 000000000000..4b182a8155a5 --- /dev/null +++ b/crates/diesel_models/src/payment_link.rs @@ -0,0 +1,50 @@ +use diesel::{Identifiable, Insertable, Queryable}; +use serde::{self, Deserialize, Serialize}; +use time::PrimitiveDateTime; + +use crate::{enums as storage_enums, schema::payment_link}; + +#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Serialize, Deserialize)] +#[diesel(table_name = payment_link)] +#[diesel(primary_key(payment_link_id))] +pub struct PaymentLink { + pub payment_link_id: String, + pub payment_id: String, + pub link_to_pay: String, + pub merchant_id: String, + pub amount: i64, + pub currency: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub last_modified_at: PrimitiveDateTime, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub fulfilment_time: Option, +} + +#[derive( + Clone, + Debug, + Default, + Eq, + PartialEq, + Insertable, + serde::Serialize, + serde::Deserialize, + router_derive::DebugAsDisplay, +)] +#[diesel(table_name = payment_link)] +pub struct PaymentLinkNew { + pub payment_link_id: String, + pub payment_id: String, + pub link_to_pay: String, + pub merchant_id: String, + pub amount: i64, + pub currency: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub created_at: Option, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub last_modified_at: Option, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub fulfilment_time: Option, +} diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index bd280864cc4e..ef4ab9f32fa3 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -18,6 +18,7 @@ pub mod merchant_connector_account; pub mod merchant_key_store; pub mod payment_attempt; pub mod payment_intent; +pub mod payment_link; pub mod payment_method; pub mod payout_attempt; pub mod payouts; diff --git a/crates/diesel_models/src/query/payment_link.rs b/crates/diesel_models/src/query/payment_link.rs new file mode 100644 index 000000000000..085257633c65 --- /dev/null +++ b/crates/diesel_models/src/query/payment_link.rs @@ -0,0 +1,29 @@ +use diesel::{associations::HasTable, ExpressionMethods}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + payment_link::{PaymentLink, PaymentLinkNew}, + schema::payment_link::dsl, + PgPooledConn, StorageResult, +}; + +impl PaymentLinkNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl PaymentLink { + pub async fn find_link_by_payment_link_id( + conn: &PgPooledConn, + payment_link_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::payment_link_id.eq(payment_link_id.to_owned()), + ) + .await + } +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 0da819f2a70c..93a204446a3d 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -610,10 +610,33 @@ diesel::table! { profile_id -> Nullable, #[max_length = 64] merchant_decision -> Nullable, + #[max_length = 255] + payment_link_id -> Nullable, payment_confirm_source -> Nullable, } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + payment_link (payment_link_id) { + #[max_length = 255] + payment_link_id -> Varchar, + #[max_length = 64] + payment_id -> Varchar, + #[max_length = 255] + link_to_pay -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + amount -> Int8, + currency -> Nullable, + created_at -> Timestamp, + last_modified_at -> Timestamp, + fulfilment_time -> Nullable, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -833,6 +856,7 @@ diesel::allow_tables_to_appear_in_same_query!( merchant_key_store, payment_attempt, payment_intent, + payment_link, payment_methods, payout_attempt, payouts, diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index f271c6e3629c..da207e91493f 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -81,6 +81,7 @@ strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } +tera = "1.19.1" url = { version = "2.4.0", features = ["serde"] } utoipa = { version = "3.3.0", features = ["preserve_order", "time"] } utoipa-swagger-ui = { version = "3.1.3", features = ["actix-web"] } diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index d79533e73b40..5d4d3082aff5 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -232,6 +232,8 @@ pub enum StripeErrorCode { HyperswitchUnprocessableEntity { message: String }, #[error(error_type = StripeErrorType::InvalidRequestError, code = "", message = "{message}")] CurrencyNotSupported { message: String }, + #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "Payment Link does not exist in our records")] + PaymentLinkNotFound, #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "Resource Busy. Please try again later")] LockTimeout, // [#216]: https://github.com/juspay/hyperswitch/issues/216 @@ -490,6 +492,7 @@ impl From for StripeErrorCode { errors::ApiErrorResponse::ClientSecretNotGiven | errors::ApiErrorResponse::ClientSecretExpired => Self::ClientSecretNotFound, errors::ApiErrorResponse::MerchantAccountNotFound => Self::MerchantAccountNotFound, + errors::ApiErrorResponse::PaymentLinkNotFound => Self::PaymentLinkNotFound, errors::ApiErrorResponse::ResourceIdNotFound => Self::ResourceIdNotFound, errors::ApiErrorResponse::MerchantConnectorAccountNotFound { id } => { Self::MerchantConnectorAccountNotFound { id } @@ -652,6 +655,7 @@ impl actix_web::ResponseError for StripeErrorCode { | Self::PaymentMethodUnactivated => StatusCode::BAD_REQUEST, Self::RefundFailed | Self::PayoutFailed + | Self::PaymentLinkNotFound | Self::InternalServerError | Self::MandateActive | Self::CustomerRedacted diff --git a/crates/router/src/compatibility/wrap.rs b/crates/router/src/compatibility/wrap.rs index edc56fcee731..f1892698acee 100644 --- a/crates/router/src/compatibility/wrap.rs +++ b/crates/router/src/compatibility/wrap.rs @@ -132,6 +132,20 @@ where .respond_to(request) .map_into_boxed_body() } + + Ok(api::ApplicationResponse::PaymenkLinkForm(payment_link_data)) => { + match api::build_payment_link_html(*payment_link_data) { + Ok(rendered_html) => api::http_response_html_data(rendered_html), + Err(_) => api::http_response_err( + r#"{ + "error": { + "message": "Error while rendering payment link html page" + } + }"#, + ), + } + } + Err(error) => api::log_and_return_error_response(error), }; diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 9ee84e5baea7..5dd58c5d5f26 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -100,6 +100,12 @@ pub struct Settings { pub applepay_merchant_configs: ApplepayMerchantConfigs, pub lock_settings: LockSettings, pub temp_locker_enable_config: TempLockerEnableConfig, + pub payment_link: PaymentLink, +} + +#[derive(Debug, Deserialize, Clone, Default)] +pub struct PaymentLink { + pub sdk_url: String, } #[derive(Debug, Deserialize, Clone, Default)] diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 2e090a6436b2..a3bb3c78915c 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -10,6 +10,7 @@ pub mod errors; pub mod files; pub mod mandate; pub mod metrics; +pub mod payment_link; pub mod payment_methods; pub mod payments; #[cfg(feature = "payouts")] diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index 6805bc2be192..e606771b372c 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -230,6 +230,8 @@ pub enum ApiErrorResponse { IncorrectPaymentMethodConfiguration, #[error(error_type = ErrorType::InvalidRequestError, code = "WE_05", message = "Unable to process the webhook body")] WebhookUnprocessableEntity, + #[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "Payment Link does not exist in our records")] + PaymentLinkNotFound, #[error(error_type = ErrorType::InvalidRequestError, code = "WE_05", message = "Merchant Secret set my merchant for webhook source verification is invalid")] WebhookInvalidMerchantSecret, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_19", message = "{message}")] diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index 37725b7391fa..38d42596cdf0 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -264,6 +264,9 @@ impl ErrorSwitch for ApiErrorRespon Self::ResourceBusy => { AER::Unprocessable(ApiError::new("WE", 5, "There was an issue processing the webhook body", None)) } + Self::PaymentLinkNotFound => { + AER::NotFound(ApiError::new("HE", 2, "Payment Link does not exist in our records", None)) + } } } } diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs new file mode 100644 index 000000000000..2f817beb53e4 --- /dev/null +++ b/crates/router/src/core/payment_link.rs @@ -0,0 +1,172 @@ +use common_utils::ext_traits::AsyncExt; +use error_stack::ResultExt; + +use super::errors::{self, StorageErrorExt}; +use crate::{ + core::payments::helpers, + errors::RouterResponse, + routes::AppState, + services, + types::{domain, storage::enums as storage_enums, transformers::ForeignFrom}, + utils::OptionExt, +}; + +pub async fn retrieve_payment_link( + state: AppState, + payment_link_id: String, +) -> RouterResponse { + let db = &*state.store; + let payment_link_object = db + .find_payment_link_by_payment_link_id(&payment_link_id) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; + + let response = + api_models::payments::RetrievePaymentLinkResponse::foreign_from(payment_link_object); + Ok(services::ApplicationResponse::Json(response)) +} + +pub async fn intiate_payment_link_flow( + state: AppState, + merchant_account: domain::MerchantAccount, + merchant_id: String, + payment_id: String, +) -> RouterResponse { + let db = &*state.store; + let payment_intent = db + .find_payment_intent_by_payment_id_merchant_id( + &payment_id, + &merchant_id, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + let fulfillment_time = payment_intent + .payment_link_id + .as_ref() + .async_and_then(|pli| async move { + db.find_payment_link_by_payment_link_id(pli) + .await + .ok()? + .fulfilment_time + .ok_or(errors::ApiErrorResponse::PaymentNotFound) + .ok() + }) + .await + .get_required_value("fulfillment_time") + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + + helpers::validate_payment_status_against_not_allowed_statuses( + &payment_intent.status, + &[ + storage_enums::IntentStatus::Cancelled, + storage_enums::IntentStatus::Succeeded, + storage_enums::IntentStatus::Processing, + storage_enums::IntentStatus::RequiresCapture, + storage_enums::IntentStatus::RequiresMerchantAction, + ], + "create payment link", + )?; + + let expiry = fulfillment_time.assume_utc().unix_timestamp(); + + let js_script = get_js_script( + payment_intent.amount.to_string(), + payment_intent.currency.unwrap_or_default().to_string(), + merchant_account.publishable_key.unwrap_or_default(), + payment_intent.client_secret.unwrap_or_default(), + payment_intent.payment_id, + expiry, + ); + + let payment_link_data = services::PaymentLinkFormData { + js_script, + sdk_url: state.conf.payment_link.sdk_url.clone(), + }; + Ok(services::ApplicationResponse::PaymenkLinkForm(Box::new( + payment_link_data, + ))) +} + +/* +The get_js_script function is used to inject dynamic value to payment_link sdk, which is unique to every payment. +*/ + +fn get_js_script( + amount: String, + currency: String, + pub_key: String, + secret: String, + payment_id: String, + expiry: i64, +) -> String { + format!( + "window.__PAYMENT_DETAILS_STR = JSON.stringify({{ + client_secret: '{secret}', + amount: '{amount}', + currency: '{currency}', + payment_id: '{payment_id}', + expiry: {expiry}, + // TODO: Remove hardcoded values + merchant_logo: 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', + return_url: 'http://localhost:5500/public/index.html', + currency_symbol: '$', + merchant: 'Steam', + max_items_visible_after_collapse: 3, + order_details: [ + {{ + product_name: + 'dskjghbdsiuh sagfvbsajd ugbfiusedg fiudshgiu sdhgvishd givuhdsifu gnb gidsug biuesbdg iubsedg bsduxbg jhdxbgv jdskfbgi sdfgibuh ew87t54378 ghdfjbv jfdhgvb dufhvbfidu hg5784ghdfbjnk f (taxes incl.)', + quantity: 2, + amount: 100, + product_image: + 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', + }}, + {{ + product_name: \"F1 '23\", + quantity: 4, + amount: 500, + product_image: + 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', + }}, + {{ + product_name: \"Motosport '24\", + quantity: 4, + amount: 500, + product_image: + 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', + }}, + {{ + product_name: 'Trackmania', + quantity: 4, + amount: 500, + product_image: + 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', + }}, + {{ + product_name: 'Ghost Recon', + quantity: 4, + amount: 500, + product_image: + 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', + }}, + {{ + product_name: 'Cup of Tea', + quantity: 4, + amount: 500, + product_image: + 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', + }}, + {{ + product_name: 'Tea cups', + quantity: 4, + amount: 500, + product_image: + 'https://upload.wikimedia.org/wikipedia/commons/8/83/Steam_icon_logo.svg', + }}, + ] + }}); + + const hyper = Hyper(\"{pub_key}\");" + ) +} diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html new file mode 100644 index 000000000000..4ce2ff1919be --- /dev/null +++ b/crates/router/src/core/payment_link/payment_link.html @@ -0,0 +1,1310 @@ + + + + {{ hyperloader_sdk_link }} + + + + + + +
+ +
+
+
+
+
+
+
+
+
+ + + + + + + + +
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + Your Cart + + + +
+
+
+
+
+
+
+
+
+
+ +
+
+ + +
+
+
+
+ + + + + diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 14b97fec42e4..39c709687d86 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1486,6 +1486,7 @@ where pub redirect_response: Option, pub surcharge_details: Option, pub frm_message: Option, + pub payment_link_data: Option, } #[derive(Debug, Default, Clone)] diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index a4cb7b2b9e65..c513ed02ae05 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2194,6 +2194,26 @@ pub fn authenticate_client_secret( } } +pub async fn get_merchant_fullfillment_time( + payment_link_id: Option, + intent_fulfillment_time: Option, + db: &dyn StorageInterface, +) -> RouterResult> { + if let Some(payment_link_id) = payment_link_id { + let payment_link_db = db + .find_payment_link_by_payment_link_id(&payment_link_id) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentLinkNotFound)?; + + let curr_time = common_utils::date_time::now(); + Ok(payment_link_db + .fulfilment_time + .map(|merchant_expiry_time| (merchant_expiry_time - curr_time).whole_seconds())) + } else { + Ok(intent_fulfillment_time) + } +} + pub(crate) fn validate_payment_status_against_not_allowed_statuses( intent_status: &storage_enums::IntentStatus, not_allowed_statuses: &[storage_enums::IntentStatus], @@ -2252,11 +2272,14 @@ pub async fn verify_payment_intent_time_and_client_secret( .await .change_context(errors::ApiErrorResponse::PaymentNotFound)?; - authenticate_client_secret( - Some(&cs), - &payment_intent, + let intent_fulfillment_time = get_merchant_fullfillment_time( + payment_intent.payment_link_id.clone(), merchant_account.intent_fulfillment_time, - )?; + db, + ) + .await?; + + authenticate_client_secret(Some(&cs), &payment_intent, intent_fulfillment_time)?; Ok(payment_intent) }) .await @@ -2377,6 +2400,7 @@ mod tests { connector_metadata: None, feature_metadata: None, attempt_count: 1, + payment_link_id: None, profile_id: None, merchant_decision: None, payment_confirm_source: None, @@ -2386,7 +2410,7 @@ mod tests { assert!(authenticate_client_secret( req_cs.as_ref(), &payment_intent, - merchant_fulfillment_time + merchant_fulfillment_time, ) .is_ok()); // Check if the result is an Ok variant } @@ -2424,6 +2448,7 @@ mod tests { connector_metadata: None, feature_metadata: None, attempt_count: 1, + payment_link_id: None, profile_id: None, merchant_decision: None, payment_confirm_source: None, @@ -2433,7 +2458,7 @@ mod tests { assert!(authenticate_client_secret( req_cs.as_ref(), &payment_intent, - merchant_fulfillment_time + merchant_fulfillment_time, ) .is_err()) } @@ -2471,6 +2496,7 @@ mod tests { connector_metadata: None, feature_metadata: None, attempt_count: 1, + payment_link_id: None, profile_id: None, merchant_decision: None, payment_confirm_source: None, @@ -2480,7 +2506,7 @@ mod tests { assert!(authenticate_client_secret( req_cs.as_ref(), &payment_intent, - merchant_fulfillment_time + merchant_fulfillment_time, ) .is_err()) } @@ -3380,3 +3406,24 @@ impl ApplePayData { Ok(decrypted) } } + +pub fn validate_payment_link_request( + payment_link_object: &api_models::payments::PaymentLinkObject, + confirm: Option, +) -> Result<(), errors::ApiErrorResponse> { + if let Some(cnf) = confirm { + if !cnf { + let current_time = Some(common_utils::date_time::now()); + if current_time > payment_link_object.link_expiry { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "link_expiry time cannot be less than current time".to_string(), + }); + } + } else { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "cannot confirm a payment while creating a payment link".to_string(), + }); + } + } + Ok(()) +} diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index edb6478efec0..95995c46bfb9 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -253,6 +253,7 @@ impl redirect_response, surcharge_details: None, frm_message: frm_response.ok(), + payment_link_data: None, }, Some(CustomerDetails { customer_id: request.customer_id.clone(), diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index d92d86e79239..cd221c0be708 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -173,6 +173,7 @@ impl redirect_response: None, surcharge_details: None, frm_message: None, + payment_link_data: None, }, None, )) diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index eb88265bfc88..0d5dd5c77417 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -231,6 +231,7 @@ impl redirect_response: None, surcharge_details: None, frm_message: None, + payment_link_data: None, }, None, )) diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 029652902248..25143df386a7 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -248,6 +248,7 @@ impl redirect_response, surcharge_details: None, frm_message: None, + payment_link_data: None, }, Some(CustomerDetails { customer_id: request.customer_id.clone(), diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index a40de370b368..86304cbf1750 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -88,10 +88,17 @@ impl "confirm", )?; + let intent_fulfillment_time = helpers::get_merchant_fullfillment_time( + payment_intent.payment_link_id.clone(), + merchant_account.intent_fulfillment_time, + db, + ) + .await?; + helpers::authenticate_client_secret( request.client_secret.as_ref(), &payment_intent, - merchant_account.intent_fulfillment_time, + intent_fulfillment_time, )?; let customer_details = helpers::get_customer_details_from_request(request); @@ -361,6 +368,7 @@ impl redirect_response: None, surcharge_details: None, frm_message: None, + payment_link_data: None, }, Some(customer_details), )) diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 0be92418183f..aec9a13f0388 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -70,6 +70,21 @@ impl .get_payment_intent_id() .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + let payment_link_data = if let Some(payment_link_object) = &request.payment_link_object { + create_payment_link( + request, + payment_link_object.clone(), + merchant_id.clone(), + payment_id.clone(), + db, + state, + amount, + ) + .await? + } else { + None + }; + helpers::validate_business_details( request.business_country, request.business_label.as_ref(), @@ -147,6 +162,7 @@ impl money, request, shipping_address.clone().map(|x| x.address_id), + payment_link_data.clone(), billing_address.clone().map(|x| x.address_id), attempt_id, state, @@ -239,7 +255,6 @@ impl request.confirm, self, ); - let creds_identifier = request .merchant_connector_details .as_ref() @@ -296,6 +311,7 @@ impl redirect_response: None, surcharge_details: None, frm_message: None, + payment_link_data, }, Some(customer_details), )) @@ -431,6 +447,7 @@ impl .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; let customer_id = payment_data.payment_intent.customer_id.clone(); + payment_data.payment_intent = db .update_payment_intent( payment_data.payment_intent, @@ -447,7 +464,6 @@ impl .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; // payment_data.mandate_id = response.and_then(|router_data| router_data.request.mandate_id); - Ok(( payments::is_confirm(self, payment_data.confirm), payment_data, @@ -469,6 +485,10 @@ impl ValidateRequest { helpers::validate_customer_details_in_request(request)?; + if let Some(payment_link_object) = &request.payment_link_object { + helpers::validate_payment_link_request(payment_link_object, request.confirm)?; + } + let given_payment_id = match &request.payment_id { Some(id_type) => Some( id_type @@ -615,6 +635,7 @@ impl PaymentCreate { money: (api::Amount, enums::Currency), request: &api::PaymentsRequest, shipping_address_id: Option, + payment_link_data: Option, billing_address_id: Option, active_attempt_id: String, state: &AppState, @@ -657,6 +678,8 @@ impl PaymentCreate { .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error converting feature_metadata to Value")?; + let payment_link_id = payment_link_data.map(|pl_data| pl_data.payment_link_id); + Ok(storage::PaymentIntentNew { payment_id: payment_id.to_string(), merchant_id: merchant_account.merchant_id.to_string(), @@ -689,6 +712,7 @@ impl PaymentCreate { attempt_count: 1, profile_id: Some(profile_id), merchant_decision: None, + payment_link_id, payment_confirm_source: None, }) } @@ -744,3 +768,50 @@ pub fn payments_create_request_validation( let amount = req.amount.get_required_value("amount")?; Ok((amount, currency)) } + +async fn create_payment_link( + request: &api::PaymentsRequest, + payment_link_object: api_models::payments::PaymentLinkObject, + merchant_id: String, + payment_id: String, + db: &dyn StorageInterface, + state: &AppState, + amount: api::Amount, +) -> RouterResult> { + let created_at @ last_modified_at = Some(common_utils::date_time::now()); + let domain = if let Some(domain_name) = payment_link_object.merchant_custom_domain_name { + format!("https://{domain_name}") + } else { + state.conf.server.base_url.clone() + }; + + let payment_link_id = utils::generate_id(consts::ID_LENGTH, "plink"); + let payment_link = format!( + "{}/payment_link/{}/{}", + domain, + merchant_id.clone(), + payment_id.clone() + ); + let payment_link_req = storage::PaymentLinkNew { + payment_link_id: payment_link_id.clone(), + payment_id: payment_id.clone(), + merchant_id: merchant_id.clone(), + link_to_pay: payment_link.clone(), + amount: amount.into(), + currency: request.currency, + created_at, + last_modified_at, + fulfilment_time: payment_link_object.link_expiry, + }; + let payment_link_db = db + .insert_payment_link(payment_link_req) + .await + .to_duplicate_response(errors::ApiErrorResponse::GenericDuplicateError { + message: "payment link already exists!".to_string(), + })?; + + Ok(Some(api_models::payments::PaymentLinkResponse { + link: payment_link_db.link_to_pay, + payment_link_id: payment_link_db.payment_link_id, + })) +} diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs index 1b33f2dddb81..77e0a7dce2bb 100644 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -197,6 +197,7 @@ impl redirect_response: None, surcharge_details: None, frm_message: None, + payment_link_data: None, }, Some(payments::CustomerDetails { customer_id: request.customer_id.clone(), diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 2e9bd3eb9ed3..938ce92af291 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -159,6 +159,7 @@ impl redirect_response: None, surcharge_details: None, frm_message: frm_response.ok(), + payment_link_data: None, }, None, )) diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index cc91482440ba..1decf13b37e7 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -70,11 +70,19 @@ impl "create a session token for", )?; + let intent_fulfillment_time = helpers::get_merchant_fullfillment_time( + payment_intent.payment_link_id.clone(), + merchant_account.intent_fulfillment_time, + db, + ) + .await?; + helpers::authenticate_client_secret( Some(&request.client_secret), &payment_intent, - merchant_account.intent_fulfillment_time, + intent_fulfillment_time, )?; + let mut payment_attempt = db .find_payment_attempt_by_payment_id_merchant_id_attempt_id( payment_intent.payment_id.as_str(), @@ -193,6 +201,7 @@ impl redirect_response: None, surcharge_details: None, frm_message: None, + payment_link_data: None, }, Some(customer_details), )) diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 4979b7902c85..c3462079cb0f 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -70,10 +70,17 @@ impl "update", )?; + let intent_fulfillment_time = helpers::get_merchant_fullfillment_time( + payment_intent.payment_link_id.clone(), + merchant_account.intent_fulfillment_time, + db, + ) + .await?; + helpers::authenticate_client_secret( payment_intent.client_secret.as_ref(), &payment_intent, - merchant_account.intent_fulfillment_time, + intent_fulfillment_time, )?; payment_attempt = db .find_payment_attempt_by_payment_id_merchant_id_attempt_id( @@ -168,6 +175,7 @@ impl redirect_response: None, surcharge_details: None, frm_message: None, + payment_link_data: None, }, Some(customer_details), )) diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index c2762a966631..ed642d3c4680 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -235,11 +235,18 @@ async fn get_tracker_for_sync< ) .await?; + let intent_fulfillment_time = helpers::get_merchant_fullfillment_time( + payment_intent.payment_link_id.clone(), + merchant_account.intent_fulfillment_time, + db, + ) + .await?; helpers::authenticate_client_secret( request.client_secret.as_ref(), &payment_intent, - merchant_account.intent_fulfillment_time, + intent_fulfillment_time, )?; + let payment_id_str = payment_attempt.payment_id.clone(); let mut connector_response = db @@ -402,6 +409,7 @@ async fn get_tracker_for_sync< ephemeral_key: None, multiple_capture_data, redirect_response: None, + payment_link_data: None, surcharge_details: None, frm_message: frm_response.ok(), }, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 1c7b432c3276..12c065c53eb3 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -82,10 +82,17 @@ impl "update", )?; + let intent_fulfillment_time = helpers::get_merchant_fullfillment_time( + payment_intent.payment_link_id.clone(), + merchant_account.intent_fulfillment_time, + db, + ) + .await?; + helpers::authenticate_client_secret( request.client_secret.as_ref(), &payment_intent, - merchant_account.intent_fulfillment_time, + intent_fulfillment_time, )?; let ( token, @@ -347,6 +354,7 @@ impl redirect_response: None, surcharge_details: None, frm_message: None, + payment_link_data: None, }, Some(customer_details), )) diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index f93b2e9ff4aa..ac15401a3341 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -353,6 +353,7 @@ where { let payment_attempt = payment_data.payment_attempt; let payment_intent = payment_data.payment_intent; + let payment_link_data = payment_data.payment_link_data; let currency = payment_attempt .currency @@ -423,6 +424,7 @@ where .change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "payment_method_data", })?; + let merchant_decision = payment_intent.merchant_decision.to_owned(); let frm_message = payment_data.frm_message.map(FrmMessage::foreign_from); @@ -679,6 +681,7 @@ where .set_feature_metadata(payment_intent.feature_metadata) .set_connector_metadata(payment_intent.connector_metadata) .set_reference_id(payment_attempt.connector_response_reference_id) + .set_payment_link(payment_link_data) .set_profile_id(payment_intent.profile_id) .set_attempt_count(payment_intent.attempt_count) .to_owned(), @@ -739,6 +742,7 @@ where allowed_payment_method_types: payment_intent.allowed_payment_method_types, reference_id: payment_attempt.connector_response_reference_id, attempt_count: payment_intent.attempt_count, + payment_link: payment_link_data, ..Default::default() }, headers, diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 356c1c6a512f..fae242bc6de7 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -17,6 +17,7 @@ pub mod mandate; pub mod merchant_account; pub mod merchant_connector_account; pub mod merchant_key_store; +pub mod payment_link; pub mod payment_method; pub mod payout_attempt; pub mod payouts; @@ -74,6 +75,7 @@ pub trait StorageInterface: + cards_info::CardsInfoInterface + merchant_key_store::MerchantKeyStoreInterface + MasterKeyInterface + + payment_link::PaymentLinkInterface + RedisConnInterface + business_profile::BusinessProfileInterface + 'static diff --git a/crates/router/src/db/payment_link.rs b/crates/router/src/db/payment_link.rs new file mode 100644 index 000000000000..38b59b1d60de --- /dev/null +++ b/crates/router/src/db/payment_link.rs @@ -0,0 +1,66 @@ +use error_stack::IntoReport; + +use super::{MockDb, Store}; +use crate::{ + connection, + core::errors::{self, CustomResult}, + types::storage, +}; + +#[async_trait::async_trait] +pub trait PaymentLinkInterface { + async fn find_payment_link_by_payment_link_id( + &self, + payment_link_id: &str, + ) -> CustomResult; + + async fn insert_payment_link( + &self, + _payment_link: storage::PaymentLinkNew, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl PaymentLinkInterface for Store { + async fn find_payment_link_by_payment_link_id( + &self, + payment_link_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + storage::PaymentLink::find_link_by_payment_link_id(&conn, payment_link_id) + .await + .map_err(Into::into) + .into_report() + } + + async fn insert_payment_link( + &self, + payment_link_object: storage::PaymentLinkNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + payment_link_object + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl PaymentLinkInterface for MockDb { + async fn insert_payment_link( + &self, + _payment_link: storage::PaymentLinkNew, + ) -> CustomResult { + // TODO: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } + + async fn find_payment_link_by_payment_link_id( + &self, + _payment_link_id: &str, + ) -> CustomResult { + // TODO: Implement function for `MockDb`x + Err(errors::StorageError::MockDbError)? + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 738646b2964b..008991bb45b0 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -127,7 +127,8 @@ pub fn mk_app( server_app = server_app .service(routes::PaymentMethods::server(state.clone())) .service(routes::EphemeralKey::server(state.clone())) - .service(routes::Webhooks::server(state.clone())); + .service(routes::Webhooks::server(state.clone())) + .service(routes::PaymentLink::server(state.clone())); } #[cfg(feature = "olap")] diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index a0f8643be3ec..5a36b03e7aea 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -61,6 +61,7 @@ Never share your secret api keys. Keep them guarded and secure. (name = "Disputes", description = "Manage disputes"), // (name = "API Key", description = "Create and manage API Keys"), (name = "Payouts", description = "Create and manage payouts"), + (name = "payment link", description = "Create payment link"), ), paths( crate::routes::refunds::refunds_create, @@ -112,6 +113,7 @@ Never share your secret api keys. Keep them guarded and secure. crate::routes::payouts::payouts_fulfill, crate::routes::payouts::payouts_retrieve, crate::routes::payouts::payouts_update, + crate::routes::payment_link::payment_link_retrieve ), components(schemas( crate::types::api::refunds::RefundRequest, @@ -336,7 +338,12 @@ Never share your secret api keys. Keep them guarded and secure. crate::types::api::api_keys::CreateApiKeyResponse, crate::types::api::api_keys::RetrieveApiKeyResponse, crate::types::api::api_keys::RevokeApiKeyResponse, - crate::types::api::api_keys::UpdateApiKeyRequest + crate::types::api::api_keys::UpdateApiKeyRequest, + api_models::payments::RetrievePaymentLinkRequest, + api_models::payments::PaymentLinkResponse, + api_models::payments::RetrievePaymentLinkResponse, + api_models::payments::PaymentLinkInitiateRequest, + api_models::payments::PaymentLinkObject )), modifiers(&SecurityAddon) )] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 20357040ce16..307797e8ac9d 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -14,6 +14,7 @@ pub mod health; pub mod lock_utils; pub mod mandates; pub mod metrics; +pub mod payment_link; pub mod payment_methods; pub mod payments; #[cfg(feature = "payouts")] @@ -31,8 +32,8 @@ pub use self::app::Payouts; pub use self::app::Verify; pub use self::app::{ ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, Customers, Disputes, EphemeralKey, - Files, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentMethods, Payments, - Refunds, Webhooks, + Files, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, + PaymentMethods, Payments, Refunds, Webhooks, }; #[cfg(feature = "stripe")] pub use super::compatibility::stripe::StripeApis; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 971459dc8604..24d7217aefe8 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -17,7 +17,7 @@ use super::payouts::*; use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(feature = "olap")] use super::{admin::*, api_keys::*, disputes::*, files::*}; -use super::{cache::*, health::*}; +use super::{cache::*, health::*, payment_link::*}; #[cfg(any(feature = "olap", feature = "oltp"))] use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; #[cfg(feature = "oltp")] @@ -576,6 +576,22 @@ impl Cache { } } +pub struct PaymentLink; + +impl PaymentLink { + pub fn server(state: AppState) -> Scope { + web::scope("/payment_link") + .app_data(web::Data::new(state)) + .service( + web::resource("/{payment_link_id}").route(web::get().to(payment_link_retrieve)), + ) + .service( + web::resource("{merchant_id}/{payment_id}") + .route(web::get().to(initiate_payment_link)), + ) + } +} + pub struct BusinessProfile; #[cfg(feature = "olap")] diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 80763ab434fe..cec510cd0c77 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -21,6 +21,7 @@ pub enum ApiIdentifier { Business, Verification, ApiKeys, + PaymentLink, } impl From for ApiIdentifier { @@ -112,6 +113,8 @@ impl From for ApiIdentifier { | Flow::BusinessProfileList => Self::Business, Flow::Verification => Self::Verification, + + Flow::PaymentLinkInitiate | Flow::PaymentLinkRetrieve => Self::PaymentLink, } } } diff --git a/crates/router/src/routes/metrics/request.rs b/crates/router/src/routes/metrics/request.rs index 1f1a9b3e3821..30db586b7f9c 100644 --- a/crates/router/src/routes/metrics/request.rs +++ b/crates/router/src/routes/metrics/request.rs @@ -60,6 +60,7 @@ pub fn track_response_status_code(response: &ApplicationResponse) -> i64 { | ApplicationResponse::StatusOk | ApplicationResponse::TextPlain(_) | ApplicationResponse::Form(_) + | ApplicationResponse::PaymenkLinkForm(_) | ApplicationResponse::FileData(_) | ApplicationResponse::JsonWithHeaders(_) => 200, ApplicationResponse::JsonForRedirection(_) => 302, diff --git a/crates/router/src/routes/payment_link.rs b/crates/router/src/routes/payment_link.rs new file mode 100644 index 000000000000..b664ee4429d4 --- /dev/null +++ b/crates/router/src/routes/payment_link.rs @@ -0,0 +1,82 @@ +use actix_web::{web, Responder}; +use router_env::{instrument, tracing, Flow}; + +use crate::{ + core::{api_locking, payment_link::*}, + services::{api, authentication as auth}, + AppState, +}; + +/// Payments Link - Retrieve +/// +/// To retrieve the properties of a Payment Link. This may be used to get the status of a previously initiated payment or next action for an ongoing payment +#[utoipa::path( + get, + path = "/payment_link/{payment_link_id}", + params( + ("payment_link_id" = String, Path, description = "The identifier for payment link") + ), + request_body=RetrievePaymentLinkRequest, + responses( + (status = 200, description = "Gets details regarding payment link", body = RetrievePaymentLinkResponse), + (status = 404, description = "No payment link found") + ), + tag = "Payments", + operation_id = "Retrieve a Payment Link", + security(("api_key" = []), ("publishable_key" = [])) +)] +#[instrument(skip(state, req), fields(flow = ?Flow::PaymentLinkRetrieve))] + +pub async fn payment_link_retrieve( + state: web::Data, + req: actix_web::HttpRequest, + path: web::Path, + json_payload: web::Query, +) -> impl Responder { + let flow = Flow::PaymentLinkRetrieve; + let payload = json_payload.into_inner(); + let (auth_type, _) = match auth::check_client_secret_and_get_auth(req.headers(), &payload) { + Ok(auth) => auth, + Err(err) => return api::log_and_return_error_response(error_stack::report!(err)), + }; + api::server_wrap( + flow, + state, + &req, + payload.clone(), + |state, _auth, _| retrieve_payment_link(state, path.clone()), + &*auth_type, + api_locking::LockAction::NotApplicable, + ) + .await +} + +pub async fn initiate_payment_link( + state: web::Data, + req: actix_web::HttpRequest, + path: web::Path<(String, String)>, +) -> impl Responder { + let flow = Flow::PaymentLinkInitiate; + let (merchant_id, payment_id) = path.into_inner(); + let payload = api_models::payments::PaymentLinkInitiateRequest { + payment_id, + merchant_id: merchant_id.clone(), + }; + api::server_wrap( + flow, + state, + &req, + payload.clone(), + |state, auth, _| { + intiate_payment_link_flow( + state, + auth.merchant_account, + payload.merchant_id.clone(), + payload.payment_id.clone(), + ) + }, + &crate::services::authentication::MerchantIdAuth(merchant_id), + api_locking::LockAction::NotApplicable, + ) + .await +} diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index def00bb3f2f4..b56e55b6c7b8 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -1,6 +1,5 @@ pub mod client; pub mod request; - use std::{ collections::HashMap, error::Error, @@ -20,6 +19,7 @@ use masking::{ExposeOptionInterface, PeekInterface}; use router_env::{instrument, tracing, tracing_actix_web::RequestId, Tag}; use serde::Serialize; use serde_json::json; +use tera::{Context, Tera}; use self::request::{HeaderExt, RequestBuilderExt}; use crate::{ @@ -655,10 +655,17 @@ pub enum ApplicationResponse { TextPlain(String), JsonForRedirection(api::RedirectionResponse), Form(Box), + PaymenkLinkForm(Box), FileData((Vec, mime::Mime)), JsonWithHeaders((R, Vec<(String, String)>)), } +#[derive(Debug, Eq, PartialEq, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentLinkFormData { + pub js_script: String, + pub sdk_url: String, +} + #[derive(Debug, Eq, PartialEq)] pub struct RedirectionFormData { pub redirect_form: RedirectForm, @@ -887,6 +894,20 @@ where .respond_to(request) .map_into_boxed_body() } + + Ok(ApplicationResponse::PaymenkLinkForm(payment_link_data)) => { + match build_payment_link_html(*payment_link_data) { + Ok(rendered_html) => http_response_html_data(rendered_html), + Err(_) => http_response_err( + r#"{ + "error": { + "message": "Error while rendering payment link html page" + } + }"#, + ), + } + } + Ok(ApplicationResponse::JsonWithHeaders((response, headers))) => { let request_elapsed_time = request.headers().get(X_HS_LATENCY).and_then(|value| { if value == "true" { @@ -1006,6 +1027,10 @@ pub fn http_response_file_data( HttpResponse::Ok().content_type(content_type).body(res) } +pub fn http_response_html_data(res: T) -> HttpResponse { + HttpResponse::Ok().content_type(mime::TEXT_HTML).body(res) +} + pub fn http_response_ok() -> HttpResponse { HttpResponse::Ok().finish() } @@ -1329,3 +1354,32 @@ mod tests { assert_eq!(mime::APPLICATION_JSON.essence_str(), "application/json"); } } + +pub fn build_payment_link_html( + payment_link_data: PaymentLinkFormData, +) -> CustomResult { + let html_template = include_str!("../core/payment_link/payment_link.html").to_string(); + + let mut tera = Tera::default(); + + let _ = tera.add_raw_template("payment_link", &html_template); + + let mut context = Context::new(); + context.insert( + "hyperloader_sdk_link", + &get_hyper_loader_sdk(&payment_link_data.sdk_url), + ); + context.insert("payment_details_js_script", &payment_link_data.js_script); + + match tera.render("payment_link", &context) { + Ok(rendered_html) => Ok(rendered_html), + Err(tera_error) => { + crate::logger::warn!("{tera_error}"); + Err(errors::ApiErrorResponse::InternalServerError)? + } + } +} + +fn get_hyper_loader_sdk(sdk_url: &str) -> String { + format!("") +} diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 922d560a4a24..eec872a9f34f 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -378,6 +378,12 @@ impl ClientSecretFetch for api_models::payments::PaymentsRetrieveRequest { } } +impl ClientSecretFetch for api_models::payments::RetrievePaymentLinkRequest { + fn get_client_secret(&self) -> Option<&String> { + self.client_secret.as_ref() + } +} + pub fn get_auth_type_and_flow( headers: &HeaderMap, ) -> RouterResult<( diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index 9995f7cce89f..623a5f94989c 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -19,6 +19,7 @@ pub mod merchant_account; pub mod merchant_connector_account; pub mod merchant_key_store; pub mod payment_attempt; +pub mod payment_link; pub mod payment_method; pub use diesel_models::{ProcessTracker, ProcessTrackerNew, ProcessTrackerUpdate}; pub use scheduler::db::process_tracker; @@ -37,8 +38,9 @@ pub use data_models::payments::{ pub use self::{ address::*, api_keys::*, capture::*, cards_info::*, configs::*, connector_response::*, customers::*, dispute::*, ephemeral_key::*, events::*, file::*, locker_mock_up::*, mandate::*, - merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_method::*, - payout_attempt::*, payouts::*, process_tracker::*, refund::*, reverse_lookup::*, + merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, + payment_method::*, payout_attempt::*, payouts::*, process_tracker::*, refund::*, + reverse_lookup::*, }; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/crates/router/src/types/storage/payment_link.rs b/crates/router/src/types/storage/payment_link.rs new file mode 100644 index 000000000000..1fa2465e5131 --- /dev/null +++ b/crates/router/src/types/storage/payment_link.rs @@ -0,0 +1 @@ +pub use diesel_models::payment_link::{PaymentLink, PaymentLinkNew}; diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 04334fc201ff..2e376b0c2185 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -831,6 +831,22 @@ impl } } +impl ForeignFrom for api_models::payments::RetrievePaymentLinkResponse { + fn foreign_from(payment_link_object: storage::PaymentLink) -> Self { + Self { + payment_link_id: payment_link_object.payment_link_id, + payment_id: payment_link_object.payment_id, + merchant_id: payment_link_object.merchant_id, + link_to_pay: payment_link_object.link_to_pay, + amount: payment_link_object.amount, + currency: payment_link_object.currency, + created_at: payment_link_object.created_at, + last_modified_at: payment_link_object.last_modified_at, + link_expiry: payment_link_object.fulfilment_time, + } + } +} + impl From for payments::AddressDetails { fn from(addr: domain::Address) -> Self { Self { diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index d633cb4a5eeb..e838c5d9d53f 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -195,6 +195,10 @@ pub enum Flow { RetrieveDisputeEvidence, /// Invalidate cache flow CacheInvalidate, + /// Payment Link Retrieve flow + PaymentLinkRetrieve, + /// payment Link Initiate flow + PaymentLinkInitiate, /// Create a business profile BusinessProfileCreate, /// Update a business profile diff --git a/crates/storage_impl/src/mock_db.rs b/crates/storage_impl/src/mock_db.rs index e41d7a3aba07..ca043ac587bd 100644 --- a/crates/storage_impl/src/mock_db.rs +++ b/crates/storage_impl/src/mock_db.rs @@ -40,6 +40,7 @@ pub struct MockDb { pub merchant_key_store: Arc>>, pub business_profiles: Arc>>, pub reverse_lookups: Arc>>, + pub payment_link: Arc>>, } impl MockDb { @@ -72,6 +73,7 @@ impl MockDb { merchant_key_store: Default::default(), business_profiles: Default::default(), reverse_lookups: Default::default(), + payment_link: Default::default(), }) } } diff --git a/crates/storage_impl/src/mock_db/payment_intent.rs b/crates/storage_impl/src/mock_db/payment_intent.rs index 43d7207d452a..2dc720b9f5d0 100644 --- a/crates/storage_impl/src/mock_db/payment_intent.rs +++ b/crates/storage_impl/src/mock_db/payment_intent.rs @@ -105,6 +105,7 @@ impl PaymentIntentInterface for MockDb { attempt_count: new.attempt_count, profile_id: new.profile_id, merchant_decision: new.merchant_decision, + payment_link_id: new.payment_link_id, payment_confirm_source: new.payment_confirm_source, }; payment_intents.push(payment_intent.clone()); diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index 6814512028b4..c4718b34258f 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -90,6 +90,7 @@ impl PaymentIntentInterface for KVRouterStore { attempt_count: new.attempt_count, profile_id: new.profile_id.clone(), merchant_decision: new.merchant_decision.clone(), + payment_link_id: new.payment_link_id.clone(), payment_confirm_source: new.payment_confirm_source, }; @@ -697,6 +698,7 @@ impl DataModelExt for PaymentIntentNew { attempt_count: self.attempt_count, profile_id: self.profile_id, merchant_decision: self.merchant_decision, + payment_link_id: self.payment_link_id, payment_confirm_source: self.payment_confirm_source, } } @@ -734,6 +736,7 @@ impl DataModelExt for PaymentIntentNew { attempt_count: storage_model.attempt_count, profile_id: storage_model.profile_id, merchant_decision: storage_model.merchant_decision, + payment_link_id: storage_model.payment_link_id, payment_confirm_source: storage_model.payment_confirm_source, } } @@ -766,9 +769,9 @@ impl DataModelExt for PaymentIntent { setup_future_usage: self.setup_future_usage, off_session: self.off_session, client_secret: self.client_secret, - active_attempt_id: self.active_attempt_id, business_country: self.business_country, business_label: self.business_label, + active_attempt_id: self.active_attempt_id, order_details: self.order_details, allowed_payment_method_types: self.allowed_payment_method_types, connector_metadata: self.connector_metadata, @@ -776,6 +779,7 @@ impl DataModelExt for PaymentIntent { attempt_count: self.attempt_count, profile_id: self.profile_id, merchant_decision: self.merchant_decision, + payment_link_id: self.payment_link_id, payment_confirm_source: self.payment_confirm_source, } } @@ -814,6 +818,7 @@ impl DataModelExt for PaymentIntent { attempt_count: storage_model.attempt_count, profile_id: storage_model.profile_id, merchant_decision: storage_model.merchant_decision, + payment_link_id: storage_model.payment_link_id, payment_confirm_source: storage_model.payment_confirm_source, } } diff --git a/migrations/2023-09-08-101302_add_payment_link/down.sql b/migrations/2023-09-08-101302_add_payment_link/down.sql new file mode 100644 index 000000000000..f2ff37da5583 --- /dev/null +++ b/migrations/2023-09-08-101302_add_payment_link/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +drop table payment_link; \ No newline at end of file diff --git a/migrations/2023-09-08-101302_add_payment_link/up.sql b/migrations/2023-09-08-101302_add_payment_link/up.sql new file mode 100644 index 000000000000..565f53d9e5c5 --- /dev/null +++ b/migrations/2023-09-08-101302_add_payment_link/up.sql @@ -0,0 +1,13 @@ +-- Your SQL goes here +CREATE TABLE payment_link ( + payment_link_id VARCHAR(255) NOT NULL, + payment_id VARCHAR(64) NOT NULL, + link_to_pay VARCHAR(255) NOT NULL, + merchant_id VARCHAR(64) NOT NULL, + amount INT8 NOT NULL, + currency "Currency", + created_at TIMESTAMP NOT NULL, + last_modified_at TIMESTAMP NOT NULL, + fulfilment_time TIMESTAMP, + PRIMARY KEY (payment_link_id) +); diff --git a/migrations/2023-09-08-114828_add_payment_link_id_in_payment_intent/down.sql b/migrations/2023-09-08-114828_add_payment_link_id_in_payment_intent/down.sql new file mode 100644 index 000000000000..6cead9562391 --- /dev/null +++ b/migrations/2023-09-08-114828_add_payment_link_id_in_payment_intent/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent DROP COLUMN payment_link_id; diff --git a/migrations/2023-09-08-114828_add_payment_link_id_in_payment_intent/up.sql b/migrations/2023-09-08-114828_add_payment_link_id_in_payment_intent/up.sql new file mode 100644 index 000000000000..9c827740a98e --- /dev/null +++ b/migrations/2023-09-08-114828_add_payment_link_id_in_payment_intent/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_intent ADD column payment_link_id VARCHAR(255); \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 27b73f1c097d..d9fd4055c94a 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -794,6 +794,60 @@ ] } }, + "/payment_link/{payment_link_id}": { + "get": { + "tags": [ + "Payments" + ], + "summary": "Payments Link - Retrieve", + "description": "Payments Link - Retrieve\n\nTo retrieve the properties of a Payment Link. This may be used to get the status of a previously initiated payment or next action for an ongoing payment", + "operationId": "Retrieve a Payment Link", + "parameters": [ + { + "name": "payment_link_id", + "in": "path", + "description": "The identifier for payment link", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RetrievePaymentLinkRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Gets details regarding payment link", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RetrievePaymentLinkResponse" + } + } + } + }, + "404": { + "description": "No payment link found" + } + }, + "security": [ + { + "api_key": [] + }, + { + "publishable_key": [] + } + ] + } + }, "/payment_methods": { "post": { "tags": [ @@ -7710,6 +7764,50 @@ } ] }, + "PaymentLinkInitiateRequest": { + "type": "object", + "required": [ + "merchant_id", + "payment_id" + ], + "properties": { + "merchant_id": { + "type": "string" + }, + "payment_id": { + "type": "string" + } + } + }, + "PaymentLinkObject": { + "type": "object", + "properties": { + "link_expiry": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "merchant_custom_domain_name": { + "type": "string", + "nullable": true + } + } + }, + "PaymentLinkResponse": { + "type": "object", + "required": [ + "link", + "payment_link_id" + ], + "properties": { + "link": { + "type": "string" + }, + "payment_link_id": { + "type": "string" + } + } + }, "PaymentListConstraints": { "type": "object", "properties": { @@ -8769,6 +8867,14 @@ ], "nullable": true }, + "payment_link_object": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentLinkObject" + } + ], + "nullable": true + }, "profile_id": { "type": "string", "description": "The business profile to use for this payment, if not passed the default business profile\nassociated with the merchant account will be used.", @@ -9116,6 +9222,14 @@ ], "nullable": true }, + "payment_link_object": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentLinkObject" + } + ], + "nullable": true + }, "profile_id": { "type": "string", "description": "The business profile to use for this payment, if not passed the default business profile\nassociated with the merchant account will be used.", @@ -9510,6 +9624,14 @@ "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", @@ -10573,6 +10695,66 @@ } } }, + "RetrievePaymentLinkRequest": { + "type": "object", + "properties": { + "client_secret": { + "type": "string", + "nullable": true + } + } + }, + "RetrievePaymentLinkResponse": { + "type": "object", + "required": [ + "payment_link_id", + "payment_id", + "merchant_id", + "link_to_pay", + "amount", + "created_at", + "last_modified_at" + ], + "properties": { + "payment_link_id": { + "type": "string" + }, + "payment_id": { + "type": "string" + }, + "merchant_id": { + "type": "string" + }, + "link_to_pay": { + "type": "string" + }, + "amount": { + "type": "integer", + "format": "int64" + }, + "currency": { + "allOf": [ + { + "$ref": "#/components/schemas/Currency" + } + ], + "nullable": true + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "last_modified_at": { + "type": "string", + "format": "date-time" + }, + "link_expiry": { + "type": "string", + "format": "date-time", + "nullable": true + } + } + }, "RetryAction": { "type": "string", "enum": [ @@ -11508,6 +11690,10 @@ { "name": "Payouts", "description": "Create and manage payouts" + }, + { + "name": "payment link", + "description": "Create payment link" } ] } \ No newline at end of file From 0889a6ed0691abeed7bba44e7024545abcc74aef Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Thu, 12 Oct 2023 14:58:52 +0530 Subject: [PATCH 15/21] refactor(connector): [noon] use connector_request_reference_id for sync (#2558) --- crates/router/src/connector/noon.rs | 11 +++++------ .../router/src/connector/noon/transformers.rs | 18 ++++++++++++++---- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/crates/router/src/connector/noon.rs b/crates/router/src/connector/noon.rs index 5bd7f2495408..771c444a43e4 100644 --- a/crates/router/src/connector/noon.rs +++ b/crates/router/src/connector/noon.rs @@ -9,7 +9,6 @@ use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; use transformers as noon; -use super::utils::PaymentsSyncRequestData; use crate::{ configs::settings, connector::utils as connector_utils, @@ -276,10 +275,10 @@ impl ConnectorIntegration CustomResult { - let connector_transaction_id = req.request.get_connector_transaction_id()?; Ok(format!( - "{}payment/v1/order/{connector_transaction_id}", - self.base_url(connectors) + "{}payment/v1/order/getbyreference/{}", + self.base_url(connectors), + req.connector_request_reference_id )) } @@ -569,9 +568,9 @@ impl ConnectorIntegration CustomResult { Ok(format!( - "{}payment/v1/order/{}", + "{}payment/v1/order/getbyreference/{}", self.base_url(connectors), - req.request.connector_transaction_id + req.connector_request_reference_id )) } diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index 5300525b7cbd..749c8c8dea7b 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -4,8 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::{ connector::utils::{ - self as conn_utils, CardData, PaymentsAuthorizeRequestData, RefundsRequestData, RouterData, - WalletData, + self as conn_utils, CardData, PaymentsAuthorizeRequestData, RouterData, WalletData, }, core::errors, services, @@ -438,6 +437,7 @@ impl pub struct NoonActionTransaction { amount: String, currency: diesel_models::enums::Currency, + transaction_reference: Option, } #[derive(Debug, Serialize)] @@ -466,6 +466,7 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for NoonPaymentsActionRequest { item.request.currency, )?, currency: item.request.currency, + transaction_reference: None, }; Ok(Self { api_operation: NoonApiOperations::Capture, @@ -507,6 +508,7 @@ impl TryFrom<&types::RefundsRouterData> for NoonPaymentsActionRequest { item.request.currency, )?, currency: item.request.currency, + transaction_reference: Some(item.request.refund_id.clone()), }; Ok(Self { api_operation: NoonApiOperations::Refund, @@ -570,9 +572,11 @@ impl TryFrom> } #[derive(Default, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct NoonRefundResponseTransactions { id: String, status: RefundStatus, + transaction_reference: Option, } #[derive(Default, Debug, Deserialize)] @@ -592,13 +596,19 @@ impl TryFrom> fn try_from( item: types::RefundsResponseRouterData, ) -> Result { - let connector_refund_id = item.data.request.get_connector_refund_id()?; let noon_transaction: &NoonRefundResponseTransactions = item .response .result .transactions .iter() - .find(|transaction| transaction.id == connector_refund_id) + .find(|transaction| { + transaction + .transaction_reference + .clone() + .map_or(false, |transaction_instance| { + transaction_instance == item.data.request.refund_id + }) + }) .ok_or(errors::ConnectorError::ResponseHandlingFailed)?; Ok(Self { From 53d760460305e16f03d86f699acb035151dfdfad Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Thu, 12 Oct 2023 15:00:56 +0530 Subject: [PATCH 16/21] fix(router): add customer_id validation for `payment method create` flow (#2543) --- crates/router/src/core/payment_methods/cards.rs | 17 ++++++++++++----- crates/router/src/core/payments/tokenization.rs | 10 +++------- crates/router/src/core/payouts/helpers.rs | 5 ++--- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index ee5f34938f99..c775dbc6ecdf 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -54,6 +54,7 @@ use crate::{ }; #[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] pub async fn create_payment_method( db: &dyn db::StorageInterface, req: &api::PaymentMethodCreate, @@ -62,7 +63,12 @@ pub async fn create_payment_method( merchant_id: &str, pm_metadata: Option, payment_method_data: Option, -) -> errors::CustomResult { + key_store: &domain::MerchantKeyStore, +) -> errors::CustomResult { + db.find_customer_by_customer_id_merchant_id(customer_id, merchant_id, key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)?; + let response = db .insert_payment_method(storage::PaymentMethodNew { customer_id: customer_id.to_string(), @@ -76,7 +82,9 @@ pub async fn create_payment_method( payment_method_data, ..storage::PaymentMethodNew::default() }) - .await?; + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to add payment method in db")?; Ok(response) } @@ -141,10 +149,9 @@ pub async fn add_payment_method( &resp.merchant_id, pm_metadata.cloned(), pm_data_encrypted, + key_store, ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to save Payment Method")?; + .await?; } Ok(resp).map(services::ApplicationResponse::Json) diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index ebffee3d1a5d..f7831465e1ce 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -126,12 +126,9 @@ where merchant_id, pm_metadata, pm_data_encrypted, + key_store, ) .await - .change_context( - errors::ApiErrorResponse::InternalServerError, - ) - .attach_printable("Failed to add payment method in db") } _ => { Err(report!(errors::ApiErrorResponse::InternalServerError) @@ -155,10 +152,9 @@ where merchant_id, pm_metadata, pm_data_encrypted, + key_store, ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to add payment method in db")?; + .await?; }; Some(locker_response.0.payment_method_id) } else { diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index f750b645d6c6..9890cd9d5efd 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -247,10 +247,9 @@ pub async fn save_payout_data_to_locker( &merchant_account.merchant_id, None, card_details_encrypted, + key_store, ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to save payment method")?; + .await?; Ok(()) } From 751f16eaee254ab8f0068e2e9e81e3e4b7fe133f Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Thu, 12 Oct 2023 17:17:58 +0530 Subject: [PATCH 17/21] refactor(connector): [noon] update and add recommended fields (#2381) Co-authored-by: Arjun Karthik --- .../router/src/connector/noon/transformers.rs | 56 ++++++++++++++++++- crates/router/src/connector/utils.rs | 8 +++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index 749c8c8dea7b..21a6501bb50f 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -1,3 +1,4 @@ +use common_utils::pii; use error_stack::ResultExt; use masking::Secret; use serde::{Deserialize, Serialize}; @@ -36,6 +37,23 @@ pub struct NoonSubscriptionData { name: String, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NoonBillingAddress { + street: Option>, + street2: Option>, + city: Option, + state_province: Option>, + country: Option, + postal_code: Option>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NoonBilling { + address: NoonBillingAddress, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct NoonOrder { @@ -46,6 +64,7 @@ pub struct NoonOrder { reference: String, //Short description of the order. name: String, + ip_address: Option>, } #[derive(Debug, Serialize)] @@ -164,6 +183,7 @@ pub struct NoonPaymentsRequest { configuration: NoonConfiguration, payment_data: NoonPaymentData, subscription: Option, + billing: Option, } impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { @@ -247,6 +267,27 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { .take(50) .collect(); + let ip_address = item.request.get_ip_address_as_optional(); + + let channel = NoonChannels::Web; + + let billing = item + .address + .billing + .clone() + .and_then(|billing_address| billing_address.address) + .map(|address| NoonBilling { + address: NoonBillingAddress { + street: address.line1, + street2: address.line2, + city: address.city, + // If state is passed in request, country becomes mandatory, keep a check while debugging failed payments + state_province: address.state, + country: address.country, + postal_code: address.zip, + }, + }); + let (subscription, tokenize_c_c) = match item.request.setup_future_usage.is_some().then_some(( NoonSubscriptionData { @@ -261,10 +302,11 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { let order = NoonOrder { amount: conn_utils::to_currency_base_unit(item.request.amount, item.request.currency)?, currency, - channel: NoonChannels::Web, + channel, category, reference: item.connector_request_reference_id.clone(), name, + ip_address, }; let payment_action = if item.request.is_auto_capture()? { NoonPaymentActions::Sale @@ -274,6 +316,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { Ok(Self { api_operation: NoonApiOperations::Initiate, order, + billing, configuration: NoonConfiguration { payment_action, return_url: item.request.router_return_url.clone(), @@ -333,7 +376,8 @@ impl From for enums::AttemptStatus { fn from(item: NoonPaymentStatus) -> Self { match item { NoonPaymentStatus::Authorized => Self::Authorized, - NoonPaymentStatus::Captured | NoonPaymentStatus::PartiallyCaptured => Self::Charged, + NoonPaymentStatus::Captured => Self::Charged, + NoonPaymentStatus::PartiallyCaptured => Self::PartialCharged, NoonPaymentStatus::Reversed => Self::Voided, NoonPaymentStatus::Cancelled | NoonPaymentStatus::Expired => Self::AuthenticationFailed, NoonPaymentStatus::ThreeDsEnrollInitiated | NoonPaymentStatus::ThreeDsEnrollChecked => { @@ -444,6 +488,7 @@ pub struct NoonActionTransaction { #[serde(rename_all = "camelCase")] pub struct NoonActionOrder { id: String, + cancellation_reason: Option, } #[derive(Debug, Serialize)] @@ -459,6 +504,7 @@ impl TryFrom<&types::PaymentsCaptureRouterData> for NoonPaymentsActionRequest { fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { let order = NoonActionOrder { id: item.request.connector_transaction_id.clone(), + cancellation_reason: None, }; let transaction = NoonActionTransaction { amount: conn_utils::to_currency_base_unit( @@ -488,6 +534,11 @@ impl TryFrom<&types::PaymentsCancelRouterData> for NoonPaymentsCancelRequest { fn try_from(item: &types::PaymentsCancelRouterData) -> Result { let order = NoonActionOrder { id: item.request.connector_transaction_id.clone(), + cancellation_reason: item + .request + .cancellation_reason + .clone() + .map(|reason| reason.chars().take(100).collect()), // Max 100 chars }; Ok(Self { api_operation: NoonApiOperations::Reverse, @@ -501,6 +552,7 @@ impl TryFrom<&types::RefundsRouterData> for NoonPaymentsActionRequest { fn try_from(item: &types::RefundsRouterData) -> Result { let order = NoonActionOrder { id: item.request.connector_transaction_id.clone(), + cancellation_reason: None, }; let transaction = NoonActionTransaction { amount: conn_utils::to_currency_base_unit( diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 4f59c38ea7c9..455646f619e2 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -283,6 +283,7 @@ pub trait PaymentsAuthorizeRequestData { fn get_payment_method_type(&self) -> Result; fn get_connector_mandate_id(&self) -> Result; fn get_complete_authorize_url(&self) -> Result; + fn get_ip_address_as_optional(&self) -> Option>; } impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { @@ -370,6 +371,13 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { self.connector_mandate_id() .ok_or_else(missing_field_err("connector_mandate_id")) } + fn get_ip_address_as_optional(&self) -> Option> { + self.browser_info.clone().and_then(|browser_info| { + browser_info + .ip_address + .map(|ip| Secret::new(ip.to_string())) + }) + } } pub trait ConnectorCustomerData { From efa53204e8ab1ef1192bcdc07ed99306475badbc Mon Sep 17 00:00:00 2001 From: Panagiotis Ganelis <50522617+PanGan21@users.noreply.github.com> Date: Thu, 12 Oct 2023 13:55:13 +0200 Subject: [PATCH 18/21] refactor(worldline): use `connector_request_reference_id` as reference to the connector (#2498) --- crates/router/src/connector/worldline/transformers.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/router/src/connector/worldline/transformers.rs b/crates/router/src/connector/worldline/transformers.rs index f1267c097661..d02ab60c8b91 100644 --- a/crates/router/src/connector/worldline/transformers.rs +++ b/crates/router/src/connector/worldline/transformers.rs @@ -40,11 +40,18 @@ pub struct AmountOfMoney { pub currency_code: String, } +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct References { + pub merchant_reference: String, +} + #[derive(Default, Debug, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct Order { pub amount_of_money: AmountOfMoney, pub customer: Customer, + pub references: References, } #[derive(Default, Debug, Serialize, Eq, PartialEq)] @@ -202,6 +209,9 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentsRequest { currency_code: item.request.currency.to_string().to_uppercase(), }, customer, + references: References { + merchant_reference: item.connector_request_reference_id.clone(), + }, }; let shipping = item From 28d02f94c6d52d05b6f520e4d48ba88adf7be619 Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Thu, 12 Oct 2023 19:05:37 +0530 Subject: [PATCH 19/21] fix(connector): [braintree] add 3ds redirection error mapping and metadata validation (#2552) --- crates/router/src/connector/braintree.rs | 41 +++++- .../braintree_graphql_transformers.rs | 123 ++++++++++-------- crates/router/src/core/admin.rs | 36 +++-- crates/router/src/core/errors.rs | 2 + crates/router/src/services/api.rs | 19 ++- 5 files changed, 150 insertions(+), 71 deletions(-) diff --git a/crates/router/src/connector/braintree.rs b/crates/router/src/connector/braintree.rs index ff9e93c2e9ff..06504e4a9763 100644 --- a/crates/router/src/connector/braintree.rs +++ b/crates/router/src/connector/braintree.rs @@ -1496,10 +1496,45 @@ impl services::ConnectorRedirectResponse for Braintree { fn get_flow_type( &self, _query_params: &str, - _json_payload: Option, - _action: services::PaymentAction, + json_payload: Option, + action: services::PaymentAction, ) -> CustomResult { - Ok(payments::CallConnectorAction::Trigger) + match action { + services::PaymentAction::PSync => match json_payload { + Some(payload) => { + let redirection_response:braintree_graphql_transformers::BraintreeRedirectionResponse = serde_json::from_value(payload) + .into_report() + .change_context( + errors::ConnectorError::MissingConnectorRedirectionPayload { + field_name: "redirection_response", + }, + )?; + let braintree_payload = + serde_json::from_str::< + braintree_graphql_transformers::BraintreeThreeDsErrorResponse, + >(&redirection_response.authentication_response); + let (error_code, error_message) = match braintree_payload { + Ok(braintree_response_payload) => ( + braintree_response_payload.code, + braintree_response_payload.message, + ), + Err(_) => ( + consts::NO_ERROR_CODE.to_string(), + redirection_response.authentication_response, + ), + }; + Ok(payments::CallConnectorAction::StatusUpdate { + status: enums::AttemptStatus::AuthenticationFailed, + error_code: Some(error_code), + error_message: Some(error_message), + }) + } + None => Ok(payments::CallConnectorAction::Avoid), + }, + services::PaymentAction::CompleteAuthorize => { + Ok(payments::CallConnectorAction::Trigger) + } + } } } diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs index 8e9b15ef461a..e967caaba88f 100644 --- a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs +++ b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs @@ -1,3 +1,4 @@ +use common_utils::pii; use error_stack::{IntoReport, ResultExt}; use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; @@ -77,8 +78,19 @@ pub enum BraintreePaymentsRequest { #[derive(Debug, Deserialize)] pub struct BraintreeMeta { - merchant_account_id: Option>, - merchant_config_currency: Option, + merchant_account_id: Secret, + merchant_config_currency: types::storage::enums::Currency, +} + +impl TryFrom<&Option> for BraintreeMeta { + type Error = error_stack::Report; + fn try_from(meta_data: &Option) -> Result { + let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConfig { + field_name: "merchant connector account metadata", + })?; + Ok(metadata) + } } #[derive(Debug, Serialize)] @@ -96,10 +108,13 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>> item: &BraintreeRouterData<&types::PaymentsAuthorizeRouterData>, ) -> Result { let metadata: BraintreeMeta = - utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone())?; + utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConfig { + field_name: "merchant connector account metadata", + })?; utils::validate_currency( item.router_data.request.currency, - metadata.merchant_config_currency, + Some(metadata.merchant_config_currency), )?; match item.router_data.request.payment_method_data.clone() { @@ -140,26 +155,28 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>> fn try_from( item: &BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>, ) -> Result { - match item.router_data.request.payment_method_data.clone() { - Some(api::PaymentMethodData::Card(_)) => { + match item.router_data.payment_method { + api_models::enums::PaymentMethod::Card => { Ok(Self::Card(CardPaymentRequest::try_from(item)?)) } - Some(api_models::payments::PaymentMethodData::CardRedirect(_)) - | Some(api_models::payments::PaymentMethodData::Wallet(_)) - | Some(api_models::payments::PaymentMethodData::PayLater(_)) - | Some(api_models::payments::PaymentMethodData::BankRedirect(_)) - | Some(api_models::payments::PaymentMethodData::BankDebit(_)) - | Some(api_models::payments::PaymentMethodData::BankTransfer(_)) - | Some(api_models::payments::PaymentMethodData::Crypto(_)) - | Some(api_models::payments::PaymentMethodData::MandatePayment) - | Some(api_models::payments::PaymentMethodData::Reward) - | Some(api_models::payments::PaymentMethodData::Upi(_)) - | Some(api_models::payments::PaymentMethodData::Voucher(_)) - | Some(api_models::payments::PaymentMethodData::GiftCard(_)) - | None => Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("complete authorize flow"), - ) - .into()), + api_models::enums::PaymentMethod::CardRedirect + | api_models::enums::PaymentMethod::PayLater + | api_models::enums::PaymentMethod::Wallet + | api_models::enums::PaymentMethod::BankRedirect + | api_models::enums::PaymentMethod::BankTransfer + | api_models::enums::PaymentMethod::Crypto + | api_models::enums::PaymentMethod::BankDebit + | api_models::enums::PaymentMethod::Reward + | api_models::enums::PaymentMethod::Upi + | api_models::enums::PaymentMethod::Voucher + | api_models::enums::PaymentMethod::GiftCard => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message( + "complete authorize flow", + ), + ) + .into()) + } } } } @@ -249,7 +266,6 @@ impl *client_token_data, item.data.get_payment_method_token()?, item.data.request.payment_method_data.clone(), - item.data.request.amount, )?), mandate_reference: None, connector_metadata: None, @@ -428,7 +444,6 @@ impl *client_token_data, item.data.get_payment_method_token()?, item.data.request.payment_method_data.clone(), - item.data.request.amount, )?), mandate_reference: None, connector_metadata: None, @@ -586,11 +601,14 @@ impl TryFrom>> for Braintree item: BraintreeRouterData<&types::RefundsRouterData>, ) -> Result { let metadata: BraintreeMeta = - utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone())?; + utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConfig { + field_name: "merchant connector account metadata", + })?; utils::validate_currency( item.router_data.request.currency, - metadata.merchant_config_currency, + Some(metadata.merchant_config_currency), )?; let query = REFUND_TRANSACTION_MUTATION.to_string(); let variables = BraintreeRefundVariables { @@ -598,11 +616,7 @@ impl TryFrom>> for Braintree transaction_id: item.router_data.request.connector_transaction_id.clone(), refund: RefundInputData { amount: item.amount, - merchant_account_id: metadata.merchant_account_id.ok_or( - errors::ConnectorError::MissingRequiredField { - field_name: "merchant_account_id", - }, - )?, + merchant_account_id: metadata.merchant_account_id, }, }, }; @@ -695,9 +709,16 @@ pub struct BraintreeRSyncRequest { impl TryFrom<&types::RefundSyncRouterData> for BraintreeRSyncRequest { type Error = error_stack::Report; fn try_from(item: &types::RefundSyncRouterData) -> Result { - let metadata: BraintreeMeta = - utils::to_connector_meta_from_secret(item.connector_meta_data.clone())?; - utils::validate_currency(item.request.currency, metadata.merchant_config_currency)?; + let metadata: BraintreeMeta = utils::to_connector_meta_from_secret( + item.connector_meta_data.clone(), + ) + .change_context(errors::ConnectorError::InvalidConfig { + field_name: "merchant connector account metadata", + })?; + utils::validate_currency( + item.request.currency, + Some(metadata.merchant_config_currency), + )?; let refund_id = item.request.get_connector_refund_id()?; let query = format!("query {{ search {{ refunds(input: {{ id: {{is: \"{}\"}} }}, first: 1) {{ edges {{ node {{ id status createdAt amount {{ value currencyCode }} orderId }} }} }} }} }}",refund_id); @@ -1250,6 +1271,13 @@ pub struct BraintreeThreeDsResponse { pub liability_shift_possible: bool, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BraintreeThreeDsErrorResponse { + pub code: String, + pub message: String, +} + #[derive(Debug, Deserialize)] pub struct BraintreeRedirectionResponse { pub authentication_response: String, @@ -1263,11 +1291,7 @@ impl TryFrom for BraintreeClientTokenRequest { variables: VariableClientTokenInput { input: InputClientTokenData { client_token: ClientTokenInput { - merchant_account_id: metadata.merchant_account_id.ok_or( - errors::ConnectorError::MissingRequiredField { - field_name: "merchant_account_id", - }, - )?, + merchant_account_id: metadata.merchant_account_id, }, }, }, @@ -1304,11 +1328,7 @@ impl }, transaction: TransactionBody { amount: item.amount.to_owned(), - merchant_account_id: metadata.merchant_account_id.ok_or( - errors::ConnectorError::MissingRequiredField { - field_name: "merchant_account_id", - }, - )?, + merchant_account_id: metadata.merchant_account_id, }, }, }, @@ -1324,10 +1344,13 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>> item: &BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>, ) -> Result { let metadata: BraintreeMeta = - utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone())?; + utils::to_connector_meta_from_secret(item.router_data.connector_meta_data.clone()) + .change_context(errors::ConnectorError::InvalidConfig { + field_name: "merchant connector account metadata", + })?; utils::validate_currency( item.router_data.request.currency, - metadata.merchant_config_currency, + Some(metadata.merchant_config_currency), )?; let payload_data = utils::PaymentsCompleteAuthorizeRequestData::get_redirect_response_payload( @@ -1360,11 +1383,7 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsCompleteAuthorizeRouterData>> payment_method_id: three_ds_data.nonce, transaction: TransactionBody { amount: item.amount.to_owned(), - merchant_account_id: metadata.merchant_account_id.ok_or( - errors::ConnectorError::MissingRequiredField { - field_name: "merchant_account_id", - }, - )?, + merchant_account_id: metadata.merchant_account_id, }, }, }, @@ -1376,7 +1395,6 @@ fn get_braintree_redirect_form( client_token_data: ClientTokenResponse, payment_method_token: types::PaymentMethodToken, card_details: api_models::payments::PaymentMethodData, - amount: i64, ) -> Result> { Ok(services::RedirectForm::Braintree { client_token: client_token_data @@ -1409,7 +1427,6 @@ fn get_braintree_redirect_form( errors::ConnectorError::NotImplemented("given payment method".to_owned()), )?, }, - amount, }) } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 226d77d598e6..7da0b73f74db 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -3,6 +3,7 @@ use common_utils::{ crypto::{generate_cryptographically_secure_random_string, OptionalSecretValue}, date_time, ext_traits::{AsyncExt, ConfigExt, Encode, ValueExt}, + pii, }; use data_models::MerchantStorageScheme; use error_stack::{report, FutureExt, ResultExt}; @@ -656,15 +657,26 @@ pub async fn create_payment_connector( expected_format: "auth_type and api_key".to_string(), })?; - validate_auth_type(req.connector_name, &auth).map_err(|err| { - if err.current_context() == &errors::ConnectorError::InvalidConnectorName { - err.change_context(errors::ApiErrorResponse::InvalidRequestData { - message: "The connector name is invalid".to_string(), - }) - } else { - err.change_context(errors::ApiErrorResponse::InvalidRequestData { - message: "The auth type is invalid for the connector".to_string(), - }) + validate_auth_and_metadata_type(req.connector_name, &auth, &req.metadata).map_err(|err| { + match *err.current_context() { + errors::ConnectorError::InvalidConnectorName => { + err.change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "The connector name is invalid".to_string(), + }) + } + errors::ConnectorError::InvalidConfig { field_name } => { + err.change_context(errors::ApiErrorResponse::InvalidRequestData { + message: format!("The {} is invalid", field_name), + }) + } + errors::ConnectorError::FailedToObtainAuthType => { + err.change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "The auth type is invalid for the connector".to_string(), + }) + } + _ => err.change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "The request body is invalid".to_string(), + }), } })?; @@ -1250,9 +1262,10 @@ pub async fn update_business_profile( )) } -pub(crate) fn validate_auth_type( +pub(crate) fn validate_auth_and_metadata_type( connector_name: api_models::enums::Connector, val: &types::ConnectorAuthType, + connector_meta_data: &Option, ) -> Result<(), error_stack::Report> { use crate::connector::*; @@ -1302,6 +1315,9 @@ pub(crate) fn validate_auth_type( } api_enums::Connector::Braintree => { braintree::transformers::BraintreeAuthType::try_from(val)?; + braintree::braintree_graphql_transformers::BraintreeMeta::try_from( + connector_meta_data, + )?; Ok(()) } api_enums::Connector::Cashtocode => { diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 6b205434a8dc..9d75904ef660 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -178,6 +178,8 @@ pub enum ConnectorError { message: String, connector: &'static str, }, + #[error("Invalid Configuration")] + InvalidConfig { field_name: &'static str }, } #[derive(Debug, thiserror::Error)] diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index b56e55b6c7b8..8506e26f584a 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -703,7 +703,6 @@ pub enum RedirectForm { client_token: String, card_token: String, bin: String, - amount: i64, }, } @@ -1262,7 +1261,6 @@ pub fn build_redirection_form( client_token, card_token, bin, - amount, } => { maud::html! { (maud::DOCTYPE) @@ -1270,7 +1268,7 @@ pub fn build_redirection_form( head { meta name="viewport" content="width=device-width, initial-scale=1"; (PreEscaped(r#""#)) - (PreEscaped(r#""#)) + // (PreEscaped(r#""#)) } body style="background-color: #ffffff; padding: 20px; font-family: Arial, Helvetica, Sans-Serif;" { @@ -1318,15 +1316,26 @@ pub fn build_redirection_form( }} }}, onLookupComplete: function(data, next) {{ - console.log(\"onLookup Complete\", data); + // console.log(\"onLookup Complete\", data); next(); }} }}, function(err, payload) {{ if(err) {{ console.error(err); + var f = document.createElement('form'); + f.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/response/braintree\"); + var i = document.createElement('input'); + i.type = 'hidden'; + f.method='POST'; + i.name = 'authentication_response'; + i.value = JSON.stringify(err); + f.appendChild(i); + f.body = JSON.stringify(err); + document.body.appendChild(f); + f.submit(); }} else {{ - console.log(payload); + // console.log(payload); var f = document.createElement('form'); f.action=window.location.pathname.replace(/payments\\/redirect\\/(\\w+)\\/(\\w+)\\/\\w+/, \"payments/$1/$2/redirect/complete/braintree\"); var i = document.createElement('input'); From c26620e041add914abc60c6149787be62ea5985d Mon Sep 17 00:00:00 2001 From: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> Date: Thu, 12 Oct 2023 19:58:34 +0530 Subject: [PATCH 20/21] feat(customers): add customer list endpoint (#2564) Co-authored-by: Bernard Eugine <114725419+bernard-eugine@users.noreply.github.com> --- crates/diesel_models/src/query/customers.rs | 15 +++++ .../router/src/compatibility/stripe/errors.rs | 5 ++ crates/router/src/core/customers.rs | 65 +++++++++++++------ .../core/errors/customers_error_response.rs | 3 + crates/router/src/core/errors/transformers.rs | 6 ++ crates/router/src/db/customers.rs | 56 ++++++++++++++++ crates/router/src/openapi.rs | 1 + crates/router/src/routes/app.rs | 11 ++-- crates/router/src/routes/customers.rs | 31 +++++++++ crates/router/src/routes/lock_utils.rs | 3 +- crates/router_env/src/logger/types.rs | 2 + openapi/openapi_spec.json | 33 ++++++++++ 12 files changed, 207 insertions(+), 24 deletions(-) diff --git a/crates/diesel_models/src/query/customers.rs b/crates/diesel_models/src/query/customers.rs index cd1691b39d3e..41dedbf8648f 100644 --- a/crates/diesel_models/src/query/customers.rs +++ b/crates/diesel_models/src/query/customers.rs @@ -73,6 +73,21 @@ impl Customer { .await } + #[instrument(skip(conn))] + pub async fn list_by_merchant_id( + conn: &PgPooledConn, + merchant_id: &str, + ) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::merchant_id.eq(merchant_id.to_owned()), + None, + None, + Some(dsl::created_at), + ) + .await + } + #[instrument(skip(conn))] pub async fn find_optional_by_customer_id_merchant_id( conn: &PgPooledConn, diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 5d4d3082aff5..2d10e37c10ba 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -69,6 +69,9 @@ pub enum StripeErrorCode { #[error(error_type = StripeErrorType::InvalidRequestError, code = "customer_redacted", message = "Customer has redacted")] CustomerRedacted, + #[error(error_type = StripeErrorType::InvalidRequestError, code = "customer_already_exists", message = "Customer with the given customer_id already exists")] + DuplicateCustomer, + #[error(error_type = StripeErrorType::InvalidRequestError, code = "resource_missing", message = "No such refund")] RefundNotFound, @@ -652,6 +655,7 @@ impl actix_web::ResponseError for StripeErrorCode { | Self::FileNotAvailable | Self::FileProviderNotSupported | Self::CurrencyNotSupported { .. } + | Self::DuplicateCustomer | Self::PaymentMethodUnactivated => StatusCode::BAD_REQUEST, Self::RefundFailed | Self::PayoutFailed @@ -730,6 +734,7 @@ impl ErrorSwitch for CustomersErrorResponse { Self::InternalServerError => SC::InternalServerError, Self::MandateActive => SC::MandateActive, Self::CustomerNotFound => SC::CustomerNotFound, + Self::CustomerAlreadyExists => SC::DuplicateCustomer, } } } diff --git a/crates/router/src/core/customers.rs b/crates/router/src/core/customers.rs index f7262346e2ca..f4ef3d8ed80f 100644 --- a/crates/router/src/core/customers.rs +++ b/crates/router/src/core/customers.rs @@ -2,13 +2,13 @@ use common_utils::{ crypto::{Encryptable, GcmAes256}, errors::ReportSwitchExt, }; -use error_stack::ResultExt; +use error_stack::{IntoReport, ResultExt}; use masking::ExposeInterface; use router_env::{instrument, tracing}; use crate::{ core::{ - errors::{self}, + errors::{self, StorageErrorExt}, payment_methods::cards, }, pii::PeekInterface, @@ -39,6 +39,25 @@ pub async fn create_customer( let merchant_id = &merchant_account.merchant_id; customer_data.merchant_id = merchant_id.to_owned(); + // We first need to validate whether the customer with the given customer id already exists + // this may seem like a redundant db call, as the insert_customer will anyway return this error + // + // Consider a scenerio where the address is inserted and then when inserting the customer, + // it errors out, now the address that was inserted is not deleted + match db + .find_customer_by_customer_id_merchant_id(customer_id, merchant_id, &key_store) + .await + { + Err(err) => { + if !err.current_context().is_db_not_found() { + Err(err).switch() + } else { + Ok(()) + } + } + Ok(_) => Err(errors::CustomersErrorResponse::CustomerAlreadyExists).into_report(), + }?; + let key = key_store.key.get_inner().peek(); let address = if let Some(addr) = &customer_data.address { let customer_address: api_models::payments::AddressDetails = addr.clone(); @@ -89,23 +108,10 @@ pub async fn create_customer( .switch() .attach_printable("Failed while encrypting Customer")?; - let customer = match db.insert_customer(new_customer, &key_store).await { - Ok(customer) => customer, - Err(error) => { - if error.current_context().is_db_unique_violation() { - db.find_customer_by_customer_id_merchant_id(customer_id, merchant_id, &key_store) - .await - .switch() - .attach_printable(format!( - "Failed while fetching Customer, customer_id: {customer_id}", - ))? - } else { - Err(error - .change_context(errors::CustomersErrorResponse::InternalServerError) - .attach_printable("Failed while inserting new customer"))? - } - } - }; + let customer = db + .insert_customer(new_customer, &key_store) + .await + .to_duplicate_response(errors::CustomersErrorResponse::CustomerAlreadyExists)?; let address_details = address.map(api_models::payments::AddressDetails::from); @@ -143,6 +149,27 @@ pub async fn retrieve_customer( )) } +#[instrument(skip(state))] +pub async fn list_customers( + state: AppState, + merchant_id: String, + key_store: domain::MerchantKeyStore, +) -> errors::CustomerResponse> { + let db = state.store.as_ref(); + + let domain_customers = db + .list_customers_by_merchant_id(&merchant_id, &key_store) + .await + .switch()?; + + let customers = domain_customers + .into_iter() + .map(|domain_customer| customers::CustomerResponse::from((domain_customer, None))) + .collect(); + + Ok(services::ApplicationResponse::Json(customers)) +} + #[instrument(skip_all)] pub async fn delete_customer( state: AppState, diff --git a/crates/router/src/core/errors/customers_error_response.rs b/crates/router/src/core/errors/customers_error_response.rs index b74538822e66..e5b6d4a52604 100644 --- a/crates/router/src/core/errors/customers_error_response.rs +++ b/crates/router/src/core/errors/customers_error_response.rs @@ -13,6 +13,9 @@ pub enum CustomersErrorResponse { #[error("Customer does not exist in our records")] CustomerNotFound, + + #[error("Customer with the given customer id already exists")] + CustomerAlreadyExists, } impl actix_web::ResponseError for CustomersErrorResponse { diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index 38d42596cdf0..19640a931e1a 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -314,6 +314,12 @@ impl ErrorSwitch for CustomersError "Customer does not exist in our records", None, )), + Self::CustomerAlreadyExists => AER::BadRequest(ApiError::new( + "IR", + 12, + "Customer with the given `customer_id` already exists", + None, + )), } } } diff --git a/crates/router/src/db/customers.rs b/crates/router/src/db/customers.rs index 1c62bb839f61..68b449412084 100644 --- a/crates/router/src/db/customers.rs +++ b/crates/router/src/db/customers.rs @@ -1,5 +1,6 @@ use common_utils::ext_traits::AsyncExt; use error_stack::{IntoReport, ResultExt}; +use futures::future::try_join_all; use masking::PeekInterface; use router_env::{instrument, tracing}; @@ -52,6 +53,12 @@ where key_store: &domain::MerchantKeyStore, ) -> CustomResult; + async fn list_customers_by_merchant_id( + &self, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError>; + async fn insert_customer( &self, customer_data: domain::Customer, @@ -148,6 +155,31 @@ impl CustomerInterface for Store { } } + async fn list_customers_by_merchant_id( + &self, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + + let encrypted_customers = storage::Customer::list_by_merchant_id(&conn, merchant_id) + .await + .map_err(Into::into) + .into_report()?; + + let customers = try_join_all(encrypted_customers.into_iter().map( + |encrypted_customer| async { + encrypted_customer + .convert(key_store.key.get_inner()) + .await + .change_context(errors::StorageError::DecryptionError) + }, + )) + .await?; + + Ok(customers) + } + async fn insert_customer( &self, customer_data: domain::Customer, @@ -209,6 +241,30 @@ impl CustomerInterface for MockDb { .transpose() } + async fn list_customers_by_merchant_id( + &self, + merchant_id: &str, + key_store: &domain::MerchantKeyStore, + ) -> CustomResult, errors::StorageError> { + let customers = self.customers.lock().await; + + let customers = try_join_all( + customers + .iter() + .filter(|customer| customer.merchant_id == merchant_id) + .map(|customer| async { + customer + .to_owned() + .convert(key_store.key.get_inner()) + .await + .change_context(errors::StorageError::DecryptionError) + }), + ) + .await?; + + Ok(customers) + } + #[instrument(skip_all)] async fn update_customer_by_customer_id_merchant_id( &self, diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 5a36b03e7aea..0b36c5b3a394 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -101,6 +101,7 @@ Never share your secret api keys. Keep them guarded and secure. crate::routes::customers::customers_retrieve, crate::routes::customers::customers_update, crate::routes::customers::customers_delete, + crate::routes::customers::customers_list, // crate::routes::api_keys::api_key_create, // crate::routes::api_keys::api_key_retrieve, // crate::routes::api_keys::api_key_update, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 24d7217aefe8..7f18220438c4 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -275,10 +275,12 @@ impl Customers { #[cfg(feature = "olap")] { - route = route.service( - web::resource("/{customer_id}/mandates") - .route(web::get().to(get_customer_mandates)), - ); + route = route + .service( + web::resource("/{customer_id}/mandates") + .route(web::get().to(get_customer_mandates)), + ) + .service(web::resource("/list").route(web::get().to(customers_list))) } #[cfg(feature = "oltp")] @@ -300,6 +302,7 @@ impl Customers { .route(web::delete().to(customers_delete)), ); } + route } } diff --git a/crates/router/src/routes/customers.rs b/crates/router/src/routes/customers.rs index de5fdb4d6347..ff2ffc2a3fe3 100644 --- a/crates/router/src/routes/customers.rs +++ b/crates/router/src/routes/customers.rs @@ -85,6 +85,37 @@ pub async fn customers_retrieve( ) .await } + +/// List customers for a merchant +/// +/// To filter and list the customers for a particular merchant id +#[utoipa::path( + post, + path = "/customers/list", + responses( + (status = 200, description = "Customers retrieved", body = Vec), + (status = 400, description = "Invalid Data"), + ), + tag = "Customers List", + operation_id = "List all Customers for a Merchant", + security(("api_key" = [])) +)] +#[instrument(skip_all, fields(flow = ?Flow::CustomersList))] +pub async fn customers_list(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::CustomersList; + + api::server_wrap( + flow, + state, + &req, + (), + |state, auth, _| list_customers(state, auth.merchant_account.merchant_id, auth.key_store), + &auth::ApiKeyAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} + /// Update Customer /// /// Updates the customer's details in a customer object. diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index cec510cd0c77..6a69db6257cc 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -47,7 +47,8 @@ impl From for ApiIdentifier { | Flow::CustomersRetrieve | Flow::CustomersUpdate | Flow::CustomersDelete - | Flow::CustomersGetMandates => Self::Customers, + | Flow::CustomersGetMandates + | Flow::CustomersList => Self::Customers, Flow::EphemeralKeyCreate | Flow::EphemeralKeyDelete => Self::Ephemeral, diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index e838c5d9d53f..1c56c4c7c2f0 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -104,6 +104,8 @@ pub enum Flow { PaymentMethodsList, /// Customer payment methods list flow. CustomerPaymentMethodsList, + /// List Customers for a merchant + CustomersList, /// Payment methods retrieve flow. PaymentMethodsRetrieve, /// Payment methods update flow. diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index d9fd4055c94a..526c8204d67b 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -169,6 +169,39 @@ ] } }, + "/customers/list": { + "post": { + "tags": [ + "Customers List" + ], + "summary": "List customers for a merchant", + "description": "List customers for a merchant\n\nTo filter and list the customers for a particular merchant id", + "operationId": "List all Customers for a Merchant", + "responses": { + "200": { + "description": "Customers retrieved", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerResponse" + } + } + } + } + }, + "400": { + "description": "Invalid Data" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/customers/payment_methods": { "get": { "tags": [ From fc97deda7f846870ed6c0a69592481e637067dba Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 12 Oct 2023 14:34:41 +0000 Subject: [PATCH 21/21] chore(version): v1.57.0 --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb0f6f983aa..633566dd52da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,47 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.57.0 (2023-10-12) + +### Features + +- **connector:** + - [Tsys] Use `connector_response_reference_id` as reference to the connector ([#2546](https://github.com/juspay/hyperswitch/pull/2546)) ([`550377a`](https://github.com/juspay/hyperswitch/commit/550377a6c3943d9fec4ca6a8be5a5f3aafe109ab)) + - [Cybersource] Use connector_request_reference_id as reference to the connector ([#2512](https://github.com/juspay/hyperswitch/pull/2512)) ([`81cb8da`](https://github.com/juspay/hyperswitch/commit/81cb8da4d47fe2a75330d39c665bb259faa35b00)) + - [Iatapay] use connector_response_reference_id as reference to connector ([#2524](https://github.com/juspay/hyperswitch/pull/2524)) ([`ef647b7`](https://github.com/juspay/hyperswitch/commit/ef647b7ab942707a06971b6545c81168f28cb94c)) + - [ACI] Use connector_request_reference_id as reference to the connector ([#2549](https://github.com/juspay/hyperswitch/pull/2549)) ([`c2ad200`](https://github.com/juspay/hyperswitch/commit/c2ad2002c0e6d673f62ec4c72c8fd98b07a05c0b)) +- **customers:** Add customer list endpoint ([#2564](https://github.com/juspay/hyperswitch/pull/2564)) ([`c26620e`](https://github.com/juspay/hyperswitch/commit/c26620e041add914abc60c6149787be62ea5985d)) +- **router:** + - Add kv implementation for update address in update payments flow ([#2542](https://github.com/juspay/hyperswitch/pull/2542)) ([`9f446bc`](https://github.com/juspay/hyperswitch/commit/9f446bc1742c06a7fab3d92128ba4e7d3be80ea6)) + - Add payment link support ([#2105](https://github.com/juspay/hyperswitch/pull/2105)) ([`642085d`](https://github.com/juspay/hyperswitch/commit/642085dc745f87b4edd2f7a744c31b8979b23cfa)) + +### Bug Fixes + +- **connector:** + - [noon] sync with reference_id ([#2544](https://github.com/juspay/hyperswitch/pull/2544)) ([`9ef60e4`](https://github.com/juspay/hyperswitch/commit/9ef60e425d0cbe764ce66c65c8c09b1992cbe99f)) + - [braintree] add 3ds redirection error mapping and metadata validation ([#2552](https://github.com/juspay/hyperswitch/pull/2552)) ([`28d02f9`](https://github.com/juspay/hyperswitch/commit/28d02f94c6d52d05b6f520e4d48ba88adf7be619)) +- **router:** Add customer_id validation for `payment method create` flow ([#2543](https://github.com/juspay/hyperswitch/pull/2543)) ([`53d7604`](https://github.com/juspay/hyperswitch/commit/53d760460305e16f03d86f699acb035151dfdfad)) +- Percentage float inconsistency problem and api models changes to support surcharge feature ([#2550](https://github.com/juspay/hyperswitch/pull/2550)) ([`1ee1184`](https://github.com/juspay/hyperswitch/commit/1ee11849d4a60afbf3d05103cb491a11e905b811)) +- Consume profile_id throughout payouts flow ([#2501](https://github.com/juspay/hyperswitch/pull/2501)) ([`7eabd24`](https://github.com/juspay/hyperswitch/commit/7eabd24a4da6f82fd30f8a4be739962538654214)) +- Parse allowed_payment_method_types only if there is some value p… ([#2161](https://github.com/juspay/hyperswitch/pull/2161)) ([`46f1419`](https://github.com/juspay/hyperswitch/commit/46f14191ab7e036539ef3fd58acd9376b6b6b63c)) + +### Refactors + +- **connector:** + - [Worldpay] Currency Unit Conversion ([#2436](https://github.com/juspay/hyperswitch/pull/2436)) ([`b78109b`](https://github.com/juspay/hyperswitch/commit/b78109bc93433e0886b0b8656231899df84da8cf)) + - [noon] use connector_request_reference_id for sync ([#2558](https://github.com/juspay/hyperswitch/pull/2558)) ([`0889a6e`](https://github.com/juspay/hyperswitch/commit/0889a6ed0691abeed7bba44e7024545abcc74aef)) + - [noon] update and add recommended fields ([#2381](https://github.com/juspay/hyperswitch/pull/2381)) ([`751f16e`](https://github.com/juspay/hyperswitch/commit/751f16eaee254ab8f0068e2e9e81e3e4b7fe133f)) +- **worldline:** Use `connector_request_reference_id` as reference to the connector ([#2498](https://github.com/juspay/hyperswitch/pull/2498)) ([`efa5320`](https://github.com/juspay/hyperswitch/commit/efa53204e8ab1ef1192bcdc07ed99306475badbc)) + +### Revert + +- Fix(connector): [noon] sync with reference_id ([#2556](https://github.com/juspay/hyperswitch/pull/2556)) ([`13be4d3`](https://github.com/juspay/hyperswitch/commit/13be4d36eac3d1e17d8ad9b3f3ef8993547f548b)) + +**Full Changelog:** [`v1.56.0...v1.57.0`](https://github.com/juspay/hyperswitch/compare/v1.56.0...v1.57.0) + +- - - + + ## 1.56.0 (2023-10-11) ### Features