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(payouts): Add Wallet to Payouts #3502

Merged
merged 10 commits into from
Feb 7, 2024
Merged

feat(payouts): Add Wallet to Payouts #3502

merged 10 commits into from
Feb 7, 2024

Conversation

Sakilmostak
Copy link
Contributor

@Sakilmostak Sakilmostak commented Jan 30, 2024

Type of Change

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

Description

Wallet for payout is added with implementation on Paypal for adyen

Additional Changes

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

The contract changes are made in the following file:

  • hyperswitch/crates/api_models/src/payments.rs
"payment_method_data": {
        "wallet": {
            "paypal_redirect": {
                "email": "{{paypal_email_address}}"
            }
        }
    },
  • hyperswitch/crates/api_models/src/payouts.rs
"payout_method_data": {
        "wallet": {
            "paypal": {
                "email": "{{paypal_email_address}}"
            }
        }
    },

How did you test it?

Tested through Postman:

  • Create a Merchant Account (Adyen)
  • Create a Payment Connector (Adyen) with SignatureKey
{
    "connector_type": "fiz_operations",
    "connector_name": "adyen",
    "connector_account_details": {
        "auth_type": "SignatureKey",
        "api_key": "{{api_key}}",
        "key1": "{{merchant_id}}",
        "api_secret": "{{review_key}}"
    },
    "test_mode": false,
    "disabled": false,
    "payment_methods_enabled": [
        {
            "payment_method": "card",
            "payment_method_types": [
                {
                    "payment_method_type": "credit",
                    "card_networks": [
                        "Visa",
                        "Mastercard"
                    ],
                    "minimum_amount": 1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                },
                {
                    "payment_method_type": "debit",
                    "card_networks": [
                        "Visa",
                        "Mastercard"
                    ],
                    "minimum_amount": 1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                }
            ]
        },
        {
            "payment_method": "voucher",
            "payment_method_types": [
                {
                    "payment_method_type": "boleto",
                    //"payment_experience": "redirect_to_url",
                    "minimum_amount": 1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                }
            ]
        },
        {
            "payment_method": "bank_redirect",
            "payment_method_types": [
                {
                    "payment_method_type": "open_banking_uk",
                    "payment_experience": "redirect_to_url",
                    "minimum_amount": 1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                }
            ]
        },
    ],
    "metadata": {
        "city": "NY",
        "unit": "245"
    },
    "business_country": "US",
    "business_label": "default"
}
  • Create a Payout Reqeust with Wallet Data
{
    "amount": 1,
    "currency": "EUR",
    "customer_id": "payout_customer",
    "email": "payout_customer@example.com",
    "name": "John Doe",
    "phone": "999999999",
    "phone_country_code": "+65",
    "description": "Its my first payout request",
    "payout_type": "wallet",
    "payout_method_data": {
        "wallet": {
            "email": "{{paypal_email_address}}"
        }
    },
    "billing": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "NY",
            "zip": "94122",
            "country": "US",
            "first_name": "John",
            "last_name": "Doe"
        },
        "phone": {
            "number": "999999999",
            "country_code": "+91"
        }
    },
    "entity_type": "NaturalPerson",
    "recurring": false,
    "metadata": {
        "ref": "123"
    },
    "routing": {
        "type": "single",
        "data": "adyen"
    },
    "confirm": true,
    "auto_fulfill": false
}
  • Status should be requires_fulfillment in the response
Screenshot 2024-01-30 at 7 25 22 PM
  • Fulfill the Payout
{
    "payout_id": "{{payout_id}}"
}
  • Status should be success in the response
Screenshot 2024-01-30 at 7 25 39 PM

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

@Sakilmostak Sakilmostak added A-connector-integration Area: Connector integration A-core Area: Core flows C-feature Category: Feature request or enhancement M-database-changes Metadata: This PR involves database schema changes M-api-contract-changes Metadata: This PR involves API contract changes labels Jan 30, 2024
@Sakilmostak Sakilmostak self-assigned this Jan 30, 2024
@Sakilmostak Sakilmostak requested review from a team as code owners January 30, 2024 13:56
@Sakilmostak Sakilmostak requested a review from a team as a code owner January 31, 2024 08:01
.request
.connector_payout_id
.clone()
.unwrap_or("".to_string()),
Copy link
Contributor

Choose a reason for hiding this comment

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

We can throw an error here, since original_reference is a required field for fulfillment

