diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index 03370e5e99..f073212ed5 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -8,7 +8,7 @@ readme = "README.md" license.workspace = true [features] -default = ["payouts", "frm"] +default = [] business_profile_routing = [] connector_choice_bcompat = [] errors = ["dep:actix-web", "dep:reqwest"] diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index bf88eca0a0..10a68ef1a4 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -843,13 +843,6 @@ pub struct MerchantConnectorDetails { pub metadata: Option, } -#[cfg(feature = "payouts")] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(tag = "type", content = "data", rename_all = "snake_case")] -pub enum PayoutRoutingAlgorithm { - Single(api_enums::PayoutConnectors), -} - #[derive(Clone, Debug, Deserialize, ToSchema, Default, Serialize)] #[serde(deny_unknown_fields)] pub struct BusinessProfileCreate { diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 9873f62bfe..37580edbc7 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -184,6 +184,7 @@ pub struct PaymentMethodResponse { /// Payment method details from locker #[cfg(feature = "payouts")] #[schema(value_type = Option)] + #[serde(skip_serializing_if = "Option::is_none")] pub bank_transfer: Option, #[schema(value_type = Option, example = "2024-02-24T11:04:09.922Z")] @@ -822,6 +823,7 @@ pub struct CustomerPaymentMethod { /// Payment method details from locker #[cfg(feature = "payouts")] #[schema(value_type = Option)] + #[serde(skip_serializing_if = "Option::is_none")] pub bank_transfer: Option, /// Masked bank details from PM auth services diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index f1d673bf1f..3597f11f58 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -51,8 +51,8 @@ pub struct PayoutCreateRequest { pub routing: Option, /// This allows the merchant to manually select a connector with which the payout can go through - #[schema(value_type = Option>, max_length = 255, example = json!(["wise", "adyen"]))] - pub connector: Option>, + #[schema(value_type = Option>, max_length = 255, example = json!(["wise", "adyen"]))] + pub connector: Option>, /// The boolean value to create payout with connector #[schema(value_type = bool, example = true, default = false)] @@ -272,7 +272,7 @@ pub struct Paypal { pub email: Option, } -#[derive(Debug, ToSchema, Clone, Serialize)] +#[derive(Debug, Default, ToSchema, Clone, Serialize)] #[serde(deny_unknown_fields)] pub struct PayoutCreateResponse { /// Unique identifier for the payout. This ensures idempotency for multiple payouts diff --git a/crates/connector_configs/Cargo.toml b/crates/connector_configs/Cargo.toml index 083a741ef5..e1601f0c79 100644 --- a/crates/connector_configs/Cargo.toml +++ b/crates/connector_configs/Cargo.toml @@ -12,7 +12,7 @@ production = [] development = [] sandbox = [] dummy_connector = ["api_models/dummy_connector", "development"] -payouts = [] +payouts = ["api_models/payouts"] [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } diff --git a/crates/data_models/Cargo.toml b/crates/data_models/Cargo.toml index 39bcc71341..9d4447b0c0 100644 --- a/crates/data_models/Cargo.toml +++ b/crates/data_models/Cargo.toml @@ -8,8 +8,9 @@ readme = "README.md" license.workspace = true [features] -default = ["olap"] +default = ["olap", "payouts"] olap = [] +payouts = ["api_models/payouts"] [dependencies] # First party deps diff --git a/crates/data_models/src/lib.rs b/crates/data_models/src/lib.rs index a441c827d8..30279fae54 100644 --- a/crates/data_models/src/lib.rs +++ b/crates/data_models/src/lib.rs @@ -1,6 +1,14 @@ pub mod errors; pub mod mandates; pub mod payments; +#[cfg(feature = "payouts")] +pub mod payouts; + +#[cfg(not(feature = "payouts"))] +pub trait PayoutAttemptInterface {} + +#[cfg(not(feature = "payouts"))] +pub trait PayoutsInterface {} #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub enum RemoteStorageObject { diff --git a/crates/data_models/src/payouts.rs b/crates/data_models/src/payouts.rs new file mode 100644 index 0000000000..2017f96c06 --- /dev/null +++ b/crates/data_models/src/payouts.rs @@ -0,0 +1,3 @@ +pub mod payout_attempt; +#[allow(clippy::module_inception)] +pub mod payouts; diff --git a/crates/data_models/src/payouts/payout_attempt.rs b/crates/data_models/src/payouts/payout_attempt.rs new file mode 100644 index 0000000000..081bc1cf98 --- /dev/null +++ b/crates/data_models/src/payouts/payout_attempt.rs @@ -0,0 +1,182 @@ +use common_enums as storage_enums; +use serde::{Deserialize, Serialize}; +use storage_enums::MerchantStorageScheme; +use time::PrimitiveDateTime; + +use crate::errors; + +#[async_trait::async_trait] +pub trait PayoutAttemptInterface { + async fn insert_payout_attempt( + &self, + _payout: PayoutAttemptNew, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result; + + async fn update_payout_attempt( + &self, + _this: &PayoutAttempt, + _payout_attempt_update: PayoutAttemptUpdate, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result; + + async fn find_payout_attempt_by_merchant_id_payout_attempt_id( + &self, + _merchant_id: &str, + _payout_attempt_id: &str, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result; +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct PayoutAttempt { + pub payout_attempt_id: String, + pub payout_id: String, + pub customer_id: String, + pub merchant_id: String, + pub address_id: String, + pub connector: Option, + pub connector_payout_id: String, + pub payout_token: Option, + pub status: storage_enums::PayoutStatus, + pub is_eligible: Option, + pub error_message: Option, + pub error_code: Option, + pub business_country: Option, + pub business_label: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub last_modified_at: PrimitiveDateTime, + pub profile_id: String, + pub merchant_connector_id: Option, + pub routing_info: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct PayoutAttemptNew { + pub payout_attempt_id: String, + pub payout_id: String, + pub customer_id: String, + pub merchant_id: String, + pub address_id: String, + pub connector: Option, + pub connector_payout_id: String, + pub payout_token: Option, + pub status: storage_enums::PayoutStatus, + pub is_eligible: Option, + pub error_message: Option, + pub error_code: Option, + pub business_country: Option, + pub business_label: Option, + pub created_at: Option, + pub last_modified_at: Option, + pub profile_id: String, + pub merchant_connector_id: Option, + pub routing_info: Option, +} + +impl Default for PayoutAttemptNew { + fn default() -> Self { + let now = common_utils::date_time::now(); + + Self { + payout_attempt_id: String::default(), + payout_id: String::default(), + customer_id: String::default(), + merchant_id: String::default(), + address_id: String::default(), + connector: None, + connector_payout_id: String::default(), + payout_token: None, + status: storage_enums::PayoutStatus::default(), + is_eligible: None, + error_message: None, + error_code: None, + business_country: Some(storage_enums::CountryAlpha2::default()), + business_label: None, + created_at: Some(now), + last_modified_at: Some(now), + profile_id: String::default(), + merchant_connector_id: None, + routing_info: None, + } + } +} + +#[derive(Debug)] +pub enum PayoutAttemptUpdate { + StatusUpdate { + connector_payout_id: String, + status: storage_enums::PayoutStatus, + error_message: Option, + error_code: Option, + is_eligible: Option, + }, + PayoutTokenUpdate { + payout_token: String, + }, + BusinessUpdate { + business_country: Option, + business_label: Option, + }, + UpdateRouting { + connector: String, + routing_info: Option, + }, +} + +#[derive(Clone, Debug, Default)] +pub struct PayoutAttemptUpdateInternal { + pub payout_token: Option, + pub connector_payout_id: Option, + pub status: Option, + pub error_message: Option, + pub error_code: Option, + pub is_eligible: Option, + pub business_country: Option, + pub business_label: Option, + pub connector: Option, + pub routing_info: Option, +} + +impl From for PayoutAttemptUpdateInternal { + fn from(payout_update: PayoutAttemptUpdate) -> Self { + match payout_update { + PayoutAttemptUpdate::PayoutTokenUpdate { payout_token } => Self { + payout_token: Some(payout_token), + ..Default::default() + }, + PayoutAttemptUpdate::StatusUpdate { + connector_payout_id, + status, + error_message, + error_code, + is_eligible, + } => Self { + connector_payout_id: Some(connector_payout_id), + status: Some(status), + error_message, + error_code, + is_eligible, + ..Default::default() + }, + PayoutAttemptUpdate::BusinessUpdate { + business_country, + business_label, + } => Self { + business_country, + business_label, + ..Default::default() + }, + PayoutAttemptUpdate::UpdateRouting { + connector, + routing_info, + } => Self { + connector: Some(connector), + routing_info, + ..Default::default() + }, + } + } +} diff --git a/crates/data_models/src/payouts/payouts.rs b/crates/data_models/src/payouts/payouts.rs new file mode 100644 index 0000000000..f897ca739b --- /dev/null +++ b/crates/data_models/src/payouts/payouts.rs @@ -0,0 +1,202 @@ +use common_enums as storage_enums; +use common_utils::pii; +use serde::{Deserialize, Serialize}; +use storage_enums::MerchantStorageScheme; +use time::PrimitiveDateTime; + +use crate::errors; + +#[async_trait::async_trait] +pub trait PayoutsInterface { + async fn insert_payout( + &self, + _payout: PayoutsNew, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result; + + async fn find_payout_by_merchant_id_payout_id( + &self, + _merchant_id: &str, + _payout_id: &str, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result; + + async fn update_payout( + &self, + _this: &Payouts, + _payout: PayoutsUpdate, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result; + + async fn find_optional_payout_by_merchant_id_payout_id( + &self, + _merchant_id: &str, + _payout_id: &str, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result, errors::StorageError>; +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Payouts { + pub payout_id: String, + pub merchant_id: String, + pub customer_id: String, + pub address_id: String, + pub payout_type: storage_enums::PayoutType, + pub payout_method_id: Option, + pub amount: i64, + pub destination_currency: storage_enums::Currency, + pub source_currency: storage_enums::Currency, + pub description: Option, + pub recurring: bool, + pub auto_fulfill: bool, + pub return_url: Option, + pub entity_type: storage_enums::PayoutEntityType, + pub metadata: Option, + pub created_at: PrimitiveDateTime, + pub last_modified_at: PrimitiveDateTime, + pub attempt_count: i16, + pub profile_id: String, + pub status: storage_enums::PayoutStatus, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PayoutsNew { + pub payout_id: String, + pub merchant_id: String, + pub customer_id: String, + pub address_id: String, + pub payout_type: storage_enums::PayoutType, + pub payout_method_id: Option, + pub amount: i64, + pub destination_currency: storage_enums::Currency, + pub source_currency: storage_enums::Currency, + pub description: Option, + pub recurring: bool, + pub auto_fulfill: bool, + pub return_url: Option, + pub entity_type: storage_enums::PayoutEntityType, + pub metadata: Option, + pub created_at: Option, + pub last_modified_at: Option, + pub profile_id: String, + pub status: storage_enums::PayoutStatus, + pub attempt_count: i16, +} + +impl Default for PayoutsNew { + fn default() -> Self { + let now = common_utils::date_time::now(); + + Self { + payout_id: String::default(), + merchant_id: String::default(), + customer_id: String::default(), + address_id: String::default(), + payout_type: storage_enums::PayoutType::default(), + payout_method_id: Option::default(), + amount: i64::default(), + destination_currency: storage_enums::Currency::default(), + source_currency: storage_enums::Currency::default(), + description: Option::default(), + recurring: bool::default(), + auto_fulfill: bool::default(), + return_url: None, + entity_type: storage_enums::PayoutEntityType::default(), + metadata: Option::default(), + created_at: Some(now), + last_modified_at: Some(now), + profile_id: String::default(), + status: storage_enums::PayoutStatus::default(), + attempt_count: 1, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum PayoutsUpdate { + Update { + amount: i64, + destination_currency: storage_enums::Currency, + source_currency: storage_enums::Currency, + description: Option, + recurring: bool, + auto_fulfill: bool, + return_url: Option, + entity_type: storage_enums::PayoutEntityType, + metadata: Option, + profile_id: Option, + status: Option, + }, + PayoutMethodIdUpdate { + payout_method_id: Option, + }, + RecurringUpdate { + recurring: bool, + }, + AttemptCountUpdate { + attempt_count: i16, + }, +} + +#[derive(Clone, Debug, Default)] +pub struct PayoutsUpdateInternal { + pub amount: Option, + pub destination_currency: Option, + pub source_currency: Option, + pub description: Option, + pub recurring: Option, + pub auto_fulfill: Option, + pub return_url: Option, + pub entity_type: Option, + pub metadata: Option, + pub payout_method_id: Option, + pub profile_id: Option, + pub status: Option, + pub attempt_count: Option, +} + +impl From for PayoutsUpdateInternal { + fn from(payout_update: PayoutsUpdate) -> Self { + match payout_update { + PayoutsUpdate::Update { + amount, + destination_currency, + source_currency, + description, + recurring, + auto_fulfill, + return_url, + entity_type, + metadata, + profile_id, + status, + } => Self { + amount: Some(amount), + destination_currency: Some(destination_currency), + source_currency: Some(source_currency), + description, + recurring: Some(recurring), + auto_fulfill: Some(auto_fulfill), + return_url, + entity_type: Some(entity_type), + metadata, + profile_id, + status, + ..Default::default() + }, + PayoutsUpdate::PayoutMethodIdUpdate { payout_method_id } => Self { + payout_method_id, + ..Default::default() + }, + PayoutsUpdate::RecurringUpdate { recurring } => Self { + recurring: Some(recurring), + ..Default::default() + }, + PayoutsUpdate::AttemptCountUpdate { attempt_count } => Self { + attempt_count: Some(attempt_count), + ..Default::default() + }, + } + } +} diff --git a/crates/diesel_models/src/kv.rs b/crates/diesel_models/src/kv.rs index d067192e24..cc67deb40c 100644 --- a/crates/diesel_models/src/kv.rs +++ b/crates/diesel_models/src/kv.rs @@ -6,6 +6,8 @@ use crate::{ errors, payment_attempt::{PaymentAttempt, PaymentAttemptNew, PaymentAttemptUpdate}, payment_intent::{PaymentIntentNew, PaymentIntentUpdate}, + payout_attempt::{PayoutAttempt, PayoutAttemptNew, PayoutAttemptUpdate}, + payouts::{Payouts, PayoutsNew, PayoutsUpdate}, refund::{Refund, RefundNew, RefundUpdate}, reverse_lookup::{ReverseLookup, ReverseLookupNew}, PaymentIntent, PgPooledConn, @@ -32,6 +34,8 @@ impl DBOperation { Insertable::PaymentAttempt(_) => "payment_attempt", Insertable::Refund(_) => "refund", Insertable::Address(_) => "address", + Insertable::Payouts(_) => "payouts", + Insertable::PayoutAttempt(_) => "payout_attempt", Insertable::ReverseLookUp(_) => "reverse_lookup", }, Self::Update { updatable } => match updatable { @@ -39,6 +43,8 @@ impl DBOperation { Updateable::PaymentAttemptUpdate(_) => "payment_attempt", Updateable::RefundUpdate(_) => "refund", Updateable::AddressUpdate(_) => "address", + Updateable::PayoutsUpdate(_) => "payouts", + Updateable::PayoutAttemptUpdate(_) => "payout_attempt", }, } } @@ -51,6 +57,8 @@ pub enum DBResult { Refund(Box), Address(Box
), ReverseLookUp(Box), + Payouts(Box), + PayoutAttempt(Box), } #[derive(Debug, Serialize, Deserialize)] @@ -74,6 +82,10 @@ impl DBOperation { Insertable::ReverseLookUp(rev) => { DBResult::ReverseLookUp(Box::new(rev.insert(conn).await?)) } + Insertable::Payouts(rev) => DBResult::Payouts(Box::new(rev.insert(conn).await?)), + Insertable::PayoutAttempt(rev) => { + DBResult::PayoutAttempt(Box::new(rev.insert(conn).await?)) + } }, Self::Update { updatable } => match updatable { Updateable::PaymentIntentUpdate(a) => { @@ -88,6 +100,12 @@ impl DBOperation { Updateable::AddressUpdate(a) => { DBResult::Address(Box::new(a.orig.update(conn, a.update_data).await?)) } + Updateable::PayoutsUpdate(a) => { + DBResult::Payouts(Box::new(a.orig.update(conn, a.update_data).await?)) + } + Updateable::PayoutAttemptUpdate(a) => DBResult::PayoutAttempt(Box::new( + a.orig.update_with_attempt_id(conn, a.update_data).await?, + )), }, }) } @@ -123,6 +141,8 @@ pub enum Insertable { Refund(RefundNew), Address(Box), ReverseLookUp(ReverseLookupNew), + Payouts(PayoutsNew), + PayoutAttempt(PayoutAttemptNew), } #[derive(Debug, Serialize, Deserialize)] @@ -132,6 +152,8 @@ pub enum Updateable { PaymentAttemptUpdate(PaymentAttemptUpdateMems), RefundUpdate(RefundUpdateMems), AddressUpdate(Box), + PayoutsUpdate(PayoutsUpdateMems), + PayoutAttemptUpdate(PayoutAttemptUpdateMems), } #[derive(Debug, Serialize, Deserialize)] @@ -157,3 +179,15 @@ pub struct RefundUpdateMems { pub orig: Refund, pub update_data: RefundUpdate, } + +#[derive(Debug, Serialize, Deserialize)] +pub struct PayoutsUpdateMems { + pub orig: Payouts, + pub update_data: PayoutsUpdate, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PayoutAttemptUpdateMems { + pub orig: PayoutAttempt, + pub update_data: PayoutAttemptUpdate, +} diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 44e4d17453..c93306f49d 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -55,7 +55,7 @@ pub use self::{ address::*, api_keys::*, cards_info::*, configs::*, customers::*, dispute::*, ephemeral_key::*, events::*, file::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, payment_attempt::*, payment_intent::*, payment_method::*, - process_tracker::*, refund::*, reverse_lookup::*, + payout_attempt::*, payouts::*, process_tracker::*, refund::*, reverse_lookup::*, }; /// The types and implementations provided by this module are required for the schema generated by diff --git a/crates/diesel_models/src/payout_attempt.rs b/crates/diesel_models/src/payout_attempt.rs index 014e6597c9..765cde503e 100644 --- a/crates/diesel_models/src/payout_attempt.rs +++ b/crates/diesel_models/src/payout_attempt.rs @@ -31,34 +31,6 @@ pub struct PayoutAttempt { pub routing_info: Option, } -impl Default for PayoutAttempt { - fn default() -> Self { - let now = common_utils::date_time::now(); - - Self { - payout_attempt_id: String::default(), - payout_id: String::default(), - customer_id: String::default(), - merchant_id: String::default(), - address_id: String::default(), - connector: None, - connector_payout_id: String::default(), - payout_token: None, - status: storage_enums::PayoutStatus::default(), - is_eligible: Some(true), - error_message: None, - error_code: None, - business_country: Some(storage_enums::CountryAlpha2::default()), - business_label: None, - created_at: now, - last_modified_at: now, - profile_id: String::default(), - merchant_connector_id: None, - routing_info: None, - } - } -} - #[derive( Clone, Debug, @@ -91,12 +63,12 @@ pub struct PayoutAttemptNew { pub created_at: Option, #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub last_modified_at: Option, - pub profile_id: Option, + pub profile_id: String, pub merchant_connector_id: Option, pub routing_info: Option, } -#[derive(Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum PayoutAttemptUpdate { StatusUpdate { connector_payout_id: String, @@ -104,16 +76,13 @@ pub enum PayoutAttemptUpdate { error_message: Option, error_code: Option, is_eligible: Option, - last_modified_at: Option, }, PayoutTokenUpdate { payout_token: String, - last_modified_at: Option, }, BusinessUpdate { business_country: Option, business_label: Option, - last_modified_at: Option, }, UpdateRouting { connector: String, @@ -121,7 +90,7 @@ pub enum PayoutAttemptUpdate { }, } -#[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] +#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] #[diesel(table_name = payout_attempt)] pub struct PayoutAttemptUpdateInternal { pub payout_token: Option, @@ -132,19 +101,33 @@ pub struct PayoutAttemptUpdateInternal { pub is_eligible: Option, pub business_country: Option, pub business_label: Option, - pub last_modified_at: Option, pub connector: Option, pub routing_info: Option, + pub last_modified_at: PrimitiveDateTime, +} + +impl Default for PayoutAttemptUpdateInternal { + fn default() -> Self { + Self { + payout_token: None, + connector_payout_id: None, + status: None, + error_message: None, + error_code: None, + is_eligible: None, + business_country: None, + business_label: None, + connector: None, + routing_info: None, + last_modified_at: common_utils::date_time::now(), + } + } } impl From for PayoutAttemptUpdateInternal { fn from(payout_update: PayoutAttemptUpdate) -> Self { match payout_update { - PayoutAttemptUpdate::PayoutTokenUpdate { - last_modified_at, - payout_token, - } => Self { - last_modified_at, + PayoutAttemptUpdate::PayoutTokenUpdate { payout_token } => Self { payout_token: Some(payout_token), ..Default::default() }, @@ -154,24 +137,20 @@ impl From for PayoutAttemptUpdateInternal { error_message, error_code, is_eligible, - last_modified_at, } => Self { connector_payout_id: Some(connector_payout_id), status: Some(status), error_message, error_code, is_eligible, - last_modified_at, ..Default::default() }, PayoutAttemptUpdate::BusinessUpdate { business_country, business_label, - last_modified_at, } => Self { business_country, business_label, - last_modified_at, ..Default::default() }, PayoutAttemptUpdate::UpdateRouting { @@ -185,3 +164,35 @@ impl From for PayoutAttemptUpdateInternal { } } } + +impl PayoutAttemptUpdate { + pub fn apply_changeset(self, source: PayoutAttempt) -> PayoutAttempt { + let PayoutAttemptUpdateInternal { + payout_token, + connector_payout_id, + status, + error_message, + error_code, + is_eligible, + business_country, + business_label, + connector, + routing_info, + last_modified_at, + } = self.into(); + PayoutAttempt { + payout_token: payout_token.or(source.payout_token), + connector_payout_id: connector_payout_id.unwrap_or(source.connector_payout_id), + status: status.unwrap_or(source.status), + error_message: error_message.or(source.error_message), + error_code: error_code.or(source.error_code), + is_eligible: is_eligible.or(source.is_eligible), + business_country: business_country.or(source.business_country), + business_label: business_label.or(source.business_label), + connector: connector.or(source.connector), + routing_info: routing_info.or(source.routing_info), + last_modified_at, + ..source + } + } +} diff --git a/crates/diesel_models/src/payouts.rs b/crates/diesel_models/src/payouts.rs index e8e7834714..aa3eabf6d4 100644 --- a/crates/diesel_models/src/payouts.rs +++ b/crates/diesel_models/src/payouts.rs @@ -30,33 +30,8 @@ pub struct Payouts { #[serde(with = "common_utils::custom_serde::iso8601")] pub last_modified_at: PrimitiveDateTime, pub attempt_count: i16, -} - -impl Default for Payouts { - fn default() -> Self { - let now = common_utils::date_time::now(); - - Self { - payout_id: String::default(), - merchant_id: String::default(), - customer_id: String::default(), - address_id: String::default(), - payout_type: storage_enums::PayoutType::default(), - payout_method_id: Option::default(), - amount: i64::default(), - destination_currency: storage_enums::Currency::default(), - source_currency: storage_enums::Currency::default(), - description: Option::default(), - recurring: bool::default(), - auto_fulfill: bool::default(), - return_url: None, - entity_type: storage_enums::PayoutEntityType::default(), - metadata: Option::default(), - created_at: now, - last_modified_at: now, - attempt_count: i16::default(), - } - } + pub profile_id: String, + pub status: storage_enums::PayoutStatus, } #[derive( @@ -92,10 +67,12 @@ pub struct PayoutsNew { pub created_at: Option, #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub last_modified_at: Option, + pub profile_id: String, + pub status: storage_enums::PayoutStatus, pub attempt_count: i16, } -#[derive(Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum PayoutsUpdate { Update { amount: i64, @@ -107,22 +84,21 @@ pub enum PayoutsUpdate { return_url: Option, entity_type: storage_enums::PayoutEntityType, metadata: Option, - last_modified_at: Option, + profile_id: Option, + status: Option, }, PayoutMethodIdUpdate { payout_method_id: Option, - last_modified_at: Option, }, RecurringUpdate { recurring: bool, - last_modified_at: Option, }, AttemptCountUpdate { attempt_count: i16, }, } -#[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] +#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] #[diesel(table_name = payouts)] pub struct PayoutsUpdateInternal { pub amount: Option, @@ -134,11 +110,34 @@ pub struct PayoutsUpdateInternal { pub return_url: Option, pub entity_type: Option, pub metadata: Option, - pub last_modified_at: Option, pub payout_method_id: Option, + pub profile_id: Option, + pub status: Option, + pub last_modified_at: PrimitiveDateTime, pub attempt_count: Option, } +impl Default for PayoutsUpdateInternal { + fn default() -> Self { + Self { + amount: None, + destination_currency: None, + source_currency: None, + description: None, + recurring: None, + auto_fulfill: None, + return_url: None, + entity_type: None, + metadata: None, + payout_method_id: None, + profile_id: None, + status: None, + last_modified_at: common_utils::date_time::now(), + attempt_count: None, + } + } +} + impl From for PayoutsUpdateInternal { fn from(payout_update: PayoutsUpdate) -> Self { match payout_update { @@ -152,7 +151,8 @@ impl From for PayoutsUpdateInternal { return_url, entity_type, metadata, - last_modified_at, + profile_id, + status, } => Self { amount: Some(amount), destination_currency: Some(destination_currency), @@ -163,22 +163,15 @@ impl From for PayoutsUpdateInternal { return_url, entity_type: Some(entity_type), metadata, - last_modified_at, + profile_id, + status, ..Default::default() }, - PayoutsUpdate::PayoutMethodIdUpdate { - last_modified_at, - payout_method_id, - } => Self { - last_modified_at, + PayoutsUpdate::PayoutMethodIdUpdate { payout_method_id } => Self { payout_method_id, ..Default::default() }, - PayoutsUpdate::RecurringUpdate { - last_modified_at, - recurring, - } => Self { - last_modified_at, + PayoutsUpdate::RecurringUpdate { recurring } => Self { recurring: Some(recurring), ..Default::default() }, @@ -189,3 +182,41 @@ impl From for PayoutsUpdateInternal { } } } + +impl PayoutsUpdate { + pub fn apply_changeset(self, source: Payouts) -> Payouts { + let PayoutsUpdateInternal { + amount, + destination_currency, + source_currency, + description, + recurring, + auto_fulfill, + return_url, + entity_type, + metadata, + payout_method_id, + profile_id, + status, + last_modified_at, + attempt_count, + } = self.into(); + Payouts { + amount: amount.unwrap_or(source.amount), + destination_currency: destination_currency.unwrap_or(source.destination_currency), + source_currency: source_currency.unwrap_or(source.source_currency), + description: description.or(source.description), + recurring: recurring.unwrap_or(source.recurring), + auto_fulfill: auto_fulfill.unwrap_or(source.auto_fulfill), + return_url: return_url.or(source.return_url), + entity_type: entity_type.unwrap_or(source.entity_type), + metadata: metadata.or(source.metadata), + payout_method_id: payout_method_id.or(source.payout_method_id), + profile_id: profile_id.unwrap_or(source.profile_id), + status: status.unwrap_or(source.status), + last_modified_at, + attempt_count: attempt_count.unwrap_or(source.attempt_count), + ..source + } + } +} diff --git a/crates/diesel_models/src/query/payout_attempt.rs b/crates/diesel_models/src/query/payout_attempt.rs index a2e70a4997..9129d06e59 100644 --- a/crates/diesel_models/src/query/payout_attempt.rs +++ b/crates/diesel_models/src/query/payout_attempt.rs @@ -18,6 +18,33 @@ impl PayoutAttemptNew { } impl PayoutAttempt { + pub async fn update_with_attempt_id( + self, + conn: &PgPooledConn, + payout_attempt_update: PayoutAttemptUpdate, + ) -> StorageResult { + match generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + dsl::payout_attempt_id + .eq(self.payout_attempt_id.to_owned()) + .and(dsl::merchant_id.eq(self.merchant_id.to_owned())), + PayoutAttemptUpdateInternal::from(payout_attempt_update), + ) + .await + { + Err(error) => match error.current_context() { + errors::DatabaseError::NoFieldsToUpdate => Ok(self), + _ => Err(error), + }, + result => result, + } + } + pub async fn find_by_merchant_id_payout_id( conn: &PgPooledConn, merchant_id: &str, diff --git a/crates/diesel_models/src/query/payouts.rs b/crates/diesel_models/src/query/payouts.rs index 2997584136..13756d19e8 100644 --- a/crates/diesel_models/src/query/payouts.rs +++ b/crates/diesel_models/src/query/payouts.rs @@ -1,6 +1,5 @@ use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; use error_stack::report; -use router_env::{instrument, tracing}; use super::generics; use crate::{ @@ -11,12 +10,35 @@ use crate::{ }; impl PayoutsNew { - #[instrument(skip(conn))] pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { generics::generic_insert(conn, self).await } } impl Payouts { + pub async fn update( + self, + conn: &PgPooledConn, + payout_update: PayoutsUpdate, + ) -> StorageResult { + match generics::generic_update_with_results::<::Table, _, _, _>( + conn, + dsl::payout_id + .eq(self.payout_id.to_owned()) + .and(dsl::merchant_id.eq(self.merchant_id.to_owned())), + PayoutsUpdateInternal::from(payout_update), + ) + .await + { + Err(error) => match error.current_context() { + errors::DatabaseError::NoFieldsToUpdate => Ok(self), + _ => Err(error), + }, + Ok(mut payouts) => payouts + .pop() + .ok_or(error_stack::report!(errors::DatabaseError::NotFound)), + } + } + pub async fn find_by_merchant_id_payout_id( conn: &PgPooledConn, merchant_id: &str, @@ -51,4 +73,18 @@ impl Payouts { report!(errors::DatabaseError::NotFound).attach_printable("Error while updating payout") }) } + + pub async fn find_optional_by_merchant_id_payout_id( + conn: &PgPooledConn, + merchant_id: &str, + payout_id: &str, + ) -> StorageResult> { + generics::generic_find_one_optional::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::payout_id.eq(payout_id.to_owned())), + ) + .await + } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 9f794b1ae3..70befe77e5 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -985,6 +985,9 @@ diesel::table! { created_at -> Timestamp, last_modified_at -> Timestamp, attempt_count -> Int2, + #[max_length = 64] + profile_id -> Varchar, + status -> PayoutStatus, } } diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml index 13296bcde6..1a7c007265 100644 --- a/crates/euclid_wasm/Cargo.toml +++ b/crates/euclid_wasm/Cargo.toml @@ -10,7 +10,7 @@ rust-version.workspace = true crate-type = ["cdylib"] [features] -default = ["connector_choice_bcompat","payouts", "connector_choice_mca_id"] +default = ["connector_choice_bcompat", "payouts", "connector_choice_mca_id"] release = ["connector_choice_bcompat", "connector_choice_mca_id"] connector_choice_bcompat = ["api_models/connector_choice_bcompat"] connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id", "kgraph_utils/connector_choice_mca_id"] @@ -18,7 +18,7 @@ dummy_connector = ["kgraph_utils/dummy_connector", "connector_configs/dummy_conn production = ["connector_configs/production"] development = ["connector_configs/development"] sandbox = ["connector_configs/sandbox"] -payouts = [] +payouts = ["api_models/payouts"] [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } diff --git a/crates/openapi/Cargo.toml b/crates/openapi/Cargo.toml index 236916862d..20d35a8a3d 100644 --- a/crates/openapi/Cargo.toml +++ b/crates/openapi/Cargo.toml @@ -11,4 +11,4 @@ license.workspace = true utoipa = { version = "3.3.0", features = ["preserve_order", "time"] } serde_json = "1.0.96" -api_models = { version = "0.1.0", path = "../api_models", features = ["openapi"] } +api_models = { version = "0.1.0", path = "../api_models", features = ["frm", "payouts", "openapi"] } diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 82d3821a48..629dcc86d4 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -11,7 +11,7 @@ license.workspace = true [features] default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "payout_retry", "business_profile_routing", "connector_choice_mca_id", "profile_specific_fallback_routing", "retry", "frm"] email = ["external_services/email", "scheduler/email", "olap"] -frm = [] +frm = ["api_models/frm"] stripe = ["dep:serde_qs"] release = ["stripe", "email", "backwards_compatibility", "business_profile_routing", "accounts_cache", "kv_store", "connector_choice_mca_id", "profile_specific_fallback_routing", "vergen", "recon", "external_services/aws_kms", "external_services/aws_s3"] olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap", "api_models/olap", "dep:analytics"] @@ -26,7 +26,12 @@ dummy_connector = ["api_models/dummy_connector", "euclid/dummy_connector", "kgra connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id", "kgraph_utils/connector_choice_mca_id"] external_access_dc = ["dummy_connector"] detailed_errors = ["api_models/detailed_errors", "error-stack/serde"] -payouts = ["common_enums/payouts"] +payouts = [ + "api_models/payouts", + "common_enums/payouts", + "data_models/payouts", + "storage_impl/payouts" +] payout_retry = ["payouts"] recon = ["email", "api_models/recon"] retry = [] diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 0eaabf8652..2f546cfa5a 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -206,7 +206,10 @@ pub async fn create_merchant_account( modified_at: date_time::now(), intent_fulfillment_time: None, frm_routing_algorithm: req.frm_routing_algorithm, + #[cfg(feature = "payouts")] payout_routing_algorithm: req.payout_routing_algorithm, + #[cfg(not(feature = "payouts"))] + payout_routing_algorithm: None, id: None, organization_id, is_recon_enabled: false, @@ -432,6 +435,7 @@ pub async fn update_business_profile_cascade( routing_algorithm: None, intent_fulfillment_time: None, frm_routing_algorithm: None, + #[cfg(feature = "payouts")] payout_routing_algorithm: None, applepay_verified_domains: None, payment_link_config: None, @@ -589,7 +593,10 @@ pub async fn merchant_account_update( primary_business_details, frm_routing_algorithm: req.frm_routing_algorithm, intent_fulfillment_time: None, + #[cfg(feature = "payouts")] payout_routing_algorithm: req.payout_routing_algorithm, + #[cfg(not(feature = "payouts"))] + payout_routing_algorithm: None, default_profile: business_profile_id_update, payment_link_config: None, }; @@ -1661,7 +1668,10 @@ pub async fn update_business_profile( routing_algorithm: request.routing_algorithm, intent_fulfillment_time: request.intent_fulfillment_time.map(i64::from), frm_routing_algorithm: request.frm_routing_algorithm, + #[cfg(feature = "payouts")] payout_routing_algorithm: request.payout_routing_algorithm, + #[cfg(not(feature = "payouts"))] + payout_routing_algorithm: None, is_recon_enabled: None, applepay_verified_domains: request.applepay_verified_domains, payment_link_config, diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs index c5ddb1ed6b..53f503e871 100644 --- a/crates/router/src/core/locker_migration.rs +++ b/crates/router/src/core/locker_migration.rs @@ -121,7 +121,9 @@ pub async fn call_to_locker( payment_method_issuer: pm.payment_method_issuer, payment_method_issuer_code: pm.payment_method_issuer_code, card: Some(card_details.clone()), + #[cfg(feature = "payouts")] wallet: None, + #[cfg(feature = "payouts")] bank_transfer: None, metadata: pm.metadata, customer_id: Some(pm.customer_id), diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 46e26cbcde..26f6d6db19 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -3,11 +3,10 @@ pub mod surcharge_decision_configs; pub mod transformers; pub mod vault; +pub use api_models::enums::Connector; use api_models::payments::CardToken; -pub use api_models::{ - enums::{Connector, PayoutConnectors}, - payouts as payout_types, -}; +#[cfg(feature = "payouts")] +pub use api_models::{enums::PayoutConnectors, payouts as payout_types}; pub use common_utils::request::RequestBody; use data_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; use diesel_models::enums; diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 593e7438ee..289654f93e 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -65,7 +65,7 @@ use crate::{ self, types::{decrypt, encrypt_optional, AsyncLift}, }, - storage::{self, enums, PaymentTokenData}, + storage::{self, enums, PaymentMethodListContext, PaymentTokenData}, transformers::ForeignFrom, }, utils::{self, ConnectorResponseExt, OptionExt}, @@ -140,6 +140,7 @@ pub fn store_default_payment_method( payment_method_id: pm_id, payment_method: req.payment_method, payment_method_type: req.payment_method_type, + #[cfg(feature = "payouts")] bank_transfer: None, card: None, metadata: req.metadata.clone(), @@ -226,6 +227,7 @@ pub async fn add_payment_method( let customer_id = req.customer_id.clone().get_required_value("customer_id")?; let response = match req.payment_method { + #[cfg(feature = "payouts")] api_enums::PaymentMethod::BankTransfer => match req.bank_transfer.clone() { Some(bank) => add_bank_to_locker( &state, @@ -460,8 +462,10 @@ pub async fn update_customer_payment_method( payment_method_type: pm.payment_method_type, payment_method_issuer: pm.payment_method_issuer, payment_method_issuer_code: pm.payment_method_issuer_code, + #[cfg(feature = "payouts")] bank_transfer: req.bank_transfer, card: req.card, + #[cfg(feature = "payouts")] wallet: req.wallet, metadata: req.metadata, customer_id: Some(pm.customer_id), @@ -481,6 +485,7 @@ pub async fn update_customer_payment_method( // Wrapper function to switch lockers +#[cfg(feature = "payouts")] pub async fn add_bank_to_locker( state: &routes::AppState, req: api::PaymentMethodCreate, @@ -2794,46 +2799,26 @@ pub async fn list_customer_payment_method( for pm in resp.into_iter() { let parent_payment_method_token = generate_id(consts::ID_LENGTH, "token"); - let (card, pmd, hyperswitch_token_data) = match pm.payment_method { + let payment_method_retrieval_context = match pm.payment_method { enums::PaymentMethod::Card => { let card_details = get_card_details_with_locker_fallback(&pm, key, state).await?; if card_details.is_some() { - ( + PaymentMethodListContext { card_details, - None, - PaymentTokenData::permanent_card( + #[cfg(feature = "payouts")] + bank_transfer_details: None, + hyperswitch_token_data: PaymentTokenData::permanent_card( Some(pm.payment_method_id.clone()), pm.locker_id.clone().or(Some(pm.payment_method_id.clone())), pm.locker_id.clone().unwrap_or(pm.payment_method_id.clone()), ), - ) + } } else { continue; } } - #[cfg(feature = "payouts")] - enums::PaymentMethod::BankTransfer => { - let token = generate_id(consts::ID_LENGTH, "token"); - let token_data = PaymentTokenData::temporary_generic(token.clone()); - ( - None, - Some( - get_bank_from_hs_locker( - state, - &key_store, - &token, - &pm.customer_id, - &pm.merchant_id, - pm.locker_id.as_ref().unwrap_or(&pm.payment_method_id), - ) - .await?, - ), - token_data, - ) - } - enums::PaymentMethod::BankDebit => { // Retrieve the pm_auth connector details so that it can be tokenized let bank_account_token_data = get_bank_account_connector_details(&pm, key) @@ -2844,23 +2829,55 @@ pub async fn list_customer_payment_method( }); if let Some(data) = bank_account_token_data { let token_data = PaymentTokenData::AuthBankDebit(data); - (None, None, token_data) + + PaymentMethodListContext { + card_details: None, + #[cfg(feature = "payouts")] + bank_transfer_details: None, + hyperswitch_token_data: token_data, + } } else { continue; } } - enums::PaymentMethod::Wallet => ( - None, - None, - PaymentTokenData::wallet_token(pm.payment_method_id.clone()), - ), + enums::PaymentMethod::Wallet => PaymentMethodListContext { + card_details: None, + #[cfg(feature = "payouts")] + bank_transfer_details: None, + hyperswitch_token_data: PaymentTokenData::wallet_token( + pm.payment_method_id.clone(), + ), + }, - _ => ( - None, - None, - PaymentTokenData::temporary_generic(generate_id(consts::ID_LENGTH, "token")), - ), + #[cfg(feature = "payouts")] + enums::PaymentMethod::BankTransfer => PaymentMethodListContext { + card_details: None, + bank_transfer_details: Some( + get_bank_from_hs_locker( + state, + &key_store, + &parent_payment_method_token, + &pm.customer_id, + &pm.merchant_id, + pm.locker_id.as_ref().unwrap_or(&pm.payment_method_id), + ) + .await?, + ), + hyperswitch_token_data: PaymentTokenData::temporary_generic( + parent_payment_method_token.clone(), + ), + }, + + _ => PaymentMethodListContext { + card_details: None, + #[cfg(feature = "payouts")] + bank_transfer_details: None, + hyperswitch_token_data: PaymentTokenData::temporary_generic(generate_id( + consts::ID_LENGTH, + "token", + )), + }, }; // Retrieve the masked bank details to be sent as a response @@ -2884,14 +2901,15 @@ pub async fn list_customer_payment_method( payment_method: pm.payment_method, payment_method_type: pm.payment_method_type, payment_method_issuer: pm.payment_method_issuer, - card, + card: payment_method_retrieval_context.card_details, metadata: pm.metadata, payment_method_issuer_code: pm.payment_method_issuer_code, recurring_enabled: false, installment_payment_enabled: false, payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), created: Some(pm.created_at), - bank_transfer: pmd, + #[cfg(feature = "payouts")] + bank_transfer: payment_method_retrieval_context.bank_transfer_details, bank: bank_details, surcharge_details: None, requires_cvv: requires_cvv @@ -2913,7 +2931,11 @@ pub async fn list_customer_payment_method( &parent_payment_method_token, pma.payment_method, )) - .insert(intent_created, hyperswitch_token_data, state) + .insert( + intent_created, + payment_method_retrieval_context.hyperswitch_token_data, + state, + ) .await?; if let Some(metadata) = pma.metadata { @@ -3455,6 +3477,7 @@ pub async fn retrieve_payment_method( payment_method_id: pm.payment_method_id, payment_method: pm.payment_method, payment_method_type: pm.payment_method_type, + #[cfg(feature = "payouts")] bank_transfer: None, card, metadata: pm.metadata, diff --git a/crates/router/src/core/payment_methods/transformers.rs b/crates/router/src/core/payment_methods/transformers.rs index 35491d747e..f639aa5921 100644 --- a/crates/router/src/core/payment_methods/transformers.rs +++ b/crates/router/src/core/payment_methods/transformers.rs @@ -309,6 +309,7 @@ pub async fn mk_add_locker_request_hs<'a>( Ok(request) } +#[cfg(feature = "payouts")] pub fn mk_add_bank_response_hs( bank: api::BankPayout, bank_reference: String, @@ -365,6 +366,7 @@ pub fn mk_add_card_response_hs( payment_method_id: card_reference, payment_method: req.payment_method, payment_method_type: req.payment_method_type, + #[cfg(feature = "payouts")] bank_transfer: None, card: Some(card), metadata: req.metadata, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 6a54a8eea3..f734d9825a 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1118,7 +1118,9 @@ pub(crate) async fn get_payment_method_create_request( payment_method_type, payment_method_issuer: card.card_issuer.clone(), payment_method_issuer_code: None, + #[cfg(feature = "payouts")] bank_transfer: None, + #[cfg(feature = "payouts")] wallet: None, card: Some(card_detail), metadata: None, @@ -1136,7 +1138,9 @@ pub(crate) async fn get_payment_method_create_request( payment_method_type, payment_method_issuer: None, payment_method_issuer_code: None, + #[cfg(feature = "payouts")] bank_transfer: None, + #[cfg(feature = "payouts")] wallet: None, card: None, metadata: None, diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 4628f027a0..10e39b2d1b 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -473,6 +473,7 @@ async fn skip_saving_card_in_locker( payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), metadata: None, created: Some(common_utils::date_time::now()), + #[cfg(feature = "payouts")] bank_transfer: None, last_used_at: Some(common_utils::date_time::now()), }; @@ -493,6 +494,7 @@ async fn skip_saving_card_in_locker( recurring_enabled: false, installment_payment_enabled: false, payment_experience: Some(vec![api_models::enums::PaymentExperience::RedirectToUrl]), + #[cfg(feature = "payouts")] bank_transfer: None, last_used_at: Some(common_utils::date_time::now()), }; @@ -534,6 +536,7 @@ pub async fn save_in_locker( payment_method_id: pm_id, payment_method: payment_method_request.payment_method, payment_method_type: payment_method_request.payment_method_type, + #[cfg(feature = "payouts")] bank_transfer: None, card: None, metadata: None, diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index 7f124a6914..b59ed10a49 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -26,13 +26,11 @@ use crate::{ api::{self, payouts}, domain, storage::{self, PaymentRoutingInfo}, - transformers::ForeignTryInto, }, utils::{self, OptionExt}, }; // ********************************************** TYPES ********************************************** -#[cfg(feature = "payouts")] #[derive(Clone)] pub struct PayoutData { pub billing_address: Option, @@ -64,12 +62,12 @@ pub async fn get_connector_choice( connector: Option, routing_algorithm: Option, payout_data: &mut PayoutData, - eligible_connectors: Option>, + eligible_connectors: Option>, ) -> RouterResult { let eligible_routable_connectors = eligible_connectors.map(|connectors| { connectors .into_iter() - .flat_map(|c| c.foreign_try_into()) + .map(api::enums::RoutableConnectors::from) .collect() }); let connector_choice = helpers::get_default_payout_connector(state, routing_algorithm).await?; @@ -249,7 +247,6 @@ pub async fn make_connector_decision( } } -#[cfg(feature = "payouts")] #[instrument(skip_all)] pub async fn payouts_create_core( state: AppState, @@ -303,13 +300,13 @@ pub async fn payouts_create_core( .await } -#[cfg(feature = "payouts")] pub async fn payouts_update_core( state: AppState, merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: payouts::PayoutCreateRequest, ) -> RouterResponse { + let payout_id = req.payout_id.clone().get_required_value("payout_id")?; let mut payout_data = make_payout_data( &state, &merchant_account, @@ -326,7 +323,7 @@ pub async fn payouts_update_core( return Err(report!(errors::ApiErrorResponse::InvalidRequestData { message: format!( "Payout {} cannot be updated for status {}", - payout_attempt.payout_id, status + payout_id, status ), })); } @@ -337,27 +334,23 @@ pub async fn payouts_update_core( amount: req.amount.unwrap_or(payouts.amount.into()).into(), destination_currency: req.currency.unwrap_or(payouts.destination_currency), source_currency: req.currency.unwrap_or(payouts.source_currency), - description: req.description.clone().or(payouts.description), + description: req.description.clone().or(payouts.description.clone()), recurring: req.recurring.unwrap_or(payouts.recurring), auto_fulfill: req.auto_fulfill.unwrap_or(payouts.auto_fulfill), - return_url: req.return_url.clone().or(payouts.return_url), + return_url: req.return_url.clone().or(payouts.return_url.clone()), entity_type: req.entity_type.unwrap_or(payouts.entity_type), - metadata: req.metadata.clone().or(payouts.metadata), - last_modified_at: Some(common_utils::date_time::now()), + metadata: req.metadata.clone().or(payouts.metadata.clone()), + status: Some(status), + profile_id: Some(payout_attempt.profile_id), }; let db = &*state.store; - let payout_id = req.payout_id.clone().get_required_value("payout_id")?; - let payout_attempt_id = - utils::get_payment_attempt_id(payout_id.to_owned(), payouts.attempt_count); - let merchant_id = &merchant_account.merchant_id; payout_data.payouts = db - .update_payout_by_merchant_id_payout_id(merchant_id, &payout_id, updated_payouts) + .update_payout(&payouts, updated_payouts, merchant_account.storage_scheme) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error updating payouts")?; - let payout_attempt = payout_data.payout_attempt.to_owned(); let updated_business_country = payout_attempt .business_country @@ -377,16 +370,16 @@ pub async fn payouts_update_core( match (updated_business_country, updated_business_label) { (None, None) => {} (business_country, business_label) => { - let update_payout_attempt = storage::PayoutAttemptUpdate::BusinessUpdate { + let payout_attempt = payout_data.payout_attempt; + let updated_payout_attempt = storage::PayoutAttemptUpdate::BusinessUpdate { business_country, business_label, - last_modified_at: Some(common_utils::date_time::now()), }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_attempt_id( - merchant_id, - &payout_attempt_id, - update_payout_attempt, + .update_payout_attempt( + &payout_attempt, + updated_payout_attempt, + merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -411,9 +404,10 @@ pub async fn payouts_update_core( payout_data.payout_attempt.payout_token.as_deref(), &payout_data.payout_attempt.customer_id, &payout_data.payout_attempt.merchant_id, - &payout_data.payout_attempt.payout_id, Some(&payouts.payout_type), &key_store, + Some(&payout_data), + merchant_account.storage_scheme, ) .await? .get_required_value("payout_method_data")?, @@ -450,7 +444,6 @@ pub async fn payouts_update_core( .await } -#[cfg(feature = "payouts")] #[instrument(skip_all)] pub async fn payouts_retrieve_core( state: AppState, @@ -475,7 +468,6 @@ pub async fn payouts_retrieve_core( .await } -#[cfg(feature = "payouts")] #[instrument(skip_all)] pub async fn payouts_cancel_core( state: AppState, @@ -512,14 +504,13 @@ pub async fn payouts_cancel_core( error_message: Some("Cancelled by user".to_string()), error_code: None, is_eligible: None, - last_modified_at: Some(common_utils::date_time::now()), }; payout_data.payout_attempt = state .store - .update_payout_attempt_by_merchant_id_payout_id( - &merchant_account.merchant_id, - &payout_attempt.payout_id, + .update_payout_attempt( + &payout_attempt, updated_payout_attempt, + merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -568,7 +559,6 @@ pub async fn payouts_cancel_core( .await } -#[cfg(feature = "payouts")] #[instrument(skip_all)] pub async fn payouts_fulfill_core( state: AppState, @@ -627,9 +617,10 @@ pub async fn payouts_fulfill_core( payout_attempt.payout_token.as_deref(), &payout_attempt.customer_id, &payout_attempt.merchant_id, - &payout_attempt.payout_id, Some(&payout_data.payouts.payout_type), &key_store, + Some(&payout_data), + merchant_account.storage_scheme, ) .await? .get_required_value("payout_method_data")?, @@ -663,7 +654,6 @@ pub async fn payouts_fulfill_core( } // ********************************************** HELPERS ********************************************** -#[cfg(feature = "payouts")] pub async fn call_connector_payout( state: &AppState, merchant_account: &domain::MerchantAccount, @@ -673,7 +663,7 @@ pub async fn call_connector_payout( payout_data: &mut PayoutData, ) -> RouterResult { let payout_attempt = &payout_data.payout_attempt.to_owned(); - let payouts: &diesel_models::payouts::Payouts = &payout_data.payouts.to_owned(); + let payouts = &payout_data.payouts.to_owned(); // update connector_name if payout_data.payout_attempt.connector.is_none() @@ -685,10 +675,10 @@ pub async fn call_connector_payout( routing_info: payout_data.payout_attempt.routing_info.clone(), }; let db = &*state.store; - db.update_payout_attempt_by_merchant_id_payout_attempt_id( - &payout_data.payout_attempt.merchant_id, - &payout_data.payout_attempt.payout_attempt_id, + db.update_payout_attempt( + &payout_data.payout_attempt, updated_payout_attempt, + merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -704,9 +694,10 @@ pub async fn call_connector_payout( payout_attempt.payout_token.as_deref(), &payout_attempt.customer_id, &payout_attempt.merchant_id, - &payout_attempt.payout_id, Some(&payouts.payout_type), key_store, + Some(payout_data), + merchant_account.storage_scheme, ) .await? .get_required_value("payout_method_data")?, @@ -807,7 +798,6 @@ pub async fn call_connector_payout( Ok(payout_data.to_owned()) } -#[cfg(feature = "payouts")] pub async fn create_recipient( state: &AppState, merchant_account: &domain::MerchantAccount, @@ -892,7 +882,6 @@ pub async fn create_recipient( Ok(payout_data.clone()) } -#[cfg(feature = "payouts")] pub async fn check_payout_eligibility( state: &AppState, merchant_account: &domain::MerchantAccount, @@ -933,31 +922,24 @@ pub async fn check_payout_eligibility( // 4. Process data returned by the connector let db = &*state.store; - let merchant_id = &merchant_account.merchant_id; - let payout_id = &payout_data.payouts.payout_id; - let payout_attempt_id = - &utils::get_payment_attempt_id(payout_id, payout_data.payouts.attempt_count); - match router_data_resp.response { Ok(payout_response_data) => { let payout_attempt = &payout_data.payout_attempt; let status = payout_response_data .status .unwrap_or(payout_attempt.status.to_owned()); - let updated_payout_attempt = - storage::payout_attempt::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: payout_response_data.connector_payout_id, - status, - error_code: None, - error_message: None, - is_eligible: payout_response_data.payout_eligible, - last_modified_at: Some(common_utils::date_time::now()), - }; + let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { + connector_payout_id: payout_response_data.connector_payout_id, + status, + error_code: None, + error_message: None, + is_eligible: payout_response_data.payout_eligible, + }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_attempt_id( - merchant_id, - payout_attempt_id, + .update_payout_attempt( + payout_attempt, updated_payout_attempt, + merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -971,20 +953,18 @@ pub async fn check_payout_eligibility( } } Err(err) => { - let updated_payout_attempt = - storage::payout_attempt::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: String::default(), - status: storage_enums::PayoutStatus::Failed, - error_code: Some(err.code), - error_message: Some(err.message), - is_eligible: None, - last_modified_at: Some(common_utils::date_time::now()), - }; + let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { + connector_payout_id: String::default(), + status: storage_enums::PayoutStatus::Failed, + error_code: Some(err.code), + error_message: Some(err.message), + is_eligible: Some(false), + }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_attempt_id( - merchant_id, - payout_attempt_id, + .update_payout_attempt( + &payout_data.payout_attempt, updated_payout_attempt, + merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -995,7 +975,6 @@ pub async fn check_payout_eligibility( Ok(payout_data.clone()) } -#[cfg(feature = "payouts")] pub async fn create_payout( state: &AppState, merchant_account: &domain::MerchantAccount, @@ -1042,31 +1021,24 @@ pub async fn create_payout( // 5. Process data returned by the connector let db = &*state.store; - let merchant_id = &merchant_account.merchant_id; - let payout_id = &payout_data.payouts.payout_id; - let payout_attempt_id = - &utils::get_payment_attempt_id(payout_id, payout_data.payouts.attempt_count); - match router_data_resp.response { Ok(payout_response_data) => { let payout_attempt = &payout_data.payout_attempt; let status = payout_response_data .status .unwrap_or(payout_attempt.status.to_owned()); - let updated_payout_attempt = - storage::payout_attempt::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: payout_response_data.connector_payout_id, - status, - error_code: None, - error_message: None, - is_eligible: payout_response_data.payout_eligible, - last_modified_at: Some(common_utils::date_time::now()), - }; + let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { + connector_payout_id: payout_response_data.connector_payout_id, + status, + error_code: None, + error_message: None, + is_eligible: payout_response_data.payout_eligible, + }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_attempt_id( - merchant_id, - payout_attempt_id, + .update_payout_attempt( + payout_attempt, updated_payout_attempt, + merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -1080,20 +1052,18 @@ pub async fn create_payout( } } Err(err) => { - let updated_payout_attempt = - storage::payout_attempt::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: String::default(), - status: storage_enums::PayoutStatus::Failed, - error_code: Some(err.code), - error_message: Some(err.message), - is_eligible: None, - last_modified_at: Some(common_utils::date_time::now()), - }; + let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { + connector_payout_id: String::default(), + status: storage_enums::PayoutStatus::Failed, + error_code: Some(err.code), + error_message: Some(err.message), + is_eligible: None, + }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_attempt_id( - merchant_id, - payout_attempt_id, + .update_payout_attempt( + &payout_data.payout_attempt, updated_payout_attempt, + merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -1104,7 +1074,6 @@ pub async fn create_payout( Ok(payout_data.clone()) } -#[cfg(feature = "payouts")] pub async fn cancel_payout( state: &AppState, merchant_account: &domain::MerchantAccount, @@ -1145,47 +1114,41 @@ pub async fn cancel_payout( // 4. Process data returned by the connector let db = &*state.store; - let merchant_id = &merchant_account.merchant_id; - let payout_id = &payout_data.payout_attempt.payout_id; match router_data_resp.response { Ok(payout_response_data) => { let status = payout_response_data .status .unwrap_or(payout_data.payout_attempt.status.to_owned()); - let updated_payout_attempt = - storage::payout_attempt::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: payout_response_data.connector_payout_id, - status, - error_code: None, - error_message: None, - is_eligible: payout_response_data.payout_eligible, - last_modified_at: Some(common_utils::date_time::now()), - }; + let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { + connector_payout_id: payout_response_data.connector_payout_id, + status, + error_code: None, + error_message: None, + is_eligible: payout_response_data.payout_eligible, + }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_id( - merchant_id, - payout_id, + .update_payout_attempt( + &payout_data.payout_attempt, updated_payout_attempt, + merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error updating payout_attempt in db")? } Err(err) => { - let updated_payouts_create = - storage::payout_attempt::PayoutAttemptUpdate::StatusUpdate { - connector_payout_id: String::default(), - status: storage_enums::PayoutStatus::Failed, - error_code: Some(err.code), - error_message: Some(err.message), - is_eligible: None, - last_modified_at: Some(common_utils::date_time::now()), - }; + let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { + connector_payout_id: String::default(), + status: storage_enums::PayoutStatus::Failed, + error_code: Some(err.code), + error_message: Some(err.message), + is_eligible: None, + }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_id( - merchant_id, - payout_id, - updated_payouts_create, + .update_payout_attempt( + &payout_data.payout_attempt, + updated_payout_attempt, + merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -1196,7 +1159,6 @@ pub async fn cancel_payout( Ok(payout_data.clone()) } -#[cfg(feature = "payouts")] pub async fn fulfill_payout( state: &AppState, merchant_account: &domain::MerchantAccount, @@ -1237,18 +1199,17 @@ pub async fn fulfill_payout( // 4. Process data returned by the connector let db = &*state.store; - let merchant_id = &merchant_account.merchant_id; let payout_attempt = &payout_data.payout_attempt; - let payout_id = &payout_attempt.payout_id; - let payout_attempt_id = - &utils::get_payment_attempt_id(payout_id, payout_data.payouts.attempt_count); - match router_data_resp.response { Ok(payout_response_data) => { + let status = payout_response_data + .status + .unwrap_or(payout_attempt.status.to_owned()); + payout_data.payouts.status = status; if payout_data.payouts.recurring && payout_data.payouts.payout_method_id.is_none() { helpers::save_payout_data_to_locker( state, - payout_attempt, + payout_data, &payout_data .payout_method_data .clone() @@ -1258,22 +1219,18 @@ pub async fn fulfill_payout( ) .await?; } - let status = payout_response_data - .status - .unwrap_or(payout_attempt.status.to_owned()); - let updated_payouts = storage::payout_attempt::PayoutAttemptUpdate::StatusUpdate { + let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { connector_payout_id: payout_attempt.connector_payout_id.to_owned(), status, error_code: None, error_message: None, is_eligible: payout_response_data.payout_eligible, - last_modified_at: Some(common_utils::date_time::now()), }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_attempt_id( - merchant_id, - payout_attempt_id, - updated_payouts, + .update_payout_attempt( + payout_attempt, + updated_payout_attempt, + merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -1287,19 +1244,18 @@ pub async fn fulfill_payout( } } Err(err) => { - let updated_payouts = storage::payout_attempt::PayoutAttemptUpdate::StatusUpdate { + let updated_payout_attempt = storage::PayoutAttemptUpdate::StatusUpdate { connector_payout_id: String::default(), status: storage_enums::PayoutStatus::Failed, error_code: Some(err.code), error_message: Some(err.message), is_eligible: None, - last_modified_at: Some(common_utils::date_time::now()), }; payout_data.payout_attempt = db - .update_payout_attempt_by_merchant_id_payout_attempt_id( - merchant_id, - payout_attempt_id, - updated_payouts, + .update_payout_attempt( + &payout_data.payout_attempt, + updated_payout_attempt, + merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -1310,7 +1266,6 @@ pub async fn fulfill_payout( Ok(payout_data.clone()) } -#[cfg(feature = "payouts")] pub async fn response_handler( _state: &AppState, merchant_account: &domain::MerchantAccount, @@ -1383,7 +1338,6 @@ pub async fn response_handler( // DB entries #[allow(clippy::too_many_arguments)] -#[cfg(feature = "payouts")] pub async fn payout_create_db_entries( state: &AppState, merchant_account: &domain::MerchantAccount, @@ -1454,28 +1408,27 @@ pub async fn payout_create_db_entries( None }; - let payouts_req = storage::PayoutsNew::default() - .set_payout_id(payout_id.to_owned()) - .set_merchant_id(merchant_id.to_owned()) - .set_customer_id(customer_id.to_owned()) - .set_address_id(address_id.to_owned()) - .set_payout_type(payout_type) - .set_amount(req.amount.unwrap_or(api::Amount::Zero).into()) - .set_destination_currency(currency) - .set_source_currency(currency) - .set_description(req.description.to_owned()) - .set_recurring(req.recurring.unwrap_or(false)) - .set_auto_fulfill(req.auto_fulfill.unwrap_or(false)) - .set_return_url(req.return_url.to_owned()) - .set_entity_type(req.entity_type.unwrap_or_default()) - .set_metadata(req.metadata.to_owned()) - .set_created_at(Some(common_utils::date_time::now())) - .set_last_modified_at(Some(common_utils::date_time::now())) - .set_payout_method_id(payout_method_id) - .set_attempt_count(1) - .to_owned(); + let payouts_req = storage::PayoutsNew { + payout_id: payout_id.to_string(), + merchant_id: merchant_id.to_string(), + customer_id: customer_id.to_owned(), + address_id: address_id.to_owned(), + payout_type, + amount: req.amount.unwrap_or(api::Amount::Zero).into(), + destination_currency: currency, + source_currency: currency, + description: req.description.to_owned(), + recurring: req.recurring.unwrap_or(false), + auto_fulfill: req.auto_fulfill.unwrap_or(false), + return_url: req.return_url.to_owned(), + entity_type: req.entity_type.unwrap_or_default(), + payout_method_id, + profile_id: profile_id.to_string(), + attempt_count: 1, + ..Default::default() + }; let payouts = db - .insert_payout(payouts_req) + .insert_payout(payouts_req, merchant_account.storage_scheme) .await .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayout { payout_id: payout_id.to_owned(), @@ -1493,22 +1446,21 @@ pub async fn payout_create_db_entries( }; let payout_attempt_id = utils::get_payment_attempt_id(payout_id, 1); - let payout_attempt_req = storage::PayoutAttemptNew::default() - .set_payout_attempt_id(payout_attempt_id.to_string()) - .set_payout_id(payout_id.to_owned()) - .set_customer_id(customer_id.to_owned()) - .set_merchant_id(merchant_id.to_owned()) - .set_address_id(address_id.to_owned()) - .set_status(status) - .set_business_country(req.business_country.to_owned()) - .set_business_label(req.business_label.to_owned()) - .set_payout_token(req.payout_token.to_owned()) - .set_created_at(Some(common_utils::date_time::now())) - .set_last_modified_at(Some(common_utils::date_time::now())) - .set_profile_id(Some(profile_id.to_string())) - .to_owned(); + let payout_attempt_req = storage::PayoutAttemptNew { + payout_attempt_id: payout_attempt_id.to_string(), + payout_id: payout_id.to_owned(), + customer_id: customer_id.to_owned(), + merchant_id: merchant_id.to_owned(), + address_id: address_id.to_owned(), + status, + business_country: req.business_country.to_owned(), + business_label: req.business_label.to_owned(), + payout_token: req.payout_token.to_owned(), + profile_id: profile_id.to_string(), + ..Default::default() + }; let payout_attempt = db - .insert_payout_attempt(payout_attempt_req) + .insert_payout_attempt(payout_attempt_req, merchant_account.storage_scheme) .await .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayout { payout_id: payout_id.to_owned(), @@ -1536,7 +1488,6 @@ pub async fn payout_create_db_entries( }) } -#[cfg(feature = "payouts")] pub async fn make_payout_data( state: &AppState, merchant_account: &domain::MerchantAccount, @@ -1552,14 +1503,22 @@ pub async fn make_payout_data( }; let payouts = db - .find_payout_by_merchant_id_payout_id(merchant_id, &payout_id) + .find_payout_by_merchant_id_payout_id( + merchant_id, + &payout_id, + merchant_account.storage_scheme, + ) .await .to_not_found_response(errors::ApiErrorResponse::PayoutNotFound)?; let payout_attempt_id = utils::get_payment_attempt_id(payout_id, payouts.attempt_count); let payout_attempt = db - .find_payout_attempt_by_merchant_id_payout_attempt_id(merchant_id, &payout_attempt_id) + .find_payout_attempt_by_merchant_id_payout_attempt_id( + merchant_id, + &payout_attempt_id, + merchant_account.storage_scheme, + ) .await .to_not_found_response(errors::ApiErrorResponse::PayoutNotFound)?; diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 847c555a86..4235be570e 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -46,9 +46,10 @@ pub async fn make_payout_method_data<'a>( payout_token: Option<&str>, customer_id: &str, merchant_id: &str, - payout_id: &str, payout_type: Option<&api_enums::PayoutType>, merchant_key_store: &domain::MerchantKeyStore, + payout_data: Option<&PayoutData>, + storage_scheme: storage::enums::MerchantStorageScheme, ) -> RouterResult> { let db = &*state.store; let certain_payout_type = payout_type.get_required_value("payout_type")?.to_owned(); @@ -101,9 +102,13 @@ pub async fn make_payout_method_data<'a>( None }; - match (payout_method_data.to_owned(), hyperswitch_token) { + match ( + payout_method_data.to_owned(), + hyperswitch_token, + payout_data, + ) { // Get operation - (None, Some(payout_token)) => { + (None, Some(payout_token), _) => { if payout_token.starts_with("temporary_token_") || certain_payout_type == api_enums::PayoutType::Bank { @@ -146,7 +151,7 @@ pub async fn make_payout_method_data<'a>( } // Create / Update operation - (Some(payout_method), payout_token) => { + (Some(payout_method), payout_token, Some(payout_data)) => { let lookup_key = vault::Vault::store_payout_method_data_in_locker( state, payout_token.to_owned(), @@ -158,14 +163,13 @@ pub async fn make_payout_method_data<'a>( // Update payout_token in payout_attempt table if payout_token.is_none() { - let payout_update = storage::PayoutAttemptUpdate::PayoutTokenUpdate { + let updated_payout_attempt = storage::PayoutAttemptUpdate::PayoutTokenUpdate { payout_token: lookup_key, - last_modified_at: Some(common_utils::date_time::now()), }; - db.update_payout_attempt_by_merchant_id_payout_id( - merchant_id, - payout_id, - payout_update, + db.update_payout_attempt( + &payout_data.payout_attempt, + updated_payout_attempt, + storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) @@ -181,11 +185,12 @@ pub async fn make_payout_method_data<'a>( pub async fn save_payout_data_to_locker( state: &AppState, - payout_attempt: &storage::payout_attempt::PayoutAttempt, + payout_data: &PayoutData, payout_method_data: &api::PayoutMethodData, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, ) -> RouterResult<()> { + let payout_attempt = &payout_data.payout_attempt; let (locker_req, card_details, bank_details, wallet_details, payment_method_type) = match payout_method_data { api_models::payouts::PayoutMethodData::Card(card) => { @@ -285,12 +290,11 @@ pub async fn save_payout_data_to_locker( let db = &*state.store; let updated_payout = storage::PayoutsUpdate::PayoutMethodIdUpdate { payout_method_id: Some(stored_resp.card_reference.to_owned()), - last_modified_at: Some(common_utils::date_time::now()), }; - db.update_payout_by_merchant_id_payout_id( - &merchant_account.merchant_id, - &payout_attempt.payout_id, + db.update_payout( + &payout_data.payouts, updated_payout, + merchant_account.storage_scheme, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/core/payouts/retry.rs b/crates/router/src/core/payouts/retry.rs index cb7f8bb784..a1a9d0b271 100644 --- a/crates/router/src/core/payouts/retry.rs +++ b/crates/router/src/core/payouts/retry.rs @@ -285,9 +285,12 @@ pub async fn modify_trackers( }; let payout_id = payouts.payout_id.clone(); - let merchant_id = &merchant_account.merchant_id; payout_data.payouts = db - .update_payout_by_merchant_id_payout_id(merchant_id, &payout_id.to_owned(), updated_payouts) + .update_payout( + &payout_data.payouts, + updated_payouts, + merchant_account.storage_scheme, + ) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error updating payouts")?; @@ -295,22 +298,21 @@ pub async fn modify_trackers( let payout_attempt_id = utils::get_payment_attempt_id(payout_id.to_owned(), payout_data.payouts.attempt_count); - let new_payout_attempt_req = storage::PayoutAttemptNew::default() - .set_payout_attempt_id(payout_attempt_id.to_string()) - .set_payout_id(payout_id.to_owned()) - .set_customer_id(payout_data.payout_attempt.customer_id.to_owned()) - .set_connector(Some(connector.connector_name.to_string())) - .set_merchant_id(payout_data.payout_attempt.merchant_id.to_owned()) - .set_address_id(payout_data.payout_attempt.address_id.to_owned()) - .set_business_country(payout_data.payout_attempt.business_country.to_owned()) - .set_business_label(payout_data.payout_attempt.business_label.to_owned()) - .set_payout_token(payout_data.payout_attempt.payout_token.to_owned()) - .set_created_at(Some(common_utils::date_time::now())) - .set_last_modified_at(Some(common_utils::date_time::now())) - .set_profile_id(Some(payout_data.payout_attempt.profile_id.to_string())) - .to_owned(); + let payout_attempt_req = storage::PayoutAttemptNew { + payout_attempt_id: payout_attempt_id.to_string(), + payout_id: payout_id.to_owned(), + customer_id: payout_data.payout_attempt.customer_id.to_owned(), + connector: Some(connector.connector_name.to_string()), + merchant_id: payout_data.payout_attempt.merchant_id.to_owned(), + address_id: payout_data.payout_attempt.address_id.to_owned(), + business_country: payout_data.payout_attempt.business_country.to_owned(), + business_label: payout_data.payout_attempt.business_label.to_owned(), + payout_token: payout_data.payout_attempt.payout_token.to_owned(), + profile_id: payout_data.payout_attempt.profile_id.to_string(), + ..Default::default() + }; payout_data.payout_attempt = db - .insert_payout_attempt(new_payout_attempt_req) + .insert_payout_attempt(payout_attempt_req, merchant_account.storage_scheme) .await .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayout { payout_id }) .attach_printable("Error inserting payouts in db")?; diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs index 90e3bca9de..8825835fc8 100644 --- a/crates/router/src/core/payouts/validator.rs +++ b/crates/router/src/core/payouts/validator.rs @@ -1,3 +1,4 @@ +pub use data_models::errors::StorageError; use error_stack::{report, ResultExt}; use router_env::{instrument, tracing}; @@ -13,35 +14,27 @@ use crate::{ utils, }; -#[cfg(feature = "payouts")] #[instrument(skip(db))] pub async fn validate_uniqueness_of_payout_id_against_merchant_id( db: &dyn StorageInterface, payout_id: &str, merchant_id: &str, + storage_scheme: storage::enums::MerchantStorageScheme, ) -> RouterResult> { - let payout = db - .find_payout_by_merchant_id_payout_id(merchant_id, payout_id) + let maybe_payouts = db + .find_optional_payout_by_merchant_id_payout_id(merchant_id, payout_id, storage_scheme) .await; - match payout { + match maybe_payouts { Err(err) => { - if err.current_context().is_db_not_found() { - // Empty vec should be returned by query in case of no results, this check exists just - // to be on the safer side. Fixed this, now vector is not returned but should check the flow in detail later. - Ok(None) - } else { - Err(err + let storage_err = err.current_context(); + match storage_err { + StorageError::ValueNotFound(_) => Ok(None), + _ => Err(err .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while finding payout_attempt, database error")) - } - } - Ok(payout) => { - if payout.payout_id == payout_id { - Ok(Some(payout)) - } else { - Ok(None) + .attach_printable("Failed while finding payout_attempt, database error")), } } + Ok(payout) => Ok(payout), } } @@ -49,7 +42,6 @@ pub async fn validate_uniqueness_of_payout_id_against_merchant_id( /// - merchant_id passed is same as the one in merchant_account table /// - payout_id is unique against merchant_id /// - payout_token provided is legitimate -#[cfg(feature = "payouts")] pub async fn validate_create_request( state: &AppState, merchant_account: &domain::MerchantAccount, @@ -71,18 +63,20 @@ pub async fn validate_create_request( // Payout ID let db: &dyn StorageInterface = &*state.store; let payout_id = core_utils::get_or_generate_uuid("payout_id", req.payout_id.as_ref())?; - match validate_uniqueness_of_payout_id_against_merchant_id(db, &payout_id, merchant_id) - .await - .change_context(errors::ApiErrorResponse::DuplicatePayout { - payout_id: payout_id.to_owned(), - }) - .attach_printable_lazy(|| { - format!( - "Unique violation while checking payout_id: {} against merchant_id: {}", - payout_id.to_owned(), - merchant_id - ) - })? { + match validate_uniqueness_of_payout_id_against_merchant_id( + db, + &payout_id, + merchant_id, + merchant_account.storage_scheme, + ) + .await + .attach_printable_lazy(|| { + format!( + "Unique violation while checking payout_id: {} against merchant_id: {}", + payout_id.to_owned(), + merchant_id + ) + })? { Some(_) => Err(report!(errors::ApiErrorResponse::DuplicatePayout { payout_id: payout_id.to_owned() })), @@ -99,9 +93,10 @@ pub async fn validate_create_request( Some(&payout_token), &customer_id, &merchant_account.merchant_id, - payout_id.as_ref(), req.payout_type.as_ref(), merchant_key_store, + None, + merchant_account.storage_scheme, ) .await? } diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index a6e15be0c0..9f9e61a5ff 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -28,8 +28,6 @@ pub mod merchant_key_store; pub mod organization; pub mod payment_link; pub mod payment_method; -pub mod payout_attempt; -pub mod payouts; pub mod refund; pub mod reverse_lookup; pub mod role; @@ -40,6 +38,10 @@ pub mod user_role; use data_models::payments::{ payment_attempt::PaymentAttemptInterface, payment_intent::PaymentIntentInterface, }; +#[cfg(feature = "payouts")] +use data_models::payouts::{payout_attempt::PayoutAttemptInterface, payouts::PayoutsInterface}; +#[cfg(not(feature = "payouts"))] +use data_models::{PayoutAttemptInterface, PayoutsInterface}; use diesel_models::{ fraud_check::{FraudCheck, FraudCheckNew, FraudCheckUpdate}, organization::{Organization, OrganizationNew, OrganizationUpdate}, @@ -94,8 +96,8 @@ pub trait StorageInterface: + blocklist::BlocklistInterface + blocklist_fingerprint::BlocklistFingerprintInterface + scheduler::SchedulerInterface - + payout_attempt::PayoutAttemptInterface - + payouts::PayoutsInterface + + PayoutAttemptInterface + + PayoutsInterface + refund::RefundInterface + reverse_lookup::ReverseLookupInterface + cards_info::CardsInfoInterface diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index c2159d5d8a..46b3313788 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -5,6 +5,10 @@ use common_utils::errors::CustomResult; use data_models::payments::{ payment_attempt::PaymentAttemptInterface, payment_intent::PaymentIntentInterface, }; +#[cfg(feature = "payouts")] +use data_models::payouts::{payout_attempt::PayoutAttemptInterface, payouts::PayoutsInterface}; +#[cfg(not(feature = "payouts"))] +use data_models::{PayoutAttemptInterface, PayoutsInterface}; use diesel_models::{ enums, enums::ProcessTrackerStatus, @@ -53,8 +57,6 @@ use crate::{ merchant_key_store::MerchantKeyStoreInterface, payment_link::PaymentLinkInterface, payment_method::PaymentMethodInterface, - payout_attempt::PayoutAttemptInterface, - payouts::PayoutsInterface, refund::RefundInterface, reverse_lookup::ReverseLookupInterface, routing_algorithm::RoutingAlgorithmInterface, @@ -1360,90 +1362,96 @@ impl PaymentMethodInterface for KafkaStore { } } +#[cfg(not(feature = "payouts"))] +impl PayoutAttemptInterface for KafkaStore {} + +#[cfg(feature = "payouts")] #[async_trait::async_trait] impl PayoutAttemptInterface for KafkaStore { - async fn find_payout_attempt_by_merchant_id_payout_id( - &self, - merchant_id: &str, - payout_id: &str, - ) -> CustomResult { - self.diesel_store - .find_payout_attempt_by_merchant_id_payout_id(merchant_id, payout_id) - .await - } - async fn find_payout_attempt_by_merchant_id_payout_attempt_id( &self, merchant_id: &str, payout_attempt_id: &str, - ) -> CustomResult { + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { self.diesel_store - .find_payout_attempt_by_merchant_id_payout_attempt_id(merchant_id, payout_attempt_id) - .await - } - - async fn update_payout_attempt_by_merchant_id_payout_id( - &self, - merchant_id: &str, - payout_id: &str, - payout: storage::PayoutAttemptUpdate, - ) -> CustomResult { - self.diesel_store - .update_payout_attempt_by_merchant_id_payout_id(merchant_id, payout_id, payout) - .await - } - - async fn update_payout_attempt_by_merchant_id_payout_attempt_id( - &self, - merchant_id: &str, - payout_attempt_id: &str, - payout: storage::PayoutAttemptUpdate, - ) -> CustomResult { - self.diesel_store - .update_payout_attempt_by_merchant_id_payout_attempt_id( + .find_payout_attempt_by_merchant_id_payout_attempt_id( merchant_id, payout_attempt_id, - payout, + storage_scheme, ) .await } + async fn update_payout_attempt( + &self, + this: &storage::PayoutAttempt, + payout_attempt_update: storage::PayoutAttemptUpdate, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .update_payout_attempt(this, payout_attempt_update, storage_scheme) + .await + } + async fn insert_payout_attempt( &self, - payout: storage::PayoutAttemptNew, - ) -> CustomResult { - self.diesel_store.insert_payout_attempt(payout).await + payout_attempt: storage::PayoutAttemptNew, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .insert_payout_attempt(payout_attempt, storage_scheme) + .await } } +#[cfg(not(feature = "payouts"))] +impl PayoutsInterface for KafkaStore {} + +#[cfg(feature = "payouts")] #[async_trait::async_trait] impl PayoutsInterface for KafkaStore { async fn find_payout_by_merchant_id_payout_id( &self, merchant_id: &str, payout_id: &str, - ) -> CustomResult { + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { self.diesel_store - .find_payout_by_merchant_id_payout_id(merchant_id, payout_id) + .find_payout_by_merchant_id_payout_id(merchant_id, payout_id, storage_scheme) .await } - async fn update_payout_by_merchant_id_payout_id( + async fn update_payout( &self, - merchant_id: &str, - payout_id: &str, - payout: storage::PayoutsUpdate, - ) -> CustomResult { + this: &storage::Payouts, + payout_update: storage::PayoutsUpdate, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { self.diesel_store - .update_payout_by_merchant_id_payout_id(merchant_id, payout_id, payout) + .update_payout(this, payout_update, storage_scheme) .await } async fn insert_payout( &self, payout: storage::PayoutsNew, - ) -> CustomResult { - self.diesel_store.insert_payout(payout).await + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .insert_payout(payout, storage_scheme) + .await + } + + async fn find_optional_payout_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::DataStorageError> { + self.diesel_store + .find_optional_payout_by_merchant_id_payout_id(merchant_id, payout_id, storage_scheme) + .await } } diff --git a/crates/router/src/db/payout_attempt.rs b/crates/router/src/db/payout_attempt.rs deleted file mode 100644 index 0e327eaa14..0000000000 --- a/crates/router/src/db/payout_attempt.rs +++ /dev/null @@ -1,172 +0,0 @@ -use error_stack::IntoReport; -use router_env::{instrument, tracing}; - -use super::{MockDb, Store}; -use crate::{ - connection, - core::errors::{self, CustomResult}, - types::storage, -}; - -#[async_trait::async_trait] -pub trait PayoutAttemptInterface { - async fn find_payout_attempt_by_merchant_id_payout_id( - &self, - _merchant_id: &str, - _payout_id: &str, - ) -> CustomResult; - - async fn find_payout_attempt_by_merchant_id_payout_attempt_id( - &self, - _merchant_id: &str, - _payout_attempt_id: &str, - ) -> CustomResult; - - async fn update_payout_attempt_by_merchant_id_payout_id( - &self, - _merchant_id: &str, - _payout_id: &str, - _payout: storage::PayoutAttemptUpdate, - ) -> CustomResult; - - async fn update_payout_attempt_by_merchant_id_payout_attempt_id( - &self, - _merchant_id: &str, - _payout_attempt_id: &str, - _payout: storage::PayoutAttemptUpdate, - ) -> CustomResult; - - async fn insert_payout_attempt( - &self, - _payout: storage::PayoutAttemptNew, - ) -> CustomResult; -} - -#[async_trait::async_trait] -impl PayoutAttemptInterface for Store { - #[instrument(skip_all)] - async fn find_payout_attempt_by_merchant_id_payout_id( - &self, - merchant_id: &str, - payout_id: &str, - ) -> CustomResult { - let conn = connection::pg_connection_read(self).await?; - storage::PayoutAttempt::find_by_merchant_id_payout_id(&conn, merchant_id, payout_id) - .await - .map_err(Into::into) - .into_report() - } - - #[instrument(skip_all)] - async fn find_payout_attempt_by_merchant_id_payout_attempt_id( - &self, - merchant_id: &str, - payout_attempt_id: &str, - ) -> CustomResult { - let conn = connection::pg_connection_read(self).await?; - storage::PayoutAttempt::find_by_merchant_id_payout_attempt_id( - &conn, - merchant_id, - payout_attempt_id, - ) - .await - .map_err(Into::into) - .into_report() - } - - #[instrument(skip_all)] - async fn update_payout_attempt_by_merchant_id_payout_id( - &self, - merchant_id: &str, - payout_id: &str, - payout: storage::PayoutAttemptUpdate, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - storage::PayoutAttempt::update_by_merchant_id_payout_id( - &conn, - merchant_id, - payout_id, - payout, - ) - .await - .map_err(Into::into) - .into_report() - } - - #[instrument(skip_all)] - async fn update_payout_attempt_by_merchant_id_payout_attempt_id( - &self, - merchant_id: &str, - payout_attempt_id: &str, - payout: storage::PayoutAttemptUpdate, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - storage::PayoutAttempt::update_by_merchant_id_payout_attempt_id( - &conn, - merchant_id, - payout_attempt_id, - payout, - ) - .await - .map_err(Into::into) - .into_report() - } - - #[instrument(skip_all)] - async fn insert_payout_attempt( - &self, - payout: storage::PayoutAttemptNew, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - payout.insert(&conn).await.map_err(Into::into).into_report() - } -} - -#[async_trait::async_trait] -impl PayoutAttemptInterface for MockDb { - async fn find_payout_attempt_by_merchant_id_payout_id( - &self, - _merchant_id: &str, - _payout_id: &str, - ) -> CustomResult { - // TODO: Implement function for `MockDb` - Err(errors::StorageError::MockDbError)? - } - - async fn find_payout_attempt_by_merchant_id_payout_attempt_id( - &self, - _merchant_id: &str, - _payout_attempt_id: &str, - ) -> CustomResult { - // TODO: Implement function for `MockDb` - Err(errors::StorageError::MockDbError)? - } - - async fn update_payout_attempt_by_merchant_id_payout_id( - &self, - _merchant_id: &str, - _payout_id: &str, - _payout: storage::PayoutAttemptUpdate, - ) -> CustomResult { - // TODO: Implement function for `MockDb` - Err(errors::StorageError::MockDbError)? - } - - async fn update_payout_attempt_by_merchant_id_payout_attempt_id( - &self, - _merchant_id: &str, - _payout_attempt_id: &str, - _payout: storage::PayoutAttemptUpdate, - ) -> CustomResult { - // TODO: Implement function for `MockDb` - Err(errors::StorageError::MockDbError)? - } - - async fn insert_payout_attempt( - &self, - _payout: storage::PayoutAttemptNew, - ) -> CustomResult { - // TODO: Implement function for `MockDb` - Err(errors::StorageError::MockDbError)? - } -} diff --git a/crates/router/src/db/payouts.rs b/crates/router/src/db/payouts.rs deleted file mode 100644 index b20441de0f..0000000000 --- a/crates/router/src/db/payouts.rs +++ /dev/null @@ -1,99 +0,0 @@ -use error_stack::IntoReport; -use router_env::{instrument, tracing}; - -use super::{MockDb, Store}; -use crate::{ - connection, - core::errors::{self, CustomResult}, - types::storage, -}; - -#[async_trait::async_trait] -pub trait PayoutsInterface { - async fn find_payout_by_merchant_id_payout_id( - &self, - _merchant_id: &str, - _payout_id: &str, - ) -> CustomResult; - - async fn update_payout_by_merchant_id_payout_id( - &self, - _merchant_id: &str, - _payout_id: &str, - _payout: storage::PayoutsUpdate, - ) -> CustomResult; - - async fn insert_payout( - &self, - _payout: storage::PayoutsNew, - ) -> CustomResult; -} - -#[async_trait::async_trait] -impl PayoutsInterface for Store { - #[instrument(skip_all)] - async fn find_payout_by_merchant_id_payout_id( - &self, - merchant_id: &str, - payout_id: &str, - ) -> CustomResult { - let conn = connection::pg_connection_read(self).await?; - storage::Payouts::find_by_merchant_id_payout_id(&conn, merchant_id, payout_id) - .await - .map_err(Into::into) - .into_report() - } - - #[instrument(skip_all)] - async fn update_payout_by_merchant_id_payout_id( - &self, - merchant_id: &str, - payout_id: &str, - payout: storage::PayoutsUpdate, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - storage::Payouts::update_by_merchant_id_payout_id(&conn, merchant_id, payout_id, payout) - .await - .map_err(Into::into) - .into_report() - } - - #[instrument(skip_all)] - async fn insert_payout( - &self, - payout: storage::PayoutsNew, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - payout.insert(&conn).await.map_err(Into::into).into_report() - } -} - -#[async_trait::async_trait] -impl PayoutsInterface for MockDb { - async fn find_payout_by_merchant_id_payout_id( - &self, - _merchant_id: &str, - _payout_id: &str, - ) -> CustomResult { - // TODO: Implement function for `MockDb` - Err(errors::StorageError::MockDbError)? - } - - async fn update_payout_by_merchant_id_payout_id( - &self, - _merchant_id: &str, - _payout_id: &str, - _payout: storage::PayoutsUpdate, - ) -> CustomResult { - // TODO: Implement function for `MockDb` - Err(errors::StorageError::MockDbError)? - } - - async fn insert_payout( - &self, - _payout: storage::PayoutsNew, - ) -> CustomResult { - // TODO: Implement function for `MockDb` - Err(errors::StorageError::MockDbError)? - } -} diff --git a/crates/router/src/routes/payouts.rs b/crates/router/src/routes/payouts.rs index cc47263a0c..a5bb22e87d 100644 --- a/crates/router/src/routes/payouts.rs +++ b/crates/router/src/routes/payouts.rs @@ -6,14 +6,12 @@ use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ - core::api_locking, + core::{api_locking, payouts::*}, services::{api, authentication as auth}, + types::api::payouts as payout_types, }; -#[cfg(feature = "payouts")] -use crate::{core::payouts::*, types::api::payouts as payout_types}; /// Payouts - Create -#[cfg(feature = "payouts")] #[utoipa::path( post, path = "/payouts/create", @@ -45,7 +43,6 @@ pub async fn payouts_create( .await } /// Payouts - Retrieve -#[cfg(feature = "payouts")] #[utoipa::path( get, path = "/payouts/{payout_id}", @@ -84,7 +81,6 @@ pub async fn payouts_retrieve( .await } /// Payouts - Update -#[cfg(feature = "payouts")] #[utoipa::path( post, path = "/payouts/{payout_id}", @@ -123,7 +119,6 @@ pub async fn payouts_update( .await } /// Payouts - Cancel -#[cfg(feature = "payouts")] #[utoipa::path( post, path = "/payouts/{payout_id}/cancel", @@ -162,7 +157,6 @@ pub async fn payouts_cancel( .await } /// Payouts - Fulfill -#[cfg(feature = "payouts")] #[utoipa::path( post, path = "/payouts/{payout_id}/fulfill", @@ -200,6 +194,7 @@ pub async fn payouts_fulfill( )) .await } + #[instrument(skip_all, fields(flow = ?Flow::PayoutsAccounts))] // #[get("/accounts")] pub async fn payouts_accounts() -> impl Responder { diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 82ab175279..bcf907618f 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -18,10 +18,9 @@ pub mod transformers; use std::{collections::HashMap, marker::PhantomData}; -pub use api_models::{ - enums::{Connector, PayoutConnectors}, - mandates, payouts as payout_types, -}; +pub use api_models::{enums::Connector, mandates}; +#[cfg(feature = "payouts")] +pub use api_models::{enums::PayoutConnectors, payouts as payout_types}; use common_enums::MandateStatus; pub use common_utils::request::{RequestBody, RequestContent}; use common_utils::{pii, pii::Email}; diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 50d256b034..5f8c9a96f1 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -15,6 +15,7 @@ pub mod mandates; pub mod payment_link; pub mod payment_methods; pub mod payments; +#[cfg(feature = "payouts")] pub mod payouts; pub mod refunds; pub mod routing; @@ -28,9 +29,11 @@ use error_stack::{report, IntoReport, ResultExt}; #[cfg(feature = "frm")] pub use self::fraud_check::*; +#[cfg(feature = "payouts")] +pub use self::payouts::*; pub use self::{ admin::*, api_keys::*, authentication::*, configs::*, customers::*, disputes::*, files::*, - payment_link::*, payment_methods::*, payments::*, payouts::*, refunds::*, webhooks::*, + payment_link::*, payment_methods::*, payments::*, refunds::*, webhooks::*, }; use super::ErrorResponse; use crate::{ @@ -287,6 +290,7 @@ impl ConnectorData { }) } + #[cfg(feature = "payouts")] pub fn get_payout_connector_by_name( connectors: &Connectors, name: &str, @@ -410,6 +414,20 @@ pub trait FraudCheck: #[cfg(not(feature = "frm"))] pub trait FraudCheck {} +#[cfg(feature = "payouts")] +pub trait Payouts: + ConnectorCommon + + PayoutCancel + + PayoutCreate + + PayoutEligibility + + PayoutFulfill + + PayoutQuote + + PayoutRecipient +{ +} +#[cfg(not(feature = "payouts"))] +pub trait Payouts {} + #[cfg(test)] mod test { #![allow(clippy::unwrap_used)] diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index b293f85eb1..2cc0e21d64 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -3,8 +3,7 @@ pub use api_models::admin::{ MerchantAccountDeleteResponse, MerchantAccountResponse, MerchantAccountUpdate, MerchantConnectorCreate, MerchantConnectorDeleteResponse, MerchantConnectorDetails, MerchantConnectorDetailsWrap, MerchantConnectorId, MerchantConnectorResponse, MerchantDetails, - MerchantId, PaymentMethodsEnabled, PayoutRoutingAlgorithm, ToggleKVRequest, ToggleKVResponse, - WebhookDetails, + MerchantId, PaymentMethodsEnabled, ToggleKVRequest, ToggleKVResponse, WebhookDetails, }; use common_utils::ext_traits::{Encode, ValueExt}; use error_stack::ResultExt; @@ -40,6 +39,7 @@ impl TryFrom for MerchantAccountResponse { primary_business_details, frm_routing_algorithm: item.frm_routing_algorithm, intent_fulfillment_time: item.intent_fulfillment_time, + #[cfg(feature = "payouts")] payout_routing_algorithm: item.payout_routing_algorithm, organization_id: item.organization_id, is_recon_enabled: item.is_recon_enabled, @@ -68,6 +68,7 @@ impl ForeignTryFrom for BusinessProf routing_algorithm: item.routing_algorithm, intent_fulfillment_time: item.intent_fulfillment_time, frm_routing_algorithm: item.frm_routing_algorithm, + #[cfg(feature = "payouts")] payout_routing_algorithm: item.payout_routing_algorithm, applepay_verified_domains: item.applepay_verified_domains, payment_link_config: item.payment_link_config, @@ -153,9 +154,12 @@ impl ForeignTryFrom<(domain::MerchantAccount, BusinessProfileCreate)> frm_routing_algorithm: request .frm_routing_algorithm .or(merchant_account.frm_routing_algorithm), + #[cfg(feature = "payouts")] payout_routing_algorithm: request .payout_routing_algorithm .or(merchant_account.payout_routing_algorithm), + #[cfg(not(feature = "payouts"))] + payout_routing_algorithm: None, is_recon_enabled: merchant_account.is_recon_enabled, applepay_verified_domains: request.applepay_verified_domains, payment_link_config: payment_link_config_value, diff --git a/crates/router/src/types/api/payouts.rs b/crates/router/src/types/api/payouts.rs index 9d74fc620a..0edc5d47a9 100644 --- a/crates/router/src/types/api/payouts.rs +++ b/crates/router/src/types/api/payouts.rs @@ -4,81 +4,52 @@ pub use api_models::payouts::{ PayoutRetrieveRequest, SepaBankTransfer, Wallet as WalletPayout, }; -#[cfg(feature = "payouts")] -use super::ConnectorCommon; -#[cfg(feature = "payouts")] use crate::{services::api, types}; -#[cfg(feature = "payouts")] #[derive(Debug, Clone)] pub struct PoCancel; -#[cfg(feature = "payouts")] #[derive(Debug, Clone)] pub struct PoCreate; -#[cfg(feature = "payouts")] #[derive(Debug, Clone)] pub struct PoEligibility; -#[cfg(feature = "payouts")] #[derive(Debug, Clone)] pub struct PoFulfill; -#[cfg(feature = "payouts")] #[derive(Debug, Clone)] pub struct PoQuote; -#[cfg(feature = "payouts")] #[derive(Debug, Clone)] pub struct PoRecipient; -#[cfg(feature = "payouts")] pub trait PayoutCancel: api::ConnectorIntegration { } -#[cfg(feature = "payouts")] pub trait PayoutCreate: api::ConnectorIntegration { } -#[cfg(feature = "payouts")] pub trait PayoutEligibility: api::ConnectorIntegration { } -#[cfg(feature = "payouts")] pub trait PayoutFulfill: api::ConnectorIntegration { } -#[cfg(feature = "payouts")] pub trait PayoutQuote: api::ConnectorIntegration { } -#[cfg(feature = "payouts")] pub trait PayoutRecipient: api::ConnectorIntegration { } - -#[cfg(feature = "payouts")] -pub trait Payouts: - ConnectorCommon - + PayoutCancel - + PayoutCreate - + PayoutEligibility - + PayoutFulfill - + PayoutQuote - + PayoutRecipient -{ -} -#[cfg(not(feature = "payouts"))] -pub trait Payouts {} diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 1850eb4b81..63272732e6 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -343,6 +343,7 @@ impl NewUserMerchant { parent_merchant_id: None, sub_merchants_enabled: None, frm_routing_algorithm: None, + #[cfg(feature = "payouts")] payout_routing_algorithm: None, primary_business_details: None, payment_response_hash_key: None, diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index 4baa72c1eb..ec164000b3 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -44,6 +44,11 @@ pub use data_models::payments::{ payment_intent::{PaymentIntentNew, PaymentIntentUpdate}, PaymentIntent, }; +#[cfg(feature = "payouts")] +pub use data_models::payouts::{ + payout_attempt::{PayoutAttempt, PayoutAttemptNew, PayoutAttemptUpdate}, + payouts::{Payouts, PayoutsNew, PayoutsUpdate}, +}; pub use diesel_models::{ ProcessTracker, ProcessTrackerNew, ProcessTrackerRunner, ProcessTrackerUpdate, }; @@ -55,8 +60,8 @@ pub use self::{ configs::*, customers::*, dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, fraud_check::*, gsm::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, payment_method::*, - payout_attempt::*, payouts::*, process_tracker::*, refund::*, reverse_lookup::*, role::*, - routing_algorithm::*, user::*, user_role::*, + process_tracker::*, refund::*, reverse_lookup::*, role::*, routing_algorithm::*, user::*, + user_role::*, }; use crate::types::api::routing; diff --git a/crates/router/src/types/storage/payment_method.rs b/crates/router/src/types/storage/payment_method.rs index e1d74c7008..d9f96ef482 100644 --- a/crates/router/src/types/storage/payment_method.rs +++ b/crates/router/src/types/storage/payment_method.rs @@ -10,7 +10,7 @@ pub use diesel_models::payment_method::{ TokenizeCoreWorkflow, }; -use crate::types::api::payments; +use crate::types::api::{self, payments}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] @@ -78,6 +78,14 @@ impl PaymentTokenData { } } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct PaymentMethodListContext { + pub card_details: Option, + pub hyperswitch_token_data: PaymentTokenData, + #[cfg(feature = "payouts")] + pub bank_transfer_details: Option, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct PaymentsMandateReferenceRecord { pub connector_mandate_id: String, diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 637a4b077d..2062867ca9 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -912,6 +912,7 @@ impl ForeignFrom for api_models::payments::CaptureResponse { } } +#[cfg(feature = "payouts")] impl ForeignFrom for api_enums::PaymentMethodType { fn foreign_from(value: api_models::payouts::PayoutMethodData) -> Self { match value { @@ -922,6 +923,7 @@ impl ForeignFrom for api_enums::PaymentMe } } +#[cfg(feature = "payouts")] impl ForeignFrom for api_enums::PaymentMethodType { fn foreign_from(value: api_models::payouts::Bank) -> Self { match value { @@ -932,6 +934,7 @@ impl ForeignFrom for api_enums::PaymentMethodType { } } +#[cfg(feature = "payouts")] impl ForeignFrom for api_enums::PaymentMethodType { fn foreign_from(value: api_models::payouts::Wallet) -> Self { match value { @@ -940,6 +943,7 @@ impl ForeignFrom for api_enums::PaymentMethodType { } } +#[cfg(feature = "payouts")] impl ForeignFrom for api_enums::PaymentMethod { fn foreign_from(value: api_models::payouts::PayoutMethodData) -> Self { match value { @@ -950,6 +954,7 @@ impl ForeignFrom for api_enums::PaymentMe } } +#[cfg(feature = "payouts")] impl ForeignFrom for api_enums::PaymentMethod { fn foreign_from(value: api_models::enums::PayoutType) -> Self { match value { diff --git a/crates/router/tests/connectors/payme.rs b/crates/router/tests/connectors/payme.rs index 6398b9da57..62c1bac94d 100644 --- a/crates/router/tests/connectors/payme.rs +++ b/crates/router/tests/connectors/payme.rs @@ -69,6 +69,7 @@ fn get_default_payment_info() -> Option { payment_method_token: None, country: None, currency: None, + #[cfg(feature = "payouts")] payout_method_data: None, }) } diff --git a/crates/router/tests/connectors/square.rs b/crates/router/tests/connectors/square.rs index daed5030ce..62aa8b6d24 100644 --- a/crates/router/tests/connectors/square.rs +++ b/crates/router/tests/connectors/square.rs @@ -49,6 +49,7 @@ fn get_default_payment_info(payment_method_token: Option) -> Option, pub connector_customer: Option, pub payment_method_token: Option, + #[cfg(feature = "payouts")] pub payout_method_data: Option, pub currency: Option, pub country: Option, diff --git a/crates/storage_impl/Cargo.toml b/crates/storage_impl/Cargo.toml index aea4949c1f..0bffd84650 100644 --- a/crates/storage_impl/Cargo.toml +++ b/crates/storage_impl/Cargo.toml @@ -12,6 +12,7 @@ license.workspace = true default = ["olap", "oltp"] oltp = [] olap = ["data_models/olap"] +payouts = ["data_models/payouts"] [dependencies] # First Party dependencies diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index 4fb940a063..c4bbf76833 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -14,17 +14,23 @@ mod lookup; pub mod metrics; pub mod mock_db; pub mod payments; +#[cfg(feature = "payouts")] +pub mod payouts; pub mod redis; pub mod refund; mod reverse_lookup; mod utils; use common_utils::errors::CustomResult; +#[cfg(not(feature = "payouts"))] +use data_models::{PayoutAttemptInterface, PayoutsInterface}; use database::store::PgPool; pub use mock_db::MockDb; use redis_interface::{errors::RedisError, SaddReply}; pub use crate::database::store::DatabaseStore; +#[cfg(not(feature = "payouts"))] +pub use crate::database::store::Store; #[derive(Debug, Clone)] pub struct RouterStore { @@ -331,3 +337,35 @@ impl UniqueConstraints for diesel_models::ReverseLookup { "ReverseLookup" } } + +#[cfg(feature = "payouts")] +impl UniqueConstraints for diesel_models::Payouts { + fn unique_constraints(&self) -> Vec { + vec![format!("po_{}_{}", self.merchant_id, self.payout_id)] + } + fn table_name(&self) -> &str { + "Payouts" + } +} + +#[cfg(feature = "payouts")] +impl UniqueConstraints for diesel_models::PayoutAttempt { + fn unique_constraints(&self) -> Vec { + vec![format!( + "poa_{}_{}", + self.merchant_id, self.payout_attempt_id + )] + } + fn table_name(&self) -> &str { + "PayoutAttempt" + } +} + +#[cfg(not(feature = "payouts"))] +impl PayoutAttemptInterface for KVRouterStore {} +#[cfg(not(feature = "payouts"))] +impl PayoutAttemptInterface for RouterStore {} +#[cfg(not(feature = "payouts"))] +impl PayoutsInterface for KVRouterStore {} +#[cfg(not(feature = "payouts"))] +impl PayoutsInterface for RouterStore {} diff --git a/crates/storage_impl/src/mock_db.rs b/crates/storage_impl/src/mock_db.rs index 023f30db32..77cda9817a 100644 --- a/crates/storage_impl/src/mock_db.rs +++ b/crates/storage_impl/src/mock_db.rs @@ -13,7 +13,13 @@ use crate::redis::RedisStore; pub mod payment_attempt; pub mod payment_intent; +#[cfg(feature = "payouts")] +pub mod payout_attempt; +#[cfg(feature = "payouts")] +pub mod payouts; pub mod redis_conn; +#[cfg(not(feature = "payouts"))] +use data_models::{PayoutAttemptInterface, PayoutsInterface}; #[derive(Clone)] pub struct MockDb { @@ -45,6 +51,10 @@ pub struct MockDb { pub user_roles: Arc>>, pub authorizations: Arc>>, pub dashboard_metadata: Arc>>, + #[cfg(feature = "payouts")] + pub payout_attempt: Arc>>, + #[cfg(feature = "payouts")] + pub payouts: Arc>>, pub authentications: Arc>>, pub roles: Arc>>, } @@ -84,8 +94,18 @@ impl MockDb { user_roles: Default::default(), authorizations: Default::default(), dashboard_metadata: Default::default(), + #[cfg(feature = "payouts")] + payout_attempt: Default::default(), + #[cfg(feature = "payouts")] + payouts: Default::default(), authentications: Default::default(), roles: Default::default(), }) } } + +#[cfg(not(feature = "payouts"))] +impl PayoutsInterface for MockDb {} + +#[cfg(not(feature = "payouts"))] +impl PayoutAttemptInterface for MockDb {} diff --git a/crates/storage_impl/src/mock_db/payout_attempt.rs b/crates/storage_impl/src/mock_db/payout_attempt.rs new file mode 100644 index 0000000000..543ce5cf65 --- /dev/null +++ b/crates/storage_impl/src/mock_db/payout_attempt.rs @@ -0,0 +1,42 @@ +use common_utils::errors::CustomResult; +use data_models::{ + errors::StorageError, + payouts::payout_attempt::{ + PayoutAttempt, PayoutAttemptInterface, PayoutAttemptNew, PayoutAttemptUpdate, + }, +}; +use diesel_models::enums as storage_enums; + +use super::MockDb; + +#[async_trait::async_trait] +impl PayoutAttemptInterface for MockDb { + async fn update_payout_attempt( + &self, + _this: &PayoutAttempt, + _payout_attempt_update: PayoutAttemptUpdate, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult { + // TODO: Implement function for `MockDb` + Err(StorageError::MockDbError)? + } + + async fn insert_payout_attempt( + &self, + _payout: PayoutAttemptNew, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult { + // TODO: Implement function for `MockDb` + Err(StorageError::MockDbError)? + } + + async fn find_payout_attempt_by_merchant_id_payout_attempt_id( + &self, + _merchant_id: &str, + _payout_attempt_id: &str, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult { + // TODO: Implement function for `MockDb` + Err(StorageError::MockDbError)? + } +} diff --git a/crates/storage_impl/src/mock_db/payouts.rs b/crates/storage_impl/src/mock_db/payouts.rs new file mode 100644 index 0000000000..9b4305afce --- /dev/null +++ b/crates/storage_impl/src/mock_db/payouts.rs @@ -0,0 +1,50 @@ +use common_utils::errors::CustomResult; +use data_models::{ + errors::StorageError, + payouts::payouts::{Payouts, PayoutsInterface, PayoutsNew, PayoutsUpdate}, +}; +use diesel_models::enums as storage_enums; + +use super::MockDb; + +#[async_trait::async_trait] +impl PayoutsInterface for MockDb { + async fn find_payout_by_merchant_id_payout_id( + &self, + _merchant_id: &str, + _payout_id: &str, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult { + // TODO: Implement function for `MockDb` + Err(StorageError::MockDbError)? + } + + async fn update_payout( + &self, + _this: &Payouts, + _payout_update: PayoutsUpdate, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult { + // TODO: Implement function for `MockDb` + Err(StorageError::MockDbError)? + } + + async fn insert_payout( + &self, + _payout: PayoutsNew, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult { + // TODO: Implement function for `MockDb` + Err(StorageError::MockDbError)? + } + + async fn find_optional_payout_by_merchant_id_payout_id( + &self, + _merchant_id: &str, + _payout_id: &str, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult, StorageError> { + // TODO: Implement function for `MockDb` + Err(StorageError::MockDbError)? + } +} diff --git a/crates/storage_impl/src/payouts.rs b/crates/storage_impl/src/payouts.rs new file mode 100644 index 0000000000..9e7ce08fbb --- /dev/null +++ b/crates/storage_impl/src/payouts.rs @@ -0,0 +1,10 @@ +pub mod payout_attempt; +#[allow(clippy::module_inception)] +pub mod payouts; + +use diesel_models::{payout_attempt::PayoutAttempt, payouts::Payouts}; + +use crate::redis::kv_store::KvStorePartition; + +impl KvStorePartition for Payouts {} +impl KvStorePartition for PayoutAttempt {} diff --git a/crates/storage_impl/src/payouts/payout_attempt.rs b/crates/storage_impl/src/payouts/payout_attempt.rs new file mode 100644 index 0000000000..2c82d995f0 --- /dev/null +++ b/crates/storage_impl/src/payouts/payout_attempt.rs @@ -0,0 +1,435 @@ +use common_utils::{ext_traits::Encode, fallback_reverse_lookup_not_found}; +use data_models::{ + errors, + payouts::payout_attempt::{ + PayoutAttempt, PayoutAttemptInterface, PayoutAttemptNew, PayoutAttemptUpdate, + }, +}; +use diesel_models::{ + enums::MerchantStorageScheme, + kv, + payout_attempt::{ + PayoutAttempt as DieselPayoutAttempt, PayoutAttemptNew as DieselPayoutAttemptNew, + PayoutAttemptUpdate as DieselPayoutAttemptUpdate, + }, + ReverseLookupNew, +}; +use error_stack::{IntoReport, ResultExt}; +use redis_interface::HsetnxReply; +use router_env::{instrument, tracing}; + +use crate::{ + diesel_error_to_data_error, + errors::RedisErrorExt, + lookup::ReverseLookupInterface, + redis::kv_store::{kv_wrapper, KvOperation}, + utils::{self, pg_connection_read, pg_connection_write}, + DataModelExt, DatabaseStore, KVRouterStore, +}; + +#[async_trait::async_trait] +impl PayoutAttemptInterface for KVRouterStore { + #[instrument(skip_all)] + async fn insert_payout_attempt( + &self, + new_payout_attempt: PayoutAttemptNew, + storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + match storage_scheme { + MerchantStorageScheme::PostgresOnly => { + self.router_store + .insert_payout_attempt(new_payout_attempt, storage_scheme) + .await + } + MerchantStorageScheme::RedisKv => { + let key = format!( + "mid_{}_poa_{}", + new_payout_attempt.merchant_id, new_payout_attempt.payout_id + ); + let now = common_utils::date_time::now(); + let created_attempt = PayoutAttempt { + payout_attempt_id: new_payout_attempt.payout_attempt_id.clone(), + payout_id: new_payout_attempt.payout_id.clone(), + customer_id: new_payout_attempt.customer_id.clone(), + merchant_id: new_payout_attempt.merchant_id.clone(), + address_id: new_payout_attempt.address_id.clone(), + connector: new_payout_attempt.connector.clone(), + connector_payout_id: new_payout_attempt.connector_payout_id.clone(), + payout_token: new_payout_attempt.payout_token.clone(), + status: new_payout_attempt.status, + is_eligible: new_payout_attempt.is_eligible, + error_message: new_payout_attempt.error_message.clone(), + error_code: new_payout_attempt.error_code.clone(), + business_country: new_payout_attempt.business_country, + business_label: new_payout_attempt.business_label.clone(), + created_at: new_payout_attempt.created_at.unwrap_or(now), + last_modified_at: new_payout_attempt.last_modified_at.unwrap_or(now), + profile_id: new_payout_attempt.profile_id.clone(), + merchant_connector_id: new_payout_attempt.merchant_connector_id.clone(), + routing_info: new_payout_attempt.routing_info.clone(), + }; + + let redis_entry = kv::TypedSql { + op: kv::DBOperation::Insert { + insertable: kv::Insertable::PayoutAttempt( + new_payout_attempt.to_storage_model(), + ), + }, + }; + + // Reverse lookup for payout_attempt_id + let field = format!("poa_{}", created_attempt.payout_attempt_id); + let reverse_lookup = ReverseLookupNew { + lookup_id: format!( + "poa_{}_{}", + &created_attempt.merchant_id, &created_attempt.payout_attempt_id, + ), + pk_id: key.clone(), + sk_id: field.clone(), + source: "payout_attempt".to_string(), + updated_by: storage_scheme.to_string(), + }; + self.insert_reverse_lookup(reverse_lookup, storage_scheme) + .await?; + + match kv_wrapper::( + self, + KvOperation::::HSetNx( + &field, + &created_attempt.clone().to_storage_model(), + redis_entry, + ), + &key, + ) + .await + .map_err(|err| err.to_redis_failed_response(&key))? + .try_into_hsetnx() + { + Ok(HsetnxReply::KeyNotSet) => Err(errors::StorageError::DuplicateValue { + entity: "payout attempt", + key: Some(key), + }) + .into_report(), + Ok(HsetnxReply::KeySet) => Ok(created_attempt), + Err(error) => Err(error.change_context(errors::StorageError::KVError)), + } + } + } + } + + #[instrument(skip_all)] + async fn update_payout_attempt( + &self, + this: &PayoutAttempt, + payout_update: PayoutAttemptUpdate, + storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + match storage_scheme { + MerchantStorageScheme::PostgresOnly => { + self.router_store + .update_payout_attempt(this, payout_update, storage_scheme) + .await + } + MerchantStorageScheme::RedisKv => { + let key = format!("mid_{}_poa_{}", this.merchant_id, this.payout_id); + let field = format!("poa_{}", this.payout_attempt_id); + + let diesel_payout_update = payout_update.to_storage_model(); + let origin_diesel_payout = this.clone().to_storage_model(); + + let diesel_payout = diesel_payout_update + .clone() + .apply_changeset(origin_diesel_payout.clone()); + // Check for database presence as well Maybe use a read replica here ? + + let redis_value = diesel_payout + .encode_to_string_of_json() + .change_context(errors::StorageError::SerializationFailed)?; + + let redis_entry = kv::TypedSql { + op: kv::DBOperation::Update { + updatable: kv::Updateable::PayoutAttemptUpdate( + kv::PayoutAttemptUpdateMems { + orig: origin_diesel_payout, + update_data: diesel_payout_update, + }, + ), + }, + }; + + kv_wrapper::<(), _, _>( + self, + KvOperation::::Hset((&field, redis_value), redis_entry), + &key, + ) + .await + .map_err(|err| err.to_redis_failed_response(&key))? + .try_into_hset() + .change_context(errors::StorageError::KVError)?; + + Ok(PayoutAttempt::from_storage_model(diesel_payout)) + } + } + } + + #[instrument(skip_all)] + async fn find_payout_attempt_by_merchant_id_payout_attempt_id( + &self, + merchant_id: &str, + payout_attempt_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + match storage_scheme { + MerchantStorageScheme::PostgresOnly => { + self.router_store + .find_payout_attempt_by_merchant_id_payout_attempt_id( + merchant_id, + payout_attempt_id, + storage_scheme, + ) + .await + } + MerchantStorageScheme::RedisKv => { + let lookup_id = format!("poa_{merchant_id}_{payout_attempt_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payout_attempt_by_merchant_id_payout_attempt_id( + merchant_id, + payout_attempt_id, + storage_scheme + ) + .await + ); + let key = &lookup.pk_id; + Box::pin(utils::try_redis_get_else_try_database_get( + async { + kv_wrapper( + self, + KvOperation::::HGet(&lookup.sk_id), + key, + ) + .await? + .try_into_hget() + }, + || async { + self.router_store + .find_payout_attempt_by_merchant_id_payout_attempt_id( + merchant_id, + payout_attempt_id, + storage_scheme, + ) + .await + }, + )) + .await + } + } + } +} + +#[async_trait::async_trait] +impl PayoutAttemptInterface for crate::RouterStore { + #[instrument(skip_all)] + async fn insert_payout_attempt( + &self, + new: PayoutAttemptNew, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + let conn = pg_connection_write(self).await?; + new.to_storage_model() + .insert(&conn) + .await + .map_err(|er| { + let new_err = diesel_error_to_data_error(er.current_context()); + er.change_context(new_err) + }) + .map(PayoutAttempt::from_storage_model) + } + + #[instrument(skip_all)] + async fn update_payout_attempt( + &self, + this: &PayoutAttempt, + payout: PayoutAttemptUpdate, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + let conn = pg_connection_write(self).await?; + this.clone() + .to_storage_model() + .update_with_attempt_id(&conn, payout.to_storage_model()) + .await + .map_err(|er| { + let new_err = diesel_error_to_data_error(er.current_context()); + er.change_context(new_err) + }) + .map(PayoutAttempt::from_storage_model) + } + + #[instrument(skip_all)] + async fn find_payout_attempt_by_merchant_id_payout_attempt_id( + &self, + merchant_id: &str, + payout_attempt_id: &str, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + let conn = pg_connection_read(self).await?; + DieselPayoutAttempt::find_by_merchant_id_payout_attempt_id( + &conn, + merchant_id, + payout_attempt_id, + ) + .await + .map(PayoutAttempt::from_storage_model) + .map_err(|er| { + let new_err = diesel_error_to_data_error(er.current_context()); + er.change_context(new_err) + }) + } +} + +impl DataModelExt for PayoutAttempt { + type StorageModel = DieselPayoutAttempt; + + fn to_storage_model(self) -> Self::StorageModel { + DieselPayoutAttempt { + payout_attempt_id: self.payout_attempt_id, + payout_id: self.payout_id, + customer_id: self.customer_id, + merchant_id: self.merchant_id, + address_id: self.address_id, + connector: self.connector, + connector_payout_id: self.connector_payout_id, + payout_token: self.payout_token, + status: self.status, + is_eligible: self.is_eligible, + error_message: self.error_message, + error_code: self.error_code, + business_country: self.business_country, + business_label: self.business_label, + created_at: self.created_at, + last_modified_at: self.last_modified_at, + profile_id: self.profile_id, + merchant_connector_id: self.merchant_connector_id, + routing_info: self.routing_info, + } + } + + fn from_storage_model(storage_model: Self::StorageModel) -> Self { + Self { + payout_attempt_id: storage_model.payout_attempt_id, + payout_id: storage_model.payout_id, + customer_id: storage_model.customer_id, + merchant_id: storage_model.merchant_id, + address_id: storage_model.address_id, + connector: storage_model.connector, + connector_payout_id: storage_model.connector_payout_id, + payout_token: storage_model.payout_token, + status: storage_model.status, + is_eligible: storage_model.is_eligible, + error_message: storage_model.error_message, + error_code: storage_model.error_code, + business_country: storage_model.business_country, + business_label: storage_model.business_label, + created_at: storage_model.created_at, + last_modified_at: storage_model.last_modified_at, + profile_id: storage_model.profile_id, + merchant_connector_id: storage_model.merchant_connector_id, + routing_info: storage_model.routing_info, + } + } +} +impl DataModelExt for PayoutAttemptNew { + type StorageModel = DieselPayoutAttemptNew; + + fn to_storage_model(self) -> Self::StorageModel { + DieselPayoutAttemptNew { + payout_attempt_id: self.payout_attempt_id, + payout_id: self.payout_id, + customer_id: self.customer_id, + merchant_id: self.merchant_id, + address_id: self.address_id, + connector: self.connector, + connector_payout_id: self.connector_payout_id, + payout_token: self.payout_token, + status: self.status, + is_eligible: self.is_eligible, + error_message: self.error_message, + error_code: self.error_code, + business_country: self.business_country, + business_label: self.business_label, + created_at: self.created_at, + last_modified_at: self.last_modified_at, + profile_id: self.profile_id, + merchant_connector_id: self.merchant_connector_id, + routing_info: self.routing_info, + } + } + + fn from_storage_model(storage_model: Self::StorageModel) -> Self { + Self { + payout_attempt_id: storage_model.payout_attempt_id, + payout_id: storage_model.payout_id, + customer_id: storage_model.customer_id, + merchant_id: storage_model.merchant_id, + address_id: storage_model.address_id, + connector: storage_model.connector, + connector_payout_id: storage_model.connector_payout_id, + payout_token: storage_model.payout_token, + status: storage_model.status, + is_eligible: storage_model.is_eligible, + error_message: storage_model.error_message, + error_code: storage_model.error_code, + business_country: storage_model.business_country, + business_label: storage_model.business_label, + created_at: storage_model.created_at, + last_modified_at: storage_model.last_modified_at, + profile_id: storage_model.profile_id, + merchant_connector_id: storage_model.merchant_connector_id, + routing_info: storage_model.routing_info, + } + } +} +impl DataModelExt for PayoutAttemptUpdate { + type StorageModel = DieselPayoutAttemptUpdate; + fn to_storage_model(self) -> Self::StorageModel { + match self { + Self::StatusUpdate { + connector_payout_id, + status, + error_message, + error_code, + is_eligible, + } => DieselPayoutAttemptUpdate::StatusUpdate { + connector_payout_id, + status, + error_message, + error_code, + is_eligible, + }, + Self::PayoutTokenUpdate { payout_token } => { + DieselPayoutAttemptUpdate::PayoutTokenUpdate { payout_token } + } + Self::BusinessUpdate { + business_country, + business_label, + } => DieselPayoutAttemptUpdate::BusinessUpdate { + business_country, + business_label, + }, + Self::UpdateRouting { + connector, + routing_info, + } => DieselPayoutAttemptUpdate::UpdateRouting { + connector, + routing_info, + }, + } + } + + #[allow(clippy::todo)] + fn from_storage_model(_storage_model: Self::StorageModel) -> Self { + todo!("Reverse map should no longer be needed") + } +} diff --git a/crates/storage_impl/src/payouts/payouts.rs b/crates/storage_impl/src/payouts/payouts.rs new file mode 100644 index 0000000000..5f27ddbbf1 --- /dev/null +++ b/crates/storage_impl/src/payouts/payouts.rs @@ -0,0 +1,462 @@ +use common_utils::ext_traits::Encode; +use data_models::{ + errors::StorageError, + payouts::payouts::{Payouts, PayoutsInterface, PayoutsNew, PayoutsUpdate}, +}; +use diesel_models::{ + enums::MerchantStorageScheme, + kv, + payouts::{ + Payouts as DieselPayouts, PayoutsNew as DieselPayoutsNew, + PayoutsUpdate as DieselPayoutsUpdate, + }, +}; +use error_stack::{IntoReport, ResultExt}; +use redis_interface::HsetnxReply; +use router_env::{instrument, tracing}; + +use crate::{ + diesel_error_to_data_error, + errors::RedisErrorExt, + redis::kv_store::{kv_wrapper, KvOperation}, + utils::{self, pg_connection_read, pg_connection_write}, + DataModelExt, DatabaseStore, KVRouterStore, +}; + +#[async_trait::async_trait] +impl PayoutsInterface for KVRouterStore { + #[instrument(skip_all)] + async fn insert_payout( + &self, + new: PayoutsNew, + storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + match storage_scheme { + MerchantStorageScheme::PostgresOnly => { + self.router_store.insert_payout(new, storage_scheme).await + } + MerchantStorageScheme::RedisKv => { + let key = format!("mid_{}_po_{}", new.merchant_id, new.payout_id); + let field = format!("po_{}", new.payout_id); + let now = common_utils::date_time::now(); + let created_payout = Payouts { + payout_id: new.payout_id.clone(), + merchant_id: new.merchant_id.clone(), + customer_id: new.customer_id.clone(), + address_id: new.address_id.clone(), + payout_type: new.payout_type, + payout_method_id: new.payout_method_id.clone(), + amount: new.amount, + destination_currency: new.destination_currency, + source_currency: new.source_currency, + description: new.description.clone(), + recurring: new.recurring, + auto_fulfill: new.auto_fulfill, + return_url: new.return_url.clone(), + entity_type: new.entity_type, + metadata: new.metadata.clone(), + created_at: new.created_at.unwrap_or(now), + last_modified_at: new.last_modified_at.unwrap_or(now), + profile_id: new.profile_id.clone(), + status: new.status, + attempt_count: new.attempt_count, + }; + + let redis_entry = kv::TypedSql { + op: kv::DBOperation::Insert { + insertable: kv::Insertable::Payouts(new.to_storage_model()), + }, + }; + + match kv_wrapper::( + self, + KvOperation::::HSetNx( + &field, + &created_payout.clone().to_storage_model(), + redis_entry, + ), + &key, + ) + .await + .map_err(|err| err.to_redis_failed_response(&key))? + .try_into_hsetnx() + { + Ok(HsetnxReply::KeyNotSet) => Err(StorageError::DuplicateValue { + entity: "payouts", + key: Some(key), + }) + .into_report(), + Ok(HsetnxReply::KeySet) => Ok(created_payout), + Err(error) => Err(error.change_context(StorageError::KVError)), + } + } + } + } + + #[instrument(skip_all)] + async fn update_payout( + &self, + this: &Payouts, + payout_update: PayoutsUpdate, + storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + match storage_scheme { + MerchantStorageScheme::PostgresOnly => { + self.router_store + .update_payout(this, payout_update, storage_scheme) + .await + } + MerchantStorageScheme::RedisKv => { + let key = format!("mid_{}_po_{}", this.merchant_id, this.payout_id); + let field = format!("po_{}", this.payout_id); + + let diesel_payout_update = payout_update.to_storage_model(); + let origin_diesel_payout = this.clone().to_storage_model(); + + let diesel_payout = diesel_payout_update + .clone() + .apply_changeset(origin_diesel_payout.clone()); + // Check for database presence as well Maybe use a read replica here ? + + let redis_value = diesel_payout + .encode_to_string_of_json() + .change_context(StorageError::SerializationFailed)?; + + let redis_entry = kv::TypedSql { + op: kv::DBOperation::Update { + updatable: kv::Updateable::PayoutsUpdate(kv::PayoutsUpdateMems { + orig: origin_diesel_payout, + update_data: diesel_payout_update, + }), + }, + }; + + kv_wrapper::<(), _, _>( + self, + KvOperation::::Hset((&field, redis_value), redis_entry), + &key, + ) + .await + .map_err(|err| err.to_redis_failed_response(&key))? + .try_into_hset() + .change_context(StorageError::KVError)?; + + Ok(Payouts::from_storage_model(diesel_payout)) + } + } + } + + #[instrument(skip_all)] + async fn find_payout_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + let database_call = || async { + let conn = pg_connection_read(self).await?; + DieselPayouts::find_by_merchant_id_payout_id(&conn, merchant_id, payout_id) + .await + .map_err(|er| { + let new_err = diesel_error_to_data_error(er.current_context()); + er.change_context(new_err) + }) + }; + match storage_scheme { + MerchantStorageScheme::PostgresOnly => database_call().await, + MerchantStorageScheme::RedisKv => { + let key = format!("mid_{merchant_id}_po_{payout_id}"); + let field = format!("po_{payout_id}"); + Box::pin(utils::try_redis_get_else_try_database_get( + async { + kv_wrapper::( + self, + KvOperation::::HGet(&field), + &key, + ) + .await? + .try_into_hget() + }, + database_call, + )) + .await + } + } + .map(Payouts::from_storage_model) + } + + #[instrument(skip_all)] + async fn find_optional_payout_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result, StorageError> { + let database_call = || async { + let conn = pg_connection_read(self).await?; + DieselPayouts::find_optional_by_merchant_id_payout_id(&conn, merchant_id, payout_id) + .await + .map_err(|er| { + let new_err = diesel_error_to_data_error(er.current_context()); + er.change_context(new_err) + }) + }; + match storage_scheme { + MerchantStorageScheme::PostgresOnly => { + let maybe_payouts = database_call().await?; + Ok(maybe_payouts.and_then(|payout| { + if payout.payout_id == payout_id { + Some(payout) + } else { + None + } + })) + } + MerchantStorageScheme::RedisKv => { + let key = format!("mid_{merchant_id}_po_{payout_id}"); + let field = format!("po_{payout_id}"); + Box::pin(utils::try_redis_get_else_try_database_get( + async { + kv_wrapper::( + self, + KvOperation::::HGet(&field), + &key, + ) + .await? + .try_into_hget() + .map(Some) + }, + database_call, + )) + .await + } + } + .map(|payout| payout.map(Payouts::from_storage_model)) + } +} + +#[async_trait::async_trait] +impl PayoutsInterface for crate::RouterStore { + #[instrument(skip_all)] + async fn insert_payout( + &self, + new: PayoutsNew, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + let conn = pg_connection_write(self).await?; + new.to_storage_model() + .insert(&conn) + .await + .map_err(|er| { + let new_err = diesel_error_to_data_error(er.current_context()); + er.change_context(new_err) + }) + .map(Payouts::from_storage_model) + } + + #[instrument(skip_all)] + async fn update_payout( + &self, + this: &Payouts, + payout: PayoutsUpdate, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + let conn = pg_connection_write(self).await?; + this.clone() + .to_storage_model() + .update(&conn, payout.to_storage_model()) + .await + .map_err(|er| { + let new_err = diesel_error_to_data_error(er.current_context()); + er.change_context(new_err) + }) + .map(Payouts::from_storage_model) + } + + #[instrument(skip_all)] + async fn find_payout_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + let conn = pg_connection_read(self).await?; + DieselPayouts::find_by_merchant_id_payout_id(&conn, merchant_id, payout_id) + .await + .map(Payouts::from_storage_model) + .map_err(|er| { + let new_err = diesel_error_to_data_error(er.current_context()); + er.change_context(new_err) + }) + } + + #[instrument(skip_all)] + async fn find_optional_payout_by_merchant_id_payout_id( + &self, + merchant_id: &str, + payout_id: &str, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result, StorageError> { + let conn = pg_connection_read(self).await?; + DieselPayouts::find_optional_by_merchant_id_payout_id(&conn, merchant_id, payout_id) + .await + .map(|x| x.map(Payouts::from_storage_model)) + .map_err(|er| { + let new_err = diesel_error_to_data_error(er.current_context()); + er.change_context(new_err) + }) + } +} + +impl DataModelExt for Payouts { + type StorageModel = DieselPayouts; + + fn to_storage_model(self) -> Self::StorageModel { + DieselPayouts { + payout_id: self.payout_id, + merchant_id: self.merchant_id, + customer_id: self.customer_id, + address_id: self.address_id, + payout_type: self.payout_type, + payout_method_id: self.payout_method_id, + amount: self.amount, + destination_currency: self.destination_currency, + source_currency: self.source_currency, + description: self.description, + recurring: self.recurring, + auto_fulfill: self.auto_fulfill, + return_url: self.return_url, + entity_type: self.entity_type, + metadata: self.metadata, + created_at: self.created_at, + last_modified_at: self.last_modified_at, + profile_id: self.profile_id, + status: self.status, + attempt_count: self.attempt_count, + } + } + + fn from_storage_model(storage_model: Self::StorageModel) -> Self { + Self { + payout_id: storage_model.payout_id, + merchant_id: storage_model.merchant_id, + customer_id: storage_model.customer_id, + address_id: storage_model.address_id, + payout_type: storage_model.payout_type, + payout_method_id: storage_model.payout_method_id, + amount: storage_model.amount, + destination_currency: storage_model.destination_currency, + source_currency: storage_model.source_currency, + description: storage_model.description, + recurring: storage_model.recurring, + auto_fulfill: storage_model.auto_fulfill, + return_url: storage_model.return_url, + entity_type: storage_model.entity_type, + metadata: storage_model.metadata, + created_at: storage_model.created_at, + last_modified_at: storage_model.last_modified_at, + profile_id: storage_model.profile_id, + status: storage_model.status, + attempt_count: storage_model.attempt_count, + } + } +} +impl DataModelExt for PayoutsNew { + type StorageModel = DieselPayoutsNew; + + fn to_storage_model(self) -> Self::StorageModel { + DieselPayoutsNew { + payout_id: self.payout_id, + merchant_id: self.merchant_id, + customer_id: self.customer_id, + address_id: self.address_id, + payout_type: self.payout_type, + payout_method_id: self.payout_method_id, + amount: self.amount, + destination_currency: self.destination_currency, + source_currency: self.source_currency, + description: self.description, + recurring: self.recurring, + auto_fulfill: self.auto_fulfill, + return_url: self.return_url, + entity_type: self.entity_type, + metadata: self.metadata, + created_at: self.created_at, + last_modified_at: self.last_modified_at, + profile_id: self.profile_id, + status: self.status, + attempt_count: self.attempt_count, + } + } + + fn from_storage_model(storage_model: Self::StorageModel) -> Self { + Self { + payout_id: storage_model.payout_id, + merchant_id: storage_model.merchant_id, + customer_id: storage_model.customer_id, + address_id: storage_model.address_id, + payout_type: storage_model.payout_type, + payout_method_id: storage_model.payout_method_id, + amount: storage_model.amount, + destination_currency: storage_model.destination_currency, + source_currency: storage_model.source_currency, + description: storage_model.description, + recurring: storage_model.recurring, + auto_fulfill: storage_model.auto_fulfill, + return_url: storage_model.return_url, + entity_type: storage_model.entity_type, + metadata: storage_model.metadata, + created_at: storage_model.created_at, + last_modified_at: storage_model.last_modified_at, + profile_id: storage_model.profile_id, + status: storage_model.status, + attempt_count: storage_model.attempt_count, + } + } +} +impl DataModelExt for PayoutsUpdate { + type StorageModel = DieselPayoutsUpdate; + fn to_storage_model(self) -> Self::StorageModel { + match self { + Self::Update { + amount, + destination_currency, + source_currency, + description, + recurring, + auto_fulfill, + return_url, + entity_type, + metadata, + profile_id, + status, + } => DieselPayoutsUpdate::Update { + amount, + destination_currency, + source_currency, + description, + recurring, + auto_fulfill, + return_url, + entity_type, + metadata, + profile_id, + status, + }, + Self::PayoutMethodIdUpdate { payout_method_id } => { + DieselPayoutsUpdate::PayoutMethodIdUpdate { payout_method_id } + } + Self::RecurringUpdate { recurring } => { + DieselPayoutsUpdate::RecurringUpdate { recurring } + } + Self::AttemptCountUpdate { attempt_count } => { + DieselPayoutsUpdate::AttemptCountUpdate { attempt_count } + } + } + } + + #[allow(clippy::todo)] + fn from_storage_model(_storage_model: Self::StorageModel) -> Self { + todo!("Reverse map should no longer be needed") + } +} diff --git a/migrations/2024-02-29-082737_update_payouts_for_analytics/down.sql b/migrations/2024-02-29-082737_update_payouts_for_analytics/down.sql new file mode 100644 index 0000000000..783317c8b0 --- /dev/null +++ b/migrations/2024-02-29-082737_update_payouts_for_analytics/down.sql @@ -0,0 +1,5 @@ +ALTER TABLE + PAYOUTS DROP COLUMN status; + +ALTER TABLE + PAYOUTS DROP COLUMN profile_id; \ No newline at end of file diff --git a/migrations/2024-02-29-082737_update_payouts_for_analytics/up.sql b/migrations/2024-02-29-082737_update_payouts_for_analytics/up.sql new file mode 100644 index 0000000000..8a48bd6421 --- /dev/null +++ b/migrations/2024-02-29-082737_update_payouts_for_analytics/up.sql @@ -0,0 +1,41 @@ +ALTER TABLE + PAYOUTS +ADD + COLUMN profile_id VARCHAR(64); + +UPDATE + PAYOUTS AS PO +SET + profile_id = POA.profile_id +FROM + PAYOUT_ATTEMPT AS POA +WHERE + PO.payout_id = POA.payout_id; + +ALTER TABLE + PAYOUTS +ALTER COLUMN + profile_id +SET + NOT NULL; + +ALTER TABLE + PAYOUTS +ADD + COLUMN status "PayoutStatus"; + +UPDATE + PAYOUTS AS PO +SET + status = POA.status +FROM + PAYOUT_ATTEMPT AS POA +WHERE + PO.payout_id = POA.payout_id; + +ALTER TABLE + PAYOUTS +ALTER COLUMN + status +SET + NOT NULL; \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index dd33ca7274..bd4eee14d8 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -14899,7 +14899,7 @@ "connector": { "type": "array", "items": { - "$ref": "#/components/schemas/Connector" + "$ref": "#/components/schemas/PayoutConnectors" }, "description": "This allows the merchant to manually select a connector with which the payout can go through", "example": [