Skip to content

feat(router): add support for apple pay pre-decrypted token in the payments confirm call #8815

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

Merged
merged 14 commits into from
Aug 5, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 63 additions & 2 deletions api-reference/v1/openapi_spec_v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -7459,6 +7459,38 @@
"$ref": "#/components/schemas/ApplePayAddressParameters"
}
},
"ApplePayCryptogramData": {
"type": "object",
"description": "This struct represents the cryptogram data for Apple Pay transactions",
"required": [
"online_payment_cryptogram",
"eci_indicator"
],
"properties": {
"online_payment_cryptogram": {
"type": "string",
"description": "The online payment cryptogram",
"example": "A1B2C3D4E5F6G7H8"
},
"eci_indicator": {
"type": "string",
"description": "The ECI (Electronic Commerce Indicator) value",
"example": "05"
}
}
},
"ApplePayPaymentData": {
"oneOf": [
{
"$ref": "#/components/schemas/ApplePayPredecryptData"
},
{
"type": "string",
"description": "This variant contains the encrypted Apple Pay payment data as a string."
}
],
"description": "This enum is used to represent the Apple Pay payment data, which can either be encrypted or decrypted."
},
"ApplePayPaymentRequest": {
"type": "object",
"required": [
Expand Down Expand Up @@ -7529,6 +7561,36 @@
"recurring"
]
},
"ApplePayPredecryptData": {
"type": "object",
"description": "This struct represents the decrypted Apple Pay payment data",
"required": [
"application_primary_account_number",
"application_expiration_month",
"application_expiration_year",
"payment_data"
],
"properties": {
"application_primary_account_number": {
"type": "string",
"description": "The primary account number",
"example": "4242424242424242"
},
"application_expiration_month": {
"type": "string",
"description": "The application expiration date (PAN expiry month)",
"example": "12"
},
"application_expiration_year": {
"type": "string",
"description": "The application expiration date (PAN expiry year)",
"example": "24"
},
"payment_data": {
"$ref": "#/components/schemas/ApplePayCryptogramData"
}
}
},
"ApplePayRecurringDetails": {
"type": "object",
"required": [
Expand Down Expand Up @@ -7705,8 +7767,7 @@
],
"properties": {
"payment_data": {
"type": "string",
"description": "The payment data of Apple pay"
"$ref": "#/components/schemas/Connector"
},
"payment_method": {
"$ref": "#/components/schemas/ApplepayPaymentMethod"
Expand Down
65 changes: 63 additions & 2 deletions api-reference/v2/openapi_spec_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -4467,6 +4467,38 @@
"$ref": "#/components/schemas/ApplePayAddressParameters"
}
},
"ApplePayCryptogramData": {
"type": "object",
"description": "This struct represents the cryptogram data for Apple Pay transactions",
"required": [
"online_payment_cryptogram",
"eci_indicator"
],
"properties": {
"online_payment_cryptogram": {
"type": "string",
"description": "The online payment cryptogram",
"example": "A1B2C3D4E5F6G7H8"
},
"eci_indicator": {
"type": "string",
"description": "The ECI (Electronic Commerce Indicator) value",
"example": "05"
}
}
},
"ApplePayPaymentData": {
"oneOf": [
{
"$ref": "#/components/schemas/ApplePayPredecryptData"
},
{
"type": "string",
"description": "This variant contains the encrypted Apple Pay payment data as a string."
}
],
"description": "This enum is used to represent the Apple Pay payment data, which can either be encrypted or decrypted."
},
"ApplePayPaymentRequest": {
"type": "object",
"required": [
Expand Down Expand Up @@ -4537,6 +4569,36 @@
"recurring"
]
},
"ApplePayPredecryptData": {
"type": "object",
"description": "This struct represents the decrypted Apple Pay payment data",
"required": [
"application_primary_account_number",
"application_expiration_month",
"application_expiration_year",
"payment_data"
],
"properties": {
"application_primary_account_number": {
"type": "string",
"description": "The primary account number",
"example": "4242424242424242"
},
"application_expiration_month": {
"type": "string",
"description": "The application expiration date (PAN expiry month)",
"example": "12"
},
"application_expiration_year": {
"type": "string",
"description": "The application expiration date (PAN expiry year)",
"example": "24"
},
"payment_data": {
"$ref": "#/components/schemas/ApplePayCryptogramData"
}
}
},
"ApplePayRecurringDetails": {
"type": "object",
"required": [
Expand Down Expand Up @@ -4713,8 +4775,7 @@
],
"properties": {
"payment_data": {
"type": "string",
"description": "The payment data of Apple pay"
"$ref": "#/components/schemas/Connector"
},
"payment_method": {
"$ref": "#/components/schemas/ApplepayPaymentMethod"
Expand Down
3 changes: 2 additions & 1 deletion crates/api_models/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3975,7 +3975,8 @@ pub struct GpayTokenizationData {
#[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)]
pub struct ApplePayWalletData {
/// The payment data of Apple pay
pub payment_data: String,
#[schema(value_type = Connector)]
pub payment_data: common_types::payments::ApplePayPaymentData,
/// The payment method of Apple pay
pub payment_method: ApplepayPaymentMethod,
/// The unique identifier for the transaction
Expand Down
1 change: 1 addition & 0 deletions crates/common_types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ error-stack = "0.4.1"

common_enums = { version = "0.1.0", path = "../common_enums" }
common_utils = { version = "0.1.0", path = "../common_utils"}
cards = { version = "0.1.0", path = "../cards"}
euclid = { version = "0.1.0", path = "../euclid" }
masking = { version = "0.1.0", path = "../masking" }

Expand Down
111 changes: 110 additions & 1 deletion crates/common_types/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
use std::collections::HashMap;

use common_enums::enums;
use common_utils::{date_time, errors, events, impl_to_sql_from_sql_json, pii, types::MinorUnit};
use common_utils::{
date_time, errors, events, ext_traits::OptionExt, impl_to_sql_from_sql_json, pii,
types::MinorUnit,
};
use diesel::{
sql_types::{Jsonb, Text},
AsExpression, FromSqlRow,
Expand Down Expand Up @@ -409,3 +412,109 @@ pub struct XenditMultipleSplitResponse {
pub routes: Vec<XenditSplitRoute>,
}
impl_to_sql_from_sql_json!(XenditMultipleSplitResponse);

#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)]
#[serde(rename_all = "snake_case")]
#[serde(untagged)]
/// This enum is used to represent the Apple Pay payment data, which can either be encrypted or decrypted.
pub enum ApplePayPaymentData {
/// This variant contains the decrypted Apple Pay payment data as a structured object.
Decrypted(ApplePayPredecryptData),
/// This variant contains the encrypted Apple Pay payment data as a string.
Encrypted(String),
}

#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)]
#[serde(rename_all = "snake_case")]
/// This struct represents the decrypted Apple Pay payment data
pub struct ApplePayPredecryptData {
/// The primary account number
#[schema(value_type = String, example = "4242424242424242")]
pub application_primary_account_number: cards::CardNumber, // Should this be String or should we have validation for this?
/// The application expiration date (PAN expiry month)
#[schema(value_type = String, example = "12")]
pub application_expiration_month: Secret<String>,
/// The application expiration date (PAN expiry year)
#[schema(value_type = String, example = "24")]
pub application_expiration_year: Secret<String>,
/// The payment data, which contains the cryptogram and ECI indicator
#[schema(value_type = ApplePayCryptogramData)]
pub payment_data: ApplePayCryptogramData,
}

#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)]
#[serde(rename_all = "snake_case")]
/// This struct represents the cryptogram data for Apple Pay transactions
pub struct ApplePayCryptogramData {
/// The online payment cryptogram
#[schema(value_type = String, example = "A1B2C3D4E5F6G7H8")]
pub online_payment_cryptogram: Secret<String>,
/// The ECI (Electronic Commerce Indicator) value
#[schema(value_type = String, example = "05")]
pub eci_indicator: Option<String>,
}

impl ApplePayPaymentData {
/// Get the encrypted Apple Pay payment data if it exists
pub fn get_encrypted_apple_pay_payment_data_optional(&self) -> Option<&String> {
match self {
Self::Encrypted(encrypted_data) => Some(encrypted_data),
Self::Decrypted(_) => None,
}
}

/// Get the decrypted Apple Pay payment data if it exists
pub fn get_decrypted_apple_pay_payment_data_optional(&self) -> Option<&ApplePayPredecryptData> {
match self {
Self::Encrypted(_) => None,
Self::Decrypted(decrypted_data) => Some(decrypted_data),
}
}

/// Get the encrypted Apple Pay payment data, returning an error if it does not exist
pub fn get_encrypted_apple_pay_payment_data_mandatory(
&self,
) -> Result<&String, errors::ValidationError> {
self.get_encrypted_apple_pay_payment_data_optional()
.get_required_value("Encrypted Apple Pay payment data")
.attach_printable("Encrypted Apple Pay payment data is mandatory")
}

/// Get the decrypted Apple Pay payment data, returning an error if it does not exist
pub fn get_decrypted_apple_pay_payment_data_mandatory(
&self,
) -> Result<&ApplePayPredecryptData, errors::ValidationError> {
self.get_decrypted_apple_pay_payment_data_optional()
.get_required_value("Decrypted Apple Pay payment data")
.attach_printable("Decrypted Apple Pay payment data is mandatory")
}
}

impl ApplePayPredecryptData {
/// Get the four-digit expiration year from the Apple Pay pre-decrypt data
pub fn get_two_digit_expiry_year(&self) -> Result<Secret<String>, errors::ValidationError> {
let binding = self.application_expiration_year.clone();
let year = binding.peek();
Ok(Secret::new(
year.get(year.len() - 2..)
.ok_or(errors::ValidationError::InvalidValue {
message: "Invalid two-digit year".to_string(),
})?
.to_string(),
))
}

/// Get the four-digit expiration year from the Apple Pay pre-decrypt data
pub fn get_four_digit_expiry_year(&self) -> Secret<String> {
let mut year = self.application_expiration_year.peek().clone();
if year.len() == 2 {
year = format!("20{year}");
}
Secret::new(year)
}

/// Get the expiration month from the Apple Pay pre-decrypt data
pub fn get_expiry_month(&self) -> Secret<String> {
self.application_expiration_month.clone()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,9 @@ use crate::{
SubmitEvidenceRouterData,
},
utils::{
self, is_manual_capture, missing_field_err, AddressDetailsData, ApplePayDecrypt,
BrowserInformationData, CardData, ForeignTryFrom,
NetworkTokenData as UtilsNetworkTokenData, PaymentsAuthorizeRequestData, PhoneDetailsData,
RouterData as OtherRouterData,
self, is_manual_capture, missing_field_err, AddressDetailsData, BrowserInformationData,
CardData, ForeignTryFrom, NetworkTokenData as UtilsNetworkTokenData,
PaymentsAuthorizeRequestData, PhoneDetailsData, RouterData as OtherRouterData,
},
};

Expand Down Expand Up @@ -1264,7 +1263,7 @@ pub struct AdyenPazeData {
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AdyenApplePayDecryptData {
number: Secret<String>,
number: CardNumber,
expiry_month: Secret<String>,
expiry_year: Secret<String>,
brand: String,
Expand Down Expand Up @@ -2216,8 +2215,8 @@ impl TryFrom<(&WalletData, &PaymentsAuthorizeRouterData)> for AdyenPaymentMethod
if let Some(PaymentMethodToken::ApplePayDecrypt(apple_pay_decrypte)) =
item.payment_method_token.clone()
{
let expiry_year_4_digit = apple_pay_decrypte.get_four_digit_expiry_year()?;
let exp_month = apple_pay_decrypte.get_expiry_month()?;
let expiry_year_4_digit = apple_pay_decrypte.get_four_digit_expiry_year();
let exp_month = apple_pay_decrypte.get_expiry_month();
let apple_pay_decrypted_data = AdyenApplePayDecryptData {
number: apple_pay_decrypte.application_primary_account_number,
expiry_month: exp_month,
Expand All @@ -2229,8 +2228,14 @@ impl TryFrom<(&WalletData, &PaymentsAuthorizeRouterData)> for AdyenPaymentMethod
apple_pay_decrypted_data,
)))
} else {
let apple_pay_encrypted_data = data
.payment_data
.get_encrypted_apple_pay_payment_data_mandatory()
.change_context(errors::ConnectorError::MissingRequiredField {
field_name: "Apple pay encrypted data",
})?;
let apple_pay_data = AdyenApplePay {
apple_pay_token: Secret::new(data.payment_data.to_string()),
apple_pay_token: Secret::new(apple_pay_encrypted_data.to_string()),
};
Ok(AdyenPaymentMethod::ApplePay(Box::new(apple_pay_data)))
}
Expand Down
Loading
Loading