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>, pub customer_id_resp: String, pub connector_payment_id: Option, pub token: Option>, pub exp_date: Option>, pub card_network: Option, pub payment_method_sub_type: Option, pub clean_bank_name: Option, pub country_name: Option, pub daily_retry_history: Option, } #[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, pub failed_records: Vec, } #[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, pub card_exp_month: Option>, pub card_exp_year: Option>, pub card_network: Option, pub card_issuer: Option, pub card_issuing_country: Option, pub daily_retry_history: Option>, } impl ApiEventMetric for RevenueRecoveryDataBackfillResponse { fn get_api_event_type(&self) -> Option { Some(common_utils::events::ApiEventsType::Miscellaneous) } } impl ApiEventMetric for UnlockStatusResponse { fn get_api_event_type(&self) -> Option { Some(common_utils::events::ApiEventsType::Miscellaneous) } } impl ApiEventMetric for CsvParsingResult { fn get_api_event_type(&self) -> Option { Some(common_utils::events::ApiEventsType::Miscellaneous) } } impl ApiEventMetric for CsvParsingError { fn get_api_event_type(&self) -> Option { Some(common_utils::events::ApiEventsType::Miscellaneous) } } impl ApiEventMetric for RedisDataResponse { fn get_api_event_type(&self) -> Option { Some(common_utils::events::ApiEventsType::Miscellaneous) } } impl ApiEventMetric for UpdateTokenStatusRequest { fn get_api_event_type(&self) -> Option { Some(common_utils::events::ApiEventsType::Miscellaneous) } } impl ApiEventMetric for UpdateTokenStatusResponse { fn get_api_event_type(&self) -> Option { 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, } #[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, } #[derive(Debug, Serialize)] pub enum ScheduledAtUpdate { SetToNull, SetToDateTime(PrimitiveDateTime), } impl<'de> Deserialize<'de> for ScheduledAtUpdate { fn deserialize(deserializer: D) -> Result 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, pub scheduled_at: Option, pub is_hard_decline: Option, pub error_code: Option, } #[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 { // 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::() .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, }) } }