diff --git a/api-reference/v1/openapi_spec_v1.json b/api-reference/v1/openapi_spec_v1.json index fe57a935d16..85223163b29 100644 --- a/api-reference/v1/openapi_spec_v1.json +++ b/api-reference/v1/openapi_spec_v1.json @@ -24303,6 +24303,13 @@ "client_secret" ], "properties": { + "merchant_order_reference_id": { + "type": "string", + "description": "Your unique identifier for this payout or order. This ID helps you reconcile payouts on your system. If provided, it is passed to the connector if supported.", + "example": "merchant_order_ref_123", + "nullable": true, + "maxLength": 255 + }, "amount": { "type": "integer", "format": "int64", @@ -24609,6 +24616,13 @@ "example": "merchant_1668273825", "maxLength": 255 }, + "merchant_order_reference_id": { + "type": "string", + "description": "Your unique identifier for this payout or order. This ID helps you reconcile payouts on your system. If provided, it is passed to the connector if supported.", + "example": "merchant_order_ref_123", + "nullable": true, + "maxLength": 255 + }, "amount": { "type": "integer", "format": "int64", @@ -24902,13 +24916,13 @@ "starting_after": { "type": "string", "description": "A cursor for use in pagination, fetch the next list after some object", - "example": "pay_fafa124123", + "example": "payout_fafa124123", "nullable": true }, "ending_before": { "type": "string", "description": "A cursor for use in pagination, fetch the previous list before some object", - "example": "pay_fafa124123", + "example": "payout_fafa124123", "nullable": true }, "limit": { @@ -24955,6 +24969,13 @@ "maxLength": 30, "minLength": 30 }, + "merchant_order_reference_id": { + "type": "string", + "description": "The merchant order reference ID for payout", + "example": "merchant_order_ref_123", + "nullable": true, + "maxLength": 255 + }, "profile_id": { "type": "string", "description": "The identifier for business profile", @@ -25249,6 +25270,13 @@ "PayoutUpdateRequest": { "type": "object", "properties": { + "merchant_order_reference_id": { + "type": "string", + "description": "Your unique identifier for this payout or order. This ID helps you reconcile payouts on your system. If provided, it is passed to the connector if supported.", + "example": "merchant_order_ref_123", + "nullable": true, + "maxLength": 255 + }, "amount": { "type": "integer", "format": "int64", @@ -25479,6 +25507,13 @@ "currency" ], "properties": { + "merchant_order_reference_id": { + "type": "string", + "description": "Your unique identifier for this payout or order. This ID helps you reconcile payouts on your system. If provided, it is passed to the connector if supported.", + "example": "merchant_order_ref_123", + "nullable": true, + "maxLength": 255 + }, "amount": { "type": "integer", "format": "int64", diff --git a/api-reference/v2/openapi_spec_v2.json b/api-reference/v2/openapi_spec_v2.json index 5ceb5f032f5..0bd642c8be8 100644 --- a/api-reference/v2/openapi_spec_v2.json +++ b/api-reference/v2/openapi_spec_v2.json @@ -19579,7 +19579,19 @@ "additionalProperties": false }, "PayoutActionRequest": { - "type": "object" + "type": "object", + "required": [ + "payout_id" + ], + "properties": { + "payout_id": { + "type": "string", + "description": "Unique identifier for the payout. This ensures idempotency for multiple payouts\nthat have been done by a single merchant. This field is auto generated and is returned in the API response.", + "example": "187282ab-40ef-47a9-9206-5099ba31e432", + "maxLength": 30, + "minLength": 30 + } + } }, "PayoutAttemptResponse": { "type": "object", @@ -19746,6 +19758,13 @@ "nullable": true, "maxLength": 255 }, + "merchant_order_reference_id": { + "type": "string", + "description": "Your unique identifier for this payout or order. This ID helps you reconcile payouts on your system. If provided, it is passed to the connector if supported.", + "example": "merchant_order_ref_123", + "nullable": true, + "maxLength": 255 + }, "amount": { "type": "integer", "format": "int64", @@ -20001,6 +20020,13 @@ "example": "merchant_1668273825", "maxLength": 255 }, + "merchant_order_reference_id": { + "type": "string", + "description": "Your unique identifier for this payout or order. This ID helps you reconcile payouts on your system. If provided, it is passed to the connector if supported.", + "example": "merchant_order_ref_123", + "nullable": true, + "maxLength": 255 + }, "amount": { "type": "integer", "format": "int64", @@ -20279,13 +20305,13 @@ "starting_after": { "type": "string", "description": "A cursor for use in pagination, fetch the next list after some object", - "example": "pay_fafa124123", + "example": "payout_fafa124123", "nullable": true }, "ending_before": { "type": "string", "description": "A cursor for use in pagination, fetch the previous list before some object", - "example": "pay_fafa124123", + "example": "payout_fafa124123", "nullable": true }, "limit": { @@ -20332,6 +20358,13 @@ "maxLength": 30, "minLength": 30 }, + "merchant_order_reference_id": { + "type": "string", + "description": "The merchant order reference ID for payout", + "example": "merchant_order_ref_123", + "nullable": true, + "maxLength": 255 + }, "profile_id": { "type": "string", "description": "The identifier for business profile", diff --git a/crates/api_models/src/events/payouts.rs b/crates/api_models/src/events/payouts.rs index a08f3ed94a8..b7eded681a3 100644 --- a/crates/api_models/src/events/payouts.rs +++ b/crates/api_models/src/events/payouts.rs @@ -9,7 +9,7 @@ use crate::payouts::{ impl ApiEventMetric for PayoutRetrieveRequest { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Payout { - payout_id: self.payout_id.clone(), + payout_id: self.payout_id.to_owned(), }) } } @@ -17,7 +17,7 @@ impl ApiEventMetric for PayoutRetrieveRequest { impl ApiEventMetric for PayoutCreateRequest { fn get_api_event_type(&self) -> Option { self.payout_id.as_ref().map(|id| ApiEventsType::Payout { - payout_id: id.clone(), + payout_id: id.to_owned(), }) } } @@ -25,7 +25,7 @@ impl ApiEventMetric for PayoutCreateRequest { impl ApiEventMetric for PayoutCreateResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Payout { - payout_id: self.payout_id.clone(), + payout_id: self.payout_id.to_owned(), }) } } @@ -33,7 +33,7 @@ impl ApiEventMetric for PayoutCreateResponse { impl ApiEventMetric for PayoutActionRequest { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Payout { - payout_id: self.payout_id.clone(), + payout_id: self.payout_id.to_owned(), }) } } @@ -65,7 +65,7 @@ impl ApiEventMetric for PayoutListFilters { impl ApiEventMetric for PayoutLinkInitiateRequest { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Payout { - payout_id: self.payout_id.clone(), + payout_id: self.payout_id.to_owned(), }) } } diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index cf07fd9e109..a6fdaa2b6e5 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -16,7 +16,7 @@ use utoipa::ToSchema; use crate::{enums as api_enums, payment_methods::RequiredFieldInfo, payments}; -#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[derive(Debug, Serialize, Clone, ToSchema)] pub enum PayoutRequest { PayoutActionRequest(PayoutActionRequest), PayoutCreateRequest(Box), @@ -37,13 +37,17 @@ pub struct PayoutCreateRequest { example = "187282ab-40ef-47a9-9206-5099ba31e432" )] #[remove_in(PayoutsCreateRequest, PayoutUpdateRequest, PayoutConfirmRequest)] - pub payout_id: Option, // TODO: #1321 https://github.com/juspay/hyperswitch/issues/1321 + pub payout_id: Option, /// This is an identifier for the merchant account. This is inferred from the API key provided during the request, **not required to be included in the Payout Create/Update Request.** #[schema(max_length = 255, value_type = Option, example = "merchant_1668273825")] #[remove_in(PayoutsCreateRequest, PayoutUpdateRequest, PayoutConfirmRequest)] pub merchant_id: Option, + /// Your unique identifier for this payout or order. This ID helps you reconcile payouts on your system. If provided, it is passed to the connector if supported. + #[schema(value_type = Option, max_length = 255, example = "merchant_order_ref_123")] + pub merchant_order_reference_id: Option, + /// The payout amount. Amount for the payout in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., #[schema(value_type = Option, example = 1000)] #[mandatory_in(PayoutsCreateRequest = u64)] @@ -399,13 +403,17 @@ pub struct PayoutCreateResponse { max_length = 30, example = "187282ab-40ef-47a9-9206-5099ba31e432" )] - pub payout_id: String, // TODO: Update this to PayoutIdType similar to PaymentIdType + pub payout_id: id_type::PayoutId, /// This is an identifier for the merchant account. This is inferred from the API key /// provided during the request #[schema(max_length = 255, value_type = String, example = "merchant_1668273825")] pub merchant_id: id_type::MerchantId, + /// Your unique identifier for this payout or order. This ID helps you reconcile payouts on your system. If provided, it is passed to the connector if supported. + #[schema(value_type = Option, max_length = 255, example = "merchant_order_ref_123")] + pub merchant_order_reference_id: Option, + /// The payout amount. Amount for the payout in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., #[schema(value_type = i64, example = 1000)] pub amount: common_utils::types::MinorUnit, @@ -638,7 +646,7 @@ pub struct PayoutRetrieveBody { pub merchant_id: Option, } -#[derive(Default, Debug, Serialize, ToSchema, Clone, Deserialize)] +#[derive(Debug, Serialize, ToSchema, Clone, Deserialize)] pub struct PayoutRetrieveRequest { /// Unique identifier for the payout. This ensures idempotency for multiple payouts /// that have been done by a single merchant. This field is auto generated and is returned in the API response. @@ -648,7 +656,7 @@ pub struct PayoutRetrieveRequest { max_length = 30, example = "187282ab-40ef-47a9-9206-5099ba31e432" )] - pub payout_id: String, + pub payout_id: id_type::PayoutId, /// `force_sync` with the connector to get payout details /// (defaults to false) @@ -660,9 +668,7 @@ pub struct PayoutRetrieveRequest { pub merchant_id: Option, } -#[derive( - Default, Debug, Deserialize, Serialize, Clone, ToSchema, router_derive::PolymorphicSchema, -)] +#[derive(Debug, Serialize, Clone, ToSchema, router_derive::PolymorphicSchema)] #[generate_schemas(PayoutCancelRequest, PayoutFulfillRequest)] pub struct PayoutActionRequest { /// Unique identifier for the payout. This ensures idempotency for multiple payouts @@ -673,8 +679,7 @@ pub struct PayoutActionRequest { max_length = 30, example = "187282ab-40ef-47a9-9206-5099ba31e432" )] - #[serde(skip_deserializing)] - pub payout_id: String, + pub payout_id: id_type::PayoutId, } #[derive(Default, Debug, ToSchema, Clone, Deserialize)] @@ -722,12 +727,12 @@ pub struct PayoutListConstraints { pub customer_id: Option, /// A cursor for use in pagination, fetch the next list after some object - #[schema(example = "pay_fafa124123")] - pub starting_after: Option, + #[schema(example = "payout_fafa124123", value_type = Option,)] + pub starting_after: Option, /// A cursor for use in pagination, fetch the previous list before some object - #[schema(example = "pay_fafa124123")] - pub ending_before: Option, + #[schema(example = "payout_fafa124123", value_type = Option,)] + pub ending_before: Option, /// limit on the number of objects to return #[schema(default = 10, maximum = 100)] @@ -755,7 +760,10 @@ pub struct PayoutListFilterConstraints { max_length = 30, example = "187282ab-40ef-47a9-9206-5099ba31e432" )] - pub payout_id: Option, + pub payout_id: Option, + /// The merchant order reference ID for payout + #[schema(value_type = Option, max_length = 255, example = "merchant_order_ref_123")] + pub merchant_order_reference_id: Option, /// The identifier for business profile #[schema(value_type = Option)] pub profile_id: Option, @@ -826,7 +834,8 @@ pub struct PayoutLinkResponse { pub struct PayoutLinkInitiateRequest { #[schema(value_type = String)] pub merchant_id: id_type::MerchantId, - pub payout_id: String, + #[schema(value_type = String)] + pub payout_id: id_type::PayoutId, } #[derive(Clone, Debug, serde::Serialize)] @@ -834,7 +843,7 @@ pub struct PayoutLinkDetails { pub publishable_key: Secret, pub client_secret: Secret, pub payout_link_id: String, - pub payout_id: String, + pub payout_id: id_type::PayoutId, pub customer_id: id_type::CustomerId, #[serde(with = "common_utils::custom_serde::iso8601")] pub session_expiry: PrimitiveDateTime, @@ -870,7 +879,7 @@ pub struct RequiredFieldsOverrideRequest { #[derive(Clone, Debug, serde::Serialize)] pub struct PayoutLinkStatusDetails { pub payout_link_id: String, - pub payout_id: String, + pub payout_id: id_type::PayoutId, pub customer_id: id_type::CustomerId, #[serde(with = "common_utils::custom_serde::iso8601")] pub session_expiry: PrimitiveDateTime, diff --git a/crates/api_models/src/webhooks.rs b/crates/api_models/src/webhooks.rs index 4667866073b..177310de18f 100644 --- a/crates/api_models/src/webhooks.rs +++ b/crates/api_models/src/webhooks.rs @@ -98,7 +98,7 @@ pub enum WebhookResponseTracker { }, #[cfg(feature = "payouts")] Payout { - payout_id: String, + payout_id: common_utils::id_type::PayoutId, status: common_enums::PayoutStatus, }, #[cfg(feature = "v1")] diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 376b57fbb47..e1964296755 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -12,7 +12,7 @@ pub trait ApiEventMetric { #[serde(tag = "flow_type", rename_all = "snake_case")] pub enum ApiEventsType { Payout { - payout_id: String, + payout_id: id_type::PayoutId, }, #[cfg(feature = "v1")] Payment { diff --git a/crates/common_utils/src/id_type.rs b/crates/common_utils/src/id_type.rs index d9daa5caf9d..6d73a90eab2 100644 --- a/crates/common_utils/src/id_type.rs +++ b/crates/common_utils/src/id_type.rs @@ -11,6 +11,7 @@ mod merchant; mod merchant_connector_account; mod organization; mod payment; +mod payout; mod profile; mod profile_acquirer; mod refunds; @@ -27,6 +28,7 @@ use diesel::{ serialize::{Output, ToSql}, sql_types, }; +pub use payout::PayoutId; use serde::{Deserialize, Serialize}; use thiserror::Error; diff --git a/crates/common_utils/src/id_type/payout.rs b/crates/common_utils/src/id_type/payout.rs new file mode 100644 index 00000000000..8faa661d363 --- /dev/null +++ b/crates/common_utils/src/id_type/payout.rs @@ -0,0 +1,10 @@ +crate::id_type!( + PayoutId, + "A domain type for payout_id that can be used for payout ids" +); +crate::impl_id_type_methods!(PayoutId, "payout_id"); +crate::impl_debug_id_type!(PayoutId); +crate::impl_try_from_cow_str_id_type!(PayoutId, "payout_id"); +crate::impl_generate_id_id_type!(PayoutId, "payout"); +crate::impl_queryable_id_type!(PayoutId); +crate::impl_to_sql_from_sql_id_type!(PayoutId); diff --git a/crates/common_utils/src/link_utils.rs b/crates/common_utils/src/link_utils.rs index 2201000d062..89a52f44792 100644 --- a/crates/common_utils/src/link_utils.rs +++ b/crates/common_utils/src/link_utils.rs @@ -149,7 +149,7 @@ pub struct PayoutLinkData { /// Identifier for the customer pub customer_id: id_type::CustomerId, /// Identifier for the payouts resource - pub payout_id: String, + pub payout_id: id_type::PayoutId, /// Link to render the payout link pub link: url::Url, /// Client secret generated for authenticating frontend APIs diff --git a/crates/diesel_models/src/events.rs b/crates/diesel_models/src/events.rs index cbc39340dcf..bc9f0788113 100644 --- a/crates/diesel_models/src/events.rs +++ b/crates/diesel_models/src/events.rs @@ -74,7 +74,7 @@ pub enum EventMetadata { payment_id: common_utils::id_type::GlobalPaymentId, }, Payout { - payout_id: String, + payout_id: common_utils::id_type::PayoutId, }, #[cfg(feature = "v1")] Refund { diff --git a/crates/diesel_models/src/generic_link.rs b/crates/diesel_models/src/generic_link.rs index 28402692480..d6071268eae 100644 --- a/crates/diesel_models/src/generic_link.rs +++ b/crates/diesel_models/src/generic_link.rs @@ -156,7 +156,7 @@ pub struct PaymentMethodCollectLinkData { #[diesel(primary_key(link_id))] pub struct PayoutLink { pub link_id: String, - pub primary_reference: String, + pub primary_reference: common_utils::id_type::PayoutId, pub merchant_id: common_utils::id_type::MerchantId, #[serde(with = "common_utils::custom_serde::iso8601")] pub created_at: PrimitiveDateTime, diff --git a/crates/diesel_models/src/payout_attempt.rs b/crates/diesel_models/src/payout_attempt.rs index 0e12a9ed801..a908ef7bdf5 100644 --- a/crates/diesel_models/src/payout_attempt.rs +++ b/crates/diesel_models/src/payout_attempt.rs @@ -14,7 +14,7 @@ use crate::{enums as storage_enums, schema::payout_attempt}; #[diesel(table_name = payout_attempt, primary_key(payout_attempt_id), check_for_backend(diesel::pg::Pg))] pub struct PayoutAttempt { pub payout_attempt_id: String, - pub payout_id: String, + pub payout_id: common_utils::id_type::PayoutId, pub customer_id: Option, pub merchant_id: common_utils::id_type::MerchantId, pub address_id: Option, @@ -37,6 +37,7 @@ pub struct PayoutAttempt { pub unified_code: Option, pub unified_message: Option, pub additional_payout_method_data: Option, + pub merchant_order_reference_id: Option, } #[derive( @@ -53,7 +54,7 @@ pub struct PayoutAttempt { #[diesel(table_name = payout_attempt)] pub struct PayoutAttemptNew { pub payout_attempt_id: String, - pub payout_id: String, + pub payout_id: common_utils::id_type::PayoutId, pub customer_id: Option, pub merchant_id: common_utils::id_type::MerchantId, pub address_id: Option, @@ -76,6 +77,7 @@ pub struct PayoutAttemptNew { pub unified_code: Option, pub unified_message: Option, pub additional_payout_method_data: Option, + pub merchant_order_reference_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -128,6 +130,7 @@ pub struct PayoutAttemptUpdateInternal { pub unified_code: Option, pub unified_message: Option, pub additional_payout_method_data: Option, + pub merchant_order_reference_id: Option, } impl Default for PayoutAttemptUpdateInternal { @@ -150,6 +153,7 @@ impl Default for PayoutAttemptUpdateInternal { unified_code: None, unified_message: None, additional_payout_method_data: None, + merchant_order_reference_id: None, } } } @@ -231,6 +235,7 @@ impl PayoutAttemptUpdate { unified_code, unified_message, additional_payout_method_data, + merchant_order_reference_id, } = self.into(); PayoutAttempt { payout_token: payout_token.or(source.payout_token), @@ -251,6 +256,8 @@ impl PayoutAttemptUpdate { unified_message: unified_message.or(source.unified_message), additional_payout_method_data: additional_payout_method_data .or(source.additional_payout_method_data), + merchant_order_reference_id: merchant_order_reference_id + .or(source.merchant_order_reference_id), ..source } } diff --git a/crates/diesel_models/src/payouts.rs b/crates/diesel_models/src/payouts.rs index 8acdc4a0a5a..881875494ab 100644 --- a/crates/diesel_models/src/payouts.rs +++ b/crates/diesel_models/src/payouts.rs @@ -11,7 +11,7 @@ use crate::{enums as storage_enums, schema::payouts}; )] #[diesel(table_name = payouts, primary_key(payout_id), check_for_backend(diesel::pg::Pg))] pub struct Payouts { - pub payout_id: String, + pub payout_id: common_utils::id_type::PayoutId, pub merchant_id: common_utils::id_type::MerchantId, pub customer_id: Option, pub address_id: Option, @@ -52,7 +52,7 @@ pub struct Payouts { )] #[diesel(table_name = payouts)] pub struct PayoutsNew { - pub payout_id: String, + pub payout_id: common_utils::id_type::PayoutId, pub merchant_id: common_utils::id_type::MerchantId, pub customer_id: Option, pub address_id: Option, diff --git a/crates/diesel_models/src/query/generic_link.rs b/crates/diesel_models/src/query/generic_link.rs index 75f1fc8ded8..96966e14547 100644 --- a/crates/diesel_models/src/query/generic_link.rs +++ b/crates/diesel_models/src/query/generic_link.rs @@ -224,7 +224,11 @@ impl TryFrom for PayoutLink { Ok(Self { link_id: db_val.link_id, - primary_reference: db_val.primary_reference, + primary_reference: common_utils::id_type::PayoutId::try_from(std::borrow::Cow::Owned( + db_val.primary_reference, + )) + .change_context(errors::ParsingError::UnknownError) + .attach_printable("Failed to parse PayoutId from primary_reference string")?, merchant_id: db_val.merchant_id, created_at: db_val.created_at, last_modified_at: db_val.last_modified_at, diff --git a/crates/diesel_models/src/query/payout_attempt.rs b/crates/diesel_models/src/query/payout_attempt.rs index 939336a223a..300b6fdb647 100644 --- a/crates/diesel_models/src/query/payout_attempt.rs +++ b/crates/diesel_models/src/query/payout_attempt.rs @@ -56,7 +56,7 @@ impl PayoutAttempt { pub async fn find_by_merchant_id_payout_id( conn: &PgPooledConn, merchant_id: &common_utils::id_type::MerchantId, - payout_id: &str, + payout_id: &common_utils::id_type::PayoutId, ) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, @@ -95,10 +95,24 @@ impl PayoutAttempt { .await } + pub async fn find_by_merchant_id_merchant_order_reference_id( + conn: &PgPooledConn, + merchant_id_input: &common_utils::id_type::MerchantId, + merchant_order_reference_id_input: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id.eq(merchant_id_input.to_owned()).and( + dsl::merchant_order_reference_id.eq(merchant_order_reference_id_input.to_owned()), + ), + ) + .await + } + pub async fn update_by_merchant_id_payout_id( conn: &PgPooledConn, merchant_id: &common_utils::id_type::MerchantId, - payout_id: &str, + payout_id: &common_utils::id_type::PayoutId, payout: PayoutAttemptUpdate, ) -> StorageResult { generics::generic_update_with_results::<::Table, _, _, _>( @@ -152,7 +166,7 @@ impl PayoutAttempt { .map(|payout| { format!( "{}_{}", - payout.payout_id.clone(), + payout.payout_id.get_string_repr(), payout.attempt_count.clone() ) }) @@ -160,8 +174,8 @@ impl PayoutAttempt { let active_payout_ids = payouts .iter() - .map(|payout| payout.payout_id.clone()) - .collect::>(); + .map(|payout| payout.payout_id.to_owned()) + .collect::>(); let filter = ::table() .filter(dsl::merchant_id.eq(merchant_id.to_owned())) diff --git a/crates/diesel_models/src/query/payouts.rs b/crates/diesel_models/src/query/payouts.rs index 25c7bfc4f7e..e7d06d05ffe 100644 --- a/crates/diesel_models/src/query/payouts.rs +++ b/crates/diesel_models/src/query/payouts.rs @@ -47,7 +47,7 @@ impl Payouts { pub async fn find_by_merchant_id_payout_id( conn: &PgPooledConn, merchant_id: &common_utils::id_type::MerchantId, - payout_id: &str, + payout_id: &common_utils::id_type::PayoutId, ) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, @@ -61,7 +61,7 @@ impl Payouts { pub async fn update_by_merchant_id_payout_id( conn: &PgPooledConn, merchant_id: &common_utils::id_type::MerchantId, - payout_id: &str, + payout_id: &common_utils::id_type::PayoutId, payout: PayoutsUpdate, ) -> StorageResult { generics::generic_update_with_results::<::Table, _, _, _>( @@ -82,7 +82,7 @@ impl Payouts { pub async fn find_optional_by_merchant_id_payout_id( conn: &PgPooledConn, merchant_id: &common_utils::id_type::MerchantId, - payout_id: &str, + payout_id: &common_utils::id_type::PayoutId, ) -> StorageResult> { generics::generic_find_one_optional::<::Table, _, _>( conn, @@ -96,7 +96,7 @@ impl Payouts { pub async fn get_total_count_of_payouts( conn: &PgPooledConn, merchant_id: &common_utils::id_type::MerchantId, - active_payout_ids: &[String], + active_payout_ids: &[common_utils::id_type::PayoutId], connector: Option>, currency: Option>, status: Option>, @@ -106,7 +106,7 @@ impl Payouts { .inner_join(payout_attempt::table.on(payout_attempt::dsl::payout_id.eq(dsl::payout_id))) .count() .filter(dsl::merchant_id.eq(merchant_id.to_owned())) - .filter(dsl::payout_id.eq_any(active_payout_ids.to_owned())) + .filter(dsl::payout_id.eq_any(active_payout_ids.to_vec())) .into_boxed(); if let Some(connector) = connector { diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index e303dfc553f..1629780458c 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1151,7 +1151,7 @@ diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; - payout_attempt (payout_attempt_id) { + payout_attempt (merchant_id, payout_attempt_id) { #[max_length = 64] payout_attempt_id -> Varchar, #[max_length = 64] @@ -1188,6 +1188,8 @@ diesel::table! { #[max_length = 1024] unified_message -> Nullable, additional_payout_method_data -> Nullable, + #[max_length = 255] + merchant_order_reference_id -> Nullable, } } @@ -1195,7 +1197,7 @@ diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; - payouts (payout_id) { + payouts (merchant_id, payout_id) { #[max_length = 64] payout_id -> Varchar, #[max_length = 64] diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 9ba5cdfaff6..c931cec977a 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1090,7 +1090,7 @@ diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; - payout_attempt (payout_attempt_id) { + payout_attempt (merchant_id, payout_attempt_id) { #[max_length = 64] payout_attempt_id -> Varchar, #[max_length = 64] @@ -1127,6 +1127,8 @@ diesel::table! { #[max_length = 1024] unified_message -> Nullable, additional_payout_method_data -> Nullable, + #[max_length = 255] + merchant_order_reference_id -> Nullable, } } @@ -1134,7 +1136,7 @@ diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; - payouts (payout_id) { + payouts (merchant_id, payout_id) { #[max_length = 64] payout_id -> Varchar, #[max_length = 64] diff --git a/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs b/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs index 8eec0abb9fc..8f4d667711b 100644 --- a/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs @@ -5351,7 +5351,7 @@ impl TryFrom<&AdyenRouterData<&PayoutsRouterData>> for AdyenPayoutCreateRe }, merchant_account, payment_data: PayoutPaymentMethodData::PayoutWalletData(payout_wallet), - reference: item.router_data.request.payout_id.to_owned(), + reference: item.router_data.connector_request_reference_id.clone(), shopper_reference: item.router_data.merchant_id.get_string_repr().to_owned(), shopper_email: customer_email, shopper_name: ShopperName { diff --git a/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs b/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs index 7262ec9f3ff..82c9afadeed 100644 --- a/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs +++ b/crates/hyperswitch_connectors/src/connectors/adyenplatform/transformers/payouts.rs @@ -361,7 +361,7 @@ impl TryFrom<&AdyenPlatformRouterData<&types::PayoutsRouterData>> for Adye counterparty, priority, reference: item.router_data.connector_request_reference_id.clone(), - reference_for_beneficiary: request.payout_id.clone(), + reference_for_beneficiary: item.router_data.connector_request_reference_id.clone(), description: item.router_data.description.clone(), }) } diff --git a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs index 6b4101a15d2..09b468c3caf 100644 --- a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs @@ -3958,7 +3958,7 @@ impl TryFrom<&CybersourceRouterData<&PayoutsRouterData>> match payout_type { enums::PayoutType::Card => { let client_reference_information = ClientReferenceInformation { - code: Some(item.router_data.request.payout_id.clone()), + code: Some(item.router_data.connector_request_reference_id.clone()), }; let order_information = OrderInformation { @@ -3974,7 +3974,7 @@ impl TryFrom<&CybersourceRouterData<&PayoutsRouterData>> CybersourceRecipientInfo::try_from((billing_address, phone_address))?; let sender_information = CybersourceSenderInfo { - reference_number: item.router_data.request.payout_id.clone(), + reference_number: item.router_data.connector_request_reference_id.clone(), account: CybersourceAccountInfo { funds_source: CybersourcePayoutFundSourceType::Disbursement, }, diff --git a/crates/hyperswitch_connectors/src/connectors/nomupay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/nomupay/transformers.rs index 9a1a50e8642..3eab923ff16 100644 --- a/crates/hyperswitch_connectors/src/connectors/nomupay/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/nomupay/transformers.rs @@ -385,7 +385,7 @@ impl TryFrom<&PayoutsRouterData> for OnboardSubAccountRequest { match payout_type { Some(common_enums::PayoutType::Bank) => Ok(Self { account_id: nomupay_auth_type.eid, - client_sub_account_id: Secret::new(request.payout_id), + client_sub_account_id: Secret::new(item.connector_request_reference_id.clone()), profile, }), _ => Err(errors::ConnectorError::NotImplemented( @@ -498,7 +498,7 @@ impl TryFrom<(&PayoutsRouterData, FloatMajorUnit)> for NomupayPaymentReque Ok(Self { source_id: nomupay_auth_type.eid, destination_id: Secret::new(destination), - payment_reference: item.request.clone().payout_id, + payment_reference: item.connector_request_reference_id.clone(), amount, currency_code: item.request.destination_currency, purpose: PURPOSE_OF_PAYMENT_IS_OTHER.to_string(), diff --git a/crates/hyperswitch_connectors/src/connectors/paypal/transformers.rs b/crates/hyperswitch_connectors/src/connectors/paypal/transformers.rs index 6b56ccea208..18c7ac02e71 100644 --- a/crates/hyperswitch_connectors/src/connectors/paypal/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/paypal/transformers.rs @@ -2309,7 +2309,7 @@ impl TryFrom<&PaypalRouterData<&PayoutsRouterData>> for PaypalFulfill let item_data = PaypalPayoutItem::try_from(item)?; Ok(Self { sender_batch_header: PayoutBatchHeader { - sender_batch_id: item.router_data.request.payout_id.to_owned(), + sender_batch_id: item.router_data.connector_request_reference_id.to_owned(), }, items: vec![item_data], }) diff --git a/crates/hyperswitch_connectors/src/connectors/stripe/transformers/connect.rs b/crates/hyperswitch_connectors/src/connectors/stripe/transformers/connect.rs index bb8d4509dac..321aad76551 100644 --- a/crates/hyperswitch_connectors/src/connectors/stripe/transformers/connect.rs +++ b/crates/hyperswitch_connectors/src/connectors/stripe/transformers/connect.rs @@ -216,7 +216,7 @@ impl TryFrom<&PayoutsRouterData> for StripeConnectPayoutCreateRequest { amount: request.amount, currency: request.destination_currency, destination: connector_customer_id, - transfer_group: request.payout_id, + transfer_group: item.connector_request_reference_id.clone(), }) } } diff --git a/crates/hyperswitch_connectors/src/connectors/wise/transformers.rs b/crates/hyperswitch_connectors/src/connectors/wise/transformers.rs index 1a9ff7bf89f..f844e15a488 100644 --- a/crates/hyperswitch_connectors/src/connectors/wise/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/wise/transformers.rs @@ -510,7 +510,7 @@ impl TryFrom<&PayoutsRouterData> for WisePayoutCreateRequest { Ok(Self { target_account, quote_uuid, - customer_transaction_id: request.payout_id, + customer_transaction_id: request.payout_id.get_string_repr().to_string(), details: wise_transfer_details, }) } diff --git a/crates/hyperswitch_domain_models/src/errors/api_error_response.rs b/crates/hyperswitch_domain_models/src/errors/api_error_response.rs index 21accfa22c5..2924093da3a 100644 --- a/crates/hyperswitch_domain_models/src/errors/api_error_response.rs +++ b/crates/hyperswitch_domain_models/src/errors/api_error_response.rs @@ -79,8 +79,10 @@ pub enum ApiErrorResponse { DuplicatePayment { payment_id: common_utils::id_type::PaymentId, }, - #[error(error_type = ErrorType::DuplicateRequest, code = "HE_01", message = "The payout with the specified payout_id '{payout_id}' already exists in our records")] - DuplicatePayout { payout_id: String }, + #[error(error_type = ErrorType::DuplicateRequest, code = "HE_01", message = "The payout with the specified payout_id '{payout_id:?}' already exists in our records")] + DuplicatePayout { + payout_id: common_utils::id_type::PayoutId, + }, #[error(error_type = ErrorType::DuplicateRequest, code = "HE_01", message = "The config with the specified key already exists in our records")] DuplicateConfig, #[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "Refund does not exist in our records")] @@ -403,7 +405,7 @@ impl ErrorSwitch for ApiErrorRespon AER::BadRequest(ApiError::new("HE", 1, "The payment with the specified payment_id already exists in our records", Some(Extra {reason: Some(format!("{payment_id:?} already exists")), ..Default::default()}))) } Self::DuplicatePayout { payout_id } => { - AER::BadRequest(ApiError::new("HE", 1, format!("The payout with the specified payout_id '{payout_id}' already exists in our records"), None)) + AER::BadRequest(ApiError::new("HE", 1, format!("The payout with the specified payout_id '{payout_id:?}' already exists in our records"), None)) } Self::DuplicateConfig => { AER::BadRequest(ApiError::new("HE", 1, "The config with the specified key already exists in our records", None)) diff --git a/crates/hyperswitch_domain_models/src/payouts.rs b/crates/hyperswitch_domain_models/src/payouts.rs index 8c6d751ebec..9a40dc119c0 100644 --- a/crates/hyperswitch_domain_models/src/payouts.rs +++ b/crates/hyperswitch_domain_models/src/payouts.rs @@ -7,7 +7,7 @@ use common_utils::{consts, id_type}; use time::PrimitiveDateTime; pub enum PayoutFetchConstraints { - Single { payout_id: String }, + Single { payout_id: id_type::PayoutId }, List(Box), } @@ -21,10 +21,11 @@ pub struct PayoutListParams { pub payout_method: Option>, pub profile_id: Option, pub customer_id: Option, - pub starting_after_id: Option, - pub ending_before_id: Option, + pub starting_after_id: Option, + pub ending_before_id: Option, pub entity_type: Option, pub limit: Option, + pub merchant_order_reference_id: Option, } impl From for PayoutFetchConstraints { @@ -44,6 +45,7 @@ impl From for PayoutFetchConstraints starting_after_id: value.starting_after, ending_before_id: value.ending_before, entity_type: None, + merchant_order_reference_id: None, limit: Some(std::cmp::min( value.limit, consts::PAYOUTS_LIST_MAX_LIMIT_GET, @@ -67,6 +69,7 @@ impl From for PayoutFetchConstraints { starting_after_id: None, ending_before_id: None, entity_type: None, + merchant_order_reference_id: None, limit: None, })) } @@ -90,6 +93,7 @@ impl From for PayoutFetchConst starting_after_id: None, ending_before_id: None, entity_type: value.entity_type, + merchant_order_reference_id: value.merchant_order_reference_id, limit: Some(std::cmp::min( value.limit, consts::PAYOUTS_LIST_MAX_LIMIT_POST, diff --git a/crates/hyperswitch_domain_models/src/payouts/payout_attempt.rs b/crates/hyperswitch_domain_models/src/payouts/payout_attempt.rs index dcb61bddc97..b6293faa841 100644 --- a/crates/hyperswitch_domain_models/src/payouts/payout_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payouts/payout_attempt.rs @@ -42,6 +42,13 @@ pub trait PayoutAttemptInterface { _storage_scheme: MerchantStorageScheme, ) -> error_stack::Result; + async fn find_payout_attempt_by_merchant_id_merchant_order_reference_id( + &self, + _merchant_id: &id_type::MerchantId, + _merchant_order_reference_id: &str, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result; + async fn get_filters_for_payouts( &self, _payout: &[Payouts], @@ -61,7 +68,7 @@ pub struct PayoutListFilters { #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct PayoutAttempt { pub payout_attempt_id: String, - pub payout_id: String, + pub payout_id: id_type::PayoutId, pub customer_id: Option, pub merchant_id: id_type::MerchantId, pub address_id: Option, @@ -84,12 +91,13 @@ pub struct PayoutAttempt { pub unified_code: Option, pub unified_message: Option, pub additional_payout_method_data: Option, + pub merchant_order_reference_id: Option, } #[derive(Clone, Debug, PartialEq)] pub struct PayoutAttemptNew { pub payout_attempt_id: String, - pub payout_id: String, + pub payout_id: id_type::PayoutId, pub customer_id: Option, pub merchant_id: id_type::MerchantId, pub address_id: Option, @@ -110,6 +118,7 @@ pub struct PayoutAttemptNew { pub unified_code: Option, pub unified_message: Option, pub additional_payout_method_data: Option, + pub merchant_order_reference_id: Option, } #[derive(Debug, Clone)] diff --git a/crates/hyperswitch_domain_models/src/payouts/payouts.rs b/crates/hyperswitch_domain_models/src/payouts/payouts.rs index 7c34b3c26df..1d41a5f9a32 100644 --- a/crates/hyperswitch_domain_models/src/payouts/payouts.rs +++ b/crates/hyperswitch_domain_models/src/payouts/payouts.rs @@ -20,7 +20,7 @@ pub trait PayoutsInterface { async fn find_payout_by_merchant_id_payout_id( &self, _merchant_id: &id_type::MerchantId, - _payout_id: &str, + _payout_id: &id_type::PayoutId, _storage_scheme: MerchantStorageScheme, ) -> error_stack::Result; @@ -35,7 +35,7 @@ pub trait PayoutsInterface { async fn find_optional_payout_by_merchant_id_payout_id( &self, _merchant_id: &id_type::MerchantId, - _payout_id: &str, + _payout_id: &id_type::PayoutId, _storage_scheme: MerchantStorageScheme, ) -> error_stack::Result, Self::Error>; @@ -76,7 +76,7 @@ pub trait PayoutsInterface { async fn get_total_count_of_filtered_payouts( &self, _merchant_id: &id_type::MerchantId, - _active_payout_ids: &[String], + _active_payout_ids: &[id_type::PayoutId], _connector: Option>, _currency: Option>, _status: Option>, @@ -88,12 +88,12 @@ pub trait PayoutsInterface { &self, _merchant_id: &id_type::MerchantId, _constraints: &PayoutFetchConstraints, - ) -> error_stack::Result, Self::Error>; + ) -> error_stack::Result, Self::Error>; } #[derive(Clone, Debug, Eq, PartialEq)] pub struct Payouts { - pub payout_id: String, + pub payout_id: id_type::PayoutId, pub merchant_id: id_type::MerchantId, pub customer_id: Option, pub address_id: Option, @@ -121,7 +121,7 @@ pub struct Payouts { #[derive(Clone, Debug, Eq, PartialEq)] pub struct PayoutsNew { - pub payout_id: String, + pub payout_id: id_type::PayoutId, pub merchant_id: id_type::MerchantId, pub customer_id: Option, pub address_id: Option, diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index e811b872c8c..c7987122ead 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -890,7 +890,7 @@ pub struct UploadFileRequestData { #[cfg(feature = "payouts")] #[derive(Debug, Clone)] pub struct PayoutsData { - pub payout_id: String, + pub payout_id: id_type::PayoutId, pub amount: i64, pub connector_payout_id: Option, pub destination_currency: storage_enums::Currency, diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 58f707b2db6..83f7fe69053 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -1,4 +1,4 @@ -use common_utils::errors::ErrorSwitch; +use common_utils::{errors::ErrorSwitch, id_type}; use hyperswitch_domain_models::errors::api_error_response as errors; use crate::core::errors::CustomersErrorResponse; @@ -133,7 +133,7 @@ pub enum StripeErrorCode { EventNotFound, #[error(error_type = StripeErrorType::InvalidRequestError, code = "token_already_used", message = "Duplicate payout request")] - DuplicatePayout { payout_id: String }, + DuplicatePayout { payout_id: id_type::PayoutId }, #[error(error_type = StripeErrorType::InvalidRequestError, code = "parameter_missing", message = "Return url is not available")] ReturnUrlUnavailable, @@ -209,9 +209,7 @@ pub enum StripeErrorCode { PaymentIntentMandateInvalid { message: String }, #[error(error_type = StripeErrorType::InvalidRequestError, code = "", message = "The payment with the specified payment_id already exists in our records.")] - DuplicatePayment { - payment_id: common_utils::id_type::PaymentId, - }, + DuplicatePayment { payment_id: id_type::PaymentId }, #[error(error_type = StripeErrorType::ConnectorError, code = "", message = "{code}: {message}")] ExternalConnectorError { diff --git a/crates/router/src/compatibility/stripe/webhooks.rs b/crates/router/src/compatibility/stripe/webhooks.rs index f5dde401b35..3bd0f83b7db 100644 --- a/crates/router/src/compatibility/stripe/webhooks.rs +++ b/crates/router/src/compatibility/stripe/webhooks.rs @@ -4,9 +4,12 @@ use api_models::{ enums::{Currency, DisputeStatus, MandateStatus}, webhooks::{self as api}, }; -#[cfg(feature = "payouts")] -use common_utils::pii::{self, Email}; use common_utils::{crypto::SignMessage, date_time, ext_traits::Encode}; +#[cfg(feature = "payouts")] +use common_utils::{ + id_type, + pii::{self, Email}, +}; use error_stack::ResultExt; use router_env::logger; use serde::Serialize; @@ -94,7 +97,7 @@ pub struct StripeDisputeResponse { pub id: String, pub amount: String, pub currency: Currency, - pub payment_intent: common_utils::id_type::PaymentId, + pub payment_intent: id_type::PaymentId, pub reason: Option, pub status: StripeDisputeStatus, } @@ -110,7 +113,7 @@ pub struct StripeMandateResponse { #[cfg(feature = "payouts")] #[derive(Clone, Serialize, Debug)] pub struct StripePayoutResponse { - pub id: String, + pub id: id_type::PayoutId, pub amount: i64, pub currency: String, pub payout_type: Option, diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 4cc667bdc8a..7126ed9617e 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -483,9 +483,11 @@ pub async fn perform_static_routing_v1( .get_string_repr() .to_string(), #[cfg(feature = "payouts")] - routing::TransactionData::Payout(payout_data) => { - payout_data.payout_attempt.payout_id.clone() - } + routing::TransactionData::Payout(payout_data) => payout_data + .payout_attempt + .payout_id + .get_string_repr() + .to_string(), }; let routing_events_wrapper = utils::RoutingEventsWrapper::new( diff --git a/crates/router/src/core/payout_link.rs b/crates/router/src/core/payout_link.rs index 8c234889630..1d224ffbd79 100644 --- a/crates/router/src/core/payout_link.rs +++ b/crates/router/src/core/payout_link.rs @@ -22,6 +22,7 @@ use crate::{ routes::{app::StorageInterface, SessionState}, services, types::{api, domain, transformers::ForeignFrom}, + utils::get_payout_attempt_id, }; #[cfg(feature = "v2")] @@ -56,7 +57,7 @@ pub async fn initiate_payout_link( let payout_attempt = db .find_payout_attempt_by_merchant_id_payout_attempt_id( merchant_id, - &format!("{}_{}", payout.payout_id, payout.attempt_count), + &get_payout_attempt_id(payout.payout_id.get_string_repr(), payout.attempt_count), merchant_context.get_merchant_account().storage_scheme, ) .await @@ -146,13 +147,11 @@ pub async fn initiate_payout_link( .await .change_context(errors::ApiErrorResponse::InvalidRequestData { message: format!( - "Customer [{}] not found for link_id - {}", - payout_link.primary_reference, payout_link.link_id + "Customer [{:?}] not found for link_id - {}", + customer_id, payout_link.link_id ), }) - .attach_printable_lazy(|| { - format!("customer [{}] not found", payout_link.primary_reference) - })?; + .attach_printable_lazy(|| format!("customer [{:?}] not found", customer_id))?; let address = payout .address_id .as_ref() @@ -169,7 +168,7 @@ pub async fn initiate_payout_link( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable_lazy(|| { format!( - "Failed while fetching address [id - {:?}] for payout [id - {}]", + "Failed while fetching address [id - {:?}] for payout [id - {:?}]", payout.address_id, payout.payout_id ) })?; @@ -227,7 +226,7 @@ pub async fn initiate_payout_link( ), client_secret: link_data.client_secret.clone(), payout_link_id: payout_link.link_id, - payout_id: payout_link.primary_reference, + payout_id: payout_link.primary_reference.clone(), customer_id: customer.customer_id, session_expiry: payout_link.expiry, return_url: payout_link @@ -284,7 +283,7 @@ pub async fn initiate_payout_link( .await?; let js_data = payouts::PayoutLinkStatusDetails { payout_link_id: payout_link.link_id, - payout_id: payout_link.primary_reference, + payout_id: payout_link.primary_reference.clone(), customer_id: link_data.customer_id, session_expiry: payout_link.expiry, return_url: payout_link diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index 495bea84eac..3cc996bd9a6 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -17,7 +17,7 @@ use common_enums::PayoutRetryType; use common_utils::{ consts, ext_traits::{AsyncExt, ValueExt}, - id_type::CustomerId, + id_type::{self, GenerateId}, link_utils::{GenericLinkStatus, GenericLinkUiConfig, PayoutLinkData, PayoutLinkStatus}, types::{MinorUnit, UnifiedCode, UnifiedMessage}, }; @@ -73,7 +73,7 @@ pub struct PayoutData { pub payouts: storage::Payouts, pub payout_attempt: storage::PayoutAttempt, pub payout_method_data: Option, - pub profile_id: common_utils::id_type::ProfileId, + pub profile_id: id_type::ProfileId, pub should_terminate: bool, pub payout_link: Option, pub current_locale: String, @@ -451,7 +451,7 @@ pub async fn payouts_update_core( if helpers::is_payout_terminal_state(status) || helpers::is_payout_initiated(status) { return Err(report!(errors::ApiErrorResponse::InvalidRequestData { message: format!( - "Payout {} cannot be updated for status {}", + "Payout {:?} cannot be updated for status {}", payout_id, status ), })); @@ -506,7 +506,7 @@ pub async fn payouts_update_core( pub async fn payouts_retrieve_core( state: SessionState, merchant_context: domain::MerchantContext, - profile_id: Option, + profile_id: Option, req: payouts::PayoutRetrieveRequest, ) -> RouterResponse { let mut payout_data = Box::pin(make_payout_data( @@ -568,7 +568,7 @@ pub async fn payouts_cancel_core( if helpers::is_payout_terminal_state(status) { return Err(report!(errors::ApiErrorResponse::InvalidRequestData { message: format!( - "Payout {} cannot be cancelled for status {}", + "Payout {:?} cannot be cancelled for status {}", payout_attempt.payout_id, status ), })); @@ -663,7 +663,7 @@ pub async fn payouts_fulfill_core( { return Err(report!(errors::ApiErrorResponse::InvalidRequestData { message: format!( - "Payout {} cannot be fulfilled for status {}", + "Payout {:?} cannot be fulfilled for status {}", payout_attempt.payout_id, status ), })); @@ -733,7 +733,7 @@ pub async fn payouts_fulfill_core( pub async fn payouts_list_core( _state: SessionState, _merchant_context: domain::MerchantContext, - _profile_id_list: Option>, + _profile_id_list: Option>, _constraints: payouts::PayoutListConstraints, ) -> RouterResponse { todo!() @@ -743,7 +743,7 @@ pub async fn payouts_list_core( pub async fn payouts_list_core( state: SessionState, merchant_context: domain::MerchantContext, - profile_id_list: Option>, + profile_id_list: Option>, constraints: payouts::PayoutListConstraints, ) -> RouterResponse { validator::validate_payout_list_request(&constraints)?; @@ -765,7 +765,10 @@ pub async fn payouts_list_core( match db .find_payout_attempt_by_merchant_id_payout_attempt_id( merchant_id, - &utils::get_payout_attempt_id(payout.payout_id.clone(), payout.attempt_count), + &utils::get_payout_attempt_id( + payout.payout_id.get_string_repr(), + payout.attempt_count, + ), storage_enums::MerchantStorageScheme::PostgresOnly, ) .await @@ -795,7 +798,7 @@ pub async fn payouts_list_core( }; let payout_id_as_payment_id_type = - common_utils::id_type::PaymentId::wrap(payout.payout_id.clone()) + id_type::PaymentId::wrap(payout.payout_id.get_string_repr().to_string()) .change_context(errors::ApiErrorResponse::InvalidRequestData { message: "payout_id contains invalid data".to_string(), }) @@ -862,7 +865,7 @@ pub async fn payouts_list_core( pub async fn payouts_filtered_list_core( state: SessionState, merchant_context: domain::MerchantContext, - profile_id_list: Option>, + profile_id_list: Option>, filters: payouts::PayoutListFilterConstraints, ) -> RouterResponse { let limit = &filters.limit; @@ -976,7 +979,7 @@ pub async fn payouts_filtered_list_core( pub async fn payouts_list_available_filters_core( state: SessionState, merchant_context: domain::MerchantContext, - profile_id_list: Option>, + profile_id_list: Option>, time_range: common_utils::types::TimeRange, ) -> RouterResponse { let db = state.store.as_ref(); @@ -2540,6 +2543,7 @@ pub async fn response_handler( let response = api::PayoutCreateResponse { payout_id: payouts.payout_id.to_owned(), merchant_id: merchant_context.get_merchant_account().get_id().to_owned(), + merchant_order_reference_id: payout_attempt.merchant_order_reference_id.clone(), amount: payouts.amount, currency: payouts.destination_currency.to_owned(), connector: payout_attempt.connector, @@ -2615,8 +2619,8 @@ pub async fn payout_create_db_entries( state: &SessionState, merchant_context: &domain::MerchantContext, req: &payouts::PayoutCreateRequest, - payout_id: &String, - profile_id: &common_utils::id_type::ProfileId, + payout_id: &id_type::PayoutId, + profile_id: &id_type::ProfileId, stored_payout_method_data: Option<&payouts::PayoutMethodData>, locale: &str, customer: Option<&domain::Customer>, @@ -2655,12 +2659,13 @@ pub async fn payout_create_db_entries( // We have to do this because the function that is being used to create / get address is from payments // which expects a payment_id - let payout_id_as_payment_id_type = - common_utils::id_type::PaymentId::try_from(std::borrow::Cow::Owned(payout_id.to_string())) - .change_context(errors::ApiErrorResponse::InvalidRequestData { - message: "payout_id contains invalid data".to_string(), - }) - .attach_printable("Error converting payout_id to PaymentId type")?; + let payout_id_as_payment_id_type = id_type::PaymentId::try_from(std::borrow::Cow::Owned( + payout_id.get_string_repr().to_string(), + )) + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "payout_id contains invalid data".to_string(), + }) + .attach_printable("Error converting payout_id to PaymentId type")?; // Get or create address let billing_address = payment_helpers::create_or_find_address_for_payment_by_request( @@ -2696,7 +2701,7 @@ pub async fn payout_create_db_entries( let client_secret = utils::generate_id( consts::ID_LENGTH, - format!("payout_{payout_id}_secret").as_str(), + format!("payout_{payout_id:?}_secret").as_str(), ); let amount = MinorUnit::from(req.amount.unwrap_or(api::Amount::Zero)); let status = if req.payout_method_data.is_some() @@ -2712,7 +2717,7 @@ pub async fn payout_create_db_entries( }; let payouts_req = storage::PayoutsNew { - payout_id: payout_id.to_string(), + payout_id: payout_id.clone(), merchant_id: merchant_id.to_owned(), customer_id: customer_id.to_owned(), address_id: address_id.to_owned(), @@ -2746,11 +2751,11 @@ pub async fn payout_create_db_entries( ) .await .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayout { - payout_id: payout_id.to_owned(), + payout_id: payout_id.clone(), }) .attach_printable("Error inserting payouts in db")?; // Make payout_attempt entry - let payout_attempt_id = utils::get_payout_attempt_id(payout_id, 1); + let payout_attempt_id = utils::get_payout_attempt_id(payout_id.get_string_repr(), 1); let additional_pm_data_value = req .payout_method_data @@ -2764,9 +2769,10 @@ pub async fn payout_create_db_entries( let payout_attempt_req = storage::PayoutAttemptNew { payout_attempt_id: payout_attempt_id.to_string(), - payout_id: payout_id.to_owned(), + payout_id: payout_id.clone(), additional_payout_method_data: additional_pm_data_value, merchant_id: merchant_id.to_owned(), + merchant_order_reference_id: req.merchant_order_reference_id.clone(), status, business_country: req.business_country.to_owned(), business_label: req.business_label.to_owned(), @@ -2794,7 +2800,7 @@ pub async fn payout_create_db_entries( ) .await .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayout { - payout_id: payout_id.to_owned(), + payout_id: payout_id.clone(), }) .attach_printable("Error inserting payout_attempt in db")?; @@ -2823,7 +2829,7 @@ pub async fn payout_create_db_entries( pub async fn make_payout_data( _state: &SessionState, _merchant_context: &domain::MerchantContext, - _auth_profile_id: Option, + _auth_profile_id: Option, _req: &payouts::PayoutRequest, locale: &str, ) -> RouterResult { @@ -2834,7 +2840,7 @@ pub async fn make_payout_data( pub async fn make_payout_data( state: &SessionState, merchant_context: &domain::MerchantContext, - auth_profile_id: Option, + auth_profile_id: Option, req: &payouts::PayoutRequest, locale: &str, ) -> RouterResult { @@ -2842,7 +2848,9 @@ pub async fn make_payout_data( let merchant_id = merchant_context.get_merchant_account().get_id(); let payout_id = match req { payouts::PayoutRequest::PayoutActionRequest(r) => r.payout_id.clone(), - payouts::PayoutRequest::PayoutCreateRequest(r) => r.payout_id.clone().unwrap_or_default(), + payouts::PayoutRequest::PayoutCreateRequest(r) => { + r.payout_id.clone().unwrap_or(id_type::PayoutId::generate()) + } payouts::PayoutRequest::PayoutRetrieveRequest(r) => r.payout_id.clone(), }; @@ -2856,7 +2864,8 @@ pub async fn make_payout_data( .to_not_found_response(errors::ApiErrorResponse::PayoutNotFound)?; core_utils::validate_profile_id_from_auth_layer(auth_profile_id, &payouts)?; - let payout_attempt_id = utils::get_payout_attempt_id(payout_id, payouts.attempt_count); + let payout_attempt_id = + utils::get_payout_attempt_id(payouts.payout_id.get_string_repr(), payouts.attempt_count); let mut payout_attempt = db .find_payout_attempt_by_merchant_id_payout_attempt_id( @@ -2871,9 +2880,9 @@ pub async fn make_payout_data( // We have to do this because the function that is being used to create / get address is from payments // which expects a payment_id - let payout_id_as_payment_id_type = common_utils::id_type::PaymentId::try_from( - std::borrow::Cow::Owned(payouts.payout_id.clone()), - ) + let payout_id_as_payment_id_type = id_type::PaymentId::try_from(std::borrow::Cow::Owned( + payouts.payout_id.get_string_repr().to_string(), + )) .change_context(errors::ApiErrorResponse::InvalidRequestData { message: "payout_id contains invalid data".to_string(), }) @@ -2906,7 +2915,7 @@ pub async fn make_payout_data( .map_err(|err| err.change_context(errors::ApiErrorResponse::InternalServerError)) .attach_printable_lazy(|| { format!( - "Failed while fetching optional customer [id - {:?}] for payout [id - {}]", + "Failed while fetching optional customer [id - {:?}] for payout [id - {:?}]", customer_id, payout_id ) }) @@ -3079,8 +3088,8 @@ pub async fn add_external_account_addition_task( async fn validate_and_get_business_profile( state: &SessionState, merchant_key_store: &domain::MerchantKeyStore, - profile_id: &common_utils::id_type::ProfileId, - merchant_id: &common_utils::id_type::MerchantId, + profile_id: &id_type::ProfileId, + merchant_id: &id_type::MerchantId, ) -> RouterResult { let db = &*state.store; let key_manager_state = &state.into(); @@ -3108,10 +3117,10 @@ async fn validate_and_get_business_profile( pub async fn create_payout_link( state: &SessionState, business_profile: &domain::Profile, - customer_id: &CustomerId, - merchant_id: &common_utils::id_type::MerchantId, + customer_id: &id_type::CustomerId, + merchant_id: &id_type::MerchantId, req: &payouts::PayoutCreateRequest, - payout_id: &str, + payout_id: &id_type::PayoutId, locale: &str, ) -> RouterResult { let payout_link_config_req = req.payout_link_config.to_owned(); @@ -3175,7 +3184,7 @@ pub async fn create_payout_link( .as_ref() .map_or(default_config.expiry, |expiry| *expiry); let url = format!( - "{base_url}/payout_link/{}/{payout_id}?locale={}", + "{base_url}/payout_link/{}/{payout_id:?}?locale={}", merchant_id.get_string_repr(), locale ); @@ -3214,7 +3223,7 @@ pub async fn create_payout_link( let data = PayoutLinkData { payout_link_id: payout_link_id.clone(), customer_id: customer_id.clone(), - payout_id: payout_id.to_string(), + payout_id: payout_id.clone(), link, client_secret: Secret::new(client_secret), session_expiry, @@ -3232,7 +3241,7 @@ pub async fn create_payout_link( pub async fn create_payout_link_db_entry( state: &SessionState, - merchant_id: &common_utils::id_type::MerchantId, + merchant_id: &id_type::MerchantId, payout_link_data: &PayoutLinkData, return_url: Option, ) -> RouterResult { @@ -3244,7 +3253,7 @@ pub async fn create_payout_link_db_entry( let payout_link = GenericLinkNew { link_id: payout_link_data.payout_link_id.to_string(), - primary_reference: payout_link_data.payout_id.to_string(), + primary_reference: payout_link_data.payout_id.get_string_repr().to_string(), merchant_id: merchant_id.to_owned(), link_type: common_enums::GenericLinkType::PayoutLink, link_status: GenericLinkStatus::PayoutLink(PayoutLinkStatus::Initiated), @@ -3267,9 +3276,9 @@ pub async fn create_payout_link_db_entry( pub async fn get_mca_from_profile_id( state: &SessionState, merchant_context: &domain::MerchantContext, - profile_id: &common_utils::id_type::ProfileId, + profile_id: &id_type::ProfileId, connector_name: &str, - merchant_connector_id: Option<&common_utils::id_type::MerchantConnectorAccountId>, + merchant_connector_id: Option<&id_type::MerchantConnectorAccountId>, ) -> RouterResult { let merchant_connector_account = payment_helpers::get_merchant_connector_account( state, diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 1003ee699f2..9e006ed9ce4 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -1201,7 +1201,8 @@ pub async fn update_payouts_and_payout_attempt( return Err(report!(errors::ApiErrorResponse::InvalidRequestData { message: format!( "Payout {} cannot be updated for status {}", - payout_id, status + payout_id.get_string_repr(), + status ), })); } @@ -1226,12 +1227,13 @@ pub async fn update_payouts_and_payout_attempt( // We have to do this because the function that is being used to create / get address is from payments // which expects a payment_id - let payout_id_as_payment_id_type = - id_type::PaymentId::try_from(std::borrow::Cow::Owned(payout_id.clone())) - .change_context(errors::ApiErrorResponse::InvalidRequestData { - message: "payout_id contains invalid data".to_string(), - }) - .attach_printable("Error converting payout_id to PaymentId type")?; + let payout_id_as_payment_id_type = id_type::PaymentId::try_from(std::borrow::Cow::Owned( + payout_id.get_string_repr().to_string(), + )) + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "payout_id contains invalid data for PaymentId conversion".to_string(), + }) + .attach_printable("Error converting payout_id to PaymentId type")?; // Fetch address details from request and create new or else use existing address that was attached let billing_address = payment_helpers::create_or_find_address_for_payment_by_request( diff --git a/crates/router/src/core/payouts/retry.rs b/crates/router/src/core/payouts/retry.rs index 3b5e93eaf11..30100a580d4 100644 --- a/crates/router/src/core/payouts/retry.rs +++ b/crates/router/src/core/payouts/retry.rs @@ -269,12 +269,18 @@ pub async fn modify_trackers( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Error updating payouts")?; - let payout_attempt_id = - utils::get_payout_attempt_id(payout_id.to_owned(), payout_data.payouts.attempt_count); + let payout_attempt_id = utils::get_payout_attempt_id( + payout_id.get_string_repr(), + payout_data.payouts.attempt_count, + ); let payout_attempt_req = storage::PayoutAttemptNew { payout_attempt_id: payout_attempt_id.to_string(), payout_id: payout_id.to_owned(), + merchant_order_reference_id: payout_data + .payout_attempt + .merchant_order_reference_id + .clone(), 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(), diff --git a/crates/router/src/core/payouts/transformers.rs b/crates/router/src/core/payouts/transformers.rs index a1c92ec3b29..7040559d9f4 100644 --- a/crates/router/src/core/payouts/transformers.rs +++ b/crates/router/src/core/payouts/transformers.rs @@ -69,6 +69,7 @@ impl payout_id: payout.payout_id, merchant_id: payout.merchant_id, merchant_connector_id: payout_attempt.merchant_connector_id, + merchant_order_reference_id: payout_attempt.merchant_order_reference_id.clone(), amount: payout.amount, currency: payout.destination_currency, connector: payout_attempt.connector, diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs index 35c4599dbc2..1af3f4d2cb5 100644 --- a/crates/router/src/core/payouts/validator.rs +++ b/crates/router/src/core/payouts/validator.rs @@ -3,7 +3,10 @@ use std::collections::HashSet; use actix_web::http::header; #[cfg(feature = "olap")] use common_utils::errors::CustomResult; -use common_utils::validation::validate_domain_against_allowed_domains; +use common_utils::{ + id_type::{self, GenerateId}, + validation::validate_domain_against_allowed_domains, +}; use diesel_models::generic_link::PayoutLink; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::payment_methods::PaymentMethod; @@ -29,8 +32,8 @@ use crate::{ #[instrument(skip(db))] pub async fn validate_uniqueness_of_payout_id_against_merchant_id( db: &dyn StorageInterface, - payout_id: &str, - merchant_id: &common_utils::id_type::MerchantId, + payout_id: &id_type::PayoutId, + merchant_id: &id_type::MerchantId, storage_scheme: storage::enums::MerchantStorageScheme, ) -> RouterResult> { let maybe_payouts = db @@ -75,9 +78,9 @@ pub async fn validate_create_request( merchant_context: &domain::MerchantContext, req: &payouts::PayoutCreateRequest, ) -> RouterResult<( - String, + id_type::PayoutId, Option, - common_utils::id_type::ProfileId, + id_type::ProfileId, Option, Option, )> { @@ -101,7 +104,11 @@ 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())?; + let payout_id = match req.payout_id.as_ref() { + Some(provided_payout_id) => provided_payout_id.clone(), + None => id_type::PayoutId::generate(), + }; + match validate_uniqueness_of_payout_id_against_merchant_id( db, &payout_id, @@ -111,13 +118,12 @@ pub async fn validate_create_request( .await .attach_printable_lazy(|| { format!( - "Unique violation while checking payout_id: {} against merchant_id: {:?}", - payout_id.to_owned(), - merchant_id + "Unique violation while checking payout_id: {:?} against merchant_id: {:?}", + payout_id, merchant_id ) })? { Some(_) => Err(report!(errors::ApiErrorResponse::DuplicatePayout { - payout_id: payout_id.to_owned() + payout_id: payout_id.clone() })), None => Ok(()), }?; diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 17ac3f9abd0..d0a27406eb5 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -34,7 +34,6 @@ use masking::{ExposeInterface, PeekInterface}; use maud::{html, PreEscaped}; use regex::Regex; use router_env::{instrument, tracing}; -use uuid::Uuid; use super::payments::helpers; #[cfg(feature = "payouts")] @@ -57,7 +56,7 @@ use crate::{ storage::{self, enums}, PollConfig, }, - utils::{generate_id, generate_uuid, OptionExt, ValueExt}, + utils::{generate_id, OptionExt, ValueExt}, }; pub const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLOW: &str = @@ -188,7 +187,7 @@ pub async fn construct_payout_router_data<'a, F>( minor_amount_captured: None, payment_method_status: None, request: types::PayoutsData { - payout_id: payouts.payout_id.to_owned(), + payout_id: payouts.payout_id.clone(), amount: payouts.amount.get_amount_as_i64(), minor_amount: payouts.amount, connector_payout_id: payout_attempt.connector_payout_id.clone(), @@ -622,16 +621,6 @@ pub fn get_or_generate_id( .map_or(Ok(generate_id(consts::ID_LENGTH, prefix)), validate_id) } -pub fn get_or_generate_uuid( - key: &str, - provided_id: Option<&String>, -) -> Result { - let validate_id = |id: String| validate_uuid(id, key); - provided_id - .cloned() - .map_or(Ok(generate_uuid()), validate_id) -} - fn invalid_id_format_error(key: &str) -> errors::ApiErrorResponse { errors::ApiErrorResponse::InvalidDataFormat { field_name: key.to_string(), @@ -650,13 +639,6 @@ pub fn validate_id(id: String, key: &str) -> Result Result { - match (Uuid::parse_str(&uuid), uuid.len() > consts::MAX_ID_LENGTH) { - (Ok(_), false) => Ok(uuid), - (_, _) => Err(invalid_id_format_error(key)), - } -} - #[cfg(feature = "v1")] pub fn get_split_refunds( split_refund_input: refunds_transformers::SplitRefundInput, diff --git a/crates/router/src/core/webhooks/incoming.rs b/crates/router/src/core/webhooks/incoming.rs index 5fef952532f..cc1dc7bc506 100644 --- a/crates/router/src/core/webhooks/incoming.rs +++ b/crates/router/src/core/webhooks/incoming.rs @@ -997,7 +997,10 @@ async fn payouts_incoming_webhook_flow( business_profile, outgoing_event_type, enums::EventClass::Payouts, - updated_payout_attempt.payout_id.clone(), + updated_payout_attempt + .payout_id + .get_string_repr() + .to_string(), enums::EventObjectType::PayoutDetails, api::OutgoingWebhookContent::PayoutDetails(Box::new(payout_create_response)), Some(updated_payout_attempt.created_at), diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 48d0d582abb..7f24d2f5c99 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -2325,6 +2325,22 @@ impl PayoutAttemptInterface for KafkaStore {} #[async_trait::async_trait] impl PayoutAttemptInterface for KafkaStore { type Error = errors::StorageError; + + async fn find_payout_attempt_by_merchant_id_merchant_order_reference_id( + &self, + merchant_id: &id_type::MerchantId, + merchant_order_reference_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult { + self.diesel_store + .find_payout_attempt_by_merchant_id_merchant_order_reference_id( + merchant_id, + merchant_order_reference_id, + storage_scheme, + ) + .await + } + async fn find_payout_attempt_by_merchant_id_payout_attempt_id( &self, merchant_id: &id_type::MerchantId, @@ -2431,7 +2447,7 @@ impl PayoutsInterface for KafkaStore { async fn find_payout_by_merchant_id_payout_id( &self, merchant_id: &id_type::MerchantId, - payout_id: &str, + payout_id: &id_type::PayoutId, storage_scheme: MerchantStorageScheme, ) -> CustomResult { self.diesel_store @@ -2477,7 +2493,7 @@ impl PayoutsInterface for KafkaStore { async fn find_optional_payout_by_merchant_id_payout_id( &self, merchant_id: &id_type::MerchantId, - payout_id: &str, + payout_id: &id_type::PayoutId, storage_scheme: MerchantStorageScheme, ) -> CustomResult, errors::StorageError> { self.diesel_store @@ -2533,7 +2549,7 @@ impl PayoutsInterface for KafkaStore { async fn get_total_count_of_filtered_payouts( &self, merchant_id: &id_type::MerchantId, - active_payout_ids: &[String], + active_payout_ids: &[id_type::PayoutId], connector: Option>, currency: Option>, status: Option>, @@ -2556,7 +2572,7 @@ impl PayoutsInterface for KafkaStore { &self, merchant_id: &id_type::MerchantId, constraints: &hyperswitch_domain_models::payouts::PayoutFetchConstraints, - ) -> CustomResult, errors::StorageError> { + ) -> CustomResult, errors::StorageError> { self.diesel_store .filter_active_payout_ids_by_constraints(merchant_id, constraints) .await diff --git a/crates/router/src/events/outgoing_webhook_logs.rs b/crates/router/src/events/outgoing_webhook_logs.rs index 7574332f778..43f2ae54be4 100644 --- a/crates/router/src/events/outgoing_webhook_logs.rs +++ b/crates/router/src/events/outgoing_webhook_logs.rs @@ -38,7 +38,7 @@ pub enum OutgoingWebhookEventContent { content: Value, }, Payout { - payout_id: String, + payout_id: common_utils::id_type::PayoutId, content: Value, }, #[cfg(feature = "v1")] diff --git a/crates/router/src/routes/payout_link.rs b/crates/router/src/routes/payout_link.rs index 0727890bb32..71e3439d337 100644 --- a/crates/router/src/routes/payout_link.rs +++ b/crates/router/src/routes/payout_link.rs @@ -15,7 +15,10 @@ use crate::{ pub async fn render_payout_link( state: web::Data, req: actix_web::HttpRequest, - path: web::Path<(common_utils::id_type::MerchantId, String)>, + path: web::Path<( + common_utils::id_type::MerchantId, + common_utils::id_type::PayoutId, + )>, ) -> impl Responder { let flow = Flow::PayoutLinkInitiate; let (merchant_id, payout_id) = path.into_inner(); diff --git a/crates/router/src/routes/payouts.rs b/crates/router/src/routes/payouts.rs index be0aa027d70..5bccdd06668 100644 --- a/crates/router/src/routes/payouts.rs +++ b/crates/router/src/routes/payouts.rs @@ -2,6 +2,7 @@ use actix_web::{ body::{BoxBody, MessageBody}, web, HttpRequest, HttpResponse, Responder, }; +use common_utils::id_type; use router_env::{instrument, tracing, Flow}; use super::app::AppState; @@ -50,7 +51,7 @@ pub async fn payouts_create( pub async fn payouts_retrieve( state: web::Data, req: HttpRequest, - path: web::Path, + path: web::Path, query_params: web::Query, ) -> HttpResponse { let payout_retrieve_request = payout_types::PayoutRetrieveRequest { @@ -90,7 +91,7 @@ pub async fn payouts_retrieve( pub async fn payouts_update( state: web::Data, req: HttpRequest, - path: web::Path, + path: web::Path, json_payload: web::Json, ) -> HttpResponse { let flow = Flow::PayoutsUpdate; @@ -122,12 +123,12 @@ pub async fn payouts_confirm( state: web::Data, req: HttpRequest, json_payload: web::Json, - path: web::Path, + path: web::Path, ) -> HttpResponse { let flow = Flow::PayoutsConfirm; let mut payload = json_payload.into_inner(); let payout_id = path.into_inner(); - tracing::Span::current().record("payout_id", &payout_id); + tracing::Span::current().record("payout_id", payout_id.get_string_repr()); payload.payout_id = Some(payout_id); payload.confirm = Some(true); let api_auth = auth::ApiKeyAuth::default(); @@ -160,12 +161,12 @@ pub async fn payouts_confirm( pub async fn payouts_cancel( state: web::Data, req: HttpRequest, - json_payload: web::Json, - path: web::Path, + path: web::Path, ) -> HttpResponse { let flow = Flow::PayoutsCancel; - let mut payload = json_payload.into_inner(); - payload.payout_id = path.into_inner(); + let payload = payout_types::PayoutActionRequest { + payout_id: path.into_inner(), + }; Box::pin(api::server_wrap( flow, @@ -191,12 +192,12 @@ pub async fn payouts_cancel( pub async fn payouts_fulfill( state: web::Data, req: HttpRequest, - json_payload: web::Json, - path: web::Path, + path: web::Path, ) -> HttpResponse { let flow = Flow::PayoutsFulfill; - let mut payload = json_payload.into_inner(); - payload.payout_id = path.into_inner(); + let payload = payout_types::PayoutActionRequest { + payout_id: path.into_inner(), + }; Box::pin(api::server_wrap( flow, diff --git a/crates/router/src/services/kafka/payout.rs b/crates/router/src/services/kafka/payout.rs index babf7c1c568..e27009b3989 100644 --- a/crates/router/src/services/kafka/payout.rs +++ b/crates/router/src/services/kafka/payout.rs @@ -5,7 +5,7 @@ use time::OffsetDateTime; #[derive(serde::Serialize, Debug)] pub struct KafkaPayout<'a> { - pub payout_id: &'a String, + pub payout_id: &'a id_type::PayoutId, pub payout_attempt_id: &'a String, pub merchant_id: &'a id_type::MerchantId, pub customer_id: Option<&'a id_type::CustomerId>, diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index f89dbbec70a..a733de87656 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -41,7 +41,6 @@ use nanoid::nanoid; use serde::de::DeserializeOwned; use serde_json::Value; use tracing_futures::Instrument; -use uuid::Uuid; pub use self::ext_traits::{OptionExt, ValidateCall}; use crate::{ @@ -115,11 +114,6 @@ pub fn generate_id(length: usize, prefix: &str) -> String { format!("{}_{}", prefix, nanoid!(length, &consts::ALPHABETS)) } -#[inline] -pub fn generate_uuid() -> String { - Uuid::new_v4().to_string() -} - pub trait ConnectorResponseExt: Sized { fn get_response(self) -> RouterResult; fn get_error_response(self) -> RouterResult; @@ -164,7 +158,7 @@ impl ConnectorResponseExt } #[inline] -pub fn get_payout_attempt_id(payout_id: impl std::fmt::Display, attempt_count: i16) -> String { +pub fn get_payout_attempt_id(payout_id: &str, attempt_count: i16) -> String { format!("{payout_id}_{attempt_count}") } @@ -1356,7 +1350,7 @@ pub async fn trigger_payouts_webhook( business_profile, event_type, diesel_models::enums::EventClass::Payouts, - cloned_response.payout_id.clone(), + cloned_response.payout_id.get_string_repr().to_owned(), diesel_models::enums::EventObjectType::PayoutDetails, webhooks::OutgoingWebhookContent::PayoutDetails(Box::new(cloned_response)), primary_object_created_at, diff --git a/crates/router/src/workflows/outgoing_webhook_retry.rs b/crates/router/src/workflows/outgoing_webhook_retry.rs index 8a04094f31d..243c7382023 100644 --- a/crates/router/src/workflows/outgoing_webhook_retry.rs +++ b/crates/router/src/workflows/outgoing_webhook_retry.rs @@ -8,6 +8,7 @@ use api_models::{ use common_utils::{ consts::DEFAULT_LOCALE, ext_traits::{StringExt, ValueExt}, + id_type, }; use diesel_models::process_tracker::business_status; use error_stack::ResultExt; @@ -283,7 +284,7 @@ impl ProcessTrackerWorkflow for OutgoingWebhookRetryWorkflow { #[instrument(skip_all)] pub(crate) async fn get_webhook_delivery_retry_schedule_time( db: &dyn StorageInterface, - merchant_id: &common_utils::id_type::MerchantId, + merchant_id: &id_type::MerchantId, retry_count: i32, ) -> Option { let key = "pt_mapping_outgoing_webhooks"; @@ -329,7 +330,7 @@ pub(crate) async fn get_webhook_delivery_retry_schedule_time( #[instrument(skip_all)] pub(crate) async fn retry_webhook_delivery_task( db: &dyn StorageInterface, - merchant_id: &common_utils::id_type::MerchantId, + merchant_id: &id_type::MerchantId, process: storage::ProcessTracker, ) -> errors::CustomResult<(), errors::StorageError> { let schedule_time = @@ -385,15 +386,14 @@ async fn get_outgoing_webhook_content_and_event_type( match tracking_data.event_class { diesel_models::enums::EventClass::Payments => { let payment_id = tracking_data.primary_object_id.clone(); - let payment_id = - common_utils::id_type::PaymentId::try_from(std::borrow::Cow::Owned(payment_id)) - .map_err(|payment_id_parsing_error| { - logger::error!( - ?payment_id_parsing_error, - "Failed to parse payment ID from tracking data" - ); - errors::ProcessTrackerError::DeserializationFailed - })?; + let payment_id = id_type::PaymentId::try_from(std::borrow::Cow::Owned(payment_id)) + .map_err(|payment_id_parsing_error| { + logger::error!( + ?payment_id_parsing_error, + "Failed to parse payment ID from tracking data" + ); + errors::ProcessTrackerError::DeserializationFailed + })?; let request = PaymentsRetrieveRequest { resource_id: PaymentIdType::PaymentIntentId(payment_id), merchant_id: Some(tracking_data.merchant_id.clone()), @@ -539,7 +539,9 @@ async fn get_outgoing_webhook_content_and_event_type( diesel_models::enums::EventClass::Payouts => { let payout_id = tracking_data.primary_object_id.clone(); let request = payout_models::PayoutRequest::PayoutActionRequest( - payout_models::PayoutActionRequest { payout_id }, + payout_models::PayoutActionRequest { + payout_id: id_type::PayoutId::try_from(std::borrow::Cow::Owned(payout_id))?, + }, ); let payout_data = Box::pin(payouts::make_payout_data( diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 4bc234cca24..bf9bc355334 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -1,11 +1,9 @@ use std::{fmt::Debug, marker::PhantomData, str::FromStr, sync::Arc, time::Duration}; use async_trait::async_trait; -use common_utils::pii::Email; +use common_utils::{id_type::GenerateId, pii::Email}; use error_stack::Report; use masking::Secret; -#[cfg(feature = "payouts")] -use router::core::utils as core_utils; use router::{ configs::settings::Settings, core::{errors::ConnectorError, payments}, @@ -454,8 +452,7 @@ pub trait ConnectorActions: Connector { ) -> RouterData { self.generate_data( types::PayoutsData { - payout_id: core_utils::get_or_generate_uuid("payout_id", None) - .map_or("payout_3154763247".to_string(), |p| p), + payout_id: common_utils::id_type::PayoutId::generate(), amount: 1, minor_amount: MinorUnit::new(1), connector_payout_id, diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index 156ebee2dea..b2a2dba5779 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -427,7 +427,7 @@ impl UniqueConstraints for diesel_models::Payouts { vec![format!( "po_{}_{}", self.merchant_id.get_string_repr(), - self.payout_id + self.payout_id.get_string_repr() )] } fn table_name(&self) -> &str { diff --git a/crates/storage_impl/src/mock_db/payout_attempt.rs b/crates/storage_impl/src/mock_db/payout_attempt.rs index 1fd2058e95a..008e9f921b0 100644 --- a/crates/storage_impl/src/mock_db/payout_attempt.rs +++ b/crates/storage_impl/src/mock_db/payout_attempt.rs @@ -65,4 +65,13 @@ impl PayoutAttemptInterface for MockDb { > { Err(StorageError::MockDbError)? } + + async fn find_payout_attempt_by_merchant_id_merchant_order_reference_id( + &self, + _merchant_id: &common_utils::id_type::MerchantId, + _merchant_order_reference_id: &str, + _storage_scheme: storage_enums::MerchantStorageScheme, + ) -> CustomResult { + Err(StorageError::MockDbError)? + } } diff --git a/crates/storage_impl/src/mock_db/payouts.rs b/crates/storage_impl/src/mock_db/payouts.rs index ecba8f88df6..648e09bd5c6 100644 --- a/crates/storage_impl/src/mock_db/payouts.rs +++ b/crates/storage_impl/src/mock_db/payouts.rs @@ -13,7 +13,7 @@ impl PayoutsInterface for MockDb { async fn find_payout_by_merchant_id_payout_id( &self, _merchant_id: &common_utils::id_type::MerchantId, - _payout_id: &str, + _payout_id: &common_utils::id_type::PayoutId, _storage_scheme: storage_enums::MerchantStorageScheme, ) -> CustomResult { // TODO: Implement function for `MockDb` @@ -43,7 +43,7 @@ impl PayoutsInterface for MockDb { async fn find_optional_payout_by_merchant_id_payout_id( &self, _merchant_id: &common_utils::id_type::MerchantId, - _payout_id: &str, + _payout_id: &common_utils::id_type::PayoutId, _storage_scheme: storage_enums::MerchantStorageScheme, ) -> CustomResult, StorageError> { // TODO: Implement function for `MockDb` @@ -95,7 +95,7 @@ impl PayoutsInterface for MockDb { async fn get_total_count_of_filtered_payouts( &self, _merchant_id: &common_utils::id_type::MerchantId, - _active_payout_ids: &[String], + _active_payout_ids: &[common_utils::id_type::PayoutId], _connector: Option>, _currency: Option>, _status: Option>, @@ -110,7 +110,7 @@ impl PayoutsInterface for MockDb { &self, _merchant_id: &common_utils::id_type::MerchantId, _constraints: &hyperswitch_domain_models::payouts::PayoutFetchConstraints, - ) -> CustomResult, StorageError> { + ) -> CustomResult, StorageError> { // TODO: Implement function for `MockDb` Err(StorageError::MockDbError)? } diff --git a/crates/storage_impl/src/payouts/payout_attempt.rs b/crates/storage_impl/src/payouts/payout_attempt.rs index e1706fad124..35939c11323 100644 --- a/crates/storage_impl/src/payouts/payout_attempt.rs +++ b/crates/storage_impl/src/payouts/payout_attempt.rs @@ -60,7 +60,7 @@ impl PayoutAttemptInterface for KVRouterStore { let payout_attempt_id = new_payout_attempt.payout_id.clone(); let key = PartitionKey::MerchantIdPayoutAttemptId { merchant_id: &merchant_id, - payout_attempt_id: &payout_attempt_id, + payout_attempt_id: payout_attempt_id.get_string_repr(), }; let key_str = key.to_string(); let created_attempt = PayoutAttempt { @@ -88,6 +88,9 @@ impl PayoutAttemptInterface for KVRouterStore { routing_info: new_payout_attempt.routing_info.clone(), unified_code: new_payout_attempt.unified_code.clone(), unified_message: new_payout_attempt.unified_message.clone(), + merchant_order_reference_id: new_payout_attempt + .merchant_order_reference_id + .clone(), }; let redis_entry = kv::TypedSql { @@ -149,7 +152,7 @@ impl PayoutAttemptInterface for KVRouterStore { ) -> error_stack::Result { let key = PartitionKey::MerchantIdPayoutAttemptId { merchant_id: &this.merchant_id, - payout_attempt_id: &this.payout_id, + payout_attempt_id: this.payout_id.get_string_repr(), }; let field = format!("poa_{}", this.payout_attempt_id); let storage_scheme = Box::pin(decide_storage_scheme::<_, DieselPayoutAttempt>( @@ -378,6 +381,22 @@ impl PayoutAttemptInterface for KVRouterStore { .get_filters_for_payouts(payouts, merchant_id, storage_scheme) .await } + + #[instrument(skip_all)] + async fn find_payout_attempt_by_merchant_id_merchant_order_reference_id( + &self, + merchant_id: &common_utils::id_type::MerchantId, + merchant_order_reference_id: &str, + storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + self.router_store + .find_payout_attempt_by_merchant_id_merchant_order_reference_id( + merchant_id, + merchant_order_reference_id, + storage_scheme, + ) + .await + } } #[async_trait::async_trait] @@ -504,6 +523,27 @@ impl PayoutAttemptInterface for crate::RouterStore { }, ) } + + #[instrument(skip_all)] + async fn find_payout_attempt_by_merchant_id_merchant_order_reference_id( + &self, + merchant_id: &common_utils::id_type::MerchantId, + merchant_order_reference_id: &str, + _storage_scheme: MerchantStorageScheme, + ) -> error_stack::Result { + let conn = pg_connection_read(self).await?; + DieselPayoutAttempt::find_by_merchant_id_merchant_order_reference_id( + &conn, + merchant_id, + merchant_order_reference_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 { @@ -533,6 +573,7 @@ impl DataModelExt for PayoutAttempt { unified_code: self.unified_code, unified_message: self.unified_message, additional_payout_method_data: self.additional_payout_method_data, + merchant_order_reference_id: self.merchant_order_reference_id, } } @@ -560,6 +601,7 @@ impl DataModelExt for PayoutAttempt { unified_code: storage_model.unified_code, unified_message: storage_model.unified_message, additional_payout_method_data: storage_model.additional_payout_method_data, + merchant_order_reference_id: storage_model.merchant_order_reference_id, } } } @@ -590,6 +632,7 @@ impl DataModelExt for PayoutAttemptNew { unified_code: self.unified_code, unified_message: self.unified_message, additional_payout_method_data: self.additional_payout_method_data, + merchant_order_reference_id: self.merchant_order_reference_id, } } @@ -617,6 +660,7 @@ impl DataModelExt for PayoutAttemptNew { unified_code: storage_model.unified_code, unified_message: storage_model.unified_message, additional_payout_method_data: storage_model.additional_payout_method_data, + merchant_order_reference_id: storage_model.merchant_order_reference_id, } } } diff --git a/crates/storage_impl/src/payouts/payouts.rs b/crates/storage_impl/src/payouts/payouts.rs index 484328aabab..c9d16b7d8a2 100644 --- a/crates/storage_impl/src/payouts/payouts.rs +++ b/crates/storage_impl/src/payouts/payouts.rs @@ -80,7 +80,7 @@ impl PayoutsInterface for KVRouterStore { payout_id: &payout_id, }; let key_str = key.to_string(); - let field = format!("po_{}", new.payout_id); + let field = format!("po_{}", new.payout_id.get_string_repr()); let created_payout = Payouts { payout_id: new.payout_id.clone(), merchant_id: new.merchant_id.clone(), @@ -151,7 +151,7 @@ impl PayoutsInterface for KVRouterStore { merchant_id: &this.merchant_id, payout_id: &this.payout_id, }; - let field = format!("po_{}", this.payout_id); + let field = format!("po_{}", this.payout_id.get_string_repr()); let storage_scheme = Box::pin(decide_storage_scheme::<_, DieselPayouts>( self, storage_scheme, @@ -207,7 +207,7 @@ impl PayoutsInterface for KVRouterStore { async fn find_payout_by_merchant_id_payout_id( &self, merchant_id: &common_utils::id_type::MerchantId, - payout_id: &str, + payout_id: &common_utils::id_type::PayoutId, storage_scheme: MerchantStorageScheme, ) -> error_stack::Result { let database_call = || async { @@ -232,7 +232,7 @@ impl PayoutsInterface for KVRouterStore { merchant_id, payout_id, }; - let field = format!("po_{payout_id}"); + let field = format!("po_{}", payout_id.get_string_repr()); Box::pin(utils::try_redis_get_else_try_database_get( async { Box::pin(kv_wrapper::( @@ -255,7 +255,7 @@ impl PayoutsInterface for KVRouterStore { async fn find_optional_payout_by_merchant_id_payout_id( &self, merchant_id: &common_utils::id_type::MerchantId, - payout_id: &str, + payout_id: &common_utils::id_type::PayoutId, storage_scheme: MerchantStorageScheme, ) -> error_stack::Result, StorageError> { let database_call = || async { @@ -276,20 +276,14 @@ impl PayoutsInterface for KVRouterStore { 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 - } - })) + Ok(maybe_payouts.filter(|payout| &payout.payout_id == payout_id)) } MerchantStorageScheme::RedisKv => { let key = PartitionKey::MerchantIdPayoutId { merchant_id, payout_id, }; - let field = format!("po_{payout_id}"); + let field = format!("po_{}", payout_id.get_string_repr()); Box::pin(utils::try_redis_get_else_try_database_get( async { Box::pin(kv_wrapper::( @@ -360,7 +354,7 @@ impl PayoutsInterface for KVRouterStore { async fn get_total_count_of_filtered_payouts( &self, merchant_id: &common_utils::id_type::MerchantId, - active_payout_ids: &[String], + active_payout_ids: &[common_utils::id_type::PayoutId], connector: Option>, currency: Option>, status: Option>, @@ -383,7 +377,7 @@ impl PayoutsInterface for KVRouterStore { &self, merchant_id: &common_utils::id_type::MerchantId, constraints: &PayoutFetchConstraints, - ) -> error_stack::Result, StorageError> { + ) -> error_stack::Result, StorageError> { self.router_store .filter_active_payout_ids_by_constraints(merchant_id, constraints) .await @@ -434,7 +428,7 @@ impl PayoutsInterface for crate::RouterStore { async fn find_payout_by_merchant_id_payout_id( &self, merchant_id: &common_utils::id_type::MerchantId, - payout_id: &str, + payout_id: &common_utils::id_type::PayoutId, _storage_scheme: MerchantStorageScheme, ) -> error_stack::Result { let conn = pg_connection_read(self).await?; @@ -451,7 +445,7 @@ impl PayoutsInterface for crate::RouterStore { async fn find_optional_payout_by_merchant_id_payout_id( &self, merchant_id: &common_utils::id_type::MerchantId, - payout_id: &str, + payout_id: &common_utils::id_type::PayoutId, _storage_scheme: MerchantStorageScheme, ) -> error_stack::Result, StorageError> { let conn = pg_connection_read(self).await?; @@ -498,7 +492,7 @@ impl PayoutsInterface for crate::RouterStore { query = query.filter(po_dsl::profile_id.eq(profile_id.clone())); } - query = match (params.starting_at, ¶ms.starting_after_id) { + query = match (params.starting_at, params.starting_after_id.as_ref()) { (Some(starting_at), _) => query.filter(po_dsl::created_at.ge(starting_at)), (None, Some(starting_after_id)) => { // TODO: Fetch partial columns for this query since we only need some columns @@ -515,7 +509,7 @@ impl PayoutsInterface for crate::RouterStore { (None, None) => query, }; - query = match (params.ending_at, ¶ms.ending_before_id) { + query = match (params.ending_at, params.ending_before_id.as_ref()) { (Some(ending_at), _) => query.filter(po_dsl::created_at.le(ending_at)), (None, Some(ending_before_id)) => { // TODO: Fetch partial columns for this query since we only need some columns @@ -619,10 +613,18 @@ impl PayoutsInterface for crate::RouterStore { query = query.filter(po_dsl::profile_id.eq(profile_id.clone())); } - query = match (params.starting_at, ¶ms.starting_after_id) { + if let Some(merchant_order_reference_id_filter) = + ¶ms.merchant_order_reference_id + { + query = query.filter( + poa_dsl::merchant_order_reference_id + .eq(merchant_order_reference_id_filter.clone()), + ); + } + + query = match (params.starting_at, params.starting_after_id.as_ref()) { (Some(starting_at), _) => query.filter(po_dsl::created_at.ge(starting_at)), (None, Some(starting_after_id)) => { - // TODO: Fetch partial columns for this query since we only need some columns let starting_at = self .find_payout_by_merchant_id_payout_id( merchant_id, @@ -636,10 +638,9 @@ impl PayoutsInterface for crate::RouterStore { (None, None) => query, }; - query = match (params.ending_at, ¶ms.ending_before_id) { + query = match (params.ending_at, params.ending_before_id.as_ref()) { (Some(ending_at), _) => query.filter(po_dsl::created_at.le(ending_at)), (None, Some(ending_before_id)) => { - // TODO: Fetch partial columns for this query since we only need some columns let ending_at = self .find_payout_by_merchant_id_payout_id( merchant_id, @@ -665,20 +666,24 @@ impl PayoutsInterface for crate::RouterStore { .map(|c| c.iter().map(|c| c.to_string()).collect::>()); query = match connectors { - Some(connectors) => query.filter(poa_dsl::connector.eq_any(connectors)), - None => query, + Some(conn_filters) if !conn_filters.is_empty() => { + query.filter(poa_dsl::connector.eq_any(conn_filters)) + } + _ => query, }; query = match ¶ms.status { - Some(status) => query.filter(po_dsl::status.eq_any(status.clone())), - None => query, + Some(status_filters) if !status_filters.is_empty() => { + query.filter(po_dsl::status.eq_any(status_filters.clone())) + } + _ => query, }; query = match ¶ms.payout_method { - Some(payout_method) => { + Some(payout_method) if !payout_method.is_empty() => { query.filter(po_dsl::payout_type.eq_any(payout_method.clone())) } - None => query, + _ => query, }; query @@ -760,7 +765,7 @@ impl PayoutsInterface for crate::RouterStore { async fn get_total_count_of_filtered_payouts( &self, merchant_id: &common_utils::id_type::MerchantId, - active_payout_ids: &[String], + active_payout_ids: &[common_utils::id_type::PayoutId], connector: Option>, currency: Option>, status: Option>, @@ -800,7 +805,7 @@ impl PayoutsInterface for crate::RouterStore { &self, merchant_id: &common_utils::id_type::MerchantId, constraints: &PayoutFetchConstraints, - ) -> error_stack::Result, StorageError> { + ) -> error_stack::Result, StorageError> { let conn = connection::pg_connection_read(self).await?; let conn = async_bb8_diesel::Connection::as_async_conn(&conn); let mut query = DieselPayouts::table() @@ -868,8 +873,14 @@ impl PayoutsInterface for crate::RouterStore { error_stack::report!(diesel_models::errors::DatabaseError::from(er)) .attach_printable("Error filtering payout records"), ) - .into() + })? + .into_iter() + .map(|s| { + common_utils::id_type::PayoutId::try_from(std::borrow::Cow::Owned(s)) + .change_context(StorageError::DeserializationFailed) + .attach_printable("Failed to deserialize PayoutId from database string") }) + .collect::, _>>() } #[cfg(all(feature = "olap", feature = "v2"))] @@ -878,7 +889,7 @@ impl PayoutsInterface for crate::RouterStore { &self, _merchant_id: &common_utils::id_type::MerchantId, _constraints: &PayoutFetchConstraints, - ) -> error_stack::Result, StorageError> { + ) -> error_stack::Result, StorageError> { todo!() } } diff --git a/crates/storage_impl/src/redis/kv_store.rs b/crates/storage_impl/src/redis/kv_store.rs index fd583f500ed..e7c47c58996 100644 --- a/crates/storage_impl/src/redis/kv_store.rs +++ b/crates/storage_impl/src/redis/kv_store.rs @@ -41,7 +41,7 @@ pub enum PartitionKey<'a> { }, MerchantIdPayoutId { merchant_id: &'a common_utils::id_type::MerchantId, - payout_id: &'a str, + payout_id: &'a common_utils::id_type::PayoutId, }, MerchantIdPayoutAttemptId { merchant_id: &'a common_utils::id_type::MerchantId, @@ -93,8 +93,9 @@ impl std::fmt::Display for PartitionKey<'_> { merchant_id, payout_id, } => f.write_str(&format!( - "mid_{}_po_{payout_id}", - merchant_id.get_string_repr() + "mid_{}_po_{}", + merchant_id.get_string_repr(), + payout_id.get_string_repr() )), PartitionKey::MerchantIdPayoutAttemptId { merchant_id, diff --git a/flake.nix b/flake.nix index 35fa05b73a7..05058a75d9e 100644 --- a/flake.nix +++ b/flake.nix @@ -43,6 +43,7 @@ devPackages = base ++ (with pkgs; [ cargo-watch nixd + protobuf rust-bin.stable.${rustDevVersion}.default swagger-cli ]); diff --git a/migrations/2025-06-19-170656_alter_payout_primary_key/down.sql b/migrations/2025-06-19-170656_alter_payout_primary_key/down.sql new file mode 100644 index 00000000000..e8ff6e1c5a1 --- /dev/null +++ b/migrations/2025-06-19-170656_alter_payout_primary_key/down.sql @@ -0,0 +1,7 @@ +ALTER TABLE payout_attempt DROP COLUMN merchant_order_reference_id; + +ALTER TABLE payout_attempt DROP CONSTRAINT payout_attempt_pkey; +ALTER TABLE payout_attempt ADD PRIMARY KEY (payout_attempt_id); + +ALTER TABLE payouts DROP CONSTRAINT payouts_pkey; +ALTER TABLE payouts ADD PRIMARY KEY (payout_id); diff --git a/migrations/2025-06-19-170656_alter_payout_primary_key/up.sql b/migrations/2025-06-19-170656_alter_payout_primary_key/up.sql new file mode 100644 index 00000000000..d65c931719f --- /dev/null +++ b/migrations/2025-06-19-170656_alter_payout_primary_key/up.sql @@ -0,0 +1,7 @@ +ALTER TABLE payout_attempt DROP CONSTRAINT payout_attempt_pkey; +ALTER TABLE payout_attempt ADD PRIMARY KEY (merchant_id, payout_attempt_id); + +ALTER TABLE payouts DROP CONSTRAINT payouts_pkey; +ALTER TABLE payouts ADD PRIMARY KEY (merchant_id, payout_id); + +ALTER TABLE payout_attempt ADD COLUMN merchant_order_reference_id VARCHAR(255) NULL;