mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 19:46:48 +08:00
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
246 lines
7.4 KiB
Rust
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,
|
|
})
|
|
}
|
|
}
|