Comment on lines 216 to 288
api_models::payouts::PayoutMethodData::Bank(bank) => {
let key = key_store.key.get_inner().peek();
let enc_data = async {
serde_json::to_value(payout_method_data.to_owned())
.into_report()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to encode payout method data")
.ok()
.map(|v| {
let secret: Secret<String> = Secret::new(v.to_string());
secret
})
.async_lift(|inner| domain_types::encrypt_optional(inner, key))
.await
}
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to encrypt payout method data")?
.map(Encryption::from)
.map(|e| e.into_inner())
.map_or(Err(errors::ApiErrorResponse::InternalServerError), |e| {
Ok(hex::encode(e.peek()))
})?;
let payload = StoreLockerReq::LockerGeneric(StoreGenericReq {
merchant_id: &merchant_account.merchant_id,
merchant_customer_id: payout_attempt.customer_id.to_owned(),
enc_data,
});
(
payload,
None,
Some(bank.to_owned()),
None,
api_enums::PaymentMethodType::foreign_from(bank.to_owned()),
)
}
api_models::payouts::PayoutMethodData::Wallet(wallet) => {
let key = key_store.key.get_inner().peek();
let enc_data = async {
serde_json::to_value(payout_method_data.to_owned())
.into_report()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to encode payout method data")
.ok()
.map(|v| {
let secret: Secret<String> = Secret::new(v.to_string());
secret
})
.async_lift(|inner| domain_types::encrypt_optional(inner, key))
.await
}
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to encrypt payout method data")?
.map(Encryption::from)
.map(|e| e.into_inner())
.map_or(Err(errors::ApiErrorResponse::InternalServerError), |e| {
Ok(hex::encode(e.peek()))
})?;
let payload = StoreLockerReq::LockerGeneric(StoreGenericReq {
merchant_id: &merchant_account.merchant_id,
merchant_customer_id: payout_attempt.customer_id.to_owned(),
enc_data,
});
(
payload,
None,
None,
Some(wallet.to_owned()),
api_enums::PaymentMethodType::foreign_from(wallet.to_owned()),
)
}
};
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
api_models::payouts::PayoutMethodData::Bank(bank) => {
let key = key_store.key.get_inner().peek();
let enc_data = async {
serde_json::to_value(payout_method_data.to_owned())
.into_report()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to encode payout method data")
.ok()
.map(|v| {
let secret: Secret<String> = Secret::new(v.to_string());
secret
})
.async_lift(|inner| domain_types::encrypt_optional(inner, key))
.await
}
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to encrypt payout method data")?
.map(Encryption::from)
.map(|e| e.into_inner())
.map_or(Err(errors::ApiErrorResponse::InternalServerError), |e| {
Ok(hex::encode(e.peek()))
})?;
let payload = StoreLockerReq::LockerGeneric(StoreGenericReq {
merchant_id: &merchant_account.merchant_id,
merchant_customer_id: payout_attempt.customer_id.to_owned(),
enc_data,
});
(
payload,
None,
Some(bank.to_owned()),
None,
api_enums::PaymentMethodType::foreign_from(bank.to_owned()),
)
}
api_models::payouts::PayoutMethodData::Wallet(wallet) => {
let key = key_store.key.get_inner().peek();
let enc_data = async {
serde_json::to_value(payout_method_data.to_owned())
.into_report()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to encode payout method data")
.ok()
.map(|v| {
let secret: Secret<String> = Secret::new(v.to_string());
secret
})
.async_lift(|inner| domain_types::encrypt_optional(inner, key))
.await
}
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to encrypt payout method data")?
.map(Encryption::from)
.map(|e| e.into_inner())
.map_or(Err(errors::ApiErrorResponse::InternalServerError), |e| {
Ok(hex::encode(e.peek()))
})?;
let payload = StoreLockerReq::LockerGeneric(StoreGenericReq {
merchant_id: &merchant_account.merchant_id,
merchant_customer_id: payout_attempt.customer_id.to_owned(),
enc_data,
});
(
payload,
None,
None,
Some(wallet.to_owned()),
api_enums::PaymentMethodType::foreign_from(wallet.to_owned()),
)
}
};
_ => {
let key = key_store.key.get_inner().peek();
let enc_data = async {
serde_json::to_value(payout_method_data.to_owned())
.into_report()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to encode payout method data")
.ok()
.map(|v| {
let secret: Secret<String> = Secret::new(v.to_string());
secret
})
.async_lift(|inner| domain_types::encrypt_optional(inner, key))
.await
}
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to encrypt payout method data")?
.map(Encryption::from)
.map(|e| e.into_inner())
.map_or(Err(errors::ApiErrorResponse::InternalServerError), |e| {
Ok(hex::encode(e.peek()))
})?;
let payload = StoreLockerReq::LockerGeneric(StoreGenericReq {
merchant_id: &merchant_account.merchant_id,
merchant_customer_id: payout_attempt.customer_id.to_owned(),
enc_data,
});
match payout_method_data {
api_models::payouts::PayoutMethodData::Bank(b) => {
(
payload,
None,
Some(bank.to_owned()),
None,
api_enums::PaymentMethodType::foreign_from(bank.to_owned()),
)
}
api_models::payouts::PayoutMethodData::Wallet(w) => {
(
payload,
None,
None,
Some(wallet.to_owned()),
api_enums::PaymentMethodType::foreign_from(wallet.to_owned()),
)
}
}
}
};

Encryption of data is done the same way for both banks and wallets, so we can merge the steps

@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
SELECT 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this supposed to be this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it is the default case since we don't necessarily need to rollback for a type alter👍

Copy link
Contributor

@kashif-m kashif-m left a comment

Choose a reason for hiding this comment

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

Added minor comments. I don't see provision for storing wallet details in payment_methods table, let's do it as a separate task in a separate PR?

kashif-m
kashif-m previously approved these changes Feb 1, 2024
merchant_account: Secret<String>,
bank: PayoutBankDetails,
bank: Option<PayoutBankDetails>,
additional_data: Option<PayoutAdditionalData>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's create enums for payment method data instead of making all fields optional

shopper_reference: item.router_data.merchant_id.to_owned(),
shopper_email: customer_email,
shopper_name: ShopperName {
first_name: address.get_first_name().ok().cloned(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we need to throw missing field error in case of first_name and last_name none?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since it is a mandatory field, error propagation is enabled now

@likhinbopanna likhinbopanna added this pull request to the merge queue Feb 7, 2024
Merged via the queue into main with commit 3af6aaf Feb 7, 2024
10 of 12 checks passed
@likhinbopanna likhinbopanna deleted the payout_wallet branch February 7, 2024 07:06
@kashif-m kashif-m mentioned this pull request Feb 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-connector-integration Area: Connector integration A-core Area: Core flows C-feature Category: Feature request or enhancement M-api-contract-changes Metadata: This PR involves API contract changes M-database-changes Metadata: This PR involves database schema changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants