Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mandates): allow off-session payments using payment_method_id #4132

Merged
merged 5 commits into from
Mar 28, 2024

Conversation

Chethan-rao
Copy link
Contributor

@Chethan-rao Chethan-rao commented Mar 19, 2024

Type of Change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring
  • Dependency updates
  • Documentation
  • CI/CD

Description

When a mandate is created on connector's end, we store the connector mandate details in our payment methods table. Now in order to make recurring payment with this, we basically have to do a token based payment where set the setup_future_usage = off_session and we list the payment methods of a customer and make a payment with payment token. But we should also allow off-session payments using payment method id without having to list customer payment methods. For this, we accept off_session = true and payment method id in the request and make a recurring mandate payment.

Additional Changes

  • This PR modifies the API contract
  • This PR modifies the database schema
  • This PR modifies application configuration/environment variables

Api contract changes -
Added new field in PaymentsRequest:

"recurring_details": {
      "type": "payment_method_id/mandate_id",
      "data": "String"
},

Motivation and Context

How did you test it?

  1. mca creation -
curl --location 'https://localhost:8080/account/merchant_1710841026/connectors' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: test_admin' \
--data '{
    "connector_type": "fiz_operations",
    "business_country": "US",
    "business_label": "default",
    "connector_name": "cybersource",
    "connector_account_details": {
        "auth_type": "SignatureKey",
        "api_key": "abc",
        "api_secret": "xyz",
        "key1": "key"
    },
    "test_mode": true,
    "disabled": false,
    "payment_methods_enabled": [
        {
            "payment_method": "card",
            "payment_method_types": [
                {
                    "payment_method_type": "credit",
                    "payment_experience": null,
                    "card_networks": [
                        "Visa",
                        "Mastercard"
                    ],
                    "accepted_currencies": null,
                    "accepted_countries": null,
                    "minimum_amount": 1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                }
            ]
        }
    ],
    "connector_webhook_details": {
        "merchant_secret": "Xavior"
    }
}'
  1. Setup mandate on connector's end
curl --location 'https://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: abc' \
--data-raw '{
    "amount": 0,
    "currency": "USD",
    "confirm": true,
    "profile_id": "pro_ByhEgJFlWSahAgSqaUZl",
    "capture_method": "automatic",
    "customer_id": "mandate_customer1",
    "email": "[email protected]",
    "setup_future_usage": "off_session",
    "customer_acceptance": {
        "acceptance_type": "online"
    },
    "payment_method": "card",
    "payment_method_type": "credit",
    "payment_method_data": {
        "card": {
            "card_number": "4242424242424242",
            "card_exp_month": "03",
            "card_exp_year": "2030",
            "card_holder_name": "Test Holder",
            "card_cvc": "737"
        }
    },
    "payment_type": "setup_mandate",
    "billing": {
        "address": {
            "city": "test",
            "country": "US",
            "line1": "here",
            "line2": "there",
            "line3": "anywhere",
            "zip": "560095",
            "state": "Washington",
            "first_name": "One",
            "last_name": "Two"
        },
        "phone": {
            "number": "1234567890",
            "country_code": "+1"
        },
        "email": "[email protected]"
    },
    "authentication_type": "no_three_ds"
}'

db entry -

image

  1. Recurring payment using payment_method_id
curl --location 'https://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: abc' \
--data-raw '{
    "amount": 499,
    "currency": "USD",
    "confirm": true,
    "profile_id": "pro_ByhEgJFlWSahAgSqaUZl",
    "capture_method": "automatic",
    "customer_id": "mandate_customer1",
    "email": "[email protected]",
    "off_session": true,
    "recurring_details": {
        "type": "payment_method_id",
        "data": "pm_lmTnIO5EdCiiMgRPrV9x"
    },
    "payment_method": "card",
    "billing": {
        "address": {
            "city": "test",
            "country": "US",
            "line1": "here",
            "line2": "there",
            "line3": "anywhere",
            "zip": "560095",
            "state": "Washington",
            "first_name": "One",
            "last_name": "Two"
        },
        "phone": {
            "number": "1234567890",
            "country_code": "+1"
        },
        "email": "[email protected]"
    },
    "authentication_type": "no_three_ds"
}'

