feat(router): Add /apply-payment-method-data endpoint (v2)

This commit is contained in:
Anurag Thakur
2025-10-16 08:25:06 +05:30
parent 03a7cd2ef7
commit b85f94bfe3
13 changed files with 351 additions and 5 deletions

View File

@ -1,10 +1,15 @@
use std::marker::PhantomData;
use std::{collections::HashMap, marker::PhantomData};
use api_models::payments::{
GetPaymentMethodType, PaymentMethodBalanceCheckRequest, PaymentMethodBalanceCheckResponse,
ApplyPaymentMethodDataRequest, ApplyPaymentMethodDataResponse, GetPaymentMethodType,
PaymentMethodBalanceCheckRequest, PaymentMethodBalanceCheckResponse,
};
use common_enums::CallConnectorAction;
use common_utils::{ext_traits::Encode, id_type, types::MinorUnit};
use common_utils::{
ext_traits::{Encode, StringExt},
id_type,
types::MinorUnit,
};
use error_stack::ResultExt;
use hyperswitch_domain_models::{
payments::HeaderPayload,
@ -197,6 +202,52 @@ pub async fn payments_check_gift_card_balance_core(
Ok(services::ApplicationResponse::Json(resp))
}
#[allow(clippy::too_many_arguments)]
pub async fn payments_apply_pm_data_core(
state: SessionState,
merchant_context: domain::MerchantContext,
_req_state: ReqState,
req: ApplyPaymentMethodDataRequest,
payment_id: id_type::GlobalPaymentId,
) -> RouterResponse<ApplyPaymentMethodDataResponse> {
let db = state.store.as_ref();
let key_manager_state = &(&state).into();
let storage_scheme = merchant_context.get_merchant_account().storage_scheme;
let payment_intent = db
.find_payment_intent_by_id(
key_manager_state,
&payment_id,
merchant_context.get_merchant_key_store(),
storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
let balances =
fetch_payment_methods_balances_from_redis(&state, &payment_intent.id, &req.payment_methods)
.await
.attach_printable("Failed to retrieve payment method balances from redis")?;
let total_balance: i64 = balances
.values()
.map(|value| value.balance.get_amount_as_i64())
.sum();
let remaining_amount = payment_intent
.amount_details
.order_amount
.get_amount_as_i64()
.saturating_sub(total_balance);
let resp = ApplyPaymentMethodDataResponse {
remaining_amount: MinorUnit::new(remaining_amount),
requires_additional_pm_data: remaining_amount > 0,
surcharge_details: None, // TODO: Implement surcharge recalculation logic
};
Ok(services::ApplicationResponse::Json(resp))
}
#[instrument(skip_all)]
pub async fn persist_individual_pm_balance_details_in_redis<'a>(
state: &SessionState,
@ -236,7 +287,59 @@ pub async fn persist_individual_pm_balance_details_in_redis<'a>(
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to write to redis")?;
logger::debug!("Surcharge results stored in redis with key = {}", redis_key);
}
Ok(())
}
pub async fn fetch_payment_methods_balances_from_redis(
state: &SessionState,
payment_intent_id: &id_type::GlobalPaymentId,
payment_methods: &[api_models::payments::BalanceCheckPaymentMethodData],
) -> errors::RouterResult<HashMap<domain::PaymentMethodBalanceKey, domain::PaymentMethodBalanceValue>>
{
let redis_conn = state
.store
.get_redis_conn()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to get redis connection")?;
let balance_data = domain::PaymentMethodBalanceData::new(payment_intent_id);
let balance_values: HashMap<String, domain::PaymentMethodBalanceValue> = redis_conn
.get_hash_fields::<Vec<(String, String)>>(&balance_data.get_pm_balance_redis_key().into())
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to read payment method balance data from redis")?
.into_iter()
.map(|(key, value)| {
value
.parse_struct("PaymentMethodBalanceValue")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to parse PaymentMethodBalanceValue")
.map(|parsed| (key, parsed))
})
.collect::<errors::RouterResult<HashMap<_, _>>>()?;
payment_methods
.iter()
.map(|pm| {
let api_models::payments::BalanceCheckPaymentMethodData::GiftCard(gift_card_data) = pm;
let pm_balance_key = domain::PaymentMethodBalanceKey {
payment_method_type: common_enums::PaymentMethod::GiftCard,
payment_method_subtype: gift_card_data.get_payment_method_type(),
payment_method_key: domain::GiftCardData::from(gift_card_data.clone())
.get_payment_method_key()
.expose(),
};
let redis_key = pm_balance_key.get_redis_key();
let balance_value = balance_values.get(&redis_key).cloned().ok_or(
errors::ApiErrorResponse::GenericNotFoundError {
message: "Balance not found for one or more payment methods".to_string(),
},
)?;
Ok((pm_balance_key, balance_value))
})
.collect::<errors::RouterResult<HashMap<_, _>>>()
}

View File

@ -797,6 +797,10 @@ impl Payments {
.route(web::post().to(payments::payment_check_gift_card_balance)),
),
)
.service(
web::resource("/apply-payment-method-data")
.route(web::post().to(payments::payments_apply_pm_data)),
)
.service(
web::resource("/finish-redirection/{publishable_key}/{profile_id}")
.route(web::get().to(payments::payments_finish_redirection)),

View File

@ -163,6 +163,7 @@ impl From<Flow> for ApiIdentifier {
| Flow::PaymentsCreateIntent
| Flow::PaymentsGetIntent
| Flow::PaymentMethodBalanceCheck
| Flow::ApplyPaymentMethodData
| Flow::PaymentsPostSessionTokens
| Flow::PaymentsUpdateMetadata
| Flow::PaymentsUpdateIntent

View File

@ -3189,6 +3189,60 @@ pub async fn payment_check_gift_card_balance(
.await
}
#[cfg(feature = "v2")]
#[instrument(skip_all, fields(flow = ?Flow::ApplyPaymentMethodData, payment_id))]
pub async fn payments_apply_pm_data(
state: web::Data<app::AppState>,
req: actix_web::HttpRequest,
json_payload: web::Json<api_models::payments::ApplyPaymentMethodDataRequest>,
path: web::Path<common_utils::id_type::GlobalPaymentId>,
) -> impl Responder {
let flow = Flow::ApplyPaymentMethodData;
let global_payment_id = path.into_inner();
tracing::Span::current().record("payment_id", global_payment_id.get_string_repr());
let internal_payload = internal_payload_types::PaymentsGenericRequestWithResourceId {
global_payment_id: global_payment_id.clone(),
payload: json_payload.into_inner(),
};
Box::pin(api::server_wrap(
flow,
state,
&req,
internal_payload,
|state, auth: auth::AuthenticationData, req, req_state| async {
let payment_id = req.global_payment_id;
let request = req.payload;
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
domain::Context(auth.merchant_account, auth.key_store),
));
Box::pin(gift_card::payments_apply_pm_data_core(
state,
merchant_context,
req_state,
request,
payment_id,
))
.await
},
auth::api_or_client_auth(
&auth::V2ApiKeyAuth {
is_connected_allowed: false,
is_platform_allowed: false,
},
&auth::V2ClientAuth(common_utils::types::authentication::ResourceId::Payment(
global_payment_id,
)),
req.headers(),
),
api_locking::LockAction::NotApplicable,
))
.await
}
#[cfg(feature = "v2")]
#[instrument(skip_all, fields(flow = ?Flow::ProxyConfirmIntent, payment_id))]
pub async fn proxy_confirm_intent(