Files
hyperswitch/crates/api_models/src/revenue_recovery_data_backfill.rs
AdityaWNL af159867ae feat(revenue_recovery): add support to fetch data and update additional token data in redis (#9611)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
2025-10-01 14:59:52 +00:00

246 lines
7.4 KiB
Rust

use std::{collections::HashMap, fs::File, io::BufReader};
use actix_multipart::form::{tempfile::TempFile, MultipartForm};
use actix_web::{HttpResponse, ResponseError};
use common_enums::{CardNetwork, PaymentMethodType};
use common_utils::{events::ApiEventMetric, pii::PhoneNumberStrategy};
use csv::Reader;
use masking::Secret;
use serde::{Deserialize, Serialize};
use time::{Date, PrimitiveDateTime};
#[derive(Debug, Deserialize, Serialize)]
pub struct RevenueRecoveryBackfillRequest {
pub bin_number: Option<Secret<String>>,
pub customer_id_resp: String,
pub connector_payment_id: Option<String>,
pub token: Option<Secret<String>>,
pub exp_date: Option<Secret<String>>,
pub card_network: Option<CardNetwork>,
pub payment_method_sub_type: Option<PaymentMethodType>,
pub clean_bank_name: Option<String>,
pub country_name: Option<String>,
pub daily_retry_history: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct UnlockStatusResponse {
pub unlocked: bool,
}
#[derive(Debug, Serialize)]
pub struct RevenueRecoveryDataBackfillResponse {
pub processed_records: usize,
pub failed_records: usize,
}
#[derive(Debug, Serialize)]
pub struct CsvParsingResult {
pub records: Vec<RevenueRecoveryBackfillRequest>,
pub failed_records: Vec<CsvParsingError>,
}
#[derive(Debug, Serialize)]
pub struct CsvParsingError {
pub row_number: usize,
pub error: String,
}
/// Comprehensive card
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComprehensiveCardData {
pub card_type: Option<String>,
pub card_exp_month: Option<Secret<String>>,
pub card_exp_year: Option<Secret<String>>,
pub card_network: Option<CardNetwork>,
pub card_issuer: Option<String>,
pub card_issuing_country: Option<String>,
pub daily_retry_history: Option<HashMap<Date, i32>>,
}
impl ApiEventMetric for RevenueRecoveryDataBackfillResponse {
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
Some(common_utils::events::ApiEventsType::Miscellaneous)
}
}
impl ApiEventMetric for UnlockStatusResponse {
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
Some(common_utils::events::ApiEventsType::Miscellaneous)
}
}
impl ApiEventMetric for CsvParsingResult {
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
Some(common_utils::events::ApiEventsType::Miscellaneous)
}
}
impl ApiEventMetric for CsvParsingError {
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
Some(common_utils::events::ApiEventsType::Miscellaneous)
}
}
impl ApiEventMetric for RedisDataResponse {
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
Some(common_utils::events::ApiEventsType::Miscellaneous)
}
}
impl ApiEventMetric for UpdateTokenStatusRequest {
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
Some(common_utils::events::ApiEventsType::Miscellaneous)
}
}
impl ApiEventMetric for UpdateTokenStatusResponse {
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
Some(common_utils::events::ApiEventsType::Miscellaneous)
}
}
#[derive(Debug, Clone, Serialize)]
pub enum BackfillError {
InvalidCardType(String),
DatabaseError(String),
RedisError(String),
CsvParsingError(String),
FileProcessingError(String),
}
#[derive(serde::Deserialize)]
pub struct BackfillQuery {
pub cutoff_time: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum RedisKeyType {
Status, // for customer:{id}:status
Tokens, // for customer:{id}:tokens
}
#[derive(Debug, Deserialize)]
pub struct GetRedisDataQuery {
pub key_type: RedisKeyType,
}
#[derive(Debug, Serialize)]
pub struct RedisDataResponse {
pub exists: bool,
pub ttl_seconds: i64,
pub data: Option<serde_json::Value>,
}
#[derive(Debug, Serialize)]
pub enum ScheduledAtUpdate {
SetToNull,
SetToDateTime(PrimitiveDateTime),
}
impl<'de> Deserialize<'de> for ScheduledAtUpdate {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::String(s) => {
if s.to_lowercase() == "null" {
Ok(Self::SetToNull)
} else {
// Parse as datetime using iso8601 deserializer
common_utils::custom_serde::iso8601::deserialize(
&mut serde_json::Deserializer::from_str(&format!("\"{}\"", s)),
)
.map(Self::SetToDateTime)
.map_err(serde::de::Error::custom)
}
}
_ => Err(serde::de::Error::custom(
"Expected null variable or datetime iso8601 ",
)),
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct UpdateTokenStatusRequest {
pub connector_customer_id: String,
pub payment_processor_token: Secret<String, PhoneNumberStrategy>,
pub scheduled_at: Option<ScheduledAtUpdate>,
pub is_hard_decline: Option<bool>,
pub error_code: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct UpdateTokenStatusResponse {
pub updated: bool,
pub message: String,
}
impl std::fmt::Display for BackfillError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidCardType(msg) => write!(f, "Invalid card type: {}", msg),
Self::DatabaseError(msg) => write!(f, "Database error: {}", msg),
Self::RedisError(msg) => write!(f, "Redis error: {}", msg),
Self::CsvParsingError(msg) => write!(f, "CSV parsing error: {}", msg),
Self::FileProcessingError(msg) => write!(f, "File processing error: {}", msg),
}
}
}
impl std::error::Error for BackfillError {}
impl ResponseError for BackfillError {
fn error_response(&self) -> HttpResponse {
HttpResponse::BadRequest().json(serde_json::json!({
"error": self.to_string()
}))
}
}
#[derive(Debug, MultipartForm)]
pub struct RevenueRecoveryDataBackfillForm {
#[multipart(rename = "file")]
pub file: TempFile,
}
impl RevenueRecoveryDataBackfillForm {
pub fn validate_and_get_records_with_errors(&self) -> Result<CsvParsingResult, BackfillError> {
// Step 1: Open the file
let file = File::open(self.file.file.path())
.map_err(|error| BackfillError::FileProcessingError(error.to_string()))?;
let mut csv_reader = Reader::from_reader(BufReader::new(file));
// Step 2: Parse CSV into typed records
let mut records = Vec::new();
let mut failed_records = Vec::new();
for (row_index, record_result) in csv_reader
.deserialize::<RevenueRecoveryBackfillRequest>()
.enumerate()
{
match record_result {
Ok(record) => {
records.push(record);
}
Err(err) => {
failed_records.push(CsvParsingError {
row_number: row_index + 2, // +2 because enumerate starts at 0 and CSV has header row
error: err.to_string(),
});
}
}
}
Ok(CsvParsingResult {
records,
failed_records,
})
}
}