Payment succeded -
image

Also try creating mandate on hyperswitch end by providing mandate_data in setup mandate request and get the mandate_id. Do the recurring payment with mandate_id. Also try another recurring payment with below structure

    "recurring_details": {
        "type": "mandate_id",
        "data": "xyz"
    }

Checklist

  • I formatted the code cargo +nightly fmt --all
  • I addressed lints thrown by cargo clippy
  • I reviewed the submitted code
  • I added unit tests for my changes where possible
  • I added a CHANGELOG entry if applicable

@Chethan-rao Chethan-rao added S-waiting-on-review Status: This PR has been implemented and needs to be reviewed M-api-contract-changes Metadata: This PR involves API contract changes A-mandates Area: Mandate Flows labels Mar 19, 2024
@Chethan-rao Chethan-rao added this to the March 2024 milestone Mar 19, 2024
@Chethan-rao Chethan-rao self-assigned this Mar 19, 2024
@Chethan-rao Chethan-rao requested review from a team as code owners March 19, 2024 12:12
@Chethan-rao Chethan-rao force-pushed the off-session-payments-with-pmid branch from f0a52a9 to 690be75 Compare March 19, 2024 12:14
crates/router/src/core/payments/helpers.rs Outdated Show resolved Hide resolved
crates/router/src/core/payments/helpers.rs Outdated Show resolved Hide resolved
crates/router/src/core/payments/helpers.rs Outdated Show resolved Hide resolved
Comment on lines +1002 to +1003
req.recurring_details
.check_value_present("recurring_details")?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to check mandate_id also right? One of the two should be given.

Copy link
Contributor Author

@Chethan-rao Chethan-rao Mar 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

recurring_details is of type RecurringDetails in which during from conversion itself, I'm mapping the mandate_id to RecurringDetails::MandateId or else take the request.recurring_details

impl From<&PaymentsRequest> for MandateValidationFields {
    fn from(req: &PaymentsRequest) -> Self {
        let recurring_details = req
            .mandate_id
            .clone()
            .map(RecurringDetails::MandateId)
            .or(req.recurring_details.clone());

        Self {
            recurring_details,
            confirm: req.confirm,
            customer_id: req
                .customer
                .as_ref()
                .map(|customer_details| &customer_details.id)
                .or(req.customer_id.as_ref())
                .map(ToOwned::to_owned),
            mandate_data: req.mandate_data.clone(),
            setup_future_usage: req.setup_future_usage,
            off_session: req.off_session,
        }
    }
}

