feat(connector): [AMAZONPAY] add Payment flows for Amazon Pay Wallet (#7062)

Co-authored-by: Anurag Singh <anurag.singh.001@Anurag-Singh-WPMHJ5619X.local>
Co-authored-by: Anurag Singh <anurag.singh.001@AnuragSMHJ5619X.lan>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: Anurag Singh <anurag.singh.001@MacBookPro.lan>
This commit is contained in:
Anurag
2025-08-29 14:00:32 +05:30
committed by GitHub
parent 9f0cd51cab
commit 23cf4376f5
70 changed files with 1954 additions and 278 deletions

View File

@ -115,6 +115,7 @@ pub trait RouterData {
fn get_optional_shipping(&self) -> Option<&hyperswitch_domain_models::address::Address>;
fn get_optional_shipping_line1(&self) -> Option<Secret<String>>;
fn get_optional_shipping_line2(&self) -> Option<Secret<String>>;
fn get_optional_shipping_line3(&self) -> Option<Secret<String>>;
fn get_optional_shipping_city(&self) -> Option<String>;
fn get_optional_shipping_country(&self) -> Option<enums::CountryAlpha2>;
fn get_optional_shipping_zip(&self) -> Option<Secret<String>>;
@ -364,6 +365,15 @@ impl<Flow, Request, Response> RouterData for types::RouterData<Flow, Request, Re
})
}
fn get_optional_shipping_line3(&self) -> Option<Secret<String>> {
self.address.get_shipping().and_then(|shipping_address| {
shipping_address
.clone()
.address
.and_then(|shipping_details| shipping_details.line3)
})
}
fn get_optional_shipping_city(&self) -> Option<String> {
self.address.get_shipping().and_then(|shipping_address| {
shipping_address
@ -2492,6 +2502,7 @@ pub enum PaymentMethodDataType {
AliPayQr,
AliPayRedirect,
AliPayHkRedirect,
AmazonPay,
AmazonPayRedirect,
Paysera,
Skrill,
@ -2622,6 +2633,7 @@ impl From<domain::payments::PaymentMethodData> for PaymentMethodDataType {
domain::payments::WalletData::AliPayQr(_) => Self::AliPayQr,
domain::payments::WalletData::AliPayRedirect(_) => Self::AliPayRedirect,
domain::payments::WalletData::AliPayHkRedirect(_) => Self::AliPayHkRedirect,
domain::payments::WalletData::AmazonPay(_) => Self::AmazonPay,
domain::payments::WalletData::AmazonPayRedirect(_) => Self::AmazonPayRedirect,
domain::payments::WalletData::Paysera(_) => Self::Paysera,
domain::payments::WalletData::Skrill(_) => Self::Skrill,

View File

@ -88,6 +88,10 @@ impl ConnectorAuthTypeAndMetadataValidation<'_> {
airwallex::transformers::AirwallexAuthType::try_from(self.auth_type)?;
Ok(())
}
api_enums::Connector::Amazonpay => {
amazonpay::transformers::AmazonpayAuthType::try_from(self.auth_type)?;
Ok(())
}
api_enums::Connector::Archipel => {
archipel::transformers::ArchipelAuthType::try_from(self.auth_type)?;
archipel::transformers::ArchipelConfigData::try_from(self.connector_meta_data)?;

View File

@ -1253,6 +1253,149 @@ fn create_paypal_sdk_session_token(
})
}
async fn create_amazon_pay_session_token(
router_data: &types::PaymentsSessionRouterData,
state: &routes::SessionState,
) -> RouterResult<types::PaymentsSessionRouterData> {
let amazon_pay_session_token_data = router_data
.connector_wallets_details
.clone()
.parse_value::<payment_types::AmazonPaySessionTokenData>("AmazonPaySessionTokenData")
.change_context(errors::ApiErrorResponse::MissingRequiredField {
field_name: "merchant_id or store_id",
})?;
let amazon_pay_metadata = amazon_pay_session_token_data.data;
let merchant_id = amazon_pay_metadata.merchant_id;
let store_id = amazon_pay_metadata.store_id;
let amazonpay_supported_currencies =
payments::cards::list_countries_currencies_for_connector_payment_method_util(
state.conf.pm_filters.clone(),
enums::Connector::Amazonpay,
enums::PaymentMethodType::AmazonPay,
)
.await
.currencies;
// currently supports only the US region hence USD is the only supported currency
payment_types::AmazonPayDeliveryOptions::validate_currency(
router_data.request.currency,
amazonpay_supported_currencies.clone(),
)
.change_context(errors::ApiErrorResponse::CurrencyNotSupported {
message: "USD is the only supported currency.".to_string(),
})?;
let ledger_currency = router_data.request.currency;
// currently supports only the 'automatic' capture_method
let payment_intent = payment_types::AmazonPayPaymentIntent::AuthorizeWithCapture;
let required_amount_type = StringMajorUnitForConnector;
let total_tax_amount = required_amount_type
.convert(
router_data.request.order_tax_amount.unwrap_or_default(),
router_data.request.currency,
)
.change_context(errors::ApiErrorResponse::AmountConversionFailed {
amount_type: "StringMajorUnit",
})?;
let total_base_amount = required_amount_type
.convert(
router_data.request.minor_amount,
router_data.request.currency,
)
.change_context(errors::ApiErrorResponse::AmountConversionFailed {
amount_type: "StringMajorUnit",
})?;
let delivery_options_request = router_data
.request
.metadata
.clone()
.and_then(|metadata| {
metadata
.expose()
.get("delivery_options")
.and_then(|value| value.as_array().cloned())
})
.ok_or(errors::ApiErrorResponse::MissingRequiredField {
field_name: "metadata.delivery_options",
})?;
let mut delivery_options =
payment_types::AmazonPayDeliveryOptions::parse_delivery_options_request(
&delivery_options_request,
)
.change_context(errors::ApiErrorResponse::InvalidDataFormat {
field_name: "delivery_options".to_string(),
expected_format: r#""delivery_options": [{"id": String, "price": {"amount": Number, "currency_code": String}, "shipping_method":{"shipping_method_name": String, "shipping_method_code": String}, "is_default": Boolean}]"#.to_string(),
})?;
let default_amount = payment_types::AmazonPayDeliveryOptions::get_default_delivery_amount(
delivery_options.clone(),
)
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "is_default",
})?;
for option in &delivery_options {
payment_types::AmazonPayDeliveryOptions::validate_currency(
option.price.currency_code,
amazonpay_supported_currencies.clone(),
)
.change_context(errors::ApiErrorResponse::CurrencyNotSupported {
message: "USD is the only supported currency.".to_string(),
})?;
}
payment_types::AmazonPayDeliveryOptions::insert_display_amount(
&mut delivery_options,
router_data.request.currency,
)
.change_context(errors::ApiErrorResponse::AmountConversionFailed {
amount_type: "StringMajorUnit",
})?;
let total_shipping_amount = match router_data.request.shipping_cost {
Some(shipping_cost) => {
if shipping_cost == default_amount {
required_amount_type
.convert(shipping_cost, router_data.request.currency)
.change_context(errors::ApiErrorResponse::AmountConversionFailed {
amount_type: "StringMajorUnit",
})?
} else {
return Err(errors::ApiErrorResponse::InvalidDataValue {
field_name: "shipping_cost",
})
.attach_printable(format!(
"Provided shipping_cost ({shipping_cost}) does not match the default delivery amount ({default_amount})"
));
}
}
None => {
return Err(errors::ApiErrorResponse::MissingRequiredField {
field_name: "shipping_cost",
}
.into());
}
};
Ok(types::PaymentsSessionRouterData {
response: Ok(types::PaymentsResponseData::SessionResponse {
session_token: payment_types::SessionToken::AmazonPay(Box::new(
payment_types::AmazonPaySessionTokenResponse {
merchant_id,
ledger_currency,
store_id,
payment_intent,
total_shipping_amount,
total_tax_amount,
total_base_amount,
delivery_options,
},
)),
}),
..router_data.clone()
})
}
#[async_trait]
impl RouterDataSession for types::PaymentsSessionRouterData {
async fn decide_flow<'a, 'b>(
@ -1308,6 +1451,7 @@ impl RouterDataSession for types::PaymentsSessionRouterData {
Ok(resp)
}
api::GetToken::AmazonPayMetadata => create_amazon_pay_session_token(self, state).await,
}
}
}

