From 0e9966a54d87f55b0f5c54e4dccb80742674fe26 Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Mon, 3 Feb 2025 19:22:14 +0530 Subject: [PATCH 01/46] chore: bump cypress to `v14.0.0` (#7102) --- cypress-tests/cypress.config.js | 45 +- .../cypress/e2e/configs/Payment/Nmi.js | 17 +- .../cypress/e2e/configs/Payment/Stripe.js | 12 +- .../cypress/e2e/configs/Payment/Utils.js | 34 +- cypress-tests/cypress/support/commands.js | 9 +- cypress-tests/cypress/support/e2e.js | 12 +- .../cypress/support/redirectionHandler.js | 882 ++++++++++-------- cypress-tests/cypress/utils/featureFlags.js | 46 +- cypress-tests/package-lock.json | 28 +- cypress-tests/package.json | 6 +- 10 files changed, 612 insertions(+), 479 deletions(-) diff --git a/cypress-tests/cypress.config.js b/cypress-tests/cypress.config.js index 913663c509e..dce9048339e 100644 --- a/cypress-tests/cypress.config.js +++ b/cypress-tests/cypress.config.js @@ -30,14 +30,27 @@ export default defineConfig({ }, }); on("after:spec", (spec, results) => { - if (results && results.video) { - // Do we have failures for any retry attempts? - const failures = results.tests.some((test) => + // Clean up resources after each spec + if ( + results && + results.video && + !results.tests.some((test) => test.attempts.some((attempt) => attempt.state === "failed") - ); - if (!failures) { - // delete the video if the spec passed and no tests retried - fs.unlinkSync(results.video); + ) + ) { + // Only try to delete if the video file exists + try { + if (fs.existsSync(results.video)) { + fs.unlinkSync(results.video); + } + } catch (error) { + // Log the error but don't fail the test + // eslint-disable-next-line no-console + console.warn( + `Warning: Could not delete video file: ${results.video}` + ); + // eslint-disable-next-line no-console + console.warn(error); } } }); @@ -45,6 +58,9 @@ export default defineConfig({ }, experimentalRunAllSpecs: true, + specPattern: "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}", + supportFile: "cypress/support/e2e.js", + reporter: "cypress-mochawesome-reporter", reporterOptions: { reportDir: `cypress/reports/${connectorId}`, @@ -55,12 +71,13 @@ export default defineConfig({ inlineAssets: true, saveJson: true, }, + defaultCommandTimeout: 10000, + pageLoadTimeout: 20000, + responseTimeout: 30000, + screenshotsFolder: screenshotsFolderName, + video: true, + videoCompression: 32, + videosFolder: `cypress/videos/${connectorId}`, + chromeWebSecurity: false, }, - chromeWebSecurity: false, - defaultCommandTimeout: 10000, - pageLoadTimeout: 20000, - responseTimeout: 30000, - screenshotsFolder: screenshotsFolderName, - video: true, - videoCompression: 32, }); diff --git a/cypress-tests/cypress/e2e/configs/Payment/Nmi.js b/cypress-tests/cypress/e2e/configs/Payment/Nmi.js index 8ad218953dd..e8cdd97e7a7 100644 --- a/cypress-tests/cypress/e2e/configs/Payment/Nmi.js +++ b/cypress-tests/cypress/e2e/configs/Payment/Nmi.js @@ -163,7 +163,9 @@ export const connectorDetails = { }, }, Void: { - Request: {}, + Request: { + cancellation_reason: "user_cancel", + }, Response: { status: 200, body: { @@ -172,16 +174,13 @@ export const connectorDetails = { }, }, VoidAfterConfirm: { - Request: {}, + Request: { + cancellation_reason: "user_cancel", + }, Response: { - status: 400, + status: 200, body: { - error: { - code: "IR_16", - message: - "You cannot cancel this payment because it has status processing", - type: "invalid_request", - }, + status: "processing", }, }, }, diff --git a/cypress-tests/cypress/e2e/configs/Payment/Stripe.js b/cypress-tests/cypress/e2e/configs/Payment/Stripe.js index d4d3c30fe27..b73bbf18dcc 100644 --- a/cypress-tests/cypress/e2e/configs/Payment/Stripe.js +++ b/cypress-tests/cypress/e2e/configs/Payment/Stripe.js @@ -23,7 +23,7 @@ const successfulThreeDSTestCardDetails = { const failedNo3DSCardDetails = { card_number: "4000000000000002", card_exp_month: "01", - card_exp_year: "25", + card_exp_year: "35", card_holder_name: "joseph Doe", card_cvc: "123", }; @@ -123,7 +123,7 @@ const requiredFields = { export const connectorDetails = { multi_credential_config: { - specName: ["connectorAgnostic"], + specName: ["connectorAgnosticNTID"], value: "connector_2", }, card_pm: { @@ -144,7 +144,7 @@ export const connectorDetails = { PaymentIntentOffSession: { Configs: { CONNECTOR_CREDENTIAL: { - specName: ["connectorAgnostic"], + specName: ["connectorAgnosticNTID"], value: "connector_2", }, }, @@ -564,7 +564,7 @@ export const connectorDetails = { MITAutoCapture: getCustomExchange({ Configs: { CONNECTOR_CREDENTIAL: { - specName: ["connectorAgnostic"], + specName: ["connectorAgnosticNTID"], value: "connector_2", }, }, @@ -701,7 +701,7 @@ export const connectorDetails = { SaveCardUseNo3DSAutoCaptureOffSession: { Configs: { CONNECTOR_CREDENTIAL: { - specName: ["connectorAgnostic"], + specName: ["connectorAgnosticNTID"], value: "connector_2", }, }, @@ -754,7 +754,7 @@ export const connectorDetails = { SaveCardConfirmAutoCaptureOffSession: { Configs: { CONNECTOR_CREDENTIAL: { - specName: ["connectorAgnostic"], + specName: ["connectorAgnosticNTID"], value: "connector_2", }, }, diff --git a/cypress-tests/cypress/e2e/configs/Payment/Utils.js b/cypress-tests/cypress/e2e/configs/Payment/Utils.js index c35e5950b73..5a9baa32d73 100644 --- a/cypress-tests/cypress/e2e/configs/Payment/Utils.js +++ b/cypress-tests/cypress/e2e/configs/Payment/Utils.js @@ -4,10 +4,8 @@ import { connectorDetails as adyenConnectorDetails } from "./Adyen.js"; import { connectorDetails as bankOfAmericaConnectorDetails } from "./BankOfAmerica.js"; import { connectorDetails as bluesnapConnectorDetails } from "./Bluesnap.js"; import { connectorDetails as checkoutConnectorDetails } from "./Checkout.js"; -import { - connectorDetails as CommonConnectorDetails, - updateDefaultStatusCode, -} from "./Commons.js"; +import { connectorDetails as CommonConnectorDetails } from "./Commons.js"; +import { updateDefaultStatusCode } from "./Modifiers.js"; import { connectorDetails as cybersourceConnectorDetails } from "./Cybersource.js"; import { connectorDetails as datatransConnectorDetails } from "./Datatrans.js"; import { connectorDetails as elavonConnectorDetails } from "./Elavon.js"; @@ -227,12 +225,10 @@ function getConnectorConfig( const mcaConfig = getConnectorDetails(globalState.get("connectorId")); return { - config: { - CONNECTOR_CREDENTIAL: - multipleConnector?.nextConnector && multipleConnectors?.status - ? multipleConnector - : mcaConfig?.multi_credential_config || multipleConnector, - }, + CONNECTOR_CREDENTIAL: + multipleConnector?.nextConnector && multipleConnectors?.status + ? multipleConnector + : mcaConfig?.multi_credential_config || multipleConnector, multipleConnectors, }; } @@ -243,13 +239,12 @@ export function createBusinessProfile( globalState, multipleConnector = { nextConnector: false } ) { - const { config, multipleConnectors } = getConnectorConfig( - globalState, - multipleConnector - ); + const config = getConnectorConfig(globalState, multipleConnector); const { profilePrefix } = execConfig(config); - if (shouldProceedWithOperation(multipleConnector, multipleConnectors)) { + if ( + shouldProceedWithOperation(multipleConnector, config.multipleConnectors) + ) { cy.createBusinessProfileTest( createBusinessProfileBody, globalState, @@ -266,13 +261,12 @@ export function createMerchantConnectorAccount( paymentMethodsEnabled, multipleConnector = { nextConnector: false } ) { - const { config, multipleConnectors } = getConnectorConfig( - globalState, - multipleConnector - ); + const config = getConnectorConfig(globalState, multipleConnector); const { profilePrefix, merchantConnectorPrefix } = execConfig(config); - if (shouldProceedWithOperation(multipleConnector, multipleConnectors)) { + if ( + shouldProceedWithOperation(multipleConnector, config.multipleConnectors) + ) { cy.createConnectorCallTest( paymentType, createMerchantConnectorAccountBody, diff --git a/cypress-tests/cypress/support/commands.js b/cypress-tests/cypress/support/commands.js index 60691899f7c..45be7043116 100644 --- a/cypress-tests/cypress/support/commands.js +++ b/cypress-tests/cypress/support/commands.js @@ -620,7 +620,6 @@ Cypress.Commands.add( ); if (stateUpdate) { - // cy.task("setGlobalState", stateUpdate); globalState.set( "MULTIPLE_CONNECTORS", stateUpdate.MULTIPLE_CONNECTORS @@ -2202,13 +2201,19 @@ Cypress.Commands.add( ); Cypress.Commands.add("voidCallTest", (requestBody, data, globalState) => { - const { Configs: configs = {}, Response: resData } = data || {}; + const { + Configs: configs = {}, + Response: resData, + Request: reqData, + } = data || {}; const configInfo = execConfig(validateConfig(configs)); const payment_id = globalState.get("paymentID"); const profile_id = globalState.get(`${configInfo.profilePrefix}Id`); requestBody.profile_id = profile_id; + requestBody.cancellation_reason = + reqData?.cancellation_reason ?? requestBody.cancellation_reason; cy.request({ method: "POST", diff --git a/cypress-tests/cypress/support/e2e.js b/cypress-tests/cypress/support/e2e.js index eab7b99b629..1ec43cd03b6 100644 --- a/cypress-tests/cypress/support/e2e.js +++ b/cypress-tests/cypress/support/e2e.js @@ -16,12 +16,22 @@ // Import commands.js using ES2015 syntax: import "cypress-mochawesome-reporter/register"; import "./commands"; +import "./redirectionHandler"; + +Cypress.on("window:before:load", (win) => { + // Add security headers + win.headers = { + "Content-Security-Policy": "default-src 'self'", + "X-Content-Type-Options": "nosniff", + "X-Frame-Options": "DENY", + }; +}); // Add error handling for dynamic imports Cypress.on("uncaught:exception", (err, runnable) => { // Log the error details // eslint-disable-next-line no-console - console.log( + console.error( `Error: ${err.message}\nError occurred in: ${runnable.title}\nStack trace: ${err.stack}` ); diff --git a/cypress-tests/cypress/support/redirectionHandler.js b/cypress-tests/cypress/support/redirectionHandler.js index cc9c180b35e..d840aab9e48 100644 --- a/cypress-tests/cypress/support/redirectionHandler.js +++ b/cypress-tests/cypress/support/redirectionHandler.js @@ -3,23 +3,38 @@ import jsQR from "jsqr"; // Define constants for wait times -const TIMEOUT = 20000; // 20 seconds -const WAIT_TIME = 10000; // 10 seconds +const CONSTANTS = { + TIMEOUT: 20000, // 20 seconds + WAIT_TIME: 10000, // 10 seconds + ERROR_PATTERNS: [ + /4\d{2}/, + /5\d{2}/, + /error/i, + /invalid request/i, + /server error/i, + ], + VALID_TERMINAL_STATUSES: [ + "failed", + "processing", + "requires_capture", + "succeeded", + ], +}; export function handleRedirection( - redirection_type, + redirectionType, urls, connectorId, - payment_method_type, - handler_metadata + paymentMethodType, + handlerMetadata ) { - switch (redirection_type) { + switch (redirectionType) { case "bank_redirect": bankRedirectRedirection( urls.redirection_url, urls.expected_url, connectorId, - payment_method_type + paymentMethodType ); break; case "bank_transfer": @@ -27,8 +42,8 @@ export function handleRedirection( urls.redirection_url, urls.expected_url, connectorId, - payment_method_type, - handler_metadata.next_action_type + paymentMethodType, + handlerMetadata.next_action_type ); break; case "three_ds": @@ -39,309 +54,358 @@ export function handleRedirection( urls.redirection_url, urls.expected_url, connectorId, - payment_method_type + paymentMethodType ); break; default: - throw new Error(`Redirection known: ${redirection_type}`); + throw new Error(`Unknown redirection type: ${redirectionType}`); } } function bankTransferRedirection( - redirection_url, - expected_url, + redirectionUrl, + expectedUrl, connectorId, - payment_method_type, - next_action_type + paymentMethodType, + nextActionType ) { - switch (next_action_type) { + switch (nextActionType) { case "qr_code_url": - cy.request(redirection_url.href).then((response) => { + cy.request(redirectionUrl.href).then((response) => { switch (connectorId) { case "adyen": - switch (payment_method_type) { + switch (paymentMethodType) { case "pix": expect(response.status).to.eq(200); - fetchAndParseQRCode(redirection_url.href).then((qrCodeData) => { + fetchAndParseQRCode(redirectionUrl.href).then((qrCodeData) => { expect(qrCodeData).to.eq("TestQRCodeEMVToken"); }); break; default: - verifyReturnUrl(redirection_url, expected_url, true); + verifyReturnUrl(redirectionUrl, expectedUrl, true); // expected_redirection can be used here to handle other payment methods } break; default: - verifyReturnUrl(redirection_url, expected_url, true); + verifyReturnUrl(redirectionUrl, expectedUrl, true); } }); break; case "image_data_url": switch (connectorId) { case "itaubank": - switch (payment_method_type) { + switch (paymentMethodType) { case "pix": - fetchAndParseImageData(redirection_url).then((qrCodeData) => { + fetchAndParseImageData(redirectionUrl).then((qrCodeData) => { expect(qrCodeData).to.contains("itau.com.br/pix/qr/v2"); // image data contains the following value }); break; default: - verifyReturnUrl(redirection_url, expected_url, true); + verifyReturnUrl(redirectionUrl, expectedUrl, true); } break; default: - verifyReturnUrl(redirection_url, expected_url, true); + verifyReturnUrl(redirectionUrl, expectedUrl, true); } break; default: - verifyReturnUrl(redirection_url, expected_url, true); + verifyReturnUrl(redirectionUrl, expectedUrl, true); } } function bankRedirectRedirection( - redirection_url, - expected_url, + redirectionUrl, + expectedUrl, connectorId, - payment_method_type + paymentMethodType ) { let verifyUrl = false; - cy.visit(redirection_url.href); - - switch (connectorId) { - case "adyen": - switch (payment_method_type) { - case "eps": - cy.get("h1").should("contain.text", "Acquirer Simulator"); - cy.get('[value="authorised"]').click(); - cy.url().should("include", "status=succeeded"); - cy.wait(5000); - break; - case "ideal": - cy.get(":nth-child(4) > td > p").should( - "contain.text", - "Your Payment was Authorised/Refused/Cancelled (It may take up to five minutes to show on the Payment List)" - ); - cy.get(".btnLink").click(); - cy.url().should("include", "status=succeeded"); - cy.wait(5000); - break; - case "giropay": - cy.get( - ".rds-cookies-overlay__allow-all-cookies-btn > .rds-button" - ).click(); - cy.wait(5000); - cy.get(".normal-3").should( - "contain.text", - "Bank suchen ‑ mit giropay zahlen." - ); - cy.get("#bankSearch").type("giropay TestBank{enter}"); - cy.get(".normal-2 > div").click(); - cy.get('[data-testid="customerIban"]').type("DE48499999601234567890"); - cy.get('[data-testid="customerIdentification"]').type("9123456789"); - cy.get(":nth-child(3) > .rds-button").click(); - cy.get('[data-testid="onlineBankingPin"]').type("1234"); - cy.get(".rds-button--primary").click(); - cy.get(":nth-child(5) > .rds-radio-input-group__label").click(); - cy.get(".rds-button--primary").click(); - cy.get('[data-testid="photoTan"]').type("123456"); - cy.get(".rds-button--primary").click(); - cy.wait(5000); - cy.url().should("include", "status=succeeded"); - cy.wait(5000); - break; - case "sofort": - cy.get(".modal-overlay.modal-shown.in", { timeout: TIMEOUT }).then( - ($modal) => { - // If modal is found, handle it - if ($modal.length > 0) { - cy.get("button.cookie-modal-deny-all.button-tertiary") - .should("be.visible") - .should("contain", "Reject All") - .click({ multiple: true }); - cy.get("div#TopBanks.top-banks-multistep") - .should("contain", "Demo Bank") - .as("btn") - .click(); - cy.get("@btn").click(); - } else { - cy.get("input.phone").type("9123456789"); - cy.get("#button.onContinue") - .should("contain", "Continue") - .click(); - } - } - ); - break; - case "trustly": - break; - default: - throw new Error( - `Unsupported payment method type: ${payment_method_type}` - ); - } - verifyUrl = true; - break; - case "paypal": - switch (payment_method_type) { - case "eps": - cy.get('button[name="Successful"][value="SUCCEEDED"]').click(); - break; - case "ideal": - cy.get('button[name="Successful"][value="SUCCEEDED"]').click(); - break; - case "giropay": - cy.get('button[name="Successful"][value="SUCCEEDED"]').click(); - break; - default: - throw new Error( - `Unsupported payment method type: ${payment_method_type}` - ); - } - verifyUrl = true; - break; - case "stripe": - switch (payment_method_type) { - case "eps": - cy.get('a[name="success"]').click(); - break; - case "ideal": - cy.get('a[name="success"]').click(); - break; - case "giropay": - cy.get('a[name="success"]').click(); - break; - case "sofort": - cy.get('a[name="success"]').click(); - break; - case "przelewy24": - cy.get('a[name="success"]').click(); - break; - default: - throw new Error( - `Unsupported payment method type: ${payment_method_type}` - ); - } - verifyUrl = true; - break; - case "trustpay": - switch (payment_method_type) { - case "eps": - cy.get("#bankname").type( - "Allgemeine Sparkasse Oberösterreich Bank AG (ASPKAT2LXXX / 20320)" - ); - cy.get("#selectionSubmit").click(); - cy.get("#user") - .should("be.visible") - .should("be.enabled") - .focus() - .type("Verfügernummer"); - cy.get("input#submitButton.btn.btn-primary").click(); - break; - case "ideal": - cy.contains("button", "Select your bank").click(); - cy.get( - 'button[data-testid="bank-item"][id="bank-item-INGBNL2A"]' - ).click(); + + cy.visit(redirectionUrl.href); + waitForRedirect(redirectionUrl.href); + + handleFlow( + redirectionUrl, + expectedUrl, + connectorId, + ({ connectorId, paymentMethodType, constants }) => { + switch (connectorId) { + case "adyen": + switch (paymentMethodType) { + case "eps": + cy.get("h1").should("contain.text", "Acquirer Simulator"); + cy.get('[value="authorised"]').click(); + break; + case "ideal": + cy.get(":nth-child(4) > td > p").should( + "contain.text", + "Your Payment was Authorised/Refused/Cancelled (It may take up to five minutes to show on the Payment List)" + ); + cy.get(".btnLink").click(); + cy.url().should("include", "status=succeeded"); + cy.wait(5000); + break; + case "giropay": + cy.get( + ".rds-cookies-overlay__allow-all-cookies-btn > .rds-button" + ).click(); + cy.wait(5000); + cy.get(".normal-3").should( + "contain.text", + "Bank suchen ‑ mit giropay zahlen." + ); + cy.get("#bankSearch").type("giropay TestBank{enter}"); + cy.get(".normal-2 > div").click(); + cy.get('[data-testid="customerIban"]').type( + "DE48499999601234567890" + ); + cy.get('[data-testid="customerIdentification"]').type( + "9123456789" + ); + cy.get(":nth-child(3) > .rds-button").click(); + cy.get('[data-testid="onlineBankingPin"]').type("1234"); + cy.get(".rds-button--primary").click(); + cy.get(":nth-child(5) > .rds-radio-input-group__label").click(); + cy.get(".rds-button--primary").click(); + cy.get('[data-testid="photoTan"]').type("123456"); + cy.get(".rds-button--primary").click(); + cy.wait(5000); + cy.url().should("include", "status=succeeded"); + cy.wait(5000); + break; + case "sofort": + cy.get(".modal-overlay.modal-shown.in", { + timeout: constants.TIMEOUT, + }).then(($modal) => { + // If modal is found, handle it + if ($modal.length > 0) { + cy.get("button.cookie-modal-deny-all.button-tertiary") + .should("be.visible") + .should("contain", "Reject All") + .click({ multiple: true }); + cy.get("div#TopBanks.top-banks-multistep") + .should("contain", "Demo Bank") + .as("btn") + .click(); + cy.get("@btn").click(); + } else { + cy.get("input.phone").type("9123456789"); + cy.get("#button.onContinue") + .should("contain", "Continue") + .click(); + } + }); + break; + default: + throw new Error( + `Unsupported payment method type: ${paymentMethodType}` + ); + } + verifyUrl = true; break; - case "giropay": - cy.get("._transactionId__header__iXVd_").should( - "contain.text", - "Bank suchen ‑ mit giropay zahlen." - ); - cy.get(".BankSearch_searchInput__uX_9l").type( - "Volksbank Hildesheim{enter}" - ); - cy.get(".BankSearch_searchIcon__EcVO7").click(); - cy.get(".BankSearch_bankWrapper__R5fUK").click(); - cy.get("._transactionId__primaryButton__nCa0r").click(); - cy.get(".normal-3").should("contain.text", "Kontoauswahl"); + case "paypal": + if (["eps", "ideal", "giropay"].includes(paymentMethodType)) { + cy.get('button[name="Successful"][value="SUCCEEDED"]').click(); + verifyUrl = true; + } else { + throw new Error( + `Unsupported payment method type: ${paymentMethodType}` + ); + } + verifyUrl = true; break; - case "sofort": + case "stripe": + if ( + ["eps", "ideal", "giropay", "sofort", "przelewy24"].includes( + paymentMethodType + ) + ) { + cy.get('a[name="success"]').click(); + verifyUrl = true; + } else { + throw new Error( + `Unsupported payment method type: ${paymentMethodType}` + ); + } + verifyUrl = true; break; - case "trustly": + case "trustpay": + switch (paymentMethodType) { + case "eps": + cy.get("#bankname").type( + "Allgemeine Sparkasse Oberösterreich Bank AG (ASPKAT2LXXX / 20320)" + ); + cy.get("#selectionSubmit").click(); + cy.get("#user") + .should("be.visible") + .should("be.enabled") + .focus() + .type("Verfügernummer"); + cy.get("input#submitButton.btn.btn-primary").click(); + break; + case "ideal": + cy.contains("button", "Select your bank").click(); + cy.get( + 'button[data-testid="bank-item"][id="bank-item-INGBNL2A"]' + ).click(); + break; + case "giropay": + cy.get("._transactionId__header__iXVd_").should( + "contain.text", + "Bank suchen ‑ mit giropay zahlen." + ); + cy.get(".BankSearch_searchInput__uX_9l").type( + "Volksbank Hildesheim{enter}" + ); + cy.get(".BankSearch_searchIcon__EcVO7").click(); + cy.get(".BankSearch_bankWrapper__R5fUK").click(); + cy.get("._transactionId__primaryButton__nCa0r").click(); + cy.get(".normal-3").should("contain.text", "Kontoauswahl"); + break; + default: + throw new Error( + `Unsupported payment method type: ${paymentMethodType}` + ); + } + verifyUrl = false; break; default: - throw new Error( - `Unsupported payment method type: ${payment_method_type}` - ); + throw new Error(`Unsupported connector: ${connectorId}`); } - verifyUrl = false; - break; - default: - throw new Error(`Unsupported connector: ${connectorId}`); - } + }, + { paymentMethodType } + ); cy.then(() => { - verifyReturnUrl(redirection_url, expected_url, verifyUrl); + verifyReturnUrl(redirectionUrl, expectedUrl, verifyUrl); }); } -function threeDsRedirection(redirection_url, expected_url, connectorId) { - cy.visit(redirection_url.href); - - switch (connectorId) { - case "adyen": - cy.get("iframe") - .its("0.contentDocument.body") - .within(() => { - cy.get('input[type="password"]').click(); - cy.get('input[type="password"]').type("password"); - cy.get("#buttonSubmit").click(); - }); - break; +function threeDsRedirection(redirectionUrl, expectedUrl, connectorId) { + cy.visit(redirectionUrl.href); + waitForRedirect(redirectionUrl.href); - case "bankofamerica": - case "wellsfargo": - cy.get("iframe", { timeout: TIMEOUT }) - .should("be.visible") - .its("0.contentDocument.body") - .should("not.be.empty") - .within(() => { - cy.get( - 'input[type="text"], input[type="password"], input[name="challengeDataEntry"]', - { timeout: TIMEOUT } - ) - .should("be.visible") - .should("be.enabled") - .click() - .type("1234"); + handleFlow( + redirectionUrl, + expectedUrl, + connectorId, + ({ connectorId, constants, expectedUrl }) => { + switch (connectorId) { + case "adyen": + cy.get("iframe") + .its("0.contentDocument.body") + .within(() => { + cy.get('input[type="password"]').click(); + cy.get('input[type="password"]').type("password"); + cy.get("#buttonSubmit").click(); + }); + break; - cy.get('input[value="SUBMIT"], button[type="submit"]', { - timeout: TIMEOUT, - }) + case "bankofamerica": + case "wellsfargo": + cy.get("iframe", { timeout: constants.TIMEOUT }) .should("be.visible") - .click(); - }); - break; + .its("0.contentDocument.body") + .should("not.be.empty") + .within(() => { + cy.get( + 'input[type="text"], input[type="password"], input[name="challengeDataEntry"]', + { timeout: constants.TIMEOUT } + ) + .should("be.visible") + .should("be.enabled") + .click() + .type("1234"); + + cy.get('input[value="SUBMIT"], button[type="submit"]', { + timeout: constants.TIMEOUT, + }) + .should("be.visible") + .click(); + }); + break; - case "cybersource": - cy.url({ timeout: TIMEOUT }).should("include", expected_url.origin); - break; + case "cybersource": + cy.url({ timeout: constants.TIMEOUT }).should("include", expectedUrl); + break; + + case "checkout": + cy.get("iframe", { timeout: constants.TIMEOUT }) + .its("0.contentDocument.body") + .within(() => { + cy.get('form[id="form"]', { timeout: constants.WAIT_TIME }) + .should("exist") + .then(() => { + cy.get('input[id="password"]').click(); + cy.get('input[id="password"]').type("Checkout1!"); + cy.get("#txtButton").click(); + }); + }); + break; - case "checkout": - cy.get("iframe", { timeout: TIMEOUT }) - .its("0.contentDocument.body") - .within(() => { - cy.get('form[id="form"]', { timeout: WAIT_TIME }) + case "nmi": + case "noon": + case "xendit": + cy.get("iframe", { timeout: constants.TIMEOUT }) + .its("0.contentDocument.body") + .within(() => { + cy.get("iframe", { timeout: constants.TIMEOUT }) + .its("0.contentDocument.body") + .within(() => { + cy.get('form[name="cardholderInput"]', { + timeout: constants.TIMEOUT, + }) + .should("exist") + .then(() => { + cy.get('input[name="challengeDataEntry"]') + .click() + .type("1234"); + cy.get('input[value="SUBMIT"]').click(); + }); + }); + }); + break; + + case "novalnet": + cy.get("form", { timeout: constants.WAIT_TIME }) .should("exist") .then(() => { - cy.get('input[id="password"]').click(); - cy.get('input[id="password"]').type("Checkout1!"); - cy.get("#txtButton").click(); + cy.get('input[id="submit"]').click(); }); - }); - break; + break; + + case "stripe": + cy.get("iframe", { timeout: constants.TIMEOUT }) + .its("0.contentDocument.body") + .within(() => { + cy.get("iframe") + .its("0.contentDocument.body") + .within(() => { + cy.get("#test-source-authorize-3ds").click(); + }); + }); + break; + + case "trustpay": + cy.get('form[name="challengeForm"]', { + timeout: constants.WAIT_TIME, + }) + .should("exist") + .then(() => { + cy.get("#outcomeSelect") + .select("Approve") + .should("have.value", "Y"); + cy.get('button[type="submit"]').click(); + }); + break; - case "nmi": - case "noon": - case "xendit": - cy.get("iframe", { timeout: TIMEOUT }) - .its("0.contentDocument.body") - .within(() => { - cy.get("iframe", { timeout: TIMEOUT }) + case "worldpay": + cy.get("iframe", { timeout: constants.WAIT_TIME }) .its("0.contentDocument.body") .within(() => { - cy.get('form[name="cardholderInput"]', { timeout: TIMEOUT }) + cy.get('form[name="cardholderInput"]', { + timeout: constants.WAIT_TIME, + }) .should("exist") .then(() => { cy.get('input[name="challengeDataEntry"]') @@ -350,91 +414,50 @@ function threeDsRedirection(redirection_url, expected_url, connectorId) { cy.get('input[value="SUBMIT"]').click(); }); }); - }); - break; - - case "novalnet": - cy.get("form", { timeout: WAIT_TIME }) - .should("exist") - .then(() => { - cy.get('input[id="submit"]').click(); - }); - break; - - case "stripe": - cy.get("iframe", { timeout: TIMEOUT }) - .its("0.contentDocument.body") - .within(() => { - cy.get("iframe") - .its("0.contentDocument.body") - .within(() => { - cy.get("#test-source-authorize-3ds").click(); - }); - }); - break; - - case "trustpay": - cy.get('form[name="challengeForm"]', { timeout: WAIT_TIME }) - .should("exist") - .then(() => { - cy.get("#outcomeSelect").select("Approve").should("have.value", "Y"); - cy.get('button[type="submit"]').click(); - }); - break; + break; - case "worldpay": - cy.get("iframe", { timeout: WAIT_TIME }) - .its("0.contentDocument.body") - .within(() => { - cy.get('form[name="cardholderInput"]', { timeout: WAIT_TIME }) + case "fiuu": + cy.get('form[id="cc_form"]', { timeout: constants.TIMEOUT }) .should("exist") .then(() => { - cy.get('input[name="challengeDataEntry"]').click().type("1234"); - cy.get('input[value="SUBMIT"]').click(); - }); - }); - break; - - case "fiuu": - cy.get('form[id="cc_form"]', { timeout: TIMEOUT }) - .should("exist") - .then(() => { - cy.get('button.pay-btn[name="pay"]').click(); - cy.get("div.otp") - .invoke("text") - .then((otpText) => { - const otp = otpText.match(/\d+/)[0]; - cy.get("input#otp-input").should("not.be.disabled").type(otp); - cy.get("button.pay-btn").click(); + cy.get('button.pay-btn[name="pay"]').click(); + cy.get("div.otp") + .invoke("text") + .then((otpText) => { + const otp = otpText.match(/\d+/)[0]; + cy.get("input#otp-input").should("not.be.disabled").type(otp); + cy.get("button.pay-btn").click(); + }); }); - }); - break; + break; - default: - cy.wait(WAIT_TIME); - } + default: + cy.wait(constants.WAIT_TIME); + } + } + ); // Verify return URL after handling the specific connector - verifyReturnUrl(redirection_url, expected_url, true); + verifyReturnUrl(redirectionUrl, expectedUrl, true); } function upiRedirection( - redirection_url, - expected_url, + redirectionUrl, + expectedUrl, connectorId, - payment_method_type + paymentMethodType ) { let verifyUrl = false; if (connectorId === "iatapay") { - switch (payment_method_type) { + switch (paymentMethodType) { case "upi_collect": - cy.visit(redirection_url.href); - cy.wait(TIMEOUT).then(() => { + cy.visit(redirectionUrl.href); + cy.wait(CONSTANTS.TIMEOUT).then(() => { verifyUrl = true; }); break; case "upi_intent": - cy.request(redirection_url.href).then((response) => { + cy.request(redirectionUrl.href).then((response) => { expect(response.status).to.eq(200); expect(response.body).to.have.property("iataPaymentId"); expect(response.body).to.have.property("status", "INITIATED"); @@ -446,99 +469,95 @@ function upiRedirection( break; default: throw new Error( - `Unsupported payment method type: ${payment_method_type}` + `Unsupported payment method type: ${paymentMethodType}` ); } } else { - // If connectorId is not iatapay, wait for 10 seconds - cy.wait(WAIT_TIME); + // For other connectors, nothing to do + return; } cy.then(() => { - verifyReturnUrl(redirection_url, expected_url, verifyUrl); + verifyReturnUrl(redirectionUrl, expectedUrl, verifyUrl); }); } -function verifyReturnUrl(redirection_url, expected_url, forward_flow) { - if (!forward_flow) return; - - try { - if (redirection_url.host.endsWith(expected_url.host)) { - cy.wait(WAIT_TIME / 2); - - cy.window() - .its("location") - .then((location) => { - // Check page state before taking screenshots - cy.document().then((doc) => { - // For blank page - cy.wrap(doc.body.innerText.trim()).then((text) => { - if (text === "") { - cy.wrap(text).should("eq", ""); - cy.screenshot("blank-page-error"); - } - }); +function verifyReturnUrl(redirectionUrl, expectedUrl, forwardFlow) { + if (!forwardFlow) return; - // For error pages - const errorPatterns = [ - /4\d{2}/, - /5\d{2}/, - /error/i, - /invalid request/i, - /server error/i, - ]; - - const pageText = doc.body.innerText.toLowerCase(); - cy.wrap(pageText).then((text) => { - if (errorPatterns.some((pattern) => pattern.test(text))) { - cy.wrap(text).should((content) => { - expect(errorPatterns.some((pattern) => pattern.test(content))) - .to.be.true; - }); - cy.screenshot(`error-page-${Date.now()}`); - } - }); - }); + cy.location("host", { timeout: CONSTANTS.TIMEOUT }).should((currentHost) => { + expect(currentHost).to.equal(expectedUrl.host); + }); - const url_params = new URLSearchParams(location.search); - const payment_status = url_params.get("status"); + cy.url().then((url) => { + cy.origin( + new URL(url).origin, + { + args: { + redirectionUrl: redirectionUrl.origin, + expectedUrl: expectedUrl.origin, + constants: CONSTANTS, + }, + }, + ({ redirectionUrl, expectedUrl, constants }) => { + try { + const redirectionHost = new URL(redirectionUrl).host; + const expectedHost = new URL(expectedUrl).host; + if (redirectionHost.endsWith(expectedHost)) { + cy.wait(constants.WAIT_TIME / 2); + + cy.window() + .its("location") + .then((location) => { + // Check page state before taking screenshots + cy.document().then((doc) => { + const pageText = doc.body.innerText.toLowerCase(); + if (!pageText) { + // eslint-disable-next-line cypress/assertion-before-screenshot + cy.screenshot("blank-page-error"); + } else if ( + constants.ERROR_PATTERNS.some((pattern) => + pattern.test(pageText) + ) + ) { + // eslint-disable-next-line cypress/assertion-before-screenshot + cy.screenshot(`error-page-${Date.now()}`); + } + }); - if ( - payment_status !== "failed" && - payment_status !== "processing" && - payment_status !== "requires_capture" && - payment_status !== "succeeded" - ) { - cy.wrap(payment_status).should("exist"); - cy.screenshot(`failed-payment-${payment_status}`); - throw new Error( - `Redirection failed with payment status: ${payment_status}` - ); + const urlParams = new URLSearchParams(location.search); + const paymentStatus = urlParams.get("status"); + + if ( + !constants.VALID_TERMINAL_STATUSES.includes(paymentStatus) + ) { + // eslint-disable-next-line cypress/assertion-before-screenshot + cy.screenshot(`failed-payment-${paymentStatus}`); + throw new Error( + `Redirection failed with payment status: ${paymentStatus}` + ); + } + }); + } else { + cy.window().its("location.origin").should("eq", expectedUrl); + + Cypress.on("uncaught:exception", (err, runnable) => { + // Log the error details + // eslint-disable-next-line no-console + console.error( + `Error: ${err.message}\nOccurred in: ${runnable.title}\nStack: ${err.stack}` + ); + + // Return false to prevent the error from failing the test + return false; + }); } - }); - } else { - cy.origin( - expected_url.origin, - { args: { expected_url: expected_url.origin } }, - ({ expected_url }) => { - cy.window().its("location.origin").should("eq", expected_url); - - Cypress.on("uncaught:exception", (err, runnable) => { - // Log the error details - // eslint-disable-next-line no-console - console.log( - `Error: ${err.message}\nError occurred in: ${runnable.title}\nStack trace: ${err.stack}` - ); - // Return false to prevent the error from failing the test - return false; - }); + } catch (error) { + throw new Error(`Redirection verification failed: ${error}`); } - ); - } - } catch (error) { - cy.error("Redirection verification failed:", error); - throw error; - } + } + ); + }); } async function fetchAndParseQRCode(url) { @@ -548,12 +567,17 @@ async function fetchAndParseQRCode(url) { } const blob = await response.blob(); const reader = new FileReader(); - return await new Promise((resolve, reject) => { + + return new Promise((resolve, reject) => { reader.onload = () => { - const base64Image = reader.result.split(",")[1]; // Remove data URI prefix + // Use the entire data URI from reader.result + const dataUrl = reader.result; + + // Create a new Image, assigning its src to the full data URI const image = new Image(); - image.src = base64Image; + image.src = dataUrl; + // Once the image loads, draw it to a canvas and let jsQR decode it image.onload = () => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); @@ -574,8 +598,14 @@ async function fetchAndParseQRCode(url) { reject(new Error("Failed to decode QR code")); } }; - image.onerror = reject; // Handle image loading errors + + // If the image fails to load at all, reject the promise + image.onerror = (err) => { + reject(new Error("Image failed to load: " + err?.message || err)); + }; }; + + // Read the blob as a data URL (this includes the data:image/png;base64 prefix) reader.readAsDataURL(blob); }); } @@ -608,3 +638,69 @@ async function fetchAndParseImageData(url) { image.onerror = reject; // Handle image loading errors }); } + +function waitForRedirect(redirectionUrl) { + const originalHost = new URL(redirectionUrl).host; + + cy.location("host", { timeout: CONSTANTS.TIMEOUT }).should((currentHost) => { + const hostChanged = currentHost !== originalHost; + const iframeExists = Cypress.$("iframe") + .toArray() + .some((iframeEl) => { + try { + const iframeHost = new URL(iframeEl.src).host; + return iframeHost && iframeHost !== originalHost; + } catch { + return false; + } + }); + + // The assertion will pass if either the host changed or an iframe with a foreign host exists. + expect( + hostChanged || iframeExists, + "Host changed or an iframe with foreign host exist" + ).to.be.true; + }); +} + +function handleFlow( + redirectionUrl, + expectedUrl, + connectorId, + callback, + options = {} +) { + const originalHost = new URL(redirectionUrl.href).host; + cy.location("host", { timeout: CONSTANTS.TIMEOUT }).then((currentHost) => { + if (currentHost !== originalHost) { + // Regular redirection flow - host changed, use cy.origin() + cy.url().then((currentUrl) => { + cy.origin( + new URL(currentUrl).origin, + { + args: { + connectorId, + constants: CONSTANTS, + expectedUrl: expectedUrl.origin, + ...options, // optional params like paymentMethodType + }, + }, + callback + ); + }); + } else { + // Embedded flow - host unchanged, use cy.get("iframe") + cy.get("iframe", { timeout: CONSTANTS.TIMEOUT }) + .should("be.visible") + .should("exist"); + + // Execute callback within the iframe context + callback({ + connectorId, + constants: CONSTANTS, + expectedUrl: expectedUrl.origin, + ...options, // optional params like paymentMethodType + }); + } + }); +} diff --git a/cypress-tests/cypress/utils/featureFlags.js b/cypress-tests/cypress/utils/featureFlags.js index 6d8599820f7..728016ebbb5 100644 --- a/cypress-tests/cypress/utils/featureFlags.js +++ b/cypress-tests/cypress/utils/featureFlags.js @@ -149,29 +149,41 @@ function matchesSpecName(specName) { ); } -export function determineConnectorConfig(connectorConfig) { - // Case 1: Multiple connectors configuration - if ( - connectorConfig?.nextConnector && - connectorConfig?.multipleConnectors?.status - ) { - return "connector_2"; - } +export function determineConnectorConfig(config) { + const connectorCredential = config?.CONNECTOR_CREDENTIAL; + const multipleConnectors = config?.multipleConnectors; - // Case 2: Invalid or null configuration - if (!connectorConfig || connectorConfig.value === "null") { + // If CONNECTOR_CREDENTIAL doesn't exist or value is null, return default + if (!connectorCredential || connectorCredential.value === null) { return DEFAULT_CONNECTOR; } - const { specName, value } = connectorConfig; + // Handle nextConnector cases + if ( + Object.prototype.hasOwnProperty.call(connectorCredential, "nextConnector") + ) { + if (connectorCredential.nextConnector === true) { + // Check multipleConnectors conditions if available + if ( + multipleConnectors?.status === true && + multipleConnectors?.count > 1 + ) { + return "connector_2"; + } + return DEFAULT_CONNECTOR; + } + return DEFAULT_CONNECTOR; + } - // Case 3: No spec name matching needed - if (!specName) { - return value; + // Handle specName cases + if (Object.prototype.hasOwnProperty.call(connectorCredential, "specName")) { + return matchesSpecName(connectorCredential.specName) + ? connectorCredential.value + : DEFAULT_CONNECTOR; } - // Case 4: Match spec name and return appropriate connector - return matchesSpecName(specName) ? value : DEFAULT_CONNECTOR; + // Return value if it's the only property + return connectorCredential.value; } export function execConfig(configs) { @@ -179,7 +191,7 @@ export function execConfig(configs) { cy.wait(configs.DELAY.TIMEOUT); } - const connectorType = determineConnectorConfig(configs?.CONNECTOR_CREDENTIAL); + const connectorType = determineConnectorConfig(configs); const { profileId, connectorId } = getProfileAndConnectorId(connectorType); return { diff --git a/cypress-tests/package-lock.json b/cypress-tests/package-lock.json index 57aef9f3b3e..abb5bd3cec5 100644 --- a/cypress-tests/package-lock.json +++ b/cypress-tests/package-lock.json @@ -9,10 +9,10 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@eslint/js": "^9.18.0", - "cypress": "^13.17.0", + "@eslint/js": "^9.19.0", + "cypress": "^14.0.0", "cypress-mochawesome-reporter": "^3.8.2", - "eslint": "^9.18.0", + "eslint": "^9.19.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-cypress": "^4.1.0", "eslint-plugin-prettier": "^5.2.3", @@ -191,9 +191,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", - "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", + "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", "dev": true, "license": "MIT", "engines": { @@ -1023,9 +1023,9 @@ } }, "node_modules/cypress": { - "version": "13.17.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz", - "integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.0.0.tgz", + "integrity": "sha512-kEGqQr23so5IpKeg/dp6GVi7RlHx1NmW66o2a2Q4wk9gRaAblLZQSiZJuDI8UMC4LlG5OJ7Q6joAiqTrfRNbTw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1078,7 +1078,7 @@ "cypress": "bin/cypress" }, "engines": { - "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" } }, "node_modules/cypress-mochawesome-reporter": { @@ -1335,9 +1335,9 @@ } }, "node_modules/eslint": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz", - "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==", + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", + "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", "dev": true, "license": "MIT", "dependencies": { @@ -1346,7 +1346,7 @@ "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.10.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.18.0", + "@eslint/js": "9.19.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", diff --git a/cypress-tests/package.json b/cypress-tests/package.json index a2e0a961e8f..b3701ce8fc7 100644 --- a/cypress-tests/package.json +++ b/cypress-tests/package.json @@ -20,10 +20,10 @@ "author": "Hyperswitch", "license": "ISC", "devDependencies": { - "@eslint/js": "^9.18.0", - "cypress": "^13.17.0", + "@eslint/js": "^9.19.0", + "cypress": "^14.0.0", "cypress-mochawesome-reporter": "^3.8.2", - "eslint": "^9.18.0", + "eslint": "^9.19.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-cypress": "^4.1.0", "eslint-plugin-prettier": "^5.2.3", From 64a7afa6d42270d96788119e666b97176cd753dd Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Mon, 3 Feb 2025 23:55:22 +0530 Subject: [PATCH 02/46] fix(connector): [NETCETERA] add `sdk-type` and `default-sdk-type` in netcetera authentication request (#7156) --- api-reference-v2/openapi_spec.json | 19 +++++++++++++ api-reference/openapi_spec.json | 19 +++++++++++++ crates/api_models/src/payments.rs | 17 ++++++++++++ crates/openapi/src/openapi.rs | 1 + crates/openapi/src/openapi_v2.rs | 1 + .../connector/netcetera/netcetera_types.rs | 27 ++++++++++++++++--- 6 files changed, 80 insertions(+), 4 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 13014a44e0e..236c0af3a38 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -19982,6 +19982,14 @@ "format": "int32", "description": "Indicates maximum amount of time in minutes", "minimum": 0 + }, + "sdk_type": { + "allOf": [ + { + "$ref": "#/components/schemas/SdkType" + } + ], + "nullable": true } } }, @@ -20011,6 +20019,17 @@ } } }, + "SdkType": { + "type": "string", + "description": "Enum representing the type of 3DS SDK.", + "enum": [ + "01", + "02", + "03", + "04", + "05" + ] + }, "SecretInfoToInitiateSdk": { "type": "object", "required": [ diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index d218e4d2074..9b840f53138 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -24523,6 +24523,14 @@ "format": "int32", "description": "Indicates maximum amount of time in minutes", "minimum": 0 + }, + "sdk_type": { + "allOf": [ + { + "$ref": "#/components/schemas/SdkType" + } + ], + "nullable": true } } }, @@ -24552,6 +24560,17 @@ } } }, + "SdkType": { + "type": "string", + "description": "Enum representing the type of 3DS SDK.", + "enum": [ + "01", + "02", + "03", + "04", + "05" + ] + }, "SecretInfoToInitiateSdk": { "type": "object", "required": [ diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index b6152cc9787..2c0a09bf8f0 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -6548,6 +6548,23 @@ pub struct SdkInformation { pub sdk_reference_number: String, /// Indicates maximum amount of time in minutes pub sdk_max_timeout: u8, + /// Indicates the type of 3DS SDK + pub sdk_type: Option, +} + +/// Enum representing the type of 3DS SDK. +#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)] +pub enum SdkType { + #[serde(rename = "01")] + DefaultSdk, + #[serde(rename = "02")] + SplitSdk, + #[serde(rename = "03")] + LimitedSdk, + #[serde(rename = "04")] + BrowserSdk, + #[serde(rename = "05")] + ShellSdk, } #[cfg(feature = "v2")] diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 3a634ae2a54..de358f79332 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -653,6 +653,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::enums::PaymentChargeType, api_models::enums::StripeChargeType, api_models::payments::CustomerDetailsResponse, + api_models::payments::SdkType, api_models::payments::OpenBankingData, api_models::payments::OpenBankingSessionToken, api_models::payments::BankDebitResponse, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 3b1fefefa15..701fb22d1ad 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -328,6 +328,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::BankRedirectBilling, api_models::payments::ConnectorMetadata, api_models::payments::FeatureMetadata, + api_models::payments::SdkType, api_models::payments::ApplepayConnectorMetadataRequest, api_models::payments::SessionTokenInfo, api_models::payments::PaymentProcessingDetailsAt, diff --git a/crates/router/src/connector/netcetera/netcetera_types.rs b/crates/router/src/connector/netcetera/netcetera_types.rs index 0ecbbf63e3e..36daa62e558 100644 --- a/crates/router/src/connector/netcetera/netcetera_types.rs +++ b/crates/router/src/connector/netcetera/netcetera_types.rs @@ -1538,7 +1538,7 @@ pub struct Sdk { /// /// This field is required for requests where deviceChannel = 01 (APP). /// Available for supporting EMV 3DS 2.3.1 and later versions. - sdk_type: Option, + sdk_type: Option, /// Indicates the characteristics of a Default-SDK. /// @@ -1563,16 +1563,35 @@ impl From for Sdk { sdk_reference_number: Some(sdk_info.sdk_reference_number), sdk_trans_id: Some(sdk_info.sdk_trans_id), sdk_server_signed_content: None, - sdk_type: None, - default_sdk_type: None, + sdk_type: sdk_info + .sdk_type + .map(SdkType::from) + .or(Some(SdkType::DefaultSdk)), + default_sdk_type: Some(DefaultSdkType { + // hardcoding this value because, it's the only value that is accepted + sdk_variant: "01".to_string(), + wrapped_ind: None, + }), split_sdk_type: None, } } } +impl From for SdkType { + fn from(sdk_type: api_models::payments::SdkType) -> Self { + match sdk_type { + api_models::payments::SdkType::DefaultSdk => Self::DefaultSdk, + api_models::payments::SdkType::SplitSdk => Self::SplitSdk, + api_models::payments::SdkType::LimitedSdk => Self::LimitedSdk, + api_models::payments::SdkType::BrowserSdk => Self::BrowserSdk, + api_models::payments::SdkType::ShellSdk => Self::ShellSdk, + } + } +} + /// Enum representing the type of 3DS SDK. #[derive(Serialize, Deserialize, Debug, Clone)] -pub enum SdkTypeEnum { +pub enum SdkType { #[serde(rename = "01")] DefaultSdk, #[serde(rename = "02")] From ae39374c6b41635e6c474b429fd1df59d30aa6dd Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Tue, 4 Feb 2025 01:10:24 +0530 Subject: [PATCH 03/46] feat(router): add core changes for external authentication flow through unified_authentication_service (#7063) Co-authored-by: Sahkal Poddar Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Sahkal Poddar --- .../unified_authentication_service.rs | 24 +- .../transformers.rs | 28 +- .../src/default_implementations.rs | 82 ++++- .../unified_authentication_service.rs | 3 + .../unified_authentication_service.rs | 80 +++- crates/hyperswitch_domain_models/src/types.rs | 16 +- crates/hyperswitch_interfaces/src/api.rs | 29 +- crates/hyperswitch_interfaces/src/types.rs | 13 +- crates/router/src/core/payments.rs | 109 ++++-- .../connector_integration_v2_impls.rs | 16 +- crates/router/src/core/payments/flows.rs | 63 +++- crates/router/src/core/payments/helpers.rs | 67 ++++ crates/router/src/core/payments/operations.rs | 1 + .../payments/operations/payment_confirm.rs | 216 ++++++++--- .../core/unified_authentication_service.rs | 344 ++++++++++++++++-- .../transformers.rs | 45 --- .../unified_authentication_service/types.rs | 125 ++++++- .../unified_authentication_service/utils.rs | 177 ++++++--- crates/router/src/routes/payments.rs | 9 +- crates/router/src/types.rs | 4 +- 20 files changed, 1190 insertions(+), 261 deletions(-) delete mode 100644 crates/router/src/core/unified_authentication_service/transformers.rs diff --git a/crates/hyperswitch_connectors/src/connectors/unified_authentication_service.rs b/crates/hyperswitch_connectors/src/connectors/unified_authentication_service.rs index ece7130bb1d..55f3528bbc4 100644 --- a/crates/hyperswitch_connectors/src/connectors/unified_authentication_service.rs +++ b/crates/hyperswitch_connectors/src/connectors/unified_authentication_service.rs @@ -13,12 +13,12 @@ use hyperswitch_domain_models::{ access_token_auth::AccessTokenAuth, payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void}, refunds::{Execute, RSync}, - PostAuthenticate, PreAuthenticate, + Authenticate, PostAuthenticate, PreAuthenticate, }, router_request_types::{ unified_authentication_service::{ - UasAuthenticationResponseData, UasPostAuthenticationRequestData, - UasPreAuthenticationRequestData, + UasAuthenticationRequestData, UasAuthenticationResponseData, + UasPostAuthenticationRequestData, UasPreAuthenticationRequestData, }, AccessTokenRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData, PaymentsCancelData, PaymentsCaptureData, PaymentsSessionData, PaymentsSyncData, @@ -71,6 +71,7 @@ impl api::PaymentToken for UnifiedAuthenticationService {} impl api::UnifiedAuthenticationService for UnifiedAuthenticationService {} impl api::UasPreAuthentication for UnifiedAuthenticationService {} impl api::UasPostAuthentication for UnifiedAuthenticationService {} +impl api::UasAuthentication for UnifiedAuthenticationService {} impl ConnectorIntegration for UnifiedAuthenticationService @@ -209,8 +210,16 @@ impl )?; let amount = utils::convert_amount( self.amount_converter, - transaction_details.amount, - transaction_details.currency, + transaction_details + .amount + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "amount", + })?, + transaction_details + .currency + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })?, )?; let connector_router_data = @@ -366,6 +375,11 @@ impl } } +impl ConnectorIntegration + for UnifiedAuthenticationService +{ +} + impl ConnectorIntegration for UnifiedAuthenticationService { diff --git a/crates/hyperswitch_connectors/src/connectors/unified_authentication_service/transformers.rs b/crates/hyperswitch_connectors/src/connectors/unified_authentication_service/transformers.rs index 9af21164d06..f2ca77d746f 100644 --- a/crates/hyperswitch_connectors/src/connectors/unified_authentication_service/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/unified_authentication_service/transformers.rs @@ -3,7 +3,8 @@ use common_utils::types::FloatMajorUnit; use hyperswitch_domain_models::{ router_data::{ConnectorAuthType, RouterData}, router_request_types::unified_authentication_service::{ - DynamicData, PostAuthenticationDetails, TokenDetails, UasAuthenticationResponseData, + DynamicData, PostAuthenticationDetails, PreAuthenticationDetails, TokenDetails, + UasAuthenticationResponseData, }, types::{UasPostAuthenticationRouterData, UasPreAuthenticationRouterData}, }; @@ -238,7 +239,10 @@ impl TryFrom<&UnifiedAuthenticationServiceRouterData<&UasPreAuthenticationRouter .ok_or(errors::ConnectorError::MissingRequiredField { field_name: "transaction_details", })? - .currency, + .currency + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })?, date: None, pan_source: None, protection_type: None, @@ -301,7 +305,18 @@ impl >, ) -> Result { Ok(Self { - response: Ok(UasAuthenticationResponseData::PreAuthentication {}), + response: Ok(UasAuthenticationResponseData::PreAuthentication { + authentication_details: PreAuthenticationDetails { + threeds_server_transaction_id: None, + maximum_supported_3ds_version: None, + connector_authentication_id: None, + three_ds_method_data: None, + three_ds_method_url: None, + message_version: None, + connector_metadata: None, + directory_server_id: None, + }, + }), ..item.data }) } @@ -337,7 +352,7 @@ pub struct UasTokenDetails { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct UasDynamicData { pub dynamic_data_value: Option>, - pub dynamic_data_type: String, + pub dynamic_data_type: Option, pub ds_trans_id: Option, } @@ -384,7 +399,7 @@ impl response: Ok(UasAuthenticationResponseData::PostAuthentication { authentication_details: PostAuthenticationDetails { eci: item.response.authentication_details.eci, - token_details: TokenDetails { + token_details: Some(TokenDetails { payment_token: item .response .authentication_details @@ -405,7 +420,7 @@ impl .authentication_details .token_details .token_expiration_year, - }, + }), dynamic_data_details: item .response .authentication_details @@ -415,6 +430,7 @@ impl dynamic_data_type: dynamic_data.dynamic_data_type, ds_trans_id: dynamic_data.ds_trans_id, }), + trans_status: None, }, }), ..item.data diff --git a/crates/hyperswitch_connectors/src/default_implementations.rs b/crates/hyperswitch_connectors/src/default_implementations.rs index e70aff630e3..f58a4802d50 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -33,12 +33,12 @@ use hyperswitch_domain_models::{ PreProcessing, Reject, SdkSessionUpdate, }, webhooks::VerifyWebhookSource, - PostAuthenticate, PreAuthenticate, + Authenticate, PostAuthenticate, PreAuthenticate, }, router_request_types::{ unified_authentication_service::{ - UasAuthenticationResponseData, UasPostAuthenticationRequestData, - UasPreAuthenticationRequestData, + UasAuthenticationRequestData, UasAuthenticationResponseData, + UasPostAuthenticationRequestData, UasPreAuthenticationRequestData, }, AcceptDisputeRequestData, AuthorizeSessionTokenData, CompleteAuthorizeData, ConnectorCustomerData, DefendDisputeRequestData, MandateRevokeRequestData, @@ -74,7 +74,7 @@ use hyperswitch_interfaces::{ PaymentSessionUpdate, PaymentsCompleteAuthorize, PaymentsPostProcessing, PaymentsPreProcessing, TaxCalculation, }, - ConnectorIntegration, ConnectorMandateRevoke, ConnectorRedirectResponse, + ConnectorIntegration, ConnectorMandateRevoke, ConnectorRedirectResponse, UasAuthentication, UasPostAuthentication, UasPreAuthentication, UnifiedAuthenticationService, }, errors::ConnectorError, @@ -2648,3 +2648,77 @@ default_imp_for_uas_post_authentication!( connectors::Zen, connectors::Zsl ); + +macro_rules! default_imp_for_uas_authentication { + ($($path:ident::$connector:ident),*) => { + $( impl UasAuthentication for $path::$connector {} + impl + ConnectorIntegration< + Authenticate, + UasAuthenticationRequestData, + UasAuthenticationResponseData + > for $path::$connector + {} + )* + }; +} + +default_imp_for_uas_authentication!( + connectors::Airwallex, + connectors::Amazonpay, + connectors::Bambora, + connectors::Bamboraapac, + connectors::Bankofamerica, + connectors::Billwerk, + connectors::Bitpay, + connectors::Bluesnap, + connectors::Boku, + connectors::Cashtocode, + connectors::Chargebee, + connectors::Coinbase, + connectors::Cryptopay, + connectors::CtpMastercard, + connectors::Cybersource, + connectors::Datatrans, + connectors::Deutschebank, + connectors::Digitalvirgo, + connectors::Dlocal, + connectors::Elavon, + connectors::Fiserv, + connectors::Fiservemea, + connectors::Fiuu, + connectors::Forte, + connectors::Globepay, + connectors::Gocardless, + connectors::Helcim, + connectors::Inespay, + connectors::Jpmorgan, + connectors::Nomupay, + connectors::Novalnet, + connectors::Nexinets, + connectors::Nexixpay, + connectors::Payeezy, + connectors::Payu, + connectors::Powertranz, + connectors::Prophetpay, + connectors::Mollie, + connectors::Multisafepay, + connectors::Paybox, + connectors::Placetopay, + connectors::Rapyd, + connectors::Razorpay, + connectors::Redsys, + connectors::Shift4, + connectors::Stax, + connectors::Square, + connectors::Taxjar, + connectors::Thunes, + connectors::Tsys, + connectors::Wellsfargo, + connectors::Worldline, + connectors::Worldpay, + connectors::Volt, + connectors::Xendit, + connectors::Zen, + connectors::Zsl +); diff --git a/crates/hyperswitch_domain_models/src/router_flow_types/unified_authentication_service.rs b/crates/hyperswitch_domain_models/src/router_flow_types/unified_authentication_service.rs index 329c18b741e..ddc56eb2a29 100644 --- a/crates/hyperswitch_domain_models/src/router_flow_types/unified_authentication_service.rs +++ b/crates/hyperswitch_domain_models/src/router_flow_types/unified_authentication_service.rs @@ -3,3 +3,6 @@ pub struct PreAuthenticate; #[derive(Debug, Clone)] pub struct PostAuthenticate; + +#[derive(Debug, Clone)] +pub struct Authenticate; diff --git a/crates/hyperswitch_domain_models/src/router_request_types/unified_authentication_service.rs b/crates/hyperswitch_domain_models/src/router_request_types/unified_authentication_service.rs index af2a940cb51..4238c513f44 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types/unified_authentication_service.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types/unified_authentication_service.rs @@ -1,14 +1,47 @@ +use api_models::payments::DeviceChannel; use masking::Secret; -#[derive(Clone, serde::Deserialize, Debug, serde::Serialize)] +use crate::{address::Address, payment_method_data::PaymentMethodData}; + +#[derive(Clone, Debug)] pub struct UasPreAuthenticationRequestData { pub service_details: Option, pub transaction_details: Option, + pub payment_details: Option, } -#[derive(Clone, serde::Deserialize, Debug, serde::Serialize)] +#[derive(Clone, Debug)] +pub struct UasAuthenticationRequestData { + pub payment_method_data: PaymentMethodData, + pub billing_address: Address, + pub shipping_address: Option
, + pub browser_details: Option, + pub transaction_details: TransactionDetails, + pub pre_authentication_data: super::authentication::PreAuthenticationData, + pub return_url: Option, + pub sdk_information: Option, + pub email: Option, + pub threeds_method_comp_ind: api_models::payments::ThreeDsCompletionIndicator, + pub three_ds_requestor_url: String, + pub webhook_url: String, +} + +#[derive(Clone, Debug)] pub struct CtpServiceDetails { pub service_session_ids: Option, + pub payment_details: Option, +} + +#[derive(Debug, Clone)] +pub struct PaymentDetails { + pub pan: cards::CardNumber, + pub digital_card_id: Option, + pub payment_data_type: Option, + pub encrypted_src_card_details: Option, + pub card_expiry_date: Secret, + pub cardholder_name: Option>, + pub card_token_number: Secret, + pub account_type: Option, } #[derive(Clone, serde::Deserialize, Debug, serde::Serialize)] @@ -20,26 +53,57 @@ pub struct ServiceSessionIds { #[derive(Debug, serde::Serialize, serde::Deserialize, Clone)] pub struct TransactionDetails { - pub amount: common_utils::types::MinorUnit, - pub currency: common_enums::Currency, + pub amount: Option, + pub currency: Option, + pub device_channel: Option, + pub message_category: Option, } #[derive(Clone, Debug)] -pub struct UasPostAuthenticationRequestData {} +pub struct UasPostAuthenticationRequestData { + pub threeds_server_transaction_id: Option, +} #[derive(Debug, Clone)] pub enum UasAuthenticationResponseData { - PreAuthentication {}, + PreAuthentication { + authentication_details: PreAuthenticationDetails, + }, + Authentication { + authentication_details: AuthenticationDetails, + }, PostAuthentication { authentication_details: PostAuthenticationDetails, }, } +#[derive(Debug, Clone)] +pub struct PreAuthenticationDetails { + pub threeds_server_transaction_id: Option, + pub maximum_supported_3ds_version: Option, + pub connector_authentication_id: Option, + pub three_ds_method_data: Option, + pub three_ds_method_url: Option, + pub message_version: Option, + pub connector_metadata: Option, + pub directory_server_id: Option, +} + +#[derive(Debug, Clone)] +pub struct AuthenticationDetails { + pub authn_flow_type: super::authentication::AuthNFlowType, + pub authentication_value: Option, + pub trans_status: common_enums::TransactionStatus, + pub connector_metadata: Option, + pub ds_trans_id: Option, +} + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct PostAuthenticationDetails { pub eci: Option, - pub token_details: TokenDetails, + pub token_details: Option, pub dynamic_data_details: Option, + pub trans_status: Option, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] @@ -53,6 +117,6 @@ pub struct TokenDetails { #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct DynamicData { pub dynamic_data_value: Option>, - pub dynamic_data_type: String, + pub dynamic_data_type: Option, pub ds_trans_id: Option, } diff --git a/crates/hyperswitch_domain_models/src/types.rs b/crates/hyperswitch_domain_models/src/types.rs index 4bbab24edfe..dc64560ead6 100644 --- a/crates/hyperswitch_domain_models/src/types.rs +++ b/crates/hyperswitch_domain_models/src/types.rs @@ -3,15 +3,15 @@ pub use diesel_models::types::OrderDetailsWithAmount; use crate::{ router_data::{AccessToken, RouterData}, router_flow_types::{ - mandate_revoke::MandateRevoke, AccessTokenAuth, Authorize, AuthorizeSessionToken, - CalculateTax, Capture, CompleteAuthorize, CreateConnectorCustomer, Execute, - IncrementalAuthorization, PSync, PaymentMethodToken, PostAuthenticate, PostSessionTokens, - PreAuthenticate, PreProcessing, RSync, Session, SetupMandate, Void, + mandate_revoke::MandateRevoke, AccessTokenAuth, Authenticate, Authorize, + AuthorizeSessionToken, CalculateTax, Capture, CompleteAuthorize, CreateConnectorCustomer, + Execute, IncrementalAuthorization, PSync, PaymentMethodToken, PostAuthenticate, + PostSessionTokens, PreAuthenticate, PreProcessing, RSync, Session, SetupMandate, Void, }, router_request_types::{ unified_authentication_service::{ - UasAuthenticationResponseData, UasPostAuthenticationRequestData, - UasPreAuthenticationRequestData, + UasAuthenticationRequestData, UasAuthenticationResponseData, + UasPostAuthenticationRequestData, UasPreAuthenticationRequestData, }, AccessTokenRequestData, AuthorizeSessionTokenData, CompleteAuthorizeData, ConnectorCustomerData, MandateRevokeRequestData, PaymentMethodTokenizationData, @@ -57,7 +57,6 @@ pub type PaymentsSessionRouterData = RouterData; - pub type UasPreAuthenticationRouterData = RouterData; @@ -71,3 +70,6 @@ pub type PaymentsIncrementalAuthorizationRouterData = RouterData< #[cfg(feature = "payouts")] pub type PayoutsRouterData = RouterData; + +pub type UasAuthenticationRouterData = + RouterData; diff --git a/crates/hyperswitch_interfaces/src/api.rs b/crates/hyperswitch_interfaces/src/api.rs index ca2d806762a..ab918b5334b 100644 --- a/crates/hyperswitch_interfaces/src/api.rs +++ b/crates/hyperswitch_interfaces/src/api.rs @@ -34,13 +34,13 @@ use hyperswitch_domain_models::{ UasFlowData, }, router_flow_types::{ - mandate_revoke::MandateRevoke, AccessTokenAuth, PostAuthenticate, PreAuthenticate, - VerifyWebhookSource, + mandate_revoke::MandateRevoke, AccessTokenAuth, Authenticate, PostAuthenticate, + PreAuthenticate, VerifyWebhookSource, }, router_request_types::{ unified_authentication_service::{ - UasAuthenticationResponseData, UasPostAuthenticationRequestData, - UasPreAuthenticationRequestData, + UasAuthenticationRequestData, UasAuthenticationResponseData, + UasPostAuthenticationRequestData, UasPreAuthenticationRequestData, }, AccessTokenRequestData, MandateRevokeRequestData, VerifyWebhookSourceRequestData, }, @@ -371,7 +371,7 @@ pub trait ConnectorVerifyWebhookSourceV2: /// trait UnifiedAuthenticationService pub trait UnifiedAuthenticationService: - ConnectorCommon + UasPreAuthentication + UasPostAuthentication + ConnectorCommon + UasPreAuthentication + UasPostAuthentication + UasAuthentication { } @@ -395,9 +395,15 @@ pub trait UasPostAuthentication: { } +/// trait UasAuthentication +pub trait UasAuthentication: + ConnectorIntegration +{ +} + /// trait UnifiedAuthenticationServiceV2 pub trait UnifiedAuthenticationServiceV2: - ConnectorCommon + UasPreAuthenticationV2 + UasPostAuthenticationV2 + ConnectorCommon + UasPreAuthenticationV2 + UasPostAuthenticationV2 + UasAuthenticationV2 { } @@ -423,6 +429,17 @@ pub trait UasPostAuthenticationV2: { } +/// trait UasAuthenticationV2 +pub trait UasAuthenticationV2: + ConnectorIntegrationV2< + Authenticate, + UasFlowData, + UasAuthenticationRequestData, + UasAuthenticationResponseData, +> +{ +} + /// trait ConnectorValidation pub trait ConnectorValidation: ConnectorCommon + ConnectorSpecifications { /// Validate, the payment request against the connector supported features diff --git a/crates/hyperswitch_interfaces/src/types.rs b/crates/hyperswitch_interfaces/src/types.rs index eb31fd221d6..a047e7f00d7 100644 --- a/crates/hyperswitch_interfaces/src/types.rs +++ b/crates/hyperswitch_interfaces/src/types.rs @@ -14,13 +14,13 @@ use hyperswitch_domain_models::{ Session, SetupMandate, Void, }, refunds::{Execute, RSync}, - unified_authentication_service::{PostAuthenticate, PreAuthenticate}, + unified_authentication_service::{Authenticate, PostAuthenticate, PreAuthenticate}, webhooks::VerifyWebhookSource, }, router_request_types::{ unified_authentication_service::{ - UasAuthenticationResponseData, UasPostAuthenticationRequestData, - UasPreAuthenticationRequestData, + UasAuthenticationRequestData, UasAuthenticationResponseData, + UasPostAuthenticationRequestData, UasPreAuthenticationRequestData, }, AcceptDisputeRequestData, AccessTokenRequestData, AuthorizeSessionTokenData, CompleteAuthorizeData, ConnectorCustomerData, DefendDisputeRequestData, @@ -205,3 +205,10 @@ pub type UasPostAuthenticationType = dyn ConnectorIntegration< UasPostAuthenticationRequestData, UasAuthenticationResponseData, >; + +/// Type alias for `ConnectorIntegration` +pub type UasAuthenticationType = dyn ConnectorIntegration< + Authenticate, + UasAuthenticationRequestData, + UasAuthenticationResponseData, +>; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index e6cd8409343..e991593b4b5 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -393,6 +393,7 @@ where &connector_details, &business_profile, &key_store, + mandate_type, ) .await?; } else { @@ -6407,12 +6408,17 @@ pub async fn payment_external_authentication( #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all)] -pub async fn payment_external_authentication( +pub async fn payment_external_authentication( state: SessionState, merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: api_models::payments::PaymentsExternalAuthenticationRequest, ) -> RouterResponse { + use super::unified_authentication_service::types::ExternalAuthentication; + use crate::core::unified_authentication_service::{ + types::UnifiedAuthenticationService, utils::external_authentication_update_trackers, + }; + let db = &*state.store; let key_manager_state = &(&state).into(); @@ -6584,35 +6590,78 @@ pub async fn payment_external_authentication( .get_required_value("authentication_connector_details") .attach_printable("authentication_connector_details not configured by the merchant")?; - let authentication_response = Box::pin(authentication_core::perform_authentication( - &state, - business_profile.merchant_id, - authentication_connector, - payment_method_details.0, - payment_method_details.1, - billing_address - .as_ref() - .map(|address| address.into()) - .ok_or(errors::ApiErrorResponse::MissingRequiredField { - field_name: "billing_address", - })?, - shipping_address.as_ref().map(|address| address.into()), - browser_info, - merchant_connector_account, - Some(amount), - Some(currency), - authentication::MessageCategory::Payment, - req.device_channel, - authentication, - return_url, - req.sdk_information, - req.threeds_method_comp_ind, - optional_customer.and_then(|customer| customer.email.map(pii::Email::from)), - webhook_url, - authentication_details.three_ds_requestor_url.clone(), - payment_intent.psd2_sca_exemption_type, - )) - .await?; + let authentication_response = + if helpers::is_merchant_eligible_authentication_service(merchant_account.get_id(), &state) + .await? + { + let auth_response = + >::authentication( + &state, + &business_profile, + payment_method_details.1, + payment_method_details.0, + billing_address + .as_ref() + .map(|address| address.into()) + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "billing_address", + })?, + shipping_address.as_ref().map(|address| address.into()), + browser_info, + Some(amount), + Some(currency), + authentication::MessageCategory::Payment, + req.device_channel, + authentication.clone(), + return_url, + req.sdk_information, + req.threeds_method_comp_ind, + optional_customer.and_then(|customer| customer.email.map(pii::Email::from)), + webhook_url, + authentication_details.three_ds_requestor_url.clone(), + &merchant_connector_account, + &authentication_connector, + ) + .await?; + let authentication = external_authentication_update_trackers( + &state, + auth_response, + authentication.clone(), + None, + ) + .await?; + authentication::AuthenticationResponse::try_from(authentication)? + } else { + Box::pin(authentication_core::perform_authentication( + &state, + business_profile.merchant_id, + authentication_connector, + payment_method_details.0, + payment_method_details.1, + billing_address + .as_ref() + .map(|address| address.into()) + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "billing_address", + })?, + shipping_address.as_ref().map(|address| address.into()), + browser_info, + merchant_connector_account, + Some(amount), + Some(currency), + authentication::MessageCategory::Payment, + req.device_channel, + authentication, + return_url, + req.sdk_information, + req.threeds_method_comp_ind, + optional_customer.and_then(|customer| customer.email.map(pii::Email::from)), + webhook_url, + authentication_details.three_ds_requestor_url.clone(), + payment_intent.psd2_sca_exemption_type, + )) + .await? + }; Ok(services::ApplicationResponse::Json( api_models::payments::PaymentsExternalAuthenticationResponse { transaction_status: authentication_response.trans_status, diff --git a/crates/router/src/core/payments/connector_integration_v2_impls.rs b/crates/router/src/core/payments/connector_integration_v2_impls.rs index 03b1ad0340b..5263ec9fb28 100644 --- a/crates/router/src/core/payments/connector_integration_v2_impls.rs +++ b/crates/router/src/core/payments/connector_integration_v2_impls.rs @@ -1,6 +1,9 @@ -use hyperswitch_domain_models::router_flow_types::{PostAuthenticate, PreAuthenticate}; +use hyperswitch_domain_models::router_flow_types::{ + Authenticate, PostAuthenticate, PreAuthenticate, +}; use hyperswitch_interfaces::api::{ - UasPostAuthenticationV2, UasPreAuthenticationV2, UnifiedAuthenticationServiceV2, + UasAuthenticationV2, UasPostAuthenticationV2, UasPreAuthenticationV2, + UnifiedAuthenticationServiceV2, }; #[cfg(feature = "frm")] @@ -2076,6 +2079,7 @@ macro_rules! default_imp_for_new_connector_integration_uas { $( impl UnifiedAuthenticationServiceV2 for $path::$connector {} impl UasPreAuthenticationV2 for $path::$connector {} impl UasPostAuthenticationV2 for $path::$connector {} + impl UasAuthenticationV2 for $path::$connector {} impl services::ConnectorIntegrationV2< PreAuthenticate, @@ -2092,6 +2096,14 @@ macro_rules! default_imp_for_new_connector_integration_uas { types::UasAuthenticationResponseData, > for $path::$connector {} + impl + services::ConnectorIntegrationV2< + Authenticate, + types::UasFlowData, + types::UasAuthenticationRequestData, + types::UasAuthenticationResponseData, + > for $path::$connector + {} )* }; } diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 459d8a50a37..0c80c5a70e2 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -14,11 +14,12 @@ pub mod setup_mandate_flow; use async_trait::async_trait; use hyperswitch_domain_models::{ mandates::CustomerAcceptance, - router_flow_types::{PostAuthenticate, PreAuthenticate}, + router_flow_types::{Authenticate, PostAuthenticate, PreAuthenticate}, router_request_types::PaymentsCaptureData, }; use hyperswitch_interfaces::api::{ - payouts::Payouts, UasPostAuthentication, UasPreAuthentication, UnifiedAuthenticationService, + payouts::Payouts, UasAuthentication, UasPostAuthentication, UasPreAuthentication, + UnifiedAuthenticationService, }; #[cfg(feature = "frm")] @@ -2529,6 +2530,64 @@ default_imp_for_uas_post_authentication!( connector::Wellsfargopayout, connector::Wise ); + +macro_rules! default_imp_for_uas_authentication { + ($($path:ident::$connector:ident),*) => { + $( impl UasAuthentication for $path::$connector {} + impl + services::ConnectorIntegration< + Authenticate, + types::UasAuthenticationRequestData, + types::UasAuthenticationResponseData + > for $path::$connector + {} + )* + }; +} +#[cfg(feature = "dummy_connector")] +impl UasAuthentication for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + Authenticate, + types::UasAuthenticationRequestData, + types::UasAuthenticationResponseData, + > for connector::DummyConnector +{ +} + +default_imp_for_uas_authentication!( + connector::Adyenplatform, + connector::Aci, + connector::Adyen, + connector::Authorizedotnet, + connector::Braintree, + connector::Checkout, + connector::Ebanx, + connector::Globalpay, + connector::Gpayments, + connector::Iatapay, + connector::Itaubank, + connector::Klarna, + connector::Mifinity, + connector::Netcetera, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payme, + connector::Payone, + connector::Paypal, + connector::Plaid, + connector::Riskified, + connector::Signifyd, + connector::Stripe, + connector::Threedsecureio, + connector::Trustpay, + connector::Wellsfargopayout, + connector::Wise +); /// Determines whether a capture API call should be made for a payment attempt /// This function evaluates whether an authorized payment should proceed with a capture API call /// based on various payment parameters. It's primarily used in two-step (auth + capture) payment flows for CaptureMethod SequentialAutomatic diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 797aadbbc05..6e85db98430 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -5971,6 +5971,73 @@ pub fn validate_mandate_data_and_future_usage( } } +#[derive(Debug, Clone)] +pub enum UnifiedAuthenticationServiceFlow { + ClickToPayInitiate, + ExternalAuthenticationInitiate { + acquirer_details: authentication::types::AcquirerDetails, + card_number: ::cards::CardNumber, + token: String, + }, + ExternalAuthenticationPostAuthenticate { + authentication_id: String, + }, +} + +#[cfg(feature = "v1")] +pub async fn decide_action_for_unified_authentication_service( + state: &SessionState, + key_store: &domain::MerchantKeyStore, + business_profile: &domain::Profile, + payment_data: &mut PaymentData, + connector_call_type: &api::ConnectorCallType, + mandate_type: Option, +) -> RouterResult> { + let external_authentication_flow = get_payment_external_authentication_flow_during_confirm( + state, + key_store, + business_profile, + payment_data, + connector_call_type, + mandate_type, + ) + .await?; + Ok(match external_authentication_flow { + Some(PaymentExternalAuthenticationFlow::PreAuthenticationFlow { + acquirer_details, + card_number, + token, + }) => Some( + UnifiedAuthenticationServiceFlow::ExternalAuthenticationInitiate { + acquirer_details, + card_number, + token, + }, + ), + Some(PaymentExternalAuthenticationFlow::PostAuthenticationFlow { authentication_id }) => { + Some( + UnifiedAuthenticationServiceFlow::ExternalAuthenticationPostAuthenticate { + authentication_id, + }, + ) + } + None => { + if let Some(payment_method) = payment_data.payment_attempt.payment_method { + if payment_method == storage_enums::PaymentMethod::Card + && business_profile.is_click_to_pay_enabled + && payment_data.service_details.is_some() + { + Some(UnifiedAuthenticationServiceFlow::ClickToPayInitiate) + } else { + None + } + } else { + None + } + } + }) +} + pub enum PaymentExternalAuthenticationFlow { PreAuthenticationFlow { acquirer_details: authentication::types::AcquirerDetails, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 0a264f24e93..911530ba8eb 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -309,6 +309,7 @@ pub trait Domain: Send + Sync { _connector_call_type: &ConnectorCallType, _merchant_account: &domain::Profile, _key_store: &domain::MerchantKeyStore, + _mandate_type: Option, ) -> CustomResult<(), errors::ApiErrorResponse> { Ok(()) } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index da68ee27fd9..edcd798ba99 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -1042,47 +1042,55 @@ impl Domain> for &'a self, state: &SessionState, payment_data: &mut PaymentData, - _should_continue_confirm_transaction: &mut bool, - _connector_call_type: &ConnectorCallType, + should_continue_confirm_transaction: &mut bool, + connector_call_type: &ConnectorCallType, business_profile: &domain::Profile, key_store: &domain::MerchantKeyStore, + mandate_type: Option, ) -> CustomResult<(), errors::ApiErrorResponse> { - let authentication_product_ids = business_profile + let unified_authentication_service_flow = + helpers::decide_action_for_unified_authentication_service( + state, + key_store, + business_profile, + payment_data, + connector_call_type, + mandate_type, + ) + .await?; + if let Some(unified_authentication_service_flow) = unified_authentication_service_flow { + match unified_authentication_service_flow { + helpers::UnifiedAuthenticationServiceFlow::ClickToPayInitiate => { + let authentication_product_ids = business_profile .authentication_product_ids .clone() .ok_or(errors::ApiErrorResponse::PreconditionFailed { message: "authentication_product_ids is not configured in business profile" .to_string(), })?; + let click_to_pay_mca_id = authentication_product_ids + .get_click_to_pay_connector_account_id() + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "authentication_product_ids", + })?; - if let Some(payment_method) = payment_data.payment_attempt.payment_method { - if payment_method == storage_enums::PaymentMethod::Card - && business_profile.is_click_to_pay_enabled - && payment_data.service_details.is_some() - { - let click_to_pay_mca_id = authentication_product_ids - .get_click_to_pay_connector_account_id() - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "authentication_product_ids", - })?; - - let key_manager_state = &(state).into(); - let merchant_id = &business_profile.merchant_id; + let key_manager_state = &(state).into(); + let merchant_id = &business_profile.merchant_id; - let connector_mca = state - .store - .find_by_merchant_connector_account_merchant_id_merchant_connector_id( - key_manager_state, - merchant_id, - &click_to_pay_mca_id, - key_store, - ) - .await - .to_not_found_response( - errors::ApiErrorResponse::MerchantConnectorAccountNotFound { - id: click_to_pay_mca_id.get_string_repr().to_string(), - }, - )?; + let connector_mca = state + .store + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + key_manager_state, + merchant_id, + &click_to_pay_mca_id, + key_store, + ) + .await + .to_not_found_response( + errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: click_to_pay_mca_id.get_string_repr().to_string(), + }, + )?; let authentication_id = common_utils::generate_id_with_default_len(consts::AUTHENTICATION_ID_PREFIX); @@ -1098,7 +1106,7 @@ impl Domain> for key_store, business_profile, payment_data, - &connector_mca, + &helpers::MerchantConnectorAccountType::DbVal(Box::new(connector_mca.clone())), &connector_mca.connector_name, &authentication_id, payment_method, @@ -1112,9 +1120,10 @@ impl Domain> for key_store, business_profile, payment_data, - &connector_mca, + &helpers::MerchantConnectorAccountType::DbVal(Box::new(connector_mca.clone())), &connector_mca.connector_name, payment_method, + None, ) .await?; @@ -1122,14 +1131,14 @@ impl Domain> for Ok(unified_authentication_service::UasAuthenticationResponseData::PostAuthentication { authentication_details, }) => { + let token_details = authentication_details.token_details.ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Missing authentication_details.token_details")?; (Some( hyperswitch_domain_models::payment_method_data::NetworkTokenData { - token_number: authentication_details.token_details.payment_token, - token_exp_month: authentication_details - .token_details + token_number: token_details.payment_token, + token_exp_month: token_details .token_expiration_month, - token_exp_year: authentication_details - .token_details + token_exp_year: token_details .token_expiration_year, token_cryptogram: authentication_details .dynamic_data_details @@ -1143,8 +1152,10 @@ impl Domain> for eci: authentication_details.eci, }),common_enums::AuthenticationStatus::Success) }, - Ok(unified_authentication_service::UasAuthenticationResponseData::PreAuthentication {}) => (None, common_enums::AuthenticationStatus::Started), - Err(_) => (None, common_enums::AuthenticationStatus::Failed) + Ok(unified_authentication_service::UasAuthenticationResponseData::Authentication { .. }) + | Ok(unified_authentication_service::UasAuthenticationResponseData::PreAuthentication { .. }) + => Err(errors::ApiErrorResponse::InternalServerError).attach_printable("unexpected response received")?, + Err(_) => (None, common_enums::AuthenticationStatus::Failed), }; payment_data.payment_attempt.payment_method = @@ -1166,14 +1177,127 @@ impl Domain> for authentication_status, ) .await?; - } - } - logger::info!( - payment_method=?payment_data.payment_attempt.payment_method, - click_to_pay_enabled=?business_profile.is_click_to_pay_enabled, - "skipping unified authentication service call since payment conditions are not satisfied" - ); + }, + helpers::UnifiedAuthenticationServiceFlow::ExternalAuthenticationInitiate { + acquirer_details, + token, + .. + } => { + let (authentication_connector, three_ds_connector_account) = + authentication::utils::get_authentication_connector_data(state, key_store, business_profile).await?; + let authentication_connector_name = authentication_connector.to_string(); + let authentication = authentication::utils::create_new_authentication( + state, + business_profile.merchant_id.clone(), + authentication_connector_name.clone(), + token, + business_profile.get_id().to_owned(), + Some(payment_data.payment_intent.payment_id.clone()), + three_ds_connector_account + .get_mca_id() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while finding mca_id from merchant_connector_account")?, + ) + .await?; + + let pre_auth_response = uas_utils::types::ExternalAuthentication::pre_authentication( + state, + key_store, + business_profile, + payment_data, + &three_ds_connector_account, + &authentication_connector_name, + &authentication.authentication_id, + payment_data.payment_attempt.payment_method.ok_or( + errors::ApiErrorResponse::InternalServerError + ).attach_printable("payment_method not found in payment_attempt")?, + ).await?; + let updated_authentication = uas_utils::utils::external_authentication_update_trackers( + state, + pre_auth_response, + authentication.clone(), + Some(acquirer_details), + ).await?; + payment_data.authentication = Some(updated_authentication.clone()); + if updated_authentication.is_separate_authn_required() + || updated_authentication.authentication_status.is_failed() + { + *should_continue_confirm_transaction = false; + let default_poll_config = types::PollConfig::default(); + let default_config_str = default_poll_config + .encode_to_string_of_json() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while stringifying default poll config")?; + let poll_config = state + .store + .find_config_by_key_unwrap_or( + &types::PollConfig::get_poll_config_key( + updated_authentication.authentication_connector.clone(), + ), + Some(default_config_str), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("The poll config was not found in the DB")?; + let poll_config: types::PollConfig = poll_config + .config + .parse_struct("PollConfig") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while parsing PollConfig")?; + payment_data.poll_config = Some(poll_config) + } + }, + helpers::UnifiedAuthenticationServiceFlow::ExternalAuthenticationPostAuthenticate {authentication_id} => { + let (authentication_connector, three_ds_connector_account) = + authentication::utils::get_authentication_connector_data(state, key_store, business_profile).await?; + let is_pull_mechanism_enabled = + utils::check_if_pull_mechanism_for_external_3ds_enabled_from_connector_metadata( + three_ds_connector_account + .get_metadata() + .map(|metadata| metadata.expose()), + ); + let authentication = state + .store + .find_authentication_by_merchant_id_authentication_id( + &business_profile.merchant_id, + authentication_id.clone(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable_lazy(|| format!("Error while fetching authentication record with authentication_id {authentication_id}"))?; + let updated_authentication = if !authentication.authentication_status.is_terminal_status() && is_pull_mechanism_enabled { + let post_auth_response = uas_utils::types::ExternalAuthentication::post_authentication( + state, + key_store, + business_profile, + payment_data, + &three_ds_connector_account, + &authentication_connector.to_string(), + payment_data.payment_attempt.payment_method.ok_or( + errors::ApiErrorResponse::InternalServerError + ).attach_printable("payment_method not found in payment_attempt")?, + Some(authentication.clone()), + ).await?; + uas_utils::utils::external_authentication_update_trackers( + state, + post_auth_response, + authentication, + None, + ).await? + } else { + authentication + }; + payment_data.authentication = Some(updated_authentication.clone()); + //If authentication is not successful, skip the payment connector flows and mark the payment as failure + if updated_authentication.authentication_status + != api_models::enums::AuthenticationStatus::Success + { + *should_continue_confirm_transaction = false; + } + }, + } + } Ok(()) } diff --git a/crates/router/src/core/unified_authentication_service.rs b/crates/router/src/core/unified_authentication_service.rs index 6e47dcac032..d9c36aeaa3d 100644 --- a/crates/router/src/core/unified_authentication_service.rs +++ b/crates/router/src/core/unified_authentication_service.rs @@ -1,48 +1,97 @@ -pub mod transformers; pub mod types; pub mod utils; -use api_models::payments::CtpServiceDetails; +use api_models::payments; use diesel_models::authentication::{Authentication, AuthenticationNew}; use error_stack::ResultExt; use hyperswitch_domain_models::{ errors::api_error_response::ApiErrorResponse, - router_request_types::unified_authentication_service::{ - UasPostAuthenticationRequestData, UasPreAuthenticationRequestData, + payment_method_data, + router_request_types::{ + authentication::{MessageCategory, PreAuthenticationData}, + unified_authentication_service::{ + PaymentDetails, ServiceSessionIds, TransactionDetails, UasAuthenticationRequestData, + UasPostAuthenticationRequestData, UasPreAuthenticationRequestData, + }, + BrowserInformation, + }, + types::{ + UasAuthenticationRouterData, UasPostAuthenticationRouterData, + UasPreAuthenticationRouterData, }, }; -use super::errors::RouterResult; +use super::{errors::RouterResult, payments::helpers::MerchantConnectorAccountType}; use crate::{ core::{ errors::utils::StorageErrorExt, payments::PaymentData, unified_authentication_service::types::{ - ClickToPay, UnifiedAuthenticationService, UNIFIED_AUTHENTICATION_SERVICE, + ClickToPay, ExternalAuthentication, UnifiedAuthenticationService, + UNIFIED_AUTHENTICATION_SERVICE, }, }, db::domain, routes::SessionState, - types::domain::MerchantConnectorAccount, }; #[cfg(feature = "v1")] #[async_trait::async_trait] impl UnifiedAuthenticationService for ClickToPay { + fn get_pre_authentication_request_data( + payment_data: &PaymentData, + ) -> RouterResult { + let service_details = hyperswitch_domain_models::router_request_types::unified_authentication_service::CtpServiceDetails { + service_session_ids: Some(ServiceSessionIds { + merchant_transaction_id: payment_data + .service_details + .as_ref() + .and_then(|details| details.merchant_transaction_id.clone()), + correlation_id: payment_data + .service_details + .as_ref() + .and_then(|details| details.correlation_id.clone()), + x_src_flow_id: payment_data + .service_details + .as_ref() + .and_then(|details| details.x_src_flow_id.clone()), + }), + payment_details: None, + }; + let currency = payment_data.payment_attempt.currency.ok_or( + ApiErrorResponse::MissingRequiredField { + field_name: "currency", + }, + )?; + + let amount = payment_data.payment_attempt.net_amount.get_order_amount(); + let transaction_details = TransactionDetails { + amount: Some(amount), + currency: Some(currency), + device_channel: None, + message_category: None, + }; + + Ok(UasPreAuthenticationRequestData { + service_details: Some(service_details), + transaction_details: Some(transaction_details), + payment_details: None, + }) + } + async fn pre_authentication( state: &SessionState, _key_store: &domain::MerchantKeyStore, _business_profile: &domain::Profile, payment_data: &PaymentData, - merchant_connector_account: &MerchantConnectorAccount, + merchant_connector_account: &MerchantConnectorAccountType, connector_name: &str, authentication_id: &str, payment_method: common_enums::PaymentMethod, - ) -> RouterResult<()> { - let pre_authentication_data = - UasPreAuthenticationRequestData::try_from(payment_data.clone())?; + ) -> RouterResult { + let pre_authentication_data = Self::get_pre_authentication_request_data(payment_data)?; - let pre_auth_router_data: hyperswitch_domain_models::types::UasPreAuthenticationRouterData = + let pre_auth_router_data: UasPreAuthenticationRouterData = utils::construct_uas_router_data( state, connector_name.to_string(), @@ -59,9 +108,7 @@ impl UnifiedAuthenticationService for ClickToPay { UNIFIED_AUTHENTICATION_SERVICE.to_string(), pre_auth_router_data, ) - .await?; - - Ok(()) + .await } async fn post_authentication( @@ -69,10 +116,11 @@ impl UnifiedAuthenticationService for ClickToPay { _key_store: &domain::MerchantKeyStore, _business_profile: &domain::Profile, payment_data: &PaymentData, - merchant_connector_account: &MerchantConnectorAccount, + merchant_connector_account: &MerchantConnectorAccountType, connector_name: &str, payment_method: common_enums::PaymentMethod, - ) -> RouterResult { + _authentication: Option, + ) -> RouterResult { let authentication_id = payment_data .payment_attempt .authentication_id @@ -80,34 +128,272 @@ impl UnifiedAuthenticationService for ClickToPay { .ok_or(ApiErrorResponse::InternalServerError) .attach_printable("Missing authentication id in payment attempt")?; - let post_authentication_data = UasPostAuthenticationRequestData {}; + let post_authentication_data = UasPostAuthenticationRequestData { + threeds_server_transaction_id: None, + }; - let post_auth_router_data: hyperswitch_domain_models::types::UasPostAuthenticationRouterData = utils::construct_uas_router_data( + let post_auth_router_data: UasPostAuthenticationRouterData = + utils::construct_uas_router_data( + state, + connector_name.to_string(), + payment_method, + payment_data.payment_attempt.merchant_id.clone(), + None, + post_authentication_data, + merchant_connector_account, + Some(authentication_id.clone()), + )?; + + utils::do_auth_connector_call( + state, + UNIFIED_AUTHENTICATION_SERVICE.to_string(), + post_auth_router_data, + ) + .await + } + + fn confirmation( + _state: &SessionState, + _key_store: &domain::MerchantKeyStore, + _business_profile: &domain::Profile, + _merchant_connector_account: &MerchantConnectorAccountType, + ) -> RouterResult<()> { + Ok(()) + } +} + +#[cfg(feature = "v1")] +#[async_trait::async_trait] +impl UnifiedAuthenticationService for ExternalAuthentication { + fn get_pre_authentication_request_data( + payment_data: &PaymentData, + ) -> RouterResult { + let payment_method_data = payment_data + .payment_method_data + .as_ref() + .ok_or(ApiErrorResponse::InternalServerError) + .attach_printable("payment_data.payment_method_data is missing")?; + let payment_details = + if let payment_method_data::PaymentMethodData::Card(card) = payment_method_data { + Some(PaymentDetails { + pan: card.card_number.clone(), + digital_card_id: None, + payment_data_type: None, + encrypted_src_card_details: None, + card_expiry_date: card.card_exp_year.clone(), + cardholder_name: card.card_holder_name.clone(), + card_token_number: card.card_cvc.clone(), + account_type: card.card_network.clone(), + }) + } else { + None + }; + Ok(UasPreAuthenticationRequestData { + service_details: None, + transaction_details: None, + payment_details, + }) + } + + #[allow(clippy::too_many_arguments)] + async fn pre_authentication( + state: &SessionState, + _key_store: &domain::MerchantKeyStore, + _business_profile: &domain::Profile, + payment_data: &PaymentData, + merchant_connector_account: &MerchantConnectorAccountType, + connector_name: &str, + authentication_id: &str, + payment_method: common_enums::PaymentMethod, + ) -> RouterResult { + let pre_authentication_data = Self::get_pre_authentication_request_data(payment_data)?; + + let pre_auth_router_data: UasPreAuthenticationRouterData = + utils::construct_uas_router_data( + state, + connector_name.to_string(), + payment_method, + payment_data.payment_attempt.merchant_id.clone(), + None, + pre_authentication_data, + merchant_connector_account, + Some(authentication_id.to_owned()), + )?; + + utils::do_auth_connector_call( + state, + UNIFIED_AUTHENTICATION_SERVICE.to_string(), + pre_auth_router_data, + ) + .await + } + + fn get_authentication_request_data( + payment_method_data: domain::PaymentMethodData, + billing_address: hyperswitch_domain_models::address::Address, + shipping_address: Option, + browser_details: Option, + amount: Option, + currency: Option, + message_category: MessageCategory, + device_channel: payments::DeviceChannel, + authentication: Authentication, + return_url: Option, + sdk_information: Option, + threeds_method_comp_ind: payments::ThreeDsCompletionIndicator, + email: Option, + webhook_url: String, + three_ds_requestor_url: String, + ) -> RouterResult { + Ok(UasAuthenticationRequestData { + payment_method_data, + billing_address, + shipping_address, + browser_details, + transaction_details: TransactionDetails { + amount, + currency, + device_channel: Some(device_channel), + message_category: Some(message_category), + }, + pre_authentication_data: PreAuthenticationData { + threeds_server_transaction_id: authentication.threeds_server_transaction_id.ok_or( + ApiErrorResponse::MissingRequiredField { + field_name: "authentication.threeds_server_transaction_id", + }, + )?, + message_version: authentication.message_version.ok_or( + ApiErrorResponse::MissingRequiredField { + field_name: "authentication.message_version", + }, + )?, + acquirer_bin: authentication.acquirer_bin, + acquirer_merchant_id: authentication.acquirer_merchant_id, + acquirer_country_code: authentication.acquirer_country_code, + connector_metadata: authentication.connector_metadata, + }, + return_url, + sdk_information, + email, + threeds_method_comp_ind, + three_ds_requestor_url, + webhook_url, + }) + } + + #[allow(clippy::too_many_arguments)] + async fn authentication( + state: &SessionState, + business_profile: &domain::Profile, + payment_method: common_enums::PaymentMethod, + payment_method_data: domain::PaymentMethodData, + billing_address: hyperswitch_domain_models::address::Address, + shipping_address: Option, + browser_details: Option, + amount: Option, + currency: Option, + message_category: MessageCategory, + device_channel: payments::DeviceChannel, + authentication: Authentication, + return_url: Option, + sdk_information: Option, + threeds_method_comp_ind: payments::ThreeDsCompletionIndicator, + email: Option, + webhook_url: String, + three_ds_requestor_url: String, + merchant_connector_account: &MerchantConnectorAccountType, + connector_name: &str, + ) -> RouterResult { + let authentication_data = + >::get_authentication_request_data( + payment_method_data, + billing_address, + shipping_address, + browser_details, + amount, + currency, + message_category, + device_channel, + authentication.clone(), + return_url, + sdk_information, + threeds_method_comp_ind, + email, + webhook_url, + three_ds_requestor_url, + )?; + let auth_router_data: UasAuthenticationRouterData = utils::construct_uas_router_data( state, connector_name.to_string(), payment_method, - payment_data.payment_attempt.merchant_id.clone(), + business_profile.merchant_id.clone(), None, - post_authentication_data, + authentication_data, merchant_connector_account, - Some(authentication_id.clone()), + Some(authentication.authentication_id.to_owned()), )?; - let response = utils::do_auth_connector_call( + Box::pin(utils::do_auth_connector_call( state, UNIFIED_AUTHENTICATION_SERVICE.to_string(), - post_auth_router_data, - ) - .await?; + auth_router_data, + )) + .await + } + + fn get_post_authentication_request_data( + authentication: Option, + ) -> RouterResult { + Ok(UasPostAuthenticationRequestData { + // authentication.threeds_server_transaction_id is mandatory for post-authentication in ExternalAuthentication + threeds_server_transaction_id: Some( + authentication + .and_then(|auth| auth.threeds_server_transaction_id) + .ok_or(ApiErrorResponse::MissingRequiredField { + field_name: "authentication.threeds_server_transaction_id", + })?, + ), + }) + } + + async fn post_authentication( + state: &SessionState, + _key_store: &domain::MerchantKeyStore, + business_profile: &domain::Profile, + _payment_data: &PaymentData, + merchant_connector_account: &MerchantConnectorAccountType, + connector_name: &str, + payment_method: common_enums::PaymentMethod, + authentication: Option, + ) -> RouterResult { + let authentication_data = + >::get_post_authentication_request_data( + authentication.clone(), + )?; + let auth_router_data: UasPostAuthenticationRouterData = utils::construct_uas_router_data( + state, + connector_name.to_string(), + payment_method, + business_profile.merchant_id.clone(), + None, + authentication_data, + merchant_connector_account, + authentication.map(|auth| auth.authentication_id), + )?; - Ok(response) + utils::do_auth_connector_call( + state, + UNIFIED_AUTHENTICATION_SERVICE.to_string(), + auth_router_data, + ) + .await } fn confirmation( _state: &SessionState, _key_store: &domain::MerchantKeyStore, _business_profile: &domain::Profile, - _merchant_connector_account: &MerchantConnectorAccount, + _merchant_connector_account: &MerchantConnectorAccountType, ) -> RouterResult<()> { Ok(()) } @@ -122,7 +408,7 @@ pub async fn create_new_authentication( payment_id: Option, merchant_connector_id: common_utils::id_type::MerchantConnectorAccountId, authentication_id: &str, - service_details: Option, + service_details: Option, authentication_status: common_enums::AuthenticationStatus, ) -> RouterResult { let service_details_value = service_details diff --git a/crates/router/src/core/unified_authentication_service/transformers.rs b/crates/router/src/core/unified_authentication_service/transformers.rs deleted file mode 100644 index 36b4ad7e420..00000000000 --- a/crates/router/src/core/unified_authentication_service/transformers.rs +++ /dev/null @@ -1,45 +0,0 @@ -use error_stack::Report; -use hyperswitch_domain_models::{ - errors::api_error_response::ApiErrorResponse, - router_request_types::unified_authentication_service::{ - CtpServiceDetails, ServiceSessionIds, TransactionDetails, UasPreAuthenticationRequestData, - }, -}; - -use crate::core::payments::PaymentData; - -#[cfg(feature = "v1")] -impl TryFrom> for UasPreAuthenticationRequestData { - type Error = Report; - fn try_from(payment_data: PaymentData) -> Result { - let service_details = CtpServiceDetails { - service_session_ids: Some(ServiceSessionIds { - merchant_transaction_id: payment_data - .service_details - .as_ref() - .and_then(|details| details.merchant_transaction_id.clone()), - correlation_id: payment_data - .service_details - .as_ref() - .and_then(|details| details.correlation_id.clone()), - x_src_flow_id: payment_data - .service_details - .as_ref() - .and_then(|details| details.x_src_flow_id.clone()), - }), - }; - let currency = payment_data.payment_attempt.currency.ok_or( - ApiErrorResponse::MissingRequiredField { - field_name: "currency", - }, - )?; - - let amount = payment_data.payment_attempt.net_amount.get_order_amount(); - let transaction_details = TransactionDetails { amount, currency }; - - Ok(Self { - service_details: Some(service_details), - transaction_details: Some(transaction_details), - }) - } -} diff --git a/crates/router/src/core/unified_authentication_service/types.rs b/crates/router/src/core/unified_authentication_service/types.rs index a400f62ffc3..bb9abdb1219 100644 --- a/crates/router/src/core/unified_authentication_service/types.rs +++ b/crates/router/src/core/unified_authentication_service/types.rs @@ -1,8 +1,23 @@ +use api_models::payments; +use hyperswitch_domain_models::{ + errors::api_error_response::{self as errors, NotImplementedMessage}, + router_request_types::{ + authentication::MessageCategory, + unified_authentication_service::{ + UasAuthenticationRequestData, UasPostAuthenticationRequestData, + UasPreAuthenticationRequestData, + }, + BrowserInformation, + }, +}; + use crate::{ - core::{errors::RouterResult, payments::PaymentData}, + core::{ + errors::RouterResult, + payments::{helpers::MerchantConnectorAccountType, PaymentData}, + }, db::domain, routes::SessionState, - types::domain::MerchantConnectorAccount, }; pub const CTP_MASTERCARD: &str = "ctp_mastercard"; @@ -17,34 +32,128 @@ pub const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_AUTHENTICATION_FLOW: &str pub struct ClickToPay; +pub struct ExternalAuthentication; + #[async_trait::async_trait] pub trait UnifiedAuthenticationService { + fn get_pre_authentication_request_data( + _payment_data: &PaymentData, + ) -> RouterResult { + Err(errors::ApiErrorResponse::NotImplemented { + message: NotImplementedMessage::Reason( + "get_pre_authentication_request_data".to_string(), + ), + } + .into()) + } + #[allow(clippy::too_many_arguments)] async fn pre_authentication( _state: &SessionState, _key_store: &domain::MerchantKeyStore, _business_profile: &domain::Profile, _payment_data: &PaymentData, - _merchant_connector_account: &MerchantConnectorAccount, + _merchant_connector_account: &MerchantConnectorAccountType, _connector_name: &str, _authentication_id: &str, _payment_method: common_enums::PaymentMethod, - ) -> RouterResult<()>; + ) -> RouterResult { + Err(errors::ApiErrorResponse::NotImplemented { + message: NotImplementedMessage::Reason("pre_authentication".to_string()), + } + .into()) + } + + #[allow(clippy::too_many_arguments)] + fn get_authentication_request_data( + _payment_method_data: domain::PaymentMethodData, + _billing_address: hyperswitch_domain_models::address::Address, + _shipping_address: Option, + _browser_details: Option, + _amount: Option, + _currency: Option, + _message_category: MessageCategory, + _device_channel: payments::DeviceChannel, + _authentication: diesel_models::authentication::Authentication, + _return_url: Option, + _sdk_information: Option, + _threeds_method_comp_ind: payments::ThreeDsCompletionIndicator, + _email: Option, + _webhook_url: String, + _three_ds_requestor_url: String, + ) -> RouterResult { + Err(errors::ApiErrorResponse::NotImplemented { + message: NotImplementedMessage::Reason( + "get_pre_authentication_request_data".to_string(), + ), + } + .into()) + } + #[allow(clippy::too_many_arguments)] + async fn authentication( + _state: &SessionState, + _business_profile: &domain::Profile, + _payment_method: common_enums::PaymentMethod, + _payment_method_data: domain::PaymentMethodData, + _billing_address: hyperswitch_domain_models::address::Address, + _shipping_address: Option, + _browser_details: Option, + _amount: Option, + _currency: Option, + _message_category: MessageCategory, + _device_channel: payments::DeviceChannel, + _authentication_data: diesel_models::authentication::Authentication, + _return_url: Option, + _sdk_information: Option, + _threeds_method_comp_ind: payments::ThreeDsCompletionIndicator, + _email: Option, + _webhook_url: String, + _three_ds_requestor_url: String, + _merchant_connector_account: &MerchantConnectorAccountType, + _connector_name: &str, + ) -> RouterResult { + Err(errors::ApiErrorResponse::NotImplemented { + message: NotImplementedMessage::Reason("authentication".to_string()), + } + .into()) + } + + fn get_post_authentication_request_data( + _authentication: Option, + ) -> RouterResult { + Err(errors::ApiErrorResponse::NotImplemented { + message: NotImplementedMessage::Reason("post_authentication".to_string()), + } + .into()) + } + + #[allow(clippy::too_many_arguments)] async fn post_authentication( _state: &SessionState, _key_store: &domain::MerchantKeyStore, _business_profile: &domain::Profile, _payment_data: &PaymentData, - _merchant_connector_account: &MerchantConnectorAccount, + _merchant_connector_account: &MerchantConnectorAccountType, _connector_name: &str, _payment_method: common_enums::PaymentMethod, - ) -> RouterResult; + _authentication: Option, + ) -> RouterResult { + Err(errors::ApiErrorResponse::NotImplemented { + message: NotImplementedMessage::Reason("post_authentication".to_string()), + } + .into()) + } fn confirmation( _state: &SessionState, _key_store: &domain::MerchantKeyStore, _business_profile: &domain::Profile, - _merchant_connector_account: &MerchantConnectorAccount, - ) -> RouterResult<()>; + _merchant_connector_account: &MerchantConnectorAccountType, + ) -> RouterResult<()> { + Err(errors::ApiErrorResponse::NotImplemented { + message: NotImplementedMessage::Reason("confirmation".to_string()), + } + .into()) + } } diff --git a/crates/router/src/core/unified_authentication_service/utils.rs b/crates/router/src/core/unified_authentication_service/utils.rs index c74baa3a0b3..15ab09e3cdc 100644 --- a/crates/router/src/core/unified_authentication_service/utils.rs +++ b/crates/router/src/core/unified_authentication_service/utils.rs @@ -1,7 +1,7 @@ use std::marker::PhantomData; use common_enums::enums::PaymentMethod; -use diesel_models::authentication::{Authentication, AuthenticationUpdate}; +use common_utils::ext_traits::ValueExt; use error_stack::ResultExt; use hyperswitch_domain_models::{ errors::api_error_response::ApiErrorResponse, @@ -10,7 +10,6 @@ use hyperswitch_domain_models::{ router_data_v2::UasFlowData, router_request_types::unified_authentication_service::UasAuthenticationResponseData, }; -use masking::ExposeOptionInterface; use super::types::{ IRRELEVANT_ATTEMPT_ID_IN_AUTHENTICATION_FLOW, @@ -22,56 +21,10 @@ use crate::{ payments, }, services::{self, execute_connector_processing_step}, - types::{api, domain::MerchantConnectorAccount}, + types::{api, transformers::ForeignFrom}, SessionState, }; -pub async fn update_trackers( - state: &SessionState, - router_data: RouterData, - authentication: Authentication, -) -> RouterResult { - let authentication_update = match router_data.response { - Ok(response) => match response { - UasAuthenticationResponseData::PreAuthentication {} => { - AuthenticationUpdate::AuthenticationStatusUpdate { - trans_status: common_enums::TransactionStatus::InformationOnly, - authentication_status: common_enums::AuthenticationStatus::Pending, - } - } - UasAuthenticationResponseData::PostAuthentication { - authentication_details, - } => AuthenticationUpdate::PostAuthenticationUpdate { - authentication_status: common_enums::AuthenticationStatus::Success, - trans_status: common_enums::TransactionStatus::Success, - authentication_value: authentication_details - .dynamic_data_details - .and_then(|data| data.dynamic_data_value.expose_option()), - eci: authentication_details.eci, - }, - }, - Err(error) => AuthenticationUpdate::ErrorUpdate { - connector_authentication_id: error.connector_transaction_id, - authentication_status: common_enums::AuthenticationStatus::Failed, - error_message: error - .reason - .map(|reason| format!("message: {}, reason: {}", error.message, reason)) - .or(Some(error.message)), - error_code: Some(error.code), - }, - }; - - state - .store - .update_authentication_by_merchant_id_authentication_id( - authentication, - authentication_update, - ) - .await - .change_context(ApiErrorResponse::InternalServerError) - .attach_printable("Error while updating authentication for uas") -} - pub async fn do_auth_connector_call( state: &SessionState, authentication_connector_name: String, @@ -108,12 +61,14 @@ pub fn construct_uas_router_data( merchant_id: common_utils::id_type::MerchantId, address: Option, request_data: Req, - merchant_connector_account: &MerchantConnectorAccount, + merchant_connector_account: &payments::helpers::MerchantConnectorAccountType, authentication_id: Option, ) -> RouterResult> { let auth_type: ConnectorAuthType = merchant_connector_account .get_connector_account_details() - .change_context(ApiErrorResponse::InternalServerError)?; + .parse_value("ConnectorAuthType") + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Error while parsing ConnectorAuthType")?; Ok(RouterData { flow: PhantomData, merchant_id, @@ -131,7 +86,7 @@ pub fn construct_uas_router_data( description: None, address: address.unwrap_or_default(), auth_type: common_enums::AuthenticationType::default(), - connector_meta_data: merchant_connector_account.metadata.clone(), + connector_meta_data: merchant_connector_account.get_metadata().clone(), connector_wallets_details: merchant_connector_account.get_connector_wallets_details(), amount_captured: None, minor_amount_captured: None, @@ -168,3 +123,121 @@ pub fn construct_uas_router_data( psd2_sca_exemption_type: None, }) } + +pub async fn external_authentication_update_trackers( + state: &SessionState, + router_data: RouterData, + authentication: diesel_models::authentication::Authentication, + acquirer_details: Option< + hyperswitch_domain_models::router_request_types::authentication::AcquirerDetails, + >, +) -> RouterResult { + let authentication_update = match router_data.response { + Ok(response) => match response { + UasAuthenticationResponseData::PreAuthentication { + authentication_details, + } => diesel_models::authentication::AuthenticationUpdate::PreAuthenticationUpdate { + threeds_server_transaction_id: authentication_details + .threeds_server_transaction_id + .ok_or(ApiErrorResponse::InternalServerError) + .attach_printable( + "missing threeds_server_transaction_id in PreAuthentication Details", + )?, + maximum_supported_3ds_version: authentication_details + .maximum_supported_3ds_version + .ok_or(ApiErrorResponse::InternalServerError) + .attach_printable( + "missing maximum_supported_3ds_version in PreAuthentication Details", + )?, + connector_authentication_id: authentication_details + .connector_authentication_id + .ok_or(ApiErrorResponse::InternalServerError) + .attach_printable( + "missing connector_authentication_id in PreAuthentication Details", + )?, + three_ds_method_data: authentication_details.three_ds_method_data, + three_ds_method_url: authentication_details.three_ds_method_url, + message_version: authentication_details + .message_version + .ok_or(ApiErrorResponse::InternalServerError) + .attach_printable("missing message_version in PreAuthentication Details")?, + connector_metadata: authentication_details.connector_metadata, + authentication_status: common_enums::AuthenticationStatus::Pending, + acquirer_bin: acquirer_details + .as_ref() + .map(|acquirer_details| acquirer_details.acquirer_bin.clone()), + acquirer_merchant_id: acquirer_details + .as_ref() + .map(|acquirer_details| acquirer_details.acquirer_merchant_id.clone()), + acquirer_country_code: acquirer_details + .and_then(|acquirer_details| acquirer_details.acquirer_country_code), + directory_server_id: authentication_details.directory_server_id, + }, + UasAuthenticationResponseData::Authentication { + authentication_details, + } => { + let authentication_status = common_enums::AuthenticationStatus::foreign_from( + authentication_details.trans_status.clone(), + ); + diesel_models::authentication::AuthenticationUpdate::AuthenticationUpdate { + authentication_value: authentication_details.authentication_value, + trans_status: authentication_details.trans_status, + acs_url: authentication_details.authn_flow_type.get_acs_url(), + challenge_request: authentication_details + .authn_flow_type + .get_challenge_request(), + acs_reference_number: authentication_details + .authn_flow_type + .get_acs_reference_number(), + acs_trans_id: authentication_details.authn_flow_type.get_acs_trans_id(), + acs_signed_content: authentication_details + .authn_flow_type + .get_acs_signed_content(), + authentication_type: authentication_details + .authn_flow_type + .get_decoupled_authentication_type(), + authentication_status, + connector_metadata: authentication_details.connector_metadata, + ds_trans_id: authentication_details.ds_trans_id, + } + } + UasAuthenticationResponseData::PostAuthentication { + authentication_details, + } => { + let trans_status = authentication_details + .trans_status + .ok_or(ApiErrorResponse::InternalServerError) + .attach_printable("missing trans_status in PostAuthentication Details")?; + diesel_models::authentication::AuthenticationUpdate::PostAuthenticationUpdate { + authentication_status: common_enums::AuthenticationStatus::foreign_from( + trans_status.clone(), + ), + trans_status, + authentication_value: authentication_details + .dynamic_data_details + .and_then(|details| details.dynamic_data_value) + .map(masking::ExposeInterface::expose), + eci: authentication_details.eci, + } + } + }, + Err(error) => diesel_models::authentication::AuthenticationUpdate::ErrorUpdate { + connector_authentication_id: error.connector_transaction_id, + authentication_status: common_enums::AuthenticationStatus::Failed, + error_message: error + .reason + .map(|reason| format!("message: {}, reason: {}", error.message, reason)) + .or(Some(error.message)), + error_code: Some(error.code), + }, + }; + state + .store + .update_authentication_by_merchant_id_authentication_id( + authentication, + authentication_update, + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Error while updating authentication") +} diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 16468aef74c..c88f61f7c82 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -1703,12 +1703,9 @@ pub async fn payments_external_authentication( &req, payload, |state, auth: auth::AuthenticationData, req, _| { - payments::payment_external_authentication( - state, - auth.merchant_account, - auth.key_store, - req, - ) + payments::payment_external_authentication::< + hyperswitch_domain_models::router_flow_types::Authenticate, + >(state, auth.merchant_account, auth.key_store, req) }, &auth::HeaderAuth(auth::PublishableKeyAuth), locking_action, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 2ae2af92478..5baca7bb55a 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -53,8 +53,8 @@ pub use hyperswitch_domain_models::{ }, router_request_types::{ unified_authentication_service::{ - UasAuthenticationResponseData, UasPostAuthenticationRequestData, - UasPreAuthenticationRequestData, + UasAuthenticationRequestData, UasAuthenticationResponseData, + UasPostAuthenticationRequestData, UasPreAuthenticationRequestData, }, AcceptDisputeRequestData, AccessTokenRequestData, AuthorizeSessionTokenData, BrowserInformation, ChargeRefunds, ChargeRefundsOptions, CompleteAuthorizeData, From 04a5e3823671d389bb6370570d7424a9e1d30759 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Tue, 4 Feb 2025 01:38:08 +0530 Subject: [PATCH 04/46] fix(samsung_pay): populate `payment_method_data` in the payment response (#7095) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 17 +++++++++++--- api-reference/openapi_spec.json | 17 +++++++++++--- crates/api_models/src/payment_methods.rs | 2 +- crates/api_models/src/payments.rs | 15 +++++++++---- .../src/payments/additional_info.rs | 2 +- .../src/payment_method_data.rs | 2 +- .../fraud_check/operation/fraud_check_pre.rs | 4 ++-- crates/router/src/core/payments/helpers.rs | 22 ++++++++++++++++++- .../payments/operations/payment_create.rs | 8 +++++++ crates/router/src/types/fraud_check.rs | 2 +- 10 files changed, 74 insertions(+), 17 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 236c0af3a38..b315800b08c 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -21493,8 +21493,7 @@ "type": "object", "required": [ "last4", - "card_network", - "type" + "card_network" ], "properties": { "last4": { @@ -21507,7 +21506,8 @@ }, "type": { "type": "string", - "description": "The type of payment method" + "description": "The type of payment method", + "nullable": true } } }, @@ -21864,6 +21864,17 @@ "$ref": "#/components/schemas/WalletAdditionalDataForCard" } } + }, + { + "type": "object", + "required": [ + "samsung_pay" + ], + "properties": { + "samsung_pay": { + "$ref": "#/components/schemas/WalletAdditionalDataForCard" + } + } } ], "description": "Hyperswitch supports SDK integration with Apple Pay and Google Pay wallets. For other wallets, we integrate with their respective connectors, redirecting the customer to the connector for wallet payments. As a result, we don’t receive any payment method data in the confirm call for payments made through other wallets." diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 9b840f53138..0aa2c898fc6 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -26102,8 +26102,7 @@ "type": "object", "required": [ "last4", - "card_network", - "type" + "card_network" ], "properties": { "last4": { @@ -26116,7 +26115,8 @@ }, "type": { "type": "string", - "description": "The type of payment method" + "description": "The type of payment method", + "nullable": true } } }, @@ -26473,6 +26473,17 @@ "$ref": "#/components/schemas/WalletAdditionalDataForCard" } } + }, + { + "type": "object", + "required": [ + "samsung_pay" + ], + "properties": { + "samsung_pay": { + "$ref": "#/components/schemas/WalletAdditionalDataForCard" + } + } } ], "description": "Hyperswitch supports SDK integration with Apple Pay and Google Pay wallets. For other wallets, we integrate with their respective connectors, redirecting the customer to the connector for wallet payments. As a result, we don’t receive any payment method data in the confirm call for payments made through other wallets." diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 82a7abc6ef1..12bb97d9b7d 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -829,7 +829,7 @@ pub struct PaymentMethodDataWalletInfo { pub card_network: String, /// The type of payment method #[serde(rename = "type")] - pub card_type: String, + pub card_type: Option, } impl From for PaymentMethodDataWalletInfo { diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 2c0a09bf8f0..2c8723857a8 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2601,6 +2601,7 @@ pub enum AdditionalPaymentData { Wallet { apple_pay: Option, google_pay: Option, + samsung_pay: Option, }, PayLater { klarna_sdk: Option, @@ -3874,6 +3875,8 @@ pub enum WalletResponseData { ApplePay(Box), #[schema(value_type = WalletAdditionalDataForCard)] GooglePay(Box), + #[schema(value_type = WalletAdditionalDataForCard)] + SamsungPay(Box), } #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] @@ -5410,8 +5413,9 @@ impl From for PaymentMethodDataResponse { AdditionalPaymentData::Wallet { apple_pay, google_pay, - } => match (apple_pay, google_pay) { - (Some(apple_pay_pm), _) => Self::Wallet(Box::new(WalletResponse { + samsung_pay, + } => match (apple_pay, google_pay, samsung_pay) { + (Some(apple_pay_pm), _, _) => Self::Wallet(Box::new(WalletResponse { details: Some(WalletResponseData::ApplePay(Box::new( additional_info::WalletAdditionalDataForCard { last4: apple_pay_pm @@ -5425,13 +5429,16 @@ impl From for PaymentMethodDataResponse { .rev() .collect::(), card_network: apple_pay_pm.network.clone(), - card_type: apple_pay_pm.pm_type.clone(), + card_type: Some(apple_pay_pm.pm_type.clone()), }, ))), })), - (_, Some(google_pay_pm)) => Self::Wallet(Box::new(WalletResponse { + (_, Some(google_pay_pm), _) => Self::Wallet(Box::new(WalletResponse { details: Some(WalletResponseData::GooglePay(Box::new(google_pay_pm))), })), + (_, _, Some(samsung_pay_pm)) => Self::Wallet(Box::new(WalletResponse { + details: Some(WalletResponseData::SamsungPay(Box::new(samsung_pay_pm))), + })), _ => Self::Wallet(Box::new(WalletResponse { details: None })), }, AdditionalPaymentData::BankRedirect { bank_name, details } => { diff --git a/crates/api_models/src/payments/additional_info.rs b/crates/api_models/src/payments/additional_info.rs index 9e8c910cba7..769e98214fa 100644 --- a/crates/api_models/src/payments/additional_info.rs +++ b/crates/api_models/src/payments/additional_info.rs @@ -219,5 +219,5 @@ pub struct WalletAdditionalDataForCard { pub card_network: String, /// The type of payment method #[serde(rename = "type")] - pub card_type: String, + pub card_type: Option, } diff --git a/crates/hyperswitch_domain_models/src/payment_method_data.rs b/crates/hyperswitch_domain_models/src/payment_method_data.rs index 8c19a20ef32..dcf46964170 100644 --- a/crates/hyperswitch_domain_models/src/payment_method_data.rs +++ b/crates/hyperswitch_domain_models/src/payment_method_data.rs @@ -1718,7 +1718,7 @@ impl From for payment_methods::PaymentMethodDataWalletInfo Self { last4: item.info.card_details, card_network: item.info.card_network, - card_type: item.pm_type, + card_type: Some(item.pm_type), } } } diff --git a/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs b/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs index 59a703d2b3c..da93a14dc94 100644 --- a/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs +++ b/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs @@ -239,7 +239,7 @@ where connector: router_data.connector, payment_id: router_data.payment_id.clone(), attempt_id: router_data.attempt_id, - request: FrmRequest::Checkout(FraudCheckCheckoutData { + request: FrmRequest::Checkout(Box::new(FraudCheckCheckoutData { amount: router_data.request.amount, order_details: router_data.request.order_details, currency: router_data.request.currency, @@ -247,7 +247,7 @@ where payment_method_data: router_data.request.payment_method_data, email: router_data.request.email, gateway: router_data.request.gateway, - }), + })), response: FrmResponse::Checkout(router_data.response), }) } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 6e85db98430..3833d316be6 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -4671,6 +4671,7 @@ pub async fn get_additional_payment_data( pm_type: apple_pay_wallet_data.payment_method.pm_type.clone(), }), google_pay: None, + samsung_pay: None, })) } domain::WalletData::GooglePay(google_pay_pm_data) => { @@ -4679,13 +4680,32 @@ pub async fn get_additional_payment_data( google_pay: Some(payment_additional_types::WalletAdditionalDataForCard { last4: google_pay_pm_data.info.card_details.clone(), card_network: google_pay_pm_data.info.card_network.clone(), - card_type: google_pay_pm_data.pm_type.clone(), + card_type: Some(google_pay_pm_data.pm_type.clone()), + }), + samsung_pay: None, + })) + } + domain::WalletData::SamsungPay(samsung_pay_pm_data) => { + Ok(Some(api_models::payments::AdditionalPaymentData::Wallet { + apple_pay: None, + google_pay: None, + samsung_pay: Some(payment_additional_types::WalletAdditionalDataForCard { + last4: samsung_pay_pm_data + .payment_credential + .card_last_four_digits + .clone(), + card_network: samsung_pay_pm_data + .payment_credential + .card_brand + .to_string(), + card_type: None, }), })) } _ => Ok(Some(api_models::payments::AdditionalPaymentData::Wallet { apple_pay: None, google_pay: None, + samsung_pay: None, })), }, domain::PaymentMethodData::PayLater(_) => Ok(Some( diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 2bb751d4e14..ed5c93489cd 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1178,6 +1178,14 @@ impl PaymentCreate { Some(api_models::payments::AdditionalPaymentData::Wallet { apple_pay: None, google_pay: Some(wallet.into()), + samsung_pay: None, + }) + } + Some(enums::PaymentMethodType::SamsungPay) => { + Some(api_models::payments::AdditionalPaymentData::Wallet { + apple_pay: None, + google_pay: None, + samsung_pay: Some(wallet.into()), }) } _ => None, diff --git a/crates/router/src/types/fraud_check.rs b/crates/router/src/types/fraud_check.rs index a861aca67db..386c8a9cd90 100644 --- a/crates/router/src/types/fraud_check.rs +++ b/crates/router/src/types/fraud_check.rs @@ -29,7 +29,7 @@ pub struct FrmRouterData { #[derive(Debug, Clone)] pub enum FrmRequest { Sale(FraudCheckSaleData), - Checkout(FraudCheckCheckoutData), + Checkout(Box), Transaction(FraudCheckTransactionData), Fulfillment(FraudCheckFulfillmentData), RecordReturn(FraudCheckRecordReturnData), From fa530531f51f8743761b489ac495845abdb70bdf Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 00:31:18 +0000 Subject: [PATCH 05/46] chore(version): 2025.02.04.0 --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ed7f89e30e..3d3abbda850 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2025.02.04.0 + +### Features + +- **router:** Add core changes for external authentication flow through unified_authentication_service ([#7063](https://github.com/juspay/hyperswitch/pull/7063)) ([`ae39374`](https://github.com/juspay/hyperswitch/commit/ae39374c6b41635e6c474b429fd1df59d30aa6dd)) + +### Bug Fixes + +- **connector:** [NETCETERA] add `sdk-type` and `default-sdk-type` in netcetera authentication request ([#7156](https://github.com/juspay/hyperswitch/pull/7156)) ([`64a7afa`](https://github.com/juspay/hyperswitch/commit/64a7afa6d42270d96788119e666b97176cd753dd)) +- **samsung_pay:** Populate `payment_method_data` in the payment response ([#7095](https://github.com/juspay/hyperswitch/pull/7095)) ([`04a5e38`](https://github.com/juspay/hyperswitch/commit/04a5e3823671d389bb6370570d7424a9e1d30759)) + +### Miscellaneous Tasks + +- Bump cypress to `v14.0.0` ([#7102](https://github.com/juspay/hyperswitch/pull/7102)) ([`0e9966a`](https://github.com/juspay/hyperswitch/commit/0e9966a54d87f55b0f5c54e4dccb80742674fe26)) + +**Full Changelog:** [`2025.01.31.0...2025.02.04.0`](https://github.com/juspay/hyperswitch/compare/2025.01.31.0...2025.02.04.0) + +- - - + ## 2025.01.31.0 ### Miscellaneous Tasks From 8ac1b83985dbae33afc3b53d46b85a374ff3c1e9 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Tue, 4 Feb 2025 01:43:37 +0530 Subject: [PATCH 06/46] fix: invalidate surcharge cache during update (#6907) --- crates/router/src/core/surcharge_decision_config.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/router/src/core/surcharge_decision_config.rs b/crates/router/src/core/surcharge_decision_config.rs index 0ff5f3fb373..2d817fd1afe 100644 --- a/crates/router/src/core/surcharge_decision_config.rs +++ b/crates/router/src/core/surcharge_decision_config.rs @@ -56,7 +56,7 @@ pub async fn upsert_surcharge_decision_config( message: "Invalid Request Data".to_string(), }) .attach_printable("The Request has an Invalid Comparison")?; - + let surcharge_cache_key = merchant_account.get_id().get_surcharge_dsk_key(); match read_config_key { Ok(config) => { let previous_record: SurchargeDecisionManagerRecord = config @@ -88,7 +88,7 @@ pub async fn upsert_surcharge_decision_config( .attach_printable("Error serializing the config")?; algo_id.update_surcharge_config_id(key.clone()); - let config_key = cache::CacheKind::Surcharge(key.into()); + let config_key = cache::CacheKind::Surcharge(surcharge_cache_key.into()); update_merchant_active_algorithm_ref(&state, &key_store, config_key, algo_id) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -125,7 +125,7 @@ pub async fn upsert_surcharge_decision_config( .attach_printable("Error fetching the config")?; algo_id.update_surcharge_config_id(key.clone()); - let config_key = cache::CacheKind::Surcharge(key.clone().into()); + let config_key = cache::CacheKind::Surcharge(surcharge_cache_key.into()); update_merchant_active_algorithm_ref(&state, &key_store, config_key, algo_id) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -173,7 +173,8 @@ pub async fn delete_surcharge_decision_config( .attach_printable("Could not decode the surcharge conditional_config algorithm")? .unwrap_or_default(); algo_id.surcharge_config_algo_id = None; - let config_key = cache::CacheKind::Surcharge(key.clone().into()); + let surcharge_cache_key = merchant_account.get_id().get_surcharge_dsk_key(); + let config_key = cache::CacheKind::Surcharge(surcharge_cache_key.into()); update_merchant_active_algorithm_ref(&state, &key_store, config_key, algo_id) .await .change_context(errors::ApiErrorResponse::InternalServerError) From 55bb284ba063dc84e80b4f0d83c82ec7c30ad4c5 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Tue, 4 Feb 2025 01:43:55 +0530 Subject: [PATCH 07/46] fix(router): [Cybersource] add flag to indicate final capture (#7085) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/connectors/cybersource/transformers.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs index 9f3d0241719..5fc9cad4ffe 100644 --- a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs @@ -417,6 +417,7 @@ pub enum CybersourcePaymentInitiatorTypes { pub struct CaptureOptions { capture_sequence_number: u32, total_capture_count: u32, + is_final: Option, } #[derive(Debug, Serialize)] @@ -2142,11 +2143,18 @@ impl TryFrom<&CybersourceRouterData<&PaymentsCaptureRouterData>> .clone() .map(convert_metadata_to_merchant_defined_info); + let is_final = matches!( + item.router_data.request.capture_method, + Some(enums::CaptureMethod::Manual) + ) + .then_some(true); + Ok(Self { processing_information: ProcessingInformation { capture_options: Some(CaptureOptions { capture_sequence_number: 1, total_capture_count: 1, + is_final, }), action_list: None, action_token_types: None, From ed8ef2466b7f059ed0f534aa1f3fca9b5ecbeefd Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:11:54 +0530 Subject: [PATCH 08/46] ci: disable stripe in cypress (#7183) --- .github/workflows/cypress-tests-runner.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cypress-tests-runner.yml b/.github/workflows/cypress-tests-runner.yml index 4de046499ab..67efcdb0ea3 100644 --- a/.github/workflows/cypress-tests-runner.yml +++ b/.github/workflows/cypress-tests-runner.yml @@ -13,7 +13,7 @@ concurrency: env: CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 - PAYMENTS_CONNECTORS: "cybersource stripe" + PAYMENTS_CONNECTORS: "cybersource" PAYOUTS_CONNECTORS: "wise" RUST_BACKTRACE: short RUSTUP_MAX_RETRIES: 10 @@ -50,7 +50,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: "20" - name: Install Cypress and dependencies run: | @@ -189,8 +189,7 @@ jobs: if: ${{ env.RUN_TESTS == 'true' }} uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'npm' + node-version: "20" - name: Install Cypress and dependencies if: ${{ env.RUN_TESTS == 'true' }} @@ -377,8 +376,7 @@ jobs: if: ${{ env.RUN_TESTS == 'true' }} uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'npm' + node-version: "20" - name: Install Cypress and dependencies if: ${{ env.RUN_TESTS == 'true' }} @@ -466,4 +464,4 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} files: ${{ env.CODECOV_FILE }} - disable_search: true \ No newline at end of file + disable_search: true From e2ddcc26b84e4ddcd69005080e19d211b1604827 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:48:04 +0530 Subject: [PATCH 09/46] fix(router): add dynamic fields support for `samsung_pay` (#7090) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 1 + config/deployments/integration_test.toml | 1 + config/deployments/production.toml | 1 + config/deployments/sandbox.toml | 1 + config/development.toml | 1 + config/docker_compose.toml | 1 + .../payment_connector_required_fields.rs | 21 +++++++++++++++++++ loadtest/config/development.toml | 1 + 8 files changed, 28 insertions(+) diff --git a/config/config.example.toml b/config/config.example.toml index 1d4507126fe..b76905eb534 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -460,6 +460,7 @@ bank_debit.sepa = { connector_list = "gocardless,adyen" } bank_redirect.ideal = { connector_list = "stripe,adyen,globalpay" } # Mandate supported payment method type and connector for bank_redirect bank_redirect.sofort = { connector_list = "stripe,adyen,globalpay" } wallet.apple_pay = { connector_list = "stripe,adyen,cybersource,noon,bankofamerica" } +wallet.samsung_pay = { connector_list = "cybersource" } wallet.google_pay = { connector_list = "bankofamerica" } bank_redirect.giropay = { connector_list = "adyen,globalpay" } diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index d606f192e4e..5791c6aa83e 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -169,6 +169,7 @@ card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" pay_later.klarna.connector_list = "adyen" wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet" +wallet.samsung_pay.connector_list = "cybersource" wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica,noon,globalpay,multisafepay,novalnet" wallet.paypal.connector_list = "adyen,globalpay,nexinets,novalnet,paypal" wallet.momo.connector_list = "adyen" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index ff5abd3fb03..78590f05f1b 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -169,6 +169,7 @@ card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" pay_later.klarna.connector_list = "adyen" wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet" +wallet.samsung_pay.connector_list = "cybersource" wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica,noon,globalpay,multisafepay,novalnet" wallet.paypal.connector_list = "adyen,globalpay,nexinets,novalnet,paypal" wallet.momo.connector_list = "adyen" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 208621433bb..4127a694fcb 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -169,6 +169,7 @@ card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" pay_later.klarna.connector_list = "adyen" wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet" +wallet.samsung_pay.connector_list = "cybersource" wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica,noon,globalpay,multisafepay,novalnet" wallet.paypal.connector_list = "adyen,globalpay,nexinets,novalnet,paypal" wallet.momo.connector_list = "adyen" diff --git a/config/development.toml b/config/development.toml index b293151a988..8209d49565f 100644 --- a/config/development.toml +++ b/config/development.toml @@ -628,6 +628,7 @@ card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" pay_later.klarna.connector_list = "adyen" wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet" +wallet.samsung_pay.connector_list = "cybersource" wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica,noon,globalpay,multisafepay,novalnet" wallet.paypal.connector_list = "adyen,globalpay,nexinets,novalnet,paypal" wallet.momo.connector_list = "adyen" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 656d4fa7ecb..0d3cbb4f6af 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -522,6 +522,7 @@ adyen = { banks = "aib,bank_of_scotland,danske_bank,first_direct,first_trust,hal pay_later.klarna = { connector_list = "adyen" } wallet.google_pay = { connector_list = "stripe,adyen,bankofamerica" } wallet.apple_pay = { connector_list = "stripe,adyen,cybersource,noon,bankofamerica" } +wallet.samsung_pay = { connector_list = "cybersource" } wallet.paypal = { connector_list = "adyen" } card.credit = { connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica" } card.debit = { connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica" } diff --git a/crates/router/src/configs/defaults/payment_connector_required_fields.rs b/crates/router/src/configs/defaults/payment_connector_required_fields.rs index d957109bcef..2ef89f440ad 100644 --- a/crates/router/src/configs/defaults/payment_connector_required_fields.rs +++ b/crates/router/src/configs/defaults/payment_connector_required_fields.rs @@ -53,6 +53,12 @@ impl Default for Mandates { ]), }, ), + ( + enums::PaymentMethodType::SamsungPay, + SupportedConnectorsForMandate { + connector_list: HashSet::from([enums::Connector::Cybersource]), + }, + ), ])), ), ( @@ -8889,6 +8895,21 @@ impl Default for settings::RequiredFields { ]), }, ), + ( + enums::PaymentMethodType::SamsungPay, + ConnectorFields { + fields: HashMap::from([ + ( + enums::Connector::Cybersource, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ]), + }, + ), ( enums::PaymentMethodType::GooglePay, ConnectorFields { diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index ac90dc88198..4d9c686f23b 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -338,6 +338,7 @@ card.credit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay card.debit.connector_list = "stripe,adyen,authorizedotnet,cybersource,globalpay,worldpay,multisafepay,nmi,nexinets,noon,bankofamerica,braintree,nuvei,payme,wellsfargo,bamboraapac,elavon,fiuu,nexixpay,novalnet,paybox,paypal" pay_later.klarna.connector_list = "adyen" wallet.apple_pay.connector_list = "stripe,adyen,cybersource,noon,bankofamerica,nexinets,novalnet" +wallet.samsung_pay.connector_list = "cybersource" wallet.google_pay.connector_list = "stripe,adyen,cybersource,bankofamerica,noon,globalpay,multisafepay,novalnet" wallet.paypal.connector_list = "adyen,globalpay,nexinets,novalnet,paypal" wallet.momo.connector_list = "adyen" From b9aa3ab445e7966dad3f7c09f27e644d5628f61f Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Tue, 4 Feb 2025 22:43:02 +0530 Subject: [PATCH 10/46] feat(router): add card_discovery in payment_attempt (#7039) Co-authored-by: hrithikesh026 --- crates/common_enums/src/enums.rs | 25 ++++++++++++++++ crates/diesel_models/src/enums.rs | 4 +-- crates/diesel_models/src/payment_attempt.rs | 30 +++++++++++++++++++ crates/diesel_models/src/schema.rs | 1 + crates/diesel_models/src/schema_v2.rs | 1 + crates/diesel_models/src/user/sample_data.rs | 2 ++ .../src/payments/payment_attempt.rs | 15 ++++++++++ crates/router/src/core/payments.rs | 22 ++++++++++++++ crates/router/src/core/payments/helpers.rs | 1 + .../payments/operations/payment_confirm.rs | 5 +++- .../payments/operations/payment_create.rs | 1 + crates/router/src/core/payments/retry.rs | 1 + .../src/types/storage/payment_attempt.rs | 3 ++ .../src/types/storage/payment_method.rs | 4 +++ crates/router/src/utils/user/sample_data.rs | 1 + .../src/mock_db/payment_attempt.rs | 1 + .../src/payments/payment_attempt.rs | 5 ++++ .../down.sql | 4 +++ .../up.sql | 4 +++ 19 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 migrations/2025-01-13-060852_add_card_discovery_in_payment_attempt/down.sql create mode 100644 migrations/2025-01-13-060852_add_card_discovery_in_payment_attempt/up.sql diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 562b4fdf269..c30c9ec9f14 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -181,6 +181,31 @@ impl AttemptStatus { } } +/// Indicates the method by which a card is discovered during a payment +#[derive( + Clone, + Copy, + Debug, + Default, + Hash, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + ToSchema, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum CardDiscovery { + #[default] + Manual, + SavedCard, + ClickToPay, +} + /// Pass this parameter to force 3DS or non 3DS auth for this payment. Some connectors will still force 3DS auth even in case of passing 'no_three_ds' here and vice versa. Default value is 'no_three_ds' if not set #[derive( Clone, diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index ec6e91a2ecb..1ee1a090ffa 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -4,8 +4,8 @@ pub mod diesel_exports { DbApiVersion as ApiVersion, DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType, DbBlocklistDataKind as BlocklistDataKind, DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus, - DbConnectorStatus as ConnectorStatus, DbConnectorType as ConnectorType, - DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, + DbCardDiscovery as CardDiscovery, DbConnectorStatus as ConnectorStatus, + DbConnectorType as ConnectorType, DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDashboardMetadata as DashboardMetadata, DbDeleteStatus as DeleteStatus, DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, DbEventClass as EventClass, DbEventObjectType as EventObjectType, DbEventType as EventType, diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index a450a30fa76..dbd867c9e55 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -94,6 +94,7 @@ pub struct PaymentAttempt { pub shipping_cost: Option, pub order_tax_amount: Option, pub connector_mandate_detail: Option, + pub card_discovery: Option, } #[cfg(feature = "v1")] @@ -172,6 +173,7 @@ pub struct PaymentAttempt { pub order_tax_amount: Option, pub connector_transaction_data: Option, pub connector_mandate_detail: Option, + pub card_discovery: Option, } #[cfg(feature = "v1")] @@ -278,6 +280,7 @@ pub struct PaymentAttemptNew { pub payment_method_subtype: storage_enums::PaymentMethodType, pub id: id_type::GlobalAttemptId, pub connector_mandate_detail: Option, + pub card_discovery: Option, } #[cfg(feature = "v1")] @@ -351,6 +354,7 @@ pub struct PaymentAttemptNew { pub shipping_cost: Option, pub order_tax_amount: Option, pub connector_mandate_detail: Option, + pub card_discovery: Option, } #[cfg(feature = "v1")] @@ -423,6 +427,7 @@ pub enum PaymentAttemptUpdate { shipping_cost: Option, order_tax_amount: Option, connector_mandate_detail: Option, + card_discovery: Option, }, VoidUpdate { status: storage_enums::AttemptStatus, @@ -848,6 +853,7 @@ pub struct PaymentAttemptUpdateInternal { pub order_tax_amount: Option, pub connector_transaction_data: Option, pub connector_mandate_detail: Option, + pub card_discovery: Option, } #[cfg(feature = "v1")] @@ -1031,6 +1037,7 @@ impl PaymentAttemptUpdate { order_tax_amount, connector_transaction_data, connector_mandate_detail, + card_discovery, } = PaymentAttemptUpdateInternal::from(self).populate_derived_fields(&source); PaymentAttempt { amount: amount.unwrap_or(source.amount), @@ -1089,6 +1096,7 @@ impl PaymentAttemptUpdate { connector_transaction_data: connector_transaction_data .or(source.connector_transaction_data), connector_mandate_detail: connector_mandate_detail.or(source.connector_mandate_detail), + card_discovery: card_discovery.or(source.card_discovery), ..source } } @@ -2141,6 +2149,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount: None, connector_transaction_data: None, connector_mandate_detail: None, + card_discovery: None, }, PaymentAttemptUpdate::AuthenticationTypeUpdate { authentication_type, @@ -2197,6 +2206,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount: None, connector_transaction_data: None, connector_mandate_detail: None, + card_discovery: None, }, PaymentAttemptUpdate::ConfirmUpdate { amount, @@ -2232,6 +2242,7 @@ impl From for PaymentAttemptUpdateInternal { shipping_cost, order_tax_amount, connector_mandate_detail, + card_discovery, } => Self { amount: Some(amount), currency: Some(currency), @@ -2284,6 +2295,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount, connector_transaction_data: None, connector_mandate_detail, + card_discovery, }, PaymentAttemptUpdate::VoidUpdate { status, @@ -2341,6 +2353,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount: None, connector_transaction_data: None, connector_mandate_detail: None, + card_discovery: None, }, PaymentAttemptUpdate::RejectUpdate { status, @@ -2399,6 +2412,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount: None, connector_transaction_data: None, connector_mandate_detail: None, + card_discovery: None, }, PaymentAttemptUpdate::BlocklistUpdate { status, @@ -2457,6 +2471,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount: None, connector_transaction_data: None, connector_mandate_detail: None, + card_discovery: None, }, PaymentAttemptUpdate::ConnectorMandateDetailUpdate { connector_mandate_detail, @@ -2513,6 +2528,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount: None, connector_transaction_data: None, connector_mandate_detail, + card_discovery: None, }, PaymentAttemptUpdate::PaymentMethodDetailsUpdate { payment_method_id, @@ -2569,6 +2585,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount: None, connector_transaction_data: None, connector_mandate_detail: None, + card_discovery: None, }, PaymentAttemptUpdate::ResponseUpdate { status, @@ -2650,6 +2667,7 @@ impl From for PaymentAttemptUpdateInternal { shipping_cost: None, order_tax_amount: None, connector_mandate_detail, + card_discovery: None, } } PaymentAttemptUpdate::ErrorUpdate { @@ -2723,6 +2741,7 @@ impl From for PaymentAttemptUpdateInternal { shipping_cost: None, order_tax_amount: None, connector_mandate_detail: None, + card_discovery: None, } } PaymentAttemptUpdate::StatusUpdate { status, updated_by } => Self { @@ -2777,6 +2796,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount: None, connector_transaction_data: None, connector_mandate_detail: None, + card_discovery: None, }, PaymentAttemptUpdate::UpdateTrackers { payment_token, @@ -2839,6 +2859,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount: None, connector_transaction_data: None, connector_mandate_detail: None, + card_discovery: None, }, PaymentAttemptUpdate::UnresolvedResponseUpdate { status, @@ -2908,6 +2929,7 @@ impl From for PaymentAttemptUpdateInternal { shipping_cost: None, order_tax_amount: None, connector_mandate_detail: None, + card_discovery: None, } } PaymentAttemptUpdate::PreprocessingUpdate { @@ -2976,6 +2998,7 @@ impl From for PaymentAttemptUpdateInternal { shipping_cost: None, order_tax_amount: None, connector_mandate_detail: None, + card_discovery: None, } } PaymentAttemptUpdate::CaptureUpdate { @@ -3034,6 +3057,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount: None, connector_transaction_data: None, connector_mandate_detail: None, + card_discovery: None, }, PaymentAttemptUpdate::AmountToCaptureUpdate { status, @@ -3091,6 +3115,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount: None, connector_transaction_data: None, connector_mandate_detail: None, + card_discovery: None, }, PaymentAttemptUpdate::ConnectorResponse { authentication_data, @@ -3157,6 +3182,7 @@ impl From for PaymentAttemptUpdateInternal { shipping_cost: None, order_tax_amount: None, connector_mandate_detail: None, + card_discovery: None, } } PaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { @@ -3214,6 +3240,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount: None, connector_transaction_data: None, connector_mandate_detail: None, + card_discovery: None, }, PaymentAttemptUpdate::AuthenticationUpdate { status, @@ -3273,6 +3300,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount: None, connector_transaction_data: None, connector_mandate_detail: None, + card_discovery: None, }, PaymentAttemptUpdate::ManualUpdate { status, @@ -3341,6 +3369,7 @@ impl From for PaymentAttemptUpdateInternal { shipping_cost: None, order_tax_amount: None, connector_mandate_detail: None, + card_discovery: None, } } PaymentAttemptUpdate::PostSessionTokensUpdate { @@ -3398,6 +3427,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount: None, connector_transaction_data: None, connector_mandate_detail: None, + card_discovery: None, }, } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 7930ad5d9ae..40343e44365 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -907,6 +907,7 @@ diesel::table! { #[max_length = 512] connector_transaction_data -> Nullable, connector_mandate_detail -> Nullable, + card_discovery -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 13abf8ee64b..99d6139f310 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -877,6 +877,7 @@ diesel::table! { shipping_cost -> Nullable, order_tax_amount -> Nullable, connector_mandate_detail -> Nullable, + card_discovery -> Nullable, } } diff --git a/crates/diesel_models/src/user/sample_data.rs b/crates/diesel_models/src/user/sample_data.rs index cfc9e1c4c8e..4262276c467 100644 --- a/crates/diesel_models/src/user/sample_data.rs +++ b/crates/diesel_models/src/user/sample_data.rs @@ -203,6 +203,7 @@ pub struct PaymentAttemptBatchNew { pub order_tax_amount: Option, pub connector_transaction_data: Option, pub connector_mandate_detail: Option, + pub card_discovery: Option, } #[cfg(feature = "v1")] @@ -282,6 +283,7 @@ impl PaymentAttemptBatchNew { shipping_cost: self.shipping_cost, order_tax_amount: self.order_tax_amount, connector_mandate_detail: self.connector_mandate_detail, + card_discovery: self.card_discovery, } } } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index 0a7ada39ecd..74804e658a7 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -402,6 +402,8 @@ pub struct PaymentAttempt { pub id: id_type::GlobalAttemptId, /// The connector mandate details which are stored temporarily pub connector_mandate_detail: Option, + /// Indicates the method by which a card is discovered during a payment + pub card_discovery: Option, } impl PaymentAttempt { @@ -520,6 +522,7 @@ impl PaymentAttempt { error: None, connector_mandate_detail: None, id, + card_discovery: None, }) } } @@ -590,6 +593,7 @@ pub struct PaymentAttempt { pub profile_id: id_type::ProfileId, pub organization_id: id_type::OrganizationId, pub connector_mandate_detail: Option, + pub card_discovery: Option, } #[cfg(feature = "v1")] @@ -836,6 +840,7 @@ pub struct PaymentAttemptNew { pub profile_id: id_type::ProfileId, pub organization_id: id_type::OrganizationId, pub connector_mandate_detail: Option, + pub card_discovery: Option, } #[cfg(feature = "v1")] @@ -902,6 +907,7 @@ pub enum PaymentAttemptUpdate { client_version: Option, customer_acceptance: Option, connector_mandate_detail: Option, + card_discovery: Option, }, RejectUpdate { status: storage_enums::AttemptStatus, @@ -1154,6 +1160,7 @@ impl PaymentAttemptUpdate { client_version, customer_acceptance, connector_mandate_detail, + card_discovery, } => DieselPaymentAttemptUpdate::ConfirmUpdate { amount: net_amount.get_order_amount(), currency, @@ -1188,6 +1195,7 @@ impl PaymentAttemptUpdate { shipping_cost: net_amount.get_shipping_cost(), order_tax_amount: net_amount.get_order_tax_amount(), connector_mandate_detail, + card_discovery, }, Self::VoidUpdate { status, @@ -1551,6 +1559,7 @@ impl behaviour::Conversion for PaymentAttempt { order_tax_amount: self.net_amount.get_order_tax_amount(), shipping_cost: self.net_amount.get_shipping_cost(), connector_mandate_detail: self.connector_mandate_detail, + card_discovery: self.card_discovery, }) } @@ -1632,6 +1641,7 @@ impl behaviour::Conversion for PaymentAttempt { profile_id: storage_model.profile_id, organization_id: storage_model.organization_id, connector_mandate_detail: storage_model.connector_mandate_detail, + card_discovery: storage_model.card_discovery, }) } .await @@ -1714,6 +1724,7 @@ impl behaviour::Conversion for PaymentAttempt { order_tax_amount: self.net_amount.get_order_tax_amount(), shipping_cost: self.net_amount.get_shipping_cost(), connector_mandate_detail: self.connector_mandate_detail, + card_discovery: self.card_discovery, }) } } @@ -1781,6 +1792,7 @@ impl behaviour::Conversion for PaymentAttempt { payment_method_billing_address, connector, connector_mandate_detail, + card_discovery, } = self; let AttemptAmountDetails { @@ -1858,6 +1870,7 @@ impl behaviour::Conversion for PaymentAttempt { payment_method_billing_address: payment_method_billing_address.map(Encryption::from), connector_payment_data, connector_mandate_detail, + card_discovery, }) } @@ -1969,6 +1982,7 @@ impl behaviour::Conversion for PaymentAttempt { connector: storage_model.connector, payment_method_billing_address, connector_mandate_detail: storage_model.connector_mandate_detail, + card_discovery: storage_model.card_discovery, }) } .await @@ -2053,6 +2067,7 @@ impl behaviour::Conversion for PaymentAttempt { payment_method_type_v2: self.payment_method_type, id: self.id, connector_mandate_detail: self.connector_mandate_detail, + card_discovery: self.card_discovery, }) } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index e991593b4b5..e9d3c013789 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -4450,6 +4450,28 @@ pub struct PaymentEvent { } impl PaymentData { + // Get the method by which a card is discovered during a payment + #[cfg(feature = "v1")] + fn get_card_discovery_for_card_payment_method(&self) -> Option { + match self.payment_attempt.payment_method { + Some(storage_enums::PaymentMethod::Card) => { + if self + .token_data + .as_ref() + .map(storage::PaymentTokenData::is_permanent_card) + .unwrap_or(false) + { + Some(common_enums::CardDiscovery::SavedCard) + } else if self.service_details.is_some() { + Some(common_enums::CardDiscovery::ClickToPay) + } else { + Some(common_enums::CardDiscovery::Manual) + } + } + _ => None, + } + } + fn to_event(&self) -> PaymentEvent { PaymentEvent { payment_intent: self.payment_intent.clone(), diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 3833d316be6..0d7a3b57fc1 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -4289,6 +4289,7 @@ impl AttemptType { organization_id: old_payment_attempt.organization_id, profile_id: old_payment_attempt.profile_id, connector_mandate_detail: None, + card_discovery: None, } } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index edcd798ba99..9b1c2ea3be3 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -1585,7 +1585,7 @@ impl UpdateTracker, api::PaymentsRequest> for let m_payment_token = payment_token.clone(); let m_additional_pm_data = encoded_additional_pm_data .clone() - .or(payment_data.payment_attempt.payment_method_data); + .or(payment_data.payment_attempt.payment_method_data.clone()); let m_business_sub_label = business_sub_label.clone(); let m_straight_through_algorithm = straight_through_algorithm.clone(); let m_error_code = error_code.clone(); @@ -1614,6 +1614,8 @@ impl UpdateTracker, api::PaymentsRequest> for None => (None, None, None), }; + let card_discovery = payment_data.get_card_discovery_for_card_payment_method(); + let payment_attempt_fut = tokio::spawn( async move { m_db.update_payment_attempt_with_attempt_id( @@ -1661,6 +1663,7 @@ impl UpdateTracker, api::PaymentsRequest> for connector_mandate_detail: payment_data .payment_attempt .connector_mandate_detail, + card_discovery, }, storage_scheme, ) diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index ed5c93489cd..6ace8c3018f 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1306,6 +1306,7 @@ impl PaymentCreate { organization_id: organization_id.clone(), profile_id, connector_mandate_detail: None, + card_discovery: None, }, additional_pm_data, diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index 7ae536a1f19..0401575bc66 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -654,6 +654,7 @@ pub fn make_new_payment_attempt( charge_id: Default::default(), customer_acceptance: Default::default(), connector_mandate_detail: Default::default(), + card_discovery: old_payment_attempt.card_discovery, } } diff --git a/crates/router/src/types/storage/payment_attempt.rs b/crates/router/src/types/storage/payment_attempt.rs index 0291374d54f..5958435abc7 100644 --- a/crates/router/src/types/storage/payment_attempt.rs +++ b/crates/router/src/types/storage/payment_attempt.rs @@ -217,6 +217,7 @@ mod tests { profile_id: common_utils::generate_profile_id_of_default_length(), organization_id: Default::default(), connector_mandate_detail: Default::default(), + card_discovery: Default::default(), }; let store = state @@ -301,6 +302,7 @@ mod tests { profile_id: common_utils::generate_profile_id_of_default_length(), organization_id: Default::default(), connector_mandate_detail: Default::default(), + card_discovery: Default::default(), }; let store = state .stores @@ -398,6 +400,7 @@ mod tests { profile_id: common_utils::generate_profile_id_of_default_length(), organization_id: Default::default(), connector_mandate_detail: Default::default(), + card_discovery: Default::default(), }; let store = state .stores diff --git a/crates/router/src/types/storage/payment_method.rs b/crates/router/src/types/storage/payment_method.rs index bc5f6651b6b..21c15c23b3c 100644 --- a/crates/router/src/types/storage/payment_method.rs +++ b/crates/router/src/types/storage/payment_method.rs @@ -103,6 +103,10 @@ impl PaymentTokenData { pub fn wallet_token(payment_method_id: String) -> Self { Self::WalletToken(WalletTokenData { payment_method_id }) } + + pub fn is_permanent_card(&self) -> bool { + matches!(self, Self::PermanentCard(_) | Self::Permanent(_)) + } } #[cfg(all( diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index bf84dd568a2..c5652e6cbfa 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -361,6 +361,7 @@ pub async fn generate_sample_data( order_tax_amount: None, connector_transaction_data, connector_mandate_detail: None, + card_discovery: None, }; let refund = if refunds_count < number_of_refunds && !is_failed_payment { diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index 0415625f7eb..3a45cfad9a2 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -195,6 +195,7 @@ impl PaymentAttemptInterface for MockDb { organization_id: payment_attempt.organization_id, profile_id: payment_attempt.profile_id, connector_mandate_detail: payment_attempt.connector_mandate_detail, + card_discovery: payment_attempt.card_discovery, }; payment_attempts.push(payment_attempt.clone()); Ok(payment_attempt) diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 06c7fefe85f..3480f8f4ba4 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -564,6 +564,7 @@ impl PaymentAttemptInterface for KVRouterStore { organization_id: payment_attempt.organization_id.clone(), profile_id: payment_attempt.profile_id.clone(), connector_mandate_detail: payment_attempt.connector_mandate_detail.clone(), + card_discovery: payment_attempt.card_discovery, }; let field = format!("pa_{}", created_attempt.attempt_id); @@ -1511,6 +1512,7 @@ impl DataModelExt for PaymentAttempt { shipping_cost: self.net_amount.get_shipping_cost(), order_tax_amount: self.net_amount.get_order_tax_amount(), connector_mandate_detail: self.connector_mandate_detail, + card_discovery: self.card_discovery, } } @@ -1587,6 +1589,7 @@ impl DataModelExt for PaymentAttempt { organization_id: storage_model.organization_id, profile_id: storage_model.profile_id, connector_mandate_detail: storage_model.connector_mandate_detail, + card_discovery: storage_model.card_discovery, } } } @@ -1670,6 +1673,7 @@ impl DataModelExt for PaymentAttemptNew { shipping_cost: self.net_amount.get_shipping_cost(), order_tax_amount: self.net_amount.get_order_tax_amount(), connector_mandate_detail: self.connector_mandate_detail, + card_discovery: self.card_discovery, } } @@ -1742,6 +1746,7 @@ impl DataModelExt for PaymentAttemptNew { organization_id: storage_model.organization_id, profile_id: storage_model.profile_id, connector_mandate_detail: storage_model.connector_mandate_detail, + card_discovery: storage_model.card_discovery, } } } diff --git a/migrations/2025-01-13-060852_add_card_discovery_in_payment_attempt/down.sql b/migrations/2025-01-13-060852_add_card_discovery_in_payment_attempt/down.sql new file mode 100644 index 00000000000..9b7be0e960a --- /dev/null +++ b/migrations/2025-01-13-060852_add_card_discovery_in_payment_attempt/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_attempt DROP COLUMN IF EXISTS card_discovery; + +DROP TYPE IF EXISTS "CardDiscovery"; diff --git a/migrations/2025-01-13-060852_add_card_discovery_in_payment_attempt/up.sql b/migrations/2025-01-13-060852_add_card_discovery_in_payment_attempt/up.sql new file mode 100644 index 00000000000..657d670bfc8 --- /dev/null +++ b/migrations/2025-01-13-060852_add_card_discovery_in_payment_attempt/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here +CREATE TYPE "CardDiscovery" AS ENUM ('manual', 'saved_card', 'click_to_pay'); + +ALTER TABLE payment_attempt ADD COLUMN IF NOT EXISTS card_discovery "CardDiscovery"; From f0b443eda53bfb7b56679277e6077a8d55974763 Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Wed, 5 Feb 2025 00:14:38 +0530 Subject: [PATCH 11/46] fix(connector): [novalnet] Remove first name, last name as required fields for Applepay, Googlepay, Paypal (#7152) --- .../src/connectors/novalnet/transformers.rs | 12 ++--- crates/hyperswitch_connectors/src/utils.rs | 10 ++++ .../payment_connector_required_fields.rs | 54 ------------------- 3 files changed, 16 insertions(+), 60 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs index 6d3199ecf1a..c1ef6566e03 100644 --- a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs @@ -86,8 +86,8 @@ pub struct NovalnetPaymentsRequestBilling { #[derive(Default, Debug, Serialize, Clone)] pub struct NovalnetPaymentsRequestCustomer { - first_name: Secret, - last_name: Secret, + first_name: Option>, + last_name: Option>, email: Email, mobile: Option>, billing: Option, @@ -215,8 +215,8 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym }; let customer = NovalnetPaymentsRequestCustomer { - first_name: item.router_data.get_billing_first_name()?, - last_name: item.router_data.get_billing_last_name()?, + first_name: item.router_data.get_optional_billing_first_name(), + last_name: item.router_data.get_optional_billing_last_name(), email: item .router_data .get_billing_email() @@ -1477,8 +1477,8 @@ impl TryFrom<&SetupMandateRouterData> for NovalnetPaymentsRequest { }; let customer = NovalnetPaymentsRequestCustomer { - first_name: req_address.get_first_name()?.clone(), - last_name: req_address.get_last_name()?.clone(), + first_name: req_address.get_optional_first_name(), + last_name: req_address.get_optional_last_name(), email: item.request.get_email()?.clone(), mobile: item.get_optional_billing_phone_number(), billing: Some(billing), diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index e99777ad27e..1180cb68f7c 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1188,6 +1188,8 @@ pub trait AddressDetailsData { fn get_optional_city(&self) -> Option; fn get_optional_line1(&self) -> Option>; fn get_optional_line2(&self) -> Option>; + fn get_optional_first_name(&self) -> Option>; + fn get_optional_last_name(&self) -> Option>; } impl AddressDetailsData for AddressDetails { @@ -1296,6 +1298,14 @@ impl AddressDetailsData for AddressDetails { fn get_optional_line2(&self) -> Option> { self.line2.clone() } + + fn get_optional_first_name(&self) -> Option> { + self.first_name.clone() + } + + fn get_optional_last_name(&self) -> Option> { + self.last_name.clone() + } } pub trait PhoneDetailsData { diff --git a/crates/router/src/configs/defaults/payment_connector_required_fields.rs b/crates/router/src/configs/defaults/payment_connector_required_fields.rs index 2ef89f440ad..644c213acd8 100644 --- a/crates/router/src/configs/defaults/payment_connector_required_fields.rs +++ b/crates/router/src/configs/defaults/payment_connector_required_fields.rs @@ -8706,24 +8706,6 @@ impl Default for settings::RequiredFields { non_mandate: HashMap::new(), common: HashMap::from( [ - ( - "billing.address.first_name".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.first_name".to_string(), - display_name: "first_name".to_string(), - field_type: enums::FieldType::UserFullName, - value: None, - } - ), - ( - "billing.address.last_name".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.last_name".to_string(), - display_name: "last_name".to_string(), - field_type: enums::FieldType::UserFullName, - value: None, - } - ), ( "billing.email".to_string(), RequiredFieldInfo { @@ -9032,24 +9014,6 @@ impl Default for settings::RequiredFields { non_mandate: HashMap::new(), common: HashMap::from( [ - ( - "billing.address.first_name".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.first_name".to_string(), - display_name: "first_name".to_string(), - field_type: enums::FieldType::UserFullName, - value: None, - } - ), - ( - "billing.address.last_name".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.last_name".to_string(), - display_name: "last_name".to_string(), - field_type: enums::FieldType::UserFullName, - value: None, - } - ), ( "billing.email".to_string(), RequiredFieldInfo { @@ -9733,24 +9697,6 @@ impl Default for settings::RequiredFields { non_mandate: HashMap::new(), common: HashMap::from( [ - ( - "billing.address.first_name".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.first_name".to_string(), - display_name: "first_name".to_string(), - field_type: enums::FieldType::UserFullName, - value: None, - } - ), - ( - "billing.address.last_name".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.billing.address.last_name".to_string(), - display_name: "last_name".to_string(), - field_type: enums::FieldType::UserFullName, - value: None, - } - ), ( "billing.email".to_string(), RequiredFieldInfo { From a614c200498e6859ac5a936916bc80abeed73f12 Mon Sep 17 00:00:00 2001 From: awasthi21 <107559116+awasthi21@users.noreply.github.com> Date: Wed, 5 Feb 2025 00:15:37 +0530 Subject: [PATCH 12/46] fix(connector): Fix Paybox 3DS failing issue (#7153) Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> --- .../src/connectors/paybox/transformers.rs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/paybox/transformers.rs b/crates/hyperswitch_connectors/src/connectors/paybox/transformers.rs index effe6df28db..9d01036f660 100644 --- a/crates/hyperswitch_connectors/src/connectors/paybox/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/paybox/transformers.rs @@ -148,7 +148,7 @@ pub struct ThreeDSPaymentsRequest { address1: Secret, zip_code: Secret, city: String, - country_code: api_models::enums::CountryAlpha2, + country_code: String, total_quantity: i32, } #[derive(Debug, Serialize, Eq, PartialEq)] @@ -422,7 +422,11 @@ impl TryFrom<&PayboxRouterData<&types::PaymentsAuthorizeRouterData>> for PayboxP address1: address.get_line1()?.clone(), zip_code: address.get_zip()?.clone(), city: address.get_city()?.clone(), - country_code: *address.get_country()?, + country_code: format!( + "{:03}", + common_enums::Country::from_alpha2(*address.get_country()?) + .to_numeric() + ), total_quantity: 1, })) } else { @@ -601,7 +605,7 @@ pub struct TransactionResponse { pub fn parse_url_encoded_to_struct( query_bytes: Bytes, ) -> CustomResult { - let (cow, _, _) = encoding_rs::ISO_8859_10.decode(&query_bytes); + let (cow, _, _) = encoding_rs::ISO_8859_15.decode(&query_bytes); serde_qs::from_str::(cow.as_ref()).change_context(errors::ConnectorError::ParsingFailed) } @@ -609,12 +613,13 @@ pub fn parse_paybox_response( query_bytes: Bytes, is_three_ds: bool, ) -> CustomResult { - let (cow, _, _) = encoding_rs::ISO_8859_10.decode(&query_bytes); - let response_str = cow.as_ref(); - - if response_str.starts_with("") && is_three_ds { + let (cow, _, _) = encoding_rs::ISO_8859_15.decode(&query_bytes); + let response_str = cow.as_ref().trim(); + if (response_str.starts_with("") || response_str.starts_with("")) + && is_three_ds + { let response = response_str.to_string(); - return Ok(if response.contains("Erreur") { + return Ok(if response.contains("Erreur 201") { PayboxResponse::Error(response) } else { PayboxResponse::ThreeDs(response.into()) From 5247a3c6512d39d5468188419cf22d362118ee7b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 00:26:52 +0000 Subject: [PATCH 13/46] chore(version): 2025.02.05.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d3abbda850..a6cc3a151af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2025.02.05.0 + +### Features + +- **router:** Add card_discovery in payment_attempt ([#7039](https://github.com/juspay/hyperswitch/pull/7039)) ([`b9aa3ab`](https://github.com/juspay/hyperswitch/commit/b9aa3ab445e7966dad3f7c09f27e644d5628f61f)) + +### Bug Fixes + +- **connector:** + - [novalnet] Remove first name, last name as required fields for Applepay, Googlepay, Paypal ([#7152](https://github.com/juspay/hyperswitch/pull/7152)) ([`f0b443e`](https://github.com/juspay/hyperswitch/commit/f0b443eda53bfb7b56679277e6077a8d55974763)) + - Fix Paybox 3DS failing issue ([#7153](https://github.com/juspay/hyperswitch/pull/7153)) ([`a614c20`](https://github.com/juspay/hyperswitch/commit/a614c200498e6859ac5a936916bc80abeed73f12)) +- **router:** + - [Cybersource] add flag to indicate final capture ([#7085](https://github.com/juspay/hyperswitch/pull/7085)) ([`55bb284`](https://github.com/juspay/hyperswitch/commit/55bb284ba063dc84e80b4f0d83c82ec7c30ad4c5)) + - Add dynamic fields support for `samsung_pay` ([#7090](https://github.com/juspay/hyperswitch/pull/7090)) ([`e2ddcc2`](https://github.com/juspay/hyperswitch/commit/e2ddcc26b84e4ddcd69005080e19d211b1604827)) +- Invalidate surcharge cache during update ([#6907](https://github.com/juspay/hyperswitch/pull/6907)) ([`8ac1b83`](https://github.com/juspay/hyperswitch/commit/8ac1b83985dbae33afc3b53d46b85a374ff3c1e9)) + +**Full Changelog:** [`2025.02.04.0...2025.02.05.0`](https://github.com/juspay/hyperswitch/compare/2025.02.04.0...2025.02.05.0) + +- - - + ## 2025.02.04.0 ### Features From 6fee3011ea84e08caef8459cd1f55856245e15b2 Mon Sep 17 00:00:00 2001 From: Arindam Sahoo <88739246+arindam-sahoo@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:55:10 +0530 Subject: [PATCH 14/46] refactor(ci): Remove Adyen-specific deprecated PMTs Sofort test cases in Postman (#7099) Co-authored-by: Arindam Sahoo Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> --- .../Flow Testcases/Happy Cases/.meta.json | 19 ++-- .../.meta.json | 0 .../Payments - Confirm/.event.meta.json | 0 .../Payments - Confirm/event.test.js | 0 .../Payments - Confirm/request.json | 0 .../Payments - Confirm/response.json | 0 .../Payments - Create/.event.meta.json | 0 .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 0 .../Payments - Create/response.json | 0 .../Payments - Retrieve/.event.meta.json | 0 .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve/request.json | 0 .../Payments - Retrieve/response.json | 0 .../Payments - Confirm/event.test.js | 103 ------------------ .../Payments - Confirm/request.json | 83 -------------- .../Payments - Create/request.json | 88 --------------- .../.meta.json | 0 .../Payments - Create}/.event.meta.json | 0 .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 0 .../Payments - Create}/response.json | 0 .../.event.meta.json | 0 .../Payments - Retrieve-copy/event.test.js | 0 .../Payments - Retrieve-copy}/request.json | 0 .../Payments - Retrieve-copy}/response.json | 0 .../Payments - Retrieve/.event.meta.json | 0 .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve}/request.json | 0 .../Payments - Retrieve/response.json | 0 .../.event.meta.json | 0 .../Recurring Payments - Create/event.test.js | 0 .../Recurring Payments - Create/request.json | 0 .../response.json | 0 .../Refunds - Create Copy}/.event.meta.json | 0 .../Refunds - Create Copy/event.test.js | 0 .../Refunds - Create Copy/request.json | 0 .../Refunds - Create Copy}/response.json | 0 .../Refunds - Retrieve Copy}/.event.meta.json | 0 .../Refunds - Retrieve Copy/event.test.js | 0 .../Refunds - Retrieve Copy/request.json | 0 .../Refunds - Retrieve Copy}/response.json | 0 .../.meta.json | 0 .../Payments - Confirm}/.event.meta.json | 0 .../Payments - Confirm/event.test.js | 0 .../Payments - Confirm/request.json | 0 .../Payments - Confirm}/response.json | 0 .../Payments - Create}/.event.meta.json | 0 .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 0 .../Payments - Create}/response.json | 0 .../Payments - Retrieve}/.event.meta.json | 0 .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve/request.json | 0 .../Payments - Retrieve}/response.json | 0 .../.meta.json | 0 .../Payments - Confirm/.event.meta.json | 0 .../Payments - Confirm/event.test.js | 0 .../Payments - Confirm/request.json | 0 .../Payments - Confirm/response.json | 0 .../Payments - Create/.event.meta.json | 0 .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 0 .../Payments - Create/response.json | 0 .../Payments - Retrieve/.event.meta.json | 0 .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve/request.json | 0 .../Payments - Retrieve/response.json | 0 .../.meta.json | 0 .../Payments - Confirm/.event.meta.json | 0 .../Payments - Confirm/event.test.js | 0 .../Payments - Confirm/request.json | 0 .../Payments - Confirm/response.json | 0 .../Payments - Create/.event.meta.json | 0 .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 0 .../Payments - Create/response.json | 0 .../Payments - Retrieve/.event.meta.json | 0 .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve/request.json | 0 .../Payments - Retrieve/response.json | 0 .../.meta.json | 0 .../.event.meta.json | 0 .../event.test.js | 0 .../request.json | 0 .../response.json | 0 .../Payments - Create/.event.meta.json | 0 .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 0 .../Payments - Create/response.json | 0 .../Payments - Retrieve Copy/.event.meta.json | 0 .../Payments - Retrieve Copy/event.test.js | 0 .../Payments - Retrieve Copy/request.json | 0 .../Payments - Retrieve Copy}/response.json | 0 .../Refunds - Create/.event.meta.json | 0 .../Refunds - Create/event.test.js | 0 .../Refunds - Create/request.json | 0 .../Refunds - Create}/response.json | 0 .../Refunds - Retrieve/.event.meta.json | 0 .../Refunds - Retrieve/event.test.js | 0 .../Refunds - Retrieve/request.json | 0 .../Refunds - Retrieve}/response.json | 0 .../.event.meta.json | 0 .../event.test.js | 0 .../Save card payments - Confirm/request.json | 0 .../response.json | 0 .../.event.meta.json | 0 .../Save card payments - Create/event.test.js | 0 .../Save card payments - Create/request.json | 0 .../response.json | 0 .../.meta.json | 7 -- .../Payments - Create/event.test.js | 71 ------------ .../Payments - Retrieve/.event.meta.json | 3 - .../Payments - Retrieve/event.test.js | 71 ------------ .../.meta.json | 0 .../.event.meta.json | 0 .../event.test.js | 0 .../request.json | 0 .../response.json | 0 .../Payments - Create/.event.meta.json | 0 .../Payments - Create/event.prerequest.js | 0 .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 0 .../Payments - Create}/response.json | 0 .../Payments - Retrieve/.event.meta.json | 0 .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve/request.json | 0 .../Payments - Retrieve}/response.json | 0 .../.event.meta.json | 0 .../event.test.js | 0 .../Save card payments - Confirm/request.json | 0 .../response.json | 0 .../.event.meta.json | 0 .../Save card payments - Create/event.test.js | 0 .../Save card payments - Create/request.json | 0 .../response.json | 0 .../.meta.json | 0 .../.event.meta.json | 0 .../event.test.js | 0 .../request.json | 0 .../response.json | 0 .../Payments - Create/.event.meta.json | 0 .../Payments - Create/event.prerequest.js | 0 .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 0 .../Payments - Create}/response.json | 0 .../Payments - Retrieve/.event.meta.json | 0 .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve/request.json | 0 .../Payments - Retrieve}/response.json | 0 .../.event.meta.json | 0 .../event.test.js | 0 .../Save card payments - Confirm/request.json | 0 .../response.json | 0 .../.event.meta.json | 0 .../Save card payments - Create/event.test.js | 0 .../Save card payments - Create/request.json | 0 .../response.json | 0 .../.meta.json | 0 .../Payments - Create}/.event.meta.json | 0 .../Payments - Create/event.test.js | 0 .../Payments - Create/request.json | 0 .../Payments - Create}/response.json | 0 .../Payments - Retrieve}/.event.meta.json | 0 .../Payments - Retrieve/event.test.js | 0 .../Payments - Retrieve/request.json | 0 .../Payments - Retrieve}/response.json | 0 .../Save card payments - Create/response.json | 1 - .../Payments - Create/.event.meta.json | 3 - .../Payments - Create/response.json | 1 - .../Payments - Retrieve/.event.meta.json | 3 - .../Payments - Retrieve/request.json | 28 ----- .../Payments - Retrieve/response.json | 1 - .../adyen_uk.postman_collection.json | 2 +- 174 files changed, 10 insertions(+), 474 deletions(-) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-sofort => Scenario11-Bank Redirect-eps}/.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-sofort => Scenario11-Bank Redirect-eps}/Payments - Confirm/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario12-Bank Redirect-eps => Scenario11-Bank Redirect-eps}/Payments - Confirm/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario12-Bank Redirect-eps => Scenario11-Bank Redirect-eps}/Payments - Confirm/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-sofort => Scenario11-Bank Redirect-eps}/Payments - Confirm/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-sofort => Scenario11-Bank Redirect-eps}/Payments - Create/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-sofort => Scenario11-Bank Redirect-eps}/Payments - Create/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario12-Bank Redirect-eps => Scenario11-Bank Redirect-eps}/Payments - Create/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-sofort => Scenario11-Bank Redirect-eps}/Payments - Create/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-sofort => Scenario11-Bank Redirect-eps}/Payments - Retrieve/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-sofort => Scenario11-Bank Redirect-eps}/Payments - Retrieve/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-sofort => Scenario11-Bank Redirect-eps}/Payments - Retrieve/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario11-Bank Redirect-sofort => Scenario11-Bank Redirect-eps}/Payments - Retrieve/response.json (100%) delete mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Confirm/event.test.js delete mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Confirm/request.json delete mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Create/request.json rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment => Scenario12-Refund recurring payment}/.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario12-Bank Redirect-eps/Payments - Confirm => Scenario12-Refund recurring payment/Payments - Create}/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment => Scenario12-Refund recurring payment}/Payments - Create/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment => Scenario12-Refund recurring payment}/Payments - Create/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario12-Bank Redirect-eps/Payments - Confirm => Scenario12-Refund recurring payment/Payments - Create}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario12-Bank Redirect-eps/Payments - Create => Scenario12-Refund recurring payment/Payments - Retrieve-copy}/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment => Scenario12-Refund recurring payment}/Payments - Retrieve-copy/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario12-Bank Redirect-eps/Payments - Retrieve => Scenario12-Refund recurring payment/Payments - Retrieve-copy}/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario12-Bank Redirect-eps/Payments - Create => Scenario12-Refund recurring payment/Payments - Retrieve-copy}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario12-Bank Redirect-eps => Scenario12-Refund recurring payment}/Payments - Retrieve/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment => Scenario12-Refund recurring payment}/Payments - Retrieve/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment/Payments - Retrieve-copy => Scenario12-Refund recurring payment/Payments - Retrieve}/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario12-Bank Redirect-eps => Scenario12-Refund recurring payment}/Payments - Retrieve/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment/Payments - Create => Scenario12-Refund recurring payment/Recurring Payments - Create}/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment => Scenario12-Refund recurring payment}/Recurring Payments - Create/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment => Scenario12-Refund recurring payment}/Recurring Payments - Create/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment/Payments - Create => Scenario12-Refund recurring payment/Recurring Payments - Create}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment/Payments - Retrieve-copy => Scenario12-Refund recurring payment/Refunds - Create Copy}/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment => Scenario12-Refund recurring payment}/Refunds - Create Copy/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment => Scenario12-Refund recurring payment}/Refunds - Create Copy/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment/Payments - Retrieve-copy => Scenario12-Refund recurring payment/Refunds - Create Copy}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment/Payments - Retrieve => Scenario12-Refund recurring payment/Refunds - Retrieve Copy}/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment => Scenario12-Refund recurring payment}/Refunds - Retrieve Copy/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment => Scenario12-Refund recurring payment}/Refunds - Retrieve Copy/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment/Payments - Retrieve => Scenario12-Refund recurring payment/Refunds - Retrieve Copy}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario12-Bank Redirect-eps => Scenario13-Bank debit-ach}/.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment/Recurring Payments - Create => Scenario13-Bank debit-ach/Payments - Confirm}/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario14-Bank debit-ach => Scenario13-Bank debit-ach}/Payments - Confirm/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario14-Bank debit-ach => Scenario13-Bank debit-ach}/Payments - Confirm/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment/Recurring Payments - Create => Scenario13-Bank debit-ach/Payments - Confirm}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment/Refunds - Create Copy => Scenario13-Bank debit-ach/Payments - Create}/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario12-Bank Redirect-eps => Scenario13-Bank debit-ach}/Payments - Create/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario14-Bank debit-ach => Scenario13-Bank debit-ach}/Payments - Create/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment/Refunds - Create Copy => Scenario13-Bank debit-ach/Payments - Create}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment/Refunds - Retrieve Copy => Scenario13-Bank debit-ach/Payments - Retrieve}/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario14-Bank debit-ach => Scenario13-Bank debit-ach}/Payments - Retrieve/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment => Scenario13-Bank debit-ach}/Payments - Retrieve/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario13-Refund recurring payment/Refunds - Retrieve Copy => Scenario13-Bank debit-ach/Payments - Retrieve}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario14-Bank debit-ach => Scenario14-Bank debit-Bacs}/.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario14-Bank debit-ach => Scenario14-Bank debit-Bacs}/Payments - Confirm/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario15-Bank debit-Bacs => Scenario14-Bank debit-Bacs}/Payments - Confirm/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario15-Bank debit-Bacs => Scenario14-Bank debit-Bacs}/Payments - Confirm/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario14-Bank debit-ach => Scenario14-Bank debit-Bacs}/Payments - Confirm/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario14-Bank debit-ach => Scenario14-Bank debit-Bacs}/Payments - Create/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario14-Bank debit-ach => Scenario14-Bank debit-Bacs}/Payments - Create/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario15-Bank debit-Bacs => Scenario14-Bank debit-Bacs}/Payments - Create/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario14-Bank debit-ach => Scenario14-Bank debit-Bacs}/Payments - Create/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario14-Bank debit-ach => Scenario14-Bank debit-Bacs}/Payments - Retrieve/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario15-Bank debit-Bacs => Scenario14-Bank debit-Bacs}/Payments - Retrieve/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario14-Bank debit-ach => Scenario14-Bank debit-Bacs}/Payments - Retrieve/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario14-Bank debit-ach => Scenario14-Bank debit-Bacs}/Payments - Retrieve/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario15-Bank debit-Bacs => Scenario15-Bank Redirect-Trustly}/.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario15-Bank debit-Bacs => Scenario15-Bank Redirect-Trustly}/Payments - Confirm/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario16-Bank Redirect-Trustly => Scenario15-Bank Redirect-Trustly}/Payments - Confirm/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario16-Bank Redirect-Trustly => Scenario15-Bank Redirect-Trustly}/Payments - Confirm/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario15-Bank debit-Bacs => Scenario15-Bank Redirect-Trustly}/Payments - Confirm/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario15-Bank debit-Bacs => Scenario15-Bank Redirect-Trustly}/Payments - Create/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario15-Bank debit-Bacs => Scenario15-Bank Redirect-Trustly}/Payments - Create/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario16-Bank Redirect-Trustly => Scenario15-Bank Redirect-Trustly}/Payments - Create/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario15-Bank debit-Bacs => Scenario15-Bank Redirect-Trustly}/Payments - Create/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario15-Bank debit-Bacs => Scenario15-Bank Redirect-Trustly}/Payments - Retrieve/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario12-Bank Redirect-eps => Scenario15-Bank Redirect-Trustly}/Payments - Retrieve/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario15-Bank debit-Bacs => Scenario15-Bank Redirect-Trustly}/Payments - Retrieve/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario15-Bank debit-Bacs => Scenario15-Bank Redirect-Trustly}/Payments - Retrieve/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/List payment methods for a Customer/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/List payment methods for a Customer/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/List payment methods for a Customer/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario16-Bank Redirect-Trustly/Payments - Confirm => Scenario16-Add card flow/List payment methods for a Customer}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Payments - Create/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Payments - Create/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Payments - Create/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario16-Bank Redirect-Trustly => Scenario16-Add card flow}/Payments - Create/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Payments - Retrieve Copy/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Payments - Retrieve Copy/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Payments - Retrieve Copy/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario16-Bank Redirect-Trustly/Payments - Retrieve => Scenario16-Add card flow/Payments - Retrieve Copy}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Refunds - Create/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Refunds - Create/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Refunds - Create/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow/List payment methods for a Customer => Scenario16-Add card flow/Refunds - Create}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Refunds - Retrieve/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Refunds - Retrieve/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Refunds - Retrieve/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow/Payments - Create => Scenario16-Add card flow/Refunds - Retrieve}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Save card payments - Confirm/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Save card payments - Confirm/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Save card payments - Confirm/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow/Payments - Retrieve Copy => Scenario16-Add card flow/Save card payments - Confirm}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Save card payments - Create/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Save card payments - Create/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow => Scenario16-Add card flow}/Save card payments - Create/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow/Refunds - Create => Scenario16-Add card flow/Save card payments - Create}/response.json (100%) delete mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/.meta.json delete mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Create/event.test.js delete mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Retrieve/.event.meta.json delete mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Retrieve/event.test.js rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/List payment methods for a Customer/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/List payment methods for a Customer/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/List payment methods for a Customer/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow/Refunds - Retrieve => Scenario17-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/Payments - Create/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/Payments - Create/event.prerequest.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/Payments - Create/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/Payments - Create/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow/Save card payments - Confirm => Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/Payments - Retrieve/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/Payments - Retrieve/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/Payments - Retrieve/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario17-Add card flow/Save card payments - Create => Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/Save card payments - Confirm/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/Save card payments - Confirm/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/Save card payments - Confirm/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer => Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/Save card payments - Create/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/Save card payments - Create/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment => Scenario17-Pass Invalid CVV for save card flow and verify failed payment}/Save card payments - Create/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create => Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/List payment methods for a Customer/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/List payment methods for a Customer/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/List payment methods for a Customer/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/Payments - Create/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/Payments - Create/event.prerequest.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/Payments - Create/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/Payments - Create/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/Payments - Retrieve/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/Payments - Retrieve/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/Payments - Retrieve/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/Save card payments - Confirm/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/Save card payments - Confirm/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/Save card payments - Confirm/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/Save card payments - Create/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/Save card payments - Create/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy}/Save card payments - Create/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create => Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario20-Create Gift Card payment => Scenario19-Create Gift Card payment}/.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario16-Bank Redirect-Trustly/Payments - Confirm => Scenario19-Create Gift Card payment/Payments - Create}/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario20-Create Gift Card payment => Scenario19-Create Gift Card payment}/Payments - Create/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario20-Create Gift Card payment => Scenario19-Create Gift Card payment}/Payments - Create/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve => Scenario19-Create Gift Card payment/Payments - Create}/response.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario16-Bank Redirect-Trustly/Payments - Create => Scenario19-Create Gift Card payment/Payments - Retrieve}/.event.meta.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario20-Create Gift Card payment => Scenario19-Create Gift Card payment}/Payments - Retrieve/event.test.js (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario16-Bank Redirect-Trustly => Scenario19-Create Gift Card payment}/Payments - Retrieve/request.json (100%) rename postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/{Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm => Scenario19-Create Gift Card payment/Payments - Retrieve}/response.json (100%) delete mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/response.json delete mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Create/.event.meta.json delete mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Create/response.json delete mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Retrieve/.event.meta.json delete mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Retrieve/request.json delete mode 100644 postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Retrieve/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json index 8bfdb580671..6980f1fe85e 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/.meta.json @@ -10,15 +10,14 @@ "Scenario8-Refund full payment", "Scenario9-Create a mandate and recurring payment", "Scenario10-Partial refund", - "Scenario11-Bank Redirect-sofort", - "Scenario12-Bank Redirect-eps", - "Scenario13-Refund recurring payment", - "Scenario14-Bank debit-ach", - "Scenario15-Bank debit-Bacs", - "Scenario16-Bank Redirect-Trustly", - "Scenario17-Add card flow", - "Scenario18-Pass Invalid CVV for save card flow and verify failed payment", - "Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy", - "Scenario20-Create Gift Card payment" + "Scenario11-Bank Redirect-eps", + "Scenario12-Refund recurring payment", + "Scenario13-Bank debit-ach", + "Scenario14-Bank debit-Bacs", + "Scenario15-Bank Redirect-Trustly", + "Scenario16-Add card flow", + "Scenario17-Pass Invalid CVV for save card flow and verify failed payment", + "Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy", + "Scenario19-Create Gift Card payment" ] } \ No newline at end of file diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Confirm/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Confirm/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Confirm/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Confirm/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Confirm/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Confirm/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Confirm/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Confirm/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Confirm/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Confirm/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Confirm/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Confirm/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Confirm/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Confirm/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Confirm/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Create/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Create/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Create/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Create/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Create/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Create/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Retrieve/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Retrieve/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Retrieve/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Retrieve/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Retrieve/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-eps/Payments - Retrieve/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Confirm/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Confirm/event.test.js deleted file mode 100644 index d4b9fbead7e..00000000000 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Confirm/event.test.js +++ /dev/null @@ -1,103 +0,0 @@ -// Validate status 2xx -pm.test("[POST]::/payments/:id/confirm - Status code is 2xx", function () { - pm.response.to.be.success; -}); - -// Validate if response header has matching content-type -pm.test( - "[POST]::/payments/:id/confirm - 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/:id/confirm - 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 "requires_customer_action" for "status" -if (jsonData?.status) { - pm.test( - "[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'", - function () { - pm.expect(jsonData.status).to.eql("requires_customer_action"); - }, - ); -} - -// Response body should have "next_action.redirect_to_url" -pm.test( - "[POST]::/payments - Content check if 'next_action.redirect_to_url' exists", - function () { - pm.expect(typeof jsonData.next_action.redirect_to_url !== "undefined").to.be - .true; - }, -); - -// Response body should have value "sofort" for "payment_method_type" -if (jsonData?.payment_method_type) { - pm.test( - "[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'sofort'", - function () { - pm.expect(jsonData.payment_method_type).to.eql("sofort"); - }, - ); -} - -// Response body should have value "stripe" for "connector" -if (jsonData?.connector) { - pm.test( - "[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'", - function () { - pm.expect(jsonData.connector).to.eql("adyen"); - }, - ); -} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Confirm/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Confirm/request.json deleted file mode 100644 index 001749a500f..00000000000 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Confirm/request.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_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_json_formatted": { - "client_secret": "{{client_secret}}", - "payment_method": "bank_redirect", - "payment_method_type": "sofort", - "payment_method_data": { - "bank_redirect": { - "sofort": { - "billing_details": { - "billing_name": "John Doe" - }, - "bank_name": "ing", - "preferred_language": "en", - "country": "DE" - } - } - }, - "browser_info": { - "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", - "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", - "language": "nl-NL", - "color_depth": 24, - "screen_height": 723, - "screen_width": 1536, - "time_zone": 0, - "java_enabled": true, - "java_script_enabled": true, - "ip_address": "127.0.0.1" - } - } - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id", "confirm"], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" -} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Create/request.json deleted file mode 100644 index 0b0c56d2660..00000000000 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario11-Bank Redirect-sofort/Payments - Create/request.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "method": "POST", - "header": [ - { - "key": "Content-Type", - "value": "application/json" - }, - { - "key": "Accept", - "value": "application/json" - } - ], - "body": { - "mode": "raw", - "options": { - "raw": { - "language": "json" - } - }, - "raw_json_formatted": { - "amount": 1000, - "currency": "EUR", - "confirm": false, - "capture_method": "automatic", - "capture_on": "2022-09-10T10:11:12Z", - "amount_to_capture": 1000, - "customer_id": "StripeCustomer", - "email": "abcdef123@gmail.com", - "name": "John Doe", - "phone": "999999999", - "phone_country_code": "+65", - "description": "Its my first payment request", - "authentication_type": "three_ds", - "return_url": "https://duck.com", - "billing": { - "address": { - "first_name": "John", - "last_name": "Doe", - "line1": "1467", - "line2": "Harrison Street", - "line3": "Harrison Street", - "city": "San Fransico", - "state": "California", - "zip": "94122", - "country": "DE" - } - }, - "browser_info": { - "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36", - "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", - "language": "nl-NL", - "color_depth": 24, - "screen_height": 723, - "screen_width": 1536, - "time_zone": 0, - "java_enabled": true, - "java_script_enabled": true, - "ip_address": "127.0.0.1" - }, - "shipping": { - "address": { - "line1": "1467", - "line2": "Harrison Street", - "line3": "Harrison Street", - "city": "San Fransico", - "state": "California", - "zip": "94122", - "country": "US", - "first_name": "John", - "last_name": "Doe" - } - }, - "statement_descriptor_name": "joseph", - "statement_descriptor_suffix": "JS", - "metadata": { - "udf1": "value1", - "new_customer": "true", - "login_date": "2019-09-10T10:11:12Z" - } - } - }, - "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" -} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Confirm/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Confirm/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Create/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Create/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Create/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Create/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Create/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Confirm/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Confirm/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve-copy/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve-copy/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve-copy/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve-copy/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve-copy/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve-copy/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve-copy/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Retrieve/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve-copy/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve-copy/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Create/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve-copy/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve-copy/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve-copy/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Retrieve/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Payments - Retrieve/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Recurring Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Recurring Payments - Create/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Recurring Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Recurring Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Recurring Payments - Create/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Recurring Payments - Create/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Recurring Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Recurring Payments - Create/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Recurring Payments - Create/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Recurring Payments - Create/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Recurring Payments - Create/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Create/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Recurring Payments - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve-copy/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Create Copy/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve-copy/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Create Copy/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Create Copy/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Create Copy/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Create Copy/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Create Copy/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Create Copy/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Create Copy/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Create Copy/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Create Copy/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve-copy/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Create Copy/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve-copy/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Create Copy/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Retrieve Copy/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Retrieve Copy/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Retrieve Copy/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Retrieve Copy/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Retrieve Copy/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Retrieve Copy/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Retrieve Copy/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Retrieve Copy/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Retrieve Copy/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Retrieve Copy/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Retrieve Copy/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Refund recurring payment/Refunds - Retrieve Copy/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Recurring Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Confirm/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Recurring Payments - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Confirm/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Confirm/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Confirm/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Confirm/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Confirm/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Confirm/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Confirm/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Confirm/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Confirm/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Recurring Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Recurring Payments - Create/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Confirm/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Create Copy/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Create Copy/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Create/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Create/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Create/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Create/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Create/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Create Copy/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Create Copy/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Retrieve Copy/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Retrieve Copy/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Retrieve/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Retrieve/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Payments - Retrieve/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Retrieve/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Retrieve Copy/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Refund recurring payment/Refunds - Retrieve Copy/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario13-Bank debit-ach/Payments - Retrieve/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Confirm/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Confirm/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Confirm/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Confirm/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Confirm/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Confirm/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Confirm/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Confirm/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Confirm/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Confirm/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Confirm/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Confirm/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Confirm/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Confirm/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Confirm/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Create/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Create/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Create/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Create/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Create/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Create/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Retrieve/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Retrieve/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Retrieve/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Retrieve/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-ach/Payments - Retrieve/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario14-Bank debit-Bacs/Payments - Retrieve/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Confirm/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Confirm/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Confirm/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Confirm/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Confirm/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Confirm/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Confirm/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Confirm/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Confirm/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Confirm/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Confirm/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Confirm/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Confirm/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Confirm/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Confirm/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Create/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Create/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Create/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Create/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Create/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Create/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario12-Bank Redirect-eps/Payments - Retrieve/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Retrieve/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Retrieve/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Retrieve/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank debit-Bacs/Payments - Retrieve/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario15-Bank Redirect-Trustly/Payments - Retrieve/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/List payment methods for a Customer/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/List payment methods for a Customer/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/List payment methods for a Customer/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/List payment methods for a Customer/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/List payment methods for a Customer/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/List payment methods for a Customer/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/List payment methods for a Customer/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/List payment methods for a Customer/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/List payment methods for a Customer/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/List payment methods for a Customer/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/List payment methods for a Customer/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/List payment methods for a Customer/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Confirm/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/List payment methods for a Customer/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Confirm/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/List payment methods for a Customer/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Create/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Create/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Create/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Create/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Create/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Create/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Retrieve Copy/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Retrieve Copy/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Retrieve Copy/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Retrieve Copy/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Retrieve Copy/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Retrieve Copy/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Retrieve Copy/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Retrieve Copy/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Retrieve Copy/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Retrieve Copy/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Retrieve Copy/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Retrieve Copy/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Retrieve Copy/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Retrieve/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Payments - Retrieve Copy/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Create/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Create/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Create/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Create/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Create/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Create/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Create/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/List payment methods for a Customer/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Create/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/List payment methods for a Customer/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Retrieve/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Retrieve/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Retrieve/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Retrieve/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Retrieve/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Retrieve/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Retrieve/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Retrieve/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Create/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Refunds - Retrieve/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Confirm/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Confirm/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Confirm/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Confirm/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Confirm/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Confirm/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Confirm/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Confirm/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Confirm/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Confirm/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Confirm/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Confirm/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Retrieve Copy/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Payments - Retrieve Copy/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Confirm/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Create/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Create/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Create/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Create/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Create/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Create/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Create/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Create/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Add card flow/Save card payments - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/.meta.json deleted file mode 100644 index 57d3f8e2bc7..00000000000 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/.meta.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "childrenOrder": [ - "Payments - Create", - "Payments - Confirm", - "Payments - Retrieve" - ] -} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Create/event.test.js deleted file mode 100644 index 0444324000a..00000000000 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Create/event.test.js +++ /dev/null @@ -1,71 +0,0 @@ -// 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 "requires_payment_method" for "status" -if (jsonData?.status) { - pm.test( - "[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'", - function () { - pm.expect(jsonData.status).to.eql("requires_payment_method"); - }, - ); -} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Retrieve/.event.meta.json deleted file mode 100644 index 0731450e6b2..00000000000 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Retrieve/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Retrieve/event.test.js deleted file mode 100644 index 9053ddab13b..00000000000 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Retrieve/event.test.js +++ /dev/null @@ -1,71 +0,0 @@ -// Validate status 2xx -pm.test("[GET]::/payments/:id - Status code is 2xx", function () { - pm.response.to.be.success; -}); - -// Validate if response header has matching content-type -pm.test("[GET]::/payments/:id - 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("[GET]::/payments/:id - 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 "requires_customer_action" for "status" -if (jsonData?.status) { - pm.test( - "[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'", - function () { - pm.expect(jsonData.status).to.eql("requires_customer_action"); - }, - ); -} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Refunds - Retrieve/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/event.prerequest.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/event.prerequest.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/event.prerequest.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Confirm/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Confirm/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Add card flow/Save card payments - Create/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/List payment methods for a Customer/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Create/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario17-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Payments - Retrieve/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/event.prerequest.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/event.prerequest.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/event.prerequest.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/event.prerequest.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Confirm/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Pass Invalid CVV for save card flow and verify failed payment/Save card payments - Create/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/List payment methods for a Customer/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Create/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Confirm/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Create/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Confirm/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Create/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Create/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Create/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Create/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Create/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Create/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Create/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Create/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Payments - Retrieve/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Create/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Retrieve/.event.meta.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Create/.event.meta.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Retrieve/.event.meta.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Retrieve/event.test.js similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Retrieve/event.test.js rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Retrieve/event.test.js diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Retrieve/request.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario16-Bank Redirect-Trustly/Payments - Retrieve/request.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Retrieve/request.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Retrieve/response.json similarity index 100% rename from postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Confirm/response.json rename to postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Create Gift Card payment/Payments - Retrieve/response.json diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/response.json deleted file mode 100644 index fe51488c706..00000000000 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy/Save card payments - Create/response.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Create/.event.meta.json deleted file mode 100644 index 0731450e6b2..00000000000 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Create/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Create/response.json deleted file mode 100644 index fe51488c706..00000000000 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Create/response.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Retrieve/.event.meta.json deleted file mode 100644 index 0731450e6b2..00000000000 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Retrieve/.event.meta.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "eventOrder": ["event.test.js"] -} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Retrieve/request.json deleted file mode 100644 index 6cd4b7d96c5..00000000000 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Retrieve/request.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": ["{{baseUrl}}"], - "path": ["payments", ":id"], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" -} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Retrieve/response.json deleted file mode 100644 index fe51488c706..00000000000 --- a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario20-Create Gift Card payment/Payments - Retrieve/response.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index 512f84c6a4b..ebf353bb88d 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -14375,4 +14375,4 @@ "type": "string" } ] -} +} \ No newline at end of file From e0ec27d936fc62a6feb2f8f643a218f3ad7483b5 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:24:57 +0530 Subject: [PATCH 15/46] feat(core): google pay decrypt flow (#6991) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> --- .deepsource.toml | 2 +- Cargo.lock | 9 + Cargo.toml | 2 +- INSTALL_dependencies.sh | 2 +- api-reference-v2/openapi_spec.json | 5 + api-reference/openapi_spec.json | 5 + config/config.example.toml | 3 + config/deployments/env_specific.toml | 3 + config/development.toml | 3 + crates/api_models/src/admin.rs | 4 + crates/api_models/src/payments.rs | 51 ++ crates/cards/src/lib.rs | 4 +- crates/common_enums/src/enums.rs | 10 + crates/common_utils/Cargo.toml | 1 + crates/common_utils/src/custom_serde.rs | 16 +- crates/common_utils/src/lib.rs | 10 + .../connectors/bankofamerica/transformers.rs | 10 + .../connectors/cybersource/transformers.rs | 162 +++++- .../src/connectors/fiuu/transformers.rs | 3 + .../src/connectors/gocardless/transformers.rs | 4 +- .../src/connectors/mollie/transformers.rs | 3 + .../src/connectors/square/transformers.rs | 3 + .../src/connectors/stax/transformers.rs | 6 + .../src/connectors/wellsfargo/transformers.rs | 6 + .../src/router_data.rs | 22 + crates/masking/src/secret.rs | 7 + crates/router/Cargo.toml | 2 + .../src/configs/secrets_transformers.rs | 33 ++ crates/router/src/configs/settings.rs | 11 + crates/router/src/configs/validations.rs | 15 + .../src/connector/braintree/transformers.rs | 6 + .../src/connector/checkout/transformers.rs | 6 + .../src/connector/payme/transformers.rs | 3 + .../src/connector/stripe/transformers.rs | 3 + crates/router/src/core/errors.rs | 54 ++ crates/router/src/core/payments.rs | 144 ++++- crates/router/src/core/payments/helpers.rs | 544 ++++++++++++++++++ .../router/src/core/payments/tokenization.rs | 5 + crates/router/src/types.rs | 3 +- 39 files changed, 1158 insertions(+), 27 deletions(-) diff --git a/.deepsource.toml b/.deepsource.toml index 3cac181614e..8d0b3c501af 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -13,4 +13,4 @@ name = "rust" enabled = true [analyzers.meta] -msrv = "1.78.0" +msrv = "1.80.0" diff --git a/Cargo.lock b/Cargo.lock index 7583fb2eeee..7e2ab6eccd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1528,6 +1528,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c6d128af408d8ebd08331f0331cf2cf20d19e6c44a7aec58791641ecc8c0b5" + [[package]] name = "base64-simd" version = "0.8.0" @@ -2044,6 +2050,7 @@ version = "0.1.0" dependencies = [ "async-trait", "base64 0.22.1", + "base64-serde", "blake3", "bytes 1.7.1", "common_enums", @@ -6581,6 +6588,7 @@ dependencies = [ "external_services", "futures 0.3.30", "hex", + "hkdf", "http 0.2.12", "hyper 0.14.30", "hyperswitch_connectors", @@ -6629,6 +6637,7 @@ dependencies = [ "serde_with", "serial_test", "sha1", + "sha2", "storage_impl", "strum 0.26.3", "tera", diff --git a/Cargo.toml b/Cargo.toml index dc60c64f667..94e717c5b79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" members = ["crates/*"] package.edition = "2021" -package.rust-version = "1.78.0" +package.rust-version = "1.80.0" package.license = "Apache-2.0" [workspace.dependencies] diff --git a/INSTALL_dependencies.sh b/INSTALL_dependencies.sh index e1d666b97a3..bc7d502b357 100755 --- a/INSTALL_dependencies.sh +++ b/INSTALL_dependencies.sh @@ -9,7 +9,7 @@ if [[ "${TRACE-0}" == "1" ]]; then set -o xtrace fi -RUST_MSRV="1.78.0" +RUST_MSRV="1.80.0" _DB_NAME="hyperswitch_db" _DB_USER="db_user" _DB_PASS="db_password" diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index b315800b08c..447010178de 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -6875,6 +6875,11 @@ "type": "object", "description": "This field contains the Paze certificates and credentials", "nullable": true + }, + "google_pay": { + "type": "object", + "description": "This field contains the Google Pay certificates and credentials", + "nullable": true } }, "additionalProperties": false diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 0aa2c898fc6..fa631eee788 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -9523,6 +9523,11 @@ "type": "object", "description": "This field contains the Paze certificates and credentials", "nullable": true + }, + "google_pay": { + "type": "object", + "description": "This field contains the Google Pay certificates and credentials", + "nullable": true } }, "additionalProperties": false diff --git a/config/config.example.toml b/config/config.example.toml index b76905eb534..c113d7abd57 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -607,6 +607,9 @@ apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" # Private key paze_private_key = "PAZE_PRIVATE_KEY" # Base 64 Encoded Private Key File cakey.pem generated for Paze -> Command to create private key: openssl req -newkey rsa:2048 -x509 -keyout cakey.pem -out cacert.pem -days 365 paze_private_key_passphrase = "PAZE_PRIVATE_KEY_PASSPHRASE" # PEM Passphrase used for generating Private Key File cakey.pem +[google_pay_decrypt_keys] +google_pay_root_signing_keys = "GOOGLE_PAY_ROOT_SIGNING_KEYS" # Base 64 Encoded Root Signing Keys provided by Google Pay (https://developers.google.com/pay/api/web/guides/resources/payment-data-cryptography) + [applepay_merchant_configs] # Run below command to get common merchant identifier for applepay in shell # diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index 8c846ff422e..4e5fa3dba44 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -34,6 +34,9 @@ apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" # Private key paze_private_key = "PAZE_PRIVATE_KEY" # Base 64 Encoded Private Key File cakey.pem generated for Paze -> Command to create private key: openssl req -newkey rsa:2048 -x509 -keyout cakey.pem -out cacert.pem -days 365 paze_private_key_passphrase = "PAZE_PRIVATE_KEY_PASSPHRASE" # PEM Passphrase used for generating Private Key File cakey.pem +[google_pay_decrypt_keys] +google_pay_root_signing_keys = "GOOGLE_PAY_ROOT_SIGNING_KEYS" # Base 64 Encoded Root Signing Keys provided by Google Pay (https://developers.google.com/pay/api/web/guides/resources/payment-data-cryptography) + [applepay_merchant_configs] common_merchant_identifier = "APPLE_PAY_COMMON_MERCHANT_IDENTIFIER" # Refer to config.example.toml to learn how you can generate this value merchant_cert = "APPLE_PAY_MERCHANT_CERTIFICATE" # Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate diff --git a/config/development.toml b/config/development.toml index 8209d49565f..2b81123d873 100644 --- a/config/development.toml +++ b/config/development.toml @@ -671,6 +671,9 @@ apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" paze_private_key = "PAZE_PRIVATE_KEY" paze_private_key_passphrase = "PAZE_PRIVATE_KEY_PASSPHRASE" +[google_pay_decrypt_keys] +google_pay_root_signing_keys = "GOOGLE_PAY_ROOT_SIGNING_KEYS" # Base 64 Encoded Root Signing Keys provided by Google Pay (https://developers.google.com/pay/api/web/guides/resources/payment-data-cryptography) + [generic_link] [generic_link.payment_method_collect] sdk_url = "http://localhost:9050/HyperLoader.js" diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 321ee5305f1..e47f7826d59 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1468,6 +1468,10 @@ pub struct ConnectorWalletDetails { #[serde(skip_serializing_if = "Option::is_none")] #[schema(value_type = Option)] pub paze: Option, + /// This field contains the Google Pay certificates and credentials + #[serde(skip_serializing_if = "Option::is_none")] + #[schema(value_type = Option)] + pub google_pay: Option, } /// Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc." diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 2c8723857a8..3a6fe000afe 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -5984,6 +5984,57 @@ pub struct SessionTokenForSimplifiedApplePay { pub merchant_business_country: Option, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GooglePayWalletDetails { + pub google_pay: GooglePayDetails, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GooglePayDetails { + pub provider_details: GooglePayProviderDetails, +} + +// Google Pay Provider Details can of two types: GooglePayMerchantDetails or GooglePayHyperSwitchDetails +// GooglePayHyperSwitchDetails is not implemented yet +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(untagged)] +pub enum GooglePayProviderDetails { + GooglePayMerchantDetails(GooglePayMerchantDetails), +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GooglePayMerchantDetails { + pub merchant_info: GooglePayMerchantInfo, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GooglePayMerchantInfo { + pub merchant_name: String, + pub tokenization_specification: GooglePayTokenizationSpecification, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GooglePayTokenizationSpecification { + #[serde(rename = "type")] + pub tokenization_type: GooglePayTokenizationType, + pub parameters: GooglePayTokenizationParameters, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum GooglePayTokenizationType { + PaymentGateway, + Direct, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GooglePayTokenizationParameters { + pub gateway: String, + pub public_key: Secret, + pub private_key: Secret, + pub recipient_id: Option>, +} + #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, ToSchema)] #[serde(tag = "wallet_name")] #[serde(rename_all = "snake_case")] diff --git a/crates/cards/src/lib.rs b/crates/cards/src/lib.rs index 91cb93301a9..0c0fb9d47a1 100644 --- a/crates/cards/src/lib.rs +++ b/crates/cards/src/lib.rs @@ -35,7 +35,7 @@ impl<'de> Deserialize<'de> for CardSecurityCode { } } -#[derive(Serialize)] +#[derive(Clone, Debug, Serialize)] pub struct CardExpirationMonth(StrongSecret); impl CardExpirationMonth { @@ -67,7 +67,7 @@ impl<'de> Deserialize<'de> for CardExpirationMonth { } } -#[derive(Serialize)] +#[derive(Clone, Debug, Serialize)] pub struct CardExpirationYear(StrongSecret); impl CardExpirationYear { diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index c30c9ec9f14..2f5249ded57 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -3675,3 +3675,13 @@ pub enum FeatureStatus { NotSupported, Supported, } + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum GooglePayAuthMethod { + /// Contain pan data only + PanOnly, + /// Contain cryptogram data along with pan data + #[serde(rename = "CRYPTOGRAM_3DS")] + Cryptogram, +} diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index dcdb4c760b1..3d31c7a6a98 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -26,6 +26,7 @@ payment_methods_v2 = [] [dependencies] async-trait = { version = "0.1.79", optional = true } base64 = "0.22.0" +base64-serde = "0.8.0" blake3 = { version = "1.5.1", features = ["serde"] } bytes = "1.6.0" diesel = "2.2.3" diff --git a/crates/common_utils/src/custom_serde.rs b/crates/common_utils/src/custom_serde.rs index 63ef30011f7..06087f082e0 100644 --- a/crates/common_utils/src/custom_serde.rs +++ b/crates/common_utils/src/custom_serde.rs @@ -203,8 +203,20 @@ pub mod timestamp { /// pub mod json_string { - use serde::de::{self, Deserialize, DeserializeOwned, Deserializer}; - use serde_json; + use serde::{ + de::{self, Deserialize, DeserializeOwned, Deserializer}, + ser::{self, Serialize, Serializer}, + }; + + /// Serialize a type to json_string format + pub fn serialize(value: &T, serializer: S) -> Result + where + T: Serialize, + S: Serializer, + { + let j = serde_json::to_string(value).map_err(ser::Error::custom)?; + j.serialize(serializer) + } /// Deserialize a string which is in json format pub fn deserialize<'de, T, D>(deserializer: D) -> Result diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index 463ec3ee1b6..fd0c935035f 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -35,6 +35,8 @@ pub mod hashing; #[cfg(feature = "metrics")] pub mod metrics; +pub use base64_serializer::Base64Serializer; + /// Date-time utilities. pub mod date_time { #[cfg(feature = "async_ext")] @@ -296,6 +298,14 @@ pub trait DbConnectionParams { } } +// Can't add doc comments for macro invocations, neither does the macro allow it. +#[allow(missing_docs)] +mod base64_serializer { + use base64_serde::base64_serde_type; + + base64_serde_type!(pub Base64Serializer, crate::consts::BASE64_ENGINE); +} + #[cfg(test)] mod nanoid_tests { #![allow(clippy::unwrap_used)] diff --git a/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs b/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs index 3602f771928..b33555a7c55 100644 --- a/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs @@ -962,6 +962,12 @@ impl TryFrom<&BankOfAmericaRouterData<&PaymentsAuthorizeRouterData>> PaymentMethodToken::PazeDecrypt(_) => Err( unimplemented_payment_method!("Paze", "Bank Of America"), )?, + PaymentMethodToken::GooglePayDecrypt(_) => { + Err(unimplemented_payment_method!( + "Google Pay", + "Bank Of America" + ))? + } }, None => { let email = item.router_data.request.get_email()?; @@ -2279,6 +2285,10 @@ impl TryFrom<(&SetupMandateRouterData, ApplePayWalletData)> for BankOfAmericaPay PaymentMethodToken::PazeDecrypt(_) => { Err(unimplemented_payment_method!("Paze", "Bank Of America"))? } + PaymentMethodToken::GooglePayDecrypt(_) => Err(unimplemented_payment_method!( + "Google Pay", + "Bank Of America" + ))?, }, None => PaymentInformation::from(&apple_pay_data), }; diff --git a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs index 5fc9cad4ffe..b14ffc9c79e 100644 --- a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs @@ -24,7 +24,8 @@ use hyperswitch_domain_models::{ }, router_data::{ AdditionalPaymentMethodConnectorResponse, ApplePayPredecryptData, ConnectorAuthType, - ConnectorResponseData, ErrorResponse, PaymentMethodToken, RouterData, + ConnectorResponseData, ErrorResponse, GooglePayDecryptedData, PaymentMethodToken, + RouterData, }, router_flow_types::{ payments::Authorize, @@ -196,9 +197,9 @@ impl TryFrom<&SetupMandateRouterData> for CybersourceZeroMandateRequest { ApplePayPaymentInformation { tokenized_card: TokenizedCard { number: decrypt_data.application_primary_account_number, - cryptogram: decrypt_data - .payment_data - .online_payment_cryptogram, + cryptogram: Some( + decrypt_data.payment_data.online_payment_cryptogram, + ), transaction_type: TransactionType::ApplePay, expiration_year, expiration_month, @@ -216,6 +217,9 @@ impl TryFrom<&SetupMandateRouterData> for CybersourceZeroMandateRequest { PaymentMethodToken::PazeDecrypt(_) => { Err(unimplemented_payment_method!("Paze", "Cybersource"))? } + PaymentMethodToken::GooglePayDecrypt(_) => { + Err(unimplemented_payment_method!("Google Pay", "Cybersource"))? + } }, None => ( PaymentInformation::ApplePayToken(Box::new( @@ -233,15 +237,17 @@ impl TryFrom<&SetupMandateRouterData> for CybersourceZeroMandateRequest { ), }, WalletData::GooglePay(google_pay_data) => ( - PaymentInformation::GooglePay(Box::new(GooglePayPaymentInformation { - fluid_data: FluidData { - value: Secret::from( - consts::BASE64_ENGINE - .encode(google_pay_data.tokenization_data.token), - ), - descriptor: None, + PaymentInformation::GooglePayToken(Box::new( + GooglePayTokenPaymentInformation { + fluid_data: FluidData { + value: Secret::from( + consts::BASE64_ENGINE + .encode(google_pay_data.tokenization_data.token), + ), + descriptor: None, + }, }, - })), + )), Some(PaymentSolution::GooglePay), ), WalletData::AliPayQr(_) @@ -448,7 +454,7 @@ pub struct TokenizedCard { number: Secret, expiration_month: Secret, expiration_year: Secret, - cryptogram: Secret, + cryptogram: Option>, transaction_type: TransactionType, } @@ -491,10 +497,16 @@ pub const FLUID_DATA_DESCRIPTOR_FOR_SAMSUNG_PAY: &str = "FID=COMMON.SAMSUNG.INAP #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] -pub struct GooglePayPaymentInformation { +pub struct GooglePayTokenPaymentInformation { fluid_data: FluidData, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GooglePayPaymentInformation { + tokenized_card: TokenizedCard, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct SamsungPayTokenizedCard { @@ -520,6 +532,7 @@ pub struct SamsungPayFluidDataValue { #[serde(untagged)] pub enum PaymentInformation { Cards(Box), + GooglePayToken(Box), GooglePay(Box), ApplePay(Box), ApplePayToken(Box), @@ -588,6 +601,8 @@ pub enum TransactionType { ApplePay, #[serde(rename = "1")] SamsungPay, + #[serde(rename = "1")] + GooglePay, } impl From for String { @@ -1645,7 +1660,7 @@ impl PaymentInformation::ApplePay(Box::new(ApplePayPaymentInformation { tokenized_card: TokenizedCard { number: apple_pay_data.application_primary_account_number, - cryptogram: apple_pay_data.payment_data.online_payment_cryptogram, + cryptogram: Some(apple_pay_data.payment_data.online_payment_cryptogram), transaction_type: TransactionType::ApplePay, expiration_year, expiration_month, @@ -1707,7 +1722,7 @@ impl let order_information = OrderInformationWithBill::from((item, Some(bill_to))); let payment_information = - PaymentInformation::GooglePay(Box::new(GooglePayPaymentInformation { + PaymentInformation::GooglePayToken(Box::new(GooglePayTokenPaymentInformation { fluid_data: FluidData { value: Secret::from( consts::BASE64_ENGINE.encode(google_pay_data.tokenization_data.token), @@ -1736,6 +1751,92 @@ impl } } +impl + TryFrom<( + &CybersourceRouterData<&PaymentsAuthorizeRouterData>, + Box, + GooglePayWalletData, + )> for CybersourcePaymentsRequest +{ + type Error = error_stack::Report; + fn try_from( + (item, google_pay_decrypted_data, google_pay_data): ( + &CybersourceRouterData<&PaymentsAuthorizeRouterData>, + Box, + GooglePayWalletData, + ), + ) -> Result { + let email = item + .router_data + .get_billing_email() + .or(item.router_data.request.get_email())?; + let bill_to = build_bill_to(item.router_data.get_optional_billing(), email)?; + let order_information = OrderInformationWithBill::from((item, Some(bill_to))); + + let payment_information = + PaymentInformation::GooglePay(Box::new(GooglePayPaymentInformation { + tokenized_card: TokenizedCard { + number: Secret::new( + google_pay_decrypted_data + .payment_method_details + .pan + .get_card_no(), + ), + cryptogram: google_pay_decrypted_data.payment_method_details.cryptogram, + transaction_type: TransactionType::GooglePay, + expiration_year: Secret::new( + google_pay_decrypted_data + .payment_method_details + .expiration_year + .four_digits(), + ), + expiration_month: Secret::new( + google_pay_decrypted_data + .payment_method_details + .expiration_month + .two_digits(), + ), + }, + })); + let processing_information = ProcessingInformation::try_from(( + item, + Some(PaymentSolution::GooglePay), + Some(google_pay_data.info.card_network.clone()), + ))?; + let client_reference_information = ClientReferenceInformation::from(item); + let merchant_defined_information = item + .router_data + .request + .metadata + .clone() + .map(convert_metadata_to_merchant_defined_info); + + let ucaf_collection_indicator = + match google_pay_data.info.card_network.to_lowercase().as_str() { + "mastercard" => Some("2".to_string()), + _ => None, + }; + + Ok(Self { + processing_information, + payment_information, + order_information, + client_reference_information, + consumer_authentication_information: Some(CybersourceConsumerAuthInformation { + ucaf_collection_indicator, + cavv: None, + ucaf_authentication_data: None, + xid: None, + directory_server_transaction_id: None, + specification_version: None, + pa_specification_version: None, + veres_enrolled: None, + }), + merchant_defined_information, + }) + } +} + impl TryFrom<( &CybersourceRouterData<&PaymentsAuthorizeRouterData>, @@ -1850,6 +1951,9 @@ impl TryFrom<&CybersourceRouterData<&PaymentsAuthorizeRouterData>> for Cybersour PaymentMethodToken::PazeDecrypt(_) => { Err(unimplemented_payment_method!("Paze", "Cybersource"))? } + PaymentMethodToken::GooglePayDecrypt(_) => Err( + unimplemented_payment_method!("Google Pay", "Cybersource"), + )?, }, None => { let email = item @@ -1917,7 +2021,31 @@ impl TryFrom<&CybersourceRouterData<&PaymentsAuthorizeRouterData>> for Cybersour } } WalletData::GooglePay(google_pay_data) => { - Self::try_from((item, google_pay_data)) + match item.router_data.payment_method_token.clone() { + Some(payment_method_token) => match payment_method_token { + PaymentMethodToken::GooglePayDecrypt(decrypt_data) => { + Self::try_from((item, decrypt_data, google_pay_data)) + } + PaymentMethodToken::Token(_) => { + Err(unimplemented_payment_method!( + "Apple Pay", + "Manual", + "Cybersource" + ))? + } + PaymentMethodToken::PazeDecrypt(_) => { + Err(unimplemented_payment_method!("Paze", "Cybersource"))? + } + PaymentMethodToken::ApplePayDecrypt(_) => { + Err(unimplemented_payment_method!( + "Apple Pay", + "Simplified", + "Cybersource" + ))? + } + }, + None => Self::try_from((item, google_pay_data)), + } } WalletData::SamsungPay(samsung_pay_data) => { Self::try_from((item, samsung_pay_data)) diff --git a/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs b/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs index 51fc23eee41..115079c7d00 100644 --- a/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs @@ -500,6 +500,9 @@ impl TryFrom<&FiuuRouterData<&PaymentsAuthorizeRouterData>> for FiuuPaymentReque PaymentMethodToken::PazeDecrypt(_) => { Err(unimplemented_payment_method!("Paze", "Fiuu"))? } + PaymentMethodToken::GooglePayDecrypt(_) => { + Err(unimplemented_payment_method!("Google Pay", "Fiuu"))? + } } } WalletData::AliPayQr(_) diff --git a/crates/hyperswitch_connectors/src/connectors/gocardless/transformers.rs b/crates/hyperswitch_connectors/src/connectors/gocardless/transformers.rs index d64c2ed8efd..f09d30efc9d 100644 --- a/crates/hyperswitch_connectors/src/connectors/gocardless/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/gocardless/transformers.rs @@ -437,7 +437,9 @@ impl TryFrom<&types::SetupMandateRouterData> for GocardlessMandateRequest { let payment_method_token = item.get_payment_method_token()?; let customer_bank_account = match payment_method_token { PaymentMethodToken::Token(token) => Ok(token), - PaymentMethodToken::ApplePayDecrypt(_) | PaymentMethodToken::PazeDecrypt(_) => { + PaymentMethodToken::ApplePayDecrypt(_) + | PaymentMethodToken::PazeDecrypt(_) + | PaymentMethodToken::GooglePayDecrypt(_) => { Err(errors::ConnectorError::NotImplemented( "Setup Mandate flow for selected payment method through Gocardless".to_string(), )) diff --git a/crates/hyperswitch_connectors/src/connectors/mollie/transformers.rs b/crates/hyperswitch_connectors/src/connectors/mollie/transformers.rs index 5a641327c96..fa2cb46c7b7 100644 --- a/crates/hyperswitch_connectors/src/connectors/mollie/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/mollie/transformers.rs @@ -175,6 +175,9 @@ impl TryFrom<&MollieRouterData<&types::PaymentsAuthorizeRouterData>> for MollieP PaymentMethodToken::PazeDecrypt(_) => { Err(unimplemented_payment_method!("Paze", "Mollie"))? } + PaymentMethodToken::GooglePayDecrypt(_) => { + Err(unimplemented_payment_method!("Google Pay", "Mollie"))? + } }), }, ))) diff --git a/crates/hyperswitch_connectors/src/connectors/square/transformers.rs b/crates/hyperswitch_connectors/src/connectors/square/transformers.rs index 5abc93b3f88..ff3999aee64 100644 --- a/crates/hyperswitch_connectors/src/connectors/square/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/square/transformers.rs @@ -272,6 +272,9 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for SquarePaymentsRequest { PaymentMethodToken::PazeDecrypt(_) => { Err(unimplemented_payment_method!("Paze", "Square"))? } + PaymentMethodToken::GooglePayDecrypt(_) => { + Err(unimplemented_payment_method!("Google Pay", "Square"))? + } }, amount_money: SquarePaymentsAmountData { amount: item.request.amount, diff --git a/crates/hyperswitch_connectors/src/connectors/stax/transformers.rs b/crates/hyperswitch_connectors/src/connectors/stax/transformers.rs index a675e3040e3..327785d2fc8 100644 --- a/crates/hyperswitch_connectors/src/connectors/stax/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/stax/transformers.rs @@ -85,6 +85,9 @@ impl TryFrom<&StaxRouterData<&types::PaymentsAuthorizeRouterData>> for StaxPayme PaymentMethodToken::PazeDecrypt(_) => { Err(unimplemented_payment_method!("Paze", "Stax"))? } + PaymentMethodToken::GooglePayDecrypt(_) => { + Err(unimplemented_payment_method!("Google Pay", "Stax"))? + } }, idempotency_id: Some(item.router_data.connector_request_reference_id.clone()), }) @@ -105,6 +108,9 @@ impl TryFrom<&StaxRouterData<&types::PaymentsAuthorizeRouterData>> for StaxPayme PaymentMethodToken::PazeDecrypt(_) => { Err(unimplemented_payment_method!("Paze", "Stax"))? } + PaymentMethodToken::GooglePayDecrypt(_) => { + Err(unimplemented_payment_method!("Google Pay", "Stax"))? + } }, idempotency_id: Some(item.router_data.connector_request_reference_id.clone()), }) diff --git a/crates/hyperswitch_connectors/src/connectors/wellsfargo/transformers.rs b/crates/hyperswitch_connectors/src/connectors/wellsfargo/transformers.rs index 6ce9a4d7aa9..1a4d3ce8f9c 100644 --- a/crates/hyperswitch_connectors/src/connectors/wellsfargo/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/wellsfargo/transformers.rs @@ -153,6 +153,9 @@ impl TryFrom<&SetupMandateRouterData> for WellsfargoZeroMandateRequest { PaymentMethodToken::PazeDecrypt(_) => { Err(unimplemented_payment_method!("Paze", "Wellsfargo"))? } + PaymentMethodToken::GooglePayDecrypt(_) => { + Err(unimplemented_payment_method!("Google Pay", "Wellsfargo"))? + } }, None => ( PaymentInformation::ApplePayToken(Box::new( @@ -1173,6 +1176,9 @@ impl TryFrom<&WellsfargoRouterData<&PaymentsAuthorizeRouterData>> for Wellsfargo PaymentMethodToken::PazeDecrypt(_) => { Err(unimplemented_payment_method!("Paze", "Wellsfargo"))? } + PaymentMethodToken::GooglePayDecrypt(_) => Err( + unimplemented_payment_method!("Google Pay", "Wellsfargo"), + )?, }, None => { let email = item.router_data.request.get_email()?; diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index 8e4c79a965e..322b4aa4525 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -226,6 +226,7 @@ pub struct AccessToken { pub enum PaymentMethodToken { Token(Secret), ApplePayDecrypt(Box), + GooglePayDecrypt(Box), PazeDecrypt(Box), } @@ -248,6 +249,27 @@ pub struct ApplePayCryptogramData { pub eci_indicator: Option, } +#[derive(Debug, Clone, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GooglePayDecryptedData { + pub message_expiration: String, + pub message_id: String, + #[serde(rename = "paymentMethod")] + pub payment_method_type: String, + pub payment_method_details: GooglePayPaymentMethodDetails, +} + +#[derive(Debug, Clone, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GooglePayPaymentMethodDetails { + pub auth_method: common_enums::enums::GooglePayAuthMethod, + pub expiration_month: cards::CardExpirationMonth, + pub expiration_year: cards::CardExpirationYear, + pub pan: cards::CardNumber, + pub cryptogram: Option>, + pub eci_indicator: Option, +} + #[derive(Debug, Clone, serde::Deserialize)] #[serde(rename_all = "camelCase")] pub struct PazeDecryptedData { diff --git a/crates/masking/src/secret.rs b/crates/masking/src/secret.rs index 0bd28c3af92..7f7c18094a0 100644 --- a/crates/masking/src/secret.rs +++ b/crates/masking/src/secret.rs @@ -156,3 +156,10 @@ where SecretValue::default().into() } } + +// Required by base64-serde to serialize Secret of Vec which contains the base64 decoded value +impl AsRef<[u8]> for Secret> { + fn as_ref(&self) -> &[u8] { + self.peek().as_slice() + } +} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index db65482628f..7db0273d2e1 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -68,6 +68,7 @@ encoding_rs = "0.8.33" error-stack = "0.4.1" futures = "0.3.30" hex = "0.4.3" +hkdf = "0.12.4" http = "0.2.12" hyper = "0.14.28" infer = "0.15.0" @@ -104,6 +105,7 @@ serde_repr = "0.1.19" serde_urlencoded = "0.7.1" serde_with = "3.7.0" sha1 = { version = "0.10.6" } +sha2 = "0.10.8" strum = { version = "0.26", features = ["derive"] } tera = "1.19.1" thiserror = "1.0.58" diff --git a/crates/router/src/configs/secrets_transformers.rs b/crates/router/src/configs/secrets_transformers.rs index 7b74aca7647..f51a785eaa8 100644 --- a/crates/router/src/configs/secrets_transformers.rs +++ b/crates/router/src/configs/secrets_transformers.rs @@ -199,6 +199,24 @@ impl SecretsHandler for settings::PazeDecryptConfig { } } +#[async_trait::async_trait] +impl SecretsHandler for settings::GooglePayDecryptConfig { + async fn convert_to_raw_secret( + value: SecretStateContainer, + secret_management_client: &dyn SecretManagementInterface, + ) -> CustomResult, SecretsManagementError> { + let google_pay_decrypt_keys = value.get_inner(); + + let google_pay_root_signing_keys = secret_management_client + .get_secret(google_pay_decrypt_keys.google_pay_root_signing_keys.clone()) + .await?; + + Ok(value.transition_state(|_| Self { + google_pay_root_signing_keys, + })) + } +} + #[async_trait::async_trait] impl SecretsHandler for settings::ApplepayMerchantConfigs { async fn convert_to_raw_secret( @@ -420,6 +438,20 @@ pub(crate) async fn fetch_raw_secrets( None }; + #[allow(clippy::expect_used)] + let google_pay_decrypt_keys = if let Some(google_pay_keys) = conf.google_pay_decrypt_keys { + Some( + settings::GooglePayDecryptConfig::convert_to_raw_secret( + google_pay_keys, + secret_management_client, + ) + .await + .expect("Failed to decrypt google pay decrypt configs"), + ) + } else { + None + }; + #[allow(clippy::expect_used)] let applepay_merchant_configs = settings::ApplepayMerchantConfigs::convert_to_raw_secret( conf.applepay_merchant_configs, @@ -512,6 +544,7 @@ pub(crate) async fn fetch_raw_secrets( payouts: conf.payouts, applepay_decrypt_keys, paze_decrypt_keys, + google_pay_decrypt_keys, multiple_api_version_supported_connectors: conf.multiple_api_version_supported_connectors, applepay_merchant_configs, lock_settings: conf.lock_settings, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index fa3d004aada..d721a4db967 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -99,6 +99,7 @@ pub struct Settings { pub payout_method_filters: ConnectorFilters, pub applepay_decrypt_keys: SecretStateContainer, pub paze_decrypt_keys: Option>, + pub google_pay_decrypt_keys: Option>, pub multiple_api_version_supported_connectors: MultipleApiVersionSupportedConnectors, pub applepay_merchant_configs: SecretStateContainer, pub lock_settings: LockSettings, @@ -758,6 +759,11 @@ pub struct PazeDecryptConfig { pub paze_private_key_passphrase: Secret, } +#[derive(Debug, Deserialize, Clone)] +pub struct GooglePayDecryptConfig { + pub google_pay_root_signing_keys: Secret, +} + #[derive(Debug, Deserialize, Clone, Default)] #[serde(default)] pub struct LockerBasedRecipientConnectorList { @@ -911,6 +917,11 @@ impl Settings { .map(|x| x.get_inner().validate()) .transpose()?; + self.google_pay_decrypt_keys + .as_ref() + .map(|x| x.get_inner().validate()) + .transpose()?; + self.key_manager.get_inner().validate()?; #[cfg(feature = "email")] self.email diff --git a/crates/router/src/configs/validations.rs b/crates/router/src/configs/validations.rs index 7040998ccf0..11aa4e0cf51 100644 --- a/crates/router/src/configs/validations.rs +++ b/crates/router/src/configs/validations.rs @@ -257,6 +257,21 @@ impl super::settings::PazeDecryptConfig { } } +impl super::settings::GooglePayDecryptConfig { + pub fn validate(&self) -> Result<(), ApplicationError> { + use common_utils::fp_utils::when; + + when( + self.google_pay_root_signing_keys.is_default_or_empty(), + || { + Err(ApplicationError::InvalidConfigurationValueError( + "google_pay_root_signing_keys must not be empty".into(), + )) + }, + ) + } +} + impl super::settings::KeyManagerConfig { pub fn validate(&self) -> Result<(), ApplicationError> { use common_utils::fp_utils::when; diff --git a/crates/router/src/connector/braintree/transformers.rs b/crates/router/src/connector/braintree/transformers.rs index 00624ce0f9b..5dd6db5aa24 100644 --- a/crates/router/src/connector/braintree/transformers.rs +++ b/crates/router/src/connector/braintree/transformers.rs @@ -1598,6 +1598,9 @@ impl types::PaymentMethodToken::PazeDecrypt(_) => { Err(unimplemented_payment_method!("Paze", "Braintree"))? } + types::PaymentMethodToken::GooglePayDecrypt(_) => { + Err(unimplemented_payment_method!("Google Pay", "Braintree"))? + } }, transaction: transaction_body, }, @@ -1699,6 +1702,9 @@ fn get_braintree_redirect_form( types::PaymentMethodToken::PazeDecrypt(_) => { Err(unimplemented_payment_method!("Paze", "Braintree"))? } + types::PaymentMethodToken::GooglePayDecrypt(_) => { + Err(unimplemented_payment_method!("Google Pay", "Braintree"))? + } }, bin: match card_details { domain::PaymentMethodData::Card(card_details) => { diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index cd758fffb69..bb38ef74836 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -307,6 +307,9 @@ impl TryFrom<&CheckoutRouterData<&types::PaymentsAuthorizeRouterData>> for Payme types::PaymentMethodToken::PazeDecrypt(_) => { Err(unimplemented_payment_method!("Paze", "Checkout"))? } + types::PaymentMethodToken::GooglePayDecrypt(_) => { + Err(unimplemented_payment_method!("Google Pay", "Checkout"))? + } }, })), domain::WalletData::ApplePay(_) => { @@ -336,6 +339,9 @@ impl TryFrom<&CheckoutRouterData<&types::PaymentsAuthorizeRouterData>> for Payme types::PaymentMethodToken::PazeDecrypt(_) => { Err(unimplemented_payment_method!("Paze", "Checkout"))? } + types::PaymentMethodToken::GooglePayDecrypt(_) => { + Err(unimplemented_payment_method!("Google Pay", "Checkout"))? + } } } domain::WalletData::AliPayQr(_) diff --git a/crates/router/src/connector/payme/transformers.rs b/crates/router/src/connector/payme/transformers.rs index cd8ba814eb7..7e50f7f40e6 100644 --- a/crates/router/src/connector/payme/transformers.rs +++ b/crates/router/src/connector/payme/transformers.rs @@ -729,6 +729,9 @@ impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for Pay3dsRequest { types::PaymentMethodToken::PazeDecrypt(_) => { Err(unimplemented_payment_method!("Paze", "Payme"))? } + types::PaymentMethodToken::GooglePayDecrypt(_) => { + Err(unimplemented_payment_method!("Google Pay", "Payme"))? + } }; Ok(Self { buyer_email, diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 48736c00e7d..60852779884 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -1840,6 +1840,9 @@ impl TryFrom<(&types::PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntent types::PaymentMethodToken::PazeDecrypt(_) => { Err(crate::unimplemented_payment_method!("Paze", "Stripe"))? } + types::PaymentMethodToken::GooglePayDecrypt(_) => { + Err(crate::unimplemented_payment_method!("Google Pay", "Stripe"))? + } }; Some(StripePaymentMethodData::Wallet( StripeWallet::ApplepayPayment(ApplepayPayment { diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 96321d09794..b4c1c1c2c03 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -244,6 +244,60 @@ pub enum PazeDecryptionError { CertificateParsingFailed, } +#[derive(Debug, thiserror::Error)] +pub enum GooglePayDecryptionError { + #[error("Recipient ID not found")] + RecipientIdNotFound, + #[error("Invalid expiration time")] + InvalidExpirationTime, + #[error("Failed to base64 decode input data")] + Base64DecodingFailed, + #[error("Failed to decrypt input data")] + DecryptionFailed, + #[error("Failed to deserialize input data")] + DeserializationFailed, + #[error("Certificate parsing failed")] + CertificateParsingFailed, + #[error("Key deserialization failure")] + KeyDeserializationFailed, + #[error("Failed to derive a shared ephemeral key")] + DerivingSharedEphemeralKeyFailed, + #[error("Failed to derive a shared secret key")] + DerivingSharedSecretKeyFailed, + #[error("Failed to parse the tag")] + ParsingTagError, + #[error("HMAC verification failed")] + HmacVerificationFailed, + #[error("Failed to derive Elliptic Curve key")] + DerivingEcKeyFailed, + #[error("Failed to Derive Public key")] + DerivingPublicKeyFailed, + #[error("Failed to Derive Elliptic Curve group")] + DerivingEcGroupFailed, + #[error("Failed to allocate memory for big number")] + BigNumAllocationFailed, + #[error("Failed to get the ECDSA signature")] + EcdsaSignatureFailed, + #[error("Failed to verify the signature")] + SignatureVerificationFailed, + #[error("Invalid signature is provided")] + InvalidSignature, + #[error("Failed to parse the Signed Key")] + SignedKeyParsingFailure, + #[error("The Signed Key is expired")] + SignedKeyExpired, + #[error("Failed to parse the ECDSA signature")] + EcdsaSignatureParsingFailed, + #[error("Invalid intermediate signature is provided")] + InvalidIntermediateSignature, + #[error("Invalid protocol version")] + InvalidProtocolVersion, + #[error("Decrypted Token has expired")] + DecryptedTokenExpired, + #[error("Failed to parse the given value")] + ParsingFailed, +} + #[cfg(feature = "detailed_errors")] pub mod error_stack_parsing { diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index e9d3c013789..5b8988dace0 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -3080,7 +3080,61 @@ where paze_decrypted_data, )))) } - _ => Ok(None), + TokenizationAction::DecryptGooglePayToken(payment_processing_details) => { + let google_pay_data = match payment_data.get_payment_method_data() { + Some(domain::PaymentMethodData::Wallet(domain::WalletData::GooglePay( + wallet_data, + ))) => { + let decryptor = helpers::GooglePayTokenDecryptor::new( + payment_processing_details + .google_pay_root_signing_keys + .clone(), + payment_processing_details.google_pay_recipient_id.clone(), + payment_processing_details.google_pay_private_key.clone(), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to create google pay token decryptor")?; + + // should_verify_token is set to false to disable verification of token + Some( + decryptor + .decrypt_token(wallet_data.tokenization_data.token.clone(), false) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to decrypt google pay token")?, + ) + } + Some(payment_method_data) => { + logger::info!( + "Invalid payment_method_data found for Google Pay Decrypt Flow: {:?}", + payment_method_data.get_payment_method() + ); + None + } + None => { + logger::info!("No payment_method_data found for Google Pay Decrypt Flow"); + None + } + }; + + let google_pay_predecrypt = google_pay_data + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to get GooglePayDecryptedData in response")?; + + Ok(Some(PaymentMethodToken::GooglePayDecrypt(Box::new( + google_pay_predecrypt, + )))) + } + TokenizationAction::ConnectorToken(_) => { + logger::info!("Invalid tokenization action found for decryption flow: ConnectorToken",); + Ok(None) + } + token_action => { + logger::info!( + "Invalid tokenization action found for decryption flow: {:?}", + token_action + ); + Ok(None) + } } } @@ -4052,6 +4106,64 @@ fn check_apple_pay_metadata( }) } +fn get_google_pay_connector_wallet_details( + state: &SessionState, + merchant_connector_account: &helpers::MerchantConnectorAccountType, +) -> Option { + let google_pay_root_signing_keys = + state + .conf + .google_pay_decrypt_keys + .as_ref() + .map(|google_pay_keys| { + google_pay_keys + .get_inner() + .google_pay_root_signing_keys + .clone() + }); + match ( + google_pay_root_signing_keys, + merchant_connector_account.get_connector_wallets_details(), + ) { + (Some(google_pay_root_signing_keys), Some(wallet_details)) => { + let google_pay_wallet_details = wallet_details + .parse_value::( + "GooglePayWalletDetails", + ) + .map_err(|error| { + logger::warn!(?error, "Failed to Parse Value to GooglePayWalletDetails") + }); + + google_pay_wallet_details + .ok() + .map( + |google_pay_wallet_details| { + match google_pay_wallet_details + .google_pay + .provider_details { + api_models::payments::GooglePayProviderDetails::GooglePayMerchantDetails(merchant_details) => { + GooglePayPaymentProcessingDetails { + google_pay_private_key: merchant_details + .merchant_info + .tokenization_specification + .parameters + .private_key, + google_pay_root_signing_keys, + google_pay_recipient_id: merchant_details + .merchant_info + .tokenization_specification + .parameters + .recipient_id, + } + } + } + } + ) + } + _ => None, + } +} + fn is_payment_method_type_allowed_for_connector( current_pm_type: Option, pm_type_filter: Option, @@ -4066,6 +4178,7 @@ fn is_payment_method_type_allowed_for_connector( } } +#[allow(clippy::too_many_arguments)] async fn decide_payment_method_tokenize_action( state: &SessionState, connector_name: &str, @@ -4074,6 +4187,7 @@ async fn decide_payment_method_tokenize_action( is_connector_tokenization_enabled: bool, apple_pay_flow: Option, payment_method_type: Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { if let Some(storage_enums::PaymentMethodType::Paze) = payment_method_type { // Paze generates a one time use network token which should not be tokenized in the connector or router. @@ -4090,6 +4204,20 @@ async fn decide_payment_method_tokenize_action( None => Err(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to fetch Paze configs"), } + } else if let Some(storage_enums::PaymentMethodType::GooglePay) = payment_method_type { + let google_pay_details = + get_google_pay_connector_wallet_details(state, merchant_connector_account); + + match google_pay_details { + Some(wallet_details) => Ok(TokenizationAction::DecryptGooglePayToken(wallet_details)), + None => { + if is_connector_tokenization_enabled { + Ok(TokenizationAction::TokenizeInConnectorAndRouter) + } else { + Ok(TokenizationAction::TokenizeInRouter) + } + } + } } else { match pm_parent_token { None => Ok(match (is_connector_tokenization_enabled, apple_pay_flow) { @@ -4154,6 +4282,13 @@ pub struct PazePaymentProcessingDetails { pub paze_private_key_passphrase: Secret, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GooglePayPaymentProcessingDetails { + pub google_pay_private_key: Secret, + pub google_pay_root_signing_keys: Secret, + pub google_pay_recipient_id: Option>, +} + #[derive(Clone, Debug)] pub enum TokenizationAction { TokenizeInRouter, @@ -4164,6 +4299,7 @@ pub enum TokenizationAction { DecryptApplePayToken(payments_api::PaymentProcessingDetails), TokenizeInConnectorAndApplepayPreDecrypt(payments_api::PaymentProcessingDetails), DecryptPazeToken(PazePaymentProcessingDetails), + DecryptGooglePayToken(GooglePayPaymentProcessingDetails), } #[cfg(feature = "v2")] @@ -4254,6 +4390,7 @@ where is_connector_tokenization_enabled, apple_pay_flow, payment_method_type, + merchant_connector_account, ) .await?; @@ -4312,6 +4449,11 @@ where TokenizationAction::DecryptPazeToken(paze_payment_processing_details) => { TokenizationAction::DecryptPazeToken(paze_payment_processing_details) } + TokenizationAction::DecryptGooglePayToken( + google_pay_payment_processing_details, + ) => { + TokenizationAction::DecryptGooglePayToken(google_pay_payment_processing_details) + } }; (payment_data.to_owned(), connector_tokenization_action) } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 0d7a3b57fc1..56667b0517f 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -47,6 +47,7 @@ use openssl::{ }; #[cfg(feature = "v2")] use redis_interface::errors::RedisError; +use ring::hmac; use router_env::{instrument, logger, tracing}; use uuid::Uuid; use x509_parser::parse_x509_certificate; @@ -5073,6 +5074,9 @@ async fn get_and_merge_apple_pay_metadata( paze: connector_wallets_details_optional .as_ref() .and_then(|d| d.paze.clone()), + google_pay: connector_wallets_details_optional + .as_ref() + .and_then(|d| d.google_pay.clone()), } } api_models::payments::ApplepaySessionTokenMetadata::ApplePay(apple_pay_metadata) => { @@ -5091,6 +5095,9 @@ async fn get_and_merge_apple_pay_metadata( paze: connector_wallets_details_optional .as_ref() .and_then(|d| d.paze.clone()), + google_pay: connector_wallets_details_optional + .as_ref() + .and_then(|d| d.google_pay.clone()), } } }; @@ -5427,6 +5434,543 @@ impl ApplePayData { } } +pub(crate) const SENDER_ID: &[u8] = b"Google"; +pub(crate) const PROTOCOL: &str = "ECv2"; + +// Structs for keys and the main decryptor +pub struct GooglePayTokenDecryptor { + root_signing_keys: Vec, + recipient_id: Option>, + private_key: PKey, +} + +#[derive(Debug, Clone, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EncryptedData { + signature: String, + intermediate_signing_key: IntermediateSigningKey, + protocol_version: GooglePayProtocolVersion, + #[serde(with = "common_utils::custom_serde::json_string")] + signed_message: GooglePaySignedMessage, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GooglePaySignedMessage { + #[serde(with = "common_utils::Base64Serializer")] + encrypted_message: masking::Secret>, + #[serde(with = "common_utils::Base64Serializer")] + ephemeral_public_key: masking::Secret>, + #[serde(with = "common_utils::Base64Serializer")] + tag: masking::Secret>, +} + +#[derive(Debug, Clone, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IntermediateSigningKey { + signed_key: masking::Secret, + signatures: Vec>, +} + +#[derive(Debug, Clone, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GooglePaySignedKey { + key_value: masking::Secret, + key_expiration: String, +} + +#[derive(Debug, Clone, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GooglePayRootSigningKey { + key_value: masking::Secret, + key_expiration: String, + protocol_version: GooglePayProtocolVersion, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq)] +pub enum GooglePayProtocolVersion { + #[serde(rename = "ECv2")] + EcProtocolVersion2, +} + +// Check expiration date validity +fn check_expiration_date_is_valid( + expiration: &str, +) -> CustomResult { + let expiration_ms = expiration + .parse::() + .change_context(errors::GooglePayDecryptionError::InvalidExpirationTime)?; + // convert milliseconds to nanoseconds (1 millisecond = 1_000_000 nanoseconds) to create OffsetDateTime + let expiration_time = + time::OffsetDateTime::from_unix_timestamp_nanos(expiration_ms * 1_000_000) + .change_context(errors::GooglePayDecryptionError::InvalidExpirationTime)?; + let now = time::OffsetDateTime::now_utc(); + + Ok(expiration_time > now) +} + +// Construct little endian format of u32 +fn get_little_endian_format(number: u32) -> Vec { + number.to_le_bytes().to_vec() +} + +// Filter and parse the root signing keys based on protocol version and expiration time +fn filter_root_signing_keys( + root_signing_keys: Vec, +) -> CustomResult, errors::GooglePayDecryptionError> { + let filtered_root_signing_keys = root_signing_keys + .iter() + .filter(|key| { + key.protocol_version == GooglePayProtocolVersion::EcProtocolVersion2 + && matches!( + check_expiration_date_is_valid(&key.key_expiration).inspect_err( + |err| logger::warn!( + "Failed to check expirattion due to invalid format: {:?}", + err + ) + ), + Ok(true) + ) + }) + .cloned() + .collect::>(); + + logger::info!( + "Filtered {} out of {} root signing keys", + filtered_root_signing_keys.len(), + root_signing_keys.len() + ); + + Ok(filtered_root_signing_keys) +} + +impl GooglePayTokenDecryptor { + pub fn new( + root_keys: masking::Secret, + recipient_id: Option>, + private_key: masking::Secret, + ) -> CustomResult { + // base64 decode the private key + let decoded_key = BASE64_ENGINE + .decode(private_key.expose()) + .change_context(errors::GooglePayDecryptionError::Base64DecodingFailed)?; + // create a private key from the decoded key + let private_key = PKey::private_key_from_pkcs8(&decoded_key) + .change_context(errors::GooglePayDecryptionError::KeyDeserializationFailed) + .attach_printable("cannot convert private key from decode_key")?; + + // parse the root signing keys + let root_keys_vector: Vec = root_keys + .expose() + .parse_struct("GooglePayRootSigningKey") + .change_context(errors::GooglePayDecryptionError::DeserializationFailed)?; + + // parse and filter the root signing keys by protocol version + let filtered_root_signing_keys = filter_root_signing_keys(root_keys_vector)?; + + Ok(Self { + root_signing_keys: filtered_root_signing_keys, + recipient_id, + private_key, + }) + } + + // Decrypt the Google pay token + pub fn decrypt_token( + &self, + data: String, + should_verify_signature: bool, + ) -> CustomResult< + hyperswitch_domain_models::router_data::GooglePayDecryptedData, + errors::GooglePayDecryptionError, + > { + // parse the encrypted data + let encrypted_data: EncryptedData = data + .parse_struct("EncryptedData") + .change_context(errors::GooglePayDecryptionError::DeserializationFailed)?; + + // verify the signature if required + if should_verify_signature { + self.verify_signature(&encrypted_data)?; + } + + let ephemeral_public_key = encrypted_data.signed_message.ephemeral_public_key.peek(); + let tag = encrypted_data.signed_message.tag.peek(); + let encrypted_message = encrypted_data.signed_message.encrypted_message.peek(); + + // derive the shared key + let shared_key = self.get_shared_key(ephemeral_public_key)?; + + // derive the symmetric encryption key and MAC key + let derived_key = self.derive_key(ephemeral_public_key, &shared_key)?; + // First 32 bytes for AES-256 and Remaining bytes for HMAC + let (symmetric_encryption_key, mac_key) = derived_key + .split_at_checked(32) + .ok_or(errors::GooglePayDecryptionError::ParsingFailed)?; + + // verify the HMAC of the message + self.verify_hmac(mac_key, tag, encrypted_message)?; + + // decrypt the message + let decrypted = self.decrypt_message(symmetric_encryption_key, encrypted_message)?; + + // parse the decrypted data + let decrypted_data: hyperswitch_domain_models::router_data::GooglePayDecryptedData = + decrypted + .parse_struct("GooglePayDecryptedData") + .change_context(errors::GooglePayDecryptionError::DeserializationFailed)?; + + // check the expiration date of the decrypted data + if matches!( + check_expiration_date_is_valid(&decrypted_data.message_expiration), + Ok(true) + ) { + Ok(decrypted_data) + } else { + Err(errors::GooglePayDecryptionError::DecryptedTokenExpired.into()) + } + } + + // Verify the signature of the token + fn verify_signature( + &self, + encrypted_data: &EncryptedData, + ) -> CustomResult<(), errors::GooglePayDecryptionError> { + // check the protocol version + if encrypted_data.protocol_version != GooglePayProtocolVersion::EcProtocolVersion2 { + return Err(errors::GooglePayDecryptionError::InvalidProtocolVersion.into()); + } + + // verify the intermediate signing key + self.verify_intermediate_signing_key(encrypted_data)?; + // validate and fetch the signed key + let signed_key = self.validate_signed_key(&encrypted_data.intermediate_signing_key)?; + // verify the signature of the token + self.verify_message_signature(encrypted_data, &signed_key) + } + + // Verify the intermediate signing key + fn verify_intermediate_signing_key( + &self, + encrypted_data: &EncryptedData, + ) -> CustomResult<(), errors::GooglePayDecryptionError> { + let mut signatrues: Vec = Vec::new(); + + // decode and parse the signatures + for signature in encrypted_data.intermediate_signing_key.signatures.iter() { + let signature = BASE64_ENGINE + .decode(signature.peek()) + .change_context(errors::GooglePayDecryptionError::Base64DecodingFailed)?; + let ecdsa_signature = openssl::ecdsa::EcdsaSig::from_der(&signature) + .change_context(errors::GooglePayDecryptionError::EcdsaSignatureParsingFailed)?; + signatrues.push(ecdsa_signature); + } + + // get the sender id i.e. Google + let sender_id = String::from_utf8(SENDER_ID.to_vec()) + .change_context(errors::GooglePayDecryptionError::DeserializationFailed)?; + + // construct the signed data + let signed_data = self.construct_signed_data_for_intermediate_signing_key_verification( + &sender_id, + PROTOCOL, + encrypted_data.intermediate_signing_key.signed_key.peek(), + )?; + + // check if any of the signatures are valid for any of the root signing keys + for key in self.root_signing_keys.iter() { + // decode and create public key + let public_key = self + .load_public_key(key.key_value.peek()) + .change_context(errors::GooglePayDecryptionError::DerivingPublicKeyFailed)?; + // fetch the ec key from public key + let ec_key = public_key + .ec_key() + .change_context(errors::GooglePayDecryptionError::DerivingEcKeyFailed)?; + + // hash the signed data + let message_hash = openssl::sha::sha256(&signed_data); + + // verify if any of the signatures is valid against the given key + for signature in signatrues.iter() { + let result = signature.verify(&message_hash, &ec_key).change_context( + errors::GooglePayDecryptionError::SignatureVerificationFailed, + )?; + + if result { + return Ok(()); + } + } + } + + Err(errors::GooglePayDecryptionError::InvalidIntermediateSignature.into()) + } + + // Construct signed data for intermediate signing key verification + fn construct_signed_data_for_intermediate_signing_key_verification( + &self, + sender_id: &str, + protocol_version: &str, + signed_key: &str, + ) -> CustomResult, errors::GooglePayDecryptionError> { + let length_of_sender_id = u32::try_from(sender_id.len()) + .change_context(errors::GooglePayDecryptionError::ParsingFailed)?; + let length_of_protocol_version = u32::try_from(protocol_version.len()) + .change_context(errors::GooglePayDecryptionError::ParsingFailed)?; + let length_of_signed_key = u32::try_from(signed_key.len()) + .change_context(errors::GooglePayDecryptionError::ParsingFailed)?; + + let mut signed_data: Vec = Vec::new(); + signed_data.append(&mut get_little_endian_format(length_of_sender_id)); + signed_data.append(&mut sender_id.as_bytes().to_vec()); + signed_data.append(&mut get_little_endian_format(length_of_protocol_version)); + signed_data.append(&mut protocol_version.as_bytes().to_vec()); + signed_data.append(&mut get_little_endian_format(length_of_signed_key)); + signed_data.append(&mut signed_key.as_bytes().to_vec()); + + Ok(signed_data) + } + + // Validate and parse signed key + fn validate_signed_key( + &self, + intermediate_signing_key: &IntermediateSigningKey, + ) -> CustomResult { + let signed_key: GooglePaySignedKey = intermediate_signing_key + .signed_key + .clone() + .expose() + .parse_struct("GooglePaySignedKey") + .change_context(errors::GooglePayDecryptionError::SignedKeyParsingFailure)?; + if !matches!( + check_expiration_date_is_valid(&signed_key.key_expiration), + Ok(true) + ) { + return Err(errors::GooglePayDecryptionError::SignedKeyExpired)?; + } + Ok(signed_key) + } + + // Verify the signed message + fn verify_message_signature( + &self, + encrypted_data: &EncryptedData, + signed_key: &GooglePaySignedKey, + ) -> CustomResult<(), errors::GooglePayDecryptionError> { + // create a public key from the intermediate signing key + let public_key = self.load_public_key(signed_key.key_value.peek())?; + // base64 decode the signature + let signature = BASE64_ENGINE + .decode(&encrypted_data.signature) + .change_context(errors::GooglePayDecryptionError::Base64DecodingFailed)?; + + // parse the signature using ECDSA + let ecdsa_signature = openssl::ecdsa::EcdsaSig::from_der(&signature) + .change_context(errors::GooglePayDecryptionError::EcdsaSignatureFailed)?; + + // get the EC key from the public key + let ec_key = public_key + .ec_key() + .change_context(errors::GooglePayDecryptionError::DerivingEcKeyFailed)?; + + // get the sender id i.e. Google + let sender_id = String::from_utf8(SENDER_ID.to_vec()) + .change_context(errors::GooglePayDecryptionError::DeserializationFailed)?; + + // serialize the signed message to string + let signed_message = serde_json::to_string(&encrypted_data.signed_message) + .change_context(errors::GooglePayDecryptionError::SignedKeyParsingFailure)?; + + // construct the signed data + let signed_data = self.construct_signed_data_for_signature_verification( + &sender_id, + PROTOCOL, + &signed_message, + )?; + + // hash the signed data + let message_hash = openssl::sha::sha256(&signed_data); + + // verify the signature + let result = ecdsa_signature + .verify(&message_hash, &ec_key) + .change_context(errors::GooglePayDecryptionError::SignatureVerificationFailed)?; + + if result { + Ok(()) + } else { + Err(errors::GooglePayDecryptionError::InvalidSignature)? + } + } + + // Fetch the public key + fn load_public_key( + &self, + key: &str, + ) -> CustomResult, errors::GooglePayDecryptionError> { + // decode the base64 string + let der_data = BASE64_ENGINE + .decode(key) + .change_context(errors::GooglePayDecryptionError::Base64DecodingFailed)?; + + // parse the DER-encoded data as an EC public key + let ec_key = openssl::ec::EcKey::public_key_from_der(&der_data) + .change_context(errors::GooglePayDecryptionError::DerivingEcKeyFailed)?; + + // wrap the EC key in a PKey (a more general-purpose public key type in OpenSSL) + let public_key = PKey::from_ec_key(ec_key) + .change_context(errors::GooglePayDecryptionError::DerivingPublicKeyFailed)?; + + Ok(public_key) + } + + // Construct signed data for signature verification + fn construct_signed_data_for_signature_verification( + &self, + sender_id: &str, + protocol_version: &str, + signed_key: &str, + ) -> CustomResult, errors::GooglePayDecryptionError> { + let recipient_id = self + .recipient_id + .clone() + .ok_or(errors::GooglePayDecryptionError::RecipientIdNotFound)? + .expose(); + let length_of_sender_id = u32::try_from(sender_id.len()) + .change_context(errors::GooglePayDecryptionError::ParsingFailed)?; + let length_of_recipient_id = u32::try_from(recipient_id.len()) + .change_context(errors::GooglePayDecryptionError::ParsingFailed)?; + let length_of_protocol_version = u32::try_from(protocol_version.len()) + .change_context(errors::GooglePayDecryptionError::ParsingFailed)?; + let length_of_signed_key = u32::try_from(signed_key.len()) + .change_context(errors::GooglePayDecryptionError::ParsingFailed)?; + + let mut signed_data: Vec = Vec::new(); + signed_data.append(&mut get_little_endian_format(length_of_sender_id)); + signed_data.append(&mut sender_id.as_bytes().to_vec()); + signed_data.append(&mut get_little_endian_format(length_of_recipient_id)); + signed_data.append(&mut recipient_id.as_bytes().to_vec()); + signed_data.append(&mut get_little_endian_format(length_of_protocol_version)); + signed_data.append(&mut protocol_version.as_bytes().to_vec()); + signed_data.append(&mut get_little_endian_format(length_of_signed_key)); + signed_data.append(&mut signed_key.as_bytes().to_vec()); + + Ok(signed_data) + } + + // Derive a shared key using ECDH + fn get_shared_key( + &self, + ephemeral_public_key_bytes: &[u8], + ) -> CustomResult, errors::GooglePayDecryptionError> { + let group = openssl::ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1) + .change_context(errors::GooglePayDecryptionError::DerivingEcGroupFailed)?; + + let mut big_num_context = openssl::bn::BigNumContext::new() + .change_context(errors::GooglePayDecryptionError::BigNumAllocationFailed)?; + + let ec_key = openssl::ec::EcPoint::from_bytes( + &group, + ephemeral_public_key_bytes, + &mut big_num_context, + ) + .change_context(errors::GooglePayDecryptionError::DerivingEcKeyFailed)?; + + // create an ephemeral public key from the given bytes + let ephemeral_public_key = openssl::ec::EcKey::from_public_key(&group, &ec_key) + .change_context(errors::GooglePayDecryptionError::DerivingPublicKeyFailed)?; + + // wrap the public key in a PKey + let ephemeral_pkey = PKey::from_ec_key(ephemeral_public_key) + .change_context(errors::GooglePayDecryptionError::DerivingPublicKeyFailed)?; + + // perform ECDH to derive the shared key + let mut deriver = Deriver::new(&self.private_key) + .change_context(errors::GooglePayDecryptionError::DerivingSharedSecretKeyFailed)?; + + deriver + .set_peer(&ephemeral_pkey) + .change_context(errors::GooglePayDecryptionError::DerivingSharedSecretKeyFailed)?; + + let shared_key = deriver + .derive_to_vec() + .change_context(errors::GooglePayDecryptionError::DerivingSharedSecretKeyFailed)?; + + Ok(shared_key) + } + + // Derive symmetric key and MAC key using HKDF + fn derive_key( + &self, + ephemeral_public_key_bytes: &[u8], + shared_key: &[u8], + ) -> CustomResult, errors::GooglePayDecryptionError> { + // concatenate ephemeral public key and shared key + let input_key_material = [ephemeral_public_key_bytes, shared_key].concat(); + + // initialize HKDF with SHA-256 as the hash function + // Salt is not provided as per the Google Pay documentation + // https://developers.google.com/pay/api/android/guides/resources/payment-data-cryptography#encrypt-spec + let hkdf: ::hkdf::Hkdf = ::hkdf::Hkdf::new(None, &input_key_material); + + // derive 64 bytes for the output key (symmetric encryption + MAC key) + let mut output_key = vec![0u8; 64]; + hkdf.expand(SENDER_ID, &mut output_key).map_err(|err| { + logger::error!( + "Failed to derive the shared ephemeral key for Google Pay decryption flow: {:?}", + err + ); + report!(errors::GooglePayDecryptionError::DerivingSharedEphemeralKeyFailed) + })?; + + Ok(output_key) + } + + // Verify the Hmac key + // https://developers.google.com/pay/api/android/guides/resources/payment-data-cryptography#encrypt-spec + fn verify_hmac( + &self, + mac_key: &[u8], + tag: &[u8], + encrypted_message: &[u8], + ) -> CustomResult<(), errors::GooglePayDecryptionError> { + let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, mac_key); + hmac::verify(&hmac_key, encrypted_message, tag) + .change_context(errors::GooglePayDecryptionError::HmacVerificationFailed) + } + + // Method to decrypt the AES-GCM encrypted message + fn decrypt_message( + &self, + symmetric_key: &[u8], + encrypted_message: &[u8], + ) -> CustomResult, errors::GooglePayDecryptionError> { + //initialization vector IV is typically used in AES-GCM (Galois/Counter Mode) encryption for randomizing the encryption process. + // zero iv is being passed as specified in Google Pay documentation + // https://developers.google.com/pay/api/android/guides/resources/payment-data-cryptography#decrypt-token + let iv = [0u8; 16]; + + // extract the tag from the end of the encrypted message + let tag = encrypted_message + .get(encrypted_message.len() - 16..) + .ok_or(errors::GooglePayDecryptionError::ParsingTagError)?; + + // decrypt the message using AES-256-CTR + let cipher = Cipher::aes_256_ctr(); + let decrypted_data = decrypt_aead( + cipher, + symmetric_key, + Some(&iv), + &[], + encrypted_message, + tag, + ) + .change_context(errors::GooglePayDecryptionError::DecryptionFailed)?; + + Ok(decrypted_data) + } +} + pub fn decrypt_paze_token( paze_wallet_data: PazeWalletData, paze_private_key: masking::Secret, diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 84f848ef0ab..ea81b8cb16f 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -139,6 +139,11 @@ where message: "Paze Decrypt token is not supported".to_string(), })? } + types::PaymentMethodToken::GooglePayDecrypt(_) => { + Err(errors::ApiErrorResponse::NotSupported { + message: "Google Pay Decrypt token is not supported".to_string(), + })? + } }; Some((connector_name, token)) } else { diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 5baca7bb55a..c11d54fc298 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -44,7 +44,8 @@ pub use hyperswitch_domain_models::{ router_data::{ AccessToken, AdditionalPaymentMethodConnectorResponse, ApplePayCryptogramData, ApplePayPredecryptData, ConnectorAuthType, ConnectorResponseData, ErrorResponse, - PaymentMethodBalance, PaymentMethodToken, RecurringMandatePaymentData, RouterData, + GooglePayDecryptedData, GooglePayPaymentMethodDetails, PaymentMethodBalance, + PaymentMethodToken, RecurringMandatePaymentData, RouterData, }, router_data_v2::{ AccessTokenFlowData, DisputesFlowData, ExternalAuthenticationFlowData, FilesFlowData, From 6f90b93cee6eb5fb688750b940ea884af8b1caa3 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Wed, 5 Feb 2025 17:44:40 +0530 Subject: [PATCH 16/46] fix(connector): [BOA] throw unsupported error incase of 3DS cards and limit administrative area length to 20 characters (#7174) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../connectors/bankofamerica/transformers.rs | 41 ++++++++-- .../e2e/configs/Payment/BankOfAmerica.js | 82 ++++--------------- 2 files changed, 50 insertions(+), 73 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs b/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs index b33555a7c55..a0868d688eb 100644 --- a/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs @@ -510,17 +510,26 @@ fn build_bill_to( country: None, email: email.clone(), }; + Ok(address_details .and_then(|addr| { - addr.address.as_ref().map(|addr| BillTo { - first_name: addr.first_name.clone(), - last_name: addr.last_name.clone(), - address1: addr.line1.clone(), - locality: addr.city.clone(), - administrative_area: addr.to_state_code_as_optional().ok().flatten(), - postal_code: addr.zip.clone(), - country: addr.country, - email, + addr.address.as_ref().map(|addr| { + let administrative_area = addr.to_state_code_as_optional().unwrap_or_else(|_| { + addr.state + .clone() + .map(|state| Secret::new(format!("{:.20}", state.expose()))) + }); + + BillTo { + first_name: addr.first_name.clone(), + last_name: addr.last_name.clone(), + address1: addr.line1.clone(), + locality: addr.city.clone(), + administrative_area, + postal_code: addr.zip.clone(), + country: addr.country, + email, + } }) }) .unwrap_or(default_address)) @@ -813,6 +822,13 @@ impl hyperswitch_domain_models::payment_method_data::Card, ), ) -> Result { + if item.router_data.is_three_ds() { + Err(errors::ConnectorError::NotSupported { + message: "Card 3DS".to_string(), + connector: "BankOfAmerica", + })? + }; + let email = item.router_data.request.get_email()?; let bill_to = build_bill_to(item.router_data.get_optional_billing(), email)?; let order_information = OrderInformationWithBill::from((item, Some(bill_to))); @@ -2242,6 +2258,13 @@ impl hyperswitch_domain_models::payment_method_data::Card, ), ) -> Result { + if item.is_three_ds() { + Err(errors::ConnectorError::NotSupported { + message: "Card 3DS".to_string(), + connector: "BankOfAmerica", + })? + }; + let order_information = OrderInformationWithBill::try_from(item)?; let client_reference_information = ClientReferenceInformation::from(item); let merchant_defined_information = diff --git a/cypress-tests/cypress/e2e/configs/Payment/BankOfAmerica.js b/cypress-tests/cypress/e2e/configs/Payment/BankOfAmerica.js index 308565e72fa..0c8a8803254 100644 --- a/cypress-tests/cypress/e2e/configs/Payment/BankOfAmerica.js +++ b/cypress-tests/cypress/e2e/configs/Payment/BankOfAmerica.js @@ -1,3 +1,5 @@ +import { getCustomExchange } from "./Modifiers"; + const successfulNo3DSCardDetails = { card_number: "4242424242424242", card_exp_month: "01", @@ -112,7 +114,7 @@ export const connectorDetails = { }, }, }, - "3DSManualCapture": { + "3DSManualCapture": getCustomExchange({ Request: { payment_method: "card", payment_method_data: { @@ -122,14 +124,8 @@ export const connectorDetails = { customer_acceptance: null, setup_future_usage: "on_session", }, - Response: { - status: 200, - body: { - status: "requires_capture", - }, - }, - }, - "3DSAutoCapture": { + }), + "3DSAutoCapture": getCustomExchange({ Request: { payment_method: "card", payment_method_data: { @@ -139,13 +135,7 @@ export const connectorDetails = { customer_acceptance: null, setup_future_usage: "on_session", }, - Response: { - status: 200, - body: { - status: "requires_customer_action", - }, - }, - }, + }), No3DSManualCapture: { Request: { payment_method: "card", @@ -301,7 +291,7 @@ export const connectorDetails = { }, }, }, - MandateSingleUse3DSAutoCapture: { + MandateSingleUse3DSAutoCapture: getCustomExchange({ Request: { payment_method: "card", payment_method_data: { @@ -310,14 +300,8 @@ export const connectorDetails = { currency: "USD", mandate_data: singleUseMandateData, }, - Response: { - status: 200, - body: { - status: "succeeded", - }, - }, - }, - MandateSingleUse3DSManualCapture: { + }), + MandateSingleUse3DSManualCapture: getCustomExchange({ Request: { payment_method: "card", payment_method_data: { @@ -326,13 +310,7 @@ export const connectorDetails = { currency: "USD", mandate_data: singleUseMandateData, }, - Response: { - status: 200, - body: { - status: "requires_customer_action", - }, - }, - }, + }), MandateSingleUseNo3DSAutoCapture: { Request: { payment_method: "card", @@ -397,7 +375,7 @@ export const connectorDetails = { }, }, }, - MandateMultiUse3DSAutoCapture: { + MandateMultiUse3DSAutoCapture: getCustomExchange({ Request: { payment_method: "card", payment_method_data: { @@ -406,14 +384,8 @@ export const connectorDetails = { currency: "USD", mandate_data: multiUseMandateData, }, - Response: { - status: 200, - body: { - status: "requires_capture", - }, - }, - }, - MandateMultiUse3DSManualCapture: { + }), + MandateMultiUse3DSManualCapture: getCustomExchange({ Request: { payment_method: "card", payment_method_data: { @@ -422,13 +394,7 @@ export const connectorDetails = { currency: "USD", mandate_data: multiUseMandateData, }, - Response: { - status: 200, - body: { - status: "requires_capture", - }, - }, - }, + }), MITAutoCapture: { Request: {}, Response: { @@ -659,7 +625,7 @@ export const connectorDetails = { }, }, }, - PaymentMethodIdMandate3DSAutoCapture: { + PaymentMethodIdMandate3DSAutoCapture: getCustomExchange({ Request: { payment_method: "card", payment_method_data: { @@ -677,14 +643,8 @@ export const connectorDetails = { }, }, }, - Response: { - status: 200, - body: { - status: "requires_customer_action", - }, - }, - }, - PaymentMethodIdMandate3DSManualCapture: { + }), + PaymentMethodIdMandate3DSManualCapture: getCustomExchange({ Request: { payment_method: "card", payment_method_data: { @@ -701,13 +661,7 @@ export const connectorDetails = { }, }, }, - Response: { - status: 200, - body: { - status: "requires_customer_action", - }, - }, - }, + }), }, pm_list: { PmListResponse: { From 899c207d5835ba39f5163d12c6f59aed39884359 Mon Sep 17 00:00:00 2001 From: Riddhiagrawal001 <50551695+Riddhiagrawal001@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:02:38 +0530 Subject: [PATCH 17/46] feat(users): custom role at profile read (#6875) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> --- crates/api_models/src/user_role/role.rs | 2 + crates/common_enums/src/enums.rs | 14 +- crates/diesel_models/src/query/role.rs | 137 ++++++---- crates/diesel_models/src/role.rs | 9 + crates/diesel_models/src/schema.rs | 2 + crates/diesel_models/src/schema_v2.rs | 2 + crates/router/src/core/user.rs | 1 + crates/router/src/core/user_role.rs | 3 + crates/router/src/core/user_role/role.rs | 232 +++++++++++------ crates/router/src/db/kafka_store.rs | 40 ++- crates/router/src/db/role.rs | 233 +++++++++--------- .../src/services/authorization/roles.rs | 9 +- crates/router/src/utils/user_role.rs | 38 ++- .../down.sql | 2 + .../up.sql | 2 + .../down.sql | 2 + .../up.sql | 3 + .../down.sql | 10 + .../up.sql | 13 + 19 files changed, 481 insertions(+), 273 deletions(-) create mode 100644 migrations/2024-10-17-073555_add-profile-id-to-roles/down.sql create mode 100644 migrations/2024-10-17-073555_add-profile-id-to-roles/up.sql create mode 100644 migrations/2024-10-17-123943_add-profile-enum-in-role-scope/down.sql create mode 100644 migrations/2024-10-17-123943_add-profile-enum-in-role-scope/up.sql create mode 100644 migrations/2024-12-18-061400_change-roles-index/down.sql create mode 100644 migrations/2024-12-18-061400_change-roles-index/up.sql diff --git a/crates/api_models/src/user_role/role.rs b/crates/api_models/src/user_role/role.rs index 7c877cd7477..6b8736d3e76 100644 --- a/crates/api_models/src/user_role/role.rs +++ b/crates/api_models/src/user_role/role.rs @@ -7,6 +7,7 @@ pub struct CreateRoleRequest { pub role_name: String, pub groups: Vec, pub role_scope: RoleScope, + pub entity_type: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize)] @@ -21,6 +22,7 @@ pub struct RoleInfoWithGroupsResponse { pub groups: Vec, pub role_name: String, pub role_scope: RoleScope, + pub entity_type: EntityType, } #[derive(Debug, serde::Serialize)] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 2f5249ded57..d9d76dbc53e 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -2840,8 +2840,19 @@ pub enum TransactionType { #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum RoleScope { - Merchant, Organization, + Merchant, + Profile, +} + +impl From for EntityType { + fn from(role_scope: RoleScope) -> Self { + match role_scope { + RoleScope::Organization => Self::Organization, + RoleScope::Merchant => Self::Merchant, + RoleScope::Profile => Self::Profile, + } + } } /// Indicates the transaction status @@ -3296,6 +3307,7 @@ pub enum ApiVersion { serde::Serialize, strum::Display, strum::EnumString, + strum::EnumIter, ToSchema, Hash, )] diff --git a/crates/diesel_models/src/query/role.rs b/crates/diesel_models/src/query/role.rs index 2ab58ec2382..6fae9cda789 100644 --- a/crates/diesel_models/src/query/role.rs +++ b/crates/diesel_models/src/query/role.rs @@ -1,10 +1,12 @@ use async_bb8_diesel::AsyncRunQueryDsl; +use common_enums::EntityType; use common_utils::id_type; use diesel::{ associations::HasTable, debug_query, pg::Pg, result::Error as DieselError, BoolExpressionMethods, ExpressionMethods, QueryDsl, }; use error_stack::{report, ResultExt}; +use strum::IntoEnumIterator; use crate::{ enums::RoleScope, errors, query::generics, role::*, schema::roles::dsl, PgPooledConn, @@ -18,32 +20,23 @@ impl RoleNew { } impl Role { - pub async fn find_by_role_id(conn: &PgPooledConn, role_id: &str) -> StorageResult { - generics::generic_find_one::<::Table, _, _>( - conn, - dsl::role_id.eq(role_id.to_owned()), - ) - .await + fn get_entity_list( + current_entity: EntityType, + is_lineage_data_required: bool, + ) -> Vec { + is_lineage_data_required + .then(|| { + EntityType::iter() + .filter(|variant| *variant <= current_entity) + .collect() + }) + .unwrap_or(vec![current_entity]) } - // TODO: Remove once find_by_role_id_in_lineage is stable - pub async fn find_by_role_id_in_merchant_scope( - conn: &PgPooledConn, - role_id: &str, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - tenant_id: &id_type::TenantId, - ) -> StorageResult { + pub async fn find_by_role_id(conn: &PgPooledConn, role_id: &str) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, - dsl::role_id - .eq(role_id.to_owned()) - .and(dsl::tenant_id.eq(tenant_id.to_owned())) - .and( - dsl::merchant_id.eq(merchant_id.to_owned()).or(dsl::org_id - .eq(org_id.to_owned()) - .and(dsl::scope.eq(RoleScope::Organization))), - ), + dsl::role_id.eq(role_id.to_owned()), ) .await } @@ -53,6 +46,7 @@ impl Role { role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + profile_id: &id_type::ProfileId, tenant_id: &id_type::TenantId, ) -> StorageResult { generics::generic_find_one::<::Table, _, _>( @@ -62,9 +56,14 @@ impl Role { .and(dsl::tenant_id.eq(tenant_id.to_owned())) .and(dsl::org_id.eq(org_id.to_owned())) .and( - dsl::scope.eq(RoleScope::Organization).or(dsl::merchant_id - .eq(merchant_id.to_owned()) - .and(dsl::scope.eq(RoleScope::Merchant))), + dsl::scope + .eq(RoleScope::Organization) + .or(dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::scope.eq(RoleScope::Merchant))) + .or(dsl::profile_id + .eq(profile_id.to_owned()) + .and(dsl::scope.eq(RoleScope::Profile))), ), ) .await @@ -112,37 +111,13 @@ impl Role { .await } - pub async fn list_roles( - conn: &PgPooledConn, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - tenant_id: &id_type::TenantId, - ) -> StorageResult> { - let predicate = dsl::tenant_id - .eq(tenant_id.to_owned()) - .and(dsl::org_id.eq(org_id.to_owned())) - .and( - dsl::scope.eq(RoleScope::Organization).or(dsl::merchant_id - .eq(merchant_id.to_owned()) - .and(dsl::scope.eq(RoleScope::Merchant))), - ); - - generics::generic_filter::<::Table, _, _, _>( - conn, - predicate, - None, - None, - Some(dsl::last_modified_at.asc()), - ) - .await - } - + //TODO: Remove once generic_list_roles_by_entity_type is stable pub async fn generic_roles_list_for_org( conn: &PgPooledConn, tenant_id: id_type::TenantId, org_id: id_type::OrganizationId, merchant_id: Option, - entity_type: Option, + entity_type: Option, limit: Option, ) -> StorageResult> { let mut query = ::table() @@ -183,4 +158,64 @@ impl Role { }, } } + + pub async fn generic_list_roles_by_entity_type( + conn: &PgPooledConn, + payload: ListRolesByEntityPayload, + is_lineage_data_required: bool, + tenant_id: id_type::TenantId, + org_id: id_type::OrganizationId, + ) -> StorageResult> { + let mut query = ::table() + .into_boxed() + .filter(dsl::tenant_id.eq(tenant_id)) + .filter(dsl::org_id.eq(org_id)); + + match payload { + ListRolesByEntityPayload::Organization => { + let entity_in_vec = + Self::get_entity_list(EntityType::Organization, is_lineage_data_required); + query = query.filter(dsl::entity_type.eq_any(entity_in_vec)) + } + + ListRolesByEntityPayload::Merchant(merchant_id) => { + let entity_in_vec = + Self::get_entity_list(EntityType::Merchant, is_lineage_data_required); + query = query + .filter( + dsl::scope + .eq(RoleScope::Organization) + .or(dsl::merchant_id.eq(merchant_id)), + ) + .filter(dsl::entity_type.eq_any(entity_in_vec)) + } + + ListRolesByEntityPayload::Profile(merchant_id, profile_id) => { + let entity_in_vec = + Self::get_entity_list(EntityType::Profile, is_lineage_data_required); + query = query + .filter( + dsl::scope + .eq(RoleScope::Organization) + .or(dsl::scope + .eq(RoleScope::Merchant) + .and(dsl::merchant_id.eq(merchant_id.clone()))) + .or(dsl::profile_id.eq(profile_id)), + ) + .filter(dsl::entity_type.eq_any(entity_in_vec)) + } + }; + + router_env::logger::debug!(query = %debug_query::(&query).to_string()); + + match generics::db_metrics::track_database_call::( + query.get_results_async(conn), + generics::db_metrics::DatabaseOperation::Filter, + ) + .await + { + Ok(value) => Ok(value), + Err(err) => Err(report!(err)).change_context(errors::DatabaseError::Others), + } + } } diff --git a/crates/diesel_models/src/role.rs b/crates/diesel_models/src/role.rs index 16728801933..ba63dd61a0c 100644 --- a/crates/diesel_models/src/role.rs +++ b/crates/diesel_models/src/role.rs @@ -19,6 +19,7 @@ pub struct Role { pub last_modified_at: PrimitiveDateTime, pub last_modified_by: String, pub entity_type: enums::EntityType, + pub profile_id: Option, pub tenant_id: id_type::TenantId, } @@ -37,6 +38,7 @@ pub struct RoleNew { pub last_modified_at: PrimitiveDateTime, pub last_modified_by: String, pub entity_type: enums::EntityType, + pub profile_id: Option, pub tenant_id: id_type::TenantId, } @@ -75,3 +77,10 @@ impl From for RoleUpdateInternal { } } } + +#[derive(Clone, Debug)] +pub enum ListRolesByEntityPayload { + Profile(id_type::MerchantId, id_type::ProfileId), + Merchant(id_type::MerchantId), + Organization, +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 40343e44365..e748e5beca2 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1332,6 +1332,8 @@ diesel::table! { #[max_length = 64] entity_type -> Varchar, #[max_length = 64] + profile_id -> Nullable, + #[max_length = 64] tenant_id -> Varchar, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 99d6139f310..07a76c13ae7 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1279,6 +1279,8 @@ diesel::table! { #[max_length = 64] entity_type -> Varchar, #[max_length = 64] + profile_id -> Nullable, + #[max_length = 64] tenant_id -> Varchar, } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 52d7d6a252e..ead0d4cd572 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -637,6 +637,7 @@ async fn handle_invitation( &request.role_id, &user_from_token.merchant_id, &user_from_token.org_id, + &user_from_token.profile_id, user_from_token .tenant_id .as_ref() diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index 19d91b14f01..2b09a4ae274 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -127,6 +127,7 @@ pub async fn update_user_role( &req.role_id, &user_from_token.merchant_id, &user_from_token.org_id, + &user_from_token.profile_id, user_from_token .tenant_id .as_ref() @@ -551,6 +552,7 @@ pub async fn delete_user_role( &role_to_be_deleted.role_id, &user_from_token.merchant_id, &user_from_token.org_id, + &user_from_token.profile_id, user_from_token .tenant_id .as_ref() @@ -625,6 +627,7 @@ pub async fn delete_user_role( &role_to_be_deleted.role_id, &user_from_token.merchant_id, &user_from_token.org_id, + &user_from_token.profile_id, user_from_token .tenant_id .as_ref() diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs index ca4c062244c..34f0e8187ca 100644 --- a/crates/router/src/core/user_role/role.rs +++ b/crates/router/src/core/user_role/role.rs @@ -1,9 +1,9 @@ -use std::collections::HashSet; +use std::{cmp, collections::HashSet}; use api_models::user_role::role as role_api; -use common_enums::{EntityType, ParentGroup, PermissionGroup, RoleScope}; +use common_enums::{EntityType, ParentGroup, PermissionGroup}; use common_utils::generate_id_with_default_len; -use diesel_models::role::{RoleNew, RoleUpdate}; +use diesel_models::role::{ListRolesByEntityPayload, RoleNew, RoleUpdate}; use error_stack::{report, ResultExt}; use crate::{ @@ -65,6 +65,43 @@ pub async fn create_role( _req_state: ReqState, ) -> UserResponse { let now = common_utils::date_time::now(); + + let user_entity_type = user_from_token + .get_role_info_from_db(&state) + .await + .attach_printable("Invalid role_id in JWT")? + .get_entity_type(); + + let role_entity_type = req.entity_type.unwrap_or(EntityType::Merchant); + + if matches!(role_entity_type, EntityType::Organization) { + return Err(report!(UserErrors::InvalidRoleOperation)) + .attach_printable("User trying to create org level custom role"); + } + + // TODO: Remove in PR custom-role-write-pr + if matches!(role_entity_type, EntityType::Profile) { + return Err(report!(UserErrors::InvalidRoleOperation)) + .attach_printable("User trying to create profile level custom role"); + } + + let requestor_entity_from_role_scope = EntityType::from(req.role_scope); + + if requestor_entity_from_role_scope < role_entity_type { + return Err(report!(UserErrors::InvalidRoleOperation)).attach_printable(format!( + "User is trying to create role of type {} and scope {}", + role_entity_type, requestor_entity_from_role_scope + )); + } + let max_from_scope_and_entity = cmp::max(requestor_entity_from_role_scope, role_entity_type); + + if user_entity_type < max_from_scope_and_entity { + return Err(report!(UserErrors::InvalidRoleOperation)).attach_printable(format!( + "{} is trying to create of scope {} and of type {}", + user_entity_type, requestor_entity_from_role_scope, role_entity_type + )); + } + let role_name = RoleName::new(req.role_name)?; utils::user_role::validate_role_groups(&req.groups)?; @@ -77,33 +114,38 @@ pub async fn create_role( .tenant_id .as_ref() .unwrap_or(&state.tenant.tenant_id), + &user_from_token.profile_id, + &role_entity_type, ) .await?; - let user_role_info = user_from_token.get_role_info_from_db(&state).await?; - - if matches!(req.role_scope, RoleScope::Organization) - && user_role_info.get_entity_type() < EntityType::Organization - { - return Err(report!(UserErrors::InvalidRoleOperation)).attach_printable( - "User does not have sufficient privileges to perform organization-level role operation", - ); - } + let (org_id, merchant_id, profile_id) = match role_entity_type { + EntityType::Organization | EntityType::Tenant => { + (user_from_token.org_id, user_from_token.merchant_id, None) + } + EntityType::Merchant => (user_from_token.org_id, user_from_token.merchant_id, None), + EntityType::Profile => ( + user_from_token.org_id, + user_from_token.merchant_id, + Some(user_from_token.profile_id), + ), + }; let role = state .global_store .insert_role(RoleNew { role_id: generate_id_with_default_len("role"), role_name: role_name.get_role_name(), - merchant_id: user_from_token.merchant_id, - org_id: user_from_token.org_id, + merchant_id, + org_id, groups: req.groups, scope: req.role_scope, - entity_type: EntityType::Merchant, + entity_type: role_entity_type, created_by: user_from_token.user_id.clone(), last_modified_by: user_from_token.user_id, created_at: now, last_modified_at: now, + profile_id, tenant_id: user_from_token.tenant_id.unwrap_or(state.tenant.tenant_id), }) .await @@ -115,6 +157,7 @@ pub async fn create_role( role_id: role.role_id, role_name: role.role_name, role_scope: role.scope, + entity_type: role.entity_type, }, )) } @@ -146,6 +189,7 @@ pub async fn get_role_with_groups( role_id: role.role_id, role_name: role_info.get_role_name().to_string(), role_scope: role_info.get_scope(), + entity_type: role_info.get_entity_type(), }, )) } @@ -207,6 +251,36 @@ pub async fn update_role( ) -> UserResponse { let role_name = req.role_name.map(RoleName::new).transpose()?; + let role_info = roles::RoleInfo::from_role_id_in_lineage( + &state, + role_id, + &user_from_token.merchant_id, + &user_from_token.org_id, + &user_from_token.profile_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), + ) + .await + .to_not_found_response(UserErrors::InvalidRoleOperation)?; + + let user_role_info = user_from_token.get_role_info_from_db(&state).await?; + + let requested_entity_from_role_scope = EntityType::from(role_info.get_scope()); + let requested_role_entity_type = role_info.get_entity_type(); + let max_from_scope_and_entity = + cmp::max(requested_entity_from_role_scope, requested_role_entity_type); + + if user_role_info.get_entity_type() < max_from_scope_and_entity { + return Err(report!(UserErrors::InvalidRoleOperation)).attach_printable(format!( + "{} is trying to update of scope {} and of type {}", + user_role_info.get_entity_type(), + requested_entity_from_role_scope, + requested_role_entity_type + )); + } + if let Some(ref role_name) = role_name { utils::user_role::validate_role_name( &state, @@ -217,6 +291,8 @@ pub async fn update_role( .tenant_id .as_ref() .unwrap_or(&state.tenant.tenant_id), + &user_from_token.profile_id, + &role_info.get_entity_type(), ) .await?; } @@ -225,28 +301,6 @@ pub async fn update_role( utils::user_role::validate_role_groups(groups)?; } - let role_info = roles::RoleInfo::from_role_id_in_lineage( - &state, - role_id, - &user_from_token.merchant_id, - &user_from_token.org_id, - user_from_token - .tenant_id - .as_ref() - .unwrap_or(&state.tenant.tenant_id), - ) - .await - .to_not_found_response(UserErrors::InvalidRoleOperation)?; - - let user_role_info = user_from_token.get_role_info_from_db(&state).await?; - - if matches!(role_info.get_scope(), RoleScope::Organization) - && user_role_info.get_entity_type() != EntityType::Organization - { - return Err(report!(UserErrors::InvalidRoleOperation)) - .attach_printable("Non org admin user changing org level role"); - } - let updated_role = state .global_store .update_role_by_role_id( @@ -269,6 +323,7 @@ pub async fn update_role( role_id: updated_role.role_id, role_name: updated_role.role_name, role_scope: updated_role.scope, + entity_type: updated_role.entity_type, }, )) } @@ -296,40 +351,51 @@ pub async fn list_roles_with_info( .collect::>(); let user_role_entity = user_role_info.get_entity_type(); + let is_lineage_data_required = request.entity_type.is_none(); + let tenant_id = user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id) + .to_owned(); let custom_roles = match utils::user_role::get_min_entity(user_role_entity, request.entity_type)? { EntityType::Tenant | EntityType::Organization => state .global_store - .list_roles_for_org_by_parameters( - user_from_token - .tenant_id - .as_ref() - .unwrap_or(&state.tenant.tenant_id), - &user_from_token.org_id, - None, - request.entity_type, - None, + .generic_list_roles_by_entity_type( + ListRolesByEntityPayload::Organization, + is_lineage_data_required, + tenant_id, + user_from_token.org_id, ) .await .change_context(UserErrors::InternalServerError) .attach_printable("Failed to get roles")?, EntityType::Merchant => state .global_store - .list_roles_for_org_by_parameters( - user_from_token - .tenant_id - .as_ref() - .unwrap_or(&state.tenant.tenant_id), - &user_from_token.org_id, - Some(&user_from_token.merchant_id), - request.entity_type, - None, + .generic_list_roles_by_entity_type( + ListRolesByEntityPayload::Merchant(user_from_token.merchant_id), + is_lineage_data_required, + tenant_id, + user_from_token.org_id, + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to get roles")?, + + EntityType::Profile => state + .global_store + .generic_list_roles_by_entity_type( + ListRolesByEntityPayload::Profile( + user_from_token.merchant_id, + user_from_token.profile_id, + ), + is_lineage_data_required, + tenant_id, + user_from_token.org_id, ) .await .change_context(UserErrors::InternalServerError) .attach_printable("Failed to get roles")?, - // TODO: Populate this from Db function when support for profile id and profile level custom roles is added - EntityType::Profile => Vec::new(), }; role_info_vec.extend(custom_roles.into_iter().map(roles::RoleInfo::from)); @@ -378,18 +444,21 @@ pub async fn list_roles_at_entity_level( .map(|(_, role_info)| role_info.clone()) .collect::>(); + let tenant_id = user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id) + .to_owned(); + + let is_lineage_data_required = false; let custom_roles = match req.entity_type { EntityType::Tenant | EntityType::Organization => state .global_store - .list_roles_for_org_by_parameters( - user_from_token - .tenant_id - .as_ref() - .unwrap_or(&state.tenant.tenant_id), - &user_from_token.org_id, - None, - Some(req.entity_type), - None, + .generic_list_roles_by_entity_type( + ListRolesByEntityPayload::Organization, + is_lineage_data_required, + tenant_id, + user_from_token.org_id, ) .await .change_context(UserErrors::InternalServerError) @@ -397,21 +466,30 @@ pub async fn list_roles_at_entity_level( EntityType::Merchant => state .global_store - .list_roles_for_org_by_parameters( - user_from_token - .tenant_id - .as_ref() - .unwrap_or(&state.tenant.tenant_id), - &user_from_token.org_id, - Some(&user_from_token.merchant_id), - Some(req.entity_type), - None, + .generic_list_roles_by_entity_type( + ListRolesByEntityPayload::Merchant(user_from_token.merchant_id), + is_lineage_data_required, + tenant_id, + user_from_token.org_id, + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to get roles")?, + + EntityType::Profile => state + .global_store + .generic_list_roles_by_entity_type( + ListRolesByEntityPayload::Profile( + user_from_token.merchant_id, + user_from_token.profile_id, + ), + is_lineage_data_required, + tenant_id, + user_from_token.org_id, ) .await .change_context(UserErrors::InternalServerError) .attach_printable("Failed to get roles")?, - // TODO: Populate this from Db function when support for profile id and profile level custom roles is added - EntityType::Profile => Vec::new(), }; role_info_vec.extend(custom_roles.into_iter().map(roles::RoleInfo::from)); diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 22e2f10c71c..7d4f16ee893 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -3611,28 +3611,16 @@ impl RoleInterface for KafkaStore { self.diesel_store.find_role_by_role_id(role_id).await } - //TODO:Remove once find_by_role_id_in_lineage is stable - async fn find_role_by_role_id_in_merchant_scope( - &self, - role_id: &str, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - tenant_id: &id_type::TenantId, - ) -> CustomResult { - self.diesel_store - .find_role_by_role_id_in_merchant_scope(role_id, merchant_id, org_id, tenant_id) - .await - } - async fn find_role_by_role_id_in_lineage( &self, role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + profile_id: &id_type::ProfileId, tenant_id: &id_type::TenantId, ) -> CustomResult { self.diesel_store - .find_role_by_role_id_in_lineage(role_id, merchant_id, org_id, tenant_id) + .find_role_by_role_id_in_lineage(role_id, merchant_id, org_id, profile_id, tenant_id) .await } @@ -3664,17 +3652,7 @@ impl RoleInterface for KafkaStore { self.diesel_store.delete_role_by_role_id(role_id).await } - async fn list_all_roles( - &self, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - tenant_id: &id_type::TenantId, - ) -> CustomResult, errors::StorageError> { - self.diesel_store - .list_all_roles(merchant_id, org_id, tenant_id) - .await - } - + //TODO: Remove once generic_list_roles_by_entity_type is stable async fn list_roles_for_org_by_parameters( &self, tenant_id: &id_type::TenantId, @@ -3687,6 +3665,18 @@ impl RoleInterface for KafkaStore { .list_roles_for_org_by_parameters(tenant_id, org_id, merchant_id, entity_type, limit) .await } + + async fn generic_list_roles_by_entity_type( + &self, + payload: diesel_models::role::ListRolesByEntityPayload, + is_lineage_data_required: bool, + tenant_id: id_type::TenantId, + org_id: id_type::OrganizationId, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .generic_list_roles_by_entity_type(payload, is_lineage_data_required, tenant_id, org_id) + .await + } } #[async_trait::async_trait] diff --git a/crates/router/src/db/role.rs b/crates/router/src/db/role.rs index 1006c33aaa0..532a59f5149 100644 --- a/crates/router/src/db/role.rs +++ b/crates/router/src/db/role.rs @@ -1,6 +1,8 @@ -use common_enums::enums; use common_utils::id_type; -use diesel_models::role as storage; +use diesel_models::{ + enums::{EntityType, RoleScope}, + role as storage, +}; use error_stack::report; use router_env::{instrument, tracing}; @@ -23,20 +25,12 @@ pub trait RoleInterface { role_id: &str, ) -> CustomResult; - //TODO:Remove once find_by_role_id_in_lineage is stable - async fn find_role_by_role_id_in_merchant_scope( - &self, - role_id: &str, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - tenant_id: &id_type::TenantId, - ) -> CustomResult; - async fn find_role_by_role_id_in_lineage( &self, role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + profile_id: &id_type::ProfileId, tenant_id: &id_type::TenantId, ) -> CustomResult; @@ -58,21 +52,23 @@ pub trait RoleInterface { role_id: &str, ) -> CustomResult; - async fn list_all_roles( - &self, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - tenant_id: &id_type::TenantId, - ) -> CustomResult, errors::StorageError>; - + //TODO: Remove once generic_list_roles_by_entity_type is stable async fn list_roles_for_org_by_parameters( &self, tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: Option<&id_type::MerchantId>, - entity_type: Option, + entity_type: Option, limit: Option, ) -> CustomResult, errors::StorageError>; + + async fn generic_list_roles_by_entity_type( + &self, + payload: storage::ListRolesByEntityPayload, + is_lineage_data_required: bool, + tenant_id: id_type::TenantId, + org_id: id_type::OrganizationId, + ) -> CustomResult, errors::StorageError>; } #[async_trait::async_trait] @@ -99,41 +95,28 @@ impl RoleInterface for Store { .map_err(|error| report!(errors::StorageError::from(error))) } - //TODO:Remove once find_by_role_id_in_lineage is stable #[instrument(skip_all)] - async fn find_role_by_role_id_in_merchant_scope( + async fn find_role_by_role_id_in_lineage( &self, role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + profile_id: &id_type::ProfileId, tenant_id: &id_type::TenantId, ) -> CustomResult { let conn = connection::pg_connection_read(self).await?; - storage::Role::find_by_role_id_in_merchant_scope( + storage::Role::find_by_role_id_in_lineage( &conn, role_id, merchant_id, org_id, + profile_id, tenant_id, ) .await .map_err(|error| report!(errors::StorageError::from(error))) } - #[instrument(skip_all)] - async fn find_role_by_role_id_in_lineage( - &self, - role_id: &str, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - tenant_id: &id_type::TenantId, - ) -> CustomResult { - let conn = connection::pg_connection_read(self).await?; - storage::Role::find_by_role_id_in_lineage(&conn, role_id, merchant_id, org_id, tenant_id) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - } - #[instrument(skip_all)] async fn find_by_role_id_org_id_tenant_id( &self, @@ -170,26 +153,14 @@ impl RoleInterface for Store { .map_err(|error| report!(errors::StorageError::from(error))) } - #[instrument(skip_all)] - async fn list_all_roles( - &self, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - tenant_id: &id_type::TenantId, - ) -> CustomResult, errors::StorageError> { - let conn = connection::pg_connection_read(self).await?; - storage::Role::list_roles(&conn, merchant_id, org_id, tenant_id) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - } - + //TODO: Remove once generic_list_roles_by_entity_type is stable #[instrument(skip_all)] async fn list_roles_for_org_by_parameters( &self, tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: Option<&id_type::MerchantId>, - entity_type: Option, + entity_type: Option, limit: Option, ) -> CustomResult, errors::StorageError> { let conn = connection::pg_connection_read(self).await?; @@ -204,6 +175,26 @@ impl RoleInterface for Store { .await .map_err(|error| report!(errors::StorageError::from(error))) } + + #[instrument(skip_all)] + async fn generic_list_roles_by_entity_type( + &self, + payload: storage::ListRolesByEntityPayload, + is_lineage_data_required: bool, + tenant_id: id_type::TenantId, + org_id: id_type::OrganizationId, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage::Role::generic_list_roles_by_entity_type( + &conn, + payload, + is_lineage_data_required, + tenant_id, + org_id, + ) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + } } #[async_trait::async_trait] @@ -234,6 +225,7 @@ impl RoleInterface for MockDb { created_at: role.created_at, last_modified_at: role.last_modified_at, last_modified_by: role.last_modified_by, + profile_id: role.profile_id, tenant_id: role.tenant_id, }; roles.push(role.clone()); @@ -257,38 +249,12 @@ impl RoleInterface for MockDb { ) } - // TODO: Remove once find_by_role_id_in_lineage is stable - async fn find_role_by_role_id_in_merchant_scope( - &self, - role_id: &str, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - tenant_id: &id_type::TenantId, - ) -> CustomResult { - let roles = self.roles.lock().await; - roles - .iter() - .find(|role| { - role.role_id == role_id - && (role.tenant_id == *tenant_id) - && (role.merchant_id == *merchant_id - || (role.org_id == *org_id && role.scope == enums::RoleScope::Organization)) - }) - .cloned() - .ok_or( - errors::StorageError::ValueNotFound(format!( - "No role available in merchant scope for role_id = {role_id}, \ - merchant_id = {merchant_id:?} and org_id = {org_id:?}" - )) - .into(), - ) - } - async fn find_role_by_role_id_in_lineage( &self, role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + profile_id: &id_type::ProfileId, tenant_id: &id_type::TenantId, ) -> CustomResult { let roles = self.roles.lock().await; @@ -298,9 +264,15 @@ impl RoleInterface for MockDb { role.role_id == role_id && (role.tenant_id == *tenant_id) && role.org_id == *org_id - && ((role.scope == enums::RoleScope::Organization) - || (role.merchant_id == *merchant_id - && role.scope == enums::RoleScope::Merchant)) + && ((role.scope == RoleScope::Organization) + || (role.merchant_id == *merchant_id && role.scope == RoleScope::Merchant) + || (role + .profile_id + .as_ref() + .is_some_and(|profile_id_from_role| { + profile_id_from_role == profile_id + && role.scope == RoleScope::Profile + }))) }) .cloned() .ok_or( @@ -382,43 +354,14 @@ impl RoleInterface for MockDb { Ok(roles.remove(role_index)) } - async fn list_all_roles( - &self, - merchant_id: &id_type::MerchantId, - org_id: &id_type::OrganizationId, - tenant_id: &id_type::TenantId, - ) -> CustomResult, errors::StorageError> { - let roles = self.roles.lock().await; - - let roles_list: Vec<_> = roles - .iter() - .filter(|role| { - role.tenant_id == *tenant_id - && (role.merchant_id == *merchant_id - || (role.org_id == *org_id - && role.scope == diesel_models::enums::RoleScope::Organization)) - }) - .cloned() - .collect(); - - if roles_list.is_empty() { - return Err(errors::StorageError::ValueNotFound(format!( - "No role found for merchant id = {:?} and org_id = {:?}", - merchant_id, org_id - )) - .into()); - } - - Ok(roles_list) - } - + //TODO: Remove once generic_list_roles_by_entity_type is stable #[instrument(skip_all)] async fn list_roles_for_org_by_parameters( &self, tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: Option<&id_type::MerchantId>, - entity_type: Option, + entity_type: Option, limit: Option, ) -> CustomResult, errors::StorageError> { let roles = self.roles.lock().await; @@ -442,4 +385,72 @@ impl RoleInterface for MockDb { Ok(roles_list) } + + #[instrument(skip_all)] + async fn generic_list_roles_by_entity_type( + &self, + payload: storage::ListRolesByEntityPayload, + is_lineage_data_required: bool, + tenant_id: id_type::TenantId, + org_id: id_type::OrganizationId, + ) -> CustomResult, errors::StorageError> { + let roles = self.roles.lock().await; + let roles_list: Vec<_> = roles + .iter() + .filter(|role| match &payload { + storage::ListRolesByEntityPayload::Organization => { + let entity_in_vec = if is_lineage_data_required { + vec![ + EntityType::Organization, + EntityType::Merchant, + EntityType::Profile, + ] + } else { + vec![EntityType::Organization] + }; + + role.tenant_id == tenant_id + && role.org_id == org_id + && entity_in_vec.contains(&role.entity_type) + } + storage::ListRolesByEntityPayload::Merchant(merchant_id) => { + let entity_in_vec = if is_lineage_data_required { + vec![EntityType::Merchant, EntityType::Profile] + } else { + vec![EntityType::Merchant] + }; + + role.tenant_id == tenant_id + && role.org_id == org_id + && (role.scope == RoleScope::Organization + || role.merchant_id == *merchant_id) + && entity_in_vec.contains(&role.entity_type) + } + storage::ListRolesByEntityPayload::Profile(merchant_id, profile_id) => { + let entity_in_vec = [EntityType::Profile]; + + let matches_merchant = + role.merchant_id == *merchant_id && role.scope == RoleScope::Merchant; + + let matches_profile = + role.profile_id + .as_ref() + .is_some_and(|profile_id_from_role| { + profile_id_from_role == profile_id + && role.scope == RoleScope::Profile + }); + + role.tenant_id == tenant_id + && role.org_id == org_id + && (role.scope == RoleScope::Organization + || matches_merchant + || matches_profile) + && entity_in_vec.contains(&role.entity_type) + } + }) + .cloned() + .collect(); + + Ok(roles_list) + } } diff --git a/crates/router/src/services/authorization/roles.rs b/crates/router/src/services/authorization/roles.rs index df2a14a1a1a..c6b6946d15c 100644 --- a/crates/router/src/services/authorization/roles.rs +++ b/crates/router/src/services/authorization/roles.rs @@ -121,6 +121,7 @@ impl RoleInfo { role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + profile_id: &id_type::ProfileId, tenant_id: &id_type::TenantId, ) -> CustomResult { if let Some(role) = predefined_roles::PREDEFINED_ROLES.get(role_id) { @@ -128,7 +129,13 @@ impl RoleInfo { } else { state .global_store - .find_role_by_role_id_in_lineage(role_id, merchant_id, org_id, tenant_id) + .find_role_by_role_id_in_lineage( + role_id, + merchant_id, + org_id, + profile_id, + tenant_id, + ) .await .map(Self::from) } diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index 7413e66070f..b3e51136193 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -4,6 +4,7 @@ use common_enums::{EntityType, PermissionGroup}; use common_utils::id_type; use diesel_models::{ enums::{UserRoleVersion, UserStatus}, + role::ListRolesByEntityPayload, user_role::{UserRole, UserRoleUpdate}, }; use error_stack::{report, Report, ResultExt}; @@ -49,6 +50,8 @@ pub async fn validate_role_name( merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, tenant_id: &id_type::TenantId, + profile_id: &id_type::ProfileId, + entity_type: &EntityType, ) -> UserResult<()> { let role_name_str = role_name.clone().get_role_name(); @@ -56,16 +59,37 @@ pub async fn validate_role_name( .iter() .any(|(_, role_info)| role_info.get_role_name() == role_name_str); - // TODO: Create and use find_by_role_name to make this efficient - let is_present_in_custom_roles = state + let entity_type_for_role = match entity_type { + EntityType::Tenant | EntityType::Organization => ListRolesByEntityPayload::Organization, + EntityType::Merchant => ListRolesByEntityPayload::Merchant(merchant_id.to_owned()), + EntityType::Profile => { + ListRolesByEntityPayload::Profile(merchant_id.to_owned(), profile_id.to_owned()) + } + }; + + let is_present_in_custom_role = match state .global_store - .list_all_roles(merchant_id, org_id, tenant_id) + .generic_list_roles_by_entity_type( + entity_type_for_role, + false, + tenant_id.to_owned(), + org_id.to_owned(), + ) .await - .change_context(UserErrors::InternalServerError)? - .iter() - .any(|role| role.role_name == role_name_str); + { + Ok(roles_list) => roles_list + .iter() + .any(|role| role.role_name == role_name_str), + Err(e) => { + if e.current_context().is_db_not_found() { + false + } else { + return Err(UserErrors::InternalServerError.into()); + } + } + }; - if is_present_in_predefined_roles || is_present_in_custom_roles { + if is_present_in_predefined_roles || is_present_in_custom_role { return Err(UserErrors::RoleNameAlreadyExists.into()); } diff --git a/migrations/2024-10-17-073555_add-profile-id-to-roles/down.sql b/migrations/2024-10-17-073555_add-profile-id-to-roles/down.sql new file mode 100644 index 00000000000..d611be2c3da --- /dev/null +++ b/migrations/2024-10-17-073555_add-profile-id-to-roles/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE roles DROP COLUMN IF EXISTS profile_id; \ No newline at end of file diff --git a/migrations/2024-10-17-073555_add-profile-id-to-roles/up.sql b/migrations/2024-10-17-073555_add-profile-id-to-roles/up.sql new file mode 100644 index 00000000000..b3873266fec --- /dev/null +++ b/migrations/2024-10-17-073555_add-profile-id-to-roles/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE roles ADD COLUMN IF NOT EXISTS profile_id VARCHAR(64); \ No newline at end of file diff --git a/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/down.sql b/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/down.sql new file mode 100644 index 00000000000..c7c9cbeb401 --- /dev/null +++ b/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; \ No newline at end of file diff --git a/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/up.sql b/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/up.sql new file mode 100644 index 00000000000..6fd9b07fd50 --- /dev/null +++ b/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TYPE "RoleScope" +ADD VALUE IF NOT EXISTS 'profile'; \ No newline at end of file diff --git a/migrations/2024-12-18-061400_change-roles-index/down.sql b/migrations/2024-12-18-061400_change-roles-index/down.sql new file mode 100644 index 00000000000..f59b8c87669 --- /dev/null +++ b/migrations/2024-12-18-061400_change-roles-index/down.sql @@ -0,0 +1,10 @@ +-- This file should undo anything in `up.sql` +CREATE UNIQUE INDEX role_name_org_id_org_scope_index ON roles (org_id, role_name) +WHERE + scope = 'organization'; + +CREATE UNIQUE INDEX role_name_merchant_id_merchant_scope_index ON roles (merchant_id, role_name) +WHERE + scope = 'merchant'; + +DROP INDEX IF EXISTS roles_merchant_org_index; \ No newline at end of file diff --git a/migrations/2024-12-18-061400_change-roles-index/up.sql b/migrations/2024-12-18-061400_change-roles-index/up.sql new file mode 100644 index 00000000000..08203559eb9 --- /dev/null +++ b/migrations/2024-12-18-061400_change-roles-index/up.sql @@ -0,0 +1,13 @@ +-- Your SQL goes here + +DROP INDEX IF EXISTS role_name_org_id_org_scope_index; + +DROP INDEX IF EXISTS role_name_merchant_id_merchant_scope_index; + +DROP INDEX IF EXISTS roles_merchant_org_index; + +CREATE INDEX roles_merchant_org_index ON roles ( + org_id, + merchant_id, + profile_id +); \ No newline at end of file From 8d8ebe9051675d8102c6f9ea887bb23751ea5724 Mon Sep 17 00:00:00 2001 From: Suman Maji <77887221+sumanmaji4@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:03:11 +0530 Subject: [PATCH 18/46] refactor(core): add recurring customer support for nomupay payouts. (#6687) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 10 + api-reference/openapi_spec.json | 20 ++ crates/api_models/src/payment_methods.rs | 104 ++++++- crates/api_models/src/payouts.rs | 6 + crates/diesel_models/src/payment_method.rs | 143 +++++++++- crates/hyperswitch_connectors/src/utils.rs | 20 ++ .../hyperswitch_domain_models/src/mandates.rs | 259 +++++++++++++++++- .../src/merchant_connector_account.rs | 10 +- .../src/payment_methods.rs | 248 ++++++++++++++++- .../src/router_request_types.rs | 1 + crates/router/src/core/payment_methods.rs | 8 +- .../router/src/core/payment_methods/cards.rs | 42 +-- crates/router/src/core/payments.rs | 14 +- crates/router/src/core/payments/helpers.rs | 79 +++--- .../payments/operations/payment_response.rs | 23 +- crates/router/src/core/payments/routing.rs | 2 +- .../router/src/core/payments/tokenization.rs | 59 ++-- crates/router/src/core/payouts.rs | 219 ++++++++++++++- crates/router/src/core/payouts/helpers.rs | 118 +++++--- .../router/src/core/payouts/transformers.rs | 1 + crates/router/src/core/payouts/validator.rs | 131 +++++++-- crates/router/src/core/utils.rs | 6 +- crates/router/src/core/webhooks/incoming.rs | 42 +-- crates/router/src/types/transformers.rs | 27 +- crates/router/tests/connectors/utils.rs | 1 + 25 files changed, 1355 insertions(+), 238 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 447010178de..bc593adb45b 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -16999,6 +16999,11 @@ "example": "+1", "nullable": true, "maxLength": 255 + }, + "payout_method_id": { + "type": "string", + "description": "Identifier for payout method", + "nullable": true } }, "additionalProperties": false @@ -17238,6 +17243,11 @@ "example": "Invalid card details", "nullable": true, "maxLength": 1024 + }, + "payout_method_id": { + "type": "string", + "description": "Identifier for payout method", + "nullable": true } }, "additionalProperties": false diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index fa631eee788..bce5ac3c8e8 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -20921,6 +20921,11 @@ "example": "+1", "nullable": true, "maxLength": 255 + }, + "payout_method_id": { + "type": "string", + "description": "Identifier for payout method", + "nullable": true } } }, @@ -21219,6 +21224,11 @@ "example": "Invalid card details", "nullable": true, "maxLength": 1024 + }, + "payout_method_id": { + "type": "string", + "description": "Identifier for payout method", + "nullable": true } }, "additionalProperties": false @@ -21865,6 +21875,11 @@ "example": "+1", "nullable": true, "maxLength": 255 + }, + "payout_method_id": { + "type": "string", + "description": "Identifier for payout method", + "nullable": true } } }, @@ -22077,6 +22092,11 @@ "example": "+1", "nullable": true, "maxLength": 255 + }, + "payout_method_id": { + "type": "string", + "description": "Identifier for payout method", + "nullable": true } } }, diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 12bb97d9b7d..d15fdd74432 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -243,7 +243,8 @@ pub struct PaymentMethodMigrate { pub billing: Option, /// The connector mandate details of the payment method - pub connector_mandate_details: Option, + #[serde(deserialize_with = "deserialize_connector_mandate_details")] + pub connector_mandate_details: Option, // The CIT (customer initiated transaction) transaction id associated with the payment method pub network_transaction_id: Option, @@ -267,12 +268,22 @@ pub struct PaymentMethodMigrateResponse { pub network_transaction_id_migrated: Option, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentsMandateReference( pub HashMap, ); -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct PayoutsMandateReference( + pub HashMap, +); + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct PayoutsMandateReferenceRecord { + pub transfer_method_id: Option, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] pub struct PaymentsMandateReferenceRecord { pub connector_mandate_id: String, pub payment_method_type: Option, @@ -280,6 +291,80 @@ pub struct PaymentsMandateReferenceRecord { pub original_payment_authorized_currency: Option, } +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct CommonMandateReference { + pub payments: Option, + pub payouts: Option, +} + +impl From for PaymentsMandateReference { + fn from(common_mandate: CommonMandateReference) -> Self { + common_mandate.payments.unwrap_or_default() + } +} + +impl From for CommonMandateReference { + fn from(payments_reference: PaymentsMandateReference) -> Self { + Self { + payments: Some(payments_reference), + payouts: None, + } + } +} + +fn deserialize_connector_mandate_details<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let value: Option = + as de::Deserialize>::deserialize(deserializer)?; + + let payments_data = value + .clone() + .map(|mut mandate_details| { + mandate_details + .as_object_mut() + .map(|obj| obj.remove("payouts")); + + serde_json::from_value::(mandate_details) + }) + .transpose() + .map_err(|err| { + let err_msg = format!("{err:?}"); + de::Error::custom(format_args!( + "Failed to deserialize PaymentsMandateReference `{}`", + err_msg + )) + })?; + + let payouts_data = value + .clone() + .map(|mandate_details| { + serde_json::from_value::>(mandate_details).map( + |optional_common_mandate_details| { + optional_common_mandate_details + .and_then(|common_mandate_details| common_mandate_details.payouts) + }, + ) + }) + .transpose() + .map_err(|err| { + let err_msg = format!("{err:?}"); + de::Error::custom(format_args!( + "Failed to deserialize CommonMandateReference `{}`", + err_msg + )) + })? + .flatten(); + + Ok(Some(CommonMandateReference { + payments: payments_data, + payouts: payouts_data, + })) +} + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") @@ -313,7 +398,12 @@ impl PaymentMethodCreate { payment_method_issuer_code: payment_method_migrate.payment_method_issuer_code, metadata: payment_method_migrate.metadata.clone(), payment_method_data: payment_method_migrate.payment_method_data.clone(), - connector_mandate_details: payment_method_migrate.connector_mandate_details.clone(), + connector_mandate_details: payment_method_migrate + .connector_mandate_details + .clone() + .map(|common_mandate_reference| { + PaymentsMandateReference::from(common_mandate_reference) + }), client_secret: None, billing: payment_method_migrate.billing.clone(), card: card_details, @@ -2328,7 +2418,11 @@ impl }), email: record.email, }), - connector_mandate_details, + connector_mandate_details: connector_mandate_details.map( + |payments_mandate_reference| { + CommonMandateReference::from(payments_mandate_reference) + }, + ), metadata: None, payment_method_issuer_code: None, card_network: None, diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index 237d165d572..4c8a73b75a8 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -184,6 +184,9 @@ pub struct PayoutCreateRequest { /// Customer's phone country code. _Deprecated: Use customer object instead._ #[schema(deprecated, max_length = 255, example = "+1")] pub phone_country_code: Option, + + /// Identifier for payout method + pub payout_method_id: Option, } impl PayoutCreateRequest { @@ -568,6 +571,9 @@ pub struct PayoutCreateResponse { #[remove_in(PayoutCreateResponse)] #[schema(value_type = Option, max_length = 1024, example = "Invalid card details")] pub unified_message: Option, + + /// Identifier for payout method + pub payout_method_id: Option, } /// The payout method information for response diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index 76613984363..e8c6e1737b3 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -1,8 +1,13 @@ use std::collections::HashMap; use common_enums::MerchantStorageScheme; -use common_utils::{encryption::Encryption, pii}; +use common_utils::{ + encryption::Encryption, + errors::{CustomResult, ParsingError}, + pii, +}; use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; +use error_stack::ResultExt; #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") @@ -77,7 +82,7 @@ pub struct PaymentMethod { pub payment_method_data: Option, pub locker_id: Option, pub last_used_at: PrimitiveDateTime, - pub connector_mandate_details: Option, + pub connector_mandate_details: Option, pub customer_acceptance: Option, pub status: storage_enums::PaymentMethodStatus, pub network_transaction_id: Option, @@ -165,7 +170,7 @@ pub struct PaymentMethodNew { pub payment_method_data: Option, pub locker_id: Option, pub last_used_at: PrimitiveDateTime, - pub connector_mandate_details: Option, + pub connector_mandate_details: Option, pub customer_acceptance: Option, pub status: storage_enums::PaymentMethodStatus, pub network_transaction_id: Option, @@ -293,7 +298,7 @@ pub enum PaymentMethodUpdate { locker_fingerprint_id: Option, }, ConnectorMandateDetailsUpdate { - connector_mandate_details: Option, + connector_mandate_details: Option, }, } @@ -318,7 +323,7 @@ pub struct PaymentMethodUpdateInternal { status: Option, locker_id: Option, payment_method_type_v2: Option, - connector_mandate_details: Option, + connector_mandate_details: Option, updated_by: Option, payment_method_subtype: Option, last_modified: PrimitiveDateTime, @@ -970,3 +975,131 @@ impl std::ops::DerefMut for PaymentsMandateReference { } common_utils::impl_to_sql_from_sql_json!(PaymentsMandateReference); + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct PayoutsMandateReferenceRecord { + pub transfer_method_id: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, diesel::AsExpression)] +#[diesel(sql_type = diesel::sql_types::Jsonb)] +pub struct PayoutsMandateReference( + pub HashMap, +); + +impl std::ops::Deref for PayoutsMandateReference { + type Target = + HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for PayoutsMandateReference { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, diesel::AsExpression)] +#[diesel(sql_type = diesel::sql_types::Jsonb)] +pub struct CommonMandateReference { + pub payments: Option, + pub payouts: Option, +} + +impl CommonMandateReference { + pub fn get_mandate_details_value(&self) -> CustomResult { + let mut payments = self + .payments + .as_ref() + .map_or_else(|| Ok(serde_json::json!({})), serde_json::to_value) + .change_context(ParsingError::StructParseFailure("payment mandate details"))?; + + self.payouts + .as_ref() + .map(|payouts_mandate| { + serde_json::to_value(payouts_mandate).map(|payouts_mandate_value| { + payments.as_object_mut().map(|payments_object| { + payments_object.insert("payouts".to_string(), payouts_mandate_value); + }) + }) + }) + .transpose() + .change_context(ParsingError::StructParseFailure("payout mandate details"))?; + + Ok(payments) + } +} + +impl diesel::serialize::ToSql for CommonMandateReference { + fn to_sql<'b>( + &'b self, + out: &mut diesel::serialize::Output<'b, '_, diesel::pg::Pg>, + ) -> diesel::serialize::Result { + let payments = self.get_mandate_details_value()?; + + >::to_sql(&payments, &mut out.reborrow()) + } +} + +impl diesel::deserialize::FromSql + for CommonMandateReference +where + serde_json::Value: diesel::deserialize::FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result { + let value = >::from_sql(bytes)?; + + let payments_data = value + .clone() + .as_object_mut() + .map(|obj| { + obj.remove("payouts"); + + serde_json::from_value::(serde_json::Value::Object( + obj.clone(), + )) + .inspect_err(|err| { + router_env::logger::error!("Failed to parse payments data: {}", err); + }) + .change_context(ParsingError::StructParseFailure( + "Failed to parse payments data", + )) + }) + .transpose()?; + + let payouts_data = serde_json::from_value::>(value) + .inspect_err(|err| { + router_env::logger::error!("Failed to parse payouts data: {}", err); + }) + .change_context(ParsingError::StructParseFailure( + "Failed to parse payouts data", + )) + .map(|optional_common_mandate_details| { + optional_common_mandate_details + .and_then(|common_mandate_details| common_mandate_details.payouts) + })?; + + Ok(Self { + payments: payments_data, + payouts: payouts_data, + }) + } +} + +impl From for CommonMandateReference { + fn from(payment_reference: PaymentsMandateReference) -> Self { + Self { + payments: Some(payment_reference), + payouts: None, + } + } +} diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index 1180cb68f7c..c69641ebd11 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1348,6 +1348,26 @@ impl PhoneDetailsData for PhoneDetails { } } +#[cfg(feature = "payouts")] +pub trait PayoutFulfillRequestData { + fn get_connector_payout_id(&self) -> Result; + fn get_connector_transfer_method_id(&self) -> Result; +} +#[cfg(feature = "payouts")] +impl PayoutFulfillRequestData for hyperswitch_domain_models::router_request_types::PayoutsData { + fn get_connector_payout_id(&self) -> Result { + self.connector_payout_id + .clone() + .ok_or_else(missing_field_err("connector_payout_id")) + } + + fn get_connector_transfer_method_id(&self) -> Result { + self.connector_transfer_method_id + .clone() + .ok_or_else(missing_field_err("connector_transfer_method_id")) + } +} + pub trait CustomerData { fn get_email(&self) -> Result; } diff --git a/crates/hyperswitch_domain_models/src/mandates.rs b/crates/hyperswitch_domain_models/src/mandates.rs index d12b041a8ab..53104cc692f 100644 --- a/crates/hyperswitch_domain_models/src/mandates.rs +++ b/crates/hyperswitch_domain_models/src/mandates.rs @@ -1,10 +1,17 @@ +use std::collections::HashMap; + use api_models::payments::{ AcceptanceType as ApiAcceptanceType, CustomerAcceptance as ApiCustomerAcceptance, MandateAmountData as ApiMandateAmountData, MandateData as ApiMandateData, MandateType, OnlineMandate as ApiOnlineMandate, }; use common_enums::Currency; -use common_utils::{date_time, errors::ParsingError, pii, types::MinorUnit}; +use common_utils::{ + date_time, + errors::{CustomResult, ParsingError}, + pii, + types::MinorUnit, +}; use error_stack::ResultExt; use masking::{PeekInterface, Secret}; use time::PrimitiveDateTime; @@ -254,3 +261,253 @@ impl MandateAmountData { self.metadata.clone() } } + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentsMandateReferenceRecord { + pub connector_mandate_id: String, + pub payment_method_type: Option, + pub original_payment_authorized_amount: Option, + pub original_payment_authorized_currency: Option, + pub mandate_metadata: Option, + pub connector_mandate_status: Option, + pub connector_mandate_request_reference_id: Option, +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentsMandateReferenceRecord { + pub connector_mandate_id: String, + pub payment_method_subtype: Option, + pub original_payment_authorized_amount: Option, + pub original_payment_authorized_currency: Option, + pub mandate_metadata: Option, + pub connector_mandate_status: Option, + pub connector_mandate_request_reference_id: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PayoutsMandateReferenceRecord { + pub transfer_method_id: Option, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PayoutsMandateReference( + pub HashMap, +); + +impl std::ops::Deref for PayoutsMandateReference { + type Target = + HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for PayoutsMandateReference { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentsMandateReference( + pub HashMap, +); + +impl std::ops::Deref for PaymentsMandateReference { + type Target = + HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for PaymentsMandateReference { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct CommonMandateReference { + pub payments: Option, + pub payouts: Option, +} + +impl CommonMandateReference { + pub fn get_mandate_details_value(&self) -> CustomResult { + let mut payments = self + .payments + .as_ref() + .map_or_else(|| Ok(serde_json::json!({})), serde_json::to_value) + .change_context(ParsingError::StructParseFailure("payment mandate details"))?; + + self.payouts + .as_ref() + .map(|payouts_mandate| { + serde_json::to_value(payouts_mandate).map(|payouts_mandate_value| { + payments.as_object_mut().map(|payments_object| { + payments_object.insert("payouts".to_string(), payouts_mandate_value); + }) + }) + }) + .transpose() + .change_context(ParsingError::StructParseFailure("payout mandate details"))?; + + Ok(payments) + } +} + +impl From for CommonMandateReference { + fn from(value: diesel_models::CommonMandateReference) -> Self { + Self { + payments: value.payments.map(|payments| payments.into()), + payouts: value.payouts.map(|payouts| payouts.into()), + } + } +} + +impl From for diesel_models::CommonMandateReference { + fn from(value: CommonMandateReference) -> Self { + Self { + payments: value.payments.map(|payments| payments.into()), + payouts: value.payouts.map(|payouts| payouts.into()), + } + } +} + +impl From for PayoutsMandateReference { + fn from(value: diesel_models::PayoutsMandateReference) -> Self { + Self( + value + .0 + .into_iter() + .map(|(key, record)| (key, record.into())) + .collect(), + ) + } +} + +impl From for diesel_models::PayoutsMandateReference { + fn from(value: PayoutsMandateReference) -> Self { + Self( + value + .0 + .into_iter() + .map(|(key, record)| (key, record.into())) + .collect(), + ) + } +} + +impl From for PaymentsMandateReference { + fn from(value: diesel_models::PaymentsMandateReference) -> Self { + Self( + value + .0 + .into_iter() + .map(|(key, record)| (key, record.into())) + .collect(), + ) + } +} + +impl From for diesel_models::PaymentsMandateReference { + fn from(value: PaymentsMandateReference) -> Self { + Self( + value + .0 + .into_iter() + .map(|(key, record)| (key, record.into())) + .collect(), + ) + } +} + +impl From for PayoutsMandateReferenceRecord { + fn from(value: diesel_models::PayoutsMandateReferenceRecord) -> Self { + Self { + transfer_method_id: value.transfer_method_id, + } + } +} + +impl From for diesel_models::PayoutsMandateReferenceRecord { + fn from(value: PayoutsMandateReferenceRecord) -> Self { + Self { + transfer_method_id: value.transfer_method_id, + } + } +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +impl From for PaymentsMandateReferenceRecord { + fn from(value: diesel_models::PaymentsMandateReferenceRecord) -> Self { + Self { + connector_mandate_id: value.connector_mandate_id, + payment_method_type: value.payment_method_type, + original_payment_authorized_amount: value.original_payment_authorized_amount, + original_payment_authorized_currency: value.original_payment_authorized_currency, + mandate_metadata: value.mandate_metadata, + connector_mandate_status: value.connector_mandate_status, + connector_mandate_request_reference_id: value.connector_mandate_request_reference_id, + } + } +} + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +impl From for diesel_models::PaymentsMandateReferenceRecord { + fn from(value: PaymentsMandateReferenceRecord) -> Self { + Self { + connector_mandate_id: value.connector_mandate_id, + payment_method_type: value.payment_method_type, + original_payment_authorized_amount: value.original_payment_authorized_amount, + original_payment_authorized_currency: value.original_payment_authorized_currency, + mandate_metadata: value.mandate_metadata, + connector_mandate_status: value.connector_mandate_status, + connector_mandate_request_reference_id: value.connector_mandate_request_reference_id, + } + } +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl From for PaymentsMandateReferenceRecord { + fn from(value: diesel_models::PaymentsMandateReferenceRecord) -> Self { + Self { + connector_mandate_id: value.connector_mandate_id, + payment_method_subtype: value.payment_method_subtype, + original_payment_authorized_amount: value.original_payment_authorized_amount, + original_payment_authorized_currency: value.original_payment_authorized_currency, + mandate_metadata: value.mandate_metadata, + connector_mandate_status: value.connector_mandate_status, + connector_mandate_request_reference_id: value.connector_mandate_request_reference_id, + } + } +} + +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +impl From for diesel_models::PaymentsMandateReferenceRecord { + fn from(value: PaymentsMandateReferenceRecord) -> Self { + Self { + connector_mandate_id: value.connector_mandate_id, + payment_method_subtype: value.payment_method_subtype, + original_payment_authorized_amount: value.original_payment_authorized_amount, + original_payment_authorized_currency: value.original_payment_authorized_currency, + mandate_metadata: value.mandate_metadata, + connector_mandate_status: value.connector_mandate_status, + connector_mandate_request_reference_id: value.connector_mandate_request_reference_id, + } + } +} diff --git a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs index ed52325c231..cd85d56b72f 100644 --- a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs +++ b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs @@ -15,6 +15,7 @@ use serde_json::Value; use super::behaviour; use crate::{ + mandates::CommonMandateReference, router_data, type_encryption::{crypto_operation, CryptoOperation}, }; @@ -659,7 +660,7 @@ common_utils::create_list_wrapper!( pub fn is_merchant_connector_account_id_in_connector_mandate_details( &self, profile_id: Option<&id_type::ProfileId>, - connector_mandate_details: &diesel_models::PaymentsMandateReference, + connector_mandate_details: &CommonMandateReference, ) -> bool { let mca_ids = self .iter() @@ -671,8 +672,11 @@ common_utils::create_list_wrapper!( .collect::>(); connector_mandate_details - .keys() - .any(|mca_id| mca_ids.contains(mca_id)) + .payments + .as_ref() + .as_ref().is_some_and(|payments| { + payments.0.keys().any(|mca_id| mca_ids.contains(mca_id)) + }) } } ); diff --git a/crates/hyperswitch_domain_models/src/payment_methods.rs b/crates/hyperswitch_domain_models/src/payment_methods.rs index d03762dd64e..71ad9ee9275 100644 --- a/crates/hyperswitch_domain_models/src/payment_methods.rs +++ b/crates/hyperswitch_domain_models/src/payment_methods.rs @@ -2,7 +2,7 @@ use common_utils::crypto::Encryptable; use common_utils::{ crypto::OptionalEncryptableValue, - errors::{CustomResult, ValidationError}, + errors::{CustomResult, ParsingError, ValidationError}, pii, type_name, types::keymanager, }; @@ -13,7 +13,10 @@ use time::PrimitiveDateTime; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use crate::type_encryption::OptionalEncryptableJsonType; -use crate::type_encryption::{crypto_operation, AsyncLift, CryptoOperation}; +use crate::{ + mandates::{CommonMandateReference, PaymentsMandateReference}, + type_encryption::{crypto_operation, AsyncLift, CryptoOperation}, +}; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] @@ -84,7 +87,7 @@ pub struct PaymentMethod { OptionalEncryptableJsonType, pub locker_id: Option, pub last_used_at: PrimitiveDateTime, - pub connector_mandate_details: Option, + pub connector_mandate_details: Option, pub customer_acceptance: Option, pub status: storage_enums::PaymentMethodStatus, pub network_transaction_id: Option, @@ -138,6 +141,69 @@ impl PaymentMethod { pub fn get_payment_method_subtype(&self) -> Option { self.payment_method_subtype } + + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + pub fn get_common_mandate_reference(&self) -> Result { + let payments_data = self + .connector_mandate_details + .clone() + .map(|mut mandate_details| { + mandate_details + .as_object_mut() + .map(|obj| obj.remove("payouts")); + + serde_json::from_value::(mandate_details).inspect_err( + |err| { + router_env::logger::error!("Failed to parse payments data: {:?}", err); + }, + ) + }) + .transpose() + .map_err(|err| { + router_env::logger::error!("Failed to parse payments data: {:?}", err); + ParsingError::StructParseFailure("Failed to parse payments data") + })?; + + let payouts_data = self + .connector_mandate_details + .clone() + .map(|mandate_details| { + serde_json::from_value::>(mandate_details) + .inspect_err(|err| { + router_env::logger::error!("Failed to parse payouts data: {:?}", err); + }) + .map(|optional_common_mandate_details| { + optional_common_mandate_details + .and_then(|common_mandate_details| common_mandate_details.payouts) + }) + }) + .transpose() + .map_err(|err| { + router_env::logger::error!("Failed to parse payouts data: {:?}", err); + ParsingError::StructParseFailure("Failed to parse payouts data") + })? + .flatten(); + + Ok(CommonMandateReference { + payments: payments_data, + payouts: payouts_data, + }) + } + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + pub fn get_common_mandate_reference(&self) -> Result { + if let Some(value) = &self.connector_mandate_details { + Ok(value.clone()) + } else { + Ok(CommonMandateReference { + payments: None, + payouts: None, + }) + } + } } #[cfg(all( @@ -344,7 +410,7 @@ impl super::behaviour::Conversion for PaymentMethod { payment_method_data: self.payment_method_data.map(|val| val.into()), locker_id: self.locker_id.map(|id| id.get_string_repr().clone()), last_used_at: self.last_used_at, - connector_mandate_details: self.connector_mandate_details, + connector_mandate_details: self.connector_mandate_details.map(|cmd| cmd.into()), customer_acceptance: self.customer_acceptance, status: self.status, network_transaction_id: self.network_transaction_id, @@ -397,7 +463,7 @@ impl super::behaviour::Conversion for PaymentMethod { .await?, locker_id: item.locker_id.map(VaultId::generate), last_used_at: item.last_used_at, - connector_mandate_details: item.connector_mandate_details, + connector_mandate_details: item.connector_mandate_details.map(|cmd| cmd.into()), customer_acceptance: item.customer_acceptance, status: item.status, network_transaction_id: item.network_transaction_id, @@ -455,7 +521,7 @@ impl super::behaviour::Conversion for PaymentMethod { payment_method_data: self.payment_method_data.map(|val| val.into()), locker_id: self.locker_id.map(|id| id.get_string_repr().clone()), last_used_at: self.last_used_at, - connector_mandate_details: self.connector_mandate_details, + connector_mandate_details: self.connector_mandate_details.map(|cmd| cmd.into()), customer_acceptance: self.customer_acceptance, status: self.status, network_transaction_id: self.network_transaction_id, @@ -474,3 +540,173 @@ impl super::behaviour::Conversion for PaymentMethod { }) } } + +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + use common_utils::id_type::MerchantConnectorAccountId; + + use super::*; + + fn get_payment_method_with_mandate_data( + mandate_data: Option, + ) -> PaymentMethod { + let payment_method = PaymentMethod { + customer_id: common_utils::id_type::CustomerId::default(), + merchant_id: common_utils::id_type::MerchantId::default(), + payment_method_id: String::from("abc"), + accepted_currency: None, + scheme: None, + token: None, + cardholder_name: None, + issuer_name: None, + issuer_country: None, + payer_country: None, + is_stored: None, + swift_code: None, + direct_debit_token: None, + created_at: common_utils::date_time::now(), + last_modified: common_utils::date_time::now(), + payment_method: None, + payment_method_type: None, + payment_method_issuer: None, + payment_method_issuer_code: None, + metadata: None, + payment_method_data: None, + locker_id: None, + last_used_at: common_utils::date_time::now(), + connector_mandate_details: mandate_data, + customer_acceptance: None, + status: storage_enums::PaymentMethodStatus::Active, + network_transaction_id: None, + client_secret: None, + payment_method_billing_address: None, + updated_by: None, + version: common_enums::ApiVersion::V1, + network_token_requestor_reference_id: None, + network_token_locker_id: None, + network_token_payment_method_data: None, + }; + payment_method.clone() + } + + #[test] + fn test_get_common_mandate_reference_payments_only() { + let connector_mandate_details = serde_json::json!({ + "mca_kGz30G8B95MxRwmeQqy6": { + "mandate_metadata": null, + "payment_method_type": null, + "connector_mandate_id": "RcBww0a02c-R22w22w22wNJV-V14o20u24y18sTB18sB24y06g04eVZ04e20u14o", + "connector_mandate_status": "active", + "original_payment_authorized_amount": 51, + "original_payment_authorized_currency": "USD", + "connector_mandate_request_reference_id": "RowbU9ULN9H59bMhWk" + } + }); + + let payment_method = get_payment_method_with_mandate_data(Some(connector_mandate_details)); + + let result = payment_method.get_common_mandate_reference(); + + assert!(result.is_ok()); + let common_mandate = result.unwrap(); + + assert!(common_mandate.payments.is_some()); + assert!(common_mandate.payouts.is_none()); + + let payments = common_mandate.payments.unwrap(); + let result_mca = MerchantConnectorAccountId::wrap("mca_kGz30G8B95MxRwmeQqy6".to_string()); + assert!( + result_mca.is_ok(), + "Expected Ok, but got Err: {:?}", + result_mca + ); + let mca = result_mca.unwrap(); + assert!(payments.0.contains_key(&mca)); + } + + #[test] + fn test_get_common_mandate_reference_empty_details() { + let payment_method = get_payment_method_with_mandate_data(None); + let result = payment_method.get_common_mandate_reference(); + + assert!(result.is_ok()); + let common_mandate = result.unwrap(); + + assert!(common_mandate.payments.is_none()); + assert!(common_mandate.payouts.is_none()); + } + + #[test] + fn test_get_common_mandate_reference_payouts_only() { + let connector_mandate_details = serde_json::json!({ + "payouts": { + "mca_DAHVXbXpbYSjnL7fQWEs": { + "transfer_method_id": "TRM-678ab3997b16cb7cd" + } + } + }); + + let payment_method = get_payment_method_with_mandate_data(Some(connector_mandate_details)); + + let result = payment_method.get_common_mandate_reference(); + + assert!(result.is_ok()); + let common_mandate = result.unwrap(); + + assert!(common_mandate.payments.is_some()); + assert!(common_mandate.payouts.is_some()); + + let payouts = common_mandate.payouts.unwrap(); + let result_mca = MerchantConnectorAccountId::wrap("mca_DAHVXbXpbYSjnL7fQWEs".to_string()); + assert!( + result_mca.is_ok(), + "Expected Ok, but got Err: {:?}", + result_mca + ); + let mca = result_mca.unwrap(); + assert!(payouts.0.contains_key(&mca)); + } + + #[test] + fn test_get_common_mandate_reference_invalid_data() { + let connector_mandate_details = serde_json::json!("invalid"); + let payment_method = get_payment_method_with_mandate_data(Some(connector_mandate_details)); + let result = payment_method.get_common_mandate_reference(); + assert!(result.is_err()); + } + + #[test] + fn test_get_common_mandate_reference_with_payments_and_payouts_details() { + let connector_mandate_details = serde_json::json!({ + "mca_kGz30G8B95MxRwmeQqy6": { + "mandate_metadata": null, + "payment_method_type": null, + "connector_mandate_id": "RcBww0a02c-R22w22w22wNJV-V14o20u24y18sTB18sB24y06g04eVZ04e20u14o", + "connector_mandate_status": "active", + "original_payment_authorized_amount": 51, + "original_payment_authorized_currency": "USD", + "connector_mandate_request_reference_id": "RowbU9ULN9H59bMhWk" + }, + "payouts": { + "mca_DAHVXbXpbYSjnL7fQWEs": { + "transfer_method_id": "TRM-678ab3997b16cb7cd" + } + } + }); + + let payment_method = get_payment_method_with_mandate_data(Some(connector_mandate_details)); + + let result = payment_method.get_common_mandate_reference(); + + assert!(result.is_ok()); + let common_mandate = result.unwrap(); + + assert!(common_mandate.payments.is_some()); + assert!(common_mandate.payouts.is_some()); + } +} diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index 4598fa5d1b5..51e4cd5c8ec 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -823,6 +823,7 @@ pub struct PayoutsData { // New minor amount for amount framework pub minor_amount: MinorUnit, pub priority: Option, + pub connector_transfer_method_id: Option, } #[derive(Debug, Default, Clone)] diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 25ece15d47d..d9f0db4c2ac 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -41,6 +41,12 @@ use diesel_models::{ use error_stack::{report, ResultExt}; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use hyperswitch_domain_models::api::{GenericLinks, GenericLinksData}; +#[cfg(all( + feature = "v2", + feature = "payment_methods_v2", + feature = "customer_v2" +))] +use hyperswitch_domain_models::mandates::CommonMandateReference; use hyperswitch_domain_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use masking::ExposeInterface; @@ -1340,7 +1346,7 @@ pub async fn create_payment_method_in_db( api::payment_methods::PaymentMethodsData, >, key_store: &domain::MerchantKeyStore, - connector_mandate_details: Option, + connector_mandate_details: Option, status: Option, network_transaction_id: Option, storage_scheme: enums::MerchantStorageScheme, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 7be645a81c7..1f4fb6904fd 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -47,6 +47,7 @@ use euclid::frontend::dir; use hyperswitch_constraint_graph as cgraph; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use hyperswitch_domain_models::customer::CustomerUpdate; +use hyperswitch_domain_models::mandates::CommonMandateReference; #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") @@ -845,9 +846,10 @@ pub async fn skip_locker_call_and_migrate_payment_method( .clone() .and_then(|val| if val == json!({}) { None } else { Some(true) }) .or_else(|| { - req.connector_mandate_details - .clone() - .and_then(|val| (!val.0.is_empty()).then_some(false)) + req.connector_mandate_details.clone().and_then(|val| { + val.payments + .and_then(|payin_val| (!payin_val.0.is_empty()).then_some(false)) + }) }), ); @@ -2816,11 +2818,11 @@ pub async fn update_payment_method_connector_mandate_details( key_store: &domain::MerchantKeyStore, db: &dyn db::StorageInterface, pm: domain::PaymentMethod, - connector_mandate_details: Option, + connector_mandate_details: Option, storage_scheme: MerchantStorageScheme, ) -> errors::CustomResult<(), errors::VaultError> { let pm_update = payment_method::PaymentMethodUpdate::ConnectorMandateDetailsUpdate { - connector_mandate_details, + connector_mandate_details: connector_mandate_details.map(|cmd| cmd.into()), }; db.update_payment_method(&(state.into()), key_store, pm, pm_update, storage_scheme) @@ -2838,11 +2840,20 @@ pub async fn update_payment_method_connector_mandate_details( key_store: &domain::MerchantKeyStore, db: &dyn db::StorageInterface, pm: domain::PaymentMethod, - connector_mandate_details: Option, + connector_mandate_details: Option, storage_scheme: MerchantStorageScheme, ) -> errors::CustomResult<(), errors::VaultError> { + let connector_mandate_details_value = connector_mandate_details + .map(|common_mandate| { + common_mandate.get_mandate_details_value().map_err(|err| { + router_env::logger::error!("Failed to get get_mandate_details_value : {:?}", err); + errors::VaultError::UpdateInPaymentMethodDataTableFailed + }) + }) + .transpose()?; + let pm_update = payment_method::PaymentMethodUpdate::ConnectorMandateDetailsUpdate { - connector_mandate_details, + connector_mandate_details: connector_mandate_details_value, }; db.update_payment_method(&(state.into()), key_store, pm, pm_update, storage_scheme) @@ -4935,14 +4946,7 @@ pub async fn list_customer_payment_method( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("unable to decrypt payment method billing address details")?; let connector_mandate_details = pm - .connector_mandate_details - .clone() - .map(|val| { - val.parse_value::( - "PaymentsMandateReference", - ) - }) - .transpose() + .get_common_mandate_reference() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to deserialize to Payment Mandate Reference ")?; let mca_enabled = get_mca_status( @@ -4951,7 +4955,7 @@ pub async fn list_customer_payment_method( profile_id.clone(), merchant_account.get_id(), is_connector_agnostic_mit_enabled, - connector_mandate_details, + Some(connector_mandate_details), pm.network_transaction_id.as_ref(), ) .await?; @@ -5062,7 +5066,7 @@ pub async fn list_customer_payment_method( not(feature = "payment_methods_v2"), not(feature = "customer_v2") ))] -async fn get_pm_list_context( +pub async fn get_pm_list_context( state: &routes::SessionState, payment_method: &enums::PaymentMethod, #[cfg(feature = "payouts")] key_store: &domain::MerchantKeyStore, @@ -5217,7 +5221,7 @@ pub async fn get_mca_status( profile_id: Option, merchant_id: &id_type::MerchantId, is_connector_agnostic_mit_enabled: bool, - connector_mandate_details: Option, + connector_mandate_details: Option, network_transaction_id: Option<&String>, ) -> errors::RouterResult { if is_connector_agnostic_mit_enabled && network_transaction_id.is_some() { @@ -5255,7 +5259,7 @@ pub async fn get_mca_status( profile_id: Option, merchant_id: &id_type::MerchantId, is_connector_agnostic_mit_enabled: bool, - connector_mandate_details: Option<&payment_method::PaymentsMandateReference>, + connector_mandate_details: Option<&CommonMandateReference>, network_transaction_id: Option<&String>, merchant_connector_accounts: &domain::MerchantConnectorAccounts, ) -> bool { diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 5b8988dace0..8e31ab7975f 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -5945,16 +5945,12 @@ pub async fn decide_connector_for_normal_or_recurring_payment( where D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, { - let connector_mandate_details = &payment_method_info - .connector_mandate_details - .clone() - .map(|details| { - details - .parse_value::("connector_mandate_details") - }) - .transpose() + let connector_common_mandate_details = payment_method_info + .get_common_mandate_reference() .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("unable to deserialize connector mandate details")?; + .attach_printable("Failed to get the common mandate reference")?; + + let connector_mandate_details = connector_common_mandate_details.payments; let mut connector_choice = None; diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 56667b0517f..09fb3714421 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -6805,7 +6805,7 @@ where pub async fn validate_merchant_connector_ids_in_connector_mandate_details( state: &SessionState, key_store: &domain::MerchantKeyStore, - connector_mandate_details: &api_models::payment_methods::PaymentsMandateReference, + connector_mandate_details: &api_models::payment_methods::CommonMandateReference, merchant_id: &id_type::MerchantId, card_network: Option, ) -> CustomResult<(), errors::ApiErrorResponse> { @@ -6833,46 +6833,49 @@ pub async fn validate_merchant_connector_ids_in_connector_mandate_details( }) .collect(); - for (migrating_merchant_connector_id, migrating_connector_mandate_details) in - connector_mandate_details.0.clone() - { - match ( - card_network.clone(), - merchant_connector_account_details_hash_map.get(&migrating_merchant_connector_id), - ) { - (Some(enums::CardNetwork::Discover), Some(merchant_connector_account_details)) => { - if let ("cybersource", None) = ( - merchant_connector_account_details.connector_name.as_str(), - migrating_connector_mandate_details - .original_payment_authorized_amount - .zip( - migrating_connector_mandate_details - .original_payment_authorized_currency, - ), - ) { - Err(errors::ApiErrorResponse::MissingRequiredFields { - field_names: vec![ - "original_payment_authorized_currency", - "original_payment_authorized_amount", - ], - }) - .attach_printable(format!( - "Invalid connector_mandate_details provided for connector {:?}", - migrating_merchant_connector_id - ))? + if let Some(payment_mandate_reference) = &connector_mandate_details.payments { + let payments_map = payment_mandate_reference.0.clone(); + for (migrating_merchant_connector_id, migrating_connector_mandate_details) in payments_map { + match ( + card_network.clone(), + merchant_connector_account_details_hash_map.get(&migrating_merchant_connector_id), + ) { + (Some(enums::CardNetwork::Discover), Some(merchant_connector_account_details)) => { + if let ("cybersource", None) = ( + merchant_connector_account_details.connector_name.as_str(), + migrating_connector_mandate_details + .original_payment_authorized_amount + .zip( + migrating_connector_mandate_details + .original_payment_authorized_currency, + ), + ) { + Err(errors::ApiErrorResponse::MissingRequiredFields { + field_names: vec![ + "original_payment_authorized_currency", + "original_payment_authorized_amount", + ], + }) + .attach_printable(format!( + "Invalid connector_mandate_details provided for connector {:?}", + migrating_merchant_connector_id + ))? + } } + (_, Some(_)) => (), + (_, None) => Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "merchant_connector_id", + }) + .attach_printable_lazy(|| { + format!( + "{:?} invalid merchant connector id in connector_mandate_details", + migrating_merchant_connector_id + ) + })?, } - (_, Some(_)) => (), - (_, None) => Err(errors::ApiErrorResponse::InvalidDataValue { - field_name: "merchant_connector_id", - }) - .attach_printable_lazy(|| { - format!( - "{:?} invalid merchant connector id in connector_mandate_details", - migrating_merchant_connector_id - ) - })?, } + } else { + router_env::logger::error!("payment mandate reference not found"); } Ok(()) } diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 9692b1ca8a5..07342ffb4e6 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -6,7 +6,7 @@ use api_models::routing::RoutableConnectorChoice; use async_trait::async_trait; use common_enums::{AuthorizationStatus, SessionUpdateStatus}; use common_utils::{ - ext_traits::{AsyncExt, Encode, ValueExt}, + ext_traits::{AsyncExt, Encode}, types::{keymanager::KeyManagerState, ConnectorTransactionId, MinorUnit}, }; use error_stack::{report, ResultExt}; @@ -1591,14 +1591,7 @@ async fn payment_response_update_tracker( { // Parse value to check for mandates' existence let mandate_details = payment_method - .connector_mandate_details - .clone() - .map(|val| { - val.parse_value::( - "PaymentsMandateReference", - ) - }) - .transpose() + .get_common_mandate_reference() .change_context( errors::ApiErrorResponse::InternalServerError, ) @@ -1610,15 +1603,11 @@ async fn payment_response_update_tracker( payment_data.payment_attempt.merchant_connector_id.clone() { // check if the mandate has not already been set to active - if !mandate_details - .as_ref() - .map(|payment_mandate_reference| { - - payment_mandate_reference.0.get(&mca_id) + if !mandate_details.payments + .as_ref() + .and_then(|payments| payments.0.get(&mca_id)) .map(|payment_mandate_reference_record| payment_mandate_reference_record.connector_mandate_status == Some(common_enums::ConnectorMandateStatus::Active)) .unwrap_or(false) - }) - .unwrap_or(false) { let (connector_mandate_id, mandate_metadata,connector_mandate_request_reference_id) = payment_data.payment_attempt.connector_mandate_detail.clone() @@ -1627,7 +1616,7 @@ async fn payment_response_update_tracker( // Update the connector mandate details with the payment attempt connector mandate id let connector_mandate_details = tokenization::update_connector_mandate_details( - mandate_details, + Some(mandate_details), payment_data.payment_attempt.payment_method_type, Some( payment_data diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 271a5679282..dd592bc3064 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -154,7 +154,7 @@ pub fn make_dsl_input_for_payouts( .map(api_enums::PaymentMethod::foreign_from), payment_method_type: payout_data .payout_method_data - .clone() + .as_ref() .map(api_enums::PaymentMethodType::foreign_from), card_network: None, }; diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index ea81b8cb16f..597831231a3 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -15,6 +15,9 @@ use common_utils::{ id_type, pii, }; use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::mandates::{ + CommonMandateReference, PaymentsMandateReference, PaymentsMandateReferenceRecord, +}; use masking::{ExposeInterface, Secret}; use router_env::{instrument, tracing}; @@ -469,7 +472,7 @@ where .connector_mandate_details .clone() .map(|val| { - val.parse_value::( + val.parse_value::( "PaymentsMandateReference", ) }) @@ -1213,7 +1216,7 @@ pub fn add_connector_mandate_details_in_payment_method( connector_mandate_id: Option, mandate_metadata: Option>, connector_mandate_request_reference_id: Option, -) -> Option { +) -> Option { let mut mandate_details = HashMap::new(); if let Some((mca_id, connector_mandate_id)) = @@ -1221,7 +1224,7 @@ pub fn add_connector_mandate_details_in_payment_method( { mandate_details.insert( mca_id, - diesel_models::PaymentsMandateReferenceRecord { + PaymentsMandateReferenceRecord { connector_mandate_id, payment_method_type, original_payment_authorized_amount: authorized_amount, @@ -1231,7 +1234,10 @@ pub fn add_connector_mandate_details_in_payment_method( connector_mandate_request_reference_id, }, ); - Some(diesel_models::PaymentsMandateReference(mandate_details)) + Some(CommonMandateReference { + payments: Some(PaymentsMandateReference(mandate_details)), + payouts: None, + }) } else { None } @@ -1240,7 +1246,7 @@ pub fn add_connector_mandate_details_in_payment_method( #[allow(clippy::too_many_arguments)] #[cfg(feature = "v1")] pub fn update_connector_mandate_details( - mandate_details: Option, + mandate_details: Option, payment_method_type: Option, authorized_amount: Option, authorized_currency: Option, @@ -1248,13 +1254,16 @@ pub fn update_connector_mandate_details( connector_mandate_id: Option, mandate_metadata: Option>, connector_mandate_request_reference_id: Option, -) -> RouterResult> { - let mandate_reference = match mandate_details { +) -> RouterResult> { + let mandate_reference = match mandate_details + .as_ref() + .and_then(|common_mandate| common_mandate.payments.clone()) + { Some(mut payment_mandate_reference) => { if let Some((mca_id, connector_mandate_id)) = merchant_connector_id.clone().zip(connector_mandate_id) { - let updated_record = diesel_models::PaymentsMandateReferenceRecord { + let updated_record = PaymentsMandateReferenceRecord { connector_mandate_id: connector_mandate_id.clone(), payment_method_type, original_payment_authorized_amount: authorized_amount, @@ -1268,7 +1277,7 @@ pub fn update_connector_mandate_details( payment_mandate_reference .entry(mca_id) .and_modify(|pm| *pm = updated_record) - .or_insert(diesel_models::PaymentsMandateReferenceRecord { + .or_insert(PaymentsMandateReferenceRecord { connector_mandate_id, payment_method_type, original_payment_authorized_amount: authorized_amount, @@ -1277,7 +1286,13 @@ pub fn update_connector_mandate_details( connector_mandate_status: Some(ConnectorMandateStatus::Active), connector_mandate_request_reference_id, }); - Some(payment_mandate_reference) + + let payout_data = mandate_details.and_then(|common_mandate| common_mandate.payouts); + + Some(CommonMandateReference { + payments: Some(payment_mandate_reference), + payouts: payout_data, + }) } else { None } @@ -1292,26 +1307,20 @@ pub fn update_connector_mandate_details( connector_mandate_request_reference_id, ), }; - let connector_mandate_details = mandate_reference - .map(|mand| mand.encode_to_value()) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to serialize customer acceptance to value")?; - - Ok(connector_mandate_details) + Ok(mandate_reference) } #[cfg(feature = "v1")] pub fn update_connector_mandate_details_status( merchant_connector_id: id_type::MerchantConnectorAccountId, - mut payment_mandate_reference: diesel_models::PaymentsMandateReference, + mut payment_mandate_reference: PaymentsMandateReference, status: ConnectorMandateStatus, -) -> RouterResult> { +) -> RouterResult> { let mandate_reference = { payment_mandate_reference .entry(merchant_connector_id) .and_modify(|pm| { - let update_rec = diesel_models::PaymentsMandateReferenceRecord { + let update_rec = PaymentsMandateReferenceRecord { connector_mandate_id: pm.connector_mandate_id.clone(), payment_method_type: pm.payment_method_type, original_payment_authorized_amount: pm.original_payment_authorized_amount, @@ -1326,11 +1335,9 @@ pub fn update_connector_mandate_details_status( }); Some(payment_mandate_reference) }; - let connector_mandate_details = mandate_reference - .map(|mandate| mandate.encode_to_value()) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unable to serialize customer acceptance to value")?; - Ok(connector_mandate_details) + Ok(Some(CommonMandateReference { + payments: mandate_reference, + payouts: None, + })) } diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index 11be2ebfb5b..58d5e22b4b9 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -4,7 +4,10 @@ pub mod helpers; pub mod retry; pub mod transformers; pub mod validator; -use std::{collections::HashSet, vec::IntoIter}; +use std::{ + collections::{HashMap, HashSet}, + vec::IntoIter, +}; #[cfg(feature = "olap")] use api_models::payments as payment_enums; @@ -21,10 +24,16 @@ use common_utils::{ use diesel_models::{ enums as storage_enums, generic_link::{GenericLinkNew, PayoutLink}, + CommonMandateReference, PayoutsMandateReference, PayoutsMandateReferenceRecord, +}; +#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] +use diesel_models::{ + PaymentsMandateReference, PaymentsMandateReferenceRecord as PaymentsMandateReferenceRecordV2, }; use error_stack::{report, ResultExt}; #[cfg(feature = "olap")] use futures::future::join_all; +use hyperswitch_domain_models::payment_methods::PaymentMethod; use masking::{PeekInterface, Secret}; #[cfg(feature = "payout_retry")] use retry::GsmValidation; @@ -72,6 +81,7 @@ pub struct PayoutData { pub should_terminate: bool, pub payout_link: Option, pub current_locale: String, + pub payment_method: Option, } // ********************************************** CORE FLOWS ********************************************** @@ -319,7 +329,7 @@ pub async fn payouts_create_core( req: payouts::PayoutCreateRequest, ) -> RouterResponse { // Validate create request - let (payout_id, payout_method_data, profile_id, customer) = + let (payout_id, payout_method_data, profile_id, customer, payment_method) = validator::validate_create_request(&state, &merchant_account, &req, &key_store).await?; // Create DB entries @@ -333,6 +343,7 @@ pub async fn payouts_create_core( payout_method_data.as_ref(), &state.locale, customer.as_ref(), + payment_method.clone(), ) .await?; @@ -1126,12 +1137,13 @@ pub async fn call_connector_payout( ) .await?; // Create customer's disbursement account flow - complete_create_recipient_disburse_account( + Box::pin(complete_create_recipient_disburse_account( state, merchant_account, connector_data, payout_data, - ) + key_store, + )) .await?; // Payout creation flow Box::pin(complete_create_payout( @@ -1347,6 +1359,39 @@ pub async fn create_recipient( // Helps callee functions skip the execution payout_data.should_terminate = true; + } else if let Some(status) = recipient_create_data.status { + let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { + connector_payout_id: payout_data + .payout_attempt + .connector_payout_id + .to_owned(), + status, + error_code: None, + error_message: None, + is_eligible: recipient_create_data.payout_eligible, + unified_code: None, + unified_message: None, + }; + payout_data.payout_attempt = db + .update_payout_attempt( + &payout_data.payout_attempt, + updated_payout_attempt, + &payout_data.payouts, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error updating payout_attempt in db")?; + payout_data.payouts = db + .update_payout( + &payout_data.payouts, + storage::PayoutsUpdate::StatusUpdate { status }, + &payout_data.payout_attempt, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error updating payouts in db")?; } } Err(err) => Err(errors::ApiErrorResponse::PayoutFailed { @@ -1958,17 +2003,29 @@ pub async fn complete_create_recipient_disburse_account( merchant_account: &domain::MerchantAccount, connector_data: &api::ConnectorData, payout_data: &mut PayoutData, + key_store: &domain::MerchantKeyStore, ) -> RouterResult<()> { if !payout_data.should_terminate - && payout_data.payout_attempt.status - == storage_enums::PayoutStatus::RequiresVendorAccountCreation + && matches!( + payout_data.payout_attempt.status, + storage_enums::PayoutStatus::RequiresVendorAccountCreation + | storage_enums::PayoutStatus::RequiresCreation + ) && connector_data .connector_name .supports_vendor_disburse_account_create_for_payout() + && helpers::should_create_connector_transfer_method(&*payout_data, connector_data)? + .is_none() { - create_recipient_disburse_account(state, merchant_account, connector_data, payout_data) - .await - .attach_printable("Creation of customer failed")?; + Box::pin(create_recipient_disburse_account( + state, + merchant_account, + connector_data, + payout_data, + key_store, + )) + .await + .attach_printable("Creation of customer failed")?; } Ok(()) } @@ -1978,6 +2035,7 @@ pub async fn create_recipient_disburse_account( merchant_account: &domain::MerchantAccount, connector_data: &api::ConnectorData, payout_data: &mut PayoutData, + key_store: &domain::MerchantKeyStore, ) -> RouterResult<()> { // 1. Form Router data let router_data = core_utils::construct_payout_router_data( @@ -2015,7 +2073,7 @@ pub async fn create_recipient_disburse_account( .status .unwrap_or(payout_attempt.status.to_owned()); let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: payout_response_data.connector_payout_id, + connector_payout_id: payout_response_data.connector_payout_id.clone(), status, error_code: None, error_message: None, @@ -2033,6 +2091,89 @@ pub async fn create_recipient_disburse_account( .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error updating payout_attempt in db")?; + + if let ( + true, + Some(ref payout_method_data), + Some(connector_payout_id), + Some(customer_details), + Some(merchant_connector_id), + ) = ( + payout_data.payouts.recurring, + payout_data.payout_method_data.clone(), + payout_response_data.connector_payout_id.clone(), + payout_data.customer_details.clone(), + connector_data.merchant_connector_id.clone(), + ) { + let connector_mandate_details = HashMap::from([( + merchant_connector_id.clone(), + PayoutsMandateReferenceRecord { + transfer_method_id: Some(connector_payout_id), + }, + )]); + + let common_connector_mandate = CommonMandateReference { + payments: None, + payouts: Some(PayoutsMandateReference(connector_mandate_details)), + }; + + let connector_mandate_details_value = common_connector_mandate + .get_mandate_details_value() + .map_err(|err| { + router_env::logger::error!( + "Failed to get get_mandate_details_value : {:?}", + err + ); + errors::ApiErrorResponse::MandateUpdateFailed + })?; + + if let Some(pm_method) = payout_data.payment_method.clone() { + let pm_update = + diesel_models::PaymentMethodUpdate::ConnectorMandateDetailsUpdate { + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + connector_mandate_details: Some(connector_mandate_details_value), + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + connector_mandate_details: Some(common_connector_mandate), + }; + + payout_data.payment_method = Some( + db.update_payment_method( + &(state.into()), + key_store, + pm_method, + pm_update, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) + .attach_printable("Unable to find payment method")?, + ); + } else { + #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] + let customer_id = Some(customer_details.customer_id); + + #[cfg(all(feature = "v2", feature = "customer_v2"))] + let customer_id = customer_details.merchant_reference_id; + + if let Some(customer_id) = customer_id { + helpers::save_payout_data_to_locker( + state, + payout_data, + &customer_id, + payout_method_data, + Some(connector_mandate_details_value), + merchant_account, + key_store, + ) + .await + .attach_printable("Failed to save payout data to locker")?; + } + }; + } } Err(err) => { let (error_code, error_message) = (Some(err.code), Some(err.message)); @@ -2311,6 +2452,7 @@ pub async fn fulfill_payout( payout_data, &customer_id, &payout_method_data, + None, merchant_account, key_store, ) @@ -2385,6 +2527,22 @@ pub async fn response_handler( ) -> RouterResponse { let payout_attempt = payout_data.payout_attempt.to_owned(); let payouts = payout_data.payouts.to_owned(); + + let payout_method_id: Option = payout_data.payment_method.as_ref().map(|pm| { + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] + { + pm.payment_method_id.clone() + } + + #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] + { + pm.id.clone().get_string_repr().to_string() + } + }); + let payout_link = payout_data.payout_link.to_owned(); let billing_address = payout_data.billing_address.to_owned(); let customer_details = payout_data.customer_details.to_owned(); @@ -2456,6 +2614,7 @@ pub async fn response_handler( .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to parse payout link's URL")?, + payout_method_id, }; Ok(services::ApplicationResponse::Json(response)) } @@ -2472,6 +2631,7 @@ pub async fn payout_create_db_entries( _stored_payout_method_data: Option<&payouts::PayoutMethodData>, _locale: &str, _customer: Option<&domain::Customer>, + _payment_method: Option, ) -> RouterResult { todo!() } @@ -2489,6 +2649,7 @@ pub async fn payout_create_db_entries( stored_payout_method_data: Option<&payouts::PayoutMethodData>, locale: &str, customer: Option<&domain::Customer>, + payment_method: Option, ) -> RouterResult { let db = &*state.store; let merchant_id = merchant_account.get_id(); @@ -2541,13 +2702,22 @@ pub async fn payout_create_db_entries( // Make payouts entry let currency = req.currency.to_owned().get_required_value("currency")?; - let payout_type = req.payout_type.to_owned(); - let payout_method_id = if stored_payout_method_data.is_some() { - req.payout_token.to_owned() - } else { - None + let (payout_method_id, payout_type) = match stored_payout_method_data { + Some(payout_method_data) => ( + payment_method + .as_ref() + .map(|pm| pm.payment_method_id.clone()), + Some(api_enums::PayoutType::foreign_from(payout_method_data)), + ), + None => ( + payment_method + .as_ref() + .map(|pm| pm.payment_method_id.clone()), + req.payout_type.to_owned(), + ), }; + let client_secret = utils::generate_id( consts::ID_LENGTH, format!("payout_{payout_id}_secret").as_str(), @@ -2666,6 +2836,7 @@ pub async fn payout_create_db_entries( profile_id: profile_id.to_owned(), payout_link, current_locale: locale.to_string(), + payment_method, }) } @@ -2855,6 +3026,23 @@ pub async fn make_payout_data( .await .transpose()?; + let payout_method_id = payouts.payout_method_id.clone(); + let mut payment_method: Option = None; + + if let Some(pm_id) = payout_method_id { + payment_method = Some( + db.find_payment_method( + &(state.into()), + key_store, + &pm_id, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) + .attach_printable("Unable to find payment method")?, + ); + } + Ok(PayoutData { billing_address, business_profile, @@ -2867,6 +3055,7 @@ pub async fn make_payout_data( profile_id, payout_link, current_locale: locale.to_string(), + payment_method, }) } diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 5ebdea0de67..77cd9c266ff 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -194,6 +194,36 @@ pub async fn make_payout_method_data( } } +pub fn should_create_connector_transfer_method( + payout_data: &PayoutData, + connector_data: &api::ConnectorData, +) -> RouterResult> { + let connector_transfer_method_id = payout_data.payment_method.as_ref().and_then(|pm| { + let common_mandate_reference = pm + .get_common_mandate_reference() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize connector mandate details") + .ok()?; + + connector_data + .merchant_connector_id + .as_ref() + .and_then(|merchant_connector_id| { + common_mandate_reference + .payouts + .and_then(|payouts_mandate_reference| { + payouts_mandate_reference + .get(merchant_connector_id) + .and_then(|payouts_mandate_reference_record| { + payouts_mandate_reference_record.transfer_method_id.clone() + }) + }) + }) + }); + + Ok(connector_transfer_method_id) +} + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") @@ -203,6 +233,7 @@ pub async fn save_payout_data_to_locker( payout_data: &mut PayoutData, customer_id: &id_type::CustomerId, payout_method_data: &api::PayoutMethodData, + connector_mandate_details: Option, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, ) -> RouterResult<()> { @@ -290,14 +321,14 @@ pub async fn save_payout_data_to_locker( None, Some(bank.to_owned()), None, - api_enums::PaymentMethodType::foreign_from(bank.to_owned()), + api_enums::PaymentMethodType::foreign_from(bank), ), payouts::PayoutMethodData::Wallet(wallet) => ( payload, None, None, Some(wallet.to_owned()), - api_enums::PaymentMethodType::foreign_from(wallet.to_owned()), + api_enums::PaymentMethodType::foreign_from(wallet), ), payouts::PayoutMethodData::Card(_) => { Err(errors::ApiErrorResponse::InternalServerError)? @@ -409,9 +440,7 @@ pub async fn save_payout_data_to_locker( let card_isin = card_details.as_ref().map(|c| c.card_number.get_card_isin()); let mut payment_method = api::PaymentMethodCreate { - payment_method: Some(api_enums::PaymentMethod::foreign_from( - payout_method_data.to_owned(), - )), + payment_method: Some(api_enums::PaymentMethod::foreign_from(payout_method_data)), payment_method_type: Some(payment_method_type), payment_method_issuer: None, payment_method_issuer_code: None, @@ -497,7 +526,7 @@ pub async fn save_payout_data_to_locker( None, api::PaymentMethodCreate { payment_method: Some(api_enums::PaymentMethod::foreign_from( - payout_method_data.to_owned(), + payout_method_data, )), payment_method_type: Some(payment_method_type), payment_method_issuer: None, @@ -520,28 +549,30 @@ pub async fn save_payout_data_to_locker( // Insert new entry in payment_methods table if should_insert_in_pm_table { let payment_method_id = common_utils::generate_id(consts::ID_LENGTH, "pm"); - cards::create_payment_method( - state, - &new_payment_method, - customer_id, - &payment_method_id, - Some(stored_resp.card_reference.clone()), - merchant_account.get_id(), - None, - None, - card_details_encrypted.clone().map(Into::into), - key_store, - None, - None, - None, - merchant_account.storage_scheme, - None, - None, - None, - None, - None, - ) - .await?; + payout_data.payment_method = Some( + cards::create_payment_method( + state, + &new_payment_method, + customer_id, + &payment_method_id, + Some(stored_resp.card_reference.clone()), + merchant_account.get_id(), + None, + None, + card_details_encrypted.clone().map(Into::into), + key_store, + connector_mandate_details, + None, + None, + merchant_account.storage_scheme, + None, + None, + None, + None, + None, + ) + .await?, + ); } /* 1. Delete from locker @@ -600,22 +631,28 @@ pub async fn save_payout_data_to_locker( let pm_update = storage::PaymentMethodUpdate::PaymentMethodDataUpdate { payment_method_data: card_details_encrypted.map(Into::into), }; - db.update_payment_method( - &(state.into()), - key_store, - existing_pm, - pm_update, - merchant_account.storage_scheme, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to add payment method in db")?; + payout_data.payment_method = Some( + db.update_payment_method( + &(state.into()), + key_store, + existing_pm, + pm_update, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to add payment method in db")?, + ); }; // Store card_reference in payouts table - let updated_payout = storage::PayoutsUpdate::PayoutMethodIdUpdate { - payout_method_id: stored_resp.card_reference.to_owned(), + let payout_method_id = match &payout_data.payment_method { + Some(pm) => pm.payment_method_id.clone(), + None => stored_resp.card_reference.to_owned(), }; + + let updated_payout = storage::PayoutsUpdate::PayoutMethodIdUpdate { payout_method_id }; + payout_data.payouts = db .update_payout( payouts, @@ -636,6 +673,7 @@ pub async fn save_payout_data_to_locker( _payout_data: &mut PayoutData, _customer_id: &id_type::CustomerId, _payout_method_data: &api::PayoutMethodData, + _connector_mandate_details: Option, _merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, ) -> RouterResult<()> { diff --git a/crates/router/src/core/payouts/transformers.rs b/crates/router/src/core/payouts/transformers.rs index b4e41ecb09d..cb55f4629dd 100644 --- a/crates/router/src/core/payouts/transformers.rs +++ b/crates/router/src/core/payouts/transformers.rs @@ -115,6 +115,7 @@ impl phone_country_code: customer .as_ref() .and_then(|customer| customer.phone_country_code.clone()), + payout_method_id: payout.payout_method_id, } } } diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs index c647d6eaf10..3d7d457635c 100644 --- a/crates/router/src/core/payouts/validator.rs +++ b/crates/router/src/core/payouts/validator.rs @@ -7,10 +7,17 @@ use common_utils::validation::validate_domain_against_allowed_domains; use diesel_models::generic_link::PayoutLink; use error_stack::{report, ResultExt}; pub use hyperswitch_domain_models::errors::StorageError; +use hyperswitch_domain_models::payment_methods::PaymentMethod; use router_env::{instrument, tracing, which as router_env_which, Env}; use url::Url; use super::helpers; +#[cfg(all( + any(feature = "v2", feature = "v1"), + not(feature = "payment_methods_v2"), + not(feature = "customer_v2") +))] +use crate::core::payment_methods::cards::get_pm_list_context; use crate::{ core::{ errors::{self, RouterResult}, @@ -20,6 +27,7 @@ use crate::{ routes::SessionState, types::{api::payouts, domain, storage}, utils, + utils::OptionExt, }; #[instrument(skip(db))] @@ -57,6 +65,7 @@ pub async fn validate_create_request( Option, String, Option, + Option, )> { todo!() } @@ -76,6 +85,7 @@ pub async fn validate_create_request( Option, common_utils::id_type::ProfileId, Option, + Option, )> { let merchant_id = merchant_account.get_id(); @@ -137,28 +147,6 @@ pub async fn validate_create_request( None }; - // payout_token - let payout_method_data = match (req.payout_token.as_ref(), customer.as_ref()) { - (Some(_), None) => Err(report!(errors::ApiErrorResponse::MissingRequiredField { - field_name: "customer or customer_id when payout_token is provided" - })), - (Some(payout_token), Some(customer)) => { - helpers::make_payout_method_data( - state, - req.payout_method_data.as_ref(), - Some(payout_token), - &customer.customer_id, - merchant_account.get_id(), - req.payout_type, - merchant_key_store, - None, - merchant_account.storage_scheme, - ) - .await - } - _ => Ok(None), - }?; - #[cfg(feature = "v1")] let profile_id = core_utils::get_profile_id_from_business_details( &state.into(), @@ -182,7 +170,104 @@ pub async fn validate_create_request( }) .attach_printable("Profile id is a mandatory parameter")?; - Ok((payout_id, payout_method_data, profile_id, customer)) + let payment_method: Option = + match (req.payout_token.as_ref(), req.payout_method_id.clone()) { + (Some(_), Some(_)) => Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Only one of payout_method_id or payout_token should be provided." + .to_string(), + })), + (None, Some(payment_method_id)) => match customer.as_ref() { + Some(customer) => { + let payment_method = db + .find_payment_method( + &state.into(), + merchant_key_store, + &payment_method_id, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentMethodNotFound) + .attach_printable("Unable to find payment method")?; + + utils::when(payment_method.customer_id != customer.customer_id, || { + Err(report!(errors::ApiErrorResponse::InvalidRequestData { + message: "Payment method does not belong to this customer_id".to_string(), + }) + .attach_printable( + "customer_id in payment_method does not match with customer_id in request", + )) + })?; + Ok(Some(payment_method)) + } + None => Err(report!(errors::ApiErrorResponse::MissingRequiredField { + field_name: "customer_id when payment_method_id is passed", + })), + }, + _ => Ok(None), + }?; + + // payout_token + let payout_method_data = match ( + req.payout_token.as_ref(), + customer.as_ref(), + payment_method.as_ref(), + ) { + (Some(_), None, _) => Err(report!(errors::ApiErrorResponse::MissingRequiredField { + field_name: "customer or customer_id when payout_token is provided" + })), + (Some(payout_token), Some(customer), _) => { + helpers::make_payout_method_data( + state, + req.payout_method_data.as_ref(), + Some(payout_token), + &customer.customer_id, + merchant_account.get_id(), + req.payout_type, + merchant_key_store, + None, + merchant_account.storage_scheme, + ) + .await + } + (_, Some(_), Some(payment_method)) => { + match get_pm_list_context( + state, + payment_method + .payment_method + .as_ref() + .get_required_value("payment_method_id")?, + merchant_key_store, + payment_method, + None, + false, + ) + .await? + { + Some(pm) => match (pm.card_details, pm.bank_transfer_details) { + (Some(card), _) => Ok(Some(payouts::PayoutMethodData::Card( + api_models::payouts::CardPayout { + card_number: card.card_number.get_required_value("card_number")?, + card_holder_name: card.card_holder_name, + expiry_month: card.expiry_month.get_required_value("expiry_month")?, + expiry_year: card.expiry_year.get_required_value("expiry_month")?, + }, + ))), + (_, Some(bank)) => Ok(Some(payouts::PayoutMethodData::Bank(bank))), + _ => Ok(None), + }, + None => Ok(None), + } + } + _ => Ok(None), + }?; + + Ok(( + payout_id, + payout_method_data, + profile_id, + customer, + payment_method, + )) } pub fn validate_payout_link_request( diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 0f775eda855..22bc38b3d6c 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -24,7 +24,7 @@ use uuid::Uuid; use super::payments::helpers; #[cfg(feature = "payouts")] -use super::payouts::PayoutData; +use super::payouts::{helpers as payout_helpers, PayoutData}; #[cfg(feature = "payouts")] use crate::core::payments; use crate::{ @@ -150,6 +150,9 @@ pub async fn construct_payout_router_data<'a, F>( _ => None, }; + let connector_transfer_method_id = + payout_helpers::should_create_connector_transfer_method(&*payout_data, connector_data)?; + let router_data = types::RouterData { flow: PhantomData, merchant_id: merchant_account.get_id().to_owned(), @@ -192,6 +195,7 @@ pub async fn construct_payout_router_data<'a, F>( phone: c.phone.map(Encryptable::into_inner), phone_country_code: c.phone_country_code, }), + connector_transfer_method_id, }, response: Ok(types::PayoutsResponseData::default()), access_token: None, diff --git a/crates/router/src/core/webhooks/incoming.rs b/crates/router/src/core/webhooks/incoming.rs index 71130f247ad..2c0d9b5b5c5 100644 --- a/crates/router/src/core/webhooks/incoming.rs +++ b/crates/router/src/core/webhooks/incoming.rs @@ -4,10 +4,11 @@ use actix_web::FromRequest; #[cfg(feature = "payouts")] use api_models::payouts as payout_models; use api_models::webhooks::{self, WebhookResponseTracker}; -use common_utils::{errors::ReportSwitchExt, events::ApiEventsType, ext_traits::ValueExt}; +use common_utils::{errors::ReportSwitchExt, events::ApiEventsType}; use diesel_models::ConnectorMandateReferenceId; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ + mandates::CommonMandateReference, payments::{payment_attempt::PaymentAttempt, HeaderPayload}, router_request_types::VerifyWebhookSourceRequestData, router_response_types::{VerifyWebhookSourceResponseData, VerifyWebhookStatus}, @@ -2019,14 +2020,7 @@ async fn update_connector_mandate_details( let updated_connector_mandate_details = if let Some(webhook_mandate_details) = webhook_connector_mandate_details { let mandate_details = payment_method_info - .connector_mandate_details - .clone() - .map(|val| { - val.parse_value::( - "PaymentsMandateReference", - ) - }) - .transpose() + .get_common_mandate_reference() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to deserialize to Payment Mandate Reference")?; @@ -2035,13 +2029,9 @@ async fn update_connector_mandate_details( .clone() .get_required_value("merchant_connector_id")?; - if mandate_details - .as_ref() - .map(|details: &diesel_models::PaymentsMandateReference| { - !details.0.contains_key(&merchant_connector_account_id) - }) - .unwrap_or(true) - { + if mandate_details.payments.as_ref().map_or(true, |payments| { + !payments.0.contains_key(&merchant_connector_account_id) + }) { // Update the payment attempt to maintain consistency across tables. let (mandate_metadata, connector_mandate_request_reference_id) = payment_attempt @@ -2086,7 +2076,7 @@ async fn update_connector_mandate_details( insert_mandate_details( &payment_attempt, &webhook_mandate_details, - mandate_details, + Some(mandate_details), )? } else { logger::info!( @@ -2098,8 +2088,20 @@ async fn update_connector_mandate_details( None }; + let connector_mandate_details_value = updated_connector_mandate_details + .map(|common_mandate| { + common_mandate.get_mandate_details_value().map_err(|err| { + router_env::logger::error!( + "Failed to get get_mandate_details_value : {:?}", + err + ); + errors::ApiErrorResponse::MandateUpdateFailed + }) + }) + .transpose()?; + let pm_update = diesel_models::PaymentMethodUpdate::ConnectorNetworkTransactionIdAndMandateDetailsUpdate { - connector_mandate_details: updated_connector_mandate_details.map(masking::Secret::new), + connector_mandate_details: connector_mandate_details_value.map(masking::Secret::new), network_transaction_id: webhook_connector_network_transaction_id .map(|webhook_network_transaction_id| webhook_network_transaction_id.get_id().clone()), }; @@ -2124,8 +2126,8 @@ async fn update_connector_mandate_details( fn insert_mandate_details( payment_attempt: &PaymentAttempt, webhook_mandate_details: &hyperswitch_domain_models::router_flow_types::ConnectorMandateDetails, - payment_method_mandate_details: Option, -) -> CustomResult, errors::ApiErrorResponse> { + payment_method_mandate_details: Option, +) -> CustomResult, errors::ApiErrorResponse> { let (mandate_metadata, connector_mandate_request_reference_id) = payment_attempt .connector_mandate_detail .clone() diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 2348fae72ab..c1d9b35be82 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1403,8 +1403,8 @@ impl ForeignFrom for payments::CaptureResponse { } #[cfg(feature = "payouts")] -impl ForeignFrom for api_enums::PaymentMethodType { - fn foreign_from(value: api_models::payouts::PayoutMethodData) -> Self { +impl ForeignFrom<&api_models::payouts::PayoutMethodData> for api_enums::PaymentMethodType { + fn foreign_from(value: &api_models::payouts::PayoutMethodData) -> Self { match value { api_models::payouts::PayoutMethodData::Bank(bank) => Self::foreign_from(bank), api_models::payouts::PayoutMethodData::Card(_) => Self::Debit, @@ -1414,8 +1414,8 @@ impl ForeignFrom for api_enums::PaymentMe } #[cfg(feature = "payouts")] -impl ForeignFrom for api_enums::PaymentMethodType { - fn foreign_from(value: api_models::payouts::Bank) -> Self { +impl ForeignFrom<&api_models::payouts::Bank> for api_enums::PaymentMethodType { + fn foreign_from(value: &api_models::payouts::Bank) -> Self { match value { api_models::payouts::Bank::Ach(_) => Self::Ach, api_models::payouts::Bank::Bacs(_) => Self::Bacs, @@ -1426,8 +1426,8 @@ impl ForeignFrom for api_enums::PaymentMethodType { } #[cfg(feature = "payouts")] -impl ForeignFrom for api_enums::PaymentMethodType { - fn foreign_from(value: api_models::payouts::Wallet) -> Self { +impl ForeignFrom<&api_models::payouts::Wallet> for api_enums::PaymentMethodType { + fn foreign_from(value: &api_models::payouts::Wallet) -> Self { match value { api_models::payouts::Wallet::Paypal(_) => Self::Paypal, api_models::payouts::Wallet::Venmo(_) => Self::Venmo, @@ -1436,8 +1436,8 @@ impl ForeignFrom for api_enums::PaymentMethodType { } #[cfg(feature = "payouts")] -impl ForeignFrom for api_enums::PaymentMethod { - fn foreign_from(value: api_models::payouts::PayoutMethodData) -> Self { +impl ForeignFrom<&api_models::payouts::PayoutMethodData> for api_enums::PaymentMethod { + fn foreign_from(value: &api_models::payouts::PayoutMethodData) -> Self { match value { api_models::payouts::PayoutMethodData::Bank(_) => Self::BankTransfer, api_models::payouts::PayoutMethodData::Card(_) => Self::Card, @@ -1446,6 +1446,17 @@ impl ForeignFrom for api_enums::PaymentMe } } +#[cfg(feature = "payouts")] +impl ForeignFrom<&api_models::payouts::PayoutMethodData> for api_models::enums::PayoutType { + fn foreign_from(value: &api_models::payouts::PayoutMethodData) -> Self { + match value { + api_models::payouts::PayoutMethodData::Bank(_) => Self::Bank, + api_models::payouts::PayoutMethodData::Card(_) => Self::Card, + api_models::payouts::PayoutMethodData::Wallet(_) => Self::Wallet, + } + } +} + #[cfg(feature = "payouts")] impl ForeignFrom for api_enums::PaymentMethod { fn foreign_from(value: api_models::enums::PayoutType) -> Self { diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index ca818e61753..fd84f5767de 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -471,6 +471,7 @@ pub trait ConnectorActions: Connector { }), vendor_details: None, priority: None, + connector_transfer_method_id: None, }, payment_info, ) From 12a2f2ad147346365f828d8fc97eb9fe49a845bb Mon Sep 17 00:00:00 2001 From: Sandeep Kumar <83278309+tsdk02@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:04:00 +0530 Subject: [PATCH 19/46] feat(analytics): Add currency as dimension and filter for disputes (#7006) --- crates/analytics/src/disputes/core.rs | 1 + crates/analytics/src/disputes/filters.rs | 7 ++++++- crates/analytics/src/disputes/metrics.rs | 1 + .../src/disputes/metrics/dispute_status_metric.rs | 1 + .../sessionized_metrics/dispute_status_metric.rs | 1 + .../sessionized_metrics/total_amount_disputed.rs | 1 + .../sessionized_metrics/total_dispute_lost_amount.rs | 1 + .../src/disputes/metrics/total_amount_disputed.rs | 1 + .../disputes/metrics/total_dispute_lost_amount.rs | 1 + crates/analytics/src/disputes/types.rs | 6 ++++++ crates/analytics/src/sqlx.rs | 12 ++++++++++++ crates/api_models/src/analytics/disputes.rs | 10 +++++++++- 12 files changed, 41 insertions(+), 2 deletions(-) diff --git a/crates/analytics/src/disputes/core.rs b/crates/analytics/src/disputes/core.rs index 540a14104c1..c1dcbaf2b2f 100644 --- a/crates/analytics/src/disputes/core.rs +++ b/crates/analytics/src/disputes/core.rs @@ -204,6 +204,7 @@ pub async fn get_filters( .filter_map(|fil: DisputeFilterRow| match dim { DisputeDimensions::DisputeStage => fil.dispute_stage, DisputeDimensions::Connector => fil.connector, + DisputeDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), }) .collect::>(); res.query_data.push(DisputeFilterValue { diff --git a/crates/analytics/src/disputes/filters.rs b/crates/analytics/src/disputes/filters.rs index cd60b502257..9e4aa302644 100644 --- a/crates/analytics/src/disputes/filters.rs +++ b/crates/analytics/src/disputes/filters.rs @@ -1,12 +1,16 @@ use api_models::analytics::{disputes::DisputeDimensions, Granularity, TimeRange}; use common_utils::errors::ReportSwitchExt; +use diesel_models::enums::Currency; use error_stack::ResultExt; use time::PrimitiveDateTime; use crate::{ enums::AuthInfo, query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window}, - types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow}, + types::{ + AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult, + LoadRow, + }, }; pub trait DisputeFilterAnalytics: LoadRow {} @@ -48,4 +52,5 @@ pub struct DisputeFilterRow { pub dispute_status: Option, pub connector_status: Option, pub dispute_stage: Option, + pub currency: Option>, } diff --git a/crates/analytics/src/disputes/metrics.rs b/crates/analytics/src/disputes/metrics.rs index 6514e5fcbe0..72bb09003d3 100644 --- a/crates/analytics/src/disputes/metrics.rs +++ b/crates/analytics/src/disputes/metrics.rs @@ -27,6 +27,7 @@ pub struct DisputeMetricRow { pub dispute_stage: Option>, pub dispute_status: Option>, pub connector: Option, + pub currency: Option>, pub total: Option, pub count: Option, #[serde(with = "common_utils::custom_serde::iso8601::option")] diff --git a/crates/analytics/src/disputes/metrics/dispute_status_metric.rs b/crates/analytics/src/disputes/metrics/dispute_status_metric.rs index ce962e284f6..b23dba4af38 100644 --- a/crates/analytics/src/disputes/metrics/dispute_status_metric.rs +++ b/crates/analytics/src/disputes/metrics/dispute_status_metric.rs @@ -97,6 +97,7 @@ where DisputeMetricsBucketIdentifier::new( i.dispute_stage.as_ref().map(|i| i.0), i.connector.clone(), + i.currency.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/disputes/metrics/sessionized_metrics/dispute_status_metric.rs b/crates/analytics/src/disputes/metrics/sessionized_metrics/dispute_status_metric.rs index 9a7b0535819..0d54d75aee5 100644 --- a/crates/analytics/src/disputes/metrics/sessionized_metrics/dispute_status_metric.rs +++ b/crates/analytics/src/disputes/metrics/sessionized_metrics/dispute_status_metric.rs @@ -97,6 +97,7 @@ where DisputeMetricsBucketIdentifier::new( i.dispute_stage.as_ref().map(|i| i.0), i.connector.clone(), + i.currency.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/disputes/metrics/sessionized_metrics/total_amount_disputed.rs b/crates/analytics/src/disputes/metrics/sessionized_metrics/total_amount_disputed.rs index 5c5eceb0619..bf2332ab12f 100644 --- a/crates/analytics/src/disputes/metrics/sessionized_metrics/total_amount_disputed.rs +++ b/crates/analytics/src/disputes/metrics/sessionized_metrics/total_amount_disputed.rs @@ -98,6 +98,7 @@ where DisputeMetricsBucketIdentifier::new( i.dispute_stage.as_ref().map(|i| i.0), i.connector.clone(), + i.currency.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/disputes/metrics/sessionized_metrics/total_dispute_lost_amount.rs b/crates/analytics/src/disputes/metrics/sessionized_metrics/total_dispute_lost_amount.rs index d6308b09f33..5d10becbbe2 100644 --- a/crates/analytics/src/disputes/metrics/sessionized_metrics/total_dispute_lost_amount.rs +++ b/crates/analytics/src/disputes/metrics/sessionized_metrics/total_dispute_lost_amount.rs @@ -99,6 +99,7 @@ where DisputeMetricsBucketIdentifier::new( i.dispute_stage.as_ref().map(|i| i.0), i.connector.clone(), + i.currency.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/disputes/metrics/total_amount_disputed.rs b/crates/analytics/src/disputes/metrics/total_amount_disputed.rs index 68c7fa6d166..fc85ee39b26 100644 --- a/crates/analytics/src/disputes/metrics/total_amount_disputed.rs +++ b/crates/analytics/src/disputes/metrics/total_amount_disputed.rs @@ -97,6 +97,7 @@ where DisputeMetricsBucketIdentifier::new( i.dispute_stage.as_ref().map(|i| i.0), i.connector.clone(), + i.currency.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/disputes/metrics/total_dispute_lost_amount.rs b/crates/analytics/src/disputes/metrics/total_dispute_lost_amount.rs index d14d4982701..33228cbb58b 100644 --- a/crates/analytics/src/disputes/metrics/total_dispute_lost_amount.rs +++ b/crates/analytics/src/disputes/metrics/total_dispute_lost_amount.rs @@ -98,6 +98,7 @@ where DisputeMetricsBucketIdentifier::new( i.dispute_stage.as_ref().map(|i| i.0), i.connector.clone(), + i.currency.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/disputes/types.rs b/crates/analytics/src/disputes/types.rs index 762e8d27554..da66744aede 100644 --- a/crates/analytics/src/disputes/types.rs +++ b/crates/analytics/src/disputes/types.rs @@ -24,6 +24,12 @@ where .attach_printable("Error adding dispute stage filter")?; } + if !self.currency.is_empty() { + builder + .add_filter_in_range_clause(DisputeDimensions::Currency, &self.currency) + .attach_printable("Error adding currency filter")?; + } + Ok(()) } } diff --git a/crates/analytics/src/sqlx.rs b/crates/analytics/src/sqlx.rs index 2a94d528768..653d019adeb 100644 --- a/crates/analytics/src/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -933,11 +933,17 @@ impl<'a> FromRow<'a, PgRow> for super::disputes::filters::DisputeFilterRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + let currency: Option> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; Ok(Self { dispute_stage, dispute_status, connector, connector_status, + currency, }) } } @@ -957,6 +963,11 @@ impl<'a> FromRow<'a, PgRow> for super::disputes::metrics::DisputeMetricRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + let currency: Option> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; let total: Option = row.try_get("total").or_else(|e| match e { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), @@ -976,6 +987,7 @@ impl<'a> FromRow<'a, PgRow> for super::disputes::metrics::DisputeMetricRow { dispute_stage, dispute_status, connector, + currency, total, count, start_bucket, diff --git a/crates/api_models/src/analytics/disputes.rs b/crates/api_models/src/analytics/disputes.rs index e373704b87c..179edee0413 100644 --- a/crates/api_models/src/analytics/disputes.rs +++ b/crates/api_models/src/analytics/disputes.rs @@ -4,7 +4,7 @@ use std::{ }; use super::{ForexMetric, NameDescription, TimeRange}; -use crate::enums::DisputeStage; +use crate::enums::{Currency, DisputeStage}; #[derive( Clone, @@ -58,6 +58,7 @@ pub enum DisputeDimensions { // Consult the Dashboard FE folks since these also affects the order of metrics on FE Connector, DisputeStage, + Currency, } impl From for NameDescription { @@ -82,13 +83,17 @@ impl From for NameDescription { pub struct DisputeFilters { #[serde(default)] pub dispute_stage: Vec, + #[serde(default)] pub connector: Vec, + #[serde(default)] + pub currency: Vec, } #[derive(Debug, serde::Serialize, Eq)] pub struct DisputeMetricsBucketIdentifier { pub dispute_stage: Option, pub connector: Option, + pub currency: Option, #[serde(rename = "time_range")] pub time_bucket: TimeRange, #[serde(rename = "time_bucket")] @@ -100,6 +105,7 @@ impl Hash for DisputeMetricsBucketIdentifier { fn hash(&self, state: &mut H) { self.dispute_stage.hash(state); self.connector.hash(state); + self.currency.hash(state); self.time_bucket.hash(state); } } @@ -117,11 +123,13 @@ impl DisputeMetricsBucketIdentifier { pub fn new( dispute_stage: Option, connector: Option, + currency: Option, normalized_time_range: TimeRange, ) -> Self { Self { dispute_stage, connector, + currency, time_bucket: normalized_time_range, start_time: normalized_time_range.start_time, } From 91626c0c2554126a37f2624d3b0e2b2b60be3849 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:05:36 +0530 Subject: [PATCH 20/46] build(deps): bump `openssl` from 0.10.66 to 0.10.70 (#7187) --- Cargo.lock | 8 ++++---- crates/router/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e2ab6eccd2..4737fff7aa2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5343,9 +5343,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" dependencies = [ "bitflags 2.6.0", "cfg-if 1.0.0", @@ -5375,9 +5375,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" dependencies = [ "cc", "libc", diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 7db0273d2e1..6257d3e0832 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -84,7 +84,7 @@ num_cpus = "1.16.0" num-traits = "0.2.19" once_cell = "1.19.0" openidconnect = "3.5.0" # TODO: remove reqwest -openssl = "0.10.64" +openssl = "0.10.70" quick-xml = { version = "0.31.0", features = ["serialize"] } rand = "0.8.5" rand_chacha = "0.3.1" From f71cc96a33ee3a9babb334c068dce7fbb3063e25 Mon Sep 17 00:00:00 2001 From: Debarshi Gupta Date: Wed, 5 Feb 2025 19:06:31 +0530 Subject: [PATCH 21/46] fix(connector): [Deutschebank] Display deutschebank card payment method in dashboard (#7060) Co-authored-by: Debarshi Gupta --- .../connector_configs/toml/development.toml | 8 ++++++++ crates/connector_configs/toml/production.toml | 8 ++++++++ crates/connector_configs/toml/sandbox.toml | 8 ++++++++ .../src/connectors/deutschebank.rs | 19 +++++++++++++++++++ 4 files changed, 43 insertions(+) diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 2ab348d5dd5..190114fee3a 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -1408,6 +1408,14 @@ api_secret="Shared Secret" [deutschebank] [[deutschebank.bank_debit]] payment_method_type = "sepa" +[[deutschebank.credit]] + payment_method_type = "Visa" +[[deutschebank.credit]] + payment_method_type = "Mastercard" +[[deutschebank.debit]] + payment_method_type = "Visa" +[[deutschebank.debit]] + payment_method_type = "Mastercard" [deutschebank.connector_auth.SignatureKey] api_key="Client ID" key1="Merchant ID" diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 5b963e07801..f1745587a6e 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -1143,6 +1143,14 @@ type="Text" [deutschebank] [[deutschebank.bank_debit]] payment_method_type = "sepa" +[[deutschebank.credit]] + payment_method_type = "Visa" +[[deutschebank.credit]] + payment_method_type = "Mastercard" +[[deutschebank.debit]] + payment_method_type = "Visa" +[[deutschebank.debit]] + payment_method_type = "Mastercard" [deutschebank.connector_auth.SignatureKey] api_key="Client ID" key1="Merchant ID" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 50c13dc8596..2a83d62ee9c 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -1357,6 +1357,14 @@ api_secret="Shared Secret" [deutschebank] [[deutschebank.bank_debit]] payment_method_type = "sepa" +[[deutschebank.credit]] + payment_method_type = "Visa" +[[deutschebank.credit]] + payment_method_type = "Mastercard" +[[deutschebank.debit]] + payment_method_type = "Visa" +[[deutschebank.debit]] + payment_method_type = "Mastercard" [deutschebank.connector_auth.SignatureKey] api_key="Client ID" key1="Merchant ID" diff --git a/crates/hyperswitch_connectors/src/connectors/deutschebank.rs b/crates/hyperswitch_connectors/src/connectors/deutschebank.rs index f98d6843be7..d7dcb63c476 100644 --- a/crates/hyperswitch_connectors/src/connectors/deutschebank.rs +++ b/crates/hyperswitch_connectors/src/connectors/deutschebank.rs @@ -1016,6 +1016,25 @@ lazy_static! { } ); + deutschebank_supported_payment_methods.add( + enums::PaymentMethod::Card, + enums::PaymentMethodType::Debit, + PaymentMethodDetails{ + mandates: enums::FeatureStatus::NotSupported, + refunds: enums::FeatureStatus::Supported, + supported_capture_methods: supported_capture_methods.clone(), + specific_features: Some( + api_models::feature_matrix::PaymentMethodSpecificFeatures::Card({ + api_models::feature_matrix::CardSpecificFeatures { + three_ds: common_enums::FeatureStatus::Supported, + non_three_ds: common_enums::FeatureStatus::NotSupported, + supported_card_networks: supported_card_network.clone(), + } + }), + ), + } + ); + deutschebank_supported_payment_methods }; From ea1888677df7de60a248184389d7be30ae21fc59 Mon Sep 17 00:00:00 2001 From: Debarshi Gupta Date: Wed, 5 Feb 2025 19:07:08 +0530 Subject: [PATCH 22/46] refactor(connector): [AUTHORIZEDOTNET] Add metadata information to connector request (#7011) Co-authored-by: Debarshi Gupta Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../connector/authorizedotnet/transformers.rs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index d2b212e3ea0..8e0af603a3d 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use common_utils::{ errors::CustomResult, ext_traits::{Encode, ValueExt}, @@ -6,6 +8,7 @@ use error_stack::ResultExt; use masking::{ExposeInterface, PeekInterface, Secret, StrongSecret}; use rand::distributions::{Alphanumeric, DistString}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use crate::{ connector::utils::{ @@ -143,12 +146,27 @@ struct TransactionRequest { #[serde(skip_serializing_if = "Option::is_none")] bill_to: Option, #[serde(skip_serializing_if = "Option::is_none")] + user_fields: Option, + #[serde(skip_serializing_if = "Option::is_none")] processing_options: Option, #[serde(skip_serializing_if = "Option::is_none")] subsequent_auth_information: Option, authorization_indicator_type: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UserFields { + user_field: Vec, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UserField { + name: String, + value: String, +} + #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] enum ProfileDetails { @@ -299,6 +317,25 @@ pub enum ValidationMode { LiveMode, } +impl ForeignTryFrom for Vec { + type Error = error_stack::Report; + fn foreign_try_from(metadata: Value) -> Result { + let hashmap: BTreeMap = serde_json::from_str(&metadata.to_string()) + .change_context(errors::ConnectorError::RequestEncodingFailedWithReason( + "Failed to serialize request metadata".to_owned(), + )) + .attach_printable("")?; + let mut vector: Self = Self::new(); + for (key, value) in hashmap { + vector.push(UserField { + name: key, + value: value.to_string(), + }); + } + Ok(vector) + } +} + impl TryFrom<&types::SetupMandateRouterData> for CreateCustomerProfileRequest { type Error = error_stack::Report; fn try_from(item: &types::SetupMandateRouterData) -> Result { @@ -622,6 +659,12 @@ impl zip: address.zip.clone(), country: address.country, }), + user_fields: match item.router_data.request.metadata.clone() { + Some(metadata) => Some(UserFields { + user_field: Vec::::foreign_try_from(metadata)?, + }), + None => None, + }, processing_options: Some(ProcessingOptions { is_subsequent_auth: true, }), @@ -675,6 +718,12 @@ impl }, customer: None, bill_to: None, + user_fields: match item.router_data.request.metadata.clone() { + Some(metadata) => Some(UserFields { + user_field: Vec::::foreign_try_from(metadata)?, + }), + None => None, + }, processing_options: Some(ProcessingOptions { is_subsequent_auth: true, }), @@ -764,6 +813,12 @@ impl zip: address.zip.clone(), country: address.country, }), + user_fields: match item.router_data.request.metadata.clone() { + Some(metadata) => Some(UserFields { + user_field: Vec::::foreign_try_from(metadata)?, + }), + None => None, + }, processing_options: None, subsequent_auth_information: None, authorization_indicator_type: match item.router_data.request.capture_method { @@ -815,6 +870,12 @@ impl zip: address.zip.clone(), country: address.country, }), + user_fields: match item.router_data.request.metadata.clone() { + Some(metadata) => Some(UserFields { + user_field: Vec::::foreign_try_from(metadata)?, + }), + None => None, + }, processing_options: None, subsequent_auth_information: None, authorization_indicator_type: match item.router_data.request.capture_method { From 67ea754e383d2f9539d16f7fa40f201f177b5ea3 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:07:11 +0530 Subject: [PATCH 23/46] feat(payments_v2): implement create and confirm intent flow (#7106) --- .../payments--create-and-confirm-intent.mdx | 3 + api-reference-v2/mint.json | 3 +- api-reference-v2/openapi_spec.json | 1727 +++++------------ crates/api_models/src/events/payment.rs | 38 +- crates/api_models/src/payments.rs | 280 +++ crates/diesel_models/src/payment_attempt.rs | 25 +- crates/diesel_models/src/schema_v2.rs | 2 +- .../hyperswitch_domain_models/src/consts.rs | 3 + .../src/payments/payment_attempt.rs | 112 +- .../src/router_data.rs | 265 ++- .../src/router_response_types.rs | 26 + crates/openapi/src/openapi_v2.rs | 5 +- crates/openapi/src/routes/payment_method.rs | 2 +- crates/openapi/src/routes/payments.rs | 58 +- crates/router/src/consts.rs | 5 +- crates/router/src/core/payments.rs | 186 ++ .../core/payments/flows/setup_mandate_flow.rs | 39 +- crates/router/src/core/payments/helpers.rs | 1 + .../payments/operations/payment_confirm.rs | 2 +- .../payments/operations/payment_create.rs | 2 +- .../payments/operations/payment_response.rs | 89 + .../router/src/core/payments/transformers.rs | 213 +- crates/router/src/core/utils.rs | 1 + crates/router/src/routes/app.rs | 12 +- crates/router/src/routes/lock_utils.rs | 1 + crates/router/src/routes/payments.rs | 51 + crates/router/src/services/api.rs | 1 - crates/router/src/types.rs | 1 + crates/router/src/types/api/payments.rs | 17 +- crates/router_env/src/logger/types.rs | 6 +- .../2024-08-28-081721_add_v2_columns/down.sql | 3 +- .../2024-08-28-081721_add_v2_columns/up.sql | 3 +- .../down.sql | 3 +- .../up.sql | 3 +- 34 files changed, 1866 insertions(+), 1322 deletions(-) create mode 100644 api-reference-v2/api-reference/payments/payments--create-and-confirm-intent.mdx rename v2_migrations/{2024-10-08-081847_drop_v1_columns => 2024-11-08-081847_drop_v1_columns}/down.sql (97%) rename v2_migrations/{2024-10-08-081847_drop_v1_columns => 2024-11-08-081847_drop_v1_columns}/up.sql (97%) diff --git a/api-reference-v2/api-reference/payments/payments--create-and-confirm-intent.mdx b/api-reference-v2/api-reference/payments/payments--create-and-confirm-intent.mdx new file mode 100644 index 00000000000..1259bd33dcd --- /dev/null +++ b/api-reference-v2/api-reference/payments/payments--create-and-confirm-intent.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /v2/payments +--- diff --git a/api-reference-v2/mint.json b/api-reference-v2/mint.json index d47cf7aee6d..11e716d6303 100644 --- a/api-reference-v2/mint.json +++ b/api-reference-v2/mint.json @@ -42,7 +42,8 @@ "api-reference/payments/payments--session-token", "api-reference/payments/payments--payment-methods-list", "api-reference/payments/payments--confirm-intent", - "api-reference/payments/payments--get" + "api-reference/payments/payments--get", + "api-reference/payments/payments--create-and-confirm-intent" ] }, { diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index bc593adb45b..ebb93fd8a11 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -1886,9 +1886,7 @@ "schema": { "type": "string" }, - "example": { - "X-Profile-Id": "pro_abcdefghijklmnop" - } + "example": "pro_abcdefghijklmnop" } ], "requestBody": { @@ -1959,9 +1957,7 @@ "schema": { "type": "string" }, - "example": { - "X-Profile-Id": "pro_abcdefghijklmnop" - } + "example": "pro_abcdefghijklmnop" }, { "name": "X-Client-Secret", @@ -1994,6 +1990,7 @@ "card_number": "4242424242424242" } }, + "payment_method_subtype": "credit", "payment_method_type": "card" } } @@ -2074,6 +2071,79 @@ ] } }, + "/v2/payments": { + "post": { + "tags": [ + "Payments" + ], + "summary": "Payments - Create and Confirm Intent", + "description": "**Creates and confirms a payment intent object when the amount and payment method information are passed.**\n\nYou will require the 'API - Key' from the Hyperswitch dashboard to make the call.", + "operationId": "Create and Confirm Payment Intent", + "parameters": [ + { + "name": "X-Profile-Id", + "in": "header", + "description": "Profile ID associated to the payment intent", + "required": true, + "schema": { + "type": "string" + }, + "example": "pro_abcdefghijklmnop" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsRequest" + }, + "examples": { + "Create and confirm the payment intent with amount and card details": { + "value": { + "amount_details": { + "currency": "USD", + "order_amount": 6540 + }, + "payment_method_data": { + "card": { + "card_cvc": "123", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_number": "4242424242424242" + } + }, + "payment_method_subtype": "credit", + "payment_method_type": "card" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Payment created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaymentsResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/v2/payments/{payment_id}/create-external-sdk-tokens": { "post": { "tags": [ @@ -2151,9 +2221,7 @@ "schema": { "type": "string" }, - "example": { - "X-Profile-Id": "pro_abcdefghijklmnop" - } + "example": "pro_abcdefghijklmnop" }, { "name": "X-Client-Secret", @@ -2296,9 +2364,7 @@ "schema": { "type": "string" }, - "example": { - "X-Profile-Id": "pro_abcdefghijklmnop" - } + "example": "pro_abcdefghijklmnop" } ], "responses": { @@ -6819,6 +6885,20 @@ "active" ] }, + "ConnectorTokenDetails": { + "type": "object", + "description": "Token information that can be used to initiate transactions by the merchant.", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string", + "description": "A token that can be used to make payments directly with the connector.", + "example": "pm_9UhMqBMEOooRIvJFFdeW" + } + } + }, "ConnectorType": { "type": "string", "description": "Type of the Connector for the financial use case. Could range from Payments to Accounting to Banking.", @@ -13198,26 +13278,6 @@ }, "additionalProperties": false }, - "PaymentListResponse": { - "type": "object", - "required": [ - "size", - "data" - ], - "properties": { - "size": { - "type": "integer", - "description": "The number of payments included in the list", - "minimum": 0 - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentsResponse" - } - } - } - }, "PaymentMethod": { "type": "string", "description": "Indicates the type of payment method. Eg: 'card', 'wallet', etc.", @@ -14633,6 +14693,14 @@ "example": "993672945374576J", "nullable": true }, + "connector_token_details": { + "allOf": [ + { + "$ref": "#/components/schemas/ConnectorTokenDetails" + } + ], + "nullable": true + }, "merchant_connector_id": { "type": "string", "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment" @@ -14847,198 +14915,224 @@ }, "additionalProperties": false }, - "PaymentsCreateResponseOpenApi": { + "PaymentsDynamicTaxCalculationRequest": { + "type": "object", + "required": [ + "shipping", + "client_secret", + "payment_method_type" + ], + "properties": { + "shipping": { + "$ref": "#/components/schemas/Address" + }, + "client_secret": { + "type": "string", + "description": "Client Secret" + }, + "payment_method_type": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "session_id": { + "type": "string", + "description": "Session Id", + "nullable": true + } + } + }, + "PaymentsDynamicTaxCalculationResponse": { "type": "object", "required": [ "payment_id", - "merchant_id", - "status", - "amount", "net_amount", - "amount_capturable", - "currency", - "payment_method", - "attempt_count" + "display_amount" ], "properties": { "payment_id": { "type": "string", - "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", - "example": "pay_mbabizu24mvu3mela5njyhpit4", - "maxLength": 30, - "minLength": 30 + "description": "The identifier for the payment" }, - "merchant_id": { - "type": "string", - "description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request", - "example": "merchant_1668273825", - "maxLength": 255 + "net_amount": { + "$ref": "#/components/schemas/MinorUnit" }, - "status": { + "order_tax_amount": { "allOf": [ { - "$ref": "#/components/schemas/IntentStatus" + "$ref": "#/components/schemas/MinorUnit" } ], - "default": "requires_confirmation" - }, - "amount": { - "type": "integer", - "format": "int64", - "description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,", - "example": 6540 - }, - "net_amount": { - "type": "integer", - "format": "int64", - "description": "The payment net amount. net_amount = amount + surcharge_details.surcharge_amount + surcharge_details.tax_amount + shipping_cost + order_tax_amount,\nIf no surcharge_details, shipping_cost, order_tax_amount, net_amount = amount", - "example": 6540 + "nullable": true }, "shipping_cost": { - "type": "integer", - "format": "int64", - "description": "The shipping cost for the payment.", - "example": 6540, + "allOf": [ + { + "$ref": "#/components/schemas/MinorUnit" + } + ], "nullable": true }, - "amount_capturable": { - "type": "integer", - "format": "int64", - "description": "The maximum amount that could be captured from the payment", - "example": 6540, - "minimum": 100 + "display_amount": { + "$ref": "#/components/schemas/DisplayAmountOnSdk" + } + } + }, + "PaymentsExternalAuthenticationRequest": { + "type": "object", + "required": [ + "client_secret", + "device_channel", + "threeds_method_comp_ind" + ], + "properties": { + "client_secret": { + "type": "string", + "description": "Client Secret" }, - "amount_received": { - "type": "integer", - "format": "int64", - "description": "The amount which is already captured from the payment, this helps in the cases where merchants can't capture all capturable amount at once.", - "example": 6540, + "sdk_information": { + "allOf": [ + { + "$ref": "#/components/schemas/SdkInformation" + } + ], "nullable": true }, - "connector": { + "device_channel": { + "$ref": "#/components/schemas/DeviceChannel" + }, + "threeds_method_comp_ind": { + "$ref": "#/components/schemas/ThreeDsCompletionIndicator" + } + } + }, + "PaymentsExternalAuthenticationResponse": { + "type": "object", + "required": [ + "trans_status", + "three_ds_requestor_url" + ], + "properties": { + "trans_status": { + "$ref": "#/components/schemas/TransactionStatus" + }, + "acs_url": { "type": "string", - "description": "The connector used for the payment", - "example": "stripe", + "description": "Access Server URL to be used for challenge submission", "nullable": true }, - "client_secret": { + "challenge_request": { "type": "string", - "description": "It's a token used for client side verification.", - "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo", + "description": "Challenge request which should be sent to acs_url", "nullable": true }, - "created": { + "acs_reference_number": { "type": "string", - "format": "date-time", - "description": "Time when the payment was created", - "example": "2022-09-10T10:11:12Z", + "description": "Unique identifier assigned by the EMVCo(Europay, Mastercard and Visa)", "nullable": true }, - "currency": { - "$ref": "#/components/schemas/Currency" + "acs_trans_id": { + "type": "string", + "description": "Unique identifier assigned by the ACS to identify a single transaction", + "nullable": true }, - "customer_id": { + "three_dsserver_trans_id": { "type": "string", - "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.\nThis field will be deprecated soon. Please refer to `customer.id`", - "deprecated": true, - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "nullable": true, - "maxLength": 64, - "minLength": 1 + "description": "Unique identifier assigned by the 3DS Server to identify a single transaction", + "nullable": true }, - "description": { + "acs_signed_content": { "type": "string", - "description": "A description of the payment", - "example": "It's my first payment request", + "description": "Contains the JWS object created by the ACS for the ARes(Authentication Response) message", "nullable": true }, - "refunds": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RefundResponse" - }, - "description": "List of refunds that happened on this intent, as same payment intent can have multiple refund requests depending on the nature of order", - "nullable": true + "three_ds_requestor_url": { + "type": "string", + "description": "Three DS Requestor URL" + } + } + }, + "PaymentsIncrementalAuthorizationRequest": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "type": "integer", + "format": "int64", + "description": "The total amount including previously authorized amount and additional amount", + "example": 6540 }, - "disputes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DisputeResponsePaymentsRetrieve" - }, - "description": "List of disputes that happened on this intent", + "reason": { + "type": "string", + "description": "Reason for incremental authorization", "nullable": true + } + } + }, + "PaymentsIntentResponse": { + "type": "object", + "required": [ + "id", + "status", + "amount_details", + "client_secret", + "profile_id", + "capture_method", + "authentication_type", + "customer_id", + "customer_present", + "setup_future_usage", + "apply_mit_exemption", + "payment_link_enabled", + "request_incremental_authorization", + "expires_on", + "request_external_three_ds_authentication" + ], + "properties": { + "id": { + "type": "string", + "description": "Global Payment Id for the payment" }, - "attempts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentAttemptResponse" - }, - "description": "List of attempts that happened on this intent", - "nullable": true + "status": { + "$ref": "#/components/schemas/IntentStatus" }, - "captures": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaptureResponse" - }, - "description": "List of captures done on latest attempt", - "nullable": true + "amount_details": { + "$ref": "#/components/schemas/AmountDetailsResponse" }, - "mandate_id": { + "client_secret": { "type": "string", - "description": "A unique identifier to link the payment to a mandate, can be used instead of payment_method_data, in case of setting up recurring payments", - "example": "mandate_iwer89rnjef349dni3", - "nullable": true, - "maxLength": 255 + "description": "It's a token used for client side verification.", + "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo" }, - "mandate_data": { - "allOf": [ - { - "$ref": "#/components/schemas/MandateData" - } - ], - "nullable": true + "profile_id": { + "type": "string", + "description": "The identifier for the profile. This is inferred from the `x-profile-id` header" }, - "setup_future_usage": { - "allOf": [ - { - "$ref": "#/components/schemas/FutureUsage" - } - ], - "nullable": true + "merchant_reference_id": { + "type": "string", + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", + "example": "pay_mbabizu24mvu3mela5njyhpit4", + "nullable": true, + "maxLength": 30, + "minLength": 30 }, - "off_session": { - "type": "boolean", - "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with confirm=true.", - "example": true, + "routing_algorithm_id": { + "type": "string", + "description": "The routing algorithm id to be used for the payment", "nullable": true }, "capture_method": { - "allOf": [ - { - "$ref": "#/components/schemas/CaptureMethod" - } - ], - "nullable": true - }, - "payment_method": { - "$ref": "#/components/schemas/PaymentMethod" + "$ref": "#/components/schemas/CaptureMethod" }, - "payment_method_data": { + "authentication_type": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodDataResponseWithBilling" + "$ref": "#/components/schemas/AuthenticationType" } ], - "nullable": true - }, - "payment_token": { - "type": "string", - "description": "Provide a reference to a stored payment method", - "example": "187282ab-40ef-47a9-9206-5099ba31e432", - "nullable": true + "default": "no_three_ds" }, - "shipping": { + "billing": { "allOf": [ { "$ref": "#/components/schemas/Address" @@ -15046,7 +15140,7 @@ ], "nullable": true }, - "billing": { + "shipping": { "allOf": [ { "$ref": "#/components/schemas/Address" @@ -15054,1071 +15148,238 @@ ], "nullable": true }, - "order_details": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrderDetailsWithAmount" - }, - "description": "Information about the product , quantity and amount for connectors. (e.g. Klarna)", - "example": "[{\n \"product_name\": \"gillete creme\",\n \"quantity\": 15,\n \"amount\" : 900\n }]", - "nullable": true - }, - "email": { + "customer_id": { "type": "string", - "description": "description: The customer's email address\nThis field will be deprecated soon. Please refer to `customer.email` object", - "deprecated": true, - "example": "johntest@test.com", - "nullable": true, - "maxLength": 255 + "description": "The identifier for the customer", + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", + "maxLength": 64, + "minLength": 32 }, - "name": { - "type": "string", - "description": "description: The customer's name\nThis field will be deprecated soon. Please refer to `customer.name` object", - "deprecated": true, - "example": "John Test", - "nullable": true, - "maxLength": 255 + "customer_present": { + "$ref": "#/components/schemas/PresenceOfCustomerDuringPayment" }, - "phone": { + "description": { "type": "string", - "description": "The customer's phone number\nThis field will be deprecated soon. Please refer to `customer.phone` object", - "deprecated": true, - "example": "9123456789", - "nullable": true, - "maxLength": 255 + "description": "A description for the payment", + "example": "It's my first payment request", + "nullable": true }, "return_url": { "type": "string", - "description": "The URL to redirect after the completion of the operation", + "description": "The URL to which you want the user to be redirected after the completion of the payment operation", "example": "https://hyperswitch.io", "nullable": true }, - "authentication_type": { - "allOf": [ - { - "$ref": "#/components/schemas/AuthenticationType" - } - ], - "default": "three_ds", - "nullable": true + "setup_future_usage": { + "$ref": "#/components/schemas/FutureUsage" + }, + "apply_mit_exemption": { + "$ref": "#/components/schemas/MitExemptionRequest" }, - "statement_descriptor_name": { + "statement_descriptor": { "type": "string", "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", "example": "Hyperswitch Router", "nullable": true, - "maxLength": 255 - }, - "statement_descriptor_suffix": { - "type": "string", - "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 255 characters for the concatenated descriptor.", - "example": "Payment for shoes purchase", - "nullable": true, - "maxLength": 255 - }, - "next_action": { - "allOf": [ - { - "$ref": "#/components/schemas/NextActionData" - } - ], - "nullable": true + "maxLength": 22 }, - "cancellation_reason": { - "type": "string", - "description": "If the payment was cancelled the reason will be provided here", + "order_details": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderDetailsWithAmount" + }, + "description": "Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount", + "example": "[{\n \"product_name\": \"Apple iPhone 16\",\n \"quantity\": 1,\n \"amount\" : 69000\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", "nullable": true }, - "error_code": { - "type": "string", - "description": "If there was an error while calling the connectors the code is received here", - "example": "E0001", + "allowed_payment_method_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "description": "Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent", "nullable": true }, - "error_message": { - "type": "string", - "description": "If there was an error while calling the connector the error message is received here", - "example": "Failed while verifying the card", + "metadata": { + "type": "object", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true }, - "payment_experience": { + "connector_metadata": { "allOf": [ { - "$ref": "#/components/schemas/PaymentExperience" + "$ref": "#/components/schemas/ConnectorMetadata" } ], "nullable": true }, - "payment_method_type": { + "feature_metadata": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodType" + "$ref": "#/components/schemas/FeatureMetadata" } ], "nullable": true }, - "connector_label": { - "type": "string", - "description": "The connector used for this payment along with the country and business details", - "example": "stripe_US_food", - "nullable": true + "payment_link_enabled": { + "$ref": "#/components/schemas/EnablePaymentLinkRequest" }, - "business_country": { + "payment_link_config": { "allOf": [ { - "$ref": "#/components/schemas/CountryAlpha2" + "$ref": "#/components/schemas/PaymentLinkConfigRequest" } ], "nullable": true }, - "business_label": { - "type": "string", - "description": "The business label of merchant for this payment", - "nullable": true + "request_incremental_authorization": { + "$ref": "#/components/schemas/RequestIncrementalAuthorization" }, - "business_sub_label": { + "expires_on": { "type": "string", - "description": "The business_sub_label for this payment", - "nullable": true + "format": "date-time", + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds" }, - "allowed_payment_method_types": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "description": "Allowed Payment Method Types for a given PaymentIntent", + "frm_metadata": { + "type": "object", + "description": "Additional data related to some frm(Fraud Risk Management) connectors", "nullable": true }, - "ephemeral_key": { - "allOf": [ - { - "$ref": "#/components/schemas/EphemeralKeyCreateResponse" - } - ], - "nullable": true - }, - "manual_retry_allowed": { - "type": "boolean", - "description": "If true the payment can be retried with same or different payment method which means the confirm call can be made again.", - "nullable": true - }, - "connector_transaction_id": { - "type": "string", - "description": "A unique identifier for a payment provided by the connector", - "example": "993672945374576J", - "nullable": true - }, - "frm_message": { - "allOf": [ - { - "$ref": "#/components/schemas/FrmMessage" - } - ], - "nullable": true - }, - "metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", - "nullable": true - }, - "connector_metadata": { - "allOf": [ - { - "$ref": "#/components/schemas/ConnectorMetadata" - } - ], - "nullable": true - }, - "feature_metadata": { - "allOf": [ - { - "$ref": "#/components/schemas/FeatureMetadata" - } - ], - "nullable": true - }, - "reference_id": { - "type": "string", - "description": "reference(Identifier) to the payment at connector side", - "example": "993672945374576J", - "nullable": true - }, - "payment_link": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentLinkResponse" - } - ], - "nullable": true - }, - "profile_id": { - "type": "string", - "description": "The business profile that is associated with this payment", - "nullable": true - }, - "surcharge_details": { - "allOf": [ - { - "$ref": "#/components/schemas/RequestSurchargeDetails" - } - ], - "nullable": true - }, - "attempt_count": { - "type": "integer", - "format": "int32", - "description": "Total number of attempts associated with this payment" - }, - "merchant_decision": { - "type": "string", - "description": "Denotes the action(approve or reject) taken by merchant in case of manual review. Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment", - "nullable": true - }, - "merchant_connector_id": { - "type": "string", - "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", - "nullable": true - }, - "incremental_authorization_allowed": { - "type": "boolean", - "description": "If true, incremental authorization can be performed on this payment, in case the funds authorized initially fall short.", - "nullable": true - }, - "authorization_count": { - "type": "integer", - "format": "int32", - "description": "Total number of authorizations happened in an incremental_authorization payment", - "nullable": true - }, - "incremental_authorizations": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IncrementalAuthorizationResponse" - }, - "description": "List of incremental authorizations happened to the payment", - "nullable": true - }, - "external_authentication_details": { - "allOf": [ - { - "$ref": "#/components/schemas/ExternalAuthenticationDetailsResponse" - } - ], - "nullable": true - }, - "external_3ds_authentication_attempted": { - "type": "boolean", - "description": "Flag indicating if external 3ds authentication is made or not", - "nullable": true - }, - "expires_on": { - "type": "string", - "format": "date-time", - "description": "Date Time for expiry of the payment", - "example": "2022-09-10T10:11:12Z", - "nullable": true - }, - "fingerprint": { - "type": "string", - "description": "Payment Fingerprint, to identify a particular card.\nIt is a 20 character long alphanumeric code.", - "nullable": true - }, - "browser_info": { - "allOf": [ - { - "$ref": "#/components/schemas/BrowserInformation" - } - ], - "nullable": true - }, - "payment_method_id": { - "type": "string", - "description": "Identifier for Payment Method used for the payment", - "nullable": true - }, - "payment_method_status": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodStatus" - } - ], - "nullable": true - }, - "updated": { - "type": "string", - "format": "date-time", - "description": "Date time at which payment was updated", - "example": "2022-09-10T10:11:12Z", - "nullable": true - }, - "split_payments": { - "allOf": [ - { - "$ref": "#/components/schemas/SplitPaymentsResponse" - } - ], - "nullable": true - }, - "frm_metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. FRM Metadata is useful for storing additional, structured information on an object related to FRM.", - "nullable": true - }, - "merchant_order_reference_id": { - "type": "string", - "description": "Merchant's identifier for the payment/invoice. This will be sent to the connector\nif the connector provides support to accept multiple reference ids.\nIn case the connector supports only one reference id, Hyperswitch's Payment ID will be sent as reference.", - "example": "Custom_Order_id_123", - "nullable": true, - "maxLength": 255 - }, - "order_tax_amount": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "connector_mandate_id": { - "type": "string", - "description": "Connector Identifier for the payment method", - "nullable": true - } - } - }, - "PaymentsDynamicTaxCalculationRequest": { - "type": "object", - "required": [ - "shipping", - "client_secret", - "payment_method_type" - ], - "properties": { - "shipping": { - "$ref": "#/components/schemas/Address" - }, - "client_secret": { - "type": "string", - "description": "Client Secret" - }, - "payment_method_type": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "session_id": { - "type": "string", - "description": "Session Id", - "nullable": true - } - } - }, - "PaymentsDynamicTaxCalculationResponse": { - "type": "object", - "required": [ - "payment_id", - "net_amount", - "display_amount" - ], - "properties": { - "payment_id": { - "type": "string", - "description": "The identifier for the payment" - }, - "net_amount": { - "$ref": "#/components/schemas/MinorUnit" - }, - "order_tax_amount": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "shipping_cost": { - "allOf": [ - { - "$ref": "#/components/schemas/MinorUnit" - } - ], - "nullable": true - }, - "display_amount": { - "$ref": "#/components/schemas/DisplayAmountOnSdk" - } - } - }, - "PaymentsExternalAuthenticationRequest": { - "type": "object", - "required": [ - "client_secret", - "device_channel", - "threeds_method_comp_ind" - ], - "properties": { - "client_secret": { - "type": "string", - "description": "Client Secret" - }, - "sdk_information": { - "allOf": [ - { - "$ref": "#/components/schemas/SdkInformation" - } - ], - "nullable": true - }, - "device_channel": { - "$ref": "#/components/schemas/DeviceChannel" - }, - "threeds_method_comp_ind": { - "$ref": "#/components/schemas/ThreeDsCompletionIndicator" + "request_external_three_ds_authentication": { + "$ref": "#/components/schemas/External3dsAuthenticationRequest" } - } + }, + "additionalProperties": false }, - "PaymentsExternalAuthenticationResponse": { + "PaymentsRequest": { "type": "object", "required": [ - "trans_status", - "three_ds_requestor_url" + "amount_details", + "customer_id", + "payment_method_data", + "payment_method_type", + "payment_method_subtype" ], "properties": { - "trans_status": { - "$ref": "#/components/schemas/TransactionStatus" - }, - "acs_url": { - "type": "string", - "description": "Access Server URL to be used for challenge submission", - "nullable": true - }, - "challenge_request": { - "type": "string", - "description": "Challenge request which should be sent to acs_url", - "nullable": true - }, - "acs_reference_number": { - "type": "string", - "description": "Unique identifier assigned by the EMVCo(Europay, Mastercard and Visa)", - "nullable": true - }, - "acs_trans_id": { - "type": "string", - "description": "Unique identifier assigned by the ACS to identify a single transaction", - "nullable": true - }, - "three_dsserver_trans_id": { - "type": "string", - "description": "Unique identifier assigned by the 3DS Server to identify a single transaction", - "nullable": true - }, - "acs_signed_content": { - "type": "string", - "description": "Contains the JWS object created by the ACS for the ARes(Authentication Response) message", - "nullable": true - }, - "three_ds_requestor_url": { - "type": "string", - "description": "Three DS Requestor URL" - } - } - }, - "PaymentsIncrementalAuthorizationRequest": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "type": "integer", - "format": "int64", - "description": "The total amount including previously authorized amount and additional amount", - "example": 6540 - }, - "reason": { - "type": "string", - "description": "Reason for incremental authorization", - "nullable": true - } - } - }, - "PaymentsIntentResponse": { - "type": "object", - "required": [ - "id", - "status", - "amount_details", - "client_secret", - "profile_id", - "capture_method", - "authentication_type", - "customer_id", - "customer_present", - "setup_future_usage", - "apply_mit_exemption", - "payment_link_enabled", - "request_incremental_authorization", - "expires_on", - "request_external_three_ds_authentication" - ], - "properties": { - "id": { - "type": "string", - "description": "Global Payment Id for the payment" - }, - "status": { - "$ref": "#/components/schemas/IntentStatus" - }, - "amount_details": { - "$ref": "#/components/schemas/AmountDetailsResponse" - }, - "client_secret": { - "type": "string", - "description": "It's a token used for client side verification.", - "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo" - }, - "profile_id": { - "type": "string", - "description": "The identifier for the profile. This is inferred from the `x-profile-id` header" - }, - "merchant_reference_id": { - "type": "string", - "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", - "example": "pay_mbabizu24mvu3mela5njyhpit4", - "nullable": true, - "maxLength": 30, - "minLength": 30 - }, - "routing_algorithm_id": { - "type": "string", - "description": "The routing algorithm id to be used for the payment", - "nullable": true - }, - "capture_method": { - "$ref": "#/components/schemas/CaptureMethod" - }, - "authentication_type": { - "allOf": [ - { - "$ref": "#/components/schemas/AuthenticationType" - } - ], - "default": "no_three_ds" - }, - "billing": { - "allOf": [ - { - "$ref": "#/components/schemas/Address" - } - ], - "nullable": true - }, - "shipping": { - "allOf": [ - { - "$ref": "#/components/schemas/Address" - } - ], - "nullable": true - }, - "customer_id": { - "type": "string", - "description": "The identifier for the customer", - "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", - "maxLength": 64, - "minLength": 32 - }, - "customer_present": { - "$ref": "#/components/schemas/PresenceOfCustomerDuringPayment" - }, - "description": { - "type": "string", - "description": "A description for the payment", - "example": "It's my first payment request", - "nullable": true - }, - "return_url": { - "type": "string", - "description": "The URL to which you want the user to be redirected after the completion of the payment operation", - "example": "https://hyperswitch.io", - "nullable": true - }, - "setup_future_usage": { - "$ref": "#/components/schemas/FutureUsage" - }, - "apply_mit_exemption": { - "$ref": "#/components/schemas/MitExemptionRequest" - }, - "statement_descriptor": { - "type": "string", - "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", - "example": "Hyperswitch Router", - "nullable": true, - "maxLength": 22 - }, - "order_details": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrderDetailsWithAmount" - }, - "description": "Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount", - "example": "[{\n \"product_name\": \"Apple iPhone 16\",\n \"quantity\": 1,\n \"amount\" : 69000\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", - "nullable": true - }, - "allowed_payment_method_types": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "description": "Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent", - "nullable": true - }, - "metadata": { - "type": "object", - "description": "Metadata is useful for storing additional, unstructured information on an object.", - "nullable": true - }, - "connector_metadata": { - "allOf": [ - { - "$ref": "#/components/schemas/ConnectorMetadata" - } - ], - "nullable": true - }, - "feature_metadata": { - "allOf": [ - { - "$ref": "#/components/schemas/FeatureMetadata" - } - ], - "nullable": true - }, - "payment_link_enabled": { - "$ref": "#/components/schemas/EnablePaymentLinkRequest" - }, - "payment_link_config": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentLinkConfigRequest" - } - ], - "nullable": true - }, - "request_incremental_authorization": { - "$ref": "#/components/schemas/RequestIncrementalAuthorization" - }, - "expires_on": { - "type": "string", - "format": "date-time", - "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds" - }, - "frm_metadata": { - "type": "object", - "description": "Additional data related to some frm(Fraud Risk Management) connectors", - "nullable": true - }, - "request_external_three_ds_authentication": { - "$ref": "#/components/schemas/External3dsAuthenticationRequest" - } - }, - "additionalProperties": false - }, - "PaymentsResponse": { - "type": "object", - "required": [ - "payment_id", - "merchant_id", - "status", - "amount", - "net_amount", - "amount_capturable", - "currency", - "payment_method", - "attempt_count" - ], - "properties": { - "payment_id": { - "type": "string", - "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", - "example": "pay_mbabizu24mvu3mela5njyhpit4", - "maxLength": 30, - "minLength": 30 - }, - "merchant_id": { - "type": "string", - "description": "This is an identifier for the merchant account. This is inferred from the API key\nprovided during the request", - "example": "merchant_1668273825", - "maxLength": 255 - }, - "status": { - "allOf": [ - { - "$ref": "#/components/schemas/IntentStatus" - } - ], - "default": "requires_confirmation" - }, - "amount": { - "type": "integer", - "format": "int64", - "description": "The payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc.,", - "example": 6540 - }, - "net_amount": { - "type": "integer", - "format": "int64", - "description": "The payment net amount. net_amount = amount + surcharge_details.surcharge_amount + surcharge_details.tax_amount + shipping_cost + order_tax_amount,\nIf no surcharge_details, shipping_cost, order_tax_amount, net_amount = amount", - "example": 6540 - }, - "shipping_cost": { - "type": "integer", - "format": "int64", - "description": "The shipping cost for the payment.", - "example": 6540, - "nullable": true - }, - "amount_capturable": { - "type": "integer", - "format": "int64", - "description": "The maximum amount that could be captured from the payment", - "example": 6540, - "minimum": 100 - }, - "amount_received": { - "type": "integer", - "format": "int64", - "description": "The amount which is already captured from the payment, this helps in the cases where merchants can't capture all capturable amount at once.", - "example": 6540, - "nullable": true - }, - "connector": { - "type": "string", - "description": "The connector used for the payment", - "example": "stripe", - "nullable": true - }, - "client_secret": { - "type": "string", - "description": "It's a token used for client side verification.", - "example": "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo", - "nullable": true - }, - "created": { - "type": "string", - "format": "date-time", - "description": "Time when the payment was created", - "example": "2022-09-10T10:11:12Z", - "nullable": true - }, - "currency": { - "$ref": "#/components/schemas/Currency" - }, - "customer_id": { - "type": "string", - "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.\nThis field will be deprecated soon. Please refer to `customer.id`", - "deprecated": true, - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "nullable": true, - "maxLength": 64, - "minLength": 1 - }, - "customer": { - "allOf": [ - { - "$ref": "#/components/schemas/CustomerDetailsResponse" - } - ], - "nullable": true - }, - "description": { - "type": "string", - "description": "A description of the payment", - "example": "It's my first payment request", - "nullable": true - }, - "refunds": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RefundResponse" - }, - "description": "List of refunds that happened on this intent, as same payment intent can have multiple refund requests depending on the nature of order", - "nullable": true - }, - "disputes": { - "type": "array", - "items": { - "$ref": "#/components/schemas/DisputeResponsePaymentsRetrieve" - }, - "description": "List of disputes that happened on this intent", - "nullable": true - }, - "attempts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentAttemptResponse" - }, - "description": "List of attempts that happened on this intent", - "nullable": true - }, - "captures": { - "type": "array", - "items": { - "$ref": "#/components/schemas/CaptureResponse" - }, - "description": "List of captures done on latest attempt", - "nullable": true - }, - "mandate_id": { - "type": "string", - "description": "A unique identifier to link the payment to a mandate, can be used instead of payment_method_data, in case of setting up recurring payments", - "example": "mandate_iwer89rnjef349dni3", - "nullable": true, - "maxLength": 255 - }, - "mandate_data": { - "allOf": [ - { - "$ref": "#/components/schemas/MandateData" - } - ], - "nullable": true - }, - "setup_future_usage": { - "allOf": [ - { - "$ref": "#/components/schemas/FutureUsage" - } - ], - "nullable": true - }, - "off_session": { - "type": "boolean", - "description": "Set to true to indicate that the customer is not in your checkout flow during this payment, and therefore is unable to authenticate. This parameter is intended for scenarios where you collect card details and charge them later. This parameter can only be used with confirm=true.", - "example": true, - "nullable": true - }, - "capture_on": { - "type": "string", - "format": "date-time", - "description": "A timestamp (ISO 8601 code) that determines when the payment should be captured.\nProviding this field will automatically set `capture` to true", - "example": "2022-09-10T10:11:12Z", - "nullable": true - }, - "capture_method": { - "allOf": [ - { - "$ref": "#/components/schemas/CaptureMethod" - } - ], - "nullable": true - }, - "payment_method": { - "$ref": "#/components/schemas/PaymentMethod" - }, - "payment_method_data": { - "allOf": [ - { - "$ref": "#/components/schemas/PaymentMethodDataResponseWithBilling" - } - ], - "nullable": true - }, - "payment_token": { - "type": "string", - "description": "Provide a reference to a stored payment method", - "example": "187282ab-40ef-47a9-9206-5099ba31e432", - "nullable": true - }, - "shipping": { - "allOf": [ - { - "$ref": "#/components/schemas/Address" - } - ], - "nullable": true - }, - "billing": { - "allOf": [ - { - "$ref": "#/components/schemas/Address" - } - ], - "nullable": true - }, - "order_details": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrderDetailsWithAmount" - }, - "description": "Information about the product , quantity and amount for connectors. (e.g. Klarna)", - "example": "[{\n \"product_name\": \"gillete creme\",\n \"quantity\": 15,\n \"amount\" : 900\n }]", - "nullable": true - }, - "email": { - "type": "string", - "description": "description: The customer's email address\nThis field will be deprecated soon. Please refer to `customer.email` object", - "deprecated": true, - "example": "johntest@test.com", - "nullable": true, - "maxLength": 255 - }, - "name": { - "type": "string", - "description": "description: The customer's name\nThis field will be deprecated soon. Please refer to `customer.name` object", - "deprecated": true, - "example": "John Test", - "nullable": true, - "maxLength": 255 - }, - "phone": { - "type": "string", - "description": "The customer's phone number\nThis field will be deprecated soon. Please refer to `customer.phone` object", - "deprecated": true, - "example": "9123456789", - "nullable": true, - "maxLength": 255 - }, - "return_url": { - "type": "string", - "description": "The URL to redirect after the completion of the operation", - "example": "https://hyperswitch.io", - "nullable": true - }, - "authentication_type": { - "allOf": [ - { - "$ref": "#/components/schemas/AuthenticationType" - } - ], - "default": "three_ds", - "nullable": true - }, - "statement_descriptor_name": { - "type": "string", - "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", - "example": "Hyperswitch Router", - "nullable": true, - "maxLength": 255 - }, - "statement_descriptor_suffix": { - "type": "string", - "description": "Provides information about a card payment that customers see on their statements. Concatenated with the prefix (shortened descriptor) or statement descriptor that’s set on the account to form the complete statement descriptor. Maximum 255 characters for the concatenated descriptor.", - "example": "Payment for shoes purchase", - "nullable": true, - "maxLength": 255 - }, - "next_action": { - "allOf": [ - { - "$ref": "#/components/schemas/NextActionData" - } - ], - "nullable": true - }, - "cancellation_reason": { - "type": "string", - "description": "If the payment was cancelled the reason will be provided here", - "nullable": true - }, - "error_code": { - "type": "string", - "description": "If there was an error while calling the connectors the code is received here", - "example": "E0001", - "nullable": true - }, - "error_message": { - "type": "string", - "description": "If there was an error while calling the connector the error message is received here", - "example": "Failed while verifying the card", - "nullable": true + "amount_details": { + "$ref": "#/components/schemas/AmountDetails" }, - "unified_code": { + "merchant_reference_id": { "type": "string", - "description": "error code unified across the connectors is received here if there was an error while calling connector", - "nullable": true + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", + "example": "pay_mbabizu24mvu3mela5njyhpit4", + "nullable": true, + "maxLength": 30, + "minLength": 30 }, - "unified_message": { + "routing_algorithm_id": { "type": "string", - "description": "error message unified across the connectors is received here if there was an error while calling connector", + "description": "The routing algorithm id to be used for the payment", "nullable": true }, - "payment_experience": { + "capture_method": { "allOf": [ { - "$ref": "#/components/schemas/PaymentExperience" + "$ref": "#/components/schemas/CaptureMethod" } ], "nullable": true }, - "payment_method_type": { + "authentication_type": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodType" + "$ref": "#/components/schemas/AuthenticationType" } ], + "default": "no_three_ds", "nullable": true }, - "connector_label": { - "type": "string", - "description": "The connector used for this payment along with the country and business details", - "example": "stripe_US_food", + "billing": { + "allOf": [ + { + "$ref": "#/components/schemas/Address" + } + ], "nullable": true }, - "business_country": { + "shipping": { "allOf": [ { - "$ref": "#/components/schemas/CountryAlpha2" + "$ref": "#/components/schemas/Address" } ], "nullable": true }, - "business_label": { + "customer_id": { "type": "string", - "description": "The business label of merchant for this payment", + "description": "The identifier for the customer", + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", + "maxLength": 64, + "minLength": 32 + }, + "customer_present": { + "allOf": [ + { + "$ref": "#/components/schemas/PresenceOfCustomerDuringPayment" + } + ], "nullable": true }, - "business_sub_label": { + "description": { "type": "string", - "description": "The business_sub_label for this payment", + "description": "A description for the payment", + "example": "It's my first payment request", "nullable": true }, - "allowed_payment_method_types": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PaymentMethodType" - }, - "description": "Allowed Payment Method Types for a given PaymentIntent", + "return_url": { + "type": "string", + "description": "The URL to which you want the user to be redirected after the completion of the payment operation", + "example": "https://hyperswitch.io", "nullable": true }, - "ephemeral_key": { + "setup_future_usage": { "allOf": [ { - "$ref": "#/components/schemas/EphemeralKeyCreateResponse" + "$ref": "#/components/schemas/FutureUsage" } ], "nullable": true }, - "manual_retry_allowed": { - "type": "boolean", - "description": "If true the payment can be retried with same or different payment method which means the confirm call can be made again.", + "apply_mit_exemption": { + "allOf": [ + { + "$ref": "#/components/schemas/MitExemptionRequest" + } + ], "nullable": true }, - "connector_transaction_id": { + "statement_descriptor": { "type": "string", - "description": "A unique identifier for a payment provided by the connector", - "example": "993672945374576J", + "description": "For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters.", + "example": "Hyperswitch Router", + "nullable": true, + "maxLength": 22 + }, + "order_details": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OrderDetailsWithAmount" + }, + "description": "Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount", + "example": "[{\n \"product_name\": \"Apple iPhone 16\",\n \"quantity\": 1,\n \"amount\" : 69000\n \"product_img_link\" : \"https://dummy-img-link.com\"\n }]", "nullable": true }, - "frm_message": { - "allOf": [ - { - "$ref": "#/components/schemas/FrmMessage" - } - ], + "allowed_payment_method_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PaymentMethodType" + }, + "description": "Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent", "nullable": true }, "metadata": { "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object.", + "description": "Metadata is useful for storing additional, unstructured information on an object.", "nullable": true }, "connector_metadata": { @@ -16137,151 +15398,189 @@ ], "nullable": true }, - "reference_id": { - "type": "string", - "description": "reference(Identifier) to the payment at connector side", - "example": "993672945374576J", - "nullable": true - }, - "payment_link": { + "payment_link_enabled": { "allOf": [ { - "$ref": "#/components/schemas/PaymentLinkResponse" + "$ref": "#/components/schemas/EnablePaymentLinkRequest" } ], "nullable": true }, - "profile_id": { - "type": "string", - "description": "The business profile that is associated with this payment", + "payment_link_config": { + "allOf": [ + { + "$ref": "#/components/schemas/PaymentLinkConfigRequest" + } + ], "nullable": true }, - "surcharge_details": { + "request_incremental_authorization": { "allOf": [ { - "$ref": "#/components/schemas/RequestSurchargeDetails" + "$ref": "#/components/schemas/RequestIncrementalAuthorization" } ], "nullable": true }, - "attempt_count": { + "session_expiry": { "type": "integer", "format": "int32", - "description": "Total number of attempts associated with this payment" + "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds, if not sent it will be taken from profile config\n(900) for 15 mins", + "example": 900, + "nullable": true, + "minimum": 0 }, - "merchant_decision": { - "type": "string", - "description": "Denotes the action(approve or reject) taken by merchant in case of manual review. Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment", + "frm_metadata": { + "type": "object", + "description": "Additional data related to some frm(Fraud Risk Management) connectors", "nullable": true }, - "merchant_connector_id": { - "type": "string", - "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", + "request_external_three_ds_authentication": { + "allOf": [ + { + "$ref": "#/components/schemas/External3dsAuthenticationRequest" + } + ], "nullable": true }, - "incremental_authorization_allowed": { - "type": "boolean", - "description": "If true, incremental authorization can be performed on this payment, in case the funds authorized initially fall short.", - "nullable": true + "payment_method_data": { + "$ref": "#/components/schemas/PaymentMethodDataRequest" }, - "authorization_count": { - "type": "integer", - "format": "int32", - "description": "Total number of authorizations happened in an incremental_authorization payment", - "nullable": true + "payment_method_type": { + "$ref": "#/components/schemas/PaymentMethod" }, - "incremental_authorizations": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IncrementalAuthorizationResponse" - }, - "description": "List of incremental authorizations happened to the payment", - "nullable": true + "payment_method_subtype": { + "$ref": "#/components/schemas/PaymentMethodType" }, - "external_authentication_details": { + "customer_acceptance": { "allOf": [ { - "$ref": "#/components/schemas/ExternalAuthenticationDetailsResponse" + "$ref": "#/components/schemas/CustomerAcceptance" } ], "nullable": true }, - "external_3ds_authentication_attempted": { - "type": "boolean", - "description": "Flag indicating if external 3ds authentication is made or not", + "browser_info": { + "allOf": [ + { + "$ref": "#/components/schemas/BrowserInformation" + } + ], "nullable": true + } + }, + "additionalProperties": false + }, + "PaymentsResponse": { + "type": "object", + "required": [ + "id", + "status", + "amount", + "customer_id", + "connector", + "client_secret", + "created", + "payment_method_type", + "payment_method_subtype", + "merchant_connector_id" + ], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the payment. This ensures idempotency for multiple payments\nthat have been done by a single merchant.", + "example": "12345_pay_01926c58bc6e77c09e809964e72af8c8", + "maxLength": 64, + "minLength": 32 }, - "expires_on": { + "status": { + "$ref": "#/components/schemas/IntentStatus" + }, + "amount": { + "$ref": "#/components/schemas/PaymentAmountDetailsResponse" + }, + "customer_id": { "type": "string", - "format": "date-time", - "description": "Date Time for expiry of the payment", - "example": "2022-09-10T10:11:12Z", - "nullable": true + "description": "The identifier for the customer", + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", + "maxLength": 64, + "minLength": 32 }, - "fingerprint": { + "connector": { "type": "string", - "description": "Payment Fingerprint, to identify a particular card.\nIt is a 20 character long alphanumeric code.", - "nullable": true + "description": "The connector used for the payment", + "example": "stripe" }, - "browser_info": { + "client_secret": { + "type": "string", + "description": "It's a token used for client side verification." + }, + "created": { + "type": "string", + "format": "date-time", + "description": "Time when the payment was created", + "example": "2022-09-10T10:11:12Z" + }, + "payment_method_data": { "allOf": [ { - "$ref": "#/components/schemas/BrowserInformation" + "$ref": "#/components/schemas/PaymentMethodDataResponseWithBilling" } ], "nullable": true }, - "payment_method_id": { - "type": "string", - "description": "Identifier for Payment Method used for the payment", - "nullable": true + "payment_method_type": { + "$ref": "#/components/schemas/PaymentMethod" + }, + "payment_method_subtype": { + "$ref": "#/components/schemas/PaymentMethodType" }, - "payment_method_status": { + "next_action": { "allOf": [ { - "$ref": "#/components/schemas/PaymentMethodStatus" + "$ref": "#/components/schemas/NextActionData" } ], "nullable": true }, - "updated": { + "connector_transaction_id": { "type": "string", - "format": "date-time", - "description": "Date time at which payment was updated", - "example": "2022-09-10T10:11:12Z", + "description": "A unique identifier for a payment provided by the connector", + "example": "993672945374576J", + "nullable": true + }, + "connector_reference_id": { + "type": "string", + "description": "reference(Identifier) to the payment at connector side", + "example": "993672945374576J", "nullable": true }, - "split_payments": { + "connector_token_details": { "allOf": [ { - "$ref": "#/components/schemas/SplitPaymentsResponse" + "$ref": "#/components/schemas/ConnectorTokenDetails" } ], "nullable": true }, - "frm_metadata": { - "type": "object", - "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. FRM Metadata is useful for storing additional, structured information on an object related to FRM.", - "nullable": true - }, - "merchant_order_reference_id": { + "merchant_connector_id": { "type": "string", - "description": "Merchant's identifier for the payment/invoice. This will be sent to the connector\nif the connector provides support to accept multiple reference ids.\nIn case the connector supports only one reference id, Hyperswitch's Payment ID will be sent as reference.", - "example": "Custom_Order_id_123", - "nullable": true, - "maxLength": 255 + "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment" }, - "order_tax_amount": { + "browser_info": { "allOf": [ { - "$ref": "#/components/schemas/MinorUnit" + "$ref": "#/components/schemas/BrowserInformation" } ], "nullable": true }, - "connector_mandate_id": { - "type": "string", - "description": "Connector Identifier for the payment method", + "error": { + "allOf": [ + { + "$ref": "#/components/schemas/ErrorDetails" + } + ], "nullable": true } } diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index e6de6190b83..c365445e102 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -3,13 +3,15 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; #[cfg(feature = "v2")] use super::{ PaymentStartRedirectionRequest, PaymentsConfirmIntentResponse, PaymentsCreateIntentRequest, - PaymentsGetIntentRequest, PaymentsIntentResponse, + PaymentsGetIntentRequest, PaymentsIntentResponse, PaymentsRequest, }; #[cfg(all( any(feature = "v2", feature = "v1"), not(feature = "payment_methods_v2") ))] use crate::payment_methods::CustomerPaymentMethodsListResponse; +#[cfg(feature = "v1")] +use crate::payments::{PaymentListResponse, PaymentListResponseV2}; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] use crate::{events, payment_methods::CustomerPaymentMethodsListResponse}; use crate::{ @@ -23,14 +25,14 @@ use crate::{ payments::{ self, ExtendedCardInfoResponse, PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, PaymentListFiltersV2, - PaymentListResponse, PaymentListResponseV2, PaymentsAggregateResponse, - PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, - PaymentsCompleteAuthorizeRequest, PaymentsDynamicTaxCalculationRequest, - PaymentsDynamicTaxCalculationResponse, PaymentsExternalAuthenticationRequest, - PaymentsExternalAuthenticationResponse, PaymentsIncrementalAuthorizationRequest, - PaymentsManualUpdateRequest, PaymentsManualUpdateResponse, - PaymentsPostSessionTokensRequest, PaymentsPostSessionTokensResponse, PaymentsRejectRequest, - PaymentsResponse, PaymentsRetrieveRequest, PaymentsSessionResponse, PaymentsStartRequest, + PaymentsAggregateResponse, PaymentsApproveRequest, PaymentsCancelRequest, + PaymentsCaptureRequest, PaymentsCompleteAuthorizeRequest, + PaymentsDynamicTaxCalculationRequest, PaymentsDynamicTaxCalculationResponse, + PaymentsExternalAuthenticationRequest, PaymentsExternalAuthenticationResponse, + PaymentsIncrementalAuthorizationRequest, PaymentsManualUpdateRequest, + PaymentsManualUpdateResponse, PaymentsPostSessionTokensRequest, + PaymentsPostSessionTokensResponse, PaymentsRejectRequest, PaymentsResponse, + PaymentsRetrieveRequest, PaymentsSessionResponse, PaymentsStartRequest, RedirectionResponse, }, }; @@ -150,6 +152,22 @@ impl ApiEventMetric for PaymentsCreateIntentRequest { } } +#[cfg(feature = "v2")] +impl ApiEventMetric for PaymentsRequest { + fn get_api_event_type(&self) -> Option { + None + } +} + +#[cfg(feature = "v2")] +impl ApiEventMetric for PaymentsResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.id.clone(), + }) + } +} + #[cfg(feature = "v2")] impl ApiEventMetric for PaymentsGetIntentRequest { fn get_api_event_type(&self) -> Option { @@ -355,12 +373,14 @@ impl ApiEventMetric for PaymentListConstraints { } } +#[cfg(feature = "v1")] impl ApiEventMetric for PaymentListResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::ResourceListAPI) } } +#[cfg(feature = "v1")] impl ApiEventMetric for PaymentListResponseV2 { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::ResourceListAPI) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 3a6fe000afe..4a1fa8b408e 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -4414,6 +4414,7 @@ pub struct ReceiverDetails { amount_remaining: Option, } +#[cfg(feature = "v1")] #[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema, router_derive::PolymorphicSchema)] #[generate_schemas(PaymentsCreateResponseOpenApi)] pub struct PaymentsResponse { @@ -4787,6 +4788,271 @@ pub struct PaymentsConfirmIntentRequest { pub browser_info: Option, } +// This struct contains the union of fields in `PaymentsCreateIntentRequest` and +// `PaymentsConfirmIntentRequest` +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ToSchema)] +#[serde(deny_unknown_fields)] +#[cfg(feature = "v2")] +pub struct PaymentsRequest { + /// The amount details for the payment + pub amount_details: AmountDetails, + + /// Unique identifier for the payment. This ensures idempotency for multiple payments + /// that have been done by a single merchant. + #[schema( + value_type = Option, + min_length = 30, + max_length = 30, + example = "pay_mbabizu24mvu3mela5njyhpit4" + )] + pub merchant_reference_id: Option, + + /// The routing algorithm id to be used for the payment + #[schema(value_type = Option)] + pub routing_algorithm_id: Option, + + #[schema(value_type = Option, example = "automatic")] + pub capture_method: Option, + + #[schema(value_type = Option, example = "no_three_ds", default = "no_three_ds")] + pub authentication_type: Option, + + /// The billing details of the payment. This address will be used for invoicing. + pub billing: Option
, + + /// The shipping address for the payment + pub shipping: Option
, + + /// The identifier for the customer + #[schema( + min_length = 32, + max_length = 64, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", + value_type = String + )] + pub customer_id: Option, + + /// Set to `present` to indicate that the customer is in your checkout flow during this payment, and therefore is able to authenticate. This parameter should be `absent` when merchant's doing merchant initiated payments and customer is not present while doing the payment. + #[schema(example = "present", value_type = Option)] + pub customer_present: Option, + + /// A description for the payment + #[schema(example = "It's my first payment request", value_type = Option)] + pub description: Option, + + /// The URL to which you want the user to be redirected after the completion of the payment operation + #[schema(value_type = Option, example = "https://hyperswitch.io")] + pub return_url: Option, + + #[schema(value_type = Option, example = "off_session")] + pub setup_future_usage: Option, + + /// Apply MIT exemption for a payment + #[schema(value_type = Option)] + pub apply_mit_exemption: Option, + + /// For non-card charges, you can use this value as the complete description that appears on your customers’ statements. Must contain at least one letter, maximum 22 characters. + #[schema(max_length = 22, example = "Hyperswitch Router", value_type = Option)] + pub statement_descriptor: Option, + + /// Use this object to capture the details about the different products for which the payment is being made. The sum of amount across different products here should be equal to the overall payment amount + #[schema(value_type = Option>, example = r#"[{ + "product_name": "Apple iPhone 16", + "quantity": 1, + "amount" : 69000 + "product_img_link" : "https://dummy-img-link.com" + }]"#)] + pub order_details: Option>, + + /// Use this parameter to restrict the Payment Method Types to show for a given PaymentIntent + #[schema(value_type = Option>)] + pub allowed_payment_method_types: Option>, + + /// Metadata is useful for storing additional, unstructured information on an object. + #[schema(value_type = Option, example = r#"{ "udf1": "some-value", "udf2": "some-value" }"#)] + pub metadata: Option, + + /// Some connectors like Apple pay, Airwallex and Noon might require some additional information, find specific details in the child attributes below. + pub connector_metadata: Option, + + /// Additional data that might be required by hyperswitch based on the requested features by the merchants. + pub feature_metadata: Option, + + /// Whether to generate the payment link for this payment or not (if applicable) + #[schema(value_type = Option)] + pub payment_link_enabled: Option, + + /// Configure a custom payment link for the particular payment + #[schema(value_type = Option)] + pub payment_link_config: Option, + + ///Request an incremental authorization, i.e., increase the authorized amount on a confirmed payment before you capture it. + #[schema(value_type = Option)] + pub request_incremental_authorization: Option, + + ///Will be used to expire client secret after certain amount of time to be supplied in seconds, if not sent it will be taken from profile config + ///(900) for 15 mins + #[schema(example = 900)] + pub session_expiry: Option, + + /// Additional data related to some frm(Fraud Risk Management) connectors + #[schema(value_type = Option, example = r#"{ "coverage_request" : "fraud", "fulfillment_method" : "delivery" }"#)] + pub frm_metadata: Option, + + /// Whether to perform external authentication (if applicable) + #[schema(value_type = Option)] + pub request_external_three_ds_authentication: + Option, + + /// The payment instrument data to be used for the payment + pub payment_method_data: PaymentMethodDataRequest, + + /// The payment method type to be used for the payment. This should match with the `payment_method_data` provided + #[schema(value_type = PaymentMethod, example = "card")] + pub payment_method_type: api_enums::PaymentMethod, + + /// The payment method subtype to be used for the payment. This should match with the `payment_method_data` provided + #[schema(value_type = PaymentMethodType, example = "apple_pay")] + pub payment_method_subtype: api_enums::PaymentMethodType, + + /// This "CustomerAcceptance" object is passed during Payments-Confirm request, it enlists the type, time, and mode of acceptance properties related to an acceptance done by the customer. The customer_acceptance sub object is usually passed by the SDK or client. + #[schema(value_type = Option)] + pub customer_acceptance: Option, + + /// Additional details required by 3DS 2.0 + #[schema(value_type = Option)] + pub browser_info: Option, +} + +#[cfg(feature = "v2")] +impl From<&PaymentsRequest> for PaymentsCreateIntentRequest { + fn from(request: &PaymentsRequest) -> Self { + Self { + amount_details: request.amount_details.clone(), + merchant_reference_id: request.merchant_reference_id.clone(), + routing_algorithm_id: request.routing_algorithm_id.clone(), + capture_method: request.capture_method, + authentication_type: request.authentication_type, + billing: request.billing.clone(), + shipping: request.shipping.clone(), + customer_id: request.customer_id.clone(), + customer_present: request.customer_present.clone(), + description: request.description.clone(), + return_url: request.return_url.clone(), + setup_future_usage: request.setup_future_usage, + apply_mit_exemption: request.apply_mit_exemption.clone(), + statement_descriptor: request.statement_descriptor.clone(), + order_details: request.order_details.clone(), + allowed_payment_method_types: request.allowed_payment_method_types.clone(), + metadata: request.metadata.clone(), + connector_metadata: request.connector_metadata.clone(), + feature_metadata: request.feature_metadata.clone(), + payment_link_enabled: request.payment_link_enabled.clone(), + payment_link_config: request.payment_link_config.clone(), + request_incremental_authorization: request.request_incremental_authorization, + session_expiry: request.session_expiry, + frm_metadata: request.frm_metadata.clone(), + request_external_three_ds_authentication: request + .request_external_three_ds_authentication + .clone(), + } + } +} + +#[cfg(feature = "v2")] +impl From<&PaymentsRequest> for PaymentsConfirmIntentRequest { + fn from(request: &PaymentsRequest) -> Self { + Self { + return_url: request.return_url.clone(), + payment_method_data: request.payment_method_data.clone(), + payment_method_type: request.payment_method_type, + payment_method_subtype: request.payment_method_subtype, + shipping: request.shipping.clone(), + customer_acceptance: request.customer_acceptance.clone(), + browser_info: request.browser_info.clone(), + } + } +} + +#[cfg(feature = "v2")] +#[derive(Debug, serde::Serialize, ToSchema)] +pub struct PaymentsResponse { + /// Unique identifier for the payment. This ensures idempotency for multiple payments + /// that have been done by a single merchant. + #[schema( + min_length = 32, + max_length = 64, + example = "12345_pay_01926c58bc6e77c09e809964e72af8c8", + value_type = String, + )] + pub id: id_type::GlobalPaymentId, + + #[schema(value_type = IntentStatus, example = "success")] + pub status: api_enums::IntentStatus, + + /// Amount related information for this payment and attempt + pub amount: PaymentAmountDetailsResponse, + + /// The identifier for the customer + #[schema( + min_length = 32, + max_length = 64, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", + value_type = String + )] + pub customer_id: Option, + + /// The connector used for the payment + #[schema(example = "stripe")] + pub connector: String, + + /// It's a token used for client side verification. + #[schema(value_type = String)] + pub client_secret: common_utils::types::ClientSecret, + + /// Time when the payment was created + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created: PrimitiveDateTime, + + /// The payment method information provided for making a payment + #[schema(value_type = Option)] + #[serde(serialize_with = "serialize_payment_method_data_response")] + pub payment_method_data: Option, + + /// The payment method type for this payment attempt + #[schema(value_type = PaymentMethod, example = "wallet")] + pub payment_method_type: api_enums::PaymentMethod, + + #[schema(value_type = PaymentMethodType, example = "apple_pay")] + pub payment_method_subtype: api_enums::PaymentMethodType, + + /// Additional information required for redirection + pub next_action: Option, + + /// A unique identifier for a payment provided by the connector + #[schema(value_type = Option, example = "993672945374576J")] + pub connector_transaction_id: Option, + + /// reference(Identifier) to the payment at connector side + #[schema(value_type = Option, example = "993672945374576J")] + pub connector_reference_id: Option, + + /// Connector token information that can be used to make payments directly by the merchant. + pub connector_token_details: Option, + + /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment + #[schema(value_type = String)] + pub merchant_connector_id: id_type::MerchantConnectorAccountId, + + /// The browser information used for this payment + #[schema(value_type = Option)] + pub browser_info: Option, + + /// Error details for the payment if any + pub error: Option, +} + // Serialize is implemented because, this will be serialized in the api events. // Usually request types should not have serialize implemented. // @@ -4885,6 +5151,9 @@ pub struct PaymentsConfirmIntentResponse { #[schema(value_type = Option, example = "993672945374576J")] pub connector_reference_id: Option, + /// Connector token information that can be used to make payments directly by the merchant. + pub connector_token_details: Option, + /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment #[schema(value_type = String)] pub merchant_connector_id: id_type::MerchantConnectorAccountId, @@ -4897,6 +5166,15 @@ pub struct PaymentsConfirmIntentResponse { pub error: Option, } +/// Token information that can be used to initiate transactions by the merchant. +#[cfg(feature = "v2")] +#[derive(Debug, Serialize, ToSchema)] +pub struct ConnectorTokenDetails { + /// A token that can be used to make payments directly with the connector. + #[schema(example = "pm_9UhMqBMEOooRIvJFFdeW")] + pub token: String, +} + // TODO: have a separate response for detailed, summarized /// Response for Payment Intent Confirm #[cfg(feature = "v2")] @@ -5104,6 +5382,7 @@ pub struct PaymentListConstraints { pub created_gte: Option, } +#[cfg(feature = "v1")] #[derive(Clone, Debug, serde::Serialize, ToSchema)] pub struct PaymentListResponse { /// The number of payments included in the list @@ -5130,6 +5409,7 @@ pub struct IncrementalAuthorizationResponse { pub previously_authorized_amount: MinorUnit, } +#[cfg(feature = "v1")] #[derive(Clone, Debug, serde::Serialize)] pub struct PaymentListResponseV2 { /// The number of payments included in the list for given constraints diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index dbd867c9e55..a8fdf882e70 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -90,10 +90,10 @@ pub struct PaymentAttempt { pub payment_method_billing_address: Option, pub redirection_data: Option, pub connector_payment_data: Option, + pub connector_token_details: Option, pub id: id_type::GlobalAttemptId, pub shipping_cost: Option, pub order_tax_amount: Option, - pub connector_mandate_detail: Option, pub card_discovery: Option, } @@ -216,6 +216,26 @@ impl ConnectorTransactionIdTrait for PaymentAttempt { } } +#[cfg(feature = "v2")] +#[derive( + Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize, diesel::AsExpression, +)] +#[diesel(sql_type = diesel::sql_types::Jsonb)] +pub struct ConnectorTokenDetails { + pub connector_mandate_id: Option, + pub connector_mandate_request_reference_id: Option, +} + +#[cfg(feature = "v2")] +common_utils::impl_to_sql_from_sql_json!(ConnectorTokenDetails); + +#[cfg(feature = "v2")] +impl ConnectorTokenDetails { + pub fn get_connector_mandate_request_reference_id(&self) -> Option { + self.connector_mandate_request_reference_id.clone() + } +} + #[derive(Clone, Debug, Eq, PartialEq, Queryable, Serialize, Deserialize)] pub struct PaymentListFilters { pub connector: Vec, @@ -279,7 +299,7 @@ pub struct PaymentAttemptNew { pub payment_method_type_v2: storage_enums::PaymentMethod, pub payment_method_subtype: storage_enums::PaymentMethodType, pub id: id_type::GlobalAttemptId, - pub connector_mandate_detail: Option, + pub connector_token_details: Option, pub card_discovery: Option, } @@ -796,6 +816,7 @@ pub struct PaymentAttemptUpdateInternal { // client_version: Option, // customer_acceptance: Option, // card_network: Option, + pub connector_token_details: Option, } #[cfg(feature = "v1")] diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 07a76c13ae7..9b1c3aa7d45 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -872,11 +872,11 @@ diesel::table! { redirection_data -> Nullable, #[max_length = 512] connector_payment_data -> Nullable, + connector_token_details -> Nullable, #[max_length = 64] id -> Varchar, shipping_cost -> Nullable, order_tax_amount -> Nullable, - connector_mandate_detail -> Nullable, card_discovery -> Nullable, } } diff --git a/crates/hyperswitch_domain_models/src/consts.rs b/crates/hyperswitch_domain_models/src/consts.rs index 3808e615c0a..4a87c35b5c1 100644 --- a/crates/hyperswitch_domain_models/src/consts.rs +++ b/crates/hyperswitch_domain_models/src/consts.rs @@ -5,3 +5,6 @@ pub const API_VERSION: common_enums::ApiVersion = common_enums::ApiVersion::V1; #[cfg(all(feature = "v2", feature = "customer_v2"))] pub const API_VERSION: common_enums::ApiVersion = common_enums::ApiVersion::V2; + +/// Length of the unique reference ID generated for connector mandate requests +pub const CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH: usize = 18; diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index 74804e658a7..65455b3b193 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -33,9 +33,13 @@ use time::PrimitiveDateTime; #[cfg(all(feature = "v1", feature = "olap"))] use super::PaymentIntent; #[cfg(feature = "v2")] -use crate::type_encryption::{crypto_operation, CryptoOperation}; -#[cfg(feature = "v2")] -use crate::{address::Address, merchant_key_store::MerchantKeyStore, router_response_types}; +use crate::{ + address::Address, + consts, + merchant_key_store::MerchantKeyStore, + router_response_types, + type_encryption::{crypto_operation, CryptoOperation}, +}; use crate::{ behaviour, errors, mandates::{MandateDataType, MandateDetails}, @@ -400,8 +404,8 @@ pub struct PaymentAttempt { pub payment_method_billing_address: Option>, /// The global identifier for the payment attempt pub id: id_type::GlobalAttemptId, - /// The connector mandate details which are stored temporarily - pub connector_mandate_detail: Option, + /// Connector token information that can be used to make payments directly by the merchant. + pub connector_token_details: Option, /// Indicates the method by which a card is discovered during a payment pub card_discovery: Option, } @@ -520,7 +524,12 @@ impl PaymentAttempt { external_reference_id: None, payment_method_billing_address, error: None, - connector_mandate_detail: None, + connector_token_details: Some(diesel_models::ConnectorTokenDetails { + connector_mandate_id: None, + connector_mandate_request_reference_id: Some(common_utils::generate_id_with_len( + consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH, + )), + }), id, card_discovery: None, }) @@ -1413,6 +1422,18 @@ impl PaymentAttemptUpdate { } } +#[cfg(feature = "v2")] +#[derive(Debug, Clone, Serialize)] +pub struct ConfirmIntentResponseUpdate { + pub status: storage_enums::AttemptStatus, + pub connector_payment_id: Option, + pub updated_by: String, + pub redirection_data: Option, + pub connector_metadata: Option, + pub amount_capturable: Option, + pub connector_token_details: Option, +} + #[cfg(feature = "v2")] #[derive(Debug, Clone, Serialize)] pub enum PaymentAttemptUpdate { @@ -1424,14 +1445,7 @@ pub enum PaymentAttemptUpdate { merchant_connector_id: id_type::MerchantConnectorAccountId, }, /// Update the payment attempt on confirming the intent, after calling the connector on success response - ConfirmIntentResponse { - status: storage_enums::AttemptStatus, - connector_payment_id: Option, - updated_by: String, - redirection_data: Option, - connector_metadata: Option, - amount_capturable: Option, - }, + ConfirmIntentResponse(Box), /// Update the payment attempt after force syncing with the connector SyncUpdate { status: storage_enums::AttemptStatus, @@ -1791,7 +1805,7 @@ impl behaviour::Conversion for PaymentAttempt { payment_method_id, payment_method_billing_address, connector, - connector_mandate_detail, + connector_token_details, card_discovery, } = self; @@ -1869,7 +1883,7 @@ impl behaviour::Conversion for PaymentAttempt { tax_on_surcharge, payment_method_billing_address: payment_method_billing_address.map(Encryption::from), connector_payment_data, - connector_mandate_detail, + connector_token_details, card_discovery, }) } @@ -1981,7 +1995,7 @@ impl behaviour::Conversion for PaymentAttempt { external_reference_id: storage_model.external_reference_id, connector: storage_model.connector, payment_method_billing_address, - connector_mandate_detail: storage_model.connector_mandate_detail, + connector_token_details: storage_model.connector_token_details, card_discovery: storage_model.card_discovery, }) } @@ -2066,7 +2080,7 @@ impl behaviour::Conversion for PaymentAttempt { payment_method_subtype: self.payment_method_subtype, payment_method_type_v2: self.payment_method_type, id: self.id, - connector_mandate_detail: self.connector_mandate_detail, + connector_token_details: self.connector_token_details, card_discovery: self.card_discovery, }) } @@ -2098,6 +2112,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector_metadata: None, amount_capturable: None, amount_to_capture: None, + connector_token_details: None, }, PaymentAttemptUpdate::ErrorUpdate { status, @@ -2122,33 +2137,39 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector_metadata: None, amount_capturable, amount_to_capture: None, + connector_token_details: None, }, - PaymentAttemptUpdate::ConfirmIntentResponse { - status, - connector_payment_id, - updated_by, - redirection_data, - connector_metadata, - amount_capturable, - } => Self { - status: Some(status), - amount_capturable, - error_message: None, - error_code: None, - modified_at: common_utils::date_time::now(), - browser_info: None, - error_reason: None, - updated_by, - merchant_connector_id: None, - unified_code: None, - unified_message: None, - connector_payment_id, - connector: None, - redirection_data: redirection_data - .map(diesel_models::payment_attempt::RedirectForm::from), - connector_metadata, - amount_to_capture: None, - }, + PaymentAttemptUpdate::ConfirmIntentResponse(confirm_intent_response_update) => { + let ConfirmIntentResponseUpdate { + status, + connector_payment_id, + updated_by, + redirection_data, + connector_metadata, + amount_capturable, + connector_token_details, + } = *confirm_intent_response_update; + Self { + status: Some(status), + amount_capturable, + error_message: None, + error_code: None, + modified_at: common_utils::date_time::now(), + browser_info: None, + error_reason: None, + updated_by, + merchant_connector_id: None, + unified_code: None, + unified_message: None, + connector_payment_id, + connector: None, + redirection_data: redirection_data + .map(diesel_models::payment_attempt::RedirectForm::from), + connector_metadata, + amount_to_capture: None, + connector_token_details, + } + } PaymentAttemptUpdate::SyncUpdate { status, amount_capturable, @@ -2170,6 +2191,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal redirection_data: None, connector_metadata: None, amount_to_capture: None, + connector_token_details: None, }, PaymentAttemptUpdate::CaptureUpdate { status, @@ -2192,6 +2214,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal connector: None, redirection_data: None, connector_metadata: None, + connector_token_details: None, }, PaymentAttemptUpdate::PreCaptureUpdate { amount_to_capture, @@ -2213,6 +2236,7 @@ impl From for diesel_models::PaymentAttemptUpdateInternal status: None, connector_metadata: None, amount_capturable: None, + connector_token_details: None, }, } } diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index 322b4aa4525..1ad7d2dcbdd 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -10,6 +10,14 @@ use error_stack::ResultExt; use masking::{ExposeInterface, Secret}; use crate::{payment_address::PaymentAddress, payment_method_data, payments}; +#[cfg(feature = "v2")] +use crate::{ + payments::{ + payment_attempt::{ErrorDetails, PaymentAttemptUpdate}, + payment_intent::PaymentIntentUpdate, + }, + router_flow_types, router_request_types, router_response_types, +}; #[derive(Debug, Clone)] pub struct RouterData { @@ -416,15 +424,6 @@ impl ErrorResponse { } } -#[cfg(feature = "v2")] -use crate::{ - payments::{ - payment_attempt::{ErrorDetails, PaymentAttemptUpdate}, - payment_intent::PaymentIntentUpdate, - }, - router_flow_types, router_request_types, router_response_types, -}; - /// Get updatable trakcer objects of payment intent and payment attempt #[cfg(feature = "v2")] pub trait TrackerPostUpdateObjects { @@ -518,14 +517,27 @@ impl | router_request_types::ResponseId::EncodedData(id) => Some(id.to_owned()), }; - PaymentAttemptUpdate::ConfirmIntentResponse { - status: attempt_status, - connector_payment_id, - updated_by: storage_scheme.to_string(), - redirection_data: *redirection_data.clone(), - amount_capturable, - connector_metadata: connector_metadata.clone().map(Secret::new), - } + PaymentAttemptUpdate::ConfirmIntentResponse(Box::new( + payments::payment_attempt::ConfirmIntentResponseUpdate { + status: attempt_status, + connector_payment_id, + updated_by: storage_scheme.to_string(), + redirection_data: *redirection_data.clone(), + amount_capturable, + connector_metadata: connector_metadata.clone().map(Secret::new), + connector_token_details: response_router_data + .get_updated_connector_token_details( + payment_data + .payment_attempt + .connector_token_details + .as_ref() + .and_then(|token_details| { + token_details + .get_connector_mandate_request_reference_id() + }), + ), + }, + )) } router_response_types::PaymentsResponseData::MultipleCaptureResponse { .. } => { todo!() @@ -1092,3 +1104,222 @@ impl } } } + +#[cfg(feature = "v2")] +impl + TrackerPostUpdateObjects< + router_flow_types::SetupMandate, + router_request_types::SetupMandateRequestData, + payments::PaymentConfirmData, + > + for RouterData< + router_flow_types::SetupMandate, + router_request_types::SetupMandateRequestData, + router_response_types::PaymentsResponseData, + > +{ + fn get_payment_intent_update( + &self, + payment_data: &payments::PaymentConfirmData, + storage_scheme: common_enums::MerchantStorageScheme, + ) -> PaymentIntentUpdate { + let amount_captured = self.get_captured_amount(payment_data); + match self.response { + Ok(ref _response) => PaymentIntentUpdate::ConfirmIntentPostUpdate { + status: common_enums::IntentStatus::from( + self.get_attempt_status_for_db_update(payment_data), + ), + amount_captured, + updated_by: storage_scheme.to_string(), + }, + Err(ref error) => PaymentIntentUpdate::ConfirmIntentPostUpdate { + status: error + .attempt_status + .map(common_enums::IntentStatus::from) + .unwrap_or(common_enums::IntentStatus::Failed), + amount_captured, + updated_by: storage_scheme.to_string(), + }, + } + } + + fn get_payment_attempt_update( + &self, + payment_data: &payments::PaymentConfirmData, + storage_scheme: common_enums::MerchantStorageScheme, + ) -> PaymentAttemptUpdate { + let amount_capturable = self.get_amount_capturable(payment_data); + + match self.response { + Ok(ref response_router_data) => match response_router_data { + router_response_types::PaymentsResponseData::TransactionResponse { + resource_id, + redirection_data, + mandate_reference, + connector_metadata, + network_txn_id, + connector_response_reference_id, + incremental_authorization_allowed, + charge_id, + } => { + let attempt_status = self.get_attempt_status_for_db_update(payment_data); + + let connector_payment_id = match resource_id { + router_request_types::ResponseId::NoResponseId => None, + router_request_types::ResponseId::ConnectorTransactionId(id) + | router_request_types::ResponseId::EncodedData(id) => Some(id.to_owned()), + }; + + PaymentAttemptUpdate::ConfirmIntentResponse(Box::new( + payments::payment_attempt::ConfirmIntentResponseUpdate { + status: attempt_status, + connector_payment_id, + updated_by: storage_scheme.to_string(), + redirection_data: *redirection_data.clone(), + amount_capturable, + connector_metadata: connector_metadata.clone().map(Secret::new), + connector_token_details: response_router_data + .get_updated_connector_token_details( + payment_data + .payment_attempt + .connector_token_details + .as_ref() + .and_then(|token_details| { + token_details + .get_connector_mandate_request_reference_id() + }), + ), + }, + )) + } + router_response_types::PaymentsResponseData::MultipleCaptureResponse { .. } => { + todo!() + } + router_response_types::PaymentsResponseData::SessionResponse { .. } => todo!(), + router_response_types::PaymentsResponseData::SessionTokenResponse { .. } => todo!(), + router_response_types::PaymentsResponseData::TransactionUnresolvedResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::TokenizationResponse { .. } => todo!(), + router_response_types::PaymentsResponseData::ConnectorCustomerResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::ThreeDSEnrollmentResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::PreProcessingResponse { .. } => { + todo!() + } + router_response_types::PaymentsResponseData::IncrementalAuthorizationResponse { + .. + } => todo!(), + router_response_types::PaymentsResponseData::PostProcessingResponse { .. } => { + todo!() + } + router_response_types::PaymentsResponseData::SessionUpdateResponse { .. } => { + todo!() + } + }, + Err(ref error_response) => { + let ErrorResponse { + code, + message, + reason, + status_code: _, + attempt_status, + connector_transaction_id, + } = error_response.clone(); + let attempt_status = attempt_status.unwrap_or(self.status); + + let error_details = ErrorDetails { + code, + message, + reason, + unified_code: None, + unified_message: None, + }; + + PaymentAttemptUpdate::ErrorUpdate { + status: attempt_status, + error: error_details, + amount_capturable, + connector_payment_id: connector_transaction_id, + updated_by: storage_scheme.to_string(), + } + } + } + } + + fn get_attempt_status_for_db_update( + &self, + _payment_data: &payments::PaymentConfirmData, + ) -> common_enums::AttemptStatus { + // For this step, consider whatever status was given by the connector module + // We do not need to check for amount captured or amount capturable here because we are authorizing the whole amount + self.status + } + + fn get_amount_capturable( + &self, + payment_data: &payments::PaymentConfirmData, + ) -> Option { + // Based on the status of the response, we can determine the amount capturable + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is already succeeded / failed we cannot capture any more amount + // So set the amount capturable to zero + common_enums::IntentStatus::Succeeded + | common_enums::IntentStatus::Failed + | common_enums::IntentStatus::Cancelled => Some(MinorUnit::zero()), + // For these statuses, update the capturable amount when it reaches terminal / capturable state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + // If status is requires capture, get the total amount that can be captured + // This is in cases where the capture method will be `manual` or `manual_multiple` + // We do not need to handle the case where amount_to_capture is provided here as it cannot be passed in authroize flow + common_enums::IntentStatus::RequiresCapture => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // Invalid statues for this flow, after doing authorization this state is invalid + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } + + fn get_captured_amount( + &self, + payment_data: &payments::PaymentConfirmData, + ) -> Option { + // Based on the status of the response, we can determine the amount that was captured + let intent_status = common_enums::IntentStatus::from(self.status); + match intent_status { + // If the status is succeeded then we have captured the whole amount + // we need not check for `amount_to_capture` here because passing `amount_to_capture` when authorizing is not supported + common_enums::IntentStatus::Succeeded => { + let total_amount = payment_data.payment_attempt.amount_details.get_net_amount(); + Some(total_amount) + } + // No amount is captured + common_enums::IntentStatus::Cancelled | common_enums::IntentStatus::Failed => { + Some(MinorUnit::zero()) + } + // For these statuses, update the amount captured when it reaches terminal state + common_enums::IntentStatus::RequiresCustomerAction + | common_enums::IntentStatus::RequiresMerchantAction + | common_enums::IntentStatus::Processing => None, + // Invalid states for this flow + common_enums::IntentStatus::RequiresPaymentMethod + | common_enums::IntentStatus::RequiresConfirmation => None, + // No amount has been captured yet + common_enums::IntentStatus::RequiresCapture => Some(MinorUnit::zero()), + // Invalid statues for this flow + common_enums::IntentStatus::PartiallyCaptured + | common_enums::IntentStatus::PartiallyCapturedAndCapturable => None, + } + } +} diff --git a/crates/hyperswitch_domain_models/src/router_response_types.rs b/crates/hyperswitch_domain_models/src/router_response_types.rs index 1a1488031f5..4d15d715ff7 100644 --- a/crates/hyperswitch_domain_models/src/router_response_types.rs +++ b/crates/hyperswitch_domain_models/src/router_response_types.rs @@ -149,6 +149,7 @@ impl PaymentsResponseData { .into()), } } + pub fn merge_transaction_responses( auth_response: &Self, capture_response: &Self, @@ -206,6 +207,31 @@ impl PaymentsResponseData { .into()), } } + + #[cfg(feature = "v2")] + pub fn get_updated_connector_token_details( + &self, + original_connector_mandate_request_reference_id: Option, + ) -> Option { + if let Self::TransactionResponse { + mandate_reference, .. + } = self + { + mandate_reference.clone().map(|mandate_ref| { + let connector_mandate_id = mandate_ref.connector_mandate_id; + let connector_mandate_request_reference_id = mandate_ref + .connector_mandate_request_reference_id + .or(original_connector_mandate_request_reference_id); + + diesel_models::ConnectorTokenDetails { + connector_mandate_id, + connector_mandate_request_reference_id, + } + }) + } else { + None + } + } } #[derive(Debug, Clone)] diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 701fb22d1ad..1078d8080ca 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -124,6 +124,7 @@ Never share your secret api keys. Keep them guarded and secure. routes::payments::payments_update_intent, routes::payments::payments_confirm_intent, routes::payments::payment_status, + routes::payments::payments_create_and_confirm_intent, routes::payments::payments_connector_session, routes::payments::list_payment_methods, @@ -359,8 +360,9 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::CardRedirectData, api_models::payments::CardToken, api_models::payments::CustomerAcceptance, + api_models::payments::ConnectorTokenDetails, + api_models::payments::PaymentsRequest, api_models::payments::PaymentsResponse, - api_models::payments::PaymentsCreateResponseOpenApi, api_models::payments::PaymentRetrieveBody, api_models::payments::PaymentsRetrieveRequest, api_models::payments::PaymentsCaptureRequest, @@ -424,7 +426,6 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::SamsungPayTokenData, api_models::payments::PaymentsCancelRequest, api_models::payments::PaymentListConstraints, - api_models::payments::PaymentListResponse, api_models::payments::CashappQr, api_models::payments::BankTransferData, api_models::payments::BankTransferNextStepsData, diff --git a/crates/openapi/src/routes/payment_method.rs b/crates/openapi/src/routes/payment_method.rs index afc9f4a1735..4c0c201e290 100644 --- a/crates/openapi/src/routes/payment_method.rs +++ b/crates/openapi/src/routes/payment_method.rs @@ -334,7 +334,7 @@ pub async fn payment_method_delete_api() {} ( "X-Profile-Id" = String, Header, description = "Profile ID associated to the payment method intent", - example = json!({"X-Profile-Id": "pro_abcdefghijklmnop"}) + example = "pro_abcdefghijklmnop" ), ), responses( diff --git a/crates/openapi/src/routes/payments.rs b/crates/openapi/src/routes/payments.rs index 33bb95618e8..ceccd53c4dd 100644 --- a/crates/openapi/src/routes/payments.rs +++ b/crates/openapi/src/routes/payments.rs @@ -659,7 +659,7 @@ pub fn payments_get_intent() {} ( "X-Profile-Id" = String, Header, description = "Profile ID associated to the payment intent", - example = json!({"X-Profile-Id": "pro_abcdefghijklmnop"}) + example = "pro_abcdefghijklmnop" ), ), request_body( @@ -695,7 +695,7 @@ pub fn payments_update_intent() {} ( "X-Profile-Id" = String, Header, description = "Profile ID associated to the payment intent", - example = json!({"X-Profile-Id": "pro_abcdefghijklmnop"}) + example = "pro_abcdefghijklmnop" ), ( "X-Client-Secret" = String, Header, @@ -710,6 +710,7 @@ pub fn payments_update_intent() {} "Confirm the payment intent with card details" = ( value = json!({ "payment_method_type": "card", + "payment_method_subtype": "credit", "payment_method_data": { "card": { "card_number": "4242424242424242", @@ -756,6 +757,57 @@ pub fn payments_confirm_intent() {} #[cfg(feature = "v2")] pub fn payment_status() {} +/// Payments - Create and Confirm Intent +/// +/// **Creates and confirms a payment intent object when the amount and payment method information are passed.** +/// +/// You will require the 'API - Key' from the Hyperswitch dashboard to make the call. +#[utoipa::path( + post, + path = "/v2/payments", + params ( + ( + "X-Profile-Id" = String, Header, + description = "Profile ID associated to the payment intent", + example = "pro_abcdefghijklmnop" + ) + ), + request_body( + content = PaymentsRequest, + examples( + ( + "Create and confirm the payment intent with amount and card details" = ( + value = json!({ + "amount_details": { + "order_amount": 6540, + "currency": "USD" + }, + "payment_method_type": "card", + "payment_method_subtype": "credit", + "payment_method_data": { + "card": { + "card_number": "4242424242424242", + "card_exp_month": "10", + "card_exp_year": "25", + "card_holder_name": "joseph Doe", + "card_cvc": "123" + } + }, + }) + ) + ), + ), + ), + responses( + (status = 200, description = "Payment created", body = PaymentsResponse), + (status = 400, description = "Missing Mandatory fields") + ), + tag = "Payments", + operation_id = "Create and Confirm Payment Intent", + security(("api_key" = [])), +)] +pub fn payments_create_and_confirm_intent() {} + #[derive(utoipa::ToSchema)] #[schema(rename_all = "lowercase")] pub(crate) enum ForceSync { @@ -777,7 +829,7 @@ pub(crate) enum ForceSync { ( "X-Profile-Id" = String, Header, description = "Profile ID associated to the payment intent", - example = json!({"X-Profile-Id": "pro_abcdefghijklmnop"}) + example = "pro_abcdefghijklmnop" ), ( "X-Client-Secret" = String, Header, diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 9e00bcd6bce..585e894adf9 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -6,7 +6,9 @@ pub mod user_role; use std::collections::HashSet; use common_utils::consts; +pub use hyperswitch_domain_models::consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH; pub use hyperswitch_interfaces::consts::{NO_ERROR_CODE, NO_ERROR_MESSAGE}; + // ID generation pub(crate) const ID_LENGTH: usize = 20; pub(crate) const MAX_ID_LENGTH: usize = 64; @@ -136,9 +138,6 @@ pub const DEFAULT_UNIFIED_ERROR_MESSAGE: &str = "Something went wrong"; // Recon's feature tag pub const RECON_FEATURE_TAG: &str = "RECONCILIATION AND SETTLEMENT"; -// Length of the unique reference ID generated for connector mandate requests -pub const CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH: usize = 18; - /// Default allowed domains for payment links pub const DEFAULT_ALLOWED_DOMAINS: Option> = None; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 8e31ab7975f..47d2024834e 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1610,6 +1610,192 @@ where ) } +#[allow(clippy::too_many_arguments)] +#[cfg(feature = "v2")] +pub(crate) async fn payments_create_and_confirm_intent( + state: SessionState, + req_state: ReqState, + merchant_account: domain::MerchantAccount, + profile: domain::Profile, + key_store: domain::MerchantKeyStore, + request: payments_api::PaymentsRequest, + payment_id: id_type::GlobalPaymentId, + mut header_payload: HeaderPayload, + platform_merchant_account: Option, +) -> RouterResponse { + use actix_http::body::MessageBody; + use common_utils::ext_traits::BytesExt; + use hyperswitch_domain_models::{ + payments::{PaymentConfirmData, PaymentIntentData}, + router_flow_types::{Authorize, PaymentCreateIntent, SetupMandate}, + }; + + let payload = payments_api::PaymentsCreateIntentRequest::from(&request); + + let create_intent_response = Box::pin(payments_intent_core::< + PaymentCreateIntent, + payments_api::PaymentsIntentResponse, + _, + _, + PaymentIntentData, + >( + state.clone(), + req_state.clone(), + merchant_account.clone(), + profile.clone(), + key_store.clone(), + operations::PaymentIntentCreate, + payload, + payment_id.clone(), + header_payload.clone(), + platform_merchant_account, + )) + .await?; + + logger::info!(?create_intent_response); + let create_intent_response = handle_payments_intent_response(create_intent_response)?; + + // Adding client secret to ensure client secret validation passes during confirm intent step + header_payload.client_secret = Some(create_intent_response.client_secret.clone()); + + let payload = payments_api::PaymentsConfirmIntentRequest::from(&request); + + let confirm_intent_response = decide_authorize_or_setup_intent_flow( + state, + req_state, + merchant_account, + profile, + key_store, + &create_intent_response, + payload, + payment_id, + header_payload, + ) + .await?; + + logger::info!(?confirm_intent_response); + let confirm_intent_response = handle_payments_intent_response(confirm_intent_response)?; + + construct_payments_response(create_intent_response, confirm_intent_response) +} + +#[cfg(feature = "v2")] +#[inline] +fn handle_payments_intent_response( + response: hyperswitch_domain_models::api::ApplicationResponse, +) -> CustomResult { + match response { + hyperswitch_domain_models::api::ApplicationResponse::Json(body) + | hyperswitch_domain_models::api::ApplicationResponse::JsonWithHeaders((body, _)) => { + Ok(body) + } + hyperswitch_domain_models::api::ApplicationResponse::StatusOk + | hyperswitch_domain_models::api::ApplicationResponse::TextPlain(_) + | hyperswitch_domain_models::api::ApplicationResponse::JsonForRedirection(_) + | hyperswitch_domain_models::api::ApplicationResponse::Form(_) + | hyperswitch_domain_models::api::ApplicationResponse::PaymentLinkForm(_) + | hyperswitch_domain_models::api::ApplicationResponse::FileData(_) + | hyperswitch_domain_models::api::ApplicationResponse::GenericLinkForm(_) => { + Err(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unexpected response from payment intent core") + } + } +} + +#[cfg(feature = "v2")] +#[inline] +fn construct_payments_response( + create_intent_response: payments_api::PaymentsIntentResponse, + confirm_intent_response: payments_api::PaymentsConfirmIntentResponse, +) -> RouterResponse { + let response = payments_api::PaymentsResponse { + id: confirm_intent_response.id, + status: confirm_intent_response.status, + amount: confirm_intent_response.amount, + customer_id: confirm_intent_response.customer_id, + connector: confirm_intent_response.connector, + client_secret: confirm_intent_response.client_secret, + created: confirm_intent_response.created, + payment_method_data: confirm_intent_response.payment_method_data, + payment_method_type: confirm_intent_response.payment_method_type, + payment_method_subtype: confirm_intent_response.payment_method_subtype, + next_action: confirm_intent_response.next_action, + connector_transaction_id: confirm_intent_response.connector_transaction_id, + connector_reference_id: confirm_intent_response.connector_reference_id, + connector_token_details: confirm_intent_response.connector_token_details, + merchant_connector_id: confirm_intent_response.merchant_connector_id, + browser_info: confirm_intent_response.browser_info, + error: confirm_intent_response.error, + }; + + Ok(hyperswitch_domain_models::api::ApplicationResponse::Json( + response, + )) +} + +#[cfg(feature = "v2")] +#[allow(clippy::too_many_arguments)] +async fn decide_authorize_or_setup_intent_flow( + state: SessionState, + req_state: ReqState, + merchant_account: domain::MerchantAccount, + profile: domain::Profile, + key_store: domain::MerchantKeyStore, + create_intent_response: &payments_api::PaymentsIntentResponse, + confirm_intent_request: payments_api::PaymentsConfirmIntentRequest, + payment_id: id_type::GlobalPaymentId, + header_payload: HeaderPayload, +) -> RouterResponse { + use hyperswitch_domain_models::{ + payments::PaymentConfirmData, + router_flow_types::{Authorize, SetupMandate}, + }; + + if create_intent_response.amount_details.order_amount == MinorUnit::zero() { + Box::pin(payments_core::< + SetupMandate, + api_models::payments::PaymentsConfirmIntentResponse, + _, + _, + _, + PaymentConfirmData, + >( + state, + req_state, + merchant_account, + profile, + key_store, + operations::PaymentIntentConfirm, + confirm_intent_request, + payment_id, + CallConnectorAction::Trigger, + header_payload, + )) + .await + } else { + Box::pin(payments_core::< + Authorize, + api_models::payments::PaymentsConfirmIntentResponse, + _, + _, + _, + PaymentConfirmData, + >( + state, + req_state, + merchant_account, + profile, + key_store, + operations::PaymentIntentConfirm, + confirm_intent_request, + payment_id, + CallConnectorAction::Trigger, + header_payload, + )) + .await + } +} + fn is_start_pay(operation: &Op) -> bool { format!("{operation:?}").eq("PaymentStart") } diff --git a/crates/router/src/core/payments/flows/setup_mandate_flow.rs b/crates/router/src/core/payments/flows/setup_mandate_flow.rs index ee8ff78bdab..52014730eea 100644 --- a/crates/router/src/core/payments/flows/setup_mandate_flow.rs +++ b/crates/router/src/core/payments/flows/setup_mandate_flow.rs @@ -15,6 +15,7 @@ use crate::{ types::{self, api, domain}, }; +#[cfg(feature = "v1")] #[async_trait] impl ConstructFlowSpecificData< @@ -23,7 +24,6 @@ impl types::PaymentsResponseData, > for PaymentData { - #[cfg(feature = "v1")] async fn construct_router_data<'a>( &self, state: &SessionState, @@ -52,7 +52,27 @@ impl .await } - #[cfg(feature = "v2")] + async fn get_merchant_recipient_data<'a>( + &self, + _state: &SessionState, + _merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _merchant_connector_account: &helpers::MerchantConnectorAccountType, + _connector: &api::ConnectorData, + ) -> RouterResult> { + Ok(None) + } +} + +#[cfg(feature = "v2")] +#[async_trait] +impl + ConstructFlowSpecificData< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for hyperswitch_domain_models::payments::PaymentConfirmData +{ async fn construct_router_data<'a>( &self, state: &SessionState, @@ -64,7 +84,20 @@ impl merchant_recipient_data: Option, header_payload: Option, ) -> RouterResult { - todo!() + Box::pin( + transformers::construct_payment_router_data_for_setup_mandate( + state, + self.clone(), + connector_id, + merchant_account, + key_store, + customer, + merchant_connector_account, + merchant_recipient_data, + header_payload, + ), + ) + .await } async fn get_merchant_recipient_data<'a>( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 09fb3714421..b0df22f3d04 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3163,6 +3163,7 @@ pub async fn delete_ephemeral_key( Ok(services::ApplicationResponse::Json(response)) } +#[cfg(feature = "v1")] pub fn make_pg_redirect_response( payment_id: id_type::PaymentId, response: &api::PaymentsResponse, diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 9b1c2ea3be3..5f3b515c96e 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -777,7 +777,7 @@ impl GetTracker, api::PaymentsRequest> None, // update_history None, // mandate_metadata Some(common_utils::generate_id_with_len( - consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH.to_owned(), + consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH, )), // connector_mandate_request_reference_id )), ); diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 6ace8c3018f..dd72bfff307 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -560,7 +560,7 @@ impl GetTracker, api::PaymentsRequest> None, // update_history None, // mandate_metadata Some(common_utils::generate_id_with_len( - consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH.to_owned(), + consts::CONNECTOR_MANDATE_REQUEST_REFERENCE_ID_LENGTH, )), // connector_mandate_request_reference_id ), )); diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 07342ffb4e6..748b71469ba 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -2423,6 +2423,95 @@ impl PostUpdateTracker, types::PaymentsSyncDat } } +#[cfg(feature = "v2")] +impl Operation for &PaymentResponse { + type Data = PaymentConfirmData; + fn to_post_update_tracker( + &self, + ) -> RouterResult< + &(dyn PostUpdateTracker + Send + Sync), + > { + Ok(*self) + } +} + +#[cfg(feature = "v2")] +impl Operation for PaymentResponse { + type Data = PaymentConfirmData; + fn to_post_update_tracker( + &self, + ) -> RouterResult< + &(dyn PostUpdateTracker + Send + Sync), + > { + Ok(self) + } +} + +#[cfg(feature = "v2")] +#[async_trait] +impl PostUpdateTracker, types::SetupMandateRequestData> + for PaymentResponse +{ + async fn update_tracker<'b>( + &'b self, + state: &'b SessionState, + mut payment_data: PaymentConfirmData, + response: types::RouterData, + key_store: &domain::MerchantKeyStore, + storage_scheme: enums::MerchantStorageScheme, + ) -> RouterResult> + where + F: 'b + Send + Sync, + types::RouterData: + hyperswitch_domain_models::router_data::TrackerPostUpdateObjects< + F, + types::SetupMandateRequestData, + PaymentConfirmData, + >, + { + use hyperswitch_domain_models::router_data::TrackerPostUpdateObjects; + + let db = &*state.store; + let key_manager_state = &state.into(); + + let response_router_data = response; + + let payment_intent_update = + response_router_data.get_payment_intent_update(&payment_data, storage_scheme); + let payment_attempt_update = + response_router_data.get_payment_attempt_update(&payment_data, storage_scheme); + + let updated_payment_intent = db + .update_payment_intent( + key_manager_state, + payment_data.payment_intent, + payment_intent_update, + key_store, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to update payment intent")?; + + let updated_payment_attempt = db + .update_payment_attempt( + key_manager_state, + key_store, + payment_data.payment_attempt, + payment_attempt_update, + storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to update payment attempt")?; + + payment_data.payment_intent = updated_payment_intent; + payment_data.payment_attempt = updated_payment_attempt; + + Ok(payment_data) + } +} + #[cfg(feature = "v1")] fn update_connector_mandate_details_for_the_flow( connector_mandate_id: Option, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 80c10a294ec..1e9adb0a06b 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -305,7 +305,7 @@ pub async fn construct_payment_router_data_for_authorize<'a>( }; let connector_mandate_request_reference_id = payment_data .payment_attempt - .connector_mandate_detail + .connector_token_details .as_ref() .and_then(|detail| detail.get_connector_mandate_request_reference_id()); @@ -428,7 +428,7 @@ pub async fn construct_payment_router_data_for_capture<'a>( let connector_mandate_request_reference_id = payment_data .payment_attempt - .connector_mandate_detail + .connector_token_details .as_ref() .and_then(|detail| detail.get_connector_mandate_request_reference_id()); @@ -831,6 +831,198 @@ pub async fn construct_payment_router_data_for_sdk_session<'a>( Ok(router_data) } +#[cfg(feature = "v2")] +#[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +pub async fn construct_payment_router_data_for_setup_mandate<'a>( + state: &'a SessionState, + payment_data: hyperswitch_domain_models::payments::PaymentConfirmData, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &'a Option, + merchant_connector_account: &domain::MerchantConnectorAccount, + _merchant_recipient_data: Option, + header_payload: Option, +) -> RouterResult { + fp_utils::when(merchant_connector_account.is_disabled(), || { + Err(errors::ApiErrorResponse::MerchantConnectorAccountDisabled) + })?; + + let auth_type = merchant_connector_account + .get_connector_account_details() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while parsing value for ConnectorAuthType")?; + + // TODO: Take Globalid and convert to connector reference id + let customer_id = customer + .to_owned() + .map(|customer| common_utils::id_type::CustomerId::try_from(customer.id.clone())) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Invalid global customer generated, not able to convert to reference id", + )?; + + let payment_method = payment_data.payment_attempt.payment_method_type; + + let router_base_url = &state.base_url; + let attempt = &payment_data.payment_attempt; + + let complete_authorize_url = Some(helpers::create_complete_authorize_url( + router_base_url, + attempt, + connector_id, + )); + + let webhook_url = Some(helpers::create_webhook_url( + router_base_url, + &attempt.merchant_id, + merchant_connector_account.get_id().get_string_repr(), + )); + + let router_return_url = payment_data + .payment_intent + .create_finish_redirection_url(router_base_url, &merchant_account.publishable_key) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to construct finish redirection url")? + .to_string(); + + let connector_request_reference_id = payment_data + .payment_intent + .merchant_reference_id + .map(|id| id.get_string_repr().to_owned()) + .unwrap_or(payment_data.payment_attempt.id.get_string_repr().to_owned()); + + let email = customer + .as_ref() + .and_then(|customer| customer.email.clone()) + .map(pii::Email::from); + + let browser_info = payment_data + .payment_attempt + .browser_info + .clone() + .map(types::BrowserInformation::from); + + // TODO: few fields are repeated in both routerdata and request + let request = types::SetupMandateRequestData { + currency: payment_data.payment_intent.amount_details.currency, + payment_method_data: payment_data + .payment_method_data + .get_required_value("payment_method_data")?, + amount: Some( + payment_data + .payment_attempt + .amount_details + .get_net_amount() + .get_amount_as_i64(), + ), + confirm: true, + statement_descriptor_suffix: None, + customer_acceptance: None, + mandate_id: None, + setup_future_usage: Some(payment_data.payment_intent.setup_future_usage), + off_session: None, + setup_mandate_details: None, + router_return_url: Some(router_return_url.clone()), + webhook_url, + browser_info, + email, + customer_name: None, + return_url: Some(router_return_url), + payment_method_type: Some(payment_data.payment_attempt.payment_method_subtype), + request_incremental_authorization: matches!( + payment_data + .payment_intent + .request_incremental_authorization, + RequestIncrementalAuthorization::True | RequestIncrementalAuthorization::Default + ), + metadata: payment_data.payment_intent.metadata, + minor_amount: Some(payment_data.payment_attempt.amount_details.get_net_amount()), + shipping_cost: payment_data.payment_intent.amount_details.shipping_cost, + }; + let connector_mandate_request_reference_id = payment_data + .payment_attempt + .connector_token_details + .as_ref() + .and_then(|detail| detail.get_connector_mandate_request_reference_id()); + + // TODO: evaluate the fields in router data, if they are required or not + let router_data = types::RouterData { + flow: PhantomData, + merchant_id: merchant_account.get_id().clone(), + tenant_id: state.tenant.tenant_id.clone(), + // TODO: evaluate why we need customer id at the connector level. We already have connector customer id. + customer_id, + connector: connector_id.to_owned(), + // TODO: evaluate why we need payment id at the connector level. We already have connector reference id + payment_id: payment_data + .payment_attempt + .payment_id + .get_string_repr() + .to_owned(), + // TODO: evaluate why we need attempt id at the connector level. We already have connector reference id + attempt_id: payment_data + .payment_attempt + .get_id() + .get_string_repr() + .to_owned(), + status: payment_data.payment_attempt.status, + payment_method, + connector_auth_type: auth_type, + description: payment_data + .payment_intent + .description + .as_ref() + .map(|description| description.get_string_repr()) + .map(ToOwned::to_owned), + // TODO: Create unified address + address: payment_data.payment_address.clone(), + auth_type: payment_data.payment_attempt.authentication_type, + connector_meta_data: None, + connector_wallets_details: None, + request, + response: Err(hyperswitch_domain_models::router_data::ErrorResponse::default()), + amount_captured: None, + minor_amount_captured: None, + access_token: None, + session_token: None, + reference_id: None, + payment_method_status: None, + payment_method_token: None, + connector_customer: None, + recurring_mandate_payment_data: None, + // TODO: This has to be generated as the reference id based on the connector configuration + // Some connectros might not accept accept the global id. This has to be done when generating the reference id + connector_request_reference_id, + preprocessing_id: payment_data.payment_attempt.preprocessing_step_id, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + // TODO: take this based on the env + test_mode: Some(true), + payment_method_balance: None, + connector_api_version: None, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + frm_metadata: None, + refund_id: None, + dispute_id: None, + connector_response: None, + integrity_check: Ok(()), + additional_merchant_data: None, + header_payload, + connector_mandate_request_reference_id, + authentication_id: None, + psd2_sca_exemption_type: None, + }; + + Ok(router_data) +} + #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] @@ -1433,6 +1625,10 @@ where .as_ref() .map(|_| api_models::payments::NextActionData::RedirectToUrl { redirect_to_url }); + let connector_token_details = payment_attempt + .connector_token_details + .and_then(Option::::foreign_from); + let response = api_models::payments::PaymentsConfirmIntentResponse { id: payment_intent.id.clone(), status: payment_intent.status, @@ -1447,6 +1643,7 @@ where next_action, connector_transaction_id: payment_attempt.connector_payment_id.clone(), connector_reference_id: None, + connector_token_details, merchant_connector_id, browser_info: None, error, @@ -4146,6 +4343,7 @@ impl ForeignFrom for ConnectorMandateReferenc ) } } + impl ForeignFrom for DieselConnectorMandateReferenceId { fn foreign_from(value: ConnectorMandateReferenceId) -> Self { Self { @@ -4158,6 +4356,17 @@ impl ForeignFrom for DieselConnectorMandateReferenc } } +#[cfg(feature = "v2")] +impl ForeignFrom + for Option +{ + fn foreign_from(value: diesel_models::ConnectorTokenDetails) -> Self { + value + .connector_mandate_id + .map(|mandate_id| api_models::payments::ConnectorTokenDetails { token: mandate_id }) + } +} + impl ForeignFrom<(Self, Option<&api_models::payments::AdditionalPaymentData>)> for Option { diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 22bc38b3d6c..71cc3dceebb 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1419,6 +1419,7 @@ pub fn get_external_authentication_request_poll_id( payment_id.get_external_authentication_request_poll_id() } +#[cfg(feature = "v1")] pub fn get_html_redirect_response_for_external_authentication( return_url_with_query_params: String, payment_response: &api_models::payments::PaymentsResponse, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index fffb3eab09e..17ab056e856 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -551,9 +551,15 @@ pub struct Payments; impl Payments { pub fn server(state: AppState) -> Scope { let mut route = web::scope("/v2/payments").app_data(web::Data::new(state)); - route = route.service( - web::resource("/create-intent").route(web::post().to(payments::payments_create_intent)), - ); + route = route + .service( + web::resource("") + .route(web::post().to(payments::payments_create_and_confirm_intent)), + ) + .service( + web::resource("/create-intent") + .route(web::post().to(payments::payments_create_intent)), + ); route = route.service( web::scope("/{payment_id}") diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 6550778619a..895ba4b8aff 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -146,6 +146,7 @@ impl From for ApiIdentifier { | Flow::PaymentsGetIntent | Flow::PaymentsPostSessionTokens | Flow::PaymentsUpdateIntent + | Flow::PaymentsCreateAndConfirmIntent | Flow::PaymentStartRedirection => Self::Payments, Flow::PayoutsCreate diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index c88f61f7c82..d461599b6f0 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -217,6 +217,57 @@ pub async fn payments_get_intent( .await } +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsCreateAndConfirmIntent, payment_id))] +pub async fn payments_create_and_confirm_intent( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let flow = Flow::PaymentsCreateAndConfirmIntent; + let header_payload = match HeaderPayload::foreign_try_from(req.headers()) { + Ok(headers) => headers, + Err(err) => { + return api::log_and_return_error_response(err); + } + }; + + let global_payment_id = + common_utils::id_type::GlobalPaymentId::generate(&state.conf.cell_information.id); + + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, request, req_state| { + payments::payments_create_and_confirm_intent( + state, + req_state, + auth.merchant_account, + auth.profile, + auth.key_store, + request, + global_payment_id.clone(), + header_payload.clone(), + auth.platform_merchant_account, + ) + }, + match env::which() { + env::Env::Production => &auth::HeaderAuth(auth::ApiKeyAuth), + _ => auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuth { + permission: Permission::ProfilePaymentWrite, + }, + req.headers(), + ), + }, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(feature = "v2")] #[instrument(skip_all, fields(flow = ?Flow::PaymentsUpdateIntent, payment_id))] pub async fn payments_update_intent( diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 0a56ff83f98..8095907eca8 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -592,7 +592,6 @@ async fn handle_response( match status_code { 200..=202 | 302 | 204 => { - logger::debug!(response=?response); // If needed add log line // logger:: error!( error_parsing_response=?err); let response = response diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index c11d54fc298..4ad2bd4998e 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -543,6 +543,7 @@ pub struct RedirectPaymentFlowResponse { pub profile: domain::Profile, } +#[cfg(feature = "v1")] #[derive(Clone, Debug)] pub struct AuthenticatePaymentFlowResponse { pub payments_response: api_models::payments::PaymentsResponse, diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index 6ec0fe701e9..58159fc5688 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -1,8 +1,9 @@ #[cfg(feature = "v1")] -pub use api_models::payments::PaymentsRequest; +pub use api_models::payments::{PaymentListResponse, PaymentListResponseV2}; #[cfg(feature = "v2")] pub use api_models::payments::{ - PaymentsCreateIntentRequest, PaymentsIntentResponse, PaymentsUpdateIntentRequest, + PaymentsConfirmIntentRequest, PaymentsCreateIntentRequest, PaymentsIntentResponse, + PaymentsUpdateIntentRequest, }; pub use api_models::{ feature_matrix::{ @@ -14,18 +15,18 @@ pub use api_models::{ MandateTransactionType, MandateType, MandateValidationFields, NextActionType, OnlineMandate, OpenBankingSessionToken, PayLaterData, PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, - PaymentListFiltersV2, PaymentListResponse, PaymentListResponseV2, PaymentMethodData, - PaymentMethodDataRequest, PaymentMethodDataResponse, PaymentOp, PaymentRetrieveBody, + PaymentListFiltersV2, PaymentMethodData, PaymentMethodDataRequest, + PaymentMethodDataResponse, PaymentOp, PaymentRetrieveBody, PaymentRetrieveBodyWithCredentials, PaymentsAggregateResponse, PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsCompleteAuthorizeRequest, PaymentsDynamicTaxCalculationRequest, PaymentsDynamicTaxCalculationResponse, PaymentsExternalAuthenticationRequest, PaymentsIncrementalAuthorizationRequest, PaymentsManualUpdateRequest, PaymentsPostSessionTokensRequest, PaymentsPostSessionTokensResponse, PaymentsRedirectRequest, PaymentsRedirectionResponse, - PaymentsRejectRequest, PaymentsResponse, PaymentsResponseForm, PaymentsRetrieveRequest, - PaymentsSessionRequest, PaymentsSessionResponse, PaymentsStartRequest, PgRedirectResponse, - PhoneDetails, RedirectionResponse, SessionToken, UrlDetails, VerifyRequest, VerifyResponse, - WalletData, + PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsResponseForm, + PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, + PaymentsStartRequest, PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, + UrlDetails, VerifyRequest, VerifyResponse, WalletData, }, }; use error_stack::ResultExt; diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 935efa3c78b..a267d24af1d 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -172,6 +172,10 @@ pub enum Flow { PaymentsGetIntent, /// Payments Update Intent flow PaymentsUpdateIntent, + /// Payments confirm intent flow + PaymentsConfirmIntent, + /// Payments create and confirm intent flow + PaymentsCreateAndConfirmIntent, #[cfg(feature = "payouts")] /// Payouts create flow PayoutsCreate, @@ -525,8 +529,6 @@ pub enum Flow { PaymentsManualUpdate, /// Dynamic Tax Calcultion SessionUpdateTaxCalculation, - /// Payments confirm intent - PaymentsConfirmIntent, /// Payments post session tokens flow PaymentsPostSessionTokens, /// Payments start redirection flow diff --git a/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql b/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql index a4616016abf..08e4b0a3568 100644 --- a/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql +++ b/v2_migrations/2024-08-28-081721_add_v2_columns/down.sql @@ -42,7 +42,8 @@ ALTER TABLE payment_attempt DROP COLUMN payment_method_type_v2, DROP COLUMN tax_on_surcharge, DROP COLUMN payment_method_billing_address, DROP COLUMN redirection_data, - DROP COLUMN connector_payment_data; + DROP COLUMN connector_payment_data, + DROP COLUMN connector_token_details; ALTER TABLE merchant_connector_account ALTER COLUMN payment_methods_enabled TYPE JSON [ ]; diff --git a/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql b/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql index faebb36cdf9..3926a02afa6 100644 --- a/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql +++ b/v2_migrations/2024-08-28-081721_add_v2_columns/up.sql @@ -51,7 +51,8 @@ ADD COLUMN payment_method_type_v2 VARCHAR, ADD COLUMN tax_on_surcharge BIGINT, ADD COLUMN payment_method_billing_address BYTEA, ADD COLUMN redirection_data JSONB, - ADD COLUMN connector_payment_data VARCHAR(512); + ADD COLUMN connector_payment_data VARCHAR(512), + ADD COLUMN connector_token_details JSONB; -- Change the type of the column from JSON to JSONB ALTER TABLE merchant_connector_account diff --git a/v2_migrations/2024-10-08-081847_drop_v1_columns/down.sql b/v2_migrations/2024-11-08-081847_drop_v1_columns/down.sql similarity index 97% rename from v2_migrations/2024-10-08-081847_drop_v1_columns/down.sql rename to v2_migrations/2024-11-08-081847_drop_v1_columns/down.sql index 64cbd2233ea..2dcd3a16a6c 100644 --- a/v2_migrations/2024-10-08-081847_drop_v1_columns/down.sql +++ b/v2_migrations/2024-11-08-081847_drop_v1_columns/down.sql @@ -88,7 +88,8 @@ ADD COLUMN IF NOT EXISTS attempt_id VARCHAR(64) NOT NULL, ADD COLUMN straight_through_algorithm JSONB, ADD COLUMN confirm BOOLEAN, ADD COLUMN authentication_data JSONB, - ADD COLUMN payment_method_billing_address_id VARCHAR(64); + ADD COLUMN payment_method_billing_address_id VARCHAR(64), + ADD COLUMN connector_mandate_detail JSONB; -- Create the index which was dropped because of dropping the column CREATE INDEX payment_attempt_connector_transaction_id_merchant_id_index ON payment_attempt (connector_transaction_id, merchant_id); diff --git a/v2_migrations/2024-10-08-081847_drop_v1_columns/up.sql b/v2_migrations/2024-11-08-081847_drop_v1_columns/up.sql similarity index 97% rename from v2_migrations/2024-10-08-081847_drop_v1_columns/up.sql rename to v2_migrations/2024-11-08-081847_drop_v1_columns/up.sql index 276bea10bd5..65eb29c474f 100644 --- a/v2_migrations/2024-10-08-081847_drop_v1_columns/up.sql +++ b/v2_migrations/2024-11-08-081847_drop_v1_columns/up.sql @@ -86,4 +86,5 @@ ALTER TABLE payment_attempt DROP COLUMN attempt_id, DROP COLUMN straight_through_algorithm, DROP COLUMN confirm, DROP COLUMN authentication_data, - DROP COLUMN payment_method_billing_address_id; + DROP COLUMN payment_method_billing_address_id, + DROP COLUMN connector_mandate_detail; From 190095977819efac42da5483bfdae6420a7a402c Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:07:46 +0530 Subject: [PATCH 24/46] feat(core): Implement 3ds decision manger for V2 (#7022) --- .github/CODEOWNERS | 1 + Cargo.lock | 3 + crates/api_models/src/conditional_configs.rs | 96 ++++--------------- crates/common_types/Cargo.toml | 2 + crates/common_types/src/payments.rs | 89 +++++++++++++---- crates/diesel_models/src/business_profile.rs | 6 ++ crates/diesel_models/src/schema_v2.rs | 1 + crates/euclid/src/dssa/types.rs | 24 ++++- crates/euclid_wasm/Cargo.toml | 1 + crates/euclid_wasm/src/lib.rs | 4 +- .../src/business_profile.rs | 60 ++++++++++++ crates/router/src/core/admin.rs | 2 + crates/router/src/core/conditional_config.rs | 85 ++++++++++++++-- crates/router/src/core/payments.rs | 4 +- .../src/core/payments/conditional_configs.rs | 15 +-- .../conditional_configs/transformers.rs | 22 ----- crates/router/src/core/routing/helpers.rs | 11 --- crates/router/src/routes/app.rs | 7 +- crates/router/src/routes/routing.rs | 74 +++++++++++++- .../src/services/authorization/permissions.rs | 2 +- .../down.sql | 3 + .../up.sql | 3 + 22 files changed, 362 insertions(+), 153 deletions(-) delete mode 100644 crates/router/src/core/payments/conditional_configs/transformers.rs create mode 100644 v2_migrations/2025-01-13-181842_add_three_ds_decision_manager_config_in_profile/down.sql create mode 100644 v2_migrations/2025-01-13-181842_add_three_ds_decision_manager_config_in_profile/up.sql diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index dc4010d0fd0..6ad86dd4ccc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -18,6 +18,7 @@ crates/router/src/services/ @juspay/hyperswitch-framework crates/router/src/db/ @juspay/hyperswitch-framework crates/router/src/routes/ @juspay/hyperswitch-framework migrations/ @juspay/hyperswitch-framework +v2_migrations/ @juspay/hyperswitch-framework api-reference/ @juspay/hyperswitch-framework api-reference-v2/ @juspay/hyperswitch-framework Cargo.toml @juspay/hyperswitch-framework diff --git a/Cargo.lock b/Cargo.lock index 4737fff7aa2..9e39e50ff52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2039,8 +2039,10 @@ dependencies = [ "common_enums", "common_utils", "diesel", + "euclid", "serde", "serde_json", + "strum 0.26.3", "utoipa", ] @@ -3067,6 +3069,7 @@ version = "0.1.0" dependencies = [ "api_models", "common_enums", + "common_types", "connector_configs", "currency_conversion", "euclid", diff --git a/crates/api_models/src/conditional_configs.rs b/crates/api_models/src/conditional_configs.rs index 3aed34e47a7..ac7170bbdff 100644 --- a/crates/api_models/src/conditional_configs.rs +++ b/crates/api_models/src/conditional_configs.rs @@ -1,82 +1,10 @@ use common_utils::events; -use euclid::{ - dssa::types::EuclidAnalysable, - enums, - frontend::{ - ast::Program, - dir::{DirKeyKind, DirValue, EuclidDirFilter}, - }, - types::Metadata, -}; -use serde::{Deserialize, Serialize}; - -#[derive( - Clone, - Debug, - Hash, - PartialEq, - Eq, - strum::Display, - strum::VariantNames, - strum::EnumIter, - strum::EnumString, - Serialize, - Deserialize, -)] -#[serde(rename_all = "snake_case")] -#[strum(serialize_all = "snake_case")] -pub enum AuthenticationType { - ThreeDs, - NoThreeDs, -} -impl AuthenticationType { - pub fn to_dir_value(&self) -> DirValue { - match self { - Self::ThreeDs => DirValue::AuthenticationType(enums::AuthenticationType::ThreeDs), - Self::NoThreeDs => DirValue::AuthenticationType(enums::AuthenticationType::NoThreeDs), - } - } -} - -impl EuclidAnalysable for AuthenticationType { - fn get_dir_value_for_analysis(&self, rule_name: String) -> Vec<(DirValue, Metadata)> { - let auth = self.to_string(); - - vec![( - self.to_dir_value(), - std::collections::HashMap::from_iter([( - "AUTHENTICATION_TYPE".to_string(), - serde_json::json!({ - "rule_name":rule_name, - "Authentication_type": auth, - }), - )]), - )] - } -} - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct ConditionalConfigs { - pub override_3ds: Option, -} -impl EuclidDirFilter for ConditionalConfigs { - const ALLOWED: &'static [DirKeyKind] = &[ - DirKeyKind::PaymentMethod, - DirKeyKind::CardType, - DirKeyKind::CardNetwork, - DirKeyKind::MetaData, - DirKeyKind::PaymentAmount, - DirKeyKind::PaymentCurrency, - DirKeyKind::CaptureMethod, - DirKeyKind::BillingCountry, - DirKeyKind::BusinessCountry, - ]; -} +use euclid::frontend::ast::Program; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct DecisionManagerRecord { pub name: String, - pub program: Program, + pub program: Program, pub created_at: i64, pub modified_at: i64, } @@ -89,12 +17,14 @@ impl events::ApiEventMetric for DecisionManagerRecord { #[serde(deny_unknown_fields)] pub struct ConditionalConfigReq { pub name: Option, - pub algorithm: Option>, + pub algorithm: Option>, } + +#[cfg(feature = "v1")] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct DecisionManagerRequest { pub name: Option, - pub program: Option>, + pub program: Option>, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -111,3 +41,17 @@ impl events::ApiEventMetric for DecisionManager { } pub type DecisionManagerResponse = DecisionManagerRecord; + +#[cfg(feature = "v2")] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct DecisionManagerRequest { + pub name: String, + pub program: Program, +} + +#[cfg(feature = "v2")] +impl events::ApiEventMetric for DecisionManagerRequest { + fn get_api_event_type(&self) -> Option { + Some(events::ApiEventsType::Routing) + } +} diff --git a/crates/common_types/Cargo.toml b/crates/common_types/Cargo.toml index 33dd799f0f6..3f49f546a71 100644 --- a/crates/common_types/Cargo.toml +++ b/crates/common_types/Cargo.toml @@ -15,10 +15,12 @@ v2 = ["common_utils/v2"] diesel = "2.2.3" serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.115" +strum = { version = "0.26", features = ["derive"] } utoipa = { version = "4.2.0", features = ["preserve_order", "preserve_path_order"] } common_enums = { version = "0.1.0", path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils"} +euclid = { version = "0.1.0", path = "../euclid" } [lints] workspace = true diff --git a/crates/common_types/src/payments.rs b/crates/common_types/src/payments.rs index 08075605628..ea42fbf7a41 100644 --- a/crates/common_types/src/payments.rs +++ b/crates/common_types/src/payments.rs @@ -3,8 +3,12 @@ use std::collections::HashMap; use common_enums::enums; -use common_utils::{errors, impl_to_sql_from_sql_json, types::MinorUnit}; +use common_utils::{errors, events, impl_to_sql_from_sql_json, types::MinorUnit}; use diesel::{sql_types::Jsonb, AsExpression, FromSqlRow}; +use euclid::frontend::{ + ast::Program, + dir::{DirKeyKind, EuclidDirFilter}, +}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -21,6 +25,31 @@ pub enum SplitPaymentsRequest { } impl_to_sql_from_sql_json!(SplitPaymentsRequest); +#[derive( + Serialize, Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +#[serde(deny_unknown_fields)] +/// Hashmap to store mca_id's with product names +pub struct AuthenticationConnectorAccountMap( + HashMap, +); +impl_to_sql_from_sql_json!(AuthenticationConnectorAccountMap); + +impl AuthenticationConnectorAccountMap { + /// fn to get click to pay connector_account_id + pub fn get_click_to_pay_connector_account_id( + &self, + ) -> Result { + self.0 + .get(&enums::AuthenticationProduct::ClickToPay) + .ok_or(errors::ValidationError::MissingRequiredField { + field_name: "authentication_product_id.click_to_pay".to_string(), + }) + .cloned() + } +} + #[derive( Serialize, Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, ToSchema, )] @@ -42,26 +71,48 @@ pub struct StripeSplitPaymentRequest { impl_to_sql_from_sql_json!(StripeSplitPaymentRequest); #[derive( - Serialize, Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, ToSchema, + Serialize, Default, Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, ToSchema, )] #[diesel(sql_type = Jsonb)] -#[serde(deny_unknown_fields)] -/// Hashmap to store mca_id's with product names -pub struct AuthenticationConnectorAccountMap( - HashMap, -); -impl_to_sql_from_sql_json!(AuthenticationConnectorAccountMap); +/// ConditionalConfigs +pub struct ConditionalConfigs { + /// Override 3DS + pub override_3ds: Option, +} +impl EuclidDirFilter for ConditionalConfigs { + const ALLOWED: &'static [DirKeyKind] = &[ + DirKeyKind::PaymentMethod, + DirKeyKind::CardType, + DirKeyKind::CardNetwork, + DirKeyKind::MetaData, + DirKeyKind::PaymentAmount, + DirKeyKind::PaymentCurrency, + DirKeyKind::CaptureMethod, + DirKeyKind::BillingCountry, + DirKeyKind::BusinessCountry, + ]; +} -impl AuthenticationConnectorAccountMap { - /// fn to get click to pay connector_account_id - pub fn get_click_to_pay_connector_account_id( - &self, - ) -> Result { - self.0 - .get(&enums::AuthenticationProduct::ClickToPay) - .ok_or(errors::ValidationError::MissingRequiredField { - field_name: "authentication_product_id.click_to_pay".to_string(), - }) - .cloned() +impl_to_sql_from_sql_json!(ConditionalConfigs); + +#[derive(Serialize, Deserialize, Debug, Clone, FromSqlRow, AsExpression, ToSchema)] +#[diesel(sql_type = Jsonb)] +/// DecisionManagerRecord +pub struct DecisionManagerRecord { + /// Name of the Decision Manager + pub name: String, + /// Program to be executed + pub program: Program, + /// Created at timestamp + pub created_at: i64, +} + +impl events::ApiEventMetric for DecisionManagerRecord { + fn get_api_event_type(&self) -> Option { + Some(events::ApiEventsType::Routing) } } +impl_to_sql_from_sql_json!(DecisionManagerRecord); + +/// DecisionManagerResponse +pub type DecisionManagerResponse = DecisionManagerRecord; diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index 06aa21fe9d3..2cbd7deb426 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -310,6 +310,7 @@ pub struct Profile { pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, + pub three_ds_decision_manager_config: Option, } impl Profile { @@ -371,6 +372,7 @@ pub struct ProfileNew { pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, + pub three_ds_decision_manager_config: Option, } #[cfg(feature = "v2")] @@ -416,6 +418,7 @@ pub struct ProfileUpdateInternal { pub is_click_to_pay_enabled: Option, pub authentication_product_ids: Option, + pub three_ds_decision_manager_config: Option, } #[cfg(feature = "v2")] @@ -459,6 +462,7 @@ impl ProfileUpdateInternal { max_auto_retries_enabled, is_click_to_pay_enabled, authentication_product_ids, + three_ds_decision_manager_config, } = self; Profile { id: source.id, @@ -527,6 +531,8 @@ impl ProfileUpdateInternal { .unwrap_or(source.is_click_to_pay_enabled), authentication_product_ids: authentication_product_ids .or(source.authentication_product_ids), + three_ds_decision_manager_config: three_ds_decision_manager_config + .or(source.three_ds_decision_manager_config), } } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 9b1c3aa7d45..5bd3195f43d 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -224,6 +224,7 @@ diesel::table! { max_auto_retries_enabled -> Nullable, is_click_to_pay_enabled -> Bool, authentication_product_ids -> Nullable, + three_ds_decision_manager_config -> Nullable, } } diff --git a/crates/euclid/src/dssa/types.rs b/crates/euclid/src/dssa/types.rs index f8340c31509..54e1820f0e3 100644 --- a/crates/euclid/src/dssa/types.rs +++ b/crates/euclid/src/dssa/types.rs @@ -1,4 +1,4 @@ -use std::fmt; +use std::{collections::HashMap, fmt}; use serde::Serialize; @@ -159,3 +159,25 @@ pub enum ValueType { EnumVariants(Vec), Number, } + +impl EuclidAnalysable for common_enums::AuthenticationType { + fn get_dir_value_for_analysis(&self, rule_name: String) -> Vec<(dir::DirValue, Metadata)> { + let auth = self.to_string(); + + let dir_value = match self { + Self::ThreeDs => dir::DirValue::AuthenticationType(Self::ThreeDs), + Self::NoThreeDs => dir::DirValue::AuthenticationType(Self::NoThreeDs), + }; + + vec![( + dir_value, + HashMap::from_iter([( + "AUTHENTICATION_TYPE".to_string(), + serde_json::json!({ + "rule_name": rule_name, + "Authentication_type": auth, + }), + )]), + )] + } +} diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml index 4f967df34ad..9c22ed62312 100644 --- a/crates/euclid_wasm/Cargo.toml +++ b/crates/euclid_wasm/Cargo.toml @@ -22,6 +22,7 @@ v2 = [] [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } common_enums = { version = "0.1.0", path = "../common_enums" } +common_types = { version = "0.1.0", path = "../common_types" } connector_configs = { version = "0.1.0", path = "../connector_configs" } currency_conversion = { version = "0.1.0", path = "../currency_conversion" } euclid = { version = "0.1.0", path = "../euclid", features = [] } diff --git a/crates/euclid_wasm/src/lib.rs b/crates/euclid_wasm/src/lib.rs index b299ced68b0..f977fc45541 100644 --- a/crates/euclid_wasm/src/lib.rs +++ b/crates/euclid_wasm/src/lib.rs @@ -7,7 +7,7 @@ use std::{ }; use api_models::{ - conditional_configs::ConditionalConfigs, enums as api_model_enums, routing::ConnectorSelection, + enums as api_model_enums, routing::ConnectorSelection, surcharge_decision_configs::SurchargeDecisionConfigs, }; use common_enums::RoutableConnectors; @@ -221,7 +221,7 @@ pub fn get_key_type(key: &str) -> Result { #[wasm_bindgen(js_name = getThreeDsKeys)] pub fn get_three_ds_keys() -> JsResult { - let keys = ::ALLOWED; + let keys = ::ALLOWED; Ok(serde_wasm_bindgen::to_value(keys)?) } diff --git a/crates/hyperswitch_domain_models/src/business_profile.rs b/crates/hyperswitch_domain_models/src/business_profile.rs index 47c505e8ca2..3e49a596d88 100644 --- a/crates/hyperswitch_domain_models/src/business_profile.rs +++ b/crates/hyperswitch_domain_models/src/business_profile.rs @@ -734,6 +734,7 @@ pub struct Profile { pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, + pub three_ds_decision_manager_config: Option, } #[cfg(feature = "v2")] @@ -777,6 +778,7 @@ pub struct ProfileSetter { pub is_click_to_pay_enabled: bool, pub authentication_product_ids: Option, + pub three_ds_decision_manager_config: Option, } #[cfg(feature = "v2")] @@ -826,6 +828,7 @@ impl From for Profile { is_network_tokenization_enabled: value.is_network_tokenization_enabled, is_click_to_pay_enabled: value.is_click_to_pay_enabled, authentication_product_ids: value.authentication_product_ids, + three_ds_decision_manager_config: value.three_ds_decision_manager_config, } } } @@ -879,6 +882,7 @@ pub struct ProfileGeneralUpdate { pub is_click_to_pay_enabled: Option, pub authentication_product_ids: Option, + pub three_ds_decision_manager_config: Option, } #[cfg(feature = "v2")] @@ -904,6 +908,9 @@ pub enum ProfileUpdate { CollectCvvDuringPaymentUpdate { should_collect_cvv_during_payment: bool, }, + DecisionManagerRecordUpdate { + three_ds_decision_manager_config: common_types::payments::DecisionManagerRecord, + }, } #[cfg(feature = "v2")] @@ -939,6 +946,7 @@ impl From for ProfileUpdateInternal { is_network_tokenization_enabled, is_click_to_pay_enabled, authentication_product_ids, + three_ds_decision_manager_config, } = *update; Self { profile_name, @@ -979,6 +987,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids, + three_ds_decision_manager_config, } } ProfileUpdate::RoutingAlgorithmUpdate { @@ -1022,6 +1031,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, + three_ds_decision_manager_config: None, }, ProfileUpdate::ExtendedCardInfoUpdate { is_extended_card_info_enabled, @@ -1063,6 +1073,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, + three_ds_decision_manager_config: None, }, ProfileUpdate::ConnectorAgnosticMitUpdate { is_connector_agnostic_mit_enabled, @@ -1104,6 +1115,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, + three_ds_decision_manager_config: None, }, ProfileUpdate::DefaultRoutingFallbackUpdate { default_fallback_routing, @@ -1145,6 +1157,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, + three_ds_decision_manager_config: None, }, ProfileUpdate::NetworkTokenizationUpdate { is_network_tokenization_enabled, @@ -1186,6 +1199,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, + three_ds_decision_manager_config: None, }, ProfileUpdate::CollectCvvDuringPaymentUpdate { should_collect_cvv_during_payment, @@ -1227,6 +1241,49 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, + three_ds_decision_manager_config: None, + }, + ProfileUpdate::DecisionManagerRecordUpdate { + three_ds_decision_manager_config, + } => Self { + profile_name: None, + modified_at: now, + return_url: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + webhook_details: None, + metadata: None, + is_recon_enabled: None, + applepay_verified_domains: None, + payment_link_config: None, + session_expiry: None, + authentication_connector_details: None, + payout_link_config: None, + is_extended_card_info_enabled: None, + extended_card_info_config: None, + is_connector_agnostic_mit_enabled: None, + use_billing_as_payment_method_billing: None, + collect_shipping_details_from_wallet_connector: None, + collect_billing_details_from_wallet_connector: None, + outgoing_webhook_custom_http_headers: None, + always_collect_billing_details_from_wallet_connector: None, + always_collect_shipping_details_from_wallet_connector: None, + routing_algorithm_id: None, + payout_routing_algorithm_id: None, + order_fulfillment_time: None, + order_fulfillment_time_origin: None, + frm_routing_algorithm_id: None, + default_fallback_routing: None, + should_collect_cvv_during_payment: None, + tax_connector_id: None, + is_tax_connector_enabled: None, + is_network_tokenization_enabled: None, + is_auto_retries_enabled: None, + max_auto_retries_enabled: None, + is_click_to_pay_enabled: None, + authentication_product_ids: None, + three_ds_decision_manager_config: Some(three_ds_decision_manager_config), }, } } @@ -1288,6 +1345,7 @@ impl super::behaviour::Conversion for Profile { max_auto_retries_enabled: None, is_click_to_pay_enabled: self.is_click_to_pay_enabled, authentication_product_ids: self.authentication_product_ids, + three_ds_decision_manager_config: self.three_ds_decision_manager_config, }) } @@ -1358,6 +1416,7 @@ impl super::behaviour::Conversion for Profile { is_network_tokenization_enabled: item.is_network_tokenization_enabled, is_click_to_pay_enabled: item.is_click_to_pay_enabled, authentication_product_ids: item.authentication_product_ids, + three_ds_decision_manager_config: item.three_ds_decision_manager_config, }) } .await @@ -1415,6 +1474,7 @@ impl super::behaviour::Conversion for Profile { max_auto_retries_enabled: None, is_click_to_pay_enabled: self.is_click_to_pay_enabled, authentication_product_ids: self.authentication_product_ids, + three_ds_decision_manager_config: self.three_ds_decision_manager_config, }) } } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 356b4a31cc6..3ec9d76397b 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -3798,6 +3798,7 @@ impl ProfileCreateBridge for api::ProfileCreate { is_network_tokenization_enabled: self.is_network_tokenization_enabled, is_click_to_pay_enabled: self.is_click_to_pay_enabled, authentication_product_ids: self.authentication_product_ids, + three_ds_decision_manager_config: None, })) } } @@ -4147,6 +4148,7 @@ impl ProfileUpdateBridge for api::ProfileUpdate { is_network_tokenization_enabled: self.is_network_tokenization_enabled, is_click_to_pay_enabled: self.is_click_to_pay_enabled, authentication_product_ids: self.authentication_product_ids, + three_ds_decision_manager_config: None, }, ))) } diff --git a/crates/router/src/core/conditional_config.rs b/crates/router/src/core/conditional_config.rs index 9ef409268fb..66ad750bbbe 100644 --- a/crates/router/src/core/conditional_config.rs +++ b/crates/router/src/core/conditional_config.rs @@ -1,7 +1,11 @@ +#[cfg(feature = "v2")] +use api_models::conditional_configs::DecisionManagerRequest; use api_models::conditional_configs::{ DecisionManager, DecisionManagerRecord, DecisionManagerResponse, }; use common_utils::ext_traits::StringExt; +#[cfg(feature = "v2")] +use common_utils::types::keymanager::KeyManagerState; use error_stack::ResultExt; use crate::{ @@ -10,15 +14,57 @@ use crate::{ services::api as service_api, types::domain, }; - #[cfg(feature = "v2")] pub async fn upsert_conditional_config( - _state: SessionState, - _key_store: domain::MerchantKeyStore, - _merchant_account: domain::MerchantAccount, - _request: DecisionManager, -) -> RouterResponse { - todo!() + state: SessionState, + key_store: domain::MerchantKeyStore, + request: DecisionManagerRequest, + profile: domain::Profile, +) -> RouterResponse { + use common_utils::ext_traits::OptionExt; + + let key_manager_state: &KeyManagerState = &(&state).into(); + let db = &*state.store; + let name = request.name; + let program = request.program; + let timestamp = common_utils::date_time::now_unix_timestamp(); + + euclid::frontend::ast::lowering::lower_program(program.clone()) + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Invalid Request Data".to_string(), + }) + .attach_printable("The Request has an Invalid Comparison")?; + + let decision_manager_record = common_types::payments::DecisionManagerRecord { + name, + program, + created_at: timestamp, + }; + + let business_profile_update = domain::ProfileUpdate::DecisionManagerRecordUpdate { + three_ds_decision_manager_config: decision_manager_record, + }; + let updated_profile = db + .update_profile_by_profile_id( + key_manager_state, + &key_store, + profile, + business_profile_update, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update decision manager record in business profile")?; + + Ok(service_api::ApplicationResponse::Json( + updated_profile + .three_ds_decision_manager_config + .clone() + .get_required_value("three_ds_decision_manager_config") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "Failed to get updated decision manager record in business profile", + )?, + )) } #[cfg(feature = "v1")] @@ -204,6 +250,7 @@ pub async fn delete_conditional_config( Ok(service_api::ApplicationResponse::StatusOk) } +#[cfg(feature = "v1")] pub async fn retrieve_conditional_config( state: SessionState, merchant_account: domain::MerchantAccount, @@ -229,3 +276,27 @@ pub async fn retrieve_conditional_config( }; Ok(service_api::ApplicationResponse::Json(response)) } + +#[cfg(feature = "v2")] +pub async fn retrieve_conditional_config( + state: SessionState, + key_store: domain::MerchantKeyStore, + profile: domain::Profile, +) -> RouterResponse { + let db = state.store.as_ref(); + let key_manager_state: &KeyManagerState = &(&state).into(); + let profile_id = profile.get_id(); + + let record = profile + .three_ds_decision_manager_config + .clone() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("The Conditional Config Record was not found")?; + + let response = common_types::payments::DecisionManagerRecord { + name: record.name, + program: record.program, + created_at: record.created_at, + }; + Ok(service_api::ApplicationResponse::Json(response)) +} diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 47d2024834e..33540cf0b4b 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -109,7 +109,7 @@ use crate::{ api::{self, ConnectorCallType, ConnectorCommon}, domain, storage::{self, enums as storage_enums, payment_attempt::PaymentAttemptExt}, - transformers::{ForeignInto, ForeignTryInto}, + transformers::ForeignTryInto, }, utils::{ self, add_apple_pay_flow_metrics, add_connector_http_status_code_metrics, Encode, @@ -1145,7 +1145,7 @@ where Ok(payment_dsl_data .payment_attempt .authentication_type - .or(output.override_3ds.map(ForeignInto::foreign_into)) + .or(output.override_3ds) .or(Some(storage_enums::AuthenticationType::NoThreeDs))) } diff --git a/crates/router/src/core/payments/conditional_configs.rs b/crates/router/src/core/payments/conditional_configs.rs index cc6cc9cd743..d511c0fd6a7 100644 --- a/crates/router/src/core/payments/conditional_configs.rs +++ b/crates/router/src/core/payments/conditional_configs.rs @@ -1,9 +1,4 @@ -mod transformers; - -use api_models::{ - conditional_configs::{ConditionalConfigs, DecisionManagerRecord}, - routing, -}; +use api_models::{conditional_configs::DecisionManagerRecord, routing}; use common_utils::ext_traits::StringExt; use error_stack::ResultExt; use euclid::backend::{self, inputs as dsl_inputs, EuclidBackend}; @@ -23,11 +18,11 @@ pub async fn perform_decision_management( algorithm_ref: routing::RoutingAlgorithmRef, merchant_id: &common_utils::id_type::MerchantId, payment_data: &core_routing::PaymentsDslInput<'_>, -) -> ConditionalConfigResult { +) -> ConditionalConfigResult { let algorithm_id = if let Some(id) = algorithm_ref.config_algo_id { id } else { - return Ok(ConditionalConfigs::default()); + return Ok(common_types::payments::ConditionalConfigs::default()); }; let db = &*state.store; @@ -64,8 +59,8 @@ pub async fn perform_decision_management( pub fn execute_dsl_and_get_conditional_config( backend_input: dsl_inputs::BackendInput, - interpreter: &backend::VirInterpreterBackend, -) -> ConditionalConfigResult { + interpreter: &backend::VirInterpreterBackend, +) -> ConditionalConfigResult { let routing_output = interpreter .execute(backend_input) .map(|out| out.connector_selection) diff --git a/crates/router/src/core/payments/conditional_configs/transformers.rs b/crates/router/src/core/payments/conditional_configs/transformers.rs deleted file mode 100644 index 023bd65dcf4..00000000000 --- a/crates/router/src/core/payments/conditional_configs/transformers.rs +++ /dev/null @@ -1,22 +0,0 @@ -use api_models::{self, conditional_configs}; -use diesel_models::enums as storage_enums; -use euclid::enums as dsl_enums; - -use crate::types::transformers::ForeignFrom; -impl ForeignFrom for conditional_configs::AuthenticationType { - fn foreign_from(from: dsl_enums::AuthenticationType) -> Self { - match from { - dsl_enums::AuthenticationType::ThreeDs => Self::ThreeDs, - dsl_enums::AuthenticationType::NoThreeDs => Self::NoThreeDs, - } - } -} - -impl ForeignFrom for storage_enums::AuthenticationType { - fn foreign_from(from: conditional_configs::AuthenticationType) -> Self { - match from { - conditional_configs::AuthenticationType::ThreeDs => Self::ThreeDs, - conditional_configs::AuthenticationType::NoThreeDs => Self::NoThreeDs, - } - } -} diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index ca14e3e7e48..84de32483a8 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -197,17 +197,6 @@ pub async fn update_merchant_active_algorithm_ref( Ok(()) } -#[cfg(feature = "v2")] -pub async fn update_merchant_active_algorithm_ref( - _state: &SessionState, - _key_store: &domain::MerchantKeyStore, - _config_key: cache::CacheKind<'_>, - _algorithm_id: routing_types::RoutingAlgorithmRef, -) -> RouterResult<()> { - // TODO: handle updating the active routing algorithm for v2 in merchant account - todo!() -} - #[cfg(feature = "v1")] pub async fn update_profile_active_algorithm_ref( db: &dyn StorageInterface, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 17ab056e856..7f7ea767108 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1832,7 +1832,12 @@ impl Profile { &TransactionType::Payment, ) }, - ))), + ))) + .service( + web::resource("/decision") + .route(web::put().to(routing::upsert_decision_manager_config)) + .route(web::get().to(routing::retrieve_decision_manager_config)), + ), ) } } diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index 76852bf12ef..e7227e1f752 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -688,7 +688,7 @@ pub async fn retrieve_surcharge_decision_manager_config( .await } -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "v1"))] #[instrument(skip_all)] pub async fn upsert_decision_manager_config( state: web::Data, @@ -726,6 +726,44 @@ pub async fn upsert_decision_manager_config( .await } +#[cfg(all(feature = "olap", feature = "v2"))] +#[instrument(skip_all)] +pub async fn upsert_decision_manager_config( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let flow = Flow::DecisionManagerUpsertConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, update_decision, _| { + conditional_config::upsert_conditional_config( + state, + auth.key_store, + update_decision, + auth.profile, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuth { + permission: Permission::ProfileThreeDsDecisionManagerWrite, + }, + req.headers(), + ), + #[cfg(feature = "release")] + &auth::JWTAuth { + permission: Permission::ProfileThreeDsDecisionManagerWrite, + }, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(feature = "olap")] #[instrument(skip_all)] pub async fn delete_decision_manager_config( @@ -762,6 +800,40 @@ pub async fn delete_decision_manager_config( .await } +#[cfg(all(feature = "olap", feature = "v2"))] +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn retrieve_decision_manager_config( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + let flow = Flow::DecisionManagerRetrieveConfig; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + (), + |state, auth: auth::AuthenticationData, _, _| { + conditional_config::retrieve_conditional_config(state, auth.key_store, auth.profile) + }, + #[cfg(not(feature = "release"))] + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuth { + permission: Permission::ProfileThreeDsDecisionManagerWrite, + }, + req.headers(), + ), + #[cfg(feature = "release")] + &auth::JWTAuth { + permission: Permission::ProfileThreeDsDecisionManagerWrite, + }, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(all(feature = "olap", feature = "v1"))] #[cfg(feature = "olap")] #[instrument(skip_all)] pub async fn retrieve_decision_manager_config( diff --git a/crates/router/src/services/authorization/permissions.rs b/crates/router/src/services/authorization/permissions.rs index 04d3d436736..5396fff2897 100644 --- a/crates/router/src/services/authorization/permissions.rs +++ b/crates/router/src/services/authorization/permissions.rs @@ -45,7 +45,7 @@ generate_permissions! { }, ThreeDsDecisionManager: { scopes: [Read, Write], - entities: [Merchant] + entities: [Merchant, Profile] }, SurchargeDecisionManager: { scopes: [Read, Write], diff --git a/v2_migrations/2025-01-13-181842_add_three_ds_decision_manager_config_in_profile/down.sql b/v2_migrations/2025-01-13-181842_add_three_ds_decision_manager_config_in_profile/down.sql new file mode 100644 index 00000000000..a5295472f12 --- /dev/null +++ b/v2_migrations/2025-01-13-181842_add_three_ds_decision_manager_config_in_profile/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE business_profile +DROP COLUMN IF EXISTS three_ds_decision_manager_config; \ No newline at end of file diff --git a/v2_migrations/2025-01-13-181842_add_three_ds_decision_manager_config_in_profile/up.sql b/v2_migrations/2025-01-13-181842_add_three_ds_decision_manager_config_in_profile/up.sql new file mode 100644 index 00000000000..e329a39f1a9 --- /dev/null +++ b/v2_migrations/2025-01-13-181842_add_three_ds_decision_manager_config_in_profile/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE business_profile +ADD COLUMN IF NOT EXISTS three_ds_decision_manager_config jsonb; \ No newline at end of file From 698a0aa75af646107ac796f719b51e74530f11dc Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:08:18 +0530 Subject: [PATCH 25/46] fix(connector): [Authorizedotnet] fix deserialization error for Paypal while canceling payment (#7141) --- .../src/connector/authorizedotnet/transformers.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index 8e0af603a3d..c430191a1ec 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -1872,13 +1872,13 @@ pub struct PaypalPaymentConfirm { #[derive(Debug, Serialize, Deserialize)] pub struct Paypal { #[serde(rename = "payerID")] - payer_id: Secret, + payer_id: Option>, } #[derive(Debug, Serialize, Deserialize)] pub struct PaypalQueryParams { #[serde(rename = "PayerID")] - payer_id: Secret, + payer_id: Option>, } impl TryFrom<&AuthorizedotnetRouterData<&types::PaymentsCompleteAuthorizeRouterData>> @@ -1895,10 +1895,13 @@ impl TryFrom<&AuthorizedotnetRouterData<&types::PaymentsCompleteAuthorizeRouterD .as_ref() .and_then(|redirect_response| redirect_response.params.as_ref()) .ok_or(errors::ConnectorError::ResponseDeserializationFailed)?; - let payer_id: Secret = - serde_urlencoded::from_str::(params.peek()) - .change_context(errors::ConnectorError::ResponseDeserializationFailed)? - .payer_id; + + let query_params: PaypalQueryParams = serde_urlencoded::from_str(params.peek()) + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .attach_printable("Failed to parse connector response")?; + + let payer_id = query_params.payer_id; + let transaction_type = match item.router_data.request.capture_method { Some(enums::CaptureMethod::Manual) => Ok(TransactionType::ContinueAuthorization), Some(enums::CaptureMethod::SequentialAutomatic) From 8917235b4c1c606cba92539b9cb50449fc70474a Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:08:40 +0530 Subject: [PATCH 26/46] fix(core): Add payment_link_data in PaymentData for Psync (#7137) Co-authored-by: pranav-arjunan --- crates/router/src/core/payments.rs | 18 ++++++++++++++++++ .../core/payments/operations/payment_status.rs | 9 ++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 33540cf0b4b..dce89107afd 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -4843,6 +4843,24 @@ impl CustomerDetailsExt for CustomerDetails { } } +pub async fn get_payment_link_response_from_id( + state: &SessionState, + payment_link_id: &str, +) -> CustomResult { + 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)?; + + Ok(api_models::payments::PaymentLinkResponse { + link: payment_link_object.link_to_pay.clone(), + secure_link: payment_link_object.secure_link, + payment_link_id: payment_link_object.payment_link_id, + }) +} + #[cfg(feature = "v1")] pub fn if_not_create_change_operation<'a, Op, F>( status: storage_enums::IntentStatus, diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index dd28043ba2b..44f0c9172f9 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -467,6 +467,13 @@ async fn get_tracker_for_sync< }).await .transpose()?; + let payment_link_data = payment_intent + .payment_link_id + .as_ref() + .async_map(|id| crate::core::payments::get_payment_link_response_from_id(state, id)) + .await + .transpose()?; + let payment_data = PaymentData { flow: PhantomData, payment_intent, @@ -512,7 +519,7 @@ async fn get_tracker_for_sync< ephemeral_key: None, multiple_capture_data, redirect_response: None, - payment_link_data: None, + payment_link_data, surcharge_details: None, frm_message: frm_response, incremental_authorization_details: None, From d443a4cf1ee7bb9f5daa5147bd2854b3e4f4c76d Mon Sep 17 00:00:00 2001 From: Kashif Date: Wed, 5 Feb 2025 19:10:48 +0530 Subject: [PATCH 27/46] fix(connector): [worldpay] remove threeDS data from Authorize request for NTI flows (#7097) --- .../src/connectors/worldpay/requests.rs | 2 +- .../src/connectors/worldpay/transformers.rs | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs index 52ae2c4f16f..6e19c20edab 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/requests.rs @@ -32,7 +32,7 @@ pub struct Instruction { pub value: PaymentValue, #[serde(skip_serializing_if = "Option::is_none")] pub debt_repayment: Option, - #[serde(rename = "threeDS")] + #[serde(rename = "threeDS", skip_serializing_if = "Option::is_none")] pub three_ds: Option, /// For setting up mandates pub token_creation: Option, diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs index 6ae507790dd..e4656021a59 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs @@ -390,8 +390,14 @@ fn create_three_ds_request( router_data: &T, is_mandate_payment: bool, ) -> Result, error_stack::Report> { - match router_data.get_auth_type() { - enums::AuthenticationType::ThreeDs => { + match ( + router_data.get_auth_type(), + router_data.get_payment_method_data(), + ) { + // 3DS for NTI flow + (_, PaymentMethodData::CardDetailsForNetworkTransactionId(_)) => Ok(None), + // 3DS for regular payments + (enums::AuthenticationType::ThreeDs, _) => { let browser_info = router_data.get_browser_info().ok_or( errors::ConnectorError::MissingRequiredField { field_name: "browser_info", @@ -439,6 +445,7 @@ fn create_three_ds_request( }, })) } + // Non 3DS _ => Ok(None), } } From ce2485c3c77d86a2bce01d20c410ae11ac08c555 Mon Sep 17 00:00:00 2001 From: Sagnik Mitra <83326850+ImSagnik007@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:11:41 +0530 Subject: [PATCH 28/46] feat(connector): [INESPAY] Integrate Sepa Bank Debit (#6755) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 2 + api-reference/openapi_spec.json | 2 + config/development.toml | 3 + crates/common_enums/src/connector_enums.rs | 7 +- crates/connector_configs/src/connector.rs | 4 +- .../src/connectors/inespay.rs | 221 ++++++-- .../src/connectors/inespay/transformers.rs | 479 ++++++++++++++---- crates/router/src/core/admin.rs | 8 +- crates/router/src/types/api.rs | 6 +- crates/router/src/types/transformers.rs | 2 +- 10 files changed, 574 insertions(+), 160 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index ebb93fd8a11..7556a4e7765 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -6716,6 +6716,7 @@ "gocardless", "gpayments", "helcim", + "inespay", "iatapay", "itaubank", "jpmorgan", @@ -18734,6 +18735,7 @@ "gocardless", "helcim", "iatapay", + "inespay", "itaubank", "jpmorgan", "klarna", diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index bce5ac3c8e8..ea0a95c1d57 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -9298,6 +9298,7 @@ "gocardless", "gpayments", "helcim", + "inespay", "iatapay", "itaubank", "jpmorgan", @@ -23995,6 +23996,7 @@ "gocardless", "helcim", "iatapay", + "inespay", "itaubank", "jpmorgan", "klarna", diff --git a/config/development.toml b/config/development.toml index 2b81123d873..473c453d1c8 100644 --- a/config/development.toml +++ b/config/development.toml @@ -565,6 +565,9 @@ debit = { currency = "USD" } [pm_filters.fiuu] duit_now = { country = "MY", currency = "MYR" } +[pm_filters.inespay] +sepa = { currency = "EUR" } + [tokenization] stripe = { long_lived_token = false, payment_method = "wallet", payment_method_type = { type = "disable_only", list = "google_pay" } } checkout = { long_lived_token = false, payment_method = "wallet", apple_pay_pre_decrypt_flow = "network_tokenization" } diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index 288520722c3..28dbf33d97d 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -85,7 +85,7 @@ pub enum RoutableConnectors { Gocardless, Helcim, Iatapay, - // Inespay, + Inespay, Itaubank, Jpmorgan, Klarna, @@ -221,7 +221,7 @@ pub enum Connector { Gocardless, Gpayments, Helcim, - // Inespay, + Inespay, Iatapay, Itaubank, Jpmorgan, @@ -370,7 +370,7 @@ impl Connector { | Self::Gpayments | Self::Helcim | Self::Iatapay - // | Self::Inespay + | Self::Inespay | Self::Itaubank | Self::Jpmorgan | Self::Klarna @@ -540,6 +540,7 @@ impl From for Connector { RoutableConnectors::Plaid => Self::Plaid, RoutableConnectors::Zsl => Self::Zsl, RoutableConnectors::Xendit => Self::Xendit, + RoutableConnectors::Inespay => Self::Inespay, } } } diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index 791a0679ea3..3175bbde8f2 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -196,7 +196,7 @@ pub struct ConnectorConfig { pub gocardless: Option, pub gpayments: Option, pub helcim: Option, - // pub inespay: Option, + pub inespay: Option, pub jpmorgan: Option, pub klarna: Option, pub mifinity: Option, @@ -358,7 +358,7 @@ impl ConnectorConfig { Connector::Gocardless => Ok(connector_data.gocardless), Connector::Gpayments => Ok(connector_data.gpayments), Connector::Helcim => Ok(connector_data.helcim), - // Connector::Inespay => Ok(connector_data.inespay), + Connector::Inespay => Ok(connector_data.inespay), Connector::Jpmorgan => Ok(connector_data.jpmorgan), Connector::Klarna => Ok(connector_data.klarna), Connector::Mifinity => Ok(connector_data.mifinity), diff --git a/crates/hyperswitch_connectors/src/connectors/inespay.rs b/crates/hyperswitch_connectors/src/connectors/inespay.rs index 3d3ea346b78..41ae798ade3 100644 --- a/crates/hyperswitch_connectors/src/connectors/inespay.rs +++ b/crates/hyperswitch_connectors/src/connectors/inespay.rs @@ -1,12 +1,15 @@ pub mod transformers; +use base64::Engine; use common_utils::{ + consts::BASE64_ENGINE, + crypto, errors::CustomResult, - ext_traits::BytesExt, + ext_traits::{ByteSliceExt, BytesExt}, request::{Method, Request, RequestBuilder, RequestContent}, types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, }; -use error_stack::{report, ResultExt}; +use error_stack::ResultExt; use hyperswitch_domain_models::{ router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::{ @@ -36,7 +39,8 @@ use hyperswitch_interfaces::{ types::{self, Response}, webhooks, }; -use masking::{ExposeInterface, Mask}; +use masking::{ExposeInterface, Mask, Secret}; +use ring::hmac; use transformers as inespay; use crate::{constants::headers, types::ResponseRouterData, utils}; @@ -86,8 +90,8 @@ where headers::CONTENT_TYPE.to_string(), self.get_content_type().to_string().into(), )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); + let mut auth_headers = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut auth_headers); Ok(header) } } @@ -98,10 +102,7 @@ impl ConnectorCommon for Inespay { } fn get_currency_unit(&self) -> api::CurrencyUnit { - api::CurrencyUnit::Base - // TODO! Check connector documentation, on which unit they are processing the currency. - // If the connector accepts amount in lower unit ( i.e cents for USD) then return api::CurrencyUnit::Minor, - // if connector accepts amount in base unit (i.e dollars for USD) then return api::CurrencyUnit::Base + api::CurrencyUnit::Minor } fn common_get_content_type(&self) -> &'static str { @@ -118,10 +119,16 @@ impl ConnectorCommon for Inespay { ) -> CustomResult)>, errors::ConnectorError> { let auth = inespay::InespayAuthType::try_from(auth_type) .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - Ok(vec![( - headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), - )]) + Ok(vec![ + ( + headers::AUTHORIZATION.to_string(), + auth.authorization.expose().into_masked(), + ), + ( + headers::X_API_KEY.to_string(), + auth.api_key.expose().into_masked(), + ), + ]) } fn build_error_response( @@ -139,9 +146,9 @@ impl ConnectorCommon for Inespay { Ok(ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, + code: response.status, + message: response.status_desc, + reason: None, attempt_status: None, connector_transaction_id: None, }) @@ -176,9 +183,9 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}/payins/single/init", self.base_url(connectors))) } fn get_request_body( @@ -192,9 +199,19 @@ impl ConnectorIntegration { + let connector_router_data = inespay::InespayRouterData::from((amount, req)); + let connector_req = + inespay::InespayPaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + _ => Err(errors::ConnectorError::CurrencyNotSupported { + message: req.request.currency.to_string(), + connector: "Inespay", + } + .into()), + } } fn build_request( @@ -262,10 +279,20 @@ impl ConnectorIntegration for Ine fn get_url( &self, - _req: &PaymentsSyncRouterData, - _connectors: &Connectors, + req: &PaymentsSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_payment_id = req + .request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}{}{}", + self.base_url(connectors), + "/payins/single/", + connector_payment_id, + )) } fn build_request( @@ -289,7 +316,7 @@ impl ConnectorIntegration for Ine event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: inespay::InespayPaymentsResponse = res + let response: inespay::InespayPSyncResponse = res .response .parse_struct("inespay PaymentsSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -406,9 +433,9 @@ impl ConnectorIntegration for Inespay fn get_url( &self, _req: &RefundsRouterData, - _connectors: &Connectors, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}/refunds/init", self.base_url(connectors))) } fn get_request_body( @@ -452,9 +479,9 @@ impl ConnectorIntegration for Inespay event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult, errors::ConnectorError> { - let response: inespay::RefundResponse = res + let response: inespay::InespayRefundsResponse = res .response - .parse_struct("inespay RefundResponse") + .parse_struct("inespay InespayRefundsResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -489,10 +516,20 @@ impl ConnectorIntegration for Inespay { fn get_url( &self, - _req: &RefundSyncRouterData, - _connectors: &Connectors, + req: &RefundSyncRouterData, + connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + let connector_refund_id = req + .request + .connector_refund_id + .clone() + .ok_or(errors::ConnectorError::MissingConnectorRefundID)?; + Ok(format!( + "{}{}{}", + self.base_url(connectors), + "/refunds/", + connector_refund_id, + )) } fn build_request( @@ -519,7 +556,7 @@ impl ConnectorIntegration for Inespay { event_builder: Option<&mut ConnectorEvent>, res: Response, ) -> CustomResult { - let response: inespay::RefundResponse = res + let response: inespay::InespayRSyncResponse = res .response .parse_struct("inespay RefundSyncResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -541,27 +578,133 @@ impl ConnectorIntegration for Inespay { } } +fn get_webhook_body( + body: &[u8], +) -> CustomResult { + let notif_item: inespay::InespayWebhookEvent = + serde_urlencoded::from_bytes::(body) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + let encoded_data_return = notif_item.data_return; + let decoded_data_return = BASE64_ENGINE + .decode(encoded_data_return) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + let data_return: inespay::InespayWebhookEventData = decoded_data_return + .parse_struct("inespay InespayWebhookEventData") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(data_return) +} + #[async_trait::async_trait] impl webhooks::IncomingWebhook for Inespay { - fn get_webhook_object_reference_id( + fn get_webhook_source_verification_algorithm( &self, _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::HmacSha256)) + } + + fn get_webhook_source_verification_signature( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let notif_item = serde_urlencoded::from_bytes::(request.body) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + Ok(notif_item.signature_data_return.as_bytes().to_owned()) + } + + fn get_webhook_source_verification_message( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, + _merchant_id: &common_utils::id_type::MerchantId, + _connector_webhook_secrets: &api_models::webhooks::ConnectorWebhookSecrets, + ) -> CustomResult, errors::ConnectorError> { + let notif_item = serde_urlencoded::from_bytes::(request.body) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + Ok(notif_item.data_return.into_bytes()) + } + + async fn verify_webhook_source( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, + merchant_id: &common_utils::id_type::MerchantId, + connector_webhook_details: Option, + _connector_account_details: crypto::Encryptable>, + connector_label: &str, + ) -> CustomResult { + let connector_webhook_secrets = self + .get_webhook_source_verification_merchant_secret( + merchant_id, + connector_label, + connector_webhook_details, + ) + .await?; + let signature = + self.get_webhook_source_verification_signature(request, &connector_webhook_secrets)?; + + let message = self.get_webhook_source_verification_message( + request, + merchant_id, + &connector_webhook_secrets, + )?; + let secret = connector_webhook_secrets.secret; + + let signing_key = hmac::Key::new(hmac::HMAC_SHA256, &secret); + let signed_message = hmac::sign(&signing_key, &message); + let computed_signature = hex::encode(signed_message.as_ref()); + let payload_sign = BASE64_ENGINE.encode(computed_signature); + Ok(payload_sign.as_bytes().eq(&signature)) + } + + fn get_webhook_object_reference_id( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let data_return = get_webhook_body(request.body) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + match data_return { + inespay::InespayWebhookEventData::Payment(data) => { + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId( + data.single_payin_id, + ), + )) + } + inespay::InespayWebhookEventData::Refund(data) => { + Ok(api_models::webhooks::ObjectReferenceId::RefundId( + api_models::webhooks::RefundIdType::ConnectorRefundId(data.refund_id), + )) + } + } } fn get_webhook_event_type( &self, - _request: &webhooks::IncomingWebhookRequestDetails<'_>, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let data_return = get_webhook_body(request.body) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(api_models::webhooks::IncomingWebhookEvent::from( + data_return, + )) } fn get_webhook_resource_object( &self, - _request: &webhooks::IncomingWebhookRequestDetails<'_>, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError> { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let data_return = get_webhook_body(request.body) + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(match data_return { + inespay::InespayWebhookEventData::Payment(payment_webhook_data) => { + Box::new(payment_webhook_data) + } + inespay::InespayWebhookEventData::Refund(refund_webhook_data) => { + Box::new(refund_webhook_data) + } + }) } } diff --git a/crates/hyperswitch_connectors/src/connectors/inespay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/inespay/transformers.rs index 296d76546c8..2739aa66bee 100644 --- a/crates/hyperswitch_connectors/src/connectors/inespay/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/inespay/transformers.rs @@ -1,31 +1,33 @@ use common_enums::enums; -use common_utils::types::StringMinorUnit; +use common_utils::{ + request::Method, + types::{MinorUnit, StringMinorUnit, StringMinorUnitForConnector}, +}; +use error_stack::ResultExt; use hyperswitch_domain_models::{ - payment_method_data::PaymentMethodData, - router_data::{ConnectorAuthType, RouterData}, + payment_method_data::{BankDebitData, PaymentMethodData}, + router_data::{ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::refunds::{Execute, RSync}, router_request_types::ResponseId, - router_response_types::{PaymentsResponseData, RefundsResponseData}, + router_response_types::{PaymentsResponseData, RedirectForm, RefundsResponseData}, types::{PaymentsAuthorizeRouterData, RefundsRouterData}, }; use hyperswitch_interfaces::errors; use masking::Secret; use serde::{Deserialize, Serialize}; +use url::Url; use crate::{ types::{RefundsResponseRouterData, ResponseRouterData}, - utils::PaymentsAuthorizeRequestData, + utils::{self, PaymentsAuthorizeRequestData, RouterData as _}, }; - -//TODO: Fill the struct with respective fields pub struct InespayRouterData { - pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub amount: StringMinorUnit, pub router_data: T, } impl From<(StringMinorUnit, T)> for InespayRouterData { fn from((amount, item): (StringMinorUnit, T)) -> Self { - //Todo : use utils to convert the amount to the type of amount that a connector accepts Self { amount, router_data: item, @@ -33,20 +35,15 @@ impl From<(StringMinorUnit, T)> for InespayRouterData { } } -//TODO: Fill the struct with respective fields #[derive(Default, Debug, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct InespayPaymentsRequest { + description: String, amount: StringMinorUnit, - card: InespayCard, -} - -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct InespayCard { - number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, + reference: String, + debtor_account: Option>, + success_link_redirect: Option, + notif_url: Option, } impl TryFrom<&InespayRouterData<&PaymentsAuthorizeRouterData>> for InespayPaymentsRequest { @@ -55,17 +52,17 @@ impl TryFrom<&InespayRouterData<&PaymentsAuthorizeRouterData>> for InespayPaymen item: &InespayRouterData<&PaymentsAuthorizeRouterData>, ) -> Result { match item.router_data.request.payment_method_data.clone() { - PaymentMethodData::Card(req_card) => { - let card = InespayCard { - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.router_data.request.is_auto_capture()?, - }; + PaymentMethodData::BankDebit(BankDebitData::SepaBankDebit { iban, .. }) => { + let order_id = item.router_data.connector_request_reference_id.clone(); + let webhook_url = item.router_data.request.get_webhook_url()?; + let return_url = item.router_data.request.get_router_return_url()?; Ok(Self { + description: item.router_data.get_description()?, amount: item.amount.clone(), - card, + reference: order_id, + debtor_account: Some(iban), + success_link_redirect: Some(return_url), + notif_url: Some(webhook_url), }) } _ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()), @@ -73,156 +70,422 @@ impl TryFrom<&InespayRouterData<&PaymentsAuthorizeRouterData>> for InespayPaymen } } -//TODO: Fill the struct with respective fields -// Auth Struct pub struct InespayAuthType { pub(super) api_key: Secret, + pub authorization: Secret, } impl TryFrom<&ConnectorAuthType> for InespayAuthType { type Error = error_stack::Report; fn try_from(auth_type: &ConnectorAuthType) -> Result { match auth_type { - ConnectorAuthType::HeaderKey { api_key } => Ok(Self { + ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { api_key: api_key.to_owned(), + authorization: key1.to_owned(), }), _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), } } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum InespayPaymentStatus { - Succeeded, + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct InespayPaymentsResponseData { + status: String, + status_desc: String, + single_payin_id: String, + single_payin_link: String, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum InespayPaymentsResponse { + InespayPaymentsData(InespayPaymentsResponseData), + InespayPaymentsError(InespayErrorResponse), +} + +impl TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + let (status, response) = match item.response { + InespayPaymentsResponse::InespayPaymentsData(data) => { + let redirection_url = Url::parse(data.single_payin_link.as_str()) + .change_context(errors::ConnectorError::ParsingFailed)?; + let redirection_data = RedirectForm::from((redirection_url, Method::Get)); + + ( + common_enums::AttemptStatus::AuthenticationPending, + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + data.single_payin_id.clone(), + ), + redirection_data: Box::new(Some(redirection_data)), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }), + ) + } + InespayPaymentsResponse::InespayPaymentsError(data) => ( + common_enums::AttemptStatus::Failure, + Err(ErrorResponse { + code: data.status.clone(), + message: data.status_desc.clone(), + reason: Some(data.status_desc.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }), + ), + }; + Ok(Self { + status, + response, + ..item.data + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum InespayPSyncStatus { + Ok, + Created, + Opened, + BankSelected, + Initiated, + Pending, + Aborted, + Unfinished, + Rejected, + Cancelled, + PartiallyAccepted, Failed, - #[default] - Processing, + Settled, + PartRefunded, + Refunded, } -impl From for common_enums::AttemptStatus { - fn from(item: InespayPaymentStatus) -> Self { +impl From for common_enums::AttemptStatus { + fn from(item: InespayPSyncStatus) -> Self { match item { - InespayPaymentStatus::Succeeded => Self::Charged, - InespayPaymentStatus::Failed => Self::Failure, - InespayPaymentStatus::Processing => Self::Authorizing, + InespayPSyncStatus::Ok | InespayPSyncStatus::Settled => Self::Charged, + InespayPSyncStatus::Created + | InespayPSyncStatus::Opened + | InespayPSyncStatus::BankSelected + | InespayPSyncStatus::Initiated + | InespayPSyncStatus::Pending + | InespayPSyncStatus::Unfinished + | InespayPSyncStatus::PartiallyAccepted => Self::AuthenticationPending, + InespayPSyncStatus::Aborted + | InespayPSyncStatus::Rejected + | InespayPSyncStatus::Cancelled + | InespayPSyncStatus::Failed => Self::Failure, + InespayPSyncStatus::PartRefunded | InespayPSyncStatus::Refunded => Self::AutoRefunded, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct InespayPaymentsResponse { - status: InespayPaymentStatus, - id: String, +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct InespayPSyncResponseData { + cod_status: InespayPSyncStatus, + status_desc: String, + single_payin_id: String, + single_payin_link: String, } -impl TryFrom> +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum InespayPSyncResponse { + InespayPSyncData(InespayPSyncResponseData), + InespayPSyncWebhook(InespayPaymentWebhookData), + InespayPSyncError(InespayErrorResponse), +} + +impl TryFrom> for RouterData { type Error = error_stack::Report; fn try_from( - item: ResponseRouterData, + item: ResponseRouterData, ) -> Result { - Ok(Self { - status: common_enums::AttemptStatus::from(item.response.status), - response: Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(item.response.id), - redirection_data: Box::new(None), - mandate_reference: Box::new(None), - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charge_id: None, + match item.response { + InespayPSyncResponse::InespayPSyncData(data) => { + let redirection_url = Url::parse(data.single_payin_link.as_str()) + .change_context(errors::ConnectorError::ParsingFailed)?; + let redirection_data = RedirectForm::from((redirection_url, Method::Get)); + + Ok(Self { + status: common_enums::AttemptStatus::from(data.cod_status), + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + data.single_payin_id.clone(), + ), + redirection_data: Box::new(Some(redirection_data)), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }) + } + InespayPSyncResponse::InespayPSyncWebhook(data) => { + let status = enums::AttemptStatus::from(data.cod_status); + Ok(Self { + status, + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + data.single_payin_id.clone(), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }) + } + InespayPSyncResponse::InespayPSyncError(data) => Ok(Self { + response: Err(ErrorResponse { + code: data.status.clone(), + message: data.status_desc.clone(), + reason: Some(data.status_desc.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }), + ..item.data }), - ..item.data - }) + } } } -//TODO: Fill the struct with respective fields -// REFUND : -// Type definition for RefundRequest -#[derive(Default, Debug, Serialize)] +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct InespayRefundRequest { - pub amount: StringMinorUnit, + single_payin_id: String, + amount: Option, } impl TryFrom<&InespayRouterData<&RefundsRouterData>> for InespayRefundRequest { type Error = error_stack::Report; fn try_from(item: &InespayRouterData<&RefundsRouterData>) -> Result { + let amount = utils::convert_back_amount_to_minor_units( + &StringMinorUnitForConnector, + item.amount.to_owned(), + item.router_data.request.currency, + )?; Ok(Self { - amount: item.amount.to_owned(), + single_payin_id: item.router_data.request.connector_transaction_id.clone(), + amount: Some(amount), }) } } -// Type definition for Refund Response - -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, - Failed, +#[derive(Debug, Serialize, Default, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum InespayRSyncStatus { + Confirmed, #[default] - Processing, + Pending, + Rejected, + Denied, + Reversed, + Mistake, } -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { +impl From for enums::RefundStatus { + fn from(item: InespayRSyncStatus) -> Self { match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping + InespayRSyncStatus::Confirmed => Self::Success, + InespayRSyncStatus::Pending => Self::Pending, + InespayRSyncStatus::Rejected + | InespayRSyncStatus::Denied + | InespayRSyncStatus::Reversed + | InespayRSyncStatus::Mistake => Self::Failure, } } } -//TODO: Fill the struct with respective fields #[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse { - id: String, - status: RefundStatus, +#[serde(rename_all = "camelCase")] +pub struct RefundsData { + status: String, + status_desc: String, + refund_id: String, } -impl TryFrom> for RefundsRouterData { +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum InespayRefundsResponse { + InespayRefundsData(RefundsData), + InespayRefundsError(InespayErrorResponse), +} + +impl TryFrom> + for RefundsRouterData +{ type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: RefundsResponseRouterData, ) -> Result { - Ok(Self { - response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + match item.response { + InespayRefundsResponse::InespayRefundsData(data) => Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id: data.refund_id, + refund_status: enums::RefundStatus::Pending, + }), + ..item.data }), - ..item.data - }) + InespayRefundsResponse::InespayRefundsError(data) => Ok(Self { + response: Err(ErrorResponse { + code: data.status.clone(), + message: data.status_desc.clone(), + reason: Some(data.status_desc.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, + }), + ..item.data + }), + } } } -impl TryFrom> for RefundsRouterData { +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct InespayRSyncResponseData { + cod_status: InespayRSyncStatus, + status_desc: String, + refund_id: String, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum InespayRSyncResponse { + InespayRSyncData(InespayRSyncResponseData), + InespayRSyncWebhook(InespayRefundWebhookData), + InespayRSyncError(InespayErrorResponse), +} + +impl TryFrom> for RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: RefundsResponseRouterData, ) -> Result { - Ok(Self { - response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + let response = match item.response { + InespayRSyncResponse::InespayRSyncData(data) => Ok(RefundsResponseData { + connector_refund_id: data.refund_id, + refund_status: enums::RefundStatus::from(data.cod_status), + }), + InespayRSyncResponse::InespayRSyncWebhook(data) => Ok(RefundsResponseData { + connector_refund_id: data.refund_id, + refund_status: enums::RefundStatus::from(data.cod_status), + }), + InespayRSyncResponse::InespayRSyncError(data) => Err(ErrorResponse { + code: data.status.clone(), + message: data.status_desc.clone(), + reason: Some(data.status_desc.clone()), + attempt_status: None, + connector_transaction_id: None, + status_code: item.http_code, }), + }; + Ok(Self { + response, ..item.data }) } } -//TODO: Fill the struct with respective fields +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct InespayPaymentWebhookData { + pub single_payin_id: String, + pub cod_status: InespayPSyncStatus, + pub description: String, + pub amount: MinorUnit, + pub reference: String, + pub creditor_account: Secret, + pub debtor_name: Secret, + pub debtor_account: Secret, + pub custom_data: Option, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct InespayRefundWebhookData { + pub refund_id: String, + pub simple_payin_id: String, + pub cod_status: InespayRSyncStatus, + pub description: String, + pub amount: MinorUnit, + pub reference: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(untagged)] +pub enum InespayWebhookEventData { + Payment(InespayPaymentWebhookData), + Refund(InespayRefundWebhookData), +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct InespayWebhookEvent { + pub data_return: String, + pub signature_data_return: String, +} + #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct InespayErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, - pub reason: Option, + pub status: String, + pub status_desc: String, +} + +impl From for api_models::webhooks::IncomingWebhookEvent { + fn from(item: InespayWebhookEventData) -> Self { + match item { + InespayWebhookEventData::Payment(payment_data) => match payment_data.cod_status { + InespayPSyncStatus::Ok | InespayPSyncStatus::Settled => Self::PaymentIntentSuccess, + InespayPSyncStatus::Failed | InespayPSyncStatus::Rejected => { + Self::PaymentIntentFailure + } + InespayPSyncStatus::Created + | InespayPSyncStatus::Opened + | InespayPSyncStatus::BankSelected + | InespayPSyncStatus::Initiated + | InespayPSyncStatus::Pending + | InespayPSyncStatus::Unfinished + | InespayPSyncStatus::PartiallyAccepted => Self::PaymentIntentProcessing, + InespayPSyncStatus::Aborted + | InespayPSyncStatus::Cancelled + | InespayPSyncStatus::PartRefunded + | InespayPSyncStatus::Refunded => Self::EventNotSupported, + }, + InespayWebhookEventData::Refund(refund_data) => match refund_data.cod_status { + InespayRSyncStatus::Confirmed => Self::RefundSuccess, + InespayRSyncStatus::Rejected + | InespayRSyncStatus::Denied + | InespayRSyncStatus::Reversed + | InespayRSyncStatus::Mistake => Self::RefundFailure, + InespayRSyncStatus::Pending => Self::EventNotSupported, + }, + } + } } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 3ec9d76397b..7b4cbb282df 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1395,10 +1395,10 @@ impl ConnectorAuthTypeAndMetadataValidation<'_> { iatapay::transformers::IatapayAuthType::try_from(self.auth_type)?; Ok(()) } - // api_enums::Connector::Inespay => { - // inespay::transformers::InespayAuthType::try_from(self.auth_type)?; - // Ok(()) - // } + api_enums::Connector::Inespay => { + inespay::transformers::InespayAuthType::try_from(self.auth_type)?; + Ok(()) + } api_enums::Connector::Itaubank => { itaubank::transformers::ItaubankAuthType::try_from(self.auth_type)?; Ok(()) diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 114a877d26f..4dc5da6475f 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -442,9 +442,9 @@ impl ConnectorData { enums::Connector::Iatapay => { Ok(ConnectorEnum::Old(Box::new(connector::Iatapay::new()))) } - // enums::Connector::Inespay => { - // Ok(ConnectorEnum::Old(Box::new(connector::Inespay::new()))) - // } + enums::Connector::Inespay => { + Ok(ConnectorEnum::Old(Box::new(connector::Inespay::new()))) + } enums::Connector::Itaubank => { Ok(ConnectorEnum::Old(Box::new(connector::Itaubank::new()))) } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index c1d9b35be82..2a287a337c9 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -255,7 +255,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { } api_enums::Connector::Helcim => Self::Helcim, api_enums::Connector::Iatapay => Self::Iatapay, - // api_enums::Connector::Inespay => Self::Inespay, + api_enums::Connector::Inespay => Self::Inespay, api_enums::Connector::Itaubank => Self::Itaubank, api_enums::Connector::Jpmorgan => Self::Jpmorgan, api_enums::Connector::Klarna => Self::Klarna, From b54a3f9142388a3d870406c54fd1d314c7c7748d Mon Sep 17 00:00:00 2001 From: sweta-kumari-sharma <77436883+Sweta-Kumari-Sharma@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:12:35 +0530 Subject: [PATCH 29/46] FEAT: Add Support for Amazon Pay Redirect and Amazon Pay payment via Stripe (#7056) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 15 +++++++++++++ api-reference/openapi_spec.json | 15 +++++++++++++ crates/api_models/src/payments.rs | 7 ++++++ crates/common_enums/src/enums.rs | 1 + crates/common_enums/src/transformers.rs | 1 + .../connector_configs/toml/development.toml | 2 ++ crates/connector_configs/toml/production.toml | 2 ++ crates/connector_configs/toml/sandbox.toml | 2 ++ crates/euclid/src/frontend/dir/enums.rs | 1 + crates/euclid/src/frontend/dir/lowering.rs | 1 + .../euclid/src/frontend/dir/transformers.rs | 1 + .../src/connectors/airwallex/transformers.rs | 1 + .../connectors/bankofamerica/transformers.rs | 2 ++ .../src/connectors/bluesnap/transformers.rs | 1 + .../src/connectors/boku/transformers.rs | 1 + .../connectors/cybersource/transformers.rs | 2 ++ .../src/connectors/fiuu/transformers.rs | 1 + .../src/connectors/globepay/transformers.rs | 1 + .../connectors/multisafepay/transformers.rs | 3 +++ .../src/connectors/nexinets/transformers.rs | 1 + .../src/connectors/novalnet/transformers.rs | 2 ++ .../src/connectors/shift4/transformers.rs | 1 + .../src/connectors/square/transformers.rs | 1 + .../src/connectors/wellsfargo/transformers.rs | 2 ++ .../src/connectors/worldpay/transformers.rs | 1 + .../src/connectors/zen/transformers.rs | 1 + crates/hyperswitch_connectors/src/utils.rs | 2 ++ .../src/payment_method_data.rs | 9 +++++++- crates/kgraph_utils/src/mca.rs | 1 + crates/kgraph_utils/src/transformers.rs | 1 + crates/openapi/src/openapi.rs | 1 + crates/openapi/src/openapi_v2.rs | 1 + .../payment_connector_required_fields.rs | 15 +++++++++++++ .../router/src/connector/aci/transformers.rs | 1 + crates/router/src/connector/adyen.rs | 3 ++- .../src/connector/adyen/transformers.rs | 1 + .../connector/authorizedotnet/transformers.rs | 1 + .../src/connector/checkout/transformers.rs | 2 ++ crates/router/src/connector/klarna.rs | 2 ++ .../src/connector/mifinity/transformers.rs | 1 + .../router/src/connector/nmi/transformers.rs | 1 + .../router/src/connector/noon/transformers.rs | 1 + .../src/connector/nuvei/transformers.rs | 1 + .../src/connector/payme/transformers.rs | 1 + .../src/connector/paypal/transformers.rs | 2 ++ .../src/connector/stripe/transformers.rs | 22 +++++++++++++++++++ crates/router/src/connector/utils.rs | 2 ++ crates/router/src/core/payments/helpers.rs | 3 ++- crates/router/src/types/transformers.rs | 3 ++- 49 files changed, 142 insertions(+), 4 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 7556a4e7765..a8abc3fb7d0 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -3118,6 +3118,9 @@ "AliPayRedirection": { "type": "object" }, + "AmazonPayRedirectData": { + "type": "object" + }, "AmountDetails": { "type": "object", "required": [ @@ -14273,6 +14276,7 @@ "ali_pay", "ali_pay_hk", "alma", + "amazon_pay", "apple_pay", "atome", "bacs", @@ -20862,6 +20866,17 @@ } } }, + { + "type": "object", + "required": [ + "amazon_pay_redirect" + ], + "properties": { + "amazon_pay_redirect": { + "$ref": "#/components/schemas/AmazonPayRedirectData" + } + } + }, { "type": "object", "required": [ diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index ea0a95c1d57..5bdaa5940c8 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -5882,6 +5882,9 @@ "AliPayRedirection": { "type": "object" }, + "AmazonPayRedirectData": { + "type": "object" + }, "AmountFilter": { "type": "object", "properties": { @@ -17247,6 +17250,7 @@ "ali_pay", "ali_pay_hk", "alma", + "amazon_pay", "apple_pay", "atome", "bacs", @@ -26182,6 +26186,17 @@ } } }, + { + "type": "object", + "required": [ + "amazon_pay_redirect" + ], + "properties": { + "amazon_pay_redirect": { + "$ref": "#/components/schemas/AmazonPayRedirectData" + } + } + }, { "type": "object", "required": [ diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 4a1fa8b408e..ec281197eda 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2362,6 +2362,7 @@ impl GetPaymentMethodType for WalletData { match self { Self::AliPayQr(_) | Self::AliPayRedirect(_) => api_enums::PaymentMethodType::AliPay, Self::AliPayHkRedirect(_) => api_enums::PaymentMethodType::AliPayHk, + Self::AmazonPayRedirect(_) => api_enums::PaymentMethodType::AmazonPay, Self::MomoRedirect(_) => api_enums::PaymentMethodType::Momo, Self::KakaoPayRedirect(_) => api_enums::PaymentMethodType::KakaoPay, Self::GoPayRedirect(_) => api_enums::PaymentMethodType::GoPay, @@ -3241,6 +3242,8 @@ pub enum WalletData { AliPayRedirect(AliPayRedirection), /// The wallet data for Ali Pay HK redirect AliPayHkRedirect(AliPayHkRedirection), + /// The wallet data for Amazon Pay redirect + AmazonPayRedirect(AmazonPayRedirectData), /// The wallet data for Momo redirect MomoRedirect(MomoRedirection), /// The wallet data for KakaoPay redirect @@ -3324,6 +3327,7 @@ impl GetAddressFromPaymentMethodData for WalletData { | Self::KakaoPayRedirect(_) | Self::GoPayRedirect(_) | Self::GcashRedirect(_) + | Self::AmazonPayRedirect(_) | Self::ApplePay(_) | Self::ApplePayRedirect(_) | Self::ApplePayThirdPartySdk(_) @@ -3481,6 +3485,9 @@ pub struct GooglePayWalletData { #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct ApplePayRedirectData {} +#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] +pub struct AmazonPayRedirectData {} + #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct GooglePayRedirectData {} diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index d9d76dbc53e..0234fbea6d5 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1557,6 +1557,7 @@ pub enum PaymentMethodType { AliPay, AliPayHk, Alma, + AmazonPay, ApplePay, Atome, Bacs, diff --git a/crates/common_enums/src/transformers.rs b/crates/common_enums/src/transformers.rs index 7611ae127ee..d351f5c927e 100644 --- a/crates/common_enums/src/transformers.rs +++ b/crates/common_enums/src/transformers.rs @@ -1799,6 +1799,7 @@ impl From for PaymentMethod { PaymentMethodType::AliPay => Self::Wallet, PaymentMethodType::AliPayHk => Self::Wallet, PaymentMethodType::Alma => Self::PayLater, + PaymentMethodType::AmazonPay => Self::Wallet, PaymentMethodType::ApplePay => Self::Wallet, PaymentMethodType::Bacs => Self::BankDebit, PaymentMethodType::BancontactCard => Self::BankRedirect, diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 190114fee3a..2b04ea8d1c2 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -3139,6 +3139,8 @@ merchant_secret="Source verification key" payment_method_type = "sepa" [[stripe.bank_transfer]] payment_method_type = "multibanco" +[[stripe.wallet]] + payment_method_type = "amazon_pay" [[stripe.wallet]] payment_method_type = "apple_pay" [[stripe.wallet]] diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index f1745587a6e..b7108c63477 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -2290,6 +2290,8 @@ merchant_secret="Source verification key" payment_method_type = "bacs" [[stripe.bank_transfer]] payment_method_type = "sepa" +[[stripe.wallet]] + payment_method_type = "amazon_pay" [[stripe.wallet]] payment_method_type = "apple_pay" [[stripe.wallet]] diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 2a83d62ee9c..b886e7410a9 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -3081,6 +3081,8 @@ merchant_secret="Source verification key" payment_method_type = "sepa" [[stripe.bank_transfer]] payment_method_type = "multibanco" +[[stripe.wallet]] + payment_method_type = "amazon_pay" [[stripe.wallet]] payment_method_type = "apple_pay" [[stripe.wallet]] diff --git a/crates/euclid/src/frontend/dir/enums.rs b/crates/euclid/src/frontend/dir/enums.rs index 6c96070159b..6fb302641db 100644 --- a/crates/euclid/src/frontend/dir/enums.rs +++ b/crates/euclid/src/frontend/dir/enums.rs @@ -71,6 +71,7 @@ pub enum PayLaterType { #[strum(serialize_all = "snake_case")] pub enum WalletType { GooglePay, + AmazonPay, ApplePay, Paypal, AliPay, diff --git a/crates/euclid/src/frontend/dir/lowering.rs b/crates/euclid/src/frontend/dir/lowering.rs index 07ff2c4364e..04a029bd109 100644 --- a/crates/euclid/src/frontend/dir/lowering.rs +++ b/crates/euclid/src/frontend/dir/lowering.rs @@ -38,6 +38,7 @@ impl From for global_enums::PaymentMethodType { fn from(value: enums::WalletType) -> Self { match value { enums::WalletType::GooglePay => Self::GooglePay, + enums::WalletType::AmazonPay => Self::AmazonPay, enums::WalletType::ApplePay => Self::ApplePay, enums::WalletType::Paypal => Self::Paypal, enums::WalletType::AliPay => Self::AliPay, diff --git a/crates/euclid/src/frontend/dir/transformers.rs b/crates/euclid/src/frontend/dir/transformers.rs index 914a9444918..1a3bb52e0fa 100644 --- a/crates/euclid/src/frontend/dir/transformers.rs +++ b/crates/euclid/src/frontend/dir/transformers.rs @@ -19,6 +19,7 @@ impl IntoDirValue for (global_enums::PaymentMethodType, global_enums::PaymentMet global_enums::PaymentMethodType::AfterpayClearpay => { Ok(dirval!(PayLaterType = AfterpayClearpay)) } + global_enums::PaymentMethodType::AmazonPay => Ok(dirval!(WalletType = AmazonPay)), global_enums::PaymentMethodType::GooglePay => Ok(dirval!(WalletType = GooglePay)), global_enums::PaymentMethodType::ApplePay => Ok(dirval!(WalletType = ApplePay)), global_enums::PaymentMethodType::Paypal => Ok(dirval!(WalletType = Paypal)), diff --git a/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs b/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs index 45a7b577a86..8c5adefc613 100644 --- a/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs @@ -337,6 +337,7 @@ fn get_wallet_details( WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) diff --git a/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs b/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs index a0868d688eb..11ff53d439c 100644 --- a/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs @@ -296,6 +296,7 @@ impl TryFrom<&SetupMandateRouterData> for BankOfAmericaPaymentsRequest { WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) @@ -1045,6 +1046,7 @@ impl TryFrom<&BankOfAmericaRouterData<&PaymentsAuthorizeRouterData>> WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) diff --git a/crates/hyperswitch_connectors/src/connectors/bluesnap/transformers.rs b/crates/hyperswitch_connectors/src/connectors/bluesnap/transformers.rs index 7970b1b1d42..809902fa2a2 100644 --- a/crates/hyperswitch_connectors/src/connectors/bluesnap/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/bluesnap/transformers.rs @@ -368,6 +368,7 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsAuthorizeRouterData>> for Blues WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) diff --git a/crates/hyperswitch_connectors/src/connectors/boku/transformers.rs b/crates/hyperswitch_connectors/src/connectors/boku/transformers.rs index da5f775489c..8bc7b3639b7 100644 --- a/crates/hyperswitch_connectors/src/connectors/boku/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/boku/transformers.rs @@ -180,6 +180,7 @@ fn get_wallet_type(wallet_data: &WalletData) -> Result for CybersourceZeroMandateRequest { WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) @@ -2066,6 +2067,7 @@ impl TryFrom<&CybersourceRouterData<&PaymentsAuthorizeRouterData>> for Cybersour WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) diff --git a/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs b/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs index 115079c7d00..e23cdd548cb 100644 --- a/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs @@ -508,6 +508,7 @@ impl TryFrom<&FiuuRouterData<&PaymentsAuthorizeRouterData>> for FiuuPaymentReque WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) diff --git a/crates/hyperswitch_connectors/src/connectors/globepay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/globepay/transformers.rs index 5d30146293c..36c83859be9 100644 --- a/crates/hyperswitch_connectors/src/connectors/globepay/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/globepay/transformers.rs @@ -59,6 +59,7 @@ impl TryFrom<&GlobepayRouterData<&types::PaymentsAuthorizeRouterData>> for Globe WalletData::WeChatPayQr(_) => GlobepayChannel::Wechat, WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) diff --git a/crates/hyperswitch_connectors/src/connectors/multisafepay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/multisafepay/transformers.rs index a60e8bdc79e..54aca37a76a 100644 --- a/crates/hyperswitch_connectors/src/connectors/multisafepay/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/multisafepay/transformers.rs @@ -492,6 +492,7 @@ impl TryFrom<&MultisafepayRouterData<&types::PaymentsAuthorizeRouterData>> WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) @@ -556,6 +557,7 @@ impl TryFrom<&MultisafepayRouterData<&types::PaymentsAuthorizeRouterData>> WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) @@ -715,6 +717,7 @@ impl TryFrom<&MultisafepayRouterData<&types::PaymentsAuthorizeRouterData>> WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) diff --git a/crates/hyperswitch_connectors/src/connectors/nexinets/transformers.rs b/crates/hyperswitch_connectors/src/connectors/nexinets/transformers.rs index 8d6dc42926b..21fa1ec3217 100644 --- a/crates/hyperswitch_connectors/src/connectors/nexinets/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/nexinets/transformers.rs @@ -705,6 +705,7 @@ fn get_wallet_details( WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) diff --git a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs index c1ef6566e03..112d409d6e2 100644 --- a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs @@ -341,6 +341,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym WalletDataPaymentMethod::AliPayQr(_) | WalletDataPaymentMethod::AliPayRedirect(_) | WalletDataPaymentMethod::AliPayHkRedirect(_) + | WalletDataPaymentMethod::AmazonPayRedirect(_) | WalletDataPaymentMethod::MomoRedirect(_) | WalletDataPaymentMethod::KakaoPayRedirect(_) | WalletDataPaymentMethod::GoPayRedirect(_) @@ -1586,6 +1587,7 @@ impl TryFrom<&SetupMandateRouterData> for NovalnetPaymentsRequest { WalletDataPaymentMethod::AliPayQr(_) | WalletDataPaymentMethod::AliPayRedirect(_) | WalletDataPaymentMethod::AliPayHkRedirect(_) + | WalletDataPaymentMethod::AmazonPayRedirect(_) | WalletDataPaymentMethod::MomoRedirect(_) | WalletDataPaymentMethod::KakaoPayRedirect(_) | WalletDataPaymentMethod::GoPayRedirect(_) diff --git a/crates/hyperswitch_connectors/src/connectors/shift4/transformers.rs b/crates/hyperswitch_connectors/src/connectors/shift4/transformers.rs index 1973e622be1..5c5615aa945 100644 --- a/crates/hyperswitch_connectors/src/connectors/shift4/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/shift4/transformers.rs @@ -286,6 +286,7 @@ impl TryFrom<&WalletData> for Shift4PaymentMethod { fn try_from(wallet_data: &WalletData) -> Result { match wallet_data { WalletData::AliPayRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::ApplePay(_) | WalletData::WeChatPayRedirect(_) | WalletData::AliPayQr(_) diff --git a/crates/hyperswitch_connectors/src/connectors/square/transformers.rs b/crates/hyperswitch_connectors/src/connectors/square/transformers.rs index ff3999aee64..6c47e5d58d4 100644 --- a/crates/hyperswitch_connectors/src/connectors/square/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/square/transformers.rs @@ -107,6 +107,7 @@ impl TryFrom<(&types::TokenizationRouterData, WalletData)> for SquareTokenReques | WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) diff --git a/crates/hyperswitch_connectors/src/connectors/wellsfargo/transformers.rs b/crates/hyperswitch_connectors/src/connectors/wellsfargo/transformers.rs index 1a4d3ce8f9c..5eaf54065d7 100644 --- a/crates/hyperswitch_connectors/src/connectors/wellsfargo/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/wellsfargo/transformers.rs @@ -187,6 +187,7 @@ impl TryFrom<&SetupMandateRouterData> for WellsfargoZeroMandateRequest { WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) @@ -1248,6 +1249,7 @@ impl TryFrom<&WellsfargoRouterData<&PaymentsAuthorizeRouterData>> for Wellsfargo WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs index e4656021a59..47a5d3b2b2b 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs @@ -156,6 +156,7 @@ fn fetch_payment_instrument( WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) diff --git a/crates/hyperswitch_connectors/src/connectors/zen/transformers.rs b/crates/hyperswitch_connectors/src/connectors/zen/transformers.rs index cc6795f157a..a9e21f9071c 100644 --- a/crates/hyperswitch_connectors/src/connectors/zen/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/zen/transformers.rs @@ -487,6 +487,7 @@ impl | WalletData::AliPayQr(_) | WalletData::AliPayRedirect(_) | WalletData::AliPayHkRedirect(_) + | WalletData::AmazonPayRedirect(_) | WalletData::MomoRedirect(_) | WalletData::KakaoPayRedirect(_) | WalletData::GoPayRedirect(_) diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index c69641ebd11..418c505cf52 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -2296,6 +2296,7 @@ pub enum PaymentMethodDataType { AliPayQr, AliPayRedirect, AliPayHkRedirect, + AmazonPayRedirect, MomoRedirect, KakaoPayRedirect, GoPayRedirect, @@ -2415,6 +2416,7 @@ impl From for PaymentMethodDataType { payment_method_data::WalletData::AliPayQr(_) => Self::AliPayQr, payment_method_data::WalletData::AliPayRedirect(_) => Self::AliPayRedirect, payment_method_data::WalletData::AliPayHkRedirect(_) => Self::AliPayHkRedirect, + payment_method_data::WalletData::AmazonPayRedirect(_) => Self::AmazonPayRedirect, payment_method_data::WalletData::MomoRedirect(_) => Self::MomoRedirect, payment_method_data::WalletData::KakaoPayRedirect(_) => Self::KakaoPayRedirect, payment_method_data::WalletData::GoPayRedirect(_) => Self::GoPayRedirect, diff --git a/crates/hyperswitch_domain_models/src/payment_method_data.rs b/crates/hyperswitch_domain_models/src/payment_method_data.rs index dcf46964170..9e83810c9e0 100644 --- a/crates/hyperswitch_domain_models/src/payment_method_data.rs +++ b/crates/hyperswitch_domain_models/src/payment_method_data.rs @@ -1,6 +1,8 @@ use api_models::{ mandates, payment_methods, - payments::{additional_info as payment_additional_types, ExtendedCardInfo}, + payments::{ + additional_info as payment_additional_types, AmazonPayRedirectData, ExtendedCardInfo, + }, }; use common_enums::enums as api_enums; use common_utils::{ @@ -169,6 +171,7 @@ pub enum WalletData { AliPayQr(Box), AliPayRedirect(AliPayRedirection), AliPayHkRedirect(AliPayHkRedirection), + AmazonPayRedirect(Box), MomoRedirect(MomoRedirection), KakaoPayRedirect(KakaoPayRedirection), GoPayRedirect(GoPayRedirection), @@ -729,6 +732,9 @@ impl From for WalletData { api_models::payments::WalletData::AliPayHkRedirect(_) => { Self::AliPayHkRedirect(AliPayHkRedirection {}) } + api_models::payments::WalletData::AmazonPayRedirect(_) => { + Self::AmazonPayRedirect(Box::new(AmazonPayRedirectData {})) + } api_models::payments::WalletData::MomoRedirect(_) => { Self::MomoRedirect(MomoRedirection {}) } @@ -1518,6 +1524,7 @@ impl GetPaymentMethodType for WalletData { match self { Self::AliPayQr(_) | Self::AliPayRedirect(_) => api_enums::PaymentMethodType::AliPay, Self::AliPayHkRedirect(_) => api_enums::PaymentMethodType::AliPayHk, + Self::AmazonPayRedirect(_) => api_enums::PaymentMethodType::AmazonPay, Self::MomoRedirect(_) => api_enums::PaymentMethodType::Momo, Self::KakaoPayRedirect(_) => api_enums::PaymentMethodType::KakaoPay, Self::GoPayRedirect(_) => api_enums::PaymentMethodType::GoPay, diff --git a/crates/kgraph_utils/src/mca.rs b/crates/kgraph_utils/src/mca.rs index a3e1dfc6220..e224078493f 100644 --- a/crates/kgraph_utils/src/mca.rs +++ b/crates/kgraph_utils/src/mca.rs @@ -21,6 +21,7 @@ fn get_dir_value_payment_method( from: api_enums::PaymentMethodType, ) -> Result { match from { + api_enums::PaymentMethodType::AmazonPay => Ok(dirval!(WalletType = AmazonPay)), api_enums::PaymentMethodType::Credit => Ok(dirval!(CardType = Credit)), api_enums::PaymentMethodType::Debit => Ok(dirval!(CardType = Debit)), api_enums::PaymentMethodType::Giropay => Ok(dirval!(BankRedirectType = Giropay)), diff --git a/crates/kgraph_utils/src/transformers.rs b/crates/kgraph_utils/src/transformers.rs index 89b8c5a34ad..adfe5820866 100644 --- a/crates/kgraph_utils/src/transformers.rs +++ b/crates/kgraph_utils/src/transformers.rs @@ -130,6 +130,7 @@ impl IntoDirValue for api_enums::FutureUsage { impl IntoDirValue for (api_enums::PaymentMethodType, api_enums::PaymentMethod) { fn into_dir_value(self) -> Result { match self.0 { + api_enums::PaymentMethodType::AmazonPay => Ok(dirval!(WalletType = AmazonPay)), api_enums::PaymentMethodType::Credit => Ok(dirval!(CardType = Credit)), api_enums::PaymentMethodType::Debit => Ok(dirval!(CardType = Debit)), api_enums::PaymentMethodType::Giropay => Ok(dirval!(BankRedirectType = Giropay)), diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index de358f79332..7102c23d17e 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -479,6 +479,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::AchTransfer, api_models::payments::MultibancoTransferInstructions, api_models::payments::DokuBankTransferInstructions, + api_models::payments::AmazonPayRedirectData, api_models::payments::ApplePayRedirectData, api_models::payments::ApplePayThirdPartySdkData, api_models::payments::GooglePayRedirectData, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 1078d8080ca..ac88908fbd2 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -440,6 +440,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::AchTransfer, api_models::payments::MultibancoTransferInstructions, api_models::payments::DokuBankTransferInstructions, + api_models::payments::AmazonPayRedirectData, api_models::payments::ApplePayRedirectData, api_models::payments::ApplePayThirdPartySdkData, api_models::payments::GooglePayRedirectData, diff --git a/crates/router/src/configs/defaults/payment_connector_required_fields.rs b/crates/router/src/configs/defaults/payment_connector_required_fields.rs index 644c213acd8..421f430e74e 100644 --- a/crates/router/src/configs/defaults/payment_connector_required_fields.rs +++ b/crates/router/src/configs/defaults/payment_connector_required_fields.rs @@ -9488,6 +9488,21 @@ impl Default for settings::RequiredFields { ]), }, ), + ( + enums::PaymentMethodType::AmazonPay, + ConnectorFields { + fields: HashMap::from([ + ( + enums::Connector::Stripe, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::new(), + common: HashMap::new(), + } + ), + ]), + }, + ), ( enums::PaymentMethodType::Cashapp, ConnectorFields { diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index 3e25799c02e..11d34e55b7b 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -108,6 +108,7 @@ impl TryFrom<(&domain::WalletData, &types::PaymentsAuthorizeRouterData)> for Pay account_id: None, })), domain::WalletData::AliPayHkRedirect(_) + | domain::WalletData::AmazonPayRedirect(_) | domain::WalletData::MomoRedirect(_) | domain::WalletData::KakaoPayRedirect(_) | domain::WalletData::GoPayRedirect(_) diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index d6f6fdc8f0f..91e65759573 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -215,7 +215,8 @@ impl ConnectorValidation for Adyen { ) } }, - PaymentMethodType::CardRedirect + PaymentMethodType::AmazonPay + | PaymentMethodType::CardRedirect | PaymentMethodType::DirectCarrierBilling | PaymentMethodType::Fps | PaymentMethodType::DuitNow diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 394652c33e1..adb1a795f70 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -2211,6 +2211,7 @@ impl TryFrom<(&domain::WalletData, &types::PaymentsAuthorizeRouterData)> domain::WalletData::DanaRedirect { .. } => Ok(AdyenPaymentMethod::Dana), domain::WalletData::SwishQr(_) => Ok(AdyenPaymentMethod::Swish), domain::WalletData::AliPayQr(_) + | domain::WalletData::AmazonPayRedirect(_) | domain::WalletData::ApplePayRedirect(_) | domain::WalletData::ApplePayThirdPartySdk(_) | domain::WalletData::GooglePayRedirect(_) diff --git a/crates/router/src/connector/authorizedotnet/transformers.rs b/crates/router/src/connector/authorizedotnet/transformers.rs index c430191a1ec..276ea6bf77b 100644 --- a/crates/router/src/connector/authorizedotnet/transformers.rs +++ b/crates/router/src/connector/authorizedotnet/transformers.rs @@ -1809,6 +1809,7 @@ fn get_wallet_data( domain::WalletData::AliPayQr(_) | domain::WalletData::AliPayRedirect(_) | domain::WalletData::AliPayHkRedirect(_) + | domain::WalletData::AmazonPayRedirect(_) | domain::WalletData::MomoRedirect(_) | domain::WalletData::KakaoPayRedirect(_) | domain::WalletData::GoPayRedirect(_) diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index bb38ef74836..321a8cec765 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -93,6 +93,7 @@ impl TryFrom<&types::TokenizationRouterData> for TokenRequest { domain::WalletData::AliPayQr(_) | domain::WalletData::AliPayRedirect(_) | domain::WalletData::AliPayHkRedirect(_) + | domain::WalletData::AmazonPayRedirect(_) | domain::WalletData::MomoRedirect(_) | domain::WalletData::KakaoPayRedirect(_) | domain::WalletData::GoPayRedirect(_) @@ -347,6 +348,7 @@ impl TryFrom<&CheckoutRouterData<&types::PaymentsAuthorizeRouterData>> for Payme domain::WalletData::AliPayQr(_) | domain::WalletData::AliPayRedirect(_) | domain::WalletData::AliPayHkRedirect(_) + | domain::WalletData::AmazonPayRedirect(_) | domain::WalletData::MomoRedirect(_) | domain::WalletData::KakaoPayRedirect(_) | domain::WalletData::GoPayRedirect(_) diff --git a/crates/router/src/connector/klarna.rs b/crates/router/src/connector/klarna.rs index 4e54311a89a..6fbc48a66a0 100644 --- a/crates/router/src/connector/klarna.rs +++ b/crates/router/src/connector/klarna.rs @@ -577,6 +577,7 @@ impl | common_enums::PaymentMethodType::AliPay | common_enums::PaymentMethodType::AliPayHk | common_enums::PaymentMethodType::Alma + | common_enums::PaymentMethodType::AmazonPay | common_enums::PaymentMethodType::ApplePay | common_enums::PaymentMethodType::Atome | common_enums::PaymentMethodType::Bacs @@ -693,6 +694,7 @@ impl | common_enums::PaymentMethodType::AliPay | common_enums::PaymentMethodType::AliPayHk | common_enums::PaymentMethodType::Alma + | common_enums::PaymentMethodType::AmazonPay | common_enums::PaymentMethodType::ApplePay | common_enums::PaymentMethodType::Atome | common_enums::PaymentMethodType::Bacs diff --git a/crates/router/src/connector/mifinity/transformers.rs b/crates/router/src/connector/mifinity/transformers.rs index 1e2c420c767..f913077d1dc 100644 --- a/crates/router/src/connector/mifinity/transformers.rs +++ b/crates/router/src/connector/mifinity/transformers.rs @@ -154,6 +154,7 @@ impl TryFrom<&MifinityRouterData<&types::PaymentsAuthorizeRouterData>> for Mifin domain::WalletData::AliPayQr(_) | domain::WalletData::AliPayRedirect(_) | domain::WalletData::AliPayHkRedirect(_) + | domain::WalletData::AmazonPayRedirect(_) | domain::WalletData::MomoRedirect(_) | domain::WalletData::KakaoPayRedirect(_) | domain::WalletData::GoPayRedirect(_) diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index 4c33f020017..19f03cb1b2c 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -545,6 +545,7 @@ impl domain::WalletData::AliPayQr(_) | domain::WalletData::AliPayRedirect(_) | domain::WalletData::AliPayHkRedirect(_) + | domain::WalletData::AmazonPayRedirect(_) | domain::WalletData::MomoRedirect(_) | domain::WalletData::KakaoPayRedirect(_) | domain::WalletData::GoPayRedirect(_) diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index b96283247b4..5816aaa9295 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -314,6 +314,7 @@ impl TryFrom<&NoonRouterData<&types::PaymentsAuthorizeRouterData>> for NoonPayme domain::WalletData::AliPayQr(_) | domain::WalletData::AliPayRedirect(_) | domain::WalletData::AliPayHkRedirect(_) + | domain::WalletData::AmazonPayRedirect(_) | domain::WalletData::MomoRedirect(_) | domain::WalletData::KakaoPayRedirect(_) | domain::WalletData::GoPayRedirect(_) diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index c98a0647778..eea700f695c 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -902,6 +902,7 @@ where domain::WalletData::AliPayQr(_) | domain::WalletData::AliPayRedirect(_) | domain::WalletData::AliPayHkRedirect(_) + | domain::WalletData::AmazonPayRedirect(_) | domain::WalletData::MomoRedirect(_) | domain::WalletData::KakaoPayRedirect(_) | domain::WalletData::GoPayRedirect(_) diff --git a/crates/router/src/connector/payme/transformers.rs b/crates/router/src/connector/payme/transformers.rs index 7e50f7f40e6..c13730529e2 100644 --- a/crates/router/src/connector/payme/transformers.rs +++ b/crates/router/src/connector/payme/transformers.rs @@ -393,6 +393,7 @@ impl TryFrom<&PaymentMethodData> for SalePaymentMethod { domain::WalletData::AliPayQr(_) | domain::WalletData::AliPayRedirect(_) | domain::WalletData::AliPayHkRedirect(_) + | domain::WalletData::AmazonPayRedirect(_) | domain::WalletData::MomoRedirect(_) | domain::WalletData::KakaoPayRedirect(_) | domain::WalletData::GoPayRedirect(_) diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 8f739792a0c..18739851ae8 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -1044,6 +1044,7 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP domain::WalletData::AliPayQr(_) | domain::WalletData::AliPayRedirect(_) | domain::WalletData::AliPayHkRedirect(_) + | domain::WalletData::AmazonPayRedirect(_) | domain::WalletData::MomoRedirect(_) | domain::WalletData::KakaoPayRedirect(_) | domain::WalletData::GoPayRedirect(_) @@ -1134,6 +1135,7 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP | enums::PaymentMethodType::AliPay | enums::PaymentMethodType::AliPayHk | enums::PaymentMethodType::Alma + | enums::PaymentMethodType::AmazonPay | enums::PaymentMethodType::ApplePay | enums::PaymentMethodType::Atome | enums::PaymentMethodType::Bacs diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 60852779884..363dfb81bcc 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -531,6 +531,7 @@ pub enum StripeWallet { ApplepayToken(StripeApplePay), GooglepayToken(GooglePayToken), ApplepayPayment(ApplepayPayment), + AmazonpayPayment(AmazonpayPayment), WechatpayPayment(WechatpayPayment), AlipayPayment(AlipayPayment), Cashapp(CashappPayment), @@ -577,6 +578,12 @@ pub struct ApplepayPayment { pub payment_method_types: StripePaymentMethodType, } +#[derive(Debug, Eq, PartialEq, Serialize)] +pub struct AmazonpayPayment { + #[serde(rename = "payment_method_data[type]")] + pub payment_method_types: StripePaymentMethodType, +} + #[derive(Debug, Eq, PartialEq, Serialize)] pub struct AlipayPayment { #[serde(rename = "payment_method_data[type]")] @@ -620,6 +627,8 @@ pub enum StripePaymentMethodType { Affirm, AfterpayClearpay, Alipay, + #[serde(rename = "amazon_pay")] + AmazonPay, #[serde(rename = "au_becs_debit")] Becs, #[serde(rename = "bacs_debit")] @@ -667,6 +676,7 @@ impl TryFrom for StripePaymentMethodType { enums::PaymentMethodType::Giropay => Ok(Self::Giropay), enums::PaymentMethodType::Ideal => Ok(Self::Ideal), enums::PaymentMethodType::Sofort => Ok(Self::Sofort), + enums::PaymentMethodType::AmazonPay => Ok(Self::AmazonPay), enums::PaymentMethodType::ApplePay => Ok(Self::Card), enums::PaymentMethodType::Ach => Ok(Self::Ach), enums::PaymentMethodType::Sepa => Ok(Self::Sepa), @@ -1048,6 +1058,9 @@ impl ForeignTryFrom<&domain::WalletData> for Option { domain::WalletData::GooglePay(_) => Ok(Some(StripePaymentMethodType::Card)), domain::WalletData::WeChatPayQr(_) => Ok(Some(StripePaymentMethodType::Wechatpay)), domain::WalletData::CashappQr(_) => Ok(Some(StripePaymentMethodType::Cashapp)), + domain::WalletData::AmazonPayRedirect(_) => { + Ok(Some(StripePaymentMethodType::AmazonPay)) + } domain::WalletData::MobilePayRedirect(_) => { Err(errors::ConnectorError::NotImplemented( connector_util::get_unimplemented_payment_method_error_message("stripe"), @@ -1466,6 +1479,11 @@ impl TryFrom<(&domain::WalletData, Option)> for Strip payment_method_data_type: StripePaymentMethodType::Cashapp, }))) } + domain::WalletData::AmazonPayRedirect(_) => Ok(Self::Wallet( + StripeWallet::AmazonpayPayment(AmazonpayPayment { + payment_method_types: StripePaymentMethodType::AmazonPay, + }), + )), domain::WalletData::GooglePay(gpay_data) => Ok(Self::try_from(gpay_data)?), domain::WalletData::PaypalRedirect(_) | domain::WalletData::MobilePayRedirect(_) => { Err(errors::ConnectorError::NotImplemented( @@ -2301,6 +2319,7 @@ pub enum StripePaymentMethodDetailsResponse { Klarna, Affirm, AfterpayClearpay, + AmazonPay, ApplePay, #[serde(rename = "us_bank_account")] Ach, @@ -2348,6 +2367,7 @@ impl StripePaymentMethodDetailsResponse { | Self::Klarna | Self::Affirm | Self::AfterpayClearpay + | Self::AmazonPay | Self::ApplePay | Self::Ach | Self::Sepa @@ -2664,6 +2684,7 @@ impl | Some(StripePaymentMethodDetailsResponse::Klarna) | Some(StripePaymentMethodDetailsResponse::Affirm) | Some(StripePaymentMethodDetailsResponse::AfterpayClearpay) + | Some(StripePaymentMethodDetailsResponse::AmazonPay) | Some(StripePaymentMethodDetailsResponse::ApplePay) | Some(StripePaymentMethodDetailsResponse::Ach) | Some(StripePaymentMethodDetailsResponse::Sepa) @@ -3273,6 +3294,7 @@ pub enum StripePaymentMethodOptions { Klarna {}, Affirm {}, AfterpayClearpay {}, + AmazonPay {}, Eps {}, Giropay {}, Ideal {}, diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index cbf1867b08b..c4824d65836 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -2744,6 +2744,7 @@ pub enum PaymentMethodDataType { AliPayQr, AliPayRedirect, AliPayHkRedirect, + AmazonPayRedirect, MomoRedirect, KakaoPayRedirect, GoPayRedirect, @@ -2862,6 +2863,7 @@ impl From for PaymentMethodDataType { domain::payments::WalletData::AliPayQr(_) => Self::AliPayQr, domain::payments::WalletData::AliPayRedirect(_) => Self::AliPayRedirect, domain::payments::WalletData::AliPayHkRedirect(_) => Self::AliPayHkRedirect, + domain::payments::WalletData::AmazonPayRedirect(_) => Self::AmazonPayRedirect, domain::payments::WalletData::MomoRedirect(_) => Self::MomoRedirect, domain::payments::WalletData::KakaoPayRedirect(_) => Self::KakaoPayRedirect, domain::payments::WalletData::GoPayRedirect(_) => Self::GoPayRedirect, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index b0df22f3d04..db12a6282c3 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2740,7 +2740,8 @@ pub fn validate_payment_method_type_against_payment_method( ), api_enums::PaymentMethod::Wallet => matches!( payment_method_type, - api_enums::PaymentMethodType::ApplePay + api_enums::PaymentMethodType::AmazonPay + | api_enums::PaymentMethodType::ApplePay | api_enums::PaymentMethodType::GooglePay | api_enums::PaymentMethodType::Paypal | api_enums::PaymentMethodType::AliPay diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 2a287a337c9..c8222163b52 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -453,7 +453,8 @@ impl ForeignFrom for Option { impl ForeignFrom for api_enums::PaymentMethod { fn foreign_from(payment_method_type: api_enums::PaymentMethodType) -> Self { match payment_method_type { - api_enums::PaymentMethodType::ApplePay + api_enums::PaymentMethodType::AmazonPay + | api_enums::PaymentMethodType::ApplePay | api_enums::PaymentMethodType::GooglePay | api_enums::PaymentMethodType::Paypal | api_enums::PaymentMethodType::AliPay From 7ea630da002fcb3f8ab9093114efe7973b1d347d Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:14:16 +0530 Subject: [PATCH 30/46] feat(core): Add Authorize flow as fallback flow while fetching GSM for refund errors (#7129) --- crates/router/src/consts.rs | 3 +++ crates/router/src/core/refunds.rs | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 585e894adf9..d27ad5d6019 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -215,3 +215,6 @@ pub const AUTHENTICATION_SERVICE_ELIGIBLE_CONFIG: &str = /// Refund flow identifier used for performing GSM operations pub const REFUND_FLOW_STR: &str = "refund_flow"; + +/// Authorize flow identifier used for performing GSM operations +pub const AUTHORIZE_FLOW_STR: &str = "Authorize"; diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index 1e788b6acee..a6505f23631 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -297,6 +297,21 @@ pub async fn trigger_refund_to_gateway( consts::REFUND_FLOW_STR.to_string(), ) .await; + // Note: Some connectors do not have a separate list of refund errors + // In such cases, the error codes and messages are stored under "Authorize" flow in GSM table + // So we will have to fetch the GSM using Authorize flow in case GSM is not found using "refund_flow" + let option_gsm = if option_gsm.is_none() { + helpers::get_gsm_record( + state, + Some(err.code.clone()), + Some(err.message.clone()), + connector.connector_name.to_string(), + consts::AUTHORIZE_FLOW_STR.to_string(), + ) + .await + } else { + option_gsm + }; let gsm_unified_code = option_gsm.as_ref().and_then(|gsm| gsm.unified_code.clone()); let gsm_unified_message = option_gsm.and_then(|gsm| gsm.unified_message); From 22072fd750940ac7fec6ea971737409518600891 Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Wed, 5 Feb 2025 19:14:56 +0530 Subject: [PATCH 31/46] feat(connector): [Deutschebank] Add Access Token Error struct (#7127) --- config/config.example.toml | 3 ++ config/docker_compose.toml | 3 ++ .../src/connectors/deutschebank.rs | 35 +++++++++++++++---- .../connectors/deutschebank/transformers.rs | 17 +++++++-- 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index c113d7abd57..e982a01eb11 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -585,6 +585,9 @@ paypal = { country = "AD,AE,AL,AM,AR,AT,AU,AZ,BA,BB,BD,BE,BG,BH,BI,BM,BN,BO,BR,B [pm_filters.mifinity] mifinity = { country = "BR,CN,SG,MY,DE,CH,DK,GB,ES,AD,GI,FI,FR,GR,HR,IT,JP,MX,AR,CO,CL,PE,VE,UY,PY,BO,EC,GT,HN,SV,NI,CR,PA,DO,CU,PR,NL,NO,PL,PT,SE,RU,TR,TW,HK,MO,AX,AL,DZ,AS,AO,AI,AG,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BE,BZ,BJ,BM,BT,BQ,BA,BW,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CX,CC,KM,CG,CK,CI,CW,CY,CZ,DJ,DM,EG,GQ,ER,EE,ET,FK,FO,FJ,GF,PF,TF,GA,GM,GE,GH,GL,GD,GP,GU,GG,GN,GW,GY,HT,HM,VA,IS,IN,ID,IE,IM,IL,JE,JO,KZ,KE,KI,KW,KG,LA,LV,LB,LS,LI,LT,LU,MK,MG,MW,MV,ML,MT,MH,MQ,MR,MU,YT,FM,MD,MC,MN,ME,MS,MA,MZ,NA,NR,NP,NC,NZ,NE,NG,NU,NF,MP,OM,PK,PW,PS,PG,PH,PN,QA,RE,RO,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SX,SK,SI,SB,SO,ZA,GS,KR,LK,SR,SJ,SZ,TH,TL,TG,TK,TO,TT,TN,TM,TC,TV,UG,UA,AE,UZ,VU,VN,VG,VI,WF,EH,ZM", currency = "AUD,CAD,CHF,CNY,CZK,DKK,EUR,GBP,INR,JPY,NOK,NZD,PLN,RUB,SEK,ZAR,USD,EGP,UYU,UZS" } +[pm_filters.fiuu] +duit_now = { country = "MY", currency = "MYR" } + [connector_customer] connector_list = "gocardless,stax,stripe" payout_connector_list = "stripe,wise" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 0d3cbb4f6af..12b8df6683c 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -508,6 +508,9 @@ credit = { country = "AF,DZ,AW,AU,AZ,BS,BH,BD,BB,BZ,BM,BT,BO,BA,BW,BR,BN,BG,BI,K google_pay = { country = "AL,DZ,AS,AO,AG,AR,AU,AT,AZ,BH,BY,BE,BR,BG,CA,CL,CO,HR,CZ,DK,DO,EG,EE,FI,FR,DE,GR,HK,HU,IN,ID,IE,IL,IT,JP,JO,KZ,KE,KW,LV,LB,LT,LU,MY,MX,NL,NZ,NO,OM,PK,PA,PE,PH,PL,PT,QA,RO,RU,SA,SG,SK,ZA,ES,LK,SE,CH,TW,TH,TR,UA,AE,GB,US,UY,VN" } apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US" } +[pm_filters.fiuu] +duit_now = { country = "MY", currency = "MYR" } + [bank_config.online_banking_fpx] adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" fiuu.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_of_china,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" diff --git a/crates/hyperswitch_connectors/src/connectors/deutschebank.rs b/crates/hyperswitch_connectors/src/connectors/deutschebank.rs index d7dcb63c476..0b6b2f132e7 100644 --- a/crates/hyperswitch_connectors/src/connectors/deutschebank.rs +++ b/crates/hyperswitch_connectors/src/connectors/deutschebank.rs @@ -159,9 +159,9 @@ impl ConnectorCommon for Deutschebank { res: Response, event_builder: Option<&mut ConnectorEvent>, ) -> CustomResult { - let response: deutschebank::DeutschebankErrorResponse = res + let response: deutschebank::PaymentsErrorResponse = res .response - .parse_struct("DeutschebankErrorResponse") + .parse_struct("PaymentsErrorResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); @@ -289,7 +289,33 @@ impl ConnectorIntegration res: Response, event_builder: Option<&mut ConnectorEvent>, ) -> CustomResult { - self.build_error_response(res, event_builder) + let response: deutschebank::DeutschebankError = res + .response + .parse_struct("DeutschebankError") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + match response { + deutschebank::DeutschebankError::PaymentsErrorResponse(response) => Ok(ErrorResponse { + status_code: res.status_code, + code: response.rc, + message: response.message.clone(), + reason: Some(response.message), + attempt_status: None, + connector_transaction_id: None, + }), + deutschebank::DeutschebankError::AccessTokenErrorResponse(response) => { + Ok(ErrorResponse { + status_code: res.status_code, + code: response.cause.clone(), + message: response.cause.clone(), + reason: Some(response.description), + attempt_status: None, + connector_transaction_id: None, + }) + } + } } } @@ -912,9 +938,6 @@ impl ConnectorIntegration for Deutscheb .url(&types::RefundSyncType::get_url(self, req, connectors)?) .attach_default_headers() .headers(types::RefundSyncType::get_headers(self, req, connectors)?) - .set_body(types::RefundSyncType::get_request_body( - self, req, connectors, - )?) .build(), )) } diff --git a/crates/hyperswitch_connectors/src/connectors/deutschebank/transformers.rs b/crates/hyperswitch_connectors/src/connectors/deutschebank/transformers.rs index 8133be2fa79..b026c339e2a 100644 --- a/crates/hyperswitch_connectors/src/connectors/deutschebank/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/deutschebank/transformers.rs @@ -1224,8 +1224,21 @@ impl TryFrom> } } -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct DeutschebankErrorResponse { +#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct PaymentsErrorResponse { pub rc: String, pub message: String, } + +#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct AccessTokenErrorResponse { + pub cause: String, + pub description: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum DeutschebankError { + PaymentsErrorResponse(PaymentsErrorResponse), + AccessTokenErrorResponse(AccessTokenErrorResponse), +} From 155917898cc443edc713513ea1376f045dfc0739 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 00:26:54 +0000 Subject: [PATCH 32/46] chore(postman): update Postman collection files --- .../adyen_uk.postman_collection.json | 441 +----------------- 1 file changed, 10 insertions(+), 431 deletions(-) diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index ebf353bb88d..071c3ca52c1 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -5367,428 +5367,7 @@ ] }, { - "name": "Scenario11-Bank Redirect-sofort", - "item": [ - { - "name": "Payments - Create", - "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 \"requires_payment_method\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments - Content check if value for 'status' matches 'requires_payment_method'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_payment_method\");", - " },", - " );", - "}", - "" - ], - "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\":1000,\"currency\":\"EUR\",\"confirm\":false,\"capture_method\":\"automatic\",\"capture_on\":\"2022-09-10T10:11:12Z\",\"amount_to_capture\":1000,\"customer_id\":\"StripeCustomer\",\"email\":\"abcdef123@gmail.com\",\"name\":\"John Doe\",\"phone\":\"999999999\",\"phone_country_code\":\"+65\",\"description\":\"Its my first payment request\",\"authentication_type\":\"three_ds\",\"return_url\":\"https://duck.com\",\"billing\":{\"address\":{\"first_name\":\"John\",\"last_name\":\"Doe\",\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"DE\"}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"},\"shipping\":{\"address\":{\"line1\":\"1467\",\"line2\":\"Harrison Street\",\"line3\":\"Harrison Street\",\"city\":\"San Fransico\",\"state\":\"California\",\"zip\":\"94122\",\"country\":\"US\",\"first_name\":\"John\",\"last_name\":\"Doe\"}},\"statement_descriptor_name\":\"joseph\",\"statement_descriptor_suffix\":\"JS\",\"metadata\":{\"udf1\":\"value1\",\"new_customer\":\"true\",\"login_date\":\"2019-09-10T10:11:12Z\"}}" - }, - "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": "Payments - Confirm", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[POST]::/payments/:id/confirm - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(", - " \"[POST]::/payments/:id/confirm - 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/:id/confirm - 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 \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "", - "// Response body should have \"next_action.redirect_to_url\"", - "pm.test(", - " \"[POST]::/payments - Content check if 'next_action.redirect_to_url' exists\",", - " function () {", - " pm.expect(typeof jsonData.next_action.redirect_to_url !== \"undefined\").to.be", - " .true;", - " },", - ");", - "", - "// Response body should have value \"sofort\" for \"payment_method_type\"", - "if (jsonData?.payment_method_type) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'payment_method_type' matches 'sofort'\",", - " function () {", - " pm.expect(jsonData.payment_method_type).to.eql(\"sofort\");", - " },", - " );", - "}", - "", - "// Response body should have value \"stripe\" for \"connector\"", - "if (jsonData?.connector) {", - " pm.test(", - " \"[POST]::/payments/:id/confirm - Content check if value for 'connector' matches 'adyen'\",", - " function () {", - " pm.expect(jsonData.connector).to.eql(\"adyen\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "{{publishable_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": "{\"client_secret\":\"{{client_secret}}\",\"payment_method\":\"bank_redirect\",\"payment_method_type\":\"sofort\",\"payment_method_data\":{\"bank_redirect\":{\"sofort\":{\"billing_details\":{\"billing_name\":\"John Doe\"},\"bank_name\":\"ing\",\"preferred_language\":\"en\",\"country\":\"DE\"}}},\"browser_info\":{\"user_agent\":\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36\",\"accept_header\":\"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\",\"language\":\"nl-NL\",\"color_depth\":24,\"screen_height\":723,\"screen_width\":1536,\"time_zone\":0,\"java_enabled\":true,\"java_script_enabled\":true,\"ip_address\":\"127.0.0.1\"}}" - }, - "url": { - "raw": "{{baseUrl}}/payments/:id/confirm", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id", - "confirm" - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "This API is to confirm the payment request and forward payment to the payment processor. This API provides more granular control upon when the API is forwarded to the payment processor. Alternatively you can confirm the payment within the Payments-Create API" - }, - "response": [] - }, - { - "name": "Payments - Retrieve", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "// Validate status 2xx", - "pm.test(\"[GET]::/payments/:id - Status code is 2xx\", function () {", - " pm.response.to.be.success;", - "});", - "", - "// Validate if response header has matching content-type", - "pm.test(\"[GET]::/payments/:id - 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(\"[GET]::/payments/:id - 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 \"requires_customer_action\" for \"status\"", - "if (jsonData?.status) {", - " pm.test(", - " \"[POST]::/payments:id - Content check if value for 'status' matches 'requires_customer_action'\",", - " function () {", - " pm.expect(jsonData.status).to.eql(\"requires_customer_action\");", - " },", - " );", - "}", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json" - } - ], - "url": { - "raw": "{{baseUrl}}/payments/:id?force_sync=true", - "host": [ - "{{baseUrl}}" - ], - "path": [ - "payments", - ":id" - ], - "query": [ - { - "key": "force_sync", - "value": "true" - } - ], - "variable": [ - { - "key": "id", - "value": "{{payment_id}}", - "description": "(Required) unique payment id" - } - ] - }, - "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" - }, - "response": [] - } - ] - }, - { - "name": "Scenario12-Bank Redirect-eps", + "name": "Scenario11-Bank Redirect-eps", "item": [ { "name": "Payments - Create", @@ -6209,7 +5788,7 @@ ] }, { - "name": "Scenario13-Refund recurring payment", + "name": "Scenario12-Refund recurring payment", "item": [ { "name": "Payments - Create", @@ -6969,7 +6548,7 @@ ] }, { - "name": "Scenario14-Bank debit-ach", + "name": "Scenario13-Bank debit-ach", "item": [ { "name": "Payments - Create", @@ -7381,7 +6960,7 @@ ] }, { - "name": "Scenario15-Bank debit-Bacs", + "name": "Scenario14-Bank debit-Bacs", "item": [ { "name": "Payments - Create", @@ -7793,7 +7372,7 @@ ] }, { - "name": "Scenario16-Bank Redirect-Trustly", + "name": "Scenario15-Bank Redirect-Trustly", "item": [ { "name": "Payments - Create", @@ -8214,7 +7793,7 @@ ] }, { - "name": "Scenario17-Add card flow", + "name": "Scenario16-Add card flow", "item": [ { "name": "Payments - Create", @@ -9024,7 +8603,7 @@ ] }, { - "name": "Scenario18-Pass Invalid CVV for save card flow and verify failed payment", + "name": "Scenario17-Pass Invalid CVV for save card flow and verify failed payment", "item": [ { "name": "Payments - Create", @@ -9667,7 +9246,7 @@ ] }, { - "name": "Scenario19-Don't Pass CVV for save card flow and verify failed payment Copy", + "name": "Scenario18-Don't Pass CVV for save card flow and verify failed payment Copy", "item": [ { "name": "Payments - Create", @@ -10310,7 +9889,7 @@ ] }, { - "name": "Scenario20-Create Gift Card payment", + "name": "Scenario19-Create Gift Card payment", "item": [ { "name": "Payments - Create", @@ -14375,4 +13954,4 @@ "type": "string" } ] -} \ No newline at end of file +} From 3da637e6960f73a3a69aef98b9de2e6de01fcb5e Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 00:31:16 +0000 Subject: [PATCH 33/46] chore(version): 2025.02.06.0 --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6cc3a151af..57524ca4ddd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,49 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2025.02.06.0 + +### Features + +- **analytics:** Add currency as dimension and filter for disputes ([#7006](https://github.com/juspay/hyperswitch/pull/7006)) ([`12a2f2a`](https://github.com/juspay/hyperswitch/commit/12a2f2ad147346365f828d8fc97eb9fe49a845bb)) +- **connector:** + - [INESPAY] Integrate Sepa Bank Debit ([#6755](https://github.com/juspay/hyperswitch/pull/6755)) ([`ce2485c`](https://github.com/juspay/hyperswitch/commit/ce2485c3c77d86a2bce01d20c410ae11ac08c555)) + - [Deutschebank] Add Access Token Error struct ([#7127](https://github.com/juspay/hyperswitch/pull/7127)) ([`22072fd`](https://github.com/juspay/hyperswitch/commit/22072fd750940ac7fec6ea971737409518600891)) +- **core:** + - Google pay decrypt flow ([#6991](https://github.com/juspay/hyperswitch/pull/6991)) ([`e0ec27d`](https://github.com/juspay/hyperswitch/commit/e0ec27d936fc62a6feb2f8f643a218f3ad7483b5)) + - Implement 3ds decision manger for V2 ([#7022](https://github.com/juspay/hyperswitch/pull/7022)) ([`1900959`](https://github.com/juspay/hyperswitch/commit/190095977819efac42da5483bfdae6420a7a402c)) + - Add Authorize flow as fallback flow while fetching GSM for refund errors ([#7129](https://github.com/juspay/hyperswitch/pull/7129)) ([`7ea630d`](https://github.com/juspay/hyperswitch/commit/7ea630da002fcb3f8ab9093114efe7973b1d347d)) +- **payments_v2:** Implement create and confirm intent flow ([#7106](https://github.com/juspay/hyperswitch/pull/7106)) ([`67ea754`](https://github.com/juspay/hyperswitch/commit/67ea754e383d2f9539d16f7fa40f201f177b5ea3)) +- **users:** Custom role at profile read ([#6875](https://github.com/juspay/hyperswitch/pull/6875)) ([`899c207`](https://github.com/juspay/hyperswitch/commit/899c207d5835ba39f5163d12c6f59aed39884359)) +- Add Support for Amazon Pay Redirect and Amazon Pay payment via Stripe ([#7056](https://github.com/juspay/hyperswitch/pull/7056)) ([`b54a3f9`](https://github.com/juspay/hyperswitch/commit/b54a3f9142388a3d870406c54fd1d314c7c7748d)) + +### Bug Fixes + +- **connector:** + - [BOA] throw unsupported error incase of 3DS cards and limit administrative area length to 20 characters ([#7174](https://github.com/juspay/hyperswitch/pull/7174)) ([`6f90b93`](https://github.com/juspay/hyperswitch/commit/6f90b93cee6eb5fb688750b940ea884af8b1caa3)) + - [Deutschebank] Display deutschebank card payment method in dashboard ([#7060](https://github.com/juspay/hyperswitch/pull/7060)) ([`f71cc96`](https://github.com/juspay/hyperswitch/commit/f71cc96a33ee3a9babb334c068dce7fbb3063e25)) + - [Authorizedotnet] fix deserialization error for Paypal while canceling payment ([#7141](https://github.com/juspay/hyperswitch/pull/7141)) ([`698a0aa`](https://github.com/juspay/hyperswitch/commit/698a0aa75af646107ac796f719b51e74530f11dc)) + - [worldpay] remove threeDS data from Authorize request for NTI flows ([#7097](https://github.com/juspay/hyperswitch/pull/7097)) ([`d443a4c`](https://github.com/juspay/hyperswitch/commit/d443a4cf1ee7bb9f5daa5147bd2854b3e4f4c76d)) +- **core:** Add payment_link_data in PaymentData for Psync ([#7137](https://github.com/juspay/hyperswitch/pull/7137)) ([`8917235`](https://github.com/juspay/hyperswitch/commit/8917235b4c1c606cba92539b9cb50449fc70474a)) + +### Refactors + +- **ci:** Remove Adyen-specific deprecated PMTs Sofort test cases in Postman ([#7099](https://github.com/juspay/hyperswitch/pull/7099)) ([`6fee301`](https://github.com/juspay/hyperswitch/commit/6fee3011ea84e08caef8459cd1f55856245e15b2)) +- **connector:** [AUTHORIZEDOTNET] Add metadata information to connector request ([#7011](https://github.com/juspay/hyperswitch/pull/7011)) ([`ea18886`](https://github.com/juspay/hyperswitch/commit/ea1888677df7de60a248184389d7be30ae21fc59)) +- **core:** Add recurring customer support for nomupay payouts. ([#6687](https://github.com/juspay/hyperswitch/pull/6687)) ([`8d8ebe9`](https://github.com/juspay/hyperswitch/commit/8d8ebe9051675d8102c6f9ea887bb23751ea5724)) + +### Miscellaneous Tasks + +- **postman:** Update Postman collection files ([`1559178`](https://github.com/juspay/hyperswitch/commit/155917898cc443edc713513ea1376f045dfc0739)) + +### Build System / Dependencies + +- **deps:** Bump `openssl` from 0.10.66 to 0.10.70 ([#7187](https://github.com/juspay/hyperswitch/pull/7187)) ([`91626c0`](https://github.com/juspay/hyperswitch/commit/91626c0c2554126a37f2624d3b0e2b2b60be3849)) + +**Full Changelog:** [`2025.02.05.0...2025.02.06.0`](https://github.com/juspay/hyperswitch/compare/2025.02.05.0...2025.02.06.0) + +- - - + ## 2025.02.05.0 ### Features From d5cbc1d46c79ada41d2cc7bef5ce6411a67e1136 Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Thu, 6 Feb 2025 14:57:37 +0530 Subject: [PATCH 34/46] ci(cypress): fix nmi and paypal (#7173) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .github/workflows/cypress-tests-runner.yml | 2 +- .../cypress/e2e/configs/Payment/Utils.js | 27 +++++++- .../00020-MandatesUsingNTIDProxy.cy.js | 66 +++++++------------ .../e2e/spec/Payment/00022-Variations.cy.js | 8 +++ .../Payment/00024-ConnectorAgnosticNTID.cy.js | 29 ++++++-- 5 files changed, 84 insertions(+), 48 deletions(-) diff --git a/.github/workflows/cypress-tests-runner.yml b/.github/workflows/cypress-tests-runner.yml index 67efcdb0ea3..dbef78355de 100644 --- a/.github/workflows/cypress-tests-runner.yml +++ b/.github/workflows/cypress-tests-runner.yml @@ -13,7 +13,7 @@ concurrency: env: CARGO_INCREMENTAL: 0 CARGO_NET_RETRY: 10 - PAYMENTS_CONNECTORS: "cybersource" + PAYMENTS_CONNECTORS: "cybersource stripe" PAYOUTS_CONNECTORS: "wise" RUST_BACKTRACE: short RUSTUP_MAX_RETRIES: 10 diff --git a/cypress-tests/cypress/e2e/configs/Payment/Utils.js b/cypress-tests/cypress/e2e/configs/Payment/Utils.js index 5a9baa32d73..b3de4db78d4 100644 --- a/cypress-tests/cypress/e2e/configs/Payment/Utils.js +++ b/cypress-tests/cypress/e2e/configs/Payment/Utils.js @@ -4,7 +4,7 @@ import { connectorDetails as adyenConnectorDetails } from "./Adyen.js"; import { connectorDetails as bankOfAmericaConnectorDetails } from "./BankOfAmerica.js"; import { connectorDetails as bluesnapConnectorDetails } from "./Bluesnap.js"; import { connectorDetails as checkoutConnectorDetails } from "./Checkout.js"; -import { connectorDetails as CommonConnectorDetails } from "./Commons.js"; +import { connectorDetails as commonConnectorDetails } from "./Commons.js"; import { updateDefaultStatusCode } from "./Modifiers.js"; import { connectorDetails as cybersourceConnectorDetails } from "./Cybersource.js"; import { connectorDetails as datatransConnectorDetails } from "./Datatrans.js"; @@ -32,7 +32,7 @@ const connectorDetails = { bankofamerica: bankOfAmericaConnectorDetails, bluesnap: bluesnapConnectorDetails, checkout: checkoutConnectorDetails, - commons: CommonConnectorDetails, + commons: commonConnectorDetails, cybersource: cybersourceConnectorDetails, deutschebank: deutschebankConnectorDetails, fiservemea: fiservemeaConnectorDetails, @@ -307,3 +307,26 @@ export function updateBusinessProfile( profilePrefix ); } + +export const CONNECTOR_LISTS = { + // Exclusion lists (skip these connectors) + EXCLUDE: { + CONNECTOR_AGNOSTIC_NTID: ["paypal"], + // Add more exclusion lists + }, + + // Inclusion lists (only run for these connectors) + INCLUDE: { + MANDATES_USING_NTID_PROXY: ["cybersource"], + // Add more inclusion lists + }, +}; + +// Helper functions +export const shouldExcludeConnector = (connectorId, list) => { + return list.includes(connectorId); +}; + +export const shouldIncludeConnector = (connectorId, list) => { + return !list.includes(connectorId); +}; diff --git a/cypress-tests/cypress/e2e/spec/Payment/00020-MandatesUsingNTIDProxy.cy.js b/cypress-tests/cypress/e2e/spec/Payment/00020-MandatesUsingNTIDProxy.cy.js index 1554436abfb..67697cac6c1 100644 --- a/cypress-tests/cypress/e2e/spec/Payment/00020-MandatesUsingNTIDProxy.cy.js +++ b/cypress-tests/cypress/e2e/spec/Payment/00020-MandatesUsingNTIDProxy.cy.js @@ -6,11 +6,31 @@ let globalState; let connector; describe("Card - Mandates using Network Transaction Id flow test", () => { - before("seed global state", () => { - cy.task("getGlobalState").then((state) => { - globalState = new State(state); - connector = globalState.get("connectorId"); - }); + before(function () { + // Changed to regular function instead of arrow function + let skip = false; + + cy.task("getGlobalState") + .then((state) => { + globalState = new State(state); + connector = globalState.get("connectorId"); + + // Skip the test if the connector is not in the inclusion list + // This is done because only cybersource is known to support at present + if ( + utils.shouldIncludeConnector( + connector, + utils.CONNECTOR_LISTS.INCLUDE.MANDATES_USING_NTID_PROXY + ) + ) { + skip = true; + } + }) + .then(() => { + if (skip) { + this.skip(); + } + }); }); afterEach("flush global state", () => { @@ -20,12 +40,6 @@ describe("Card - Mandates using Network Transaction Id flow test", () => { context( "Card - NoThreeDS Create and Confirm Automatic MIT payment flow test", () => { - beforeEach(function () { - if (connector !== "cybersource") { - this.skip(); - } - }); - it("Confirm No 3DS MIT", () => { const data = getConnectorDetails(globalState.get("connectorId"))[ "card_pm" @@ -46,12 +60,6 @@ describe("Card - Mandates using Network Transaction Id flow test", () => { context( "Card - NoThreeDS Create and Confirm Manual MIT payment flow test", () => { - beforeEach(function () { - if (connector !== "cybersource") { - this.skip(); - } - }); - it("Confirm No 3DS MIT", () => { const data = getConnectorDetails(globalState.get("connectorId"))[ "card_pm" @@ -72,12 +80,6 @@ describe("Card - Mandates using Network Transaction Id flow test", () => { context( "Card - NoThreeDS Create and Confirm Automatic multiple MITs payment flow test", () => { - beforeEach(function () { - if (connector !== "cybersource") { - this.skip(); - } - }); - it("Confirm No 3DS MIT", () => { const data = getConnectorDetails(globalState.get("connectorId"))[ "card_pm" @@ -114,12 +116,6 @@ describe("Card - Mandates using Network Transaction Id flow test", () => { () => { let shouldContinue = true; - beforeEach(function () { - if (connector !== "cybersource") { - this.skip(); - } - }); - it("Confirm No 3DS MIT 1", () => { const data = getConnectorDetails(globalState.get("connectorId"))[ "card_pm" @@ -177,12 +173,6 @@ describe("Card - Mandates using Network Transaction Id flow test", () => { context( "Card - ThreeDS Create and Confirm Automatic multiple MITs payment flow test", () => { - beforeEach(function () { - if (connector !== "cybersource") { - this.skip(); - } - }); - it("Confirm No 3DS MIT", () => { const data = getConnectorDetails(globalState.get("connectorId"))[ "card_pm" @@ -217,12 +207,6 @@ describe("Card - Mandates using Network Transaction Id flow test", () => { context( "Card - ThreeDS Create and Confirm Manual multiple MITs payment flow", () => { - beforeEach(function () { - if (connector !== "cybersource") { - this.skip(); - } - }); - it("Confirm No 3DS MIT", () => { const data = getConnectorDetails(globalState.get("connectorId"))[ "card_pm" diff --git a/cypress-tests/cypress/e2e/spec/Payment/00022-Variations.cy.js b/cypress-tests/cypress/e2e/spec/Payment/00022-Variations.cy.js index f1499dc93bd..5a1f765cf62 100644 --- a/cypress-tests/cypress/e2e/spec/Payment/00022-Variations.cy.js +++ b/cypress-tests/cypress/e2e/spec/Payment/00022-Variations.cy.js @@ -222,6 +222,14 @@ describe("Corner cases", () => { if (shouldContinue) shouldContinue = utils.should_continue_further(data); }); + it("Retrieve payment", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["No3DSManualCapture"]; + + cy.retrievePaymentCallTest(globalState, data); + }); + it("Capture call", () => { const data = getConnectorDetails(globalState.get("commons"))["card_pm"][ "CaptureGreaterAmount" diff --git a/cypress-tests/cypress/e2e/spec/Payment/00024-ConnectorAgnosticNTID.cy.js b/cypress-tests/cypress/e2e/spec/Payment/00024-ConnectorAgnosticNTID.cy.js index 4db0ad99900..308a8c036ca 100644 --- a/cypress-tests/cypress/e2e/spec/Payment/00024-ConnectorAgnosticNTID.cy.js +++ b/cypress-tests/cypress/e2e/spec/Payment/00024-ConnectorAgnosticNTID.cy.js @@ -4,6 +4,7 @@ import { payment_methods_enabled } from "../../configs/Payment/Commons"; import getConnectorDetails, * as utils from "../../configs/Payment/Utils"; let globalState; +let connector; /* Flow: @@ -32,10 +33,30 @@ Flow: */ describe("Connector Agnostic Tests", () => { - before("seed global state", () => { - cy.task("getGlobalState").then((state) => { - globalState = new State(state); - }); + before(function () { + // Changed to regular function instead of arrow function + let skip = false; + + cy.task("getGlobalState") + .then((state) => { + globalState = new State(state); + connector = globalState.get("connectorId"); + + // Skip running test against a connector that is added in the exclude list + if ( + utils.shouldExcludeConnector( + connector, + utils.CONNECTOR_LISTS.EXCLUDE.CONNECTOR_AGNOSTIC_NTID + ) + ) { + skip = true; + } + }) + .then(() => { + if (skip) { + this.skip(); + } + }); }); after("flush global state", () => { From c044ffff0c47ee5d3ef5f905c3f590fae4ac9a24 Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 6 Feb 2025 15:05:29 +0530 Subject: [PATCH 35/46] chore(connector): [Fiuu] log keys in the PSync response (#7189) --- .../src/connectors/fiuu.rs | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/crates/hyperswitch_connectors/src/connectors/fiuu.rs b/crates/hyperswitch_connectors/src/connectors/fiuu.rs index 70bca41582c..4dc87c244ac 100644 --- a/crates/hyperswitch_connectors/src/connectors/fiuu.rs +++ b/crates/hyperswitch_connectors/src/connectors/fiuu.rs @@ -1,6 +1,10 @@ pub mod transformers; -use std::collections::{HashMap, HashSet}; +use std::{ + any::type_name, + borrow::Cow, + collections::{HashMap, HashSet}, +}; use common_enums::{CaptureMethod, PaymentMethod, PaymentMethodType}; use common_utils::{ @@ -53,6 +57,40 @@ use crate::{ utils::{self, PaymentMethodDataType}, }; +pub fn parse_and_log_keys_in_url_encoded_response(data: &[u8]) { + match std::str::from_utf8(data) { + Ok(query_str) => { + let loggable_keys = [ + "status", + "orderid", + "tranID", + "nbcb", + "amount", + "currency", + "paydate", + "channel", + "error_desc", + "error_code", + "extraP", + ]; + let keys: Vec<(Cow<'_, str>, String)> = + url::form_urlencoded::parse(query_str.as_bytes()) + .map(|(key, value)| { + if loggable_keys.contains(&key.to_string().as_str()) { + (key, value.to_string()) + } else { + (key, "SECRET".to_string()) + } + }) + .collect(); + router_env::logger::info!("Keys in {} response\n{:?}", type_name::(), keys); + } + Err(err) => { + router_env::logger::error!("Failed to convert bytes to string: {:?}", err); + } + } +} + fn parse_response(data: &[u8]) -> Result where T: for<'de> Deserialize<'de>, @@ -87,6 +125,27 @@ where json.insert("miscellaneous".to_string(), misc_value); } + // TODO: Remove this after debugging + let loggable_keys = [ + "StatCode", + "StatName", + "TranID", + "ErrorCode", + "ErrorDesc", + "miscellaneous", + ]; + let keys: Vec<(&str, Value)> = json + .iter() + .map(|(key, value)| { + if loggable_keys.contains(&key.as_str()) { + (key.as_str(), value.to_owned()) + } else { + (key.as_str(), Value::String("SECRET".to_string())) + } + }) + .collect(); + router_env::logger::info!("Keys in response for type {}\n{:?}", type_name::(), keys); + let response: T = serde_json::from_value(Value::Object(json)).map_err(|e| { router_env::logger::error!("Error in Deserializing Response Data: {:?}", e); errors::ConnectorError::ResponseDeserializationFailed @@ -747,6 +806,7 @@ impl webhooks::IncomingWebhook for Fiuu { ) -> CustomResult, errors::ConnectorError> { let header = utils::get_header_key_value("content-type", request.headers)?; let resource: FiuuWebhooksResponse = if header == "application/x-www-form-urlencoded" { + parse_and_log_keys_in_url_encoded_response::(request.body); serde_urlencoded::from_bytes::(request.body) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)? } else { @@ -776,6 +836,7 @@ impl webhooks::IncomingWebhook for Fiuu { ) -> CustomResult, errors::ConnectorError> { let header = utils::get_header_key_value("content-type", request.headers)?; let resource: FiuuWebhooksResponse = if header == "application/x-www-form-urlencoded" { + parse_and_log_keys_in_url_encoded_response::(request.body); serde_urlencoded::from_bytes::(request.body) .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)? } else { @@ -833,6 +894,7 @@ impl webhooks::IncomingWebhook for Fiuu { ) -> CustomResult { let header = utils::get_header_key_value("content-type", request.headers)?; let resource: FiuuWebhooksResponse = if header == "application/x-www-form-urlencoded" { + parse_and_log_keys_in_url_encoded_response::(request.body); serde_urlencoded::from_bytes::(request.body) .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)? } else { @@ -866,6 +928,7 @@ impl webhooks::IncomingWebhook for Fiuu { ) -> CustomResult { let header = utils::get_header_key_value("content-type", request.headers)?; let resource: FiuuWebhooksResponse = if header == "application/x-www-form-urlencoded" { + parse_and_log_keys_in_url_encoded_response::(request.body); serde_urlencoded::from_bytes::(request.body) .change_context(errors::ConnectorError::WebhookEventTypeNotFound)? } else { @@ -891,6 +954,7 @@ impl webhooks::IncomingWebhook for Fiuu { ) -> CustomResult, errors::ConnectorError> { let header = utils::get_header_key_value("content-type", request.headers)?; let payload: FiuuWebhooksResponse = if header == "application/x-www-form-urlencoded" { + parse_and_log_keys_in_url_encoded_response::(request.body); serde_urlencoded::from_bytes::(request.body) .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)? } else { @@ -921,6 +985,9 @@ impl webhooks::IncomingWebhook for Fiuu { Option, errors::ConnectorError, > { + parse_and_log_keys_in_url_encoded_response::( + request.body, + ); let webhook_payment_response: transformers::FiuuWebhooksPaymentResponse = serde_urlencoded::from_bytes::(request.body) .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; From f9a4713a60028e26b98143c6296d9969cd090163 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Thu, 6 Feb 2025 15:57:34 +0530 Subject: [PATCH 36/46] refactor(router): store `network_transaction_id` for `off_session` payments irrespective of the `is_connector_agnostic_mit_enabled` config (#7083) Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> Co-authored-by: Pa1NarK <69745008+pixincreate@users.noreply.github.com> --- .../payments/operations/payment_response.rs | 30 +- .../router/src/core/payments/tokenization.rs | 11 +- .../Payment/00024-ConnectorAgnosticNTID.cy.js | 369 +++++++++++++++++- 3 files changed, 385 insertions(+), 25 deletions(-) diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 748b71469ba..70d5f79845b 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -576,7 +576,7 @@ impl PostUpdateTracker, types::PaymentsSyncData> for merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, - business_profile: &domain::Profile, + _business_profile: &domain::Profile, ) -> CustomResult<(), errors::ApiErrorResponse> where F: 'b + Clone + Send + Sync, @@ -617,7 +617,6 @@ impl PostUpdateTracker, types::PaymentsSyncData> for resp.status, resp.response.clone(), merchant_account.storage_scheme, - business_profile.is_connector_agnostic_mit_enabled, ) .await?; Ok(()) @@ -1201,7 +1200,7 @@ impl PostUpdateTracker, types::CompleteAuthorizeData merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, payment_data: &mut PaymentData, - business_profile: &domain::Profile, + _business_profile: &domain::Profile, ) -> CustomResult<(), errors::ApiErrorResponse> where F: 'b + Clone + Send + Sync, @@ -1241,7 +1240,6 @@ impl PostUpdateTracker, types::CompleteAuthorizeData resp.status, resp.response.clone(), merchant_account.storage_scheme, - business_profile.is_connector_agnostic_mit_enabled, ) .await?; Ok(()) @@ -2068,7 +2066,6 @@ async fn update_payment_method_status_and_ntid( attempt_status: common_enums::AttemptStatus, payment_response: Result, storage_scheme: enums::MerchantStorageScheme, - is_connector_agnostic_mit_enabled: Option, ) -> RouterResult<()> { todo!() } @@ -2084,7 +2081,6 @@ async fn update_payment_method_status_and_ntid( attempt_status: common_enums::AttemptStatus, payment_response: Result, storage_scheme: enums::MerchantStorageScheme, - is_connector_agnostic_mit_enabled: Option, ) -> RouterResult<()> { // If the payment_method is deleted then ignore the error related to retrieving payment method // This should be handled when the payment method is soft deleted @@ -2119,20 +2115,18 @@ async fn update_payment_method_status_and_ntid( }) .ok() .flatten(); - let network_transaction_id = - if let Some(network_transaction_id) = pm_resp_network_transaction_id { - if is_connector_agnostic_mit_enabled == Some(true) - && payment_data.payment_intent.setup_future_usage - == Some(diesel_models::enums::FutureUsage::OffSession) - { - Some(network_transaction_id) - } else { - logger::info!("Skip storing network transaction id"); - None - } + let network_transaction_id = if payment_data.payment_intent.setup_future_usage + == Some(diesel_models::enums::FutureUsage::OffSession) + { + if pm_resp_network_transaction_id.is_some() { + pm_resp_network_transaction_id } else { + logger::info!("Skip storing network transaction id"); None - }; + } + } else { + None + }; let pm_update = if payment_method.status != common_enums::PaymentMethodStatus::Active && payment_method.status != attempt_status.into() diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 597831231a3..d46977d8c99 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -111,12 +111,11 @@ where }; let network_transaction_id = - if let Some(network_transaction_id) = network_transaction_id { - if business_profile.is_connector_agnostic_mit_enabled == Some(true) - && save_payment_method_data.request.get_setup_future_usage() - == Some(storage_enums::FutureUsage::OffSession) - { - Some(network_transaction_id) + if save_payment_method_data.request.get_setup_future_usage() + == Some(storage_enums::FutureUsage::OffSession) + { + if network_transaction_id.is_some() { + network_transaction_id } else { logger::info!("Skip storing network transaction id"); None diff --git a/cypress-tests/cypress/e2e/spec/Payment/00024-ConnectorAgnosticNTID.cy.js b/cypress-tests/cypress/e2e/spec/Payment/00024-ConnectorAgnosticNTID.cy.js index 308a8c036ca..8491cb0b8c0 100644 --- a/cypress-tests/cypress/e2e/spec/Payment/00024-ConnectorAgnosticNTID.cy.js +++ b/cypress-tests/cypress/e2e/spec/Payment/00024-ConnectorAgnosticNTID.cy.js @@ -13,13 +13,38 @@ Flow: - Make a Payment - List Payment Method for Customer using Client Secret (will get PMID) +- Create Business Profile with connector agnostic feature disabled +- Create Merchant Connector Account +- Create Payment Intent +- List Payment Method for Customer -- Empty list; i.e., no payment method should be listed +- Confirm Payment with PMID from previous step (should fail as Connector Mandate ID is not present in the newly created Profile) + + - Create Business Profile with connector agnostic feature enabled +- Create Merchant Connector Account and Customer +- Make a Payment +- List Payment Method for Customer using Client Secret (will get PMID) + +- Create Business Profile with connector agnostic feature disabled - Create Merchant Connector Account - Create Payment Intent - List Payment Method for Customer -- Empty list; i.e., no payment method should be listed - Confirm Payment with PMID from previous step (should fail as Connector Mandate ID is not present in the newly created Profile) +- Create Business Profile with connector agnostic feature disabled +- Create Merchant Connector Account and Customer +- Make a Payment +- List Payment Method for Customer using Client Secret (will get PMID) + +- Create Business Profile with connector agnostic feature enabled +- Create Merchant Connector Account +- Create Payment Intent +- List Payment Method for Customer using Client Secret (will get PMID which is same as the one from previous step along with Payment Token) +- Confirm Payment with PMID from previous step (should pass as NTID is present in the DB) + + + - Create Business Profile with connector agnostic feature enabled - Create Merchant Connector Account and Customer - Make a Payment @@ -62,8 +87,9 @@ describe("Connector Agnostic Tests", () => { after("flush global state", () => { cy.task("setGlobalState", globalState.data); }); + context( - "Connector Agnostic Disabled for Profile 1 and Enabled for Profile 2", + "Connector Agnostic Disabled for both Profile 1 and Profile 2", () => { let shouldContinue = true; @@ -141,6 +167,121 @@ describe("Connector Agnostic Tests", () => { ); }); + it("Create Payment Intent", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["PaymentIntentOffSession"]; + + cy.createPaymentIntentTest( + fixtures.createPaymentBody, + data, + "no_three_ds", + "automatic", + globalState + ); + + if (shouldContinue) + shouldContinue = utils.should_continue_further(data); + }); + + it("List Payment Method for Customer", () => { + cy.listCustomerPMByClientSecret(globalState); + }); + + it("Confirm No 3DS MIT (PMID)", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + const commonData = getConnectorDetails(globalState.get("commons"))[ + "card_pm" + ]["MITAutoCapture"]; + + const newData = { + ...data, + Response: utils.getConnectorFlowDetails( + data, + commonData, + "ResponseCustom" + ), + }; + + cy.mitUsingPMId( + fixtures.pmIdConfirmBody, + newData, + 7000, + true, + "automatic", + globalState + ); + }); + + it("Create Payment Intent", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["PaymentIntentOffSession"]; + + cy.createPaymentIntentTest( + fixtures.createPaymentBody, + data, + "no_three_ds", + "automatic", + globalState + ); + + if (shouldContinue) + shouldContinue = utils.should_continue_further(data); + }); + + it("List Payment Method for Customer", () => { + cy.listCustomerPMByClientSecret(globalState); + }); + + it("Confirm No 3DS MIT (Token)", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["SaveCardConfirmAutoCaptureOffSession"]; + const commonData = getConnectorDetails(globalState.get("commons"))[ + "card_pm" + ]["SaveCardConfirmAutoCaptureOffSession"]; + + const newData = { + ...data, + Response: utils.getConnectorFlowDetails( + data, + commonData, + "ResponseCustom" + ), + }; + cy.saveCardConfirmCallTest( + fixtures.saveCardConfirmBody, + newData, + globalState + ); + + if (shouldContinue) + shouldContinue = utils.should_continue_further(data); + }); + } + ); + + context( + "Connector Agnostic Enabled for Profile 1 and Disabled for Profile 2", + () => { + let shouldContinue = true; + + beforeEach(function () { + if (!shouldContinue) { + this.skip(); + } + }); + + it("Create business profile", () => { + utils.createBusinessProfile( + fixtures.businessProfile.bpCreate, + globalState + ); + }); + it("Enable Connector Agnostic for Business Profile", () => { utils.updateBusinessProfile( fixtures.businessProfile.bpUpdate, @@ -153,6 +294,67 @@ describe("Connector Agnostic Tests", () => { ); }); + it("Create merchant connector account", () => { + utils.createMerchantConnectorAccount( + "payment_processor", + fixtures.createConnectorBody, + globalState, + payment_methods_enabled + ); + }); + + it("Create Customer", () => { + cy.createCustomerCallTest(fixtures.customerCreateBody, globalState); + }); + + it("Create Payment Intent", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["PaymentIntentOffSession"]; + + cy.createPaymentIntentTest( + fixtures.createPaymentBody, + data, + "no_three_ds", + "automatic", + globalState + ); + + if (shouldContinue) + shouldContinue = utils.should_continue_further(data); + }); + + it("Confirm Payment", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["SaveCardUseNo3DSAutoCaptureOffSession"]; + + cy.confirmCallTest(fixtures.confirmBody, data, true, globalState); + + if (shouldContinue) + shouldContinue = utils.should_continue_further(data); + }); + + it("List Payment Method for Customer using Client Secret", () => { + cy.listCustomerPMByClientSecret(globalState); + }); + + it("Create business profile", () => { + utils.createBusinessProfile( + fixtures.businessProfile.bpCreate, + globalState + ); + }); + + it("Create merchant connector account", () => { + utils.createMerchantConnectorAccount( + "payment_processor", + fixtures.createConnectorBody, + globalState, + payment_methods_enabled + ); + }); + it("Create Payment Intent", () => { const data = getConnectorDetails(globalState.get("connectorId"))[ "card_pm" @@ -250,6 +452,171 @@ describe("Connector Agnostic Tests", () => { } ); + context( + "Connector Agnostic Disabled for Profile 1 and Enabled for Profile 2", + () => { + let shouldContinue = true; + + beforeEach(function () { + if (!shouldContinue) { + this.skip(); + } + }); + + it("Create business profile", () => { + utils.createBusinessProfile( + fixtures.businessProfile.bpCreate, + globalState + ); + }); + + it("Create merchant connector account", () => { + utils.createMerchantConnectorAccount( + "payment_processor", + fixtures.createConnectorBody, + globalState, + payment_methods_enabled + ); + }); + + it("Create Customer", () => { + cy.createCustomerCallTest(fixtures.customerCreateBody, globalState); + }); + + it("Create Payment Intent", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["PaymentIntentOffSession"]; + + cy.createPaymentIntentTest( + fixtures.createPaymentBody, + data, + "no_three_ds", + "automatic", + globalState + ); + + if (shouldContinue) + shouldContinue = utils.should_continue_further(data); + }); + + it("Confirm Payment", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["SaveCardUseNo3DSAutoCaptureOffSession"]; + + cy.confirmCallTest(fixtures.confirmBody, data, true, globalState); + + if (shouldContinue) + shouldContinue = utils.should_continue_further(data); + }); + + it("List Payment Method for Customer using Client Secret", () => { + cy.listCustomerPMByClientSecret(globalState); + }); + + it("Create business profile", () => { + utils.createBusinessProfile( + fixtures.businessProfile.bpCreate, + globalState + ); + }); + + it("Create merchant connector account", () => { + utils.createMerchantConnectorAccount( + "payment_processor", + fixtures.createConnectorBody, + globalState, + payment_methods_enabled + ); + }); + + it("Enable Connector Agnostic for Business Profile", () => { + utils.updateBusinessProfile( + fixtures.businessProfile.bpUpdate, + true, // is_connector_agnostic_enabled + false, // collect_billing_address_from_wallet_connector + false, // collect_shipping_address_from_wallet_connector + false, // always_collect_billing_address_from_wallet_connector + false, // always_collect_shipping_address_from_wallet_connector + globalState + ); + }); + + it("Create Payment Intent", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["PaymentIntentOffSession"]; + + cy.createPaymentIntentTest( + fixtures.createPaymentBody, + data, + "no_three_ds", + "automatic", + globalState + ); + + if (shouldContinue) + shouldContinue = utils.should_continue_further(data); + }); + + it("List Payment Method for Customer", () => { + cy.listCustomerPMByClientSecret(globalState); + }); + + it("Confirm No 3DS MIT (PMID)", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["MITAutoCapture"]; + + cy.mitUsingPMId( + fixtures.pmIdConfirmBody, + data, + 7000, + true, + "automatic", + globalState + ); + }); + + it("Create Payment Intent", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["PaymentIntentOffSession"]; + + cy.createPaymentIntentTest( + fixtures.createPaymentBody, + data, + "no_three_ds", + "automatic", + globalState + ); + + if (shouldContinue) + shouldContinue = utils.should_continue_further(data); + }); + + it("List Payment Method for Customer", () => { + cy.listCustomerPMByClientSecret(globalState); + }); + + it("Confirm No 3DS MIT (Token)", () => { + const data = getConnectorDetails(globalState.get("connectorId"))[ + "card_pm" + ]["SaveCardConfirmAutoCaptureOffSession"]; + + cy.saveCardConfirmCallTest( + fixtures.saveCardConfirmBody, + data, + globalState + ); + + if (shouldContinue) + shouldContinue = utils.should_continue_further(data); + }); + } + ); + context("Connector Agnostic Enabled for Profile 1 and Profile 2", () => { let shouldContinue = true; From 775dcc5a4e3b41dd1e4d0e4c47eccca15a8a4b3a Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 6 Feb 2025 19:13:13 +0530 Subject: [PATCH 37/46] chore(roles): remove redundant variant from PermissionGroup (#6985) --- crates/common_enums/src/enums.rs | 2 -- crates/router/src/services/authorization/info.rs | 6 +++--- .../router/src/services/authorization/permission_groups.rs | 5 ++--- .../down.sql | 1 + .../up.sql | 3 +++ 5 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 migrations/2025-01-03-104019_migrate_permission_group_for_recon/down.sql create mode 100644 migrations/2025-01-03-104019_migrate_permission_group_for_recon/up.sql diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 0234fbea6d5..1faa3b90aa8 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -2936,8 +2936,6 @@ pub enum PermissionGroup { ReconReportsManage, ReconOpsView, ReconOpsManage, - // TODO: To be deprecated, make sure DB is migrated before removing - ReconOps, } #[derive(Clone, Debug, serde::Serialize, PartialEq, Eq, Hash, strum::EnumIter)] diff --git a/crates/router/src/services/authorization/info.rs b/crates/router/src/services/authorization/info.rs index e02838b3e00..b4413dfa3b3 100644 --- a/crates/router/src/services/authorization/info.rs +++ b/crates/router/src/services/authorization/info.rs @@ -40,10 +40,10 @@ fn get_group_description(group: PermissionGroup) -> &'static str { PermissionGroup::MerchantDetailsView | PermissionGroup::AccountView => "View Merchant Details", PermissionGroup::MerchantDetailsManage | PermissionGroup::AccountManage => "Create, modify and delete Merchant Details like api keys, webhooks, etc", PermissionGroup::OrganizationManage => "Manage organization level tasks like create new Merchant accounts, Organization level roles, etc", - PermissionGroup::ReconReportsView => "View and access reconciliation reports and analytics", + PermissionGroup::ReconReportsView => "View reconciliation reports and analytics", PermissionGroup::ReconReportsManage => "Manage reconciliation reports", - PermissionGroup::ReconOpsView => "View and access reconciliation operations", - PermissionGroup::ReconOpsManage | PermissionGroup::ReconOps => "Manage reconciliation operations", + PermissionGroup::ReconOpsView => "View and access all reconciliation operations including reports and analytics", + PermissionGroup::ReconOpsManage => "Manage all reconciliation operations including reports and analytics", } } diff --git a/crates/router/src/services/authorization/permission_groups.rs b/crates/router/src/services/authorization/permission_groups.rs index ceb943950d5..0cdb68ec8d7 100644 --- a/crates/router/src/services/authorization/permission_groups.rs +++ b/crates/router/src/services/authorization/permission_groups.rs @@ -33,7 +33,6 @@ impl PermissionGroupExt for PermissionGroup { | Self::OrganizationManage | Self::AccountManage | Self::ReconOpsManage - | Self::ReconOps | Self::ReconReportsManage => PermissionScope::Write, } } @@ -50,7 +49,7 @@ impl PermissionGroupExt for PermissionGroup { | Self::MerchantDetailsManage | Self::AccountView | Self::AccountManage => ParentGroup::Account, - Self::ReconOpsView | Self::ReconOpsManage | Self::ReconOps => ParentGroup::ReconOps, + Self::ReconOpsView | Self::ReconOpsManage => ParentGroup::ReconOps, Self::ReconReportsView | Self::ReconReportsManage => ParentGroup::ReconReports, } } @@ -86,7 +85,7 @@ impl PermissionGroupExt for PermissionGroup { } Self::ReconOpsView => vec![Self::ReconOpsView], - Self::ReconOpsManage | Self::ReconOps => vec![Self::ReconOpsView, Self::ReconOpsManage], + Self::ReconOpsManage => vec![Self::ReconOpsView, Self::ReconOpsManage], Self::ReconReportsView => vec![Self::ReconReportsView], Self::ReconReportsManage => vec![Self::ReconReportsView, Self::ReconReportsManage], diff --git a/migrations/2025-01-03-104019_migrate_permission_group_for_recon/down.sql b/migrations/2025-01-03-104019_migrate_permission_group_for_recon/down.sql new file mode 100644 index 00000000000..e0ac49d1ecf --- /dev/null +++ b/migrations/2025-01-03-104019_migrate_permission_group_for_recon/down.sql @@ -0,0 +1 @@ +SELECT 1; diff --git a/migrations/2025-01-03-104019_migrate_permission_group_for_recon/up.sql b/migrations/2025-01-03-104019_migrate_permission_group_for_recon/up.sql new file mode 100644 index 00000000000..0fa04632dce --- /dev/null +++ b/migrations/2025-01-03-104019_migrate_permission_group_for_recon/up.sql @@ -0,0 +1,3 @@ +UPDATE roles +SET groups = array_replace(groups, 'recon_ops', 'recon_ops_manage') +WHERE 'recon_ops' = ANY(groups); From dddb1b06bea4ac89d838641508728d2da4326ba1 Mon Sep 17 00:00:00 2001 From: awasthi21 <107559116+awasthi21@users.noreply.github.com> Date: Thu, 6 Feb 2025 19:13:36 +0530 Subject: [PATCH 38/46] feat(connector): [COINGATE] Add Template PR (#7052) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 2 + config/deployments/integration_test.toml | 1 + config/deployments/production.toml | 1 + config/deployments/sandbox.toml | 1 + config/development.toml | 2 + config/docker_compose.toml | 2 + crates/common_enums/src/connector_enums.rs | 1 + .../hyperswitch_connectors/src/connectors.rs | 21 +- .../src/connectors/coingate.rs | 571 ++++++++++++++++++ .../src/connectors/coingate/transformers.rs | 228 +++++++ .../src/default_implementations.rs | 35 ++ .../src/default_implementations_v2.rs | 22 + crates/hyperswitch_interfaces/src/configs.rs | 1 + crates/router/src/connector.rs | 32 +- crates/router/src/core/payments/flows.rs | 3 + crates/router/src/types/api.rs | 1 + crates/router/src/types/transformers.rs | 1 + crates/router/tests/connectors/coingate.rs | 420 +++++++++++++ crates/test_utils/src/connector_auth.rs | 1 + loadtest/config/development.toml | 2 + scripts/add_connector.sh | 2 +- 21 files changed, 1323 insertions(+), 27 deletions(-) create mode 100644 crates/hyperswitch_connectors/src/connectors/coingate.rs create mode 100644 crates/hyperswitch_connectors/src/connectors/coingate/transformers.rs create mode 100644 crates/router/tests/connectors/coingate.rs diff --git a/config/config.example.toml b/config/config.example.toml index e982a01eb11..7bb6d8fc567 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -196,6 +196,7 @@ cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" chargebee.base_url = "https://$.chargebee.com/api/" checkout.base_url = "https://api.sandbox.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" +coingate.base_url = "https://api-sandbox.coingate.com/v2" cryptopay.base_url = "https://business-sandbox.cryptopay.me" cybersource.base_url = "https://apitest.cybersource.com/" datatrans.base_url = "https://api.sandbox.datatrans.com/" @@ -310,6 +311,7 @@ cards = [ "adyenplatform", "authorizedotnet", "coinbase", + "coingate", "cryptopay", "braintree", "checkout", diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 5791c6aa83e..4d0bce877e8 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -42,6 +42,7 @@ cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" chargebee.base_url = "https://$.chargebee.com/api/" checkout.base_url = "https://api.sandbox.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" +coingate.base_url = "https://api-sandbox.coingate.com/v2" cryptopay.base_url = "https://business-sandbox.cryptopay.me" cybersource.base_url = "https://apitest.cybersource.com/" datatrans.base_url = "https://api.sandbox.datatrans.com/" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 78590f05f1b..8964fbc2e20 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -46,6 +46,7 @@ cashtocode.base_url = "https://cluster14.api.cashtocode.com" chargebee.base_url = "https://$.chargebee.com/api/" checkout.base_url = "https://api.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" +coingate.base_url = "https://api.coingate.com/v2" cryptopay.base_url = "https://business.cryptopay.me/" cybersource.base_url = "https://api.cybersource.com/" datatrans.base_url = "https://api.datatrans.com/" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 4127a694fcb..e98bd1e2fad 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -46,6 +46,7 @@ cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" checkout.base_url = "https://api.sandbox.checkout.com/" chargebee.base_url = "https://$.chargebee.com/api/" coinbase.base_url = "https://api.commerce.coinbase.com" +coingate.base_url = "https://api-sandbox.coingate.com/v2" cryptopay.base_url = "https://business-sandbox.cryptopay.me" cybersource.base_url = "https://apitest.cybersource.com/" datatrans.base_url = "https://api.sandbox.datatrans.com/" diff --git a/config/development.toml b/config/development.toml index 473c453d1c8..f61321fda0f 100644 --- a/config/development.toml +++ b/config/development.toml @@ -108,6 +108,7 @@ cards = [ "braintree", "checkout", "coinbase", + "coingate", "cryptopay", "cybersource", "datatrans", @@ -215,6 +216,7 @@ cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" chargebee.base_url = "https://$.chargebee.com/api/" checkout.base_url = "https://api.sandbox.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" +coingate.base_url = "https://api-sandbox.coingate.com/v2" cryptopay.base_url = "https://business-sandbox.cryptopay.me" cybersource.base_url = "https://apitest.cybersource.com/" datatrans.base_url = "https://api.sandbox.datatrans.com/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 12b8df6683c..ed8f99d20df 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -128,6 +128,7 @@ cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" chargebee.base_url = "https://$.chargebee.com/api/" checkout.base_url = "https://api.sandbox.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" +coingate.base_url = "https://api-sandbox.coingate.com/v2" cryptopay.base_url = "https://business-sandbox.cryptopay.me" cybersource.base_url = "https://apitest.cybersource.com/" datatrans.base_url = "https://api.sandbox.datatrans.com/" @@ -233,6 +234,7 @@ cards = [ "braintree", "checkout", "coinbase", + "coingate", "cryptopay", "cybersource", "datatrans", diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index 28dbf33d97d..a0e606e0741 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -68,6 +68,7 @@ pub enum RoutableConnectors { // Chargebee, Checkout, Coinbase, + // Coingate, Cryptopay, Cybersource, Datatrans, diff --git a/crates/hyperswitch_connectors/src/connectors.rs b/crates/hyperswitch_connectors/src/connectors.rs index 15345c8221f..e9738a4907a 100644 --- a/crates/hyperswitch_connectors/src/connectors.rs +++ b/crates/hyperswitch_connectors/src/connectors.rs @@ -10,6 +10,7 @@ pub mod boku; pub mod cashtocode; pub mod chargebee; pub mod coinbase; +pub mod coingate; pub mod cryptopay; pub mod ctp_mastercard; pub mod cybersource; @@ -61,16 +62,16 @@ pub use self::{ airwallex::Airwallex, amazonpay::Amazonpay, bambora::Bambora, bamboraapac::Bamboraapac, bankofamerica::Bankofamerica, billwerk::Billwerk, bitpay::Bitpay, bluesnap::Bluesnap, boku::Boku, cashtocode::Cashtocode, chargebee::Chargebee, coinbase::Coinbase, - cryptopay::Cryptopay, ctp_mastercard::CtpMastercard, cybersource::Cybersource, - datatrans::Datatrans, deutschebank::Deutschebank, digitalvirgo::Digitalvirgo, dlocal::Dlocal, - elavon::Elavon, fiserv::Fiserv, fiservemea::Fiservemea, fiuu::Fiuu, forte::Forte, - globepay::Globepay, gocardless::Gocardless, helcim::Helcim, inespay::Inespay, - jpmorgan::Jpmorgan, mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, - nexixpay::Nexixpay, nomupay::Nomupay, novalnet::Novalnet, paybox::Paybox, payeezy::Payeezy, - payu::Payu, placetopay::Placetopay, powertranz::Powertranz, prophetpay::Prophetpay, - rapyd::Rapyd, razorpay::Razorpay, redsys::Redsys, shift4::Shift4, square::Square, stax::Stax, - taxjar::Taxjar, thunes::Thunes, tsys::Tsys, - unified_authentication_service::UnifiedAuthenticationService, volt::Volt, + coingate::Coingate, cryptopay::Cryptopay, ctp_mastercard::CtpMastercard, + cybersource::Cybersource, datatrans::Datatrans, deutschebank::Deutschebank, + digitalvirgo::Digitalvirgo, dlocal::Dlocal, elavon::Elavon, fiserv::Fiserv, + fiservemea::Fiservemea, fiuu::Fiuu, forte::Forte, globepay::Globepay, gocardless::Gocardless, + helcim::Helcim, inespay::Inespay, jpmorgan::Jpmorgan, mollie::Mollie, + multisafepay::Multisafepay, nexinets::Nexinets, nexixpay::Nexixpay, nomupay::Nomupay, + novalnet::Novalnet, paybox::Paybox, payeezy::Payeezy, payu::Payu, placetopay::Placetopay, + powertranz::Powertranz, prophetpay::Prophetpay, rapyd::Rapyd, razorpay::Razorpay, + redsys::Redsys, shift4::Shift4, square::Square, stax::Stax, taxjar::Taxjar, thunes::Thunes, + tsys::Tsys, unified_authentication_service::UnifiedAuthenticationService, volt::Volt, wellsfargo::Wellsfargo, worldline::Worldline, worldpay::Worldpay, xendit::Xendit, zen::Zen, zsl::Zsl, }; diff --git a/crates/hyperswitch_connectors/src/connectors/coingate.rs b/crates/hyperswitch_connectors/src/connectors/coingate.rs new file mode 100644 index 00000000000..e3bece084b3 --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/coingate.rs @@ -0,0 +1,571 @@ +pub mod transformers; + +use common_utils::{ + errors::CustomResult, + ext_traits::BytesExt, + request::{Method, Request, RequestBuilder, RequestContent}, + types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, +}; +use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::{ + router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, + router_flow_types::{ + access_token_auth::AccessTokenAuth, + payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void}, + refunds::{Execute, RSync}, + }, + router_request_types::{ + AccessTokenRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData, + PaymentsCancelData, PaymentsCaptureData, PaymentsSessionData, PaymentsSyncData, + RefundsData, SetupMandateRequestData, + }, + router_response_types::{PaymentsResponseData, RefundsResponseData}, + types::{ + PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, + RefundSyncRouterData, RefundsRouterData, + }, +}; +use hyperswitch_interfaces::{ + api::{ + self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorSpecifications, + ConnectorValidation, + }, + configs::Connectors, + errors, + events::connector_api_logs::ConnectorEvent, + types::{self, Response}, + webhooks, +}; +use masking::{ExposeInterface, Mask}; +use transformers as coingate; + +use crate::{constants::headers, types::ResponseRouterData, utils}; + +#[derive(Clone)] +pub struct Coingate { + amount_converter: &'static (dyn AmountConvertor + Sync), +} + +impl Coingate { + pub fn new() -> &'static Self { + &Self { + amount_converter: &StringMinorUnitForConnector, + } + } +} + +impl api::Payment for Coingate {} +impl api::PaymentSession for Coingate {} +impl api::ConnectorAccessToken for Coingate {} +impl api::MandateSetup for Coingate {} +impl api::PaymentAuthorize for Coingate {} +impl api::PaymentSync for Coingate {} +impl api::PaymentCapture for Coingate {} +impl api::PaymentVoid for Coingate {} +impl api::Refund for Coingate {} +impl api::RefundExecute for Coingate {} +impl api::RefundSync for Coingate {} +impl api::PaymentToken for Coingate {} + +impl ConnectorIntegration + for Coingate +{ + // Not Implemented (R) +} + +impl ConnectorCommonExt for Coingate +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &RouterData, + _connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + +impl ConnectorCommon for Coingate { + fn id(&self) -> &'static str { + "coingate" + } + + fn get_currency_unit(&self) -> api::CurrencyUnit { + api::CurrencyUnit::Base + // TODO! Check connector documentation, on which unit they are processing the currency. + // If the connector accepts amount in lower unit ( i.e cents for USD) then return api::CurrencyUnit::Minor, + // if connector accepts amount in base unit (i.e dollars for USD) then return api::CurrencyUnit::Base + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + + fn base_url<'a>(&self, connectors: &'a Connectors) -> &'a str { + connectors.coingate.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = coingate::CoingateAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![( + headers::AUTHORIZATION.to_string(), + auth.api_key.expose().into_masked(), + )]) + } + + fn build_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + let response: coingate::CoingateErrorResponse = res + .response + .parse_struct("CoingateErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + + Ok(ErrorResponse { + status_code: res.status_code, + code: response.code, + message: response.message, + reason: response.reason, + attempt_status: None, + connector_transaction_id: None, + }) + } +} + +impl ConnectorValidation for Coingate { + //TODO: implement functions when support enabled +} + +impl ConnectorIntegration for Coingate { + //TODO: implement sessions flow +} + +impl ConnectorIntegration for Coingate {} + +impl ConnectorIntegration + for Coingate +{ +} + +impl ConnectorIntegration for Coingate { + fn get_headers( + &self, + req: &PaymentsAuthorizeRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &PaymentsAuthorizeRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &PaymentsAuthorizeRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let amount = utils::convert_amount( + self.amount_converter, + req.request.minor_amount, + req.request.currency, + )?; + + let connector_router_data = coingate::CoingateRouterData::from((amount, req)); + let connector_req = coingate::CoingatePaymentsRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &PaymentsAuthorizeRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsAuthorizeType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsAuthorizeRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: coingate::CoingatePaymentsResponse = res + .response + .parse_struct("Coingate PaymentsAuthorizeResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl ConnectorIntegration for Coingate { + fn get_headers( + &self, + req: &PaymentsSyncRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &PaymentsSyncRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn build_request( + &self, + req: &PaymentsSyncRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Get) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsSyncRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: coingate::CoingatePaymentsResponse = res + .response + .parse_struct("coingate PaymentsSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl ConnectorIntegration for Coingate { + fn get_headers( + &self, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &PaymentsCaptureRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + _req: &PaymentsCaptureRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + } + + fn build_request( + &self, + req: &PaymentsCaptureRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsCaptureType::get_headers( + self, req, connectors, + )?) + .set_body(types::PaymentsCaptureType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &PaymentsCaptureRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: coingate::CoingatePaymentsResponse = res + .response + .parse_struct("Coingate PaymentsCaptureResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl ConnectorIntegration for Coingate {} + +impl ConnectorIntegration for Coingate { + fn get_headers( + &self, + req: &RefundsRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &RefundsRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn get_request_body( + &self, + req: &RefundsRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let refund_amount = utils::convert_amount( + self.amount_converter, + req.request.minor_refund_amount, + req.request.currency, + )?; + + let connector_router_data = coingate::CoingateRouterData::from((refund_amount, req)); + let connector_req = coingate::CoingateRefundRequest::try_from(&connector_router_data)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &RefundsRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = RequestBuilder::new() + .method(Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .set_body(types::RefundExecuteType::get_request_body( + self, req, connectors, + )?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &RefundsRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + let response: coingate::RefundResponse = res + .response + .parse_struct("coingate RefundResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +impl ConnectorIntegration for Coingate { + fn get_headers( + &self, + req: &RefundSyncRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &RefundSyncRouterData, + _connectors: &Connectors, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + } + + fn build_request( + &self, + req: &RefundSyncRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Get) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .set_body(types::RefundSyncType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &RefundSyncRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: coingate::RefundResponse = res + .response + .parse_struct("coingate RefundSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + +#[async_trait::async_trait] +impl webhooks::IncomingWebhook for Coingate { + fn get_webhook_object_reference_id( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } + + fn get_webhook_event_type( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } + + fn get_webhook_resource_object( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } +} + +impl ConnectorSpecifications for Coingate {} diff --git a/crates/hyperswitch_connectors/src/connectors/coingate/transformers.rs b/crates/hyperswitch_connectors/src/connectors/coingate/transformers.rs new file mode 100644 index 00000000000..08b9ae781e1 --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/coingate/transformers.rs @@ -0,0 +1,228 @@ +use common_enums::enums; +use common_utils::types::StringMinorUnit; +use hyperswitch_domain_models::{ + payment_method_data::PaymentMethodData, + router_data::{ConnectorAuthType, RouterData}, + router_flow_types::refunds::{Execute, RSync}, + router_request_types::ResponseId, + router_response_types::{PaymentsResponseData, RefundsResponseData}, + types::{PaymentsAuthorizeRouterData, RefundsRouterData}, +}; +use hyperswitch_interfaces::errors; +use masking::Secret; +use serde::{Deserialize, Serialize}; + +use crate::{ + types::{RefundsResponseRouterData, ResponseRouterData}, + utils::PaymentsAuthorizeRequestData, +}; + +//TODO: Fill the struct with respective fields +pub struct CoingateRouterData { + pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub router_data: T, +} + +impl From<(StringMinorUnit, T)> for CoingateRouterData { + fn from((amount, item): (StringMinorUnit, T)) -> Self { + //Todo : use utils to convert the amount to the type of amount that a connector accepts + Self { + amount, + router_data: item, + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct CoingatePaymentsRequest { + amount: StringMinorUnit, + card: CoingateCard, +} + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct CoingateCard { + number: cards::CardNumber, + expiry_month: Secret, + expiry_year: Secret, + cvc: Secret, + complete: bool, +} + +impl TryFrom<&CoingateRouterData<&PaymentsAuthorizeRouterData>> for CoingatePaymentsRequest { + type Error = error_stack::Report; + fn try_from( + item: &CoingateRouterData<&PaymentsAuthorizeRouterData>, + ) -> Result { + match item.router_data.request.payment_method_data.clone() { + PaymentMethodData::Card(req_card) => { + let card = CoingateCard { + number: req_card.card_number, + expiry_month: req_card.card_exp_month, + expiry_year: req_card.card_exp_year, + cvc: req_card.card_cvc, + complete: item.router_data.request.is_auto_capture()?, + }; + Ok(Self { + amount: item.amount.clone(), + card, + }) + } + _ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()), + } + } +} + +//TODO: Fill the struct with respective fields +// Auth Struct +pub struct CoingateAuthType { + pub(super) api_key: Secret, +} + +impl TryFrom<&ConnectorAuthType> for CoingateAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &ConnectorAuthType) -> Result { + match auth_type { + ConnectorAuthType::HeaderKey { api_key } => Ok(Self { + api_key: api_key.to_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} +// PaymentsResponse +//TODO: Append the remaining status flags +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum CoingatePaymentStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for common_enums::AttemptStatus { + fn from(item: CoingatePaymentStatus) -> Self { + match item { + CoingatePaymentStatus::Succeeded => Self::Charged, + CoingatePaymentStatus::Failed => Self::Failure, + CoingatePaymentStatus::Processing => Self::Authorizing, + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct CoingatePaymentsResponse { + status: CoingatePaymentStatus, + id: String, +} + +impl TryFrom> + for RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + status: common_enums::AttemptStatus::from(item.response.status), + response: Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.id), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + incremental_authorization_allowed: None, + charge_id: None, + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +// REFUND : +// Type definition for RefundRequest +#[derive(Default, Debug, Serialize)] +pub struct CoingateRefundRequest { + pub amount: StringMinorUnit, +} + +impl TryFrom<&CoingateRouterData<&RefundsRouterData>> for CoingateRefundRequest { + type Error = error_stack::Report; + fn try_from(item: &CoingateRouterData<&RefundsRouterData>) -> Result { + Ok(Self { + amount: item.amount.to_owned(), + }) + } +} + +// Type definition for Refund Response + +#[allow(dead_code)] +#[derive(Debug, Serialize, Default, Deserialize, Clone)] +pub enum RefundStatus { + Succeeded, + Failed, + #[default] + Processing, +} + +impl From for enums::RefundStatus { + fn from(item: RefundStatus) -> Self { + match item { + RefundStatus::Succeeded => Self::Success, + RefundStatus::Failed => Self::Failure, + RefundStatus::Processing => Self::Pending, + //TODO: Review mapping + } + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct RefundResponse { + id: String, + status: RefundStatus, +} + +impl TryFrom> for RefundsRouterData { + type Error = error_stack::Report; + fn try_from( + item: RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id: item.response.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +impl TryFrom> for RefundsRouterData { + type Error = error_stack::Report; + fn try_from( + item: RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(RefundsResponseData { + connector_refund_id: item.response.id.to_string(), + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +//TODO: Fill the struct with respective fields +#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct CoingateErrorResponse { + pub status_code: u16, + pub code: String, + pub message: String, + pub reason: Option, +} diff --git a/crates/hyperswitch_connectors/src/default_implementations.rs b/crates/hyperswitch_connectors/src/default_implementations.rs index f58a4802d50..cfc65de64e8 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -107,6 +107,7 @@ default_imp_for_authorize_session_token!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -181,6 +182,7 @@ default_imp_for_calculate_tax!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -255,6 +257,7 @@ default_imp_for_session_update!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -330,6 +333,7 @@ default_imp_for_post_session_tokens!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -404,6 +408,7 @@ default_imp_for_complete_authorize!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Datatrans, connectors::Dlocal, @@ -470,6 +475,7 @@ default_imp_for_incremental_authorization!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Datatrans, connectors::Deutschebank, @@ -544,6 +550,7 @@ default_imp_for_create_customer!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -617,6 +624,7 @@ default_imp_for_connector_redirect_response!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -685,6 +693,7 @@ default_imp_for_pre_processing_steps!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Datatrans, connectors::Deutschebank, @@ -757,6 +766,7 @@ default_imp_for_post_processing_steps!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -833,6 +843,7 @@ default_imp_for_approve!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -909,6 +920,7 @@ default_imp_for_reject!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -985,6 +997,7 @@ default_imp_for_webhook_source_verification!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -1062,6 +1075,7 @@ default_imp_for_accept_dispute!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -1138,6 +1152,7 @@ default_imp_for_submit_evidence!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -1214,6 +1229,7 @@ default_imp_for_defend_dispute!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -1299,6 +1315,7 @@ default_imp_for_file_upload!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -1370,6 +1387,7 @@ default_imp_for_payouts!( connectors::Cryptopay, connectors::Datatrans, connectors::Coinbase, + connectors::Coingate, connectors::Deutschebank, connectors::Digitalvirgo, connectors::Dlocal, @@ -1444,6 +1462,7 @@ default_imp_for_payouts_create!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -1522,6 +1541,7 @@ default_imp_for_payouts_retrieve!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -1600,6 +1620,7 @@ default_imp_for_payouts_eligibility!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -1678,6 +1699,7 @@ default_imp_for_payouts_fulfill!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Datatrans, connectors::Deutschebank, @@ -1755,6 +1777,7 @@ default_imp_for_payouts_cancel!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -1833,6 +1856,7 @@ default_imp_for_payouts_quote!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -1911,6 +1935,7 @@ default_imp_for_payouts_recipient!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -1989,6 +2014,7 @@ default_imp_for_payouts_recipient_account!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -2067,6 +2093,7 @@ default_imp_for_frm_sale!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -2145,6 +2172,7 @@ default_imp_for_frm_checkout!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -2223,6 +2251,7 @@ default_imp_for_frm_transaction!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -2301,6 +2330,7 @@ default_imp_for_frm_fulfillment!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -2379,6 +2409,7 @@ default_imp_for_frm_record_return!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Cybersource, connectors::Datatrans, @@ -2454,6 +2485,7 @@ default_imp_for_revoking_mandates!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::Datatrans, connectors::Deutschebank, @@ -2528,6 +2560,7 @@ default_imp_for_uas_pre_authentication!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -2602,6 +2635,7 @@ default_imp_for_uas_post_authentication!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -2676,6 +2710,7 @@ default_imp_for_uas_authentication!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, diff --git a/crates/hyperswitch_connectors/src/default_implementations_v2.rs b/crates/hyperswitch_connectors/src/default_implementations_v2.rs index 9f0b319bfa1..51607778eca 100644 --- a/crates/hyperswitch_connectors/src/default_implementations_v2.rs +++ b/crates/hyperswitch_connectors/src/default_implementations_v2.rs @@ -217,6 +217,7 @@ default_imp_for_new_connector_integration_payment!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -294,6 +295,7 @@ default_imp_for_new_connector_integration_refund!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -366,6 +368,7 @@ default_imp_for_new_connector_integration_connector_access_token!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -444,6 +447,7 @@ default_imp_for_new_connector_integration_accept_dispute!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Datatrans, @@ -520,6 +524,7 @@ default_imp_for_new_connector_integration_submit_evidence!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Datatrans, @@ -596,6 +601,7 @@ default_imp_for_new_connector_integration_defend_dispute!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -683,6 +689,7 @@ default_imp_for_new_connector_integration_file_upload!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -762,6 +769,7 @@ default_imp_for_new_connector_integration_payouts_create!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -841,6 +849,7 @@ default_imp_for_new_connector_integration_payouts_eligibility!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -920,6 +929,7 @@ default_imp_for_new_connector_integration_payouts_fulfill!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -999,6 +1009,7 @@ default_imp_for_new_connector_integration_payouts_cancel!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -1078,6 +1089,7 @@ default_imp_for_new_connector_integration_payouts_quote!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -1157,6 +1169,7 @@ default_imp_for_new_connector_integration_payouts_recipient!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -1236,6 +1249,7 @@ default_imp_for_new_connector_integration_payouts_sync!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -1315,6 +1329,7 @@ default_imp_for_new_connector_integration_payouts_recipient_account!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -1392,6 +1407,7 @@ default_imp_for_new_connector_integration_webhook_source_verification!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -1471,6 +1487,7 @@ default_imp_for_new_connector_integration_frm_sale!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -1550,6 +1567,7 @@ default_imp_for_new_connector_integration_frm_checkout!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -1629,6 +1647,7 @@ default_imp_for_new_connector_integration_frm_transaction!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -1708,6 +1727,7 @@ default_imp_for_new_connector_integration_frm_fulfillment!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -1787,6 +1807,7 @@ default_imp_for_new_connector_integration_frm_record_return!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, @@ -1863,6 +1884,7 @@ default_imp_for_new_connector_integration_revoking_mandates!( connectors::Cashtocode, connectors::Chargebee, connectors::Coinbase, + connectors::Coingate, connectors::Cryptopay, connectors::CtpMastercard, connectors::Cybersource, diff --git a/crates/hyperswitch_interfaces/src/configs.rs b/crates/hyperswitch_interfaces/src/configs.rs index efd41aaa4ac..c832507f92e 100644 --- a/crates/hyperswitch_interfaces/src/configs.rs +++ b/crates/hyperswitch_interfaces/src/configs.rs @@ -27,6 +27,7 @@ pub struct Connectors { pub chargebee: ConnectorParams, pub checkout: ConnectorParams, pub coinbase: ConnectorParams, + pub coingate: ConnectorParams, pub cryptopay: ConnectorParams, pub ctp_mastercard: NoParams, pub cybersource: ConnectorParams, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index c926401107c..a8cdf2b6cf8 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -37,22 +37,22 @@ pub use hyperswitch_connectors::connectors::{ bamboraapac, bamboraapac::Bamboraapac, bankofamerica, bankofamerica::Bankofamerica, billwerk, billwerk::Billwerk, bitpay, bitpay::Bitpay, bluesnap, bluesnap::Bluesnap, boku, boku::Boku, cashtocode, cashtocode::Cashtocode, chargebee::Chargebee, coinbase, coinbase::Coinbase, - cryptopay, cryptopay::Cryptopay, ctp_mastercard, ctp_mastercard::CtpMastercard, cybersource, - cybersource::Cybersource, datatrans, datatrans::Datatrans, deutschebank, - deutschebank::Deutschebank, digitalvirgo, digitalvirgo::Digitalvirgo, dlocal, dlocal::Dlocal, - elavon, elavon::Elavon, fiserv, fiserv::Fiserv, fiservemea, fiservemea::Fiservemea, fiuu, - fiuu::Fiuu, forte, forte::Forte, globepay, globepay::Globepay, gocardless, - gocardless::Gocardless, helcim, helcim::Helcim, inespay, inespay::Inespay, jpmorgan, - jpmorgan::Jpmorgan, mollie, mollie::Mollie, multisafepay, multisafepay::Multisafepay, nexinets, - nexinets::Nexinets, nexixpay, nexixpay::Nexixpay, nomupay, nomupay::Nomupay, novalnet, - novalnet::Novalnet, paybox, paybox::Paybox, payeezy, payeezy::Payeezy, payu, payu::Payu, - placetopay, placetopay::Placetopay, powertranz, powertranz::Powertranz, prophetpay, - prophetpay::Prophetpay, rapyd, rapyd::Rapyd, razorpay, razorpay::Razorpay, redsys, - redsys::Redsys, shift4, shift4::Shift4, square, square::Square, stax, stax::Stax, taxjar, - taxjar::Taxjar, thunes, thunes::Thunes, tsys, tsys::Tsys, unified_authentication_service, - unified_authentication_service::UnifiedAuthenticationService, volt, volt::Volt, wellsfargo, - wellsfargo::Wellsfargo, worldline, worldline::Worldline, worldpay, worldpay::Worldpay, xendit, - xendit::Xendit, zen, zen::Zen, zsl, zsl::Zsl, + coingate, coingate::Coingate, cryptopay, cryptopay::Cryptopay, ctp_mastercard, + ctp_mastercard::CtpMastercard, cybersource, cybersource::Cybersource, datatrans, + datatrans::Datatrans, deutschebank, deutschebank::Deutschebank, digitalvirgo, + digitalvirgo::Digitalvirgo, dlocal, dlocal::Dlocal, elavon, elavon::Elavon, fiserv, + fiserv::Fiserv, fiservemea, fiservemea::Fiservemea, fiuu, fiuu::Fiuu, forte, forte::Forte, + globepay, globepay::Globepay, gocardless, gocardless::Gocardless, helcim, helcim::Helcim, + inespay, inespay::Inespay, jpmorgan, jpmorgan::Jpmorgan, mollie, mollie::Mollie, multisafepay, + multisafepay::Multisafepay, nexinets, nexinets::Nexinets, nexixpay, nexixpay::Nexixpay, + nomupay, nomupay::Nomupay, novalnet, novalnet::Novalnet, paybox, paybox::Paybox, payeezy, + payeezy::Payeezy, payu, payu::Payu, placetopay, placetopay::Placetopay, powertranz, + powertranz::Powertranz, prophetpay, prophetpay::Prophetpay, rapyd, rapyd::Rapyd, razorpay, + razorpay::Razorpay, redsys, redsys::Redsys, shift4, shift4::Shift4, square, square::Square, + stax, stax::Stax, taxjar, taxjar::Taxjar, thunes, thunes::Thunes, tsys, tsys::Tsys, + unified_authentication_service, unified_authentication_service::UnifiedAuthenticationService, + volt, volt::Volt, wellsfargo, wellsfargo::Wellsfargo, worldline, worldline::Worldline, + worldpay, worldpay::Worldpay, xendit, xendit::Xendit, zen, zen::Zen, zsl, zsl::Zsl, }; #[cfg(feature = "dummy_connector")] diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 0c80c5a70e2..852b5d95209 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -435,6 +435,7 @@ default_imp_for_connector_request_id!( connector::Chargebee, connector::Checkout, connector::Coinbase, + connector::Coingate, connector::Cryptopay, connector::Cybersource, connector::Datatrans, @@ -1528,6 +1529,7 @@ default_imp_for_fraud_check!( connector::Cryptopay, connector::Cybersource, connector::Coinbase, + connector::Coingate, connector::Datatrans, connector::Deutschebank, connector::Digitalvirgo, @@ -2117,6 +2119,7 @@ default_imp_for_connector_authentication!( connector::Checkout, connector::Cryptopay, connector::Coinbase, + connector::Coingate, connector::Cybersource, connector::Datatrans, connector::Deutschebank, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 4dc5da6475f..addb34eb973 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -368,6 +368,7 @@ impl ConnectorData { enums::Connector::Coinbase => { Ok(ConnectorEnum::Old(Box::new(&connector::Coinbase))) } + // enums::Connector::Coingate => Ok(ConnectorEnum::Old(Box::new(connector::Coingate))), enums::Connector::Cryptopay => { Ok(ConnectorEnum::Old(Box::new(connector::Cryptopay::new()))) } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index c8222163b52..3e81d939c14 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -228,6 +228,7 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { // api_enums::Connector::Chargebee => Self::Chargebee, api_enums::Connector::Checkout => Self::Checkout, api_enums::Connector::Coinbase => Self::Coinbase, + // api_enums::Connector::Coingate => Self::Coingate, api_enums::Connector::Cryptopay => Self::Cryptopay, api_enums::Connector::CtpMastercard => { Err(common_utils::errors::ValidationError::InvalidValue { diff --git a/crates/router/tests/connectors/coingate.rs b/crates/router/tests/connectors/coingate.rs new file mode 100644 index 00000000000..572aecb3dd7 --- /dev/null +++ b/crates/router/tests/connectors/coingate.rs @@ -0,0 +1,420 @@ +use masking::Secret; +use router::types::{self, api, storage::enums}; + +use crate::utils::{self, ConnectorActions}; +use test_utils::connector_auth; + +#[derive(Clone, Copy)] +struct CoingateTest; +impl ConnectorActions for CoingateTest {} +impl utils::Connector for CoingateTest { + fn get_data(&self) -> api::ConnectorData { + use router::connector::Coingate; + api::ConnectorData { + connector: Box::new(Coingate::new()), + connector_name: types::Connector::Coingate, + get_token: types::api::GetToken::Connector, + merchant_connector_id: None, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + utils::to_connector_auth_type( + connector_auth::ConnectorAuthentication::new() + .coingate + .expect("Missing connector authentication configuration") + .into(), + ) + } + + fn get_name(&self) -> String { + "coingate".to_string() + } +} + +static CONNECTOR: CoingateTest = CoingateTest {}; + +fn get_default_payment_info() -> Option { + None +} + +fn payment_method_details() -> Option { + None +} + +// Cards Positive Tests +// Creates a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_only_authorize_payment() { + let response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized); +} + +// Captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment(payment_method_details(), None, get_default_payment_info()) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Partially captures a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_capture_authorized_payment() { + let response = CONNECTOR + .authorize_and_capture_payment( + payment_method_details(), + Some(types::PaymentsCaptureData { + amount_to_capture: 50, + ..utils::PaymentCaptureType::default().0 + }), + get_default_payment_info(), + ) + .await + .expect("Capture payment response"); + assert_eq!(response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_authorized_payment() { + let authorize_response = CONNECTOR + .authorize_payment(payment_method_details(), get_default_payment_info()) + .await + .expect("Authorize payment response"); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Authorized, + Some(types::PaymentsSyncData { + connector_transaction_id: types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("PSync response"); + assert_eq!(response.status, enums::AttemptStatus::Authorized,); +} + +// Voids a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_void_authorized_payment() { + let response = CONNECTOR + .authorize_and_void_payment( + payment_method_details(), + Some(types::PaymentsCancelData { + connector_transaction_id: String::from(""), + cancellation_reason: Some("requested_by_customer".to_string()), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .expect("Void payment response"); + assert_eq!(response.status, enums::AttemptStatus::Voided); +} + +// Refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_manually_captured_payment() { + let response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Synchronizes a refund using the manual capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_manually_captured_refund() { + let refund_response = CONNECTOR + .capture_payment_and_refund( + payment_method_details(), + None, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_make_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); +} + +// Synchronizes a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_auto_captured_payment() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let response = CONNECTOR + .psync_retry_till_status_matches( + enums::AttemptStatus::Charged, + Some(types::PaymentsSyncData { + connector_transaction_id: types::ResponseId::ConnectorTransactionId( + txn_id.unwrap(), + ), + capture_method: Some(enums::CaptureMethod::Automatic), + ..Default::default() + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!(response.status, enums::AttemptStatus::Charged,); +} + +// Refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_auto_captured_payment() { + let response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Partially refunds a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_partially_refund_succeeded_payment() { + let refund_response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + refund_response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Creates multiple refunds against a payment using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_refund_succeeded_payment_multiple_times() { + CONNECTOR + .make_payment_and_multiple_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 50, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await; +} + +// Synchronizes a refund using the automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_sync_refund() { + let refund_response = CONNECTOR + .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) + .await + .unwrap(); + let response = CONNECTOR + .rsync_retry_till_status_matches( + enums::RefundStatus::Success, + refund_response.response.unwrap().connector_refund_id, + None, + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap().refund_status, + enums::RefundStatus::Success, + ); +} + +// Cards Negative scenarios +// Creates a payment with incorrect CVC. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_cvc() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_cvc: Secret::new("12345".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's security code is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry month. +#[actix_web::test] +async fn should_fail_payment_for_invalid_exp_month() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: api::PaymentMethodData::Card(api::Card { + card_exp_month: Secret::new("20".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration month is invalid.".to_string(), + ); +} + +// Creates a payment with incorrect expiry year. +#[actix_web::test] +async fn should_fail_payment_for_incorrect_expiry_year() { + let response = CONNECTOR + .make_payment( + Some(types::PaymentsAuthorizeData { + payment_method_data: api::PaymentMethodData::Card(api::Card { + card_exp_year: Secret::new("2000".to_string()), + ..utils::CCardType::default().0 + }), + ..utils::PaymentAuthorizeType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Your card's expiration year is invalid.".to_string(), + ); +} + +// Voids a payment using automatic capture flow (Non 3DS). +#[actix_web::test] +async fn should_fail_void_payment_for_auto_capture() { + let authorize_response = CONNECTOR + .make_payment(payment_method_details(), get_default_payment_info()) + .await + .unwrap(); + assert_eq!(authorize_response.status, enums::AttemptStatus::Charged); + let txn_id = utils::get_connector_transaction_id(authorize_response.response); + assert_ne!(txn_id, None, "Empty connector transaction id"); + let void_response = CONNECTOR + .void_payment(txn_id.unwrap(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + void_response.response.unwrap_err().message, + "You cannot cancel this PaymentIntent because it has a status of succeeded." + ); +} + +// Captures a payment using invalid connector payment id. +#[actix_web::test] +async fn should_fail_capture_for_invalid_payment() { + let capture_response = CONNECTOR + .capture_payment("123456789".to_string(), None, get_default_payment_info()) + .await + .unwrap(); + assert_eq!( + capture_response.response.unwrap_err().message, + String::from("No such payment_intent: '123456789'") + ); +} + +// Refunds a payment with refund amount higher than payment amount. +#[actix_web::test] +async fn should_fail_for_refund_amount_higher_than_payment_amount() { + let response = CONNECTOR + .make_payment_and_refund( + payment_method_details(), + Some(types::RefundsData { + refund_amount: 150, + ..utils::PaymentRefundType::default().0 + }), + get_default_payment_info(), + ) + .await + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Refund amount (₹1.50) is greater than charge amount (₹1.00)", + ); +} + +// Connector dependent test cases goes here + +// [#478]: add unit tests for non 3DS, wallets & webhooks in connector tests diff --git a/crates/test_utils/src/connector_auth.rs b/crates/test_utils/src/connector_auth.rs index 3b02661c5ce..0a3aa4bcff0 100644 --- a/crates/test_utils/src/connector_auth.rs +++ b/crates/test_utils/src/connector_auth.rs @@ -30,6 +30,7 @@ pub struct ConnectorAuthentication { pub chargebee: Option, pub checkout: Option, pub coinbase: Option, + pub coingate: Option, pub cryptopay: Option, pub cybersource: Option, pub datatrans: Option, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 4d9c686f23b..2fecba27bab 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -94,6 +94,7 @@ cashtocode.base_url = "https://cluster05.api-test.cashtocode.com" chargebee.base_url = "https://$.chargebee.com/api/" checkout.base_url = "https://api.sandbox.checkout.com/" coinbase.base_url = "https://api.commerce.coinbase.com" +coingate.base_url = "https://api-sandbox.coingate.com/v2" cryptopay.base_url = "https://business-sandbox.cryptopay.me" cybersource.base_url = "https://apitest.cybersource.com/" datatrans.base_url = "https://api.sandbox.datatrans.com/" @@ -199,6 +200,7 @@ cards = [ "braintree", "checkout", "coinbase", + "coingate", "cryptopay", "cybersource", "datatrans", diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 32d24fe47eb..8dee6cd21f9 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -6,7 +6,7 @@ function find_prev_connector() { git checkout $self cp $self $self.tmp # Add new connector to existing list and sort it - connectors=(aci adyen adyenplatform airwallex amazonpay applepay authorizedotnet bambora bamboraapac bankofamerica billwerk bitpay bluesnap boku braintree cashtocode chargebee checkout coinbase cryptopay cybersource datatrans deutschebank digitalvirgo dlocal dummyconnector ebanx elavon fiserv fiservemea fiuu forte globalpay globepay gocardless gpayments helcim iatapay inespay itaubank jpmorgan klarna mifinity mollie multisafepay netcetera nexinets nexixpay nomupay noon novalnet nuvei opayo opennode paybox payeezy payme payone paypal payu placetopay plaid powertranz prophetpay rapyd razorpay redsys shift4 square stax stripe taxjar threedsecureio thunes trustpay tsys unified_authentication_service volt wellsfargo wellsfargopayout wise worldline worldpay xendit zsl "$1") + connectors=(aci adyen adyenplatform airwallex amazonpay applepay authorizedotnet bambora bamboraapac bankofamerica billwerk bitpay bluesnap boku braintree cashtocode chargebee checkout coinbase coingate cryptopay cybersource datatrans deutschebank digitalvirgo dlocal dummyconnector ebanx elavon fiserv fiservemea fiuu forte globalpay globepay gocardless gpayments helcim iatapay inespay itaubank jpmorgan klarna mifinity mollie multisafepay netcetera nexinets nexixpay nomupay noon novalnet nuvei opayo opennode paybox payeezy payme payone paypal payu placetopay plaid powertranz prophetpay rapyd razorpay redsys shift4 square stax stripe taxjar threedsecureio thunes trustpay tsys unified_authentication_service volt wellsfargo wellsfargopayout wise worldline worldpay xendit zsl "$1") IFS=$'\n' sorted=($(sort <<<"${connectors[*]}")); unset IFS res="$(echo ${sorted[@]})" sed -i'' -e "s/^ connectors=.*/ connectors=($res \"\$1\")/" $self.tmp From a6367d92f629ef01cdb73aded8a81d2ba198f38c Mon Sep 17 00:00:00 2001 From: sweta-kumari-sharma <77436883+Sweta-Kumari-Sharma@users.noreply.github.com> Date: Thu, 6 Feb 2025 19:15:48 +0530 Subject: [PATCH 39/46] refactor(dynamic_fields): dynamic fields for Adyen and Stripe, renaming klarnaCheckout, WASM for KlarnaCheckout (#7015) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 11 ----------- api-reference/openapi_spec.json | 11 ----------- crates/api_models/src/payments.rs | 3 --- crates/connector_configs/src/transformer.rs | 3 ++- crates/connector_configs/toml/development.toml | 4 ++++ crates/connector_configs/toml/production.toml | 4 ++++ crates/connector_configs/toml/sandbox.toml | 4 ++++ .../src/connectors/multisafepay/transformers.rs | 1 - .../src/connectors/square/transformers.rs | 1 - .../src/connectors/zen/transformers.rs | 1 - crates/hyperswitch_connectors/src/utils.rs | 2 -- .../src/payment_method_data.rs | 3 --- .../defaults/payment_connector_required_fields.rs | 8 ++++---- crates/router/src/connector/adyen/transformers.rs | 3 +-- crates/router/src/connector/klarna.rs | 2 +- crates/router/src/connector/klarna/transformers.rs | 2 +- crates/router/src/connector/nuvei/transformers.rs | 1 - crates/router/src/connector/paypal/transformers.rs | 1 - crates/router/src/connector/stripe/transformers.rs | 1 - crates/router/src/connector/utils.rs | 2 -- 20 files changed, 21 insertions(+), 47 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index a8abc3fb7d0..ebfbfc3bf06 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -12481,17 +12481,6 @@ } } }, - { - "type": "object", - "required": [ - "klarna_checkout" - ], - "properties": { - "klarna_checkout": { - "type": "object" - } - } - }, { "type": "object", "required": [ diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 5bdaa5940c8..6c03d7a9a25 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -15599,17 +15599,6 @@ } } }, - { - "type": "object", - "required": [ - "klarna_checkout" - ], - "properties": { - "klarna_checkout": { - "type": "object" - } - } - }, { "type": "object", "required": [ diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index ec281197eda..45ba71454f4 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1876,7 +1876,6 @@ pub enum PayLaterData { /// The token for the sdk workflow token: String, }, - KlarnaCheckout {}, /// For Affirm redirect as PayLater Option AffirmRedirect {}, /// For AfterpayClearpay redirect as PayLater Option @@ -1934,7 +1933,6 @@ impl GetAddressFromPaymentMethodData for PayLaterData { | Self::WalleyRedirect {} | Self::AlmaRedirect {} | Self::KlarnaSdk { .. } - | Self::KlarnaCheckout {} | Self::AffirmRedirect {} | Self::AtomeRedirect {} => None, } @@ -2397,7 +2395,6 @@ impl GetPaymentMethodType for PayLaterData { match self { Self::KlarnaRedirect { .. } => api_enums::PaymentMethodType::Klarna, Self::KlarnaSdk { .. } => api_enums::PaymentMethodType::Klarna, - Self::KlarnaCheckout {} => api_enums::PaymentMethodType::Klarna, Self::AffirmRedirect {} => api_enums::PaymentMethodType::Affirm, Self::AfterpayClearpayRedirect { .. } => api_enums::PaymentMethodType::AfterpayClearpay, Self::PayBrightRedirect {} => api_enums::PaymentMethodType::PayBright, diff --git a/crates/connector_configs/src/transformer.rs b/crates/connector_configs/src/transformer.rs index 160e938ead3..ed3319816d5 100644 --- a/crates/connector_configs/src/transformer.rs +++ b/crates/connector_configs/src/transformer.rs @@ -45,10 +45,11 @@ impl DashboardRequestPayload { Some(api_models::enums::PaymentExperience::RedirectToUrl) } (Connector::Paypal, Paypal) => payment_experience, + (Connector::Klarna, Klarna) => payment_experience, (Connector::Zen, GooglePay) | (Connector::Zen, ApplePay) => { Some(api_models::enums::PaymentExperience::RedirectToUrl) } - (Connector::Braintree, Paypal) | (Connector::Klarna, Klarna) => { + (Connector::Braintree, Paypal) => { Some(api_models::enums::PaymentExperience::InvokeSdkClient) } (Connector::Globepay, AliPay) diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 2b04ea8d1c2..45ae3ceea61 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -1779,6 +1779,10 @@ key1="Client Secret" [klarna] [[klarna.pay_later]] payment_method_type = "klarna" + payment_experience = "invoke_sdk_client" +[[klarna.pay_later]] + payment_method_type = "klarna" + payment_experience = "redirect_to_url" [klarna.connector_auth.BodyKey] key1="Klarna Merchant Username" api_key="Klarna Merchant ID Password" diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index b7108c63477..104121de0e1 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -1491,6 +1491,10 @@ key1="Client Secret" [klarna] [[klarna.pay_later]] payment_method_type = "klarna" + payment_experience = "invoke_sdk_client" +[[klarna.pay_later]] + payment_method_type = "klarna" + payment_experience = "redirect_to_url" [klarna.connector_auth.BodyKey] key1="Klarna Merchant Username" api_key="Klarna Merchant ID Password" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index b886e7410a9..decfcf90b95 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -1727,6 +1727,10 @@ key1="Client Secret" [klarna] [[klarna.pay_later]] payment_method_type = "klarna" + payment_experience = "invoke_sdk_client" +[[klarna.pay_later]] + payment_method_type = "klarna" + payment_experience = "redirect_to_url" [klarna.connector_auth.BodyKey] key1="Klarna Merchant Username" api_key="Klarna Merchant ID Password" diff --git a/crates/hyperswitch_connectors/src/connectors/multisafepay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/multisafepay/transformers.rs index 54aca37a76a..2065fdc232a 100644 --- a/crates/hyperswitch_connectors/src/connectors/multisafepay/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/multisafepay/transformers.rs @@ -749,7 +749,6 @@ impl TryFrom<&MultisafepayRouterData<&types::PaymentsAuthorizeRouterData>> email: Some(match paylater { PayLaterData::KlarnaRedirect {} => item.router_data.get_billing_email()?, PayLaterData::KlarnaSdk { token: _ } - | PayLaterData::KlarnaCheckout {} | PayLaterData::AffirmRedirect {} | PayLaterData::AfterpayClearpayRedirect {} | PayLaterData::PayBrightRedirect {} diff --git a/crates/hyperswitch_connectors/src/connectors/square/transformers.rs b/crates/hyperswitch_connectors/src/connectors/square/transformers.rs index 6c47e5d58d4..c284212f0f5 100644 --- a/crates/hyperswitch_connectors/src/connectors/square/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/square/transformers.rs @@ -85,7 +85,6 @@ impl TryFrom<(&types::TokenizationRouterData, PayLaterData)> for SquareTokenRequ PayLaterData::AfterpayClearpayRedirect { .. } | PayLaterData::KlarnaRedirect { .. } | PayLaterData::KlarnaSdk { .. } - | PayLaterData::KlarnaCheckout {} | PayLaterData::AffirmRedirect { .. } | PayLaterData::PayBrightRedirect { .. } | PayLaterData::WalleyRedirect { .. } diff --git a/crates/hyperswitch_connectors/src/connectors/zen/transformers.rs b/crates/hyperswitch_connectors/src/connectors/zen/transformers.rs index a9e21f9071c..14e0b5ba972 100644 --- a/crates/hyperswitch_connectors/src/connectors/zen/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/zen/transformers.rs @@ -752,7 +752,6 @@ impl TryFrom<&PayLaterData> for ZenPaymentsRequest { match value { PayLaterData::KlarnaRedirect { .. } | PayLaterData::KlarnaSdk { .. } - | PayLaterData::KlarnaCheckout {} | PayLaterData::AffirmRedirect {} | PayLaterData::AfterpayClearpayRedirect { .. } | PayLaterData::PayBrightRedirect {} diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index 418c505cf52..70fe780ea64 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -2324,7 +2324,6 @@ pub enum PaymentMethodDataType { SwishQr, KlarnaRedirect, KlarnaSdk, - KlarnaCheckout, AffirmRedirect, AfterpayClearpayRedirect, PayBrightRedirect, @@ -2450,7 +2449,6 @@ impl From for PaymentMethodDataType { PaymentMethodData::PayLater(pay_later_data) => match pay_later_data { payment_method_data::PayLaterData::KlarnaRedirect { .. } => Self::KlarnaRedirect, payment_method_data::PayLaterData::KlarnaSdk { .. } => Self::KlarnaSdk, - payment_method_data::PayLaterData::KlarnaCheckout {} => Self::KlarnaCheckout, payment_method_data::PayLaterData::AffirmRedirect {} => Self::AffirmRedirect, payment_method_data::PayLaterData::AfterpayClearpayRedirect { .. } => { Self::AfterpayClearpayRedirect diff --git a/crates/hyperswitch_domain_models/src/payment_method_data.rs b/crates/hyperswitch_domain_models/src/payment_method_data.rs index 9e83810c9e0..e96368080d9 100644 --- a/crates/hyperswitch_domain_models/src/payment_method_data.rs +++ b/crates/hyperswitch_domain_models/src/payment_method_data.rs @@ -157,7 +157,6 @@ pub enum CardRedirectData { pub enum PayLaterData { KlarnaRedirect {}, KlarnaSdk { token: String }, - KlarnaCheckout {}, AffirmRedirect {}, AfterpayClearpayRedirect {}, PayBrightRedirect {}, @@ -904,7 +903,6 @@ impl From for PayLaterData { match value { api_models::payments::PayLaterData::KlarnaRedirect { .. } => Self::KlarnaRedirect {}, api_models::payments::PayLaterData::KlarnaSdk { token } => Self::KlarnaSdk { token }, - api_models::payments::PayLaterData::KlarnaCheckout {} => Self::KlarnaCheckout {}, api_models::payments::PayLaterData::AffirmRedirect {} => Self::AffirmRedirect {}, api_models::payments::PayLaterData::AfterpayClearpayRedirect { .. } => { Self::AfterpayClearpayRedirect {} @@ -1559,7 +1557,6 @@ impl GetPaymentMethodType for PayLaterData { match self { Self::KlarnaRedirect { .. } => api_enums::PaymentMethodType::Klarna, Self::KlarnaSdk { .. } => api_enums::PaymentMethodType::Klarna, - Self::KlarnaCheckout {} => api_enums::PaymentMethodType::Klarna, Self::AffirmRedirect {} => api_enums::PaymentMethodType::Affirm, Self::AfterpayClearpayRedirect { .. } => api_enums::PaymentMethodType::AfterpayClearpay, Self::PayBrightRedirect {} => api_enums::PaymentMethodType::PayBright, diff --git a/crates/router/src/configs/defaults/payment_connector_required_fields.rs b/crates/router/src/configs/defaults/payment_connector_required_fields.rs index 421f430e74e..46076b057eb 100644 --- a/crates/router/src/configs/defaults/payment_connector_required_fields.rs +++ b/crates/router/src/configs/defaults/payment_connector_required_fields.rs @@ -10508,9 +10508,9 @@ impl Default for settings::RequiredFields { RequiredFieldFinal { mandate : HashMap::new(), non_mandate: HashMap::from([ - ( "payment_method_data.pay_later.klarna.billing_country".to_string(), + ( "billing.address.country".to_string(), RequiredFieldInfo { - required_field: "payment_method_data.pay_later.klarna.billing_country".to_string(), + required_field: "payment_method_data.billing.address.country".to_string(), display_name: "billing_country".to_string(), field_type: enums::FieldType::UserAddressCountry{ options: vec![ @@ -10558,9 +10558,9 @@ impl Default for settings::RequiredFields { mandate : HashMap::new(), non_mandate: HashMap::new(), common : HashMap::from([ - ( "payment_method_data.pay_later.klarna.billing_country".to_string(), + ( "billing.address.country".to_string(), RequiredFieldInfo { - required_field: "payment_method_data.pay_later.klarna.billing_country".to_string(), + required_field: "payment_method_data.billing.address.country".to_string(), display_name: "billing_country".to_string(), field_type: enums::FieldType::UserAddressCountry{ options: vec![ diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index adb1a795f70..1daa1fbaffc 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -2351,8 +2351,7 @@ impl check_required_field(billing_address, "billing")?; Ok(AdyenPaymentMethod::Atome) } - domain::payments::PayLaterData::KlarnaCheckout {} - | domain::payments::PayLaterData::KlarnaSdk { .. } => { + domain::payments::PayLaterData::KlarnaSdk { .. } => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Adyen"), ) diff --git a/crates/router/src/connector/klarna.rs b/crates/router/src/connector/klarna.rs index 6fbc48a66a0..9b0da0ade6d 100644 --- a/crates/router/src/connector/klarna.rs +++ b/crates/router/src/connector/klarna.rs @@ -672,7 +672,7 @@ impl })), } } - domain::PaymentMethodData::PayLater(domain::PayLaterData::KlarnaCheckout {}) => { + domain::PaymentMethodData::PayLater(domain::PayLaterData::KlarnaRedirect {}) => { match (payment_experience, payment_method_type) { ( common_enums::PaymentExperience::RedirectToUrl, diff --git a/crates/router/src/connector/klarna/transformers.rs b/crates/router/src/connector/klarna/transformers.rs index 3cefa4ec6c9..d1a0b80bad6 100644 --- a/crates/router/src/connector/klarna/transformers.rs +++ b/crates/router/src/connector/klarna/transformers.rs @@ -277,7 +277,7 @@ impl TryFrom<&KlarnaRouterData<&types::PaymentsAuthorizeRouterData>> for KlarnaP })), } } - domain::PaymentMethodData::PayLater(domain::PayLaterData::KlarnaCheckout {}) => { + domain::PaymentMethodData::PayLater(domain::PayLaterData::KlarnaRedirect {}) => { match request.order_details.clone() { Some(order_details) => Ok(Self { purchase_country: item.router_data.get_billing_country()?, diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index eea700f695c..136d0531d9a 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -978,7 +978,6 @@ where get_pay_later_info(AlternativePaymentMethodType::AfterPay, item) } domain::PayLaterData::KlarnaSdk { .. } - | domain::PayLaterData::KlarnaCheckout {} | domain::PayLaterData::AffirmRedirect {} | domain::PayLaterData::PayBrightRedirect {} | domain::PayLaterData::WalleyRedirect {} diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index 18739851ae8..884e67714ff 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -1274,7 +1274,6 @@ impl TryFrom<&domain::PayLaterData> for PaypalPaymentsRequest { match value { domain::PayLaterData::KlarnaRedirect { .. } | domain::PayLaterData::KlarnaSdk { .. } - | domain::PayLaterData::KlarnaCheckout {} | domain::PayLaterData::AffirmRedirect {} | domain::PayLaterData::AfterpayClearpayRedirect { .. } | domain::PayLaterData::PayBrightRedirect {} diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 363dfb81bcc..a93ba6ecaa5 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -1002,7 +1002,6 @@ impl TryFrom<&domain::payments::PayLaterData> for StripePaymentMethodType { } domain::PayLaterData::KlarnaSdk { .. } - | domain::PayLaterData::KlarnaCheckout {} | domain::PayLaterData::PayBrightRedirect {} | domain::PayLaterData::WalleyRedirect {} | domain::PayLaterData::AlmaRedirect {} diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index c4824d65836..eff68176f8c 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -2772,7 +2772,6 @@ pub enum PaymentMethodDataType { SwishQr, KlarnaRedirect, KlarnaSdk, - KlarnaCheckout, AffirmRedirect, AfterpayClearpayRedirect, PayBrightRedirect, @@ -2897,7 +2896,6 @@ impl From for PaymentMethodDataType { domain::payments::PaymentMethodData::PayLater(pay_later_data) => match pay_later_data { domain::payments::PayLaterData::KlarnaRedirect { .. } => Self::KlarnaRedirect, domain::payments::PayLaterData::KlarnaSdk { .. } => Self::KlarnaSdk, - domain::payments::PayLaterData::KlarnaCheckout {} => Self::KlarnaCheckout, domain::payments::PayLaterData::AffirmRedirect {} => Self::AffirmRedirect, domain::payments::PayLaterData::AfterpayClearpayRedirect { .. } => { Self::AfterpayClearpayRedirect From 60ddddf24a1625b8044c095c5d01754022102813 Mon Sep 17 00:00:00 2001 From: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Date: Thu, 6 Feb 2025 19:16:00 +0530 Subject: [PATCH 40/46] feat(routing): Contract based routing integration (#6761) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .github/CODEOWNERS | 3 + api-reference/openapi_spec.json | 247 +++++++- crates/api_models/src/events/routing.rs | 25 +- crates/api_models/src/routing.rs | 248 ++++++-- crates/external_services/build.rs | 4 +- .../src/grpc_client/dynamic_routing.rs | 27 +- .../contract_routing_client.rs | 192 +++++++ ..._client.rs => elimination_based_client.rs} | 0 crates/openapi/src/openapi.rs | 6 + crates/openapi/src/routes/routing.rs | 58 +- crates/router/src/core/errors.rs | 10 + crates/router/src/core/metrics.rs | 1 + crates/router/src/core/payments.rs | 11 +- .../payments/operations/payment_response.rs | 38 +- crates/router/src/core/payments/routing.rs | 209 ++++++- crates/router/src/core/routing.rs | 346 +++++++++++- crates/router/src/core/routing/helpers.rs | 529 ++++++++++++++---- crates/router/src/routes/app.rs | 17 +- .../routes/metrics/bg_metrics_collector.rs | 3 + crates/router/src/routes/routing.rs | 96 +++- crates/storage_impl/src/redis/cache.rs | 12 + crates/storage_impl/src/redis/pub_sub.rs | 20 +- proto/contract_routing.proto | 76 +++ 23 files changed, 1968 insertions(+), 210 deletions(-) create mode 100644 crates/external_services/src/grpc_client/dynamic_routing/contract_routing_client.rs rename crates/external_services/src/grpc_client/dynamic_routing/{elimination_rate_client.rs => elimination_based_client.rs} (100%) create mode 100644 proto/contract_routing.proto diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6ad86dd4ccc..16254a6b998 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -67,6 +67,9 @@ crates/router/src/core/routing @juspay/hyperswitch-routing crates/router/src/core/routing.rs @juspay/hyperswitch-routing crates/router/src/core/payments/routing @juspay/hyperswitch-routing crates/router/src/core/payments/routing.rs @juspay/hyperswitch-routing +crates/external_services/src/grpc_client/dynamic_routing.rs @juspay/hyperswitch-routing +crates/external_services/src/grpc_client/dynamic_routing/ @juspay/hyperswitch-routing +crates/external_services/src/grpc_client/health_check_client.rs @juspay/hyperswitch-routing crates/api_models/src/payment_methods.rs @juspay/hyperswitch-routing crates/router/src/core/payment_methods.rs @juspay/hyperswitch-routing diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 6c03d7a9a25..7132fe905c7 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -4082,7 +4082,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DynamicRoutingFeatures" + "$ref": "#/components/schemas/SuccessBasedRoutingConfig" } } }, @@ -4229,7 +4229,7 @@ { "name": "enable", "in": "query", - "description": "Feature to enable for success based routing", + "description": "Feature to enable for elimination based routing", "required": true, "schema": { "$ref": "#/components/schemas/DynamicRoutingFeatures" @@ -4273,6 +4273,174 @@ ] } }, + "/account/:account_id/business_profile/:profile_id/dynamic_routing/contracts/toggle": { + "post": { + "tags": [ + "Routing" + ], + "summary": "Routing - Toggle Contract routing for profile", + "description": "Create a Contract based dynamic routing algorithm", + "operationId": "Toggle contract routing algorithm", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "Merchant id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "profile_id", + "in": "path", + "description": "Profile id under which Dynamic routing needs to be toggled", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "enable", + "in": "query", + "description": "Feature to enable for contract based routing", + "required": true, + "schema": { + "$ref": "#/components/schemas/DynamicRoutingFeatures" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContractBasedRoutingConfig" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Routing Algorithm created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingDictionaryRecord" + } + } + } + }, + "400": { + "description": "Request body is malformed" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Resource missing" + }, + "422": { + "description": "Unprocessable request" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] + } + ] + } + }, + "/account/{account_id}/business_profile/{profile_id}/dynamic_routing/contracts/config/{algorithm_id}": { + "patch": { + "tags": [ + "Routing" + ], + "summary": "Routing - Update contract based dynamic routing config for profile", + "description": "Update contract based dynamic routing algorithm", + "operationId": "Update contract based dynamic routing configs", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "Merchant id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "profile_id", + "in": "path", + "description": "Profile id under which Dynamic routing needs to be toggled", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "algorithm_id", + "in": "path", + "description": "Contract based routing algorithm id which was last activated to update the config", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContractBasedRoutingConfig" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Routing Algorithm updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoutingDictionaryRecord" + } + } + } + }, + "400": { + "description": "Update body is malformed" + }, + "403": { + "description": "Forbidden" + }, + "404": { + "description": "Resource missing" + }, + "422": { + "description": "Unprocessable request" + }, + "500": { + "description": "Internal server error" + } + }, + "security": [ + { + "api_key": [] + }, + { + "jwt_key": [] + } + ] + } + }, "/blocklist": { "delete": { "tags": [ @@ -9536,6 +9704,54 @@ }, "additionalProperties": false }, + "ContractBasedRoutingConfig": { + "type": "object", + "properties": { + "config": { + "allOf": [ + { + "$ref": "#/components/schemas/ContractBasedRoutingConfigBody" + } + ], + "nullable": true + }, + "label_info": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LabelInformation" + }, + "nullable": true + } + } + }, + "ContractBasedRoutingConfigBody": { + "type": "object", + "properties": { + "constants": { + "type": "array", + "items": { + "type": "number", + "format": "double" + }, + "nullable": true + }, + "time_scale": { + "allOf": [ + { + "$ref": "#/components/schemas/ContractBasedTimeScale" + } + ], + "nullable": true + } + } + }, + "ContractBasedTimeScale": { + "type": "string", + "enum": [ + "day", + "month" + ] + }, "CountryAlpha2": { "type": "string", "enum": [ @@ -12886,6 +13102,33 @@ } } }, + "LabelInformation": { + "type": "object", + "required": [ + "label", + "target_count", + "target_time", + "mca_id" + ], + "properties": { + "label": { + "type": "string" + }, + "target_count": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "target_time": { + "type": "integer", + "format": "int64", + "minimum": 0 + }, + "mca_id": { + "type": "string" + } + } + }, "LinkedRoutingConfigRetrieveResponse": { "oneOf": [ { diff --git a/crates/api_models/src/events/routing.rs b/crates/api_models/src/events/routing.rs index f3e169336bf..122d1f8d3c8 100644 --- a/crates/api_models/src/events/routing.rs +++ b/crates/api_models/src/events/routing.rs @@ -1,12 +1,13 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::routing::{ - LinkedRoutingConfigRetrieveResponse, MerchantRoutingAlgorithm, ProfileDefaultRoutingConfig, - RoutingAlgorithmId, RoutingConfigRequest, RoutingDictionaryRecord, RoutingKind, - RoutingLinkWrapper, RoutingPayloadWrapper, RoutingRetrieveLinkQuery, + ContractBasedRoutingPayloadWrapper, ContractBasedRoutingSetupPayloadWrapper, + DynamicRoutingUpdateConfigQuery, LinkedRoutingConfigRetrieveResponse, MerchantRoutingAlgorithm, + ProfileDefaultRoutingConfig, RoutingAlgorithmId, RoutingConfigRequest, RoutingDictionaryRecord, + RoutingKind, RoutingLinkWrapper, RoutingPayloadWrapper, RoutingRetrieveLinkQuery, RoutingRetrieveLinkQueryWrapper, RoutingRetrieveQuery, RoutingVolumeSplitWrapper, - SuccessBasedRoutingConfig, SuccessBasedRoutingPayloadWrapper, - SuccessBasedRoutingUpdateConfigQuery, ToggleDynamicRoutingQuery, ToggleDynamicRoutingWrapper, + SuccessBasedRoutingConfig, SuccessBasedRoutingPayloadWrapper, ToggleDynamicRoutingQuery, + ToggleDynamicRoutingWrapper, }; impl ApiEventMetric for RoutingKind { @@ -97,13 +98,25 @@ impl ApiEventMetric for SuccessBasedRoutingPayloadWrapper { } } +impl ApiEventMetric for ContractBasedRoutingPayloadWrapper { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + +impl ApiEventMetric for ContractBasedRoutingSetupPayloadWrapper { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + impl ApiEventMetric for ToggleDynamicRoutingWrapper { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Routing) } } -impl ApiEventMetric for SuccessBasedRoutingUpdateConfigQuery { +impl ApiEventMetric for DynamicRoutingUpdateConfigQuery { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Routing) } diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 8e502767575..d4b4f76e178 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -513,17 +513,27 @@ pub struct RoutingLinkWrapper { pub algorithm_id: RoutingAlgorithmId, } -#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct DynamicAlgorithmWithTimestamp { pub algorithm_id: Option, pub timestamp: i64, } +impl DynamicAlgorithmWithTimestamp { + pub fn new(algorithm_id: Option) -> Self { + Self { + algorithm_id, + timestamp: common_utils::date_time::now_unix_timestamp(), + } + } +} + #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] pub struct DynamicRoutingAlgorithmRef { pub success_based_algorithm: Option, pub dynamic_routing_volume_split: Option, pub elimination_routing_algorithm: Option, + pub contract_based_routing: Option, } pub trait DynamicRoutingAlgoAccessor { @@ -555,6 +565,43 @@ impl DynamicRoutingAlgoAccessor for EliminationRoutingAlgorithm { } } +impl DynamicRoutingAlgoAccessor for ContractRoutingAlgorithm { + fn get_algorithm_id_with_timestamp( + self, + ) -> DynamicAlgorithmWithTimestamp { + self.algorithm_id_with_timestamp + } + fn get_enabled_features(&mut self) -> &mut DynamicRoutingFeatures { + &mut self.enabled_feature + } +} + +impl EliminationRoutingAlgorithm { + pub fn new( + algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp< + common_utils::id_type::RoutingId, + >, + ) -> Self { + Self { + algorithm_id_with_timestamp, + enabled_feature: DynamicRoutingFeatures::None, + } + } +} + +impl SuccessBasedAlgorithm { + pub fn new( + algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp< + common_utils::id_type::RoutingId, + >, + ) -> Self { + Self { + algorithm_id_with_timestamp, + enabled_feature: DynamicRoutingFeatures::None, + } + } +} + impl DynamicRoutingAlgorithmRef { pub fn update(&mut self, new: Self) { if let Some(elimination_routing_algorithm) = new.elimination_routing_algorithm { @@ -563,9 +610,12 @@ impl DynamicRoutingAlgorithmRef { if let Some(success_based_algorithm) = new.success_based_algorithm { self.success_based_algorithm = Some(success_based_algorithm) } + if let Some(contract_based_routing) = new.contract_based_routing { + self.contract_based_routing = Some(contract_based_routing) + } } - pub fn update_specific_ref( + pub fn update_enabled_features( &mut self, algo_type: DynamicRoutingType, feature_to_enable: DynamicRoutingFeatures, @@ -581,6 +631,11 @@ impl DynamicRoutingAlgorithmRef { .as_mut() .map(|algo| algo.enabled_feature = feature_to_enable); } + DynamicRoutingType::ContractBasedRouting => { + self.contract_based_routing + .as_mut() + .map(|algo| algo.enabled_feature = feature_to_enable); + } } } @@ -589,32 +644,6 @@ impl DynamicRoutingAlgorithmRef { } } -impl EliminationRoutingAlgorithm { - pub fn new( - algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp< - common_utils::id_type::RoutingId, - >, - ) -> Self { - Self { - algorithm_id_with_timestamp, - enabled_feature: DynamicRoutingFeatures::None, - } - } -} - -impl SuccessBasedAlgorithm { - pub fn new( - algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp< - common_utils::id_type::RoutingId, - >, - ) -> Self { - Self { - algorithm_id_with_timestamp, - enabled_feature: DynamicRoutingFeatures::None, - } - } -} - #[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)] pub struct RoutingVolumeSplit { pub routing_type: RoutingType, @@ -648,6 +677,14 @@ pub struct SuccessBasedAlgorithm { pub enabled_feature: DynamicRoutingFeatures, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ContractRoutingAlgorithm { + pub algorithm_id_with_timestamp: + DynamicAlgorithmWithTimestamp, + #[serde(default)] + pub enabled_feature: DynamicRoutingFeatures, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct EliminationRoutingAlgorithm { pub algorithm_id_with_timestamp: @@ -678,24 +715,53 @@ impl DynamicRoutingAlgorithmRef { match dynamic_routing_type { DynamicRoutingType::SuccessRateBasedRouting => { self.success_based_algorithm = Some(SuccessBasedAlgorithm { - algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp { - algorithm_id: Some(new_id), - timestamp: common_utils::date_time::now_unix_timestamp(), - }, + algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp::new(Some(new_id)), enabled_feature, }) } DynamicRoutingType::EliminationRouting => { self.elimination_routing_algorithm = Some(EliminationRoutingAlgorithm { - algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp { - algorithm_id: Some(new_id), - timestamp: common_utils::date_time::now_unix_timestamp(), - }, + algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp::new(Some(new_id)), + enabled_feature, + }) + } + DynamicRoutingType::ContractBasedRouting => { + self.contract_based_routing = Some(ContractRoutingAlgorithm { + algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp::new(Some(new_id)), enabled_feature, }) } }; } + + pub fn disable_algorithm_id(&mut self, dynamic_routing_type: DynamicRoutingType) { + match dynamic_routing_type { + DynamicRoutingType::SuccessRateBasedRouting => { + if let Some(success_based_algo) = &self.success_based_algorithm { + self.success_based_algorithm = Some(SuccessBasedAlgorithm { + algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp::new(None), + enabled_feature: success_based_algo.enabled_feature, + }); + } + } + DynamicRoutingType::EliminationRouting => { + if let Some(elimination_based_algo) = &self.elimination_routing_algorithm { + self.elimination_routing_algorithm = Some(EliminationRoutingAlgorithm { + algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp::new(None), + enabled_feature: elimination_based_algo.enabled_feature, + }); + } + } + DynamicRoutingType::ContractBasedRouting => { + if let Some(contract_based_algo) = &self.contract_based_routing { + self.contract_based_routing = Some(ContractRoutingAlgorithm { + algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp::new(None), + enabled_feature: contract_based_algo.enabled_feature, + }); + } + } + } + } } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] @@ -708,7 +774,9 @@ pub struct DynamicRoutingVolumeSplitQuery { pub split: u8, } -#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, ToSchema)] +#[derive( + Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, ToSchema, +)] #[serde(rename_all = "snake_case")] pub enum DynamicRoutingFeatures { Metrics, @@ -718,7 +786,7 @@ pub enum DynamicRoutingFeatures { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] -pub struct SuccessBasedRoutingUpdateConfigQuery { +pub struct DynamicRoutingUpdateConfigQuery { #[schema(value_type = String)] pub algorithm_id: common_utils::id_type::RoutingId, #[schema(value_type = String)] @@ -827,10 +895,27 @@ pub struct SuccessBasedRoutingPayloadWrapper { pub profile_id: common_utils::id_type::ProfileId, } -#[derive(Debug, Clone, strum::Display, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ContractBasedRoutingPayloadWrapper { + pub updated_config: ContractBasedRoutingConfig, + pub algorithm_id: common_utils::id_type::RoutingId, + pub profile_id: common_utils::id_type::ProfileId, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ContractBasedRoutingSetupPayloadWrapper { + pub config: Option, + pub profile_id: common_utils::id_type::ProfileId, + pub features_to_enable: DynamicRoutingFeatures, +} + +#[derive( + Debug, Clone, Copy, strum::Display, serde::Serialize, serde::Deserialize, PartialEq, Eq, +)] pub enum DynamicRoutingType { SuccessRateBasedRouting, EliminationRouting, + ContractBasedRouting, } impl SuccessBasedRoutingConfig { @@ -872,6 +957,91 @@ impl CurrentBlockThreshold { } } +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)] +#[serde(rename_all = "snake_case")] +pub struct ContractBasedRoutingConfig { + pub config: Option, + pub label_info: Option>, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)] +#[serde(rename_all = "snake_case")] +pub struct ContractBasedRoutingConfigBody { + pub constants: Option>, + pub time_scale: Option, +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)] +#[serde(rename_all = "snake_case")] +pub struct LabelInformation { + pub label: String, + pub target_count: u64, + pub target_time: u64, + #[schema(value_type = String)] + pub mca_id: common_utils::id_type::MerchantConnectorAccountId, +} + +impl LabelInformation { + pub fn update_target_time(&mut self, new: &Self) { + self.target_time = new.target_time; + } + + pub fn update_target_count(&mut self, new: &Self) { + self.target_count = new.target_count; + } +} + +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum ContractBasedTimeScale { + Day, + Month, +} + +impl Default for ContractBasedRoutingConfig { + fn default() -> Self { + Self { + config: Some(ContractBasedRoutingConfigBody { + constants: Some(vec![0.7, 0.35]), + time_scale: Some(ContractBasedTimeScale::Day), + }), + label_info: None, + } + } +} + +impl ContractBasedRoutingConfig { + pub fn update(&mut self, new: Self) { + if let Some(new_config) = new.config { + self.config.as_mut().map(|config| config.update(new_config)); + } + if let Some(new_label_info) = new.label_info { + new_label_info.iter().for_each(|new_label_info| { + if let Some(existing_label_infos) = &mut self.label_info { + for existing_label_info in existing_label_infos { + if existing_label_info.mca_id == new_label_info.mca_id { + existing_label_info.update_target_time(new_label_info); + existing_label_info.update_target_count(new_label_info); + } + } + } else { + self.label_info = Some(vec![new_label_info.clone()]); + } + }); + } + } +} + +impl ContractBasedRoutingConfigBody { + pub fn update(&mut self, new: Self) { + if let Some(new_cons) = new.constants { + self.constants = Some(new_cons) + } + if let Some(new_time_scale) = new.time_scale { + self.time_scale = Some(new_time_scale) + } + } +} #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct RoutableConnectorChoiceWithBucketName { pub routable_connector_choice: RoutableConnectorChoice, diff --git a/crates/external_services/build.rs b/crates/external_services/build.rs index b3fa1a8fed2..8dba0bb3e98 100644 --- a/crates/external_services/build.rs +++ b/crates/external_services/build.rs @@ -6,6 +6,7 @@ fn main() -> Result<(), Box> { let proto_path = router_env::workspace_path().join("proto"); let success_rate_proto_file = proto_path.join("success_rate.proto"); + let contract_routing_proto_file = proto_path.join("contract_routing.proto"); let elimination_proto_file = proto_path.join("elimination_rate.proto"); let health_check_proto_file = proto_path.join("health_check.proto"); let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR")?); @@ -16,8 +17,9 @@ fn main() -> Result<(), Box> { .compile( &[ success_rate_proto_file, - elimination_proto_file, health_check_proto_file, + elimination_proto_file, + contract_routing_proto_file, ], &[proto_path], ) diff --git a/crates/external_services/src/grpc_client/dynamic_routing.rs b/crates/external_services/src/grpc_client/dynamic_routing.rs index e5050227fa8..e34f721b30e 100644 --- a/crates/external_services/src/grpc_client/dynamic_routing.rs +++ b/crates/external_services/src/grpc_client/dynamic_routing.rs @@ -1,14 +1,18 @@ +/// Module for Contract based routing +pub mod contract_routing_client; + use std::fmt::Debug; use common_utils::errors::CustomResult; use router_env::logger; use serde; /// Elimination Routing Client Interface Implementation -pub mod elimination_rate_client; +pub mod elimination_based_client; /// Success Routing Client Interface Implementation pub mod success_rate_client; -pub use elimination_rate_client::EliminationAnalyserClient; +pub use contract_routing_client::ContractScoreCalculatorClient; +pub use elimination_based_client::EliminationAnalyserClient; pub use success_rate_client::SuccessRateCalculatorClient; use super::Client; @@ -27,6 +31,10 @@ pub enum DynamicRoutingError { /// Error from Dynamic Routing Server while performing success_rate analysis #[error("Error from Dynamic Routing Server while perfrming success_rate analysis : {0}")] SuccessRateBasedRoutingFailure(String), + + /// Error from Dynamic Routing Server while performing contract based routing + #[error("Error from Dynamic Routing Server while performing contract based routing: {0}")] + ContractBasedRoutingFailure(String), /// Error from Dynamic Routing Server while perfrming elimination #[error("Error from Dynamic Routing Server while perfrming elimination : {0}")] EliminationRateRoutingFailure(String), @@ -37,8 +45,10 @@ pub enum DynamicRoutingError { pub struct RoutingStrategy { /// success rate service for Dynamic Routing pub success_rate_client: Option>, + /// contract based routing service for Dynamic Routing + pub contract_based_client: Option>, /// elimination service for Dynamic Routing - pub elimination_rate_client: Option>, + pub elimination_based_client: Option>, } /// Contains the Dynamic Routing Client Config @@ -65,7 +75,7 @@ impl DynamicRoutingClientConfig { self, client: Client, ) -> Result> { - let (success_rate_client, elimination_rate_client) = match self { + let (success_rate_client, contract_based_client, elimination_based_client) = match self { Self::Enabled { host, port, .. } => { let uri = format!("http://{}:{}", host, port).parse::()?; logger::info!("Connection established with dynamic routing gRPC Server"); @@ -74,14 +84,19 @@ impl DynamicRoutingClientConfig { client.clone(), uri.clone(), )), + Some(ContractScoreCalculatorClient::with_origin( + client.clone(), + uri.clone(), + )), Some(EliminationAnalyserClient::with_origin(client, uri)), ) } - Self::Disabled => (None, None), + Self::Disabled => (None, None, None), }; Ok(RoutingStrategy { success_rate_client, - elimination_rate_client, + contract_based_client, + elimination_based_client, }) } } diff --git a/crates/external_services/src/grpc_client/dynamic_routing/contract_routing_client.rs b/crates/external_services/src/grpc_client/dynamic_routing/contract_routing_client.rs new file mode 100644 index 00000000000..b210d996bc0 --- /dev/null +++ b/crates/external_services/src/grpc_client/dynamic_routing/contract_routing_client.rs @@ -0,0 +1,192 @@ +use api_models::routing::{ + ContractBasedRoutingConfig, ContractBasedRoutingConfigBody, ContractBasedTimeScale, + LabelInformation, RoutableConnectorChoice, RoutableConnectorChoiceWithStatus, +}; +use common_utils::{ + ext_traits::OptionExt, + transformers::{ForeignFrom, ForeignTryFrom}, +}; +pub use contract_routing::{ + contract_score_calculator_client::ContractScoreCalculatorClient, CalContractScoreConfig, + CalContractScoreRequest, CalContractScoreResponse, InvalidateContractRequest, + InvalidateContractResponse, LabelInformation as ProtoLabelInfo, TimeScale, + UpdateContractRequest, UpdateContractResponse, +}; +use error_stack::ResultExt; +use router_env::logger; + +use crate::grpc_client::{self, GrpcHeaders}; +#[allow( + missing_docs, + unused_qualifications, + clippy::unwrap_used, + clippy::as_conversions, + clippy::use_self +)] +pub mod contract_routing { + tonic::include_proto!("contract_routing"); +} +use super::{Client, DynamicRoutingError, DynamicRoutingResult}; +/// The trait ContractBasedDynamicRouting would have the functions required to support the calculation and updation window +#[async_trait::async_trait] +pub trait ContractBasedDynamicRouting: dyn_clone::DynClone + Send + Sync { + /// To calculate the contract scores for the list of chosen connectors + async fn calculate_contract_score( + &self, + id: String, + config: ContractBasedRoutingConfig, + params: String, + label_input: Vec, + headers: GrpcHeaders, + ) -> DynamicRoutingResult; + /// To update the contract scores with the given labels + async fn update_contracts( + &self, + id: String, + label_info: Vec, + params: String, + response: Vec, + headers: GrpcHeaders, + ) -> DynamicRoutingResult; + /// To invalidates the contract scores against the id + async fn invalidate_contracts( + &self, + id: String, + headers: GrpcHeaders, + ) -> DynamicRoutingResult; +} + +#[async_trait::async_trait] +impl ContractBasedDynamicRouting for ContractScoreCalculatorClient { + async fn calculate_contract_score( + &self, + id: String, + config: ContractBasedRoutingConfig, + params: String, + label_input: Vec, + headers: GrpcHeaders, + ) -> DynamicRoutingResult { + let labels = label_input + .into_iter() + .map(|conn_choice| conn_choice.to_string()) + .collect::>(); + + let config = config + .config + .map(ForeignTryFrom::foreign_try_from) + .transpose()?; + + let request = grpc_client::create_grpc_request( + CalContractScoreRequest { + id, + params, + labels, + config, + }, + headers, + ); + + let response = self + .clone() + .fetch_contract_score(request) + .await + .change_context(DynamicRoutingError::ContractBasedRoutingFailure( + "Failed to fetch the contract score".to_string(), + ))? + .into_inner(); + + logger::info!(dynamic_routing_response=?response); + + Ok(response) + } + + async fn update_contracts( + &self, + id: String, + label_info: Vec, + params: String, + _response: Vec, + headers: GrpcHeaders, + ) -> DynamicRoutingResult { + let labels_information = label_info + .into_iter() + .map(ProtoLabelInfo::foreign_from) + .collect::>(); + + let request = grpc_client::create_grpc_request( + UpdateContractRequest { + id, + params, + labels_information, + }, + headers, + ); + + let response = self + .clone() + .update_contract(request) + .await + .change_context(DynamicRoutingError::ContractBasedRoutingFailure( + "Failed to update the contracts".to_string(), + ))? + .into_inner(); + + logger::info!(dynamic_routing_response=?response); + + Ok(response) + } + async fn invalidate_contracts( + &self, + id: String, + headers: GrpcHeaders, + ) -> DynamicRoutingResult { + let request = grpc_client::create_grpc_request(InvalidateContractRequest { id }, headers); + + let response = self + .clone() + .invalidate_contract(request) + .await + .change_context(DynamicRoutingError::ContractBasedRoutingFailure( + "Failed to invalidate the contracts".to_string(), + ))? + .into_inner(); + Ok(response) + } +} + +impl ForeignFrom for TimeScale { + fn foreign_from(scale: ContractBasedTimeScale) -> Self { + Self { + time_scale: match scale { + ContractBasedTimeScale::Day => 0, + _ => 1, + }, + } + } +} + +impl ForeignTryFrom for CalContractScoreConfig { + type Error = error_stack::Report; + fn foreign_try_from(config: ContractBasedRoutingConfigBody) -> Result { + Ok(Self { + constants: config + .constants + .get_required_value("constants") + .change_context(DynamicRoutingError::MissingRequiredField { + field: "constants".to_string(), + })?, + time_scale: config.time_scale.clone().map(TimeScale::foreign_from), + }) + } +} + +impl ForeignFrom for ProtoLabelInfo { + fn foreign_from(config: LabelInformation) -> Self { + Self { + label: config.label, + target_count: config.target_count, + target_time: config.target_time, + current_count: 1, + } + } +} diff --git a/crates/external_services/src/grpc_client/dynamic_routing/elimination_rate_client.rs b/crates/external_services/src/grpc_client/dynamic_routing/elimination_based_client.rs similarity index 100% rename from crates/external_services/src/grpc_client/dynamic_routing/elimination_rate_client.rs rename to crates/external_services/src/grpc_client/dynamic_routing/elimination_based_client.rs diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 7102c23d17e..026eae764c4 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -166,6 +166,8 @@ Never share your secret api keys. Keep them guarded and secure. routes::routing::success_based_routing_update_configs, routes::routing::toggle_success_based_routing, routes::routing::toggle_elimination_routing, + routes::routing::contract_based_routing_setup_config, + routes::routing::contract_based_routing_update_configs, // Routes for blocklist routes::blocklist::remove_entry_from_blocklist, @@ -615,6 +617,10 @@ Never share your secret api keys. Keep them guarded and secure. api_models::routing::DynamicRoutingConfigParams, api_models::routing::CurrentBlockThreshold, api_models::routing::SuccessBasedRoutingConfigBody, + api_models::routing::ContractBasedRoutingConfig, + api_models::routing::ContractBasedRoutingConfigBody, + api_models::routing::LabelInformation, + api_models::routing::ContractBasedTimeScale, api_models::routing::LinkedRoutingConfigRetrieveResponse, api_models::routing::RoutingRetrieveResponse, api_models::routing::ProfileDefaultRoutingConfig, diff --git a/crates/openapi/src/routes/routing.rs b/crates/openapi/src/routes/routing.rs index 6b968f80172..c21669a9932 100644 --- a/crates/openapi/src/routes/routing.rs +++ b/crates/openapi/src/routes/routing.rs @@ -292,7 +292,7 @@ pub async fn toggle_success_based_routing() {} ("profile_id" = String, Path, description = "Profile id under which Dynamic routing needs to be toggled"), ("algorithm_id" = String, Path, description = "Success based routing algorithm id which was last activated to update the config"), ), - request_body = DynamicRoutingFeatures, + request_body = SuccessBasedRoutingConfig, responses( (status = 200, description = "Routing Algorithm updated", body = RoutingDictionaryRecord), (status = 400, description = "Update body is malformed"), @@ -317,7 +317,7 @@ pub async fn success_based_routing_update_configs() {} params( ("account_id" = String, Path, description = "Merchant id"), ("profile_id" = String, Path, description = "Profile id under which Dynamic routing needs to be toggled"), - ("enable" = DynamicRoutingFeatures, Query, description = "Feature to enable for success based routing"), + ("enable" = DynamicRoutingFeatures, Query, description = "Feature to enable for elimination based routing"), ), responses( (status = 200, description = "Routing Algorithm created", body = RoutingDictionaryRecord), @@ -332,3 +332,57 @@ pub async fn success_based_routing_update_configs() {} security(("api_key" = []), ("jwt_key" = [])) )] pub async fn toggle_elimination_routing() {} + +#[cfg(feature = "v1")] +/// Routing - Toggle Contract routing for profile +/// +/// Create a Contract based dynamic routing algorithm +#[utoipa::path( + post, + path = "/account/:account_id/business_profile/:profile_id/dynamic_routing/contracts/toggle", + params( + ("account_id" = String, Path, description = "Merchant id"), + ("profile_id" = String, Path, description = "Profile id under which Dynamic routing needs to be toggled"), + ("enable" = DynamicRoutingFeatures, Query, description = "Feature to enable for contract based routing"), + ), + request_body = ContractBasedRoutingConfig, + responses( + (status = 200, description = "Routing Algorithm created", body = RoutingDictionaryRecord), + (status = 400, description = "Request body is malformed"), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing"), + (status = 422, description = "Unprocessable request"), + (status = 403, description = "Forbidden"), + ), + tag = "Routing", + operation_id = "Toggle contract routing algorithm", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn contract_based_routing_setup_config() {} + +#[cfg(feature = "v1")] +/// Routing - Update contract based dynamic routing config for profile +/// +/// Update contract based dynamic routing algorithm +#[utoipa::path( + patch, + path = "/account/{account_id}/business_profile/{profile_id}/dynamic_routing/contracts/config/{algorithm_id}", + params( + ("account_id" = String, Path, description = "Merchant id"), + ("profile_id" = String, Path, description = "Profile id under which Dynamic routing needs to be toggled"), + ("algorithm_id" = String, Path, description = "Contract based routing algorithm id which was last activated to update the config"), + ), + request_body = ContractBasedRoutingConfig, + responses( + (status = 200, description = "Routing Algorithm updated", body = RoutingDictionaryRecord), + (status = 400, description = "Update body is malformed"), + (status = 500, description = "Internal server error"), + (status = 404, description = "Resource missing"), + (status = 422, description = "Unprocessable request"), + (status = 403, description = "Forbidden"), + ), + tag = "Routing", + operation_id = "Update contract based dynamic routing configs", + security(("api_key" = []), ("jwt_key" = [])) +)] +pub async fn contract_based_routing_update_configs() {} diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index b4c1c1c2c03..54cae42ceb3 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -398,6 +398,16 @@ pub enum RoutingError { GenericNotFoundError { field: String }, #[error("Unable to deserialize from '{from}' to '{to}'")] DeserializationError { from: String, to: String }, + #[error("Unable to retrieve contract based routing config")] + ContractBasedRoutingConfigError, + #[error("Params not found in contract based routing config")] + ContractBasedRoutingParamsNotFoundError, + #[error("Unable to calculate contract score from dynamic routing service")] + ContractScoreCalculationError, + #[error("contract routing client from dynamic routing gRPC service not initialized")] + ContractRoutingClientInitializationError, + #[error("Invalid contract based connector label received from dynamic routing service: '{0}'")] + InvalidContractBasedConnectorLabel(String), } #[derive(Debug, Clone, thiserror::Error)] diff --git a/crates/router/src/core/metrics.rs b/crates/router/src/core/metrics.rs index a98a4ffb259..efb463b3a37 100644 --- a/crates/router/src/core/metrics.rs +++ b/crates/router/src/core/metrics.rs @@ -82,6 +82,7 @@ counter_metric!( GLOBAL_METER ); counter_metric!(DYNAMIC_SUCCESS_BASED_ROUTING, GLOBAL_METER); +counter_metric!(DYNAMIC_CONTRACT_BASED_ROUTING, GLOBAL_METER); #[cfg(feature = "partial-auth")] counter_metric!(PARTIAL_AUTH_FAILURE, GLOBAL_METER); diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index dce89107afd..f86072fdcc2 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -6599,8 +6599,8 @@ where .attach_printable("failed to perform volume split on routing type")?; if routing_choice.routing_type.is_dynamic_routing() { - let success_based_routing_config_params_interpolator = - routing_helpers::SuccessBasedRoutingConfigParamsInterpolator::new( + let dynamic_routing_config_params_interpolator = + routing_helpers::DynamicRoutingConfigParamsInterpolator::new( payment_data.get_payment_attempt().payment_method, payment_data.get_payment_attempt().payment_method_type, payment_data.get_payment_attempt().authentication_type, @@ -6630,14 +6630,15 @@ where .and_then(|card_isin| card_isin.as_str()) .map(|card_isin| card_isin.to_string()), ); - routing::perform_success_based_routing( + + routing::perform_dynamic_routing( state, connectors.clone(), business_profile, - success_based_routing_config_params_interpolator, + dynamic_routing_config_params_interpolator, ) .await - .map_err(|e| logger::error!(success_rate_routing_error=?e)) + .map_err(|e| logger::error!(dynamic_routing_error=?e)) .unwrap_or(connectors) } else { connectors diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 70d5f79845b..516cebe24d7 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -5,6 +5,8 @@ use api_models::payments::{ConnectorMandateReferenceId, MandateReferenceId}; use api_models::routing::RoutableConnectorChoice; use async_trait::async_trait; use common_enums::{AuthorizationStatus, SessionUpdateStatus}; +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +use common_utils::ext_traits::ValueExt; use common_utils::{ ext_traits::{AsyncExt, Encode}, types::{keymanager::KeyManagerState, ConnectorTransactionId, MinorUnit}, @@ -1961,11 +1963,22 @@ async fn payment_response_update_tracker( if payment_intent.status.is_in_terminal_state() && business_profile.dynamic_routing_algorithm.is_some() { + let dynamic_routing_algo_ref: api_models::routing::DynamicRoutingAlgorithmRef = + business_profile + .dynamic_routing_algorithm + .clone() + .map(|val| val.parse_value("DynamicRoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize DynamicRoutingAlgorithmRef from JSON")? + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("DynamicRoutingAlgorithmRef not found in profile")?; + let state = state.clone(); - let business_profile = business_profile.clone(); + let profile_id = business_profile.get_id().to_owned(); let payment_attempt = payment_attempt.clone(); - let success_based_routing_config_params_interpolator = - routing_helpers::SuccessBasedRoutingConfigParamsInterpolator::new( + let dynamic_routing_config_params_interpolator = + routing_helpers::DynamicRoutingConfigParamsInterpolator::new( payment_attempt.payment_method, payment_attempt.payment_method_type, payment_attempt.authentication_type, @@ -1997,14 +2010,27 @@ async fn payment_response_update_tracker( tokio::spawn( async move { routing_helpers::push_metrics_with_update_window_for_success_based_routing( + &state, + &payment_attempt, + routable_connectors.clone(), + &profile_id, + dynamic_routing_algo_ref.clone(), + dynamic_routing_config_params_interpolator.clone(), + ) + .await + .map_err(|e| logger::error!(success_based_routing_metrics_error=?e)) + .ok(); + + routing_helpers::push_metrics_with_update_window_for_contract_based_routing( &state, &payment_attempt, routable_connectors, - &business_profile, - success_based_routing_config_params_interpolator, + &profile_id, + dynamic_routing_algo_ref, + dynamic_routing_config_params_interpolator, ) .await - .map_err(|e| logger::error!(dynamic_routing_metrics_error=?e)) + .map_err(|e| logger::error!(contract_based_routing_metrics_error=?e)) .ok(); } .in_current_span(), diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index dd592bc3064..41be7e6e2d3 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -14,6 +14,8 @@ use api_models::{ enums::{self as api_enums, CountryAlpha2}, routing::ConnectorSelection, }; +#[cfg(feature = "dynamic_routing")] +use common_utils::ext_traits::AsyncExt; use diesel_models::enums as storage_enums; use error_stack::ResultExt; use euclid::{ @@ -23,8 +25,9 @@ use euclid::{ frontend::{ast, dir as euclid_dir}, }; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] -use external_services::grpc_client::dynamic_routing::success_rate_client::{ - CalSuccessRateResponse, SuccessBasedDynamicRouting, +use external_services::grpc_client::dynamic_routing::{ + contract_routing_client::{CalContractScoreResponse, ContractBasedDynamicRouting}, + success_rate_client::{CalSuccessRateResponse, SuccessBasedDynamicRouting}, }; use hyperswitch_domain_models::address::Address; use kgraph_utils::{ @@ -1281,41 +1284,93 @@ pub fn make_dsl_input_for_surcharge( Ok(backend_input) } -/// success based dynamic routing #[cfg(all(feature = "v1", feature = "dynamic_routing"))] -#[instrument(skip_all)] -pub async fn perform_success_based_routing( +pub async fn perform_dynamic_routing( state: &SessionState, routable_connectors: Vec, - business_profile: &domain::Profile, - success_based_routing_config_params_interpolator: routing::helpers::SuccessBasedRoutingConfigParamsInterpolator, + profile: &domain::Profile, + dynamic_routing_config_params_interpolator: routing::helpers::DynamicRoutingConfigParamsInterpolator, ) -> RoutingResult> { - let success_based_dynamic_routing_algo_ref: api_routing::DynamicRoutingAlgorithmRef = - business_profile - .dynamic_routing_algorithm - .clone() - .map(|val| val.parse_value("DynamicRoutingAlgorithmRef")) - .transpose() - .change_context(errors::RoutingError::DeserializationError { - from: "JSON".to_string(), - to: "DynamicRoutingAlgorithmRef".to_string(), - }) - .attach_printable("unable to deserialize DynamicRoutingAlgorithmRef from JSON")? - .unwrap_or_default(); + let dynamic_routing_algo_ref: api_routing::DynamicRoutingAlgorithmRef = profile + .dynamic_routing_algorithm + .clone() + .map(|val| val.parse_value("DynamicRoutingAlgorithmRef")) + .transpose() + .change_context(errors::RoutingError::DeserializationError { + from: "JSON".to_string(), + to: "DynamicRoutingAlgorithmRef".to_string(), + }) + .attach_printable("unable to deserialize DynamicRoutingAlgorithmRef from JSON")? + .ok_or(errors::RoutingError::GenericNotFoundError { + field: "dynamic_routing_algorithm".to_string(), + })?; + + logger::debug!( + "performing dynamic_routing for profile {}", + profile.get_id().get_string_repr() + ); - let success_based_algo_ref = success_based_dynamic_routing_algo_ref + let connector_list = match dynamic_routing_algo_ref .success_based_algorithm - .ok_or(errors::RoutingError::GenericNotFoundError { field: "success_based_algorithm".to_string() }) - .attach_printable( - "success_based_algorithm not found in dynamic_routing_algorithm from business_profile table", - )?; + .as_ref() + .async_map(|algorithm| { + perform_success_based_routing( + state, + routable_connectors.clone(), + profile.get_id(), + dynamic_routing_config_params_interpolator.clone(), + algorithm.clone(), + ) + }) + .await + .transpose() + .inspect_err(|e| logger::error!(dynamic_routing_error=?e)) + .ok() + .flatten() + { + Some(success_based_list) => success_based_list, + None => { + // Only run contract based if success based returns None + dynamic_routing_algo_ref + .contract_based_routing + .as_ref() + .async_map(|algorithm| { + perform_contract_based_routing( + state, + routable_connectors.clone(), + profile.get_id(), + dynamic_routing_config_params_interpolator, + algorithm.clone(), + ) + }) + .await + .transpose() + .inspect_err(|e| logger::error!(dynamic_routing_error=?e)) + .ok() + .flatten() + .unwrap_or(routable_connectors) + } + }; + Ok(connector_list) +} + +/// success based dynamic routing +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +#[instrument(skip_all)] +pub async fn perform_success_based_routing( + state: &SessionState, + routable_connectors: Vec, + profile_id: &common_utils::id_type::ProfileId, + success_based_routing_config_params_interpolator: routing::helpers::DynamicRoutingConfigParamsInterpolator, + success_based_algo_ref: api_routing::SuccessBasedAlgorithm, +) -> RoutingResult> { if success_based_algo_ref.enabled_feature == api_routing::DynamicRoutingFeatures::DynamicConnectorSelection { logger::debug!( "performing success_based_routing for profile {}", - business_profile.get_id().get_string_repr() + profile_id.get_string_repr() ); let client = state .grpc_client @@ -1325,18 +1380,18 @@ pub async fn perform_success_based_routing( .ok_or(errors::RoutingError::SuccessRateClientInitializationError) .attach_printable("success_rate gRPC client not found")?; - let success_based_routing_configs = routing::helpers::fetch_success_based_routing_configs( + let success_based_routing_configs = routing::helpers::fetch_dynamic_routing_configs::< + api_routing::SuccessBasedRoutingConfig, + >( state, - business_profile, + profile_id, success_based_algo_ref .algorithm_id_with_timestamp .algorithm_id .ok_or(errors::RoutingError::GenericNotFoundError { field: "success_based_routing_algorithm_id".to_string(), }) - .attach_printable( - "success_based_routing_algorithm_id not found in business_profile", - )?, + .attach_printable("success_based_routing_algorithm_id not found in profile_id")?, ) .await .change_context(errors::RoutingError::SuccessBasedRoutingConfigError) @@ -1352,7 +1407,7 @@ pub async fn perform_success_based_routing( let success_based_connectors: CalSuccessRateResponse = client .calculate_success_rate( - business_profile.get_id().get_string_repr().into(), + profile_id.get_string_repr().into(), success_based_routing_configs, success_based_routing_config_params, routable_connectors, @@ -1398,3 +1453,95 @@ pub async fn perform_success_based_routing( Ok(routable_connectors) } } + +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +pub async fn perform_contract_based_routing( + state: &SessionState, + routable_connectors: Vec, + profile_id: &common_utils::id_type::ProfileId, + _dynamic_routing_config_params_interpolator: routing::helpers::DynamicRoutingConfigParamsInterpolator, + contract_based_algo_ref: api_routing::ContractRoutingAlgorithm, +) -> RoutingResult> { + if contract_based_algo_ref.enabled_feature + == api_routing::DynamicRoutingFeatures::DynamicConnectorSelection + { + logger::debug!( + "performing contract_based_routing for profile {}", + profile_id.get_string_repr() + ); + let client = state + .grpc_client + .dynamic_routing + .contract_based_client + .as_ref() + .ok_or(errors::RoutingError::ContractRoutingClientInitializationError) + .attach_printable("contract routing gRPC client not found")?; + + let contract_based_routing_configs = routing::helpers::fetch_dynamic_routing_configs::< + api_routing::ContractBasedRoutingConfig, + >( + state, + profile_id, + contract_based_algo_ref + .algorithm_id_with_timestamp + .algorithm_id + .ok_or(errors::RoutingError::GenericNotFoundError { + field: "contract_based_routing_algorithm_id".to_string(), + }) + .attach_printable("contract_based_routing_algorithm_id not found in profile_id")?, + ) + .await + .change_context(errors::RoutingError::ContractBasedRoutingConfigError) + .attach_printable("unable to fetch contract based dynamic routing configs")?; + + let contract_based_connectors: CalContractScoreResponse = client + .calculate_contract_score( + profile_id.get_string_repr().into(), + contract_based_routing_configs, + "".to_string(), + routable_connectors, + state.get_grpc_headers(), + ) + .await + .change_context(errors::RoutingError::ContractScoreCalculationError) + .attach_printable( + "unable to calculate/fetch contract score from dynamic routing service", + )?; + + let mut connectors = Vec::with_capacity(contract_based_connectors.labels_with_score.len()); + + for label_with_score in contract_based_connectors.labels_with_score { + let (connector, merchant_connector_id) = label_with_score.label + .split_once(':') + .ok_or(errors::RoutingError::InvalidContractBasedConnectorLabel(label_with_score.label.to_string())) + .attach_printable( + "unable to split connector_name and mca_id from the label obtained by the dynamic routing service", + )?; + + connectors.push(api_routing::RoutableConnectorChoice { + choice_kind: api_routing::RoutableChoiceKind::FullStruct, + connector: common_enums::RoutableConnectors::from_str(connector) + .change_context(errors::RoutingError::GenericConversionError { + from: "String".to_string(), + to: "RoutableConnectors".to_string(), + }) + .attach_printable("unable to convert String to RoutableConnectors")?, + merchant_connector_id: Some( + common_utils::id_type::MerchantConnectorAccountId::wrap( + merchant_connector_id.to_string(), + ) + .change_context(errors::RoutingError::GenericConversionError { + from: "String".to_string(), + to: "MerchantConnectorAccountId".to_string(), + }) + .attach_printable("unable to convert MerchantConnectorAccountId from string")?, + ), + }); + } + + logger::debug!(contract_based_routing_connectors=?connectors); + Ok(connectors) + } else { + Ok(routable_connectors) + } +} diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 99bd2b00209..8d03e82a328 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -2,6 +2,8 @@ pub mod helpers; pub mod transformers; use std::collections::HashSet; +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +use api_models::routing::DynamicRoutingAlgoAccessor; use api_models::{ enums, mandates as mandates_api, routing, routing::{self as routing_types, RoutingRetrieveQuery}, @@ -12,7 +14,10 @@ use common_utils::ext_traits::AsyncExt; use diesel_models::routing_algorithm::RoutingAlgorithm; use error_stack::ResultExt; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] -use external_services::grpc_client::dynamic_routing::success_rate_client::SuccessBasedDynamicRouting; +use external_services::grpc_client::dynamic_routing::{ + contract_routing_client::ContractBasedDynamicRouting, + success_rate_client::SuccessBasedDynamicRouting, +}; use hyperswitch_domain_models::{mandates, payment_address}; #[cfg(all(feature = "v1", feature = "dynamic_routing"))] use router_env::logger; @@ -462,6 +467,16 @@ pub async fn link_routing_config( }, enabled_feature: _ }) if id == &algorithm_id + ) || matches!( + dynamic_routing_ref.contract_based_routing, + Some(routing::ContractRoutingAlgorithm { + algorithm_id_with_timestamp: + routing_types::DynamicAlgorithmWithTimestamp { + algorithm_id: Some(ref id), + timestamp: _ + }, + enabled_feature: _ + }) if id == &algorithm_id ), || { Err(errors::ApiErrorResponse::PreconditionFailed { @@ -470,7 +485,8 @@ pub async fn link_routing_config( }, )?; - dynamic_routing_ref.update_algorithm_id( + if routing_algorithm.name == helpers::SUCCESS_BASED_DYNAMIC_ROUTING_ALGORITHM { + dynamic_routing_ref.update_algorithm_id( algorithm_id, dynamic_routing_ref .success_based_algorithm @@ -482,6 +498,34 @@ pub async fn link_routing_config( .enabled_feature, routing_types::DynamicRoutingType::SuccessRateBasedRouting, ); + } else if routing_algorithm.name == helpers::ELIMINATION_BASED_DYNAMIC_ROUTING_ALGORITHM + { + dynamic_routing_ref.update_algorithm_id( + algorithm_id, + dynamic_routing_ref + .elimination_routing_algorithm + .clone() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "missing elimination_routing_algorithm in dynamic_algorithm_ref from business_profile table", + )? + .enabled_feature, + routing_types::DynamicRoutingType::EliminationRouting, + ); + } else if routing_algorithm.name == helpers::CONTRACT_BASED_DYNAMIC_ROUTING_ALGORITHM { + dynamic_routing_ref.update_algorithm_id( + algorithm_id, + dynamic_routing_ref + .contract_based_routing + .clone() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "missing contract_based_routing in dynamic_algorithm_ref from business_profile table", + )? + .enabled_feature, + routing_types::DynamicRoutingType::ContractBasedRouting, + ); + } helpers::update_business_profile_active_dynamic_algorithm_ref( db, @@ -1419,6 +1463,304 @@ pub async fn success_based_routing_update_configs( Ok(service_api::ApplicationResponse::Json(new_record)) } +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +pub async fn contract_based_dynamic_routing_setup( + state: SessionState, + key_store: domain::MerchantKeyStore, + merchant_account: domain::MerchantAccount, + profile_id: common_utils::id_type::ProfileId, + feature_to_enable: routing_types::DynamicRoutingFeatures, + config: Option, +) -> RouterResult> { + let db = state.store.as_ref(); + let key_manager_state = &(&state).into(); + + let business_profile: domain::Profile = core_utils::validate_and_get_business_profile( + db, + key_manager_state, + &key_store, + Some(&profile_id), + merchant_account.get_id(), + ) + .await? + .get_required_value("Profile") + .change_context(errors::ApiErrorResponse::ProfileNotFound { + id: profile_id.get_string_repr().to_owned(), + })?; + + let mut dynamic_routing_algo_ref: Option = + business_profile + .dynamic_routing_algorithm + .clone() + .map(|val| val.parse_value("DynamicRoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to deserialize dynamic routing algorithm ref from business profile", + ) + .ok() + .flatten(); + + utils::when( + dynamic_routing_algo_ref + .as_mut() + .and_then(|algo| { + algo.contract_based_routing.as_mut().map(|contract_algo| { + *contract_algo.get_enabled_features() == feature_to_enable + && contract_algo + .clone() + .get_algorithm_id_with_timestamp() + .algorithm_id + .is_some() + }) + }) + .unwrap_or(false), + || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Contract Routing with specified features is already enabled".to_string(), + }) + }, + )?; + + if feature_to_enable == routing::DynamicRoutingFeatures::None { + let algorithm = dynamic_routing_algo_ref + .clone() + .get_required_value("dynamic_routing_algo_ref") + .attach_printable("Failed to get dynamic_routing_algo_ref")?; + return helpers::disable_dynamic_routing_algorithm( + &state, + key_store, + business_profile, + algorithm, + routing_types::DynamicRoutingType::ContractBasedRouting, + ) + .await; + } + + let config = config + .get_required_value("ContractBasedRoutingConfig") + .attach_printable("Failed to get ContractBasedRoutingConfig from request")?; + + let merchant_id = business_profile.merchant_id.clone(); + let algorithm_id = common_utils::generate_routing_id_of_default_length(); + let timestamp = common_utils::date_time::now(); + + let algo = RoutingAlgorithm { + algorithm_id: algorithm_id.clone(), + profile_id: profile_id.clone(), + merchant_id, + name: helpers::CONTRACT_BASED_DYNAMIC_ROUTING_ALGORITHM.to_string(), + description: None, + kind: diesel_models::enums::RoutingAlgorithmKind::Dynamic, + algorithm_data: serde_json::json!(config), + created_at: timestamp, + modified_at: timestamp, + algorithm_for: common_enums::TransactionType::Payment, + }; + + // 1. if dynamic_routing_algo_ref already present, insert contract based algo and disable success based + // 2. if dynamic_routing_algo_ref is not present, create a new dynamic_routing_algo_ref with contract algo set up + let final_algorithm = if let Some(mut algo) = dynamic_routing_algo_ref { + algo.update_algorithm_id( + algorithm_id, + feature_to_enable, + routing_types::DynamicRoutingType::ContractBasedRouting, + ); + if feature_to_enable == routing::DynamicRoutingFeatures::DynamicConnectorSelection { + algo.disable_algorithm_id(routing_types::DynamicRoutingType::SuccessRateBasedRouting); + } + algo + } else { + let contract_algo = routing_types::ContractRoutingAlgorithm { + algorithm_id_with_timestamp: routing_types::DynamicAlgorithmWithTimestamp::new(Some( + algorithm_id.clone(), + )), + enabled_feature: feature_to_enable, + }; + routing_types::DynamicRoutingAlgorithmRef { + success_based_algorithm: None, + elimination_routing_algorithm: None, + dynamic_routing_volume_split: None, + contract_based_routing: Some(contract_algo), + } + }; + + // validate the contained mca_ids + if let Some(info_vec) = &config.label_info { + let validation_futures: Vec<_> = info_vec + .iter() + .map(|info| async { + let mca_id = info.mca_id.clone(); + let label = info.label.clone(); + let mca = db + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + key_manager_state, + merchant_account.get_id(), + &mca_id, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: mca_id.get_string_repr().to_owned(), + })?; + + utils::when(mca.connector_name != label, || { + Err(error_stack::Report::new( + errors::ApiErrorResponse::InvalidRequestData { + message: "Incorrect mca configuration received".to_string(), + }, + )) + })?; + + Ok::<_, error_stack::Report>(()) + }) + .collect(); + + futures::future::try_join_all(validation_futures).await?; + } + + let record = db + .insert_routing_algorithm(algo) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to insert record in routing algorithm table")?; + + helpers::update_business_profile_active_dynamic_algorithm_ref( + db, + key_manager_state, + &key_store, + business_profile, + final_algorithm, + ) + .await?; + + let new_record = record.foreign_into(); + + metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add( + 1, + router_env::metric_attributes!(("profile_id", profile_id.get_string_repr().to_string())), + ); + Ok(service_api::ApplicationResponse::Json(new_record)) +} + +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +pub async fn contract_based_routing_update_configs( + state: SessionState, + request: routing_types::ContractBasedRoutingConfig, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + algorithm_id: common_utils::id_type::RoutingId, + profile_id: common_utils::id_type::ProfileId, +) -> RouterResponse { + metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE.add( + 1, + router_env::metric_attributes!(("profile_id", profile_id.get_string_repr().to_owned())), + ); + let db = state.store.as_ref(); + let key_manager_state = &(&state).into(); + + let dynamic_routing_algo_to_update = db + .find_routing_algorithm_by_profile_id_algorithm_id(&profile_id, &algorithm_id) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; + + let mut config_to_update: routing::ContractBasedRoutingConfig = dynamic_routing_algo_to_update + .algorithm_data + .parse_value::("ContractBasedRoutingConfig") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize algorithm data from routing table into ContractBasedRoutingConfig")?; + + // validate the contained mca_ids + if let Some(info_vec) = &request.label_info { + for info in info_vec { + let mca = db + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + key_manager_state, + merchant_account.get_id(), + &info.mca_id, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: info.mca_id.get_string_repr().to_owned(), + })?; + + utils::when(mca.connector_name != info.label, || { + Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Incorrect mca configuration received".to_string(), + }) + })?; + } + } + + config_to_update.update(request); + + let updated_algorithm_id = common_utils::generate_routing_id_of_default_length(); + let timestamp = common_utils::date_time::now(); + let algo = RoutingAlgorithm { + algorithm_id: updated_algorithm_id, + profile_id: dynamic_routing_algo_to_update.profile_id, + merchant_id: dynamic_routing_algo_to_update.merchant_id, + name: dynamic_routing_algo_to_update.name, + description: dynamic_routing_algo_to_update.description, + kind: dynamic_routing_algo_to_update.kind, + algorithm_data: serde_json::json!(config_to_update), + created_at: timestamp, + modified_at: timestamp, + algorithm_for: dynamic_routing_algo_to_update.algorithm_for, + }; + let record = db + .insert_routing_algorithm(algo) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to insert record in routing algorithm table")?; + + // redact cache for contract based routing configs + let cache_key = format!( + "{}_{}", + profile_id.get_string_repr(), + algorithm_id.get_string_repr() + ); + let cache_entries_to_redact = vec![cache::CacheKind::ContractBasedDynamicRoutingCache( + cache_key.into(), + )]; + let _ = cache::redact_from_redis_and_publish( + state.store.get_cache_store().as_ref(), + cache_entries_to_redact, + ) + .await + .map_err(|e| logger::error!("unable to publish into the redact channel for evicting the contract based routing config cache {e:?}")); + + let new_record = record.foreign_into(); + + metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE.add( + 1, + router_env::metric_attributes!(("profile_id", profile_id.get_string_repr().to_owned())), + ); + + state + .grpc_client + .clone() + .dynamic_routing + .contract_based_client + .clone() + .async_map(|ct_client| async move { + ct_client + .invalidate_contracts( + profile_id.get_string_repr().into(), + state.get_grpc_headers(), + ) + .await + .change_context(errors::ApiErrorResponse::GenericNotFoundError { + message: "Failed to invalidate the contract based routing keys".to_string(), + }) + }) + .await + .transpose()?; + + Ok(service_api::ApplicationResponse::Json(new_record)) +} + #[async_trait] pub trait GetRoutableConnectorsForChoice { async fn get_routable_connectors( diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index 84de32483a8..9b6396013f4 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -2,6 +2,7 @@ //! //! Functions that are used to perform the retrieval of merchant's //! routing dict, configs, defaults +use std::fmt::Debug; #[cfg(all(feature = "dynamic_routing", feature = "v1"))] use std::str::FromStr; #[cfg(any(feature = "dynamic_routing", feature = "v1"))] @@ -18,7 +19,10 @@ use diesel_models::dynamic_routing_stats::DynamicRoutingStatsNew; use diesel_models::routing_algorithm; use error_stack::ResultExt; #[cfg(all(feature = "dynamic_routing", feature = "v1"))] -use external_services::grpc_client::dynamic_routing::success_rate_client::SuccessBasedDynamicRouting; +use external_services::grpc_client::dynamic_routing::{ + contract_routing_client::ContractBasedDynamicRouting, + success_rate_client::SuccessBasedDynamicRouting, +}; #[cfg(feature = "v1")] use hyperswitch_domain_models::api::ApplicationResponse; #[cfg(all(feature = "dynamic_routing", feature = "v1"))] @@ -26,7 +30,7 @@ use router_env::logger; #[cfg(any(feature = "dynamic_routing", feature = "v1"))] use router_env::{instrument, tracing}; use rustc_hash::FxHashSet; -use storage_impl::redis::cache; +use storage_impl::redis::cache::{self, Cacheable}; #[cfg(all(feature = "dynamic_routing", feature = "v1"))] use crate::db::errors::StorageErrorExt; @@ -45,6 +49,8 @@ pub const SUCCESS_BASED_DYNAMIC_ROUTING_ALGORITHM: &str = "Success rate based dynamic routing algorithm"; pub const ELIMINATION_BASED_DYNAMIC_ROUTING_ALGORITHM: &str = "Elimination based dynamic routing algorithm"; +pub const CONTRACT_BASED_DYNAMIC_ROUTING_ALGORITHM: &str = + "Contract based dynamic routing algorithm"; /// Provides us with all the configured configs of the Merchant in the ascending time configured /// manner and chooses the first of them @@ -562,78 +568,146 @@ pub fn get_default_config_key( } } -/// Retrieves cached success_based routing configs specific to tenant and profile -#[cfg(all(feature = "v1", feature = "dynamic_routing"))] -pub async fn get_cached_success_based_routing_config_for_profile( - state: &SessionState, - key: &str, -) -> Option> { - cache::SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE - .get_val::>(cache::CacheKey { - key: key.to_string(), - prefix: state.tenant.redis_key_prefix.clone(), - }) +#[async_trait::async_trait] +pub trait DynamicRoutingCache { + async fn get_cached_dynamic_routing_config_for_profile( + state: &SessionState, + key: &str, + ) -> Option>; + + async fn refresh_dynamic_routing_cache( + state: &SessionState, + key: &str, + func: F, + ) -> RouterResult + where + F: FnOnce() -> Fut + Send, + T: Cacheable + serde::Serialize + serde::de::DeserializeOwned + Debug + Clone, + Fut: futures::Future> + Send; +} + +#[async_trait::async_trait] +impl DynamicRoutingCache for routing_types::SuccessBasedRoutingConfig { + async fn get_cached_dynamic_routing_config_for_profile( + state: &SessionState, + key: &str, + ) -> Option> { + cache::SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE + .get_val::>(cache::CacheKey { + key: key.to_string(), + prefix: state.tenant.redis_key_prefix.clone(), + }) + .await + } + + async fn refresh_dynamic_routing_cache( + state: &SessionState, + key: &str, + func: F, + ) -> RouterResult + where + F: FnOnce() -> Fut + Send, + T: Cacheable + serde::Serialize + serde::de::DeserializeOwned + Debug + Clone, + Fut: futures::Future> + Send, + { + cache::get_or_populate_in_memory( + state.store.get_cache_store().as_ref(), + key, + func, + &cache::SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE, + ) .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to populate SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE") + } } -/// Refreshes the cached success_based routing configs specific to tenant and profile -#[cfg(feature = "v1")] -pub async fn refresh_success_based_routing_cache( - state: &SessionState, - key: &str, - success_based_routing_config: routing_types::SuccessBasedRoutingConfig, -) -> Arc { - let config = Arc::new(success_based_routing_config); - cache::SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE - .push( - cache::CacheKey { +#[async_trait::async_trait] +impl DynamicRoutingCache for routing_types::ContractBasedRoutingConfig { + async fn get_cached_dynamic_routing_config_for_profile( + state: &SessionState, + key: &str, + ) -> Option> { + cache::CONTRACT_BASED_DYNAMIC_ALGORITHM_CACHE + .get_val::>(cache::CacheKey { key: key.to_string(), prefix: state.tenant.redis_key_prefix.clone(), - }, - config.clone(), + }) + .await + } + + async fn refresh_dynamic_routing_cache( + state: &SessionState, + key: &str, + func: F, + ) -> RouterResult + where + F: FnOnce() -> Fut + Send, + T: Cacheable + serde::Serialize + serde::de::DeserializeOwned + Debug + Clone, + Fut: futures::Future> + Send, + { + cache::get_or_populate_in_memory( + state.store.get_cache_store().as_ref(), + key, + func, + &cache::CONTRACT_BASED_DYNAMIC_ALGORITHM_CACHE, ) - .await; - config + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to populate CONTRACT_BASED_DYNAMIC_ALGORITHM_CACHE") + } } -/// Checked fetch of success based routing configs +/// Cfetch dynamic routing configs #[cfg(all(feature = "v1", feature = "dynamic_routing"))] #[instrument(skip_all)] -pub async fn fetch_success_based_routing_configs( +pub async fn fetch_dynamic_routing_configs( state: &SessionState, - business_profile: &domain::Profile, - success_based_routing_id: id_type::RoutingId, -) -> RouterResult { + profile_id: &id_type::ProfileId, + routing_id: id_type::RoutingId, +) -> RouterResult +where + T: serde::de::DeserializeOwned + + Clone + + DynamicRoutingCache + + Cacheable + + serde::Serialize + + Debug, +{ let key = format!( "{}_{}", - business_profile.get_id().get_string_repr(), - success_based_routing_id.get_string_repr() + profile_id.get_string_repr(), + routing_id.get_string_repr() ); if let Some(config) = - get_cached_success_based_routing_config_for_profile(state, key.as_str()).await + T::get_cached_dynamic_routing_config_for_profile(state, key.as_str()).await { Ok(config.as_ref().clone()) } else { - let success_rate_algorithm = state - .store - .find_routing_algorithm_by_profile_id_algorithm_id( - business_profile.get_id(), - &success_based_routing_id, - ) - .await - .change_context(errors::ApiErrorResponse::ResourceIdNotFound) - .attach_printable("unable to retrieve success_rate_algorithm for profile from db")?; - - let success_rate_config = success_rate_algorithm - .algorithm_data - .parse_value::("SuccessBasedRoutingConfig") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("unable to parse success_based_routing_config struct")?; + let func = || async { + let routing_algorithm = state + .store + .find_routing_algorithm_by_profile_id_algorithm_id(profile_id, &routing_id) + .await + .change_context(errors::StorageError::ValueNotFound( + "RoutingAlgorithm".to_string(), + )) + .attach_printable("unable to retrieve routing_algorithm for profile from db")?; + + let dynamic_routing_config = routing_algorithm + .algorithm_data + .parse_value::("dynamic_routing_config") + .change_context(errors::StorageError::DeserializationFailed) + .attach_printable("unable to parse dynamic_routing_config")?; + + Ok(dynamic_routing_config) + }; - refresh_success_based_routing_cache(state, key.as_str(), success_rate_config.clone()).await; + let dynamic_routing_config = + T::refresh_dynamic_routing_cache(state, key.as_str(), func).await?; - Ok(success_rate_config) + Ok(dynamic_routing_config) } } @@ -644,20 +718,11 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( state: &SessionState, payment_attempt: &storage::PaymentAttempt, routable_connectors: Vec, - business_profile: &domain::Profile, - success_based_routing_config_params_interpolator: SuccessBasedRoutingConfigParamsInterpolator, + profile_id: &id_type::ProfileId, + dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef, + dynamic_routing_config_params_interpolator: DynamicRoutingConfigParamsInterpolator, ) -> RouterResult<()> { - let success_based_dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef = - business_profile - .dynamic_routing_algorithm - .clone() - .map(|val| val.parse_value("DynamicRoutingAlgorithmRef")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to deserialize DynamicRoutingAlgorithmRef from JSON")? - .unwrap_or_default(); - - let success_based_algo_ref = success_based_dynamic_routing_algo_ref + let success_based_algo_ref = dynamic_routing_algo_ref .success_based_algorithm .ok_or(errors::ApiErrorResponse::InternalServerError) .attach_printable("success_based_algorithm not found in dynamic_routing_algorithm from business_profile table")?; @@ -678,22 +743,23 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( }, )?; - let success_based_routing_configs = fetch_success_based_routing_configs( - state, - business_profile, - success_based_algo_ref - .algorithm_id_with_timestamp - .algorithm_id - .ok_or(errors::ApiErrorResponse::InternalServerError) - .attach_printable( - "success_based_routing_algorithm_id not found in business_profile", - )?, - ) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("unable to retrieve success_rate based dynamic routing configs")?; + let success_based_routing_configs = + fetch_dynamic_routing_configs::( + state, + profile_id, + success_based_algo_ref + .algorithm_id_with_timestamp + .algorithm_id + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "success_based_routing_algorithm_id not found in business_profile", + )?, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to retrieve success_rate based dynamic routing configs")?; - let success_based_routing_config_params = success_based_routing_config_params_interpolator + let success_based_routing_config_params = dynamic_routing_config_params_interpolator .get_string_val( success_based_routing_configs .params @@ -704,7 +770,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( let success_based_connectors = client .calculate_entity_and_global_success_rate( - business_profile.get_id().get_string_repr().into(), + profile_id.get_string_repr().into(), success_based_routing_configs.clone(), success_based_routing_config_params.clone(), routable_connectors.clone(), @@ -717,7 +783,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( )?; let payment_status_attribute = - get_desired_payment_status_for_success_routing_metrics(payment_attempt.status); + get_desired_payment_status_for_dynamic_routing_metrics(payment_attempt.status); let first_merchant_success_based_connector = &success_based_connectors .entity_scores_with_labels @@ -743,7 +809,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( "unable to fetch the first global connector from list of connectors obtained from dynamic routing service", )?; - let outcome = get_success_based_metrics_outcome_for_payment( + let outcome = get_dynamic_routing_based_metrics_outcome_for_payment( payment_status_attribute, payment_connector.to_string(), first_merchant_success_based_connector_label.to_string(), @@ -852,7 +918,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( client .update_success_rate( - business_profile.get_id().get_string_repr().into(), + profile_id.get_string_repr().into(), success_based_routing_configs, success_based_routing_config_params, vec![routing_types::RoutableConnectorChoiceWithStatus::new( @@ -880,8 +946,212 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( } } +/// metrics for contract based dynamic routing +#[cfg(all(feature = "v1", feature = "dynamic_routing"))] +#[instrument(skip_all)] +pub async fn push_metrics_with_update_window_for_contract_based_routing( + state: &SessionState, + payment_attempt: &storage::PaymentAttempt, + routable_connectors: Vec, + profile_id: &id_type::ProfileId, + dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef, + _dynamic_routing_config_params_interpolator: DynamicRoutingConfigParamsInterpolator, +) -> RouterResult<()> { + let contract_routing_algo_ref = dynamic_routing_algo_ref + .contract_based_routing + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("contract_routing_algorithm not found in dynamic_routing_algorithm from business_profile table")?; + + if contract_routing_algo_ref.enabled_feature != routing_types::DynamicRoutingFeatures::None { + let client = state + .grpc_client + .dynamic_routing + .contract_based_client + .clone() + .ok_or(errors::ApiErrorResponse::GenericNotFoundError { + message: "contract_routing gRPC client not found".to_string(), + })?; + + let payment_connector = &payment_attempt.connector.clone().ok_or( + errors::ApiErrorResponse::GenericNotFoundError { + message: "unable to derive payment connector from payment attempt".to_string(), + }, + )?; + + let contract_based_routing_config = + fetch_dynamic_routing_configs::( + state, + profile_id, + contract_routing_algo_ref + .algorithm_id_with_timestamp + .algorithm_id + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "contract_based_routing_algorithm_id not found in business_profile", + )?, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to retrieve contract based dynamic routing configs")?; + + let mut existing_label_info = None; + + contract_based_routing_config + .label_info + .as_ref() + .map(|label_info_vec| { + for label_info in label_info_vec { + if Some(&label_info.mca_id) == payment_attempt.merchant_connector_id.as_ref() { + existing_label_info = Some(label_info.clone()); + } + } + }); + + let final_label_info = existing_label_info + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to get LabelInformation from ContractBasedRoutingConfig")?; + + logger::debug!( + "contract based routing: matched LabelInformation - {:?}", + final_label_info + ); + + let request_label_info = routing_types::LabelInformation { + label: format!( + "{}:{}", + final_label_info.label.clone(), + final_label_info.mca_id.get_string_repr() + ), + target_count: final_label_info.target_count, + target_time: final_label_info.target_time, + mca_id: final_label_info.mca_id.to_owned(), + }; + + let payment_status_attribute = + get_desired_payment_status_for_dynamic_routing_metrics(payment_attempt.status); + + if payment_status_attribute == common_enums::AttemptStatus::Charged { + client + .update_contracts( + profile_id.get_string_repr().into(), + vec![request_label_info], + "".to_string(), + vec![], + state.get_grpc_headers(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to update contract based routing window in dynamic routing service", + )?; + } + + let contract_scores = client + .calculate_contract_score( + profile_id.get_string_repr().into(), + contract_based_routing_config.clone(), + "".to_string(), + routable_connectors.clone(), + state.get_grpc_headers(), + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to calculate/fetch contract scores from dynamic routing service", + )?; + + let first_contract_based_connector = &contract_scores + .labels_with_score + .first() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to fetch the first connector from list of connectors obtained from dynamic routing service", + )?; + + let (first_contract_based_connector, connector_score, current_payment_cnt) = (first_contract_based_connector.label + .split_once(':') + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable(format!( + "unable to split connector_name and mca_id from the first connector {:?} obtained from dynamic routing service", + first_contract_based_connector + ))? + .0, first_contract_based_connector.score, first_contract_based_connector.current_count ); + + core_metrics::DYNAMIC_CONTRACT_BASED_ROUTING.add( + 1, + router_env::metric_attributes!( + ( + "tenant", + state.tenant.tenant_id.get_string_repr().to_owned(), + ), + ( + "merchant_profile_id", + format!( + "{}:{}", + payment_attempt.merchant_id.get_string_repr(), + payment_attempt.profile_id.get_string_repr() + ), + ), + ( + "contract_based_routing_connector", + first_contract_based_connector.to_string(), + ), + ( + "contract_based_routing_connector_score", + connector_score.to_string(), + ), + ( + "current_payment_count_contract_based_routing_connector", + current_payment_cnt.to_string(), + ), + ("payment_connector", payment_connector.to_string()), + ( + "currency", + payment_attempt + .currency + .map_or_else(|| "None".to_string(), |currency| currency.to_string()), + ), + ( + "payment_method", + payment_attempt.payment_method.map_or_else( + || "None".to_string(), + |payment_method| payment_method.to_string(), + ), + ), + ( + "payment_method_type", + payment_attempt.payment_method_type.map_or_else( + || "None".to_string(), + |payment_method_type| payment_method_type.to_string(), + ), + ), + ( + "capture_method", + payment_attempt.capture_method.map_or_else( + || "None".to_string(), + |capture_method| capture_method.to_string(), + ), + ), + ( + "authentication_type", + payment_attempt.authentication_type.map_or_else( + || "None".to_string(), + |authentication_type| authentication_type.to_string(), + ), + ), + ("payment_status", payment_attempt.status.to_string()), + ), + ); + logger::debug!("successfully pushed contract_based_routing metrics"); + + Ok(()) + } else { + Ok(()) + } +} + #[cfg(all(feature = "v1", feature = "dynamic_routing"))] -fn get_desired_payment_status_for_success_routing_metrics( +fn get_desired_payment_status_for_dynamic_routing_metrics( attempt_status: common_enums::AttemptStatus, ) -> common_enums::AttemptStatus { match attempt_status { @@ -917,7 +1187,7 @@ fn get_desired_payment_status_for_success_routing_metrics( } #[cfg(all(feature = "v1", feature = "dynamic_routing"))] -fn get_success_based_metrics_outcome_for_payment( +fn get_dynamic_routing_based_metrics_outcome_for_payment( payment_status_attribute: common_enums::AttemptStatus, payment_connector: String, first_success_based_connector: String, @@ -957,7 +1227,6 @@ pub async fn disable_dynamic_routing_algorithm( ) -> RouterResult> { let db = state.store.as_ref(); let key_manager_state = &state.into(); - let timestamp = common_utils::date_time::now_unix_timestamp(); let profile_id = business_profile.get_id().clone(); let (algorithm_id, dynamic_routing_algorithm, cache_entries_to_redact) = match dynamic_routing_type { @@ -988,14 +1257,12 @@ pub async fn disable_dynamic_routing_algorithm( routing_types::DynamicRoutingAlgorithmRef { success_based_algorithm: Some(routing_types::SuccessBasedAlgorithm { algorithm_id_with_timestamp: - routing_types::DynamicAlgorithmWithTimestamp { - algorithm_id: None, - timestamp, - }, + routing_types::DynamicAlgorithmWithTimestamp::new(None), enabled_feature: routing_types::DynamicRoutingFeatures::None, }), elimination_routing_algorithm: dynamic_routing_algo_ref .elimination_routing_algorithm, + contract_based_routing: dynamic_routing_algo_ref.contract_based_routing, dynamic_routing_volume_split: dynamic_routing_algo_ref .dynamic_routing_volume_split, }, @@ -1033,13 +1300,49 @@ pub async fn disable_dynamic_routing_algorithm( elimination_routing_algorithm: Some( routing_types::EliminationRoutingAlgorithm { algorithm_id_with_timestamp: - routing_types::DynamicAlgorithmWithTimestamp { - algorithm_id: None, - timestamp, - }, + routing_types::DynamicAlgorithmWithTimestamp::new(None), enabled_feature: routing_types::DynamicRoutingFeatures::None, }, ), + contract_based_routing: dynamic_routing_algo_ref.contract_based_routing, + }, + cache_entries_to_redact, + ) + } + routing_types::DynamicRoutingType::ContractBasedRouting => { + let Some(algorithm_ref) = dynamic_routing_algo_ref.contract_based_routing else { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Contract routing is already disabled".to_string(), + })? + }; + let Some(algorithm_id) = algorithm_ref.algorithm_id_with_timestamp.algorithm_id + else { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Algorithm is already inactive".to_string(), + })? + }; + let cache_key = format!( + "{}_{}", + business_profile.get_id().get_string_repr(), + algorithm_id.get_string_repr() + ); + let cache_entries_to_redact = + vec![cache::CacheKind::ContractBasedDynamicRoutingCache( + cache_key.into(), + )]; + ( + algorithm_id, + routing_types::DynamicRoutingAlgorithmRef { + success_based_algorithm: dynamic_routing_algo_ref.success_based_algorithm, + elimination_routing_algorithm: dynamic_routing_algo_ref + .elimination_routing_algorithm, + dynamic_routing_volume_split: dynamic_routing_algo_ref + .dynamic_routing_volume_split, + contract_based_routing: Some(routing_types::ContractRoutingAlgorithm { + algorithm_id_with_timestamp: + routing_types::DynamicAlgorithmWithTimestamp::new(None), + enabled_feature: routing_types::DynamicRoutingFeatures::None, + }), }, cache_entries_to_redact, ) @@ -1088,15 +1391,18 @@ pub async fn enable_dynamic_routing_algorithm( dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef, dynamic_routing_type: routing_types::DynamicRoutingType, ) -> RouterResult> { - let dynamic_routing = dynamic_routing_algo_ref.clone(); + let mut dynamic_routing = dynamic_routing_algo_ref.clone(); match dynamic_routing_type { routing_types::DynamicRoutingType::SuccessRateBasedRouting => { + dynamic_routing + .disable_algorithm_id(routing_types::DynamicRoutingType::ContractBasedRouting); + enable_specific_routing_algorithm( state, key_store, business_profile, feature_to_enable, - dynamic_routing_algo_ref, + dynamic_routing.clone(), dynamic_routing_type, dynamic_routing.success_based_algorithm, ) @@ -1108,12 +1414,18 @@ pub async fn enable_dynamic_routing_algorithm( key_store, business_profile, feature_to_enable, - dynamic_routing_algo_ref, + dynamic_routing.clone(), dynamic_routing_type, dynamic_routing.elimination_routing_algorithm, ) .await } + routing_types::DynamicRoutingType::ContractBasedRouting => { + Err((errors::ApiErrorResponse::InvalidRequestData { + message: "Contract routing cannot be set as default".to_string(), + }) + .into()) + } } } @@ -1128,7 +1440,7 @@ pub async fn enable_specific_routing_algorithm( algo_type: Option, ) -> RouterResult> where - A: routing_types::DynamicRoutingAlgoAccessor + Clone + std::fmt::Debug, + A: routing_types::DynamicRoutingAlgoAccessor + Clone + Debug, { // Algorithm wasn't created yet let Some(mut algo_type) = algo_type else { @@ -1169,9 +1481,8 @@ where } .into()); }; - *algo_type_enabled_features = feature_to_enable.clone(); - dynamic_routing_algo_ref - .update_specific_ref(dynamic_routing_type.clone(), feature_to_enable.clone()); + *algo_type_enabled_features = feature_to_enable; + dynamic_routing_algo_ref.update_enabled_features(dynamic_routing_type, feature_to_enable); update_business_profile_active_dynamic_algorithm_ref( db, &state.into(), @@ -1242,6 +1553,13 @@ pub async fn default_specific_dynamic_routing_setup( algorithm_for: common_enums::TransactionType::Payment, } } + + routing_types::DynamicRoutingType::ContractBasedRouting => { + return Err((errors::ApiErrorResponse::InvalidRequestData { + message: "Contract routing cannot be set as default".to_string(), + }) + .into()) + } }; let record = db @@ -1273,7 +1591,8 @@ pub async fn default_specific_dynamic_routing_setup( Ok(ApplicationResponse::Json(new_record)) } -pub struct SuccessBasedRoutingConfigParamsInterpolator { +#[derive(Debug, Clone)] +pub struct DynamicRoutingConfigParamsInterpolator { pub payment_method: Option, pub payment_method_type: Option, pub authentication_type: Option, @@ -1283,7 +1602,7 @@ pub struct SuccessBasedRoutingConfigParamsInterpolator { pub card_bin: Option, } -impl SuccessBasedRoutingConfigParamsInterpolator { +impl DynamicRoutingConfigParamsInterpolator { pub fn new( payment_method: Option, payment_method_type: Option, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 7f7ea767108..c2363500c04 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1870,6 +1870,10 @@ impl Profile { }), )), ) + .service( + web::resource("/set_volume_split") + .route(web::post().to(routing::set_dynamic_routing_volume_split)), + ) .service( web::scope("/elimination").service( web::resource("/toggle") @@ -1877,8 +1881,17 @@ impl Profile { ), ) .service( - web::resource("/set_volume_split") - .route(web::post().to(routing::set_dynamic_routing_volume_split)), + web::scope("/contracts") + .service(web::resource("/toggle").route( + web::post().to(routing::contract_based_routing_setup_config), + )) + .service(web::resource("/config/{algorithm_id}").route( + web::patch().to(|state, req, path, payload| { + routing::contract_based_routing_update_configs( + state, req, path, payload, + ) + }), + )), ), ); } diff --git a/crates/router/src/routes/metrics/bg_metrics_collector.rs b/crates/router/src/routes/metrics/bg_metrics_collector.rs index f3ba7076e53..c0f4062e15d 100644 --- a/crates/router/src/routes/metrics/bg_metrics_collector.rs +++ b/crates/router/src/routes/metrics/bg_metrics_collector.rs @@ -14,6 +14,9 @@ pub fn spawn_metrics_collector(metrics_collection_interval_in_secs: Option) &cache::PM_FILTERS_CGRAPH_CACHE, &cache::DECISION_MANAGER_CACHE, &cache::SURCHARGE_CACHE, + &cache::SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE, + &cache::CONTRACT_BASED_DYNAMIC_ALGORITHM_CACHE, + &cache::ELIMINATION_BASED_DYNAMIC_ALGORITHM_CACHE, ]; tokio::spawn(async move { diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index e7227e1f752..ac0f997a278 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -1130,7 +1130,7 @@ pub async fn toggle_success_based_routing( pub async fn success_based_routing_update_configs( state: web::Data, req: HttpRequest, - path: web::Path, + path: web::Path, json_payload: web::Json, ) -> impl Responder { let flow = Flow::UpdateDynamicRoutingConfigs; @@ -1165,6 +1165,100 @@ pub async fn success_based_routing_update_configs( )) .await } + +#[cfg(all(feature = "olap", feature = "v1", feature = "dynamic_routing"))] +#[instrument(skip_all)] +pub async fn contract_based_routing_setup_config( + state: web::Data, + req: HttpRequest, + path: web::Path, + query: web::Query, + json_payload: Option>, +) -> impl Responder { + let flow = Flow::ToggleDynamicRouting; + let routing_payload_wrapper = routing_types::ContractBasedRoutingSetupPayloadWrapper { + config: json_payload.map(|json| json.into_inner()), + profile_id: path.into_inner().profile_id, + features_to_enable: query.into_inner().enable, + }; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + routing_payload_wrapper.clone(), + |state, + auth: auth::AuthenticationData, + wrapper: routing_types::ContractBasedRoutingSetupPayloadWrapper, + _| async move { + Box::pin(routing::contract_based_dynamic_routing_setup( + state, + auth.key_store, + auth.merchant_account, + wrapper.profile_id, + wrapper.features_to_enable, + wrapper.config, + )) + .await + }, + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuthProfileFromRoute { + profile_id: routing_payload_wrapper.profile_id, + required_permission: Permission::ProfileRoutingWrite, + }, + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(all(feature = "olap", feature = "v1", feature = "dynamic_routing"))] +#[instrument(skip_all)] +pub async fn contract_based_routing_update_configs( + state: web::Data, + req: HttpRequest, + path: web::Path, + json_payload: web::Json, +) -> impl Responder { + let flow = Flow::UpdateDynamicRoutingConfigs; + let routing_payload_wrapper = routing_types::ContractBasedRoutingPayloadWrapper { + updated_config: json_payload.into_inner(), + algorithm_id: path.algorithm_id.clone(), + profile_id: path.profile_id.clone(), + }; + Box::pin(oss_api::server_wrap( + flow, + state, + &req, + routing_payload_wrapper.clone(), + |state, + auth: auth::AuthenticationData, + wrapper: routing_types::ContractBasedRoutingPayloadWrapper, + _| async { + Box::pin(routing::contract_based_routing_update_configs( + state, + wrapper.updated_config, + auth.merchant_account, + auth.key_store, + wrapper.algorithm_id, + wrapper.profile_id, + )) + .await + }, + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth), + &auth::JWTAuthProfileFromRoute { + profile_id: routing_payload_wrapper.profile_id, + required_permission: Permission::ProfileRoutingWrite, + }, + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(all(feature = "olap", feature = "v1", feature = "dynamic_routing"))] #[instrument(skip_all)] pub async fn toggle_elimination_routing( diff --git a/crates/storage_impl/src/redis/cache.rs b/crates/storage_impl/src/redis/cache.rs index 8302b5bf933..fb752e64b09 100644 --- a/crates/storage_impl/src/redis/cache.rs +++ b/crates/storage_impl/src/redis/cache.rs @@ -92,6 +92,16 @@ pub static ELIMINATION_BASED_DYNAMIC_ALGORITHM_CACHE: Lazy = Lazy::new(|| ) }); +/// Contract Routing based Dynamic Algorithm Cache +pub static CONTRACT_BASED_DYNAMIC_ALGORITHM_CACHE: Lazy = Lazy::new(|| { + Cache::new( + "CONTRACT_BASED_DYNAMIC_ALGORITHM_CACHE", + CACHE_TTL, + CACHE_TTI, + Some(MAX_CAPACITY), + ) +}); + /// Trait which defines the behaviour of types that's gonna be stored in Cache pub trait Cacheable: Any + Send + Sync + DynClone { fn as_any(&self) -> &dyn Any; @@ -113,6 +123,7 @@ pub enum CacheKind<'a> { CGraph(Cow<'a, str>), SuccessBasedDynamicRoutingCache(Cow<'a, str>), EliminationBasedDynamicRoutingCache(Cow<'a, str>), + ContractBasedDynamicRoutingCache(Cow<'a, str>), PmFiltersCGraph(Cow<'a, str>), All(Cow<'a, str>), } @@ -128,6 +139,7 @@ impl CacheKind<'_> { | CacheKind::CGraph(key) | CacheKind::SuccessBasedDynamicRoutingCache(key) | CacheKind::EliminationBasedDynamicRoutingCache(key) + | CacheKind::ContractBasedDynamicRoutingCache(key) | CacheKind::PmFiltersCGraph(key) | CacheKind::All(key) => key, } diff --git a/crates/storage_impl/src/redis/pub_sub.rs b/crates/storage_impl/src/redis/pub_sub.rs index 373ac370e2f..28b76148765 100644 --- a/crates/storage_impl/src/redis/pub_sub.rs +++ b/crates/storage_impl/src/redis/pub_sub.rs @@ -6,8 +6,9 @@ use router_env::{logger, tracing::Instrument}; use crate::redis::cache::{ CacheKey, CacheKind, CacheRedact, ACCOUNTS_CACHE, CGRAPH_CACHE, CONFIG_CACHE, - DECISION_MANAGER_CACHE, ELIMINATION_BASED_DYNAMIC_ALGORITHM_CACHE, PM_FILTERS_CGRAPH_CACHE, - ROUTING_CACHE, SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE, SURCHARGE_CACHE, + CONTRACT_BASED_DYNAMIC_ALGORITHM_CACHE, DECISION_MANAGER_CACHE, + ELIMINATION_BASED_DYNAMIC_ALGORITHM_CACHE, PM_FILTERS_CGRAPH_CACHE, ROUTING_CACHE, + SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE, SURCHARGE_CACHE, }; #[async_trait::async_trait] @@ -147,6 +148,15 @@ impl PubSubInterface for std::sync::Arc { .await; key } + CacheKind::ContractBasedDynamicRoutingCache(key) => { + CONTRACT_BASED_DYNAMIC_ALGORITHM_CACHE + .remove(CacheKey { + key: key.to_string(), + prefix: message.tenant.clone(), + }) + .await; + key + } CacheKind::SuccessBasedDynamicRoutingCache(key) => { SUCCESS_BASED_DYNAMIC_ALGORITHM_CACHE .remove(CacheKey { @@ -220,6 +230,12 @@ impl PubSubInterface for std::sync::Arc { prefix: message.tenant.clone(), }) .await; + CONTRACT_BASED_DYNAMIC_ALGORITHM_CACHE + .remove(CacheKey { + key: key.to_string(), + prefix: message.tenant.clone(), + }) + .await; ROUTING_CACHE .remove(CacheKey { key: key.to_string(), diff --git a/proto/contract_routing.proto b/proto/contract_routing.proto new file mode 100644 index 00000000000..6b842e3096e --- /dev/null +++ b/proto/contract_routing.proto @@ -0,0 +1,76 @@ +syntax = "proto3"; +package contract_routing; + +service ContractScoreCalculator { + rpc FetchContractScore (CalContractScoreRequest) returns (CalContractScoreResponse); + + rpc UpdateContract (UpdateContractRequest) returns (UpdateContractResponse); + + rpc InvalidateContract (InvalidateContractRequest) returns (InvalidateContractResponse); +} + +// API-1 types +message CalContractScoreRequest { + string id = 1; + string params = 2; + repeated string labels = 3; + CalContractScoreConfig config = 4; +} + +message CalContractScoreConfig { + repeated double constants = 1; + TimeScale time_scale = 2; +} + +message TimeScale { + enum Scale { + Day = 0; + Month = 1; + } + Scale time_scale = 1; +} + +message CalContractScoreResponse { + repeated ScoreData labels_with_score = 1; +} + +message ScoreData { + double score = 1; + string label = 2; + uint64 current_count = 3; +} + +// API-2 types +message UpdateContractRequest { + string id = 1; + string params = 2; + repeated LabelInformation labels_information = 3; +} + +message LabelInformation { + string label = 1; + uint64 target_count = 2; + uint64 target_time = 3; + uint64 current_count = 4; +} + +message UpdateContractResponse { + enum UpdationStatus { + CONTRACT_UPDATION_SUCCEEDED = 0; + CONTRACT_UPDATION_FAILED = 1; + } + UpdationStatus status = 1; +} + +// API-3 types +message InvalidateContractRequest { + string id = 1; +} + +message InvalidateContractResponse { + enum InvalidationStatus { + CONTRACT_INVALIDATION_SUCCEEDED = 0; + CONTRACT_INVALIDATION_FAILED = 1; + } + InvalidationStatus status = 1; +} \ No newline at end of file From 8ae5267b91cfb37b14df1acf5fd7dfc2570b58ce Mon Sep 17 00:00:00 2001 From: Anurag Date: Thu, 6 Feb 2025 19:16:11 +0530 Subject: [PATCH 41/46] fix(connector): handle unexpected error response from bluesnap connector (#7120) Co-authored-by: Anurag Singh Co-authored-by: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> --- .../src/connectors/bluesnap.rs | 148 ++++++++++-------- 1 file changed, 79 insertions(+), 69 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/bluesnap.rs b/crates/hyperswitch_connectors/src/connectors/bluesnap.rs index 8fac36424e2..3f319ee74f5 100644 --- a/crates/hyperswitch_connectors/src/connectors/bluesnap.rs +++ b/crates/hyperswitch_connectors/src/connectors/bluesnap.rs @@ -11,7 +11,7 @@ use common_utils::{ request::{Method, Request, RequestBuilder, RequestContent}, types::{AmountConvertor, StringMajorUnit, StringMajorUnitForConnector}, }; -use error_stack::{report, ResultExt}; +use error_stack::{report, Report, ResultExt}; use hyperswitch_domain_models::{ router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::{ @@ -55,9 +55,9 @@ use crate::{ utils::{ construct_not_supported_error_report, convert_amount, get_error_code_error_message_based_on_priority, get_header_key_value, get_http_header, - to_connector_meta_from_secret, to_currency_lower_unit, ConnectorErrorType, - ConnectorErrorTypeMapping, ForeignTryFrom, PaymentsAuthorizeRequestData, - RefundsRequestData, RouterData as _, + handle_json_response_deserialization_failure, to_connector_meta_from_secret, + to_currency_lower_unit, ConnectorErrorType, ConnectorErrorTypeMapping, ForeignTryFrom, + PaymentsAuthorizeRequestData, RefundsRequestData, RouterData as _, }, }; @@ -132,74 +132,84 @@ impl ConnectorCommon for Bluesnap { event_builder: Option<&mut ConnectorEvent>, ) -> CustomResult { logger::debug!(bluesnap_error_response=?res); - let response: bluesnap::BluesnapErrors = res - .response - .parse_struct("BluesnapErrorResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - - event_builder.map(|i| i.set_error_response_body(&response)); - router_env::logger::info!(connector_response=?response); + let response_data: Result< + bluesnap::BluesnapErrors, + Report, + > = res.response.parse_struct("BluesnapErrors"); + + match response_data { + Ok(response) => { + event_builder.map(|i| i.set_error_response_body(&response)); + router_env::logger::info!(connector_response=?response); - let response_error_message = match response { - bluesnap::BluesnapErrors::Payment(error_response) => { - let error_list = error_response.message.clone(); - let option_error_code_message = get_error_code_error_message_based_on_priority( - self.clone(), - error_list.into_iter().map(|errors| errors.into()).collect(), - ); - let reason = error_response - .message - .iter() - .map(|error| error.description.clone()) - .collect::>() - .join(" & "); - ErrorResponse { - status_code: res.status_code, - code: option_error_code_message - .clone() - .map(|error_code_message| error_code_message.error_code) - .unwrap_or(NO_ERROR_CODE.to_string()), - message: option_error_code_message - .map(|error_code_message| error_code_message.error_message) - .unwrap_or(NO_ERROR_MESSAGE.to_string()), - reason: Some(reason), - attempt_status: None, - connector_transaction_id: None, - } - } - bluesnap::BluesnapErrors::Auth(error_res) => ErrorResponse { - status_code: res.status_code, - code: error_res.error_code.clone(), - message: error_res.error_name.clone().unwrap_or(error_res.error_code), - reason: Some(error_res.error_description), - attempt_status: None, - connector_transaction_id: None, - }, - bluesnap::BluesnapErrors::General(error_response) => { - let (error_res, attempt_status) = if res.status_code == 403 - && error_response.contains(BLUESNAP_TRANSACTION_NOT_FOUND) - { - ( - format!( - "{} in bluesnap dashboard", - REQUEST_TIMEOUT_PAYMENT_NOT_FOUND - ), - Some(enums::AttemptStatus::Failure), // when bluesnap throws 403 for payment not found, we update the payment status to failure. - ) - } else { - (error_response.clone(), None) + let response_error_message = match response { + bluesnap::BluesnapErrors::Payment(error_response) => { + let error_list = error_response.message.clone(); + let option_error_code_message = + get_error_code_error_message_based_on_priority( + self.clone(), + error_list.into_iter().map(|errors| errors.into()).collect(), + ); + let reason = error_response + .message + .iter() + .map(|error| error.description.clone()) + .collect::>() + .join(" & "); + ErrorResponse { + status_code: res.status_code, + code: option_error_code_message + .clone() + .map(|error_code_message| error_code_message.error_code) + .unwrap_or(NO_ERROR_CODE.to_string()), + message: option_error_code_message + .map(|error_code_message| error_code_message.error_message) + .unwrap_or(NO_ERROR_MESSAGE.to_string()), + reason: Some(reason), + attempt_status: None, + connector_transaction_id: None, + } + } + bluesnap::BluesnapErrors::Auth(error_res) => ErrorResponse { + status_code: res.status_code, + code: error_res.error_code.clone(), + message: error_res.error_name.clone().unwrap_or(error_res.error_code), + reason: Some(error_res.error_description), + attempt_status: None, + connector_transaction_id: None, + }, + bluesnap::BluesnapErrors::General(error_response) => { + let (error_res, attempt_status) = if res.status_code == 403 + && error_response.contains(BLUESNAP_TRANSACTION_NOT_FOUND) + { + ( + format!( + "{} in bluesnap dashboard", + REQUEST_TIMEOUT_PAYMENT_NOT_FOUND + ), + Some(enums::AttemptStatus::Failure), // when bluesnap throws 403 for payment not found, we update the payment status to failure. + ) + } else { + (error_response.clone(), None) + }; + ErrorResponse { + status_code: res.status_code, + code: NO_ERROR_CODE.to_string(), + message: error_response, + reason: Some(error_res), + attempt_status, + connector_transaction_id: None, + } + } }; - ErrorResponse { - status_code: res.status_code, - code: NO_ERROR_CODE.to_string(), - message: error_response, - reason: Some(error_res), - attempt_status, - connector_transaction_id: None, - } + Ok(response_error_message) + } + Err(error_msg) => { + event_builder.map(|event| event.set_error(serde_json::json!({"error": res.response.escape_ascii().to_string(), "status_code": res.status_code}))); + router_env::logger::error!(deserialization_error =? error_msg); + handle_json_response_deserialization_failure(res, "bluesnap") } - }; - Ok(response_error_message) + } } } From 97e9270ed4458a24207ea5434d65c54fb4b6237d Mon Sep 17 00:00:00 2001 From: Amey Wale <76102448+AmeyWale@users.noreply.github.com> Date: Thu, 6 Feb 2025 19:16:20 +0530 Subject: [PATCH 42/46] refactor(customer): return redacted customer instead of error (#7122) --- crates/router/src/core/customers.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/router/src/core/customers.rs b/crates/router/src/core/customers.rs index 4f69cf41963..dabc9d37086 100644 --- a/crates/router/src/core/customers.rs +++ b/crates/router/src/core/customers.rs @@ -463,7 +463,7 @@ pub async fn retrieve_customer( let key_manager_state = &(&state).into(); let response = db - .find_customer_by_customer_id_merchant_id( + .find_customer_optional_with_redacted_customer_details_by_customer_id_merchant_id( key_manager_state, &customer_id, merchant_account.get_id(), @@ -471,7 +471,9 @@ pub async fn retrieve_customer( merchant_account.storage_scheme, ) .await - .switch()?; + .switch()? + .ok_or(errors::CustomersErrorResponse::CustomerNotFound)?; + let address = match &response.address_id { Some(address_id) => Some(api_models::payments::AddressDetails::from( db.find_address_by_address_id(key_manager_state, address_id, &key_store) From e17ffd1257adc1618ed60dee81ea1e7df84cb3d5 Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Thu, 6 Feb 2025 19:16:36 +0530 Subject: [PATCH 43/46] feat(core): Add support for v2 payments get intent using merchant reference id (#7123) Co-authored-by: Chikke Srujan Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../diesel_models/src/query/payment_intent.rs | 17 +++++ .../src/payments/payment_intent.rs | 9 +++ crates/router/src/core/payments.rs | 64 +++++++++++++++++++ crates/router/src/db/kafka_store.rs | 23 +++++++ crates/router/src/routes/app.rs | 6 ++ crates/router/src/routes/lock_utils.rs | 3 +- crates/router/src/routes/payments.rs | 41 ++++++++++++ crates/router_env/src/logger/types.rs | 2 + .../src/mock_db/payment_intent.rs | 22 +++++++ .../src/payments/payment_intent.rs | 59 +++++++++++++++++ 10 files changed, 245 insertions(+), 1 deletion(-) diff --git a/crates/diesel_models/src/query/payment_intent.rs b/crates/diesel_models/src/query/payment_intent.rs index 4f4099eca01..3778de3688e 100644 --- a/crates/diesel_models/src/query/payment_intent.rs +++ b/crates/diesel_models/src/query/payment_intent.rs @@ -87,6 +87,23 @@ impl PaymentIntent { .await } + // This query should be removed in the future because direct queries to the intent table without an intent ID are not allowed. + // In an active-active setup, a lookup table should be implemented, and the merchant reference ID will serve as the idempotency key. + #[cfg(feature = "v2")] + pub async fn find_by_merchant_reference_id_profile_id( + conn: &PgPooledConn, + merchant_reference_id: &common_utils::id_type::PaymentReferenceId, + profile_id: &common_utils::id_type::ProfileId, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::profile_id + .eq(profile_id.to_owned()) + .and(dsl::merchant_reference_id.eq(merchant_reference_id.to_owned())), + ) + .await + } + #[cfg(feature = "v1")] pub async fn find_by_payment_id_merchant_id( conn: &PgPooledConn, diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 0afefbab9a4..2406e5f4b1b 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -63,6 +63,15 @@ pub trait PaymentIntentInterface { merchant_key_store: &MerchantKeyStore, storage_scheme: common_enums::MerchantStorageScheme, ) -> error_stack::Result; + #[cfg(feature = "v2")] + async fn find_payment_intent_by_merchant_reference_id_profile_id( + &self, + state: &KeyManagerState, + merchant_reference_id: &id_type::PaymentReferenceId, + profile_id: &id_type::ProfileId, + merchant_key_store: &MerchantKeyStore, + storage_scheme: &common_enums::MerchantStorageScheme, + ) -> error_stack::Result; #[cfg(feature = "v2")] async fn find_payment_intent_by_id( diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index f86072fdcc2..592dda60177 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1525,6 +1525,70 @@ where ) } +#[cfg(feature = "v2")] +#[allow(clippy::too_many_arguments)] +pub async fn payments_get_intent_using_merchant_reference( + state: SessionState, + merchant_account: domain::MerchantAccount, + profile: domain::Profile, + key_store: domain::MerchantKeyStore, + req_state: ReqState, + merchant_reference_id: &id_type::PaymentReferenceId, + header_payload: HeaderPayload, + platform_merchant_account: Option, +) -> RouterResponse { + let db = state.store.as_ref(); + let storage_scheme = merchant_account.storage_scheme; + let key_manager_state = &(&state).into(); + let payment_intent = db + .find_payment_intent_by_merchant_reference_id_profile_id( + key_manager_state, + merchant_reference_id, + profile.get_id(), + &key_store, + &storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + let (payment_data, _req, customer) = Box::pin(payments_intent_operation_core::< + api::PaymentGetIntent, + _, + _, + PaymentIntentData, + >( + &state, + req_state, + merchant_account.clone(), + profile.clone(), + key_store.clone(), + operations::PaymentGetIntent, + api_models::payments::PaymentsGetIntentRequest { + id: payment_intent.get_id().clone(), + }, + payment_intent.get_id().clone(), + header_payload.clone(), + platform_merchant_account, + )) + .await?; + + transformers::ToResponse::< + api::PaymentGetIntent, + PaymentIntentData, + operations::PaymentGetIntent, + >::generate_response( + payment_data, + customer, + &state.base_url, + operations::PaymentGetIntent, + &state.conf.connector_request_reference_id_config, + None, + None, + header_payload.x_hs_latency, + &merchant_account, + ) +} + #[cfg(feature = "v2")] #[allow(clippy::too_many_arguments)] pub async fn payments_core( diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 7d4f16ee893..b8ecb2eea83 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1904,6 +1904,29 @@ impl PaymentIntentInterface for KafkaStore { ) .await } + + #[cfg(feature = "v2")] + async fn find_payment_intent_by_merchant_reference_id_profile_id( + &self, + state: &KeyManagerState, + merchant_reference_id: &id_type::PaymentReferenceId, + profile_id: &id_type::ProfileId, + merchant_key_store: &domain::MerchantKeyStore, + storage_scheme: &MerchantStorageScheme, + ) -> error_stack::Result< + hyperswitch_domain_models::payments::PaymentIntent, + errors::DataStorageError, + > { + self.diesel_store + .find_payment_intent_by_merchant_reference_id_profile_id( + state, + merchant_reference_id, + profile_id, + merchant_key_store, + storage_scheme, + ) + .await + } } #[async_trait::async_trait] diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index c2363500c04..e4da0119fdd 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -561,6 +561,12 @@ impl Payments { .route(web::post().to(payments::payments_create_intent)), ); + route = + route + .service(web::resource("/ref/{merchant_reference_id}").route( + web::get().to(payments::payment_get_intent_using_merchant_reference_id), + )); + route = route.service( web::scope("/{payment_id}") .service( diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 895ba4b8aff..b09f92364bc 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -147,7 +147,8 @@ impl From for ApiIdentifier { | Flow::PaymentsPostSessionTokens | Flow::PaymentsUpdateIntent | Flow::PaymentsCreateAndConfirmIntent - | Flow::PaymentStartRedirection => Self::Payments, + | Flow::PaymentStartRedirection + | Flow::PaymentsRetrieveUsingMerchantReferenceId => Self::Payments, Flow::PayoutsCreate | Flow::PayoutsRetrieve diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index d461599b6f0..a088f04b095 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -2470,6 +2470,47 @@ pub async fn payment_status( .await } +#[cfg(feature = "v2")] +#[instrument(skip(state, req), fields(flow, payment_id))] +pub async fn payment_get_intent_using_merchant_reference_id( + state: web::Data, + req: actix_web::HttpRequest, + path: web::Path, +) -> impl Responder { + let flow = Flow::PaymentsRetrieveUsingMerchantReferenceId; + let header_payload = match HeaderPayload::foreign_try_from(req.headers()) { + Ok(headers) => headers, + Err(err) => { + return api::log_and_return_error_response(err); + } + }; + + let merchant_reference_id = path.into_inner(); + + Box::pin(api::server_wrap( + flow, + state, + &req, + (), + |state, auth: auth::AuthenticationData, _, req_state| async { + Box::pin(payments::payments_get_intent_using_merchant_reference( + state, + auth.merchant_account, + auth.profile, + auth.key_store, + req_state, + &merchant_reference_id, + header_payload.clone(), + auth.platform_merchant_account, + )) + .await + }, + &auth::HeaderAuth(auth::ApiKeyAuth), + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(feature = "v2")] #[instrument(skip_all, fields(flow = ?Flow::PaymentsRedirect, payment_id))] pub async fn payments_finish_redirection( diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index a267d24af1d..acd6e59431c 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -144,6 +144,8 @@ pub enum Flow { PaymentsRetrieve, /// Payments Retrieve force sync flow. PaymentsRetrieveForceSync, + /// Payments Retrieve using merchant reference id + PaymentsRetrieveUsingMerchantReferenceId, /// Payments update flow. PaymentsUpdate, /// Payments confirm flow. diff --git a/crates/storage_impl/src/mock_db/payment_intent.rs b/crates/storage_impl/src/mock_db/payment_intent.rs index 3a564d958e9..0766d397bb7 100644 --- a/crates/storage_impl/src/mock_db/payment_intent.rs +++ b/crates/storage_impl/src/mock_db/payment_intent.rs @@ -185,4 +185,26 @@ impl PaymentIntentInterface for MockDb { Ok(payment_intent.clone()) } + #[cfg(feature = "v2")] + async fn find_payment_intent_by_merchant_reference_id_profile_id( + &self, + _state: &KeyManagerState, + merchant_reference_id: &common_utils::id_type::PaymentReferenceId, + profile_id: &common_utils::id_type::ProfileId, + _merchant_key_store: &MerchantKeyStore, + _storage_scheme: &common_enums::MerchantStorageScheme, + ) -> error_stack::Result { + let payment_intents = self.payment_intents.lock().await; + let payment_intent = payment_intents + .iter() + .find(|payment_intent| { + payment_intent.merchant_reference_id.as_ref() == Some(merchant_reference_id) + && payment_intent.profile_id.eq(profile_id) + }) + .ok_or(StorageError::ValueNotFound( + "PaymentIntent not found".to_string(), + ))?; + + Ok(payment_intent.clone()) + } } diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index 786cbe75a43..5290c2bfca4 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -459,6 +459,32 @@ impl PaymentIntentInterface for KVRouterStore { ) .await } + #[cfg(feature = "v2")] + async fn find_payment_intent_by_merchant_reference_id_profile_id( + &self, + state: &KeyManagerState, + merchant_reference_id: &common_utils::id_type::PaymentReferenceId, + profile_id: &common_utils::id_type::ProfileId, + merchant_key_store: &MerchantKeyStore, + storage_scheme: &MerchantStorageScheme, + ) -> error_stack::Result { + match storage_scheme { + MerchantStorageScheme::PostgresOnly => { + self.router_store + .find_payment_intent_by_merchant_reference_id_profile_id( + state, + merchant_reference_id, + profile_id, + merchant_key_store, + storage_scheme, + ) + .await + } + MerchantStorageScheme::RedisKv => { + todo!() + } + } + } } #[async_trait::async_trait] @@ -622,6 +648,39 @@ impl PaymentIntentInterface for crate::RouterStore { .change_context(StorageError::DecryptionError) } + #[cfg(feature = "v2")] + #[instrument(skip_all)] + async fn find_payment_intent_by_merchant_reference_id_profile_id( + &self, + state: &KeyManagerState, + merchant_reference_id: &common_utils::id_type::PaymentReferenceId, + profile_id: &common_utils::id_type::ProfileId, + merchant_key_store: &MerchantKeyStore, + _storage_scheme: &MerchantStorageScheme, + ) -> error_stack::Result { + let conn = pg_connection_read(self).await?; + let diesel_payment_intent = DieselPaymentIntent::find_by_merchant_reference_id_profile_id( + &conn, + merchant_reference_id, + profile_id, + ) + .await + .map_err(|er| { + let new_err = diesel_error_to_data_error(*er.current_context()); + er.change_context(new_err) + })?; + let merchant_id = diesel_payment_intent.merchant_id.clone(); + + PaymentIntent::convert_back( + state, + diesel_payment_intent, + merchant_key_store.key.get_inner(), + merchant_id.to_owned().into(), + ) + .await + .change_context(StorageError::DecryptionError) + } + #[cfg(all(feature = "v1", feature = "olap"))] #[instrument(skip_all)] async fn filter_payment_intent_by_constraints( From 9b1b2455643d7a5744a4084fc1916c84634cb48d Mon Sep 17 00:00:00 2001 From: Uzair Khan Date: Thu, 6 Feb 2025 19:17:02 +0530 Subject: [PATCH 44/46] fix(dashboard_metadata): mask `poc_email` and `data_value` for DashboardMetadata (#7130) --- .../api_models/src/user/dashboard_metadata.rs | 2 +- .../src/user/dashboard_metadata.rs | 9 +++--- .../src/core/user/dashboard_metadata.rs | 9 ++++++ crates/router/src/services/email/types.rs | 2 +- .../src/utils/user/dashboard_metadata.rs | 29 ++++++++++--------- 5 files changed, 31 insertions(+), 20 deletions(-) diff --git a/crates/api_models/src/user/dashboard_metadata.rs b/crates/api_models/src/user/dashboard_metadata.rs index 1d606ef63e8..f15029a7a49 100644 --- a/crates/api_models/src/user/dashboard_metadata.rs +++ b/crates/api_models/src/user/dashboard_metadata.rs @@ -94,7 +94,7 @@ pub struct ProdIntent { pub business_label: Option, pub business_location: Option, pub display_name: Option, - pub poc_email: Option, + pub poc_email: Option>, pub business_type: Option, pub business_identifier: Option, pub business_website: Option, diff --git a/crates/diesel_models/src/user/dashboard_metadata.rs b/crates/diesel_models/src/user/dashboard_metadata.rs index 291b4d2a3e6..004fc86693b 100644 --- a/crates/diesel_models/src/user/dashboard_metadata.rs +++ b/crates/diesel_models/src/user/dashboard_metadata.rs @@ -1,5 +1,6 @@ use common_utils::id_type; use diesel::{query_builder::AsChangeset, Identifiable, Insertable, Queryable, Selectable}; +use masking::Secret; use time::PrimitiveDateTime; use crate::{enums, schema::dashboard_metadata}; @@ -12,7 +13,7 @@ pub struct DashboardMetadata { pub merchant_id: id_type::MerchantId, pub org_id: id_type::OrganizationId, pub data_key: enums::DashboardMetadata, - pub data_value: serde_json::Value, + pub data_value: Secret, pub created_by: String, pub created_at: PrimitiveDateTime, pub last_modified_by: String, @@ -28,7 +29,7 @@ pub struct DashboardMetadataNew { pub merchant_id: id_type::MerchantId, pub org_id: id_type::OrganizationId, pub data_key: enums::DashboardMetadata, - pub data_value: serde_json::Value, + pub data_value: Secret, pub created_by: String, pub created_at: PrimitiveDateTime, pub last_modified_by: String, @@ -41,7 +42,7 @@ pub struct DashboardMetadataNew { #[diesel(table_name = dashboard_metadata)] pub struct DashboardMetadataUpdateInternal { pub data_key: enums::DashboardMetadata, - pub data_value: serde_json::Value, + pub data_value: Secret, pub last_modified_by: String, pub last_modified_at: PrimitiveDateTime, } @@ -50,7 +51,7 @@ pub struct DashboardMetadataUpdateInternal { pub enum DashboardMetadataUpdate { UpdateData { data_key: enums::DashboardMetadata, - data_value: serde_json::Value, + data_value: Secret, last_modified_by: String, }, } diff --git a/crates/router/src/core/user/dashboard_metadata.rs b/crates/router/src/core/user/dashboard_metadata.rs index 689762c1f43..2ea2ba1d3d8 100644 --- a/crates/router/src/core/user/dashboard_metadata.rs +++ b/crates/router/src/core/user/dashboard_metadata.rs @@ -1,12 +1,16 @@ +use std::str::FromStr; + use api_models::user::dashboard_metadata::{self as api, GetMultipleMetaDataPayload}; #[cfg(feature = "email")] use common_enums::EntityType; +use common_utils::pii; use diesel_models::{ enums::DashboardMetadata as DBEnum, user::dashboard_metadata::DashboardMetadata, }; use error_stack::{report, ResultExt}; #[cfg(feature = "email")] use masking::ExposeInterface; +use masking::PeekInterface; #[cfg(feature = "email")] use router_env::logger; @@ -447,6 +451,11 @@ async fn insert_metadata( metadata } types::MetaData::ProdIntent(data) => { + if let Some(poc_email) = &data.poc_email { + let inner_poc_email = poc_email.peek().as_str(); + pii::Email::from_str(inner_poc_email) + .change_context(UserErrors::EmailParsingError)?; + } let mut metadata = utils::insert_user_scoped_metadata_to_db( state, user.user_id.clone(), diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index 476cd1292f6..1b88f648678 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -454,7 +454,7 @@ impl BizEmailProd { settings: state.conf.clone(), subject: consts::user::EMAIL_SUBJECT_NEW_PROD_INTENT, user_name: data.poc_name.unwrap_or_default().into(), - poc_email: data.poc_email.unwrap_or_default().into(), + poc_email: data.poc_email.unwrap_or_default(), legal_business_name: data.legal_business_name.unwrap_or_default(), business_location: data .business_location diff --git a/crates/router/src/utils/user/dashboard_metadata.rs b/crates/router/src/utils/user/dashboard_metadata.rs index b5e16290fca..f5836f25fa7 100644 --- a/crates/router/src/utils/user/dashboard_metadata.rs +++ b/crates/router/src/utils/user/dashboard_metadata.rs @@ -10,7 +10,7 @@ use diesel_models::{ user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew, DashboardMetadataUpdate}, }; use error_stack::{report, ResultExt}; -use masking::Secret; +use masking::{ExposeInterface, PeekInterface, Secret}; use router_env::logger; use crate::{ @@ -37,7 +37,7 @@ pub async fn insert_merchant_scoped_metadata_to_db( merchant_id, org_id, data_key: metadata_key, - data_value, + data_value: Secret::from(data_value), created_by: user_id.clone(), created_at: now, last_modified_by: user_id, @@ -70,7 +70,7 @@ pub async fn insert_user_scoped_metadata_to_db( merchant_id, org_id, data_key: metadata_key, - data_value, + data_value: Secret::from(data_value), created_by: user_id.clone(), created_at: now, last_modified_by: user_id, @@ -143,7 +143,7 @@ pub async fn update_merchant_scoped_metadata( metadata_key, DashboardMetadataUpdate::UpdateData { data_key: metadata_key, - data_value, + data_value: Secret::from(data_value), last_modified_by: user_id, }, ) @@ -171,7 +171,7 @@ pub async fn update_user_scoped_metadata( metadata_key, DashboardMetadataUpdate::UpdateData { data_key: metadata_key, - data_value, + data_value: Secret::from(data_value), last_modified_by: user_id, }, ) @@ -183,7 +183,7 @@ pub fn deserialize_to_response(data: Option<&DashboardMetadata>) -> UserResul where T: serde::de::DeserializeOwned, { - data.map(|metadata| serde_json::from_value(metadata.data_value.clone())) + data.map(|metadata| serde_json::from_value(metadata.data_value.clone().expose())) .transpose() .change_context(UserErrors::InternalServerError) .attach_printable("Error Serializing Metadata from DB") @@ -278,17 +278,18 @@ pub fn parse_string_to_enums(query: String) -> UserResult, value_to_be_checked: &str) -> bool { - value - .as_ref() - .is_some_and(|mail| !mail.contains(value_to_be_checked)) +fn not_contains_string(value: Option<&str>, value_to_be_checked: &str) -> bool { + value.is_some_and(|mail| !mail.contains(value_to_be_checked)) } pub fn is_prod_email_required(data: &ProdIntent, user_email: String) -> bool { - let poc_email_check = not_contains_string(&data.poc_email, "juspay"); - let business_website_check = not_contains_string(&data.business_website, "juspay") - && not_contains_string(&data.business_website, "hyperswitch"); - let user_email_check = not_contains_string(&Some(user_email), "juspay"); + let poc_email_check = not_contains_string( + data.poc_email.as_ref().map(|email| email.peek().as_str()), + "juspay", + ); + let business_website_check = not_contains_string(data.business_website.as_deref(), "juspay") + && not_contains_string(data.business_website.as_deref(), "hyperswitch"); + let user_email_check = not_contains_string(Some(&user_email), "juspay"); if (poc_email_check && business_website_check && user_email_check).not() { logger::info!(prod_intent_email = poc_email_check); From f2117542a7dda4dbfa768fdb24229c113e25c93e Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Thu, 6 Feb 2025 19:17:11 +0530 Subject: [PATCH 45/46] feat(router): add `organization_id` in authentication table and add it in authentication events (#7168) --- crates/analytics/docs/clickhouse/scripts/authentications.sql | 4 ++++ crates/diesel_models/src/authentication.rs | 2 ++ crates/diesel_models/src/schema.rs | 2 ++ crates/diesel_models/src/schema_v2.rs | 2 ++ crates/router/src/core/authentication.rs | 3 +++ crates/router/src/core/authentication/utils.rs | 3 +++ crates/router/src/core/payments/operations/payment_confirm.rs | 3 +++ crates/router/src/core/unified_authentication_service.rs | 2 ++ crates/router/src/db/authentication.rs | 1 + crates/router/src/services/kafka/authentication.rs | 2 ++ crates/router/src/services/kafka/authentication_event.rs | 2 ++ .../down.sql | 3 +++ .../up.sql | 3 +++ 13 files changed, 32 insertions(+) create mode 100644 migrations/2025-01-30-111507_add_organization_id_in_authentication/down.sql create mode 100644 migrations/2025-01-30-111507_add_organization_id_in_authentication/up.sql diff --git a/crates/analytics/docs/clickhouse/scripts/authentications.sql b/crates/analytics/docs/clickhouse/scripts/authentications.sql index 0d540e64942..7fb00995e94 100644 --- a/crates/analytics/docs/clickhouse/scripts/authentications.sql +++ b/crates/analytics/docs/clickhouse/scripts/authentications.sql @@ -35,6 +35,7 @@ CREATE TABLE authentication_queue ( `ds_trans_id` Nullable(String), `directory_server_id` Nullable(String), `acquirer_country_code` Nullable(String), + `organization_id` String, `sign_flag` Int8 ) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', kafka_topic_list = 'hyperswitch-authentication-events', @@ -80,6 +81,7 @@ CREATE TABLE authentications ( `ds_trans_id` Nullable(String), `directory_server_id` Nullable(String), `acquirer_country_code` Nullable(String), + `organization_id` String, `sign_flag` Int8, INDEX authenticationConnectorIndex authentication_connector TYPE bloom_filter GRANULARITY 1, INDEX transStatusIndex trans_status TYPE bloom_filter GRANULARITY 1, @@ -127,6 +129,7 @@ CREATE MATERIALIZED VIEW authentication_mv TO authentications ( `ds_trans_id` Nullable(String), `directory_server_id` Nullable(String), `acquirer_country_code` Nullable(String), + `organization_id` String, `sign_flag` Int8 ) AS SELECT @@ -167,6 +170,7 @@ SELECT ds_trans_id, directory_server_id, acquirer_country_code, + organization_id, sign_flag FROM authentication_queue diff --git a/crates/diesel_models/src/authentication.rs b/crates/diesel_models/src/authentication.rs index c79892d27bf..3e0039f4ac5 100644 --- a/crates/diesel_models/src/authentication.rs +++ b/crates/diesel_models/src/authentication.rs @@ -48,6 +48,7 @@ pub struct Authentication { pub directory_server_id: Option, pub acquirer_country_code: Option, pub service_details: Option, + pub organization_id: common_utils::id_type::OrganizationId, } impl Authentication { @@ -96,6 +97,7 @@ pub struct AuthenticationNew { pub directory_server_id: Option, pub acquirer_country_code: Option, pub service_details: Option, + pub organization_id: common_utils::id_type::OrganizationId, } #[derive(Debug)] diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index e748e5beca2..170eb576e34 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -121,6 +121,8 @@ diesel::table! { #[max_length = 64] acquirer_country_code -> Nullable, service_details -> Nullable, + #[max_length = 32] + organization_id -> Varchar, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 5bd3195f43d..19456a5c65b 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -122,6 +122,8 @@ diesel::table! { #[max_length = 64] acquirer_country_code -> Nullable, service_details -> Nullable, + #[max_length = 32] + organization_id -> Varchar, } } diff --git a/crates/router/src/core/authentication.rs b/crates/router/src/core/authentication.rs index bedd7787a73..e6c27748170 100644 --- a/crates/router/src/core/authentication.rs +++ b/crates/router/src/core/authentication.rs @@ -124,6 +124,7 @@ pub async fn perform_post_authentication( } } +#[allow(clippy::too_many_arguments)] pub async fn perform_pre_authentication( state: &SessionState, key_store: &domain::MerchantKeyStore, @@ -132,6 +133,7 @@ pub async fn perform_pre_authentication( business_profile: &domain::Profile, acquirer_details: Option, payment_id: Option, + organization_id: common_utils::id_type::OrganizationId, ) -> CustomResult { let (authentication_connector, three_ds_connector_account) = utils::get_authentication_connector_data(state, key_store, business_profile).await?; @@ -147,6 +149,7 @@ pub async fn perform_pre_authentication( .get_mca_id() .ok_or(ApiErrorResponse::InternalServerError) .attach_printable("Error while finding mca_id from merchant_connector_account")?, + organization_id, ) .await?; diff --git a/crates/router/src/core/authentication/utils.rs b/crates/router/src/core/authentication/utils.rs index 21c2ae20e2c..1d742f939ec 100644 --- a/crates/router/src/core/authentication/utils.rs +++ b/crates/router/src/core/authentication/utils.rs @@ -174,6 +174,7 @@ impl ForeignFrom for common_enums::AttemptSt } } +#[allow(clippy::too_many_arguments)] pub async fn create_new_authentication( state: &SessionState, merchant_id: common_utils::id_type::MerchantId, @@ -182,6 +183,7 @@ pub async fn create_new_authentication( profile_id: common_utils::id_type::ProfileId, payment_id: Option, merchant_connector_id: common_utils::id_type::MerchantConnectorAccountId, + organization_id: common_utils::id_type::OrganizationId, ) -> RouterResult { let authentication_id = common_utils::generate_id_with_default_len(consts::AUTHENTICATION_ID_PREFIX); @@ -220,6 +222,7 @@ pub async fn create_new_authentication( directory_server_id: None, acquirer_country_code: None, service_details: None, + organization_id, }; state .store diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 5f3b515c96e..64191f77c6d 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -983,6 +983,7 @@ impl Domain> for business_profile, Some(acquirer_details), Some(payment_data.payment_attempt.payment_id.clone()), + payment_data.payment_attempt.organization_id.clone(), ) .await?; if authentication.is_separate_authn_required() @@ -1175,6 +1176,7 @@ impl Domain> for &authentication_id, payment_data.service_details.clone(), authentication_status, + payment_data.payment_attempt.organization_id.clone(), ) .await?; }, @@ -1197,6 +1199,7 @@ impl Domain> for .get_mca_id() .ok_or(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error while finding mca_id from merchant_connector_account")?, + payment_data.payment_attempt.organization_id.clone(), ) .await?; diff --git a/crates/router/src/core/unified_authentication_service.rs b/crates/router/src/core/unified_authentication_service.rs index d9c36aeaa3d..28d32f627cb 100644 --- a/crates/router/src/core/unified_authentication_service.rs +++ b/crates/router/src/core/unified_authentication_service.rs @@ -410,6 +410,7 @@ pub async fn create_new_authentication( authentication_id: &str, service_details: Option, authentication_status: common_enums::AuthenticationStatus, + organization_id: common_utils::id_type::OrganizationId, ) -> RouterResult { let service_details_value = service_details .map(serde_json::to_value) @@ -453,6 +454,7 @@ pub async fn create_new_authentication( directory_server_id: None, acquirer_country_code: None, service_details: service_details_value, + organization_id, }; state .store diff --git a/crates/router/src/db/authentication.rs b/crates/router/src/db/authentication.rs index af3da68e6ae..7c98b90134e 100644 --- a/crates/router/src/db/authentication.rs +++ b/crates/router/src/db/authentication.rs @@ -151,6 +151,7 @@ impl AuthenticationInterface for MockDb { directory_server_id: authentication.directory_server_id, acquirer_country_code: authentication.acquirer_country_code, service_details: authentication.service_details, + organization_id: authentication.organization_id, }; authentications.push(authentication.clone()); Ok(authentication) diff --git a/crates/router/src/services/kafka/authentication.rs b/crates/router/src/services/kafka/authentication.rs index 5cf2d066ee2..86652c8c76e 100644 --- a/crates/router/src/services/kafka/authentication.rs +++ b/crates/router/src/services/kafka/authentication.rs @@ -41,6 +41,7 @@ pub struct KafkaAuthentication<'a> { pub ds_trans_id: Option<&'a String>, pub directory_server_id: Option<&'a String>, pub acquirer_country_code: Option<&'a String>, + pub organization_id: &'a common_utils::id_type::OrganizationId, } impl<'a> KafkaAuthentication<'a> { @@ -82,6 +83,7 @@ impl<'a> KafkaAuthentication<'a> { ds_trans_id: authentication.ds_trans_id.as_ref(), directory_server_id: authentication.directory_server_id.as_ref(), acquirer_country_code: authentication.acquirer_country_code.as_ref(), + organization_id: &authentication.organization_id, } } } diff --git a/crates/router/src/services/kafka/authentication_event.rs b/crates/router/src/services/kafka/authentication_event.rs index 4169ff7b42a..a875b4bb492 100644 --- a/crates/router/src/services/kafka/authentication_event.rs +++ b/crates/router/src/services/kafka/authentication_event.rs @@ -42,6 +42,7 @@ pub struct KafkaAuthenticationEvent<'a> { pub ds_trans_id: Option<&'a String>, pub directory_server_id: Option<&'a String>, pub acquirer_country_code: Option<&'a String>, + pub organization_id: &'a common_utils::id_type::OrganizationId, } impl<'a> KafkaAuthenticationEvent<'a> { @@ -83,6 +84,7 @@ impl<'a> KafkaAuthenticationEvent<'a> { ds_trans_id: authentication.ds_trans_id.as_ref(), directory_server_id: authentication.directory_server_id.as_ref(), acquirer_country_code: authentication.acquirer_country_code.as_ref(), + organization_id: &authentication.organization_id, } } } diff --git a/migrations/2025-01-30-111507_add_organization_id_in_authentication/down.sql b/migrations/2025-01-30-111507_add_organization_id_in_authentication/down.sql new file mode 100644 index 00000000000..1f04d19cf25 --- /dev/null +++ b/migrations/2025-01-30-111507_add_organization_id_in_authentication/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE authentication + DROP COLUMN IF EXISTS organization_id; \ No newline at end of file diff --git a/migrations/2025-01-30-111507_add_organization_id_in_authentication/up.sql b/migrations/2025-01-30-111507_add_organization_id_in_authentication/up.sql new file mode 100644 index 00000000000..f08285b375f --- /dev/null +++ b/migrations/2025-01-30-111507_add_organization_id_in_authentication/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE authentication + ADD COLUMN IF NOT EXISTS organization_id VARCHAR(32) NOT NULL DEFAULT 'default_org'; \ No newline at end of file From 2d0ac8d46d2ecfd7287b67b646bc0b284ed838a9 Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 6 Feb 2025 19:17:56 +0530 Subject: [PATCH 46/46] chore(connectors): [fiuu] update pm_filters for apple pay and google pay (#7182) --- config/deployments/integration_test.toml | 4 +++- config/deployments/production.toml | 4 +++- config/deployments/sandbox.toml | 4 +++- config/development.toml | 2 ++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 4d0bce877e8..e8da3489b67 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -382,7 +382,9 @@ red_pagos = { country = "UY", currency = "UYU" } local_bank_transfer = { country = "CN", currency = "CNY" } [pm_filters.fiuu] -duit_now = { country ="MY", currency = "MYR" } +duit_now = { country = "MY", currency = "MYR" } +apple_pay = { country = "MY", currency = "MYR" } +google_pay = { country = "MY", currency = "MYR" } [payout_method_filters.adyenplatform] sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT,CZ,DE,HU,NO,PL,SE,GB,CH" , currency = "EUR,CZK,DKK,HUF,NOK,PLN,SEK,GBP,CHF" } diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 8964fbc2e20..7e71767ee5f 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -398,7 +398,9 @@ local_bank_transfer = { country = "CN", currency = "CNY" } [pm_filters.fiuu] -duit_now = { country ="MY", currency = "MYR" } +duit_now = { country = "MY", currency = "MYR" } +apple_pay = { country = "MY", currency = "MYR" } +google_pay = { country = "MY", currency = "MYR" } [payout_method_filters.adyenplatform] sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT,CZ,DE,HU,NO,PL,SE,GB,CH" , currency = "EUR,CZK,DKK,HUF,NOK,PLN,SEK,GBP,CHF" } diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index e98bd1e2fad..d6477a4aab3 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -400,7 +400,9 @@ local_bank_transfer = { country = "CN", currency = "CNY" } [pm_filters.fiuu] -duit_now = { country ="MY", currency = "MYR" } +duit_now = { country = "MY", currency = "MYR" } +apple_pay = { country = "MY", currency = "MYR" } +google_pay = { country = "MY", currency = "MYR" } [payout_method_filters.adyenplatform] sepa = { country = "ES,SK,AT,NL,DE,BE,FR,FI,PT,IE,EE,LT,LV,IT,CZ,DE,HU,NO,PL,SE,GB,CH" , currency = "EUR,CZK,DKK,HUF,NOK,PLN,SEK,GBP,CHF" } diff --git a/config/development.toml b/config/development.toml index f61321fda0f..660e93df12f 100644 --- a/config/development.toml +++ b/config/development.toml @@ -566,6 +566,8 @@ debit = { currency = "USD" } [pm_filters.fiuu] duit_now = { country = "MY", currency = "MYR" } +apple_pay = { country = "MY", currency = "MYR" } +google_pay = { country = "MY", currency = "MYR" } [pm_filters.inespay] sepa = { currency = "EUR" }