@@ -1949,7 +2010,8 @@ pub(crate) fn validate_payment_method_fields_present(
utils::when(
req.payment_method.is_some()
&& req.payment_method_data.is_none()
&& req.payment_token.is_none(),
&& req.payment_token.is_none()
&& req.recurring_details.is_none(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this required?

Copy link
Contributor Author

@Chethan-rao Chethan-rao Mar 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that i'm getting the payment_method from payment_method_info itself instead of relying on the request as suggested by you, we can remove this

@@ -247,6 +249,14 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
id: profile_id.to_string(),
})?;

let recurring_details = request
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we ignoring MandateId here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because in PaymentsRequest, we have both mandate_id: Option<String> and recurring_details: Option<String> where recurring_details is mapped to payment method id

@@ -2263,6 +2263,7 @@ where
pub authorizations: Vec<diesel_models::authorization::Authorization>,
pub authentication: Option<storage::Authentication>,
pub frm_metadata: Option<serde_json::Value>,
pub recurring_details: Option<String>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better if this is the enum itself. With a String, we won't know later on in the flow whether it's a recurring mandate or a recurring MIT payment.

Comment on lines +15502 to +15544
"RecurringDetails": {
"oneOf": [
{
"type": "object",
"required": [
"type",
"data"
],
"properties": {
"type": {
"type": "string",
"enum": [
"mandate_id"
]
},
"data": {
"type": "string"
}
}
},
{
"type": "object",
"required": [
"type",
"data"
],
"properties": {
"type": {
"type": "string",
"enum": [
"payment_method_id"
]
},
"data": {
"type": "string"
}
}
}
],
"discriminator": {
"propertyName": "type"
}
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"RecurringDetails": {
"oneOf": [
{
"type": "object",
"required": [
"type",
"data"
],
"properties": {
"type": {
"type": "string",
"enum": [
"mandate_id"
]
},
"data": {
"type": "string"
}
}
},
{
"type": "object",
"required": [
"type",
"data"
],
"properties": {
"type": {
"type": "string",
"enum": [
"payment_method_id"
]
},
"data": {
"type": "string"
}
}
}
],
"discriminator": {
"propertyName": "type"
}
},
"RecurringDetails": {
"type": "object",
"required": [
"type",
"data"
],
"properties": {
"type": {
"type": "string",
"enum": [
"mandate_id",
"payment_method_id"
]
},
"data": {
"type": "string"
}
},
"discriminator": {
"propertyName": "type"
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope the testing for recurring payment with mandate_id in root level and payment_token is done already

Copy link
Contributor Author

@Chethan-rao Chethan-rao Mar 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently we are providing support to pass mandate_id inside recurring_details. So it has to be one of mandate_id or payment_method_id. Later we'll deprecate the mandate_id field in root level.

I hope the testing for recurring payment with mandate_id in root level and payment_token is done already

yes

@pixincreate pixincreate added this pull request to the merge queue Mar 28, 2024
@pixincreate pixincreate removed the S-waiting-on-review Status: This PR has been implemented and needs to be reviewed label Mar 28, 2024
Merged via the queue into main with commit 7b337ac Mar 28, 2024
10 of 12 checks passed
@pixincreate pixincreate deleted the off-session-payments-with-pmid branch March 28, 2024 12:57
pixincreate added a commit that referenced this pull request Apr 1, 2024
* 'main' of github.com:juspay/hyperswitch:
  refactor(core): removed the processing status for payment_method_status (#4213)
  docs(README): remove link to outdated early access form
  build(deps): bump `error-stack` from version `0.3.1` to `0.4.1` (#4188)
  chore(version): 2024.04.01.0
  feat(mandates): allow off-session payments using `payment_method_id` (#4132)
  ci(CI-pr): determine modified crates more deterministically (#4233)
  chore(config): add billwerk base URL in deployment configs (#4243)
  feat(payment_method): API to list countries and currencies supported by a country and payment method type (#4126)
  chore(version): 2024.03.28.0
  refactor(config): allow wildcard origin for development and Docker Compose setups (#4231)
  fix(core): Amount capturable remain same for `processing` status in capture (#4229)
  fix(euclid_wasm): checkout wasm metadata issue (#4198)
  fix(trustpay): [Trustpay] Add error code mapping '800.100.100'  (#4224)
  feat(connector): [billwerk] add connector template code (#4123)
  fix(connectors): fix wallet token deserialization error  (#4133)
  fix(log): adding span metadata to `tokio` spawned futures (#4118)
  chore(version): 2024.03.27.0
  fix(connector): [CRYPTOPAY] Skip metadata serialization if none (#4205)
  fix(connector): [Trustpay] fix deserialization error for incoming webhook response for trustpay and add error code mapping '800.100.203' (#4199)
  fix(core): make eci in AuthenticationData optional (#4187)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-mandates Area: Mandate Flows M-api-contract-changes Metadata: This PR involves API contract changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow off-session payments using Payment Method ID (only with API Key auth)
4 participants