View File

@ -5404,6 +5404,9 @@ async fn get_and_merge_apple_pay_metadata(
apple_pay: connector_wallets_details_optional
.as_ref()
.and_then(|d| d.apple_pay.clone()),
amazon_pay: connector_wallets_details_optional
.as_ref()
.and_then(|d| d.amazon_pay.clone()),
samsung_pay: connector_wallets_details_optional
.as_ref()
.and_then(|d| d.samsung_pay.clone()),
@ -5425,6 +5428,9 @@ async fn get_and_merge_apple_pay_metadata(
apple_pay_combined: connector_wallets_details_optional
.as_ref()
.and_then(|d| d.apple_pay_combined.clone()),
amazon_pay: connector_wallets_details_optional
.as_ref()
.and_then(|d| d.amazon_pay.clone()),
samsung_pay: connector_wallets_details_optional
.as_ref()
.and_then(|d| d.samsung_pay.clone()),

View File

@ -514,6 +514,7 @@ impl From<api_models::enums::PaymentMethodType> for api::GetToken {
api_models::enums::PaymentMethodType::SamsungPay => Self::SamsungPayMetadata,
api_models::enums::PaymentMethodType::Paypal => Self::PaypalSdkMetadata,
api_models::enums::PaymentMethodType::Paze => Self::PazeMetadata,
api_models::enums::PaymentMethodType::AmazonPay => Self::AmazonPayMetadata,
_ => Self::Connector,
}
}

View File

@ -395,6 +395,7 @@ impl From<api_models::enums::PaymentMethodType> for api::GetToken {
api_models::enums::PaymentMethodType::SamsungPay => Self::SamsungPayMetadata,
api_models::enums::PaymentMethodType::Paypal => Self::PaypalSdkMetadata,
api_models::enums::PaymentMethodType::Paze => Self::PazeMetadata,
api_models::enums::PaymentMethodType::AmazonPay => Self::AmazonPayMetadata,
_ => Self::Connector,
}
}

View File

@ -1070,6 +1070,13 @@ pub async fn construct_payment_router_data_for_sdk_session<'a>(
ForeignInto::foreign_into((apple_pay_recurring_details, apple_pay_amount))
});
let order_tax_amount = payment_data
.payment_intent
.amount_details
.tax_details
.clone()
.and_then(|tax| tax.get_default_tax_amount());
// TODO: few fields are repeated in both routerdata and request
let request = types::PaymentsSessionData {
amount: payment_data
@ -1095,6 +1102,9 @@ pub async fn construct_payment_router_data_for_sdk_session<'a>(
minor_amount: payment_data.payment_intent.amount_details.order_amount,
apple_pay_recurring_details,
customer_name,
metadata: payment_data.payment_intent.metadata,
order_tax_amount,
shipping_cost: payment_data.payment_intent.amount_details.shipping_cost,
};
// TODO: evaluate the fields in router data, if they are required or not
@ -4814,6 +4824,13 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSessionD
ForeignInto::foreign_into((apple_pay_recurring_details, apple_pay_amount))
});
let order_tax_amount = payment_data
.payment_intent
.amount_details
.tax_details
.clone()
.and_then(|tax| tax.get_default_tax_amount());
Ok(Self {
amount: amount.get_amount_as_i64(), //need to change once we move to connector module
minor_amount: amount,
@ -4831,6 +4848,9 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSessionD
email: payment_data.email,
apple_pay_recurring_details,
customer_name: None,
metadata: payment_data.payment_intent.metadata,
order_tax_amount,
shipping_cost: payment_data.payment_intent.amount_details.shipping_cost,
})
}
}
@ -4900,6 +4920,20 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSessionD
ForeignFrom::foreign_from((apple_pay_recurring_details, apple_pay_amount))
});
let order_tax_amount = payment_data
.payment_intent
.tax_details
.clone()
.and_then(|tax| tax.get_default_tax_amount());
let shipping_cost = payment_data.payment_intent.shipping_cost;
let metadata = payment_data
.payment_intent
.metadata
.clone()
.map(Secret::new);
Ok(Self {
amount: net_amount.get_amount_as_i64(), //need to change once we move to connector module
minor_amount: amount,
@ -4917,6 +4951,9 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSessionD
surcharge_details: payment_data.surcharge_details,
apple_pay_recurring_details,
customer_name: None,
order_tax_amount,
shipping_cost,
metadata,
})
}
}

View File

@ -28,6 +28,7 @@ pub struct ConnectorData {
pub enum GetToken {
GpayMetadata,
SamsungPayMetadata,
AmazonPayMetadata,
ApplePayMetadata,
PaypalSdkMetadata,
PazeMetadata,
@ -117,9 +118,9 @@ impl ConnectorData {
enums::Connector::Airwallex => {
Ok(ConnectorEnum::Old(Box::new(connector::Airwallex::new())))
}
// enums::Connector::Amazonpay => {
// Ok(ConnectorEnum::Old(Box::new(connector::Amazonpay)))
// }
enums::Connector::Amazonpay => {
Ok(ConnectorEnum::Old(Box::new(connector::Amazonpay::new())))
}
enums::Connector::Archipel => {
Ok(ConnectorEnum::Old(Box::new(connector::Archipel::new())))
}

View File

@ -31,9 +31,9 @@ impl FeatureMatrixConnectorData {
enums::Connector::Airwallex => {
Ok(ConnectorEnum::Old(Box::new(connector::Airwallex::new())))
}
// enums::Connector::Amazonpay => {
// Ok(ConnectorEnum::Old(Box::new(connector::Amazonpay)))
// }
enums::Connector::Amazonpay => {
Ok(ConnectorEnum::Old(Box::new(connector::Amazonpay::new())))
}
enums::Connector::Archipel => {
Ok(ConnectorEnum::Old(Box::new(connector::Archipel::new())))
}

View File

@ -12,7 +12,7 @@ impl ForeignTryFrom<api_enums::Connector> for common_enums::RoutableConnectors {
api_enums::Connector::Affirm => Self::Affirm,
api_enums::Connector::Adyenplatform => Self::Adyenplatform,
api_enums::Connector::Airwallex => Self::Airwallex,
// api_enums::Connector::Amazonpay => Self::Amazonpay,
api_enums::Connector::Amazonpay => Self::Amazonpay,
api_enums::Connector::Archipel => Self::Archipel,
api_enums::Connector::Authipay => Self::Authipay,
api_enums::Connector::Authorizedotnet => Self::Authorizedotnet,