From 1be28d3adc6dd554446bf99a9997af5c10942cd5 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Mon, 23 Dec 2024 22:34:46 +0530 Subject: [PATCH] feat(router): add /retrieve api for relay (#6924) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../api-reference/relay/relay--retrieve.mdx | 3 + api-reference/api-reference/relay/relay.mdx | 3 + api-reference/mint.json | 7 + api-reference/openapi_spec.json | 281 +++++++++++++++ crates/api_models/src/lib.rs | 1 + crates/api_models/src/relay.rs | 103 ++++++ crates/common_enums/src/enums.rs | 67 ++++ crates/common_utils/src/id_type.rs | 2 + crates/common_utils/src/id_type/relay.rs | 13 + crates/diesel_models/src/enums.rs | 3 +- crates/diesel_models/src/lib.rs | 1 + crates/diesel_models/src/query.rs | 1 + crates/diesel_models/src/query/relay.rs | 49 +++ crates/diesel_models/src/relay.rs | 77 ++++ crates/diesel_models/src/schema.rs | 30 ++ crates/diesel_models/src/schema_v2.rs | 30 ++ crates/hyperswitch_domain_models/src/lib.rs | 1 + .../src/merchant_connector_account.rs | 26 +- crates/hyperswitch_domain_models/src/relay.rs | 275 +++++++++++++++ crates/openapi/src/openapi.rs | 11 + crates/openapi/src/routes.rs | 3 +- crates/openapi/src/routes/relay.rs | 57 +++ crates/router/src/core.rs | 2 + crates/router/src/core/relay.rs | 330 ++++++++++++++++++ crates/router/src/core/relay/utils.rs | 139 ++++++++ crates/router/src/db.rs | 2 + crates/router/src/db/relay.rs | 181 ++++++++++ crates/router/src/lib.rs | 3 +- crates/router/src/routes.rs | 4 +- crates/router/src/routes/app.rs | 14 +- crates/router/src/routes/lock_utils.rs | 2 + crates/router/src/routes/relay.rs | 76 ++++ crates/router/src/services/authentication.rs | 15 +- crates/router_env/src/logger/types.rs | 4 + .../down.sql | 7 + .../2024-12-17-141811_add_relay_table/up.sql | 22 ++ 36 files changed, 1833 insertions(+), 12 deletions(-) create mode 100644 api-reference/api-reference/relay/relay--retrieve.mdx create mode 100644 api-reference/api-reference/relay/relay.mdx create mode 100644 crates/api_models/src/relay.rs create mode 100644 crates/common_utils/src/id_type/relay.rs create mode 100644 crates/diesel_models/src/query/relay.rs create mode 100644 crates/diesel_models/src/relay.rs create mode 100644 crates/hyperswitch_domain_models/src/relay.rs create mode 100644 crates/openapi/src/routes/relay.rs create mode 100644 crates/router/src/core/relay.rs create mode 100644 crates/router/src/core/relay/utils.rs create mode 100644 crates/router/src/db/relay.rs create mode 100644 crates/router/src/routes/relay.rs create mode 100644 migrations/2024-12-17-141811_add_relay_table/down.sql create mode 100644 migrations/2024-12-17-141811_add_relay_table/up.sql diff --git a/api-reference/api-reference/relay/relay--retrieve.mdx b/api-reference/api-reference/relay/relay--retrieve.mdx new file mode 100644 index 000000000000..d65e62d31d74 --- /dev/null +++ b/api-reference/api-reference/relay/relay--retrieve.mdx @@ -0,0 +1,3 @@ +--- +openapi: openapi_spec get /relay/{relay_id} +--- \ No newline at end of file diff --git a/api-reference/api-reference/relay/relay.mdx b/api-reference/api-reference/relay/relay.mdx new file mode 100644 index 000000000000..a6b5962740a6 --- /dev/null +++ b/api-reference/api-reference/relay/relay.mdx @@ -0,0 +1,3 @@ +--- +openapi: openapi_spec post /relay +--- \ No newline at end of file diff --git a/api-reference/mint.json b/api-reference/mint.json index 04b16682e3c4..5e9c9c6d36c6 100644 --- a/api-reference/mint.json +++ b/api-reference/mint.json @@ -234,6 +234,13 @@ "api-reference/routing/routing--activate-config" ] }, + { + "group": "Relay", + "pages": [ + "api-reference/relay/relay", + "api-reference/relay/relay--retrieve" + ] + }, { "group": "Schemas", "pages": ["api-reference/schemas/outgoing--webhook"] diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index c31a8bd11ed6..714e4948904e 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -950,6 +950,123 @@ ] } }, + "/relay": { + "post": { + "tags": [ + "Relay" + ], + "summary": "Relay - Create", + "description": "Creates a relay request.", + "operationId": "Relay Request", + "parameters": [ + { + "name": "X-Profile-Id", + "in": "header", + "description": "Profile ID for authentication", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "X-Idempotency-Key", + "in": "header", + "description": "Idempotency Key for relay request", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelayRequest" + }, + "examples": { + "Create a relay request": { + "value": { + "connector_id": "mca_5apGeP94tMts6rg3U3kR", + "connector_resource_id": "7256228702616471803954", + "data": { + "amount": 6540, + "currency": "USD" + }, + "type": "refund" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Relay request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelayResponse" + } + } + } + }, + "400": { + "description": "Invalid data" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/relay/{relay_id}": { + "get": { + "tags": [ + "Relay" + ], + "summary": "Relay - Retrieve", + "description": "Retrieves a relay details.", + "operationId": "Retrieve a Relay details", + "parameters": [ + { + "name": "X-Profile-Id", + "in": "header", + "description": "Profile ID for authentication", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Relay Retrieved", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelayResponse" + } + } + } + }, + "404": { + "description": "Relay details was not found" + } + }, + "security": [ + { + "api_key": [] + }, + { + "ephemeral_key": [] + } + ] + } + }, "/refunds": { "post": { "tags": [ @@ -22709,6 +22826,170 @@ }, "additionalProperties": false }, + "RelayData": { + "oneOf": [ + { + "type": "object", + "required": [ + "refund" + ], + "properties": { + "refund": { + "$ref": "#/components/schemas/RelayRefundRequest" + } + } + } + ] + }, + "RelayError": { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "The error code" + }, + "message": { + "type": "string", + "description": "The error message" + } + } + }, + "RelayRefundRequest": { + "type": "object", + "required": [ + "amount", + "currency" + ], + "properties": { + "amount": { + "type": "integer", + "format": "int64", + "description": "The amount that is being refunded", + "example": 6540 + }, + "currency": { + "$ref": "#/components/schemas/Currency" + }, + "reason": { + "type": "string", + "description": "The reason for the refund", + "example": "Customer returned the product", + "nullable": true, + "maxLength": 255 + } + } + }, + "RelayRequest": { + "type": "object", + "required": [ + "connector_resource_id", + "connector_id", + "type" + ], + "properties": { + "connector_resource_id": { + "type": "string", + "description": "The identifier that is associated to a resource at the connector reference to which the relay request is being made", + "example": "7256228702616471803954" + }, + "connector_id": { + "type": "string", + "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", + "example": "mca_5apGeP94tMts6rg3U3kR" + }, + "type": { + "$ref": "#/components/schemas/RelayType" + }, + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/RelayData" + } + ], + "nullable": true + } + } + }, + "RelayResponse": { + "type": "object", + "required": [ + "id", + "status", + "connector_resource_id", + "connector_id", + "profile_id", + "type" + ], + "properties": { + "id": { + "type": "string", + "description": "The unique identifier for the Relay", + "example": "relay_mbabizu24mvu3mela5njyhpit4" + }, + "status": { + "$ref": "#/components/schemas/RelayStatus" + }, + "connector_resource_id": { + "type": "string", + "description": "The identifier that is associated to a resource at the connector reference to which the relay request is being made", + "example": "pi_3MKEivSFNglxLpam0ZaL98q9" + }, + "error": { + "allOf": [ + { + "$ref": "#/components/schemas/RelayError" + } + ], + "nullable": true + }, + "connector_reference_id": { + "type": "string", + "description": "The identifier that is associated to a resource at the connector to which the relay request is being made", + "example": "re_3QY4TnEOqOywnAIx1Mm1p7GQ", + "nullable": true + }, + "connector_id": { + "type": "string", + "description": "Identifier of the connector ( merchant connector account ) which was chosen to make the payment", + "example": "mca_5apGeP94tMts6rg3U3kR" + }, + "profile_id": { + "type": "string", + "description": "The business profile that is associated with this relay request.", + "example": "pro_abcdefghijklmnopqrstuvwxyz" + }, + "type": { + "$ref": "#/components/schemas/RelayType" + }, + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/RelayData" + } + ], + "nullable": true + } + } + }, + "RelayStatus": { + "type": "string", + "enum": [ + "created", + "pending", + "success", + "failure" + ] + }, + "RelayType": { + "type": "string", + "enum": [ + "refund" + ] + }, "RequestPaymentMethodTypes": { "type": "object", "required": [ diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index a28332e7fea0..2d1e87ebf01e 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -31,6 +31,7 @@ pub mod poll; #[cfg(feature = "recon")] pub mod recon; pub mod refunds; +pub mod relay; pub mod routing; pub mod surcharge_decision_configs; pub mod user; diff --git a/crates/api_models/src/relay.rs b/crates/api_models/src/relay.rs new file mode 100644 index 000000000000..f54e14716327 --- /dev/null +++ b/crates/api_models/src/relay.rs @@ -0,0 +1,103 @@ +use common_utils::types::MinorUnit; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::enums as api_enums; + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayRequest { + /// The identifier that is associated to a resource at the connector reference to which the relay request is being made + #[schema(example = "7256228702616471803954")] + pub connector_resource_id: String, + /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment + #[schema(example = "mca_5apGeP94tMts6rg3U3kR", value_type = String)] + pub connector_id: common_utils::id_type::MerchantConnectorAccountId, + /// The type of relay request + #[serde(rename = "type")] + #[schema(value_type = RelayType)] + pub relay_type: api_enums::RelayType, + /// The data that is associated with the relay request + pub data: Option, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum RelayData { + /// The data that is associated with a refund relay request + Refund(RelayRefundRequest), +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayRefundRequest { + /// The amount that is being refunded + #[schema(value_type = i64 , example = 6540)] + pub amount: MinorUnit, + /// The currency in which the amount is being refunded + #[schema(value_type = Currency)] + pub currency: api_enums::Currency, + /// The reason for the refund + #[schema(max_length = 255, example = "Customer returned the product")] + pub reason: Option, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayResponse { + /// The unique identifier for the Relay + #[schema(example = "relay_mbabizu24mvu3mela5njyhpit4", value_type = String)] + pub id: common_utils::id_type::RelayId, + /// The status of the relay request + #[schema(value_type = RelayStatus)] + pub status: api_enums::RelayStatus, + /// The identifier that is associated to a resource at the connector reference to which the relay request is being made + #[schema(example = "pi_3MKEivSFNglxLpam0ZaL98q9")] + pub connector_resource_id: String, + /// The error details if the relay request failed + pub error: Option, + /// The identifier that is associated to a resource at the connector to which the relay request is being made + #[schema(example = "re_3QY4TnEOqOywnAIx1Mm1p7GQ")] + pub connector_reference_id: Option, + /// Identifier of the connector ( merchant connector account ) which was chosen to make the payment + #[schema(example = "mca_5apGeP94tMts6rg3U3kR", value_type = String)] + pub connector_id: common_utils::id_type::MerchantConnectorAccountId, + /// The business profile that is associated with this relay request. + #[schema(example = "pro_abcdefghijklmnopqrstuvwxyz", value_type = String)] + pub profile_id: common_utils::id_type::ProfileId, + /// The type of relay request + #[serde(rename = "type")] + #[schema(value_type = RelayType)] + pub relay_type: api_enums::RelayType, + /// The data that is associated with the relay request + pub data: Option, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayError { + /// The error code + pub code: String, + /// The error message + pub message: String, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayRetrieveRequest { + /// The unique identifier for the Relay + #[serde(default)] + pub force_sync: bool, + /// The unique identifier for the Relay + pub id: common_utils::id_type::RelayId, +} + +#[derive(Debug, ToSchema, Clone, Deserialize, Serialize)] +pub struct RelayRetrieveBody { + /// The unique identifier for the Relay + #[serde(default)] + pub force_sync: bool, +} + +impl common_utils::events::ApiEventMetric for RelayRequest {} + +impl common_utils::events::ApiEventMetric for RelayResponse {} + +impl common_utils::events::ApiEventMetric for RelayRetrieveRequest {} + +impl common_utils::events::ApiEventMetric for RelayRetrieveBody {} diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 5aac1a07457b..193998c3fd1c 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1728,6 +1728,53 @@ pub enum RefundStatus { TransactionFailure, } +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + Hash, + PartialEq, + strum::Display, + strum::EnumString, + strum::EnumIter, + serde::Serialize, + serde::Deserialize, + ToSchema, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum RelayStatus { + Created, + #[default] + Pending, + Success, + Failure, +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + PartialEq, + strum::Display, + strum::EnumString, + strum::EnumIter, + serde::Serialize, + serde::Deserialize, + ToSchema, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum RelayType { + Refund, +} + #[derive( Clone, Copy, @@ -3372,6 +3419,26 @@ impl From for TransactionType { } } +impl From for RelayStatus { + fn from(refund_status: RefundStatus) -> Self { + match refund_status { + RefundStatus::Failure | RefundStatus::TransactionFailure => Self::Failure, + RefundStatus::ManualReview | RefundStatus::Pending => Self::Pending, + RefundStatus::Success => Self::Success, + } + } +} + +impl From for RefundStatus { + fn from(relay_status: RelayStatus) -> Self { + match relay_status { + RelayStatus::Failure => Self::Failure, + RelayStatus::Pending | RelayStatus::Created => Self::Pending, + RelayStatus::Success => Self::Success, + } + } +} + #[derive( Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize, Default, ToSchema, )] diff --git a/crates/common_utils/src/id_type.rs b/crates/common_utils/src/id_type.rs index 594078bb4057..c7393155226b 100644 --- a/crates/common_utils/src/id_type.rs +++ b/crates/common_utils/src/id_type.rs @@ -11,6 +11,7 @@ mod organization; mod payment; mod profile; mod refunds; +mod relay; mod routing; mod tenant; @@ -43,6 +44,7 @@ pub use self::{ payment::{PaymentId, PaymentReferenceId}, profile::ProfileId, refunds::RefundReferenceId, + relay::RelayId, routing::RoutingId, tenant::TenantId, }; diff --git a/crates/common_utils/src/id_type/relay.rs b/crates/common_utils/src/id_type/relay.rs new file mode 100644 index 000000000000..3ad64729fb73 --- /dev/null +++ b/crates/common_utils/src/id_type/relay.rs @@ -0,0 +1,13 @@ +crate::id_type!( + RelayId, + "A type for relay_id that can be used for relay ids" +); +crate::impl_id_type_methods!(RelayId, "relay_id"); + +crate::impl_try_from_cow_str_id_type!(RelayId, "relay_id"); +crate::impl_generate_id_id_type!(RelayId, "relay"); +crate::impl_serializable_secret_id_type!(RelayId); +crate::impl_queryable_id_type!(RelayId); +crate::impl_to_sql_from_sql_id_type!(RelayId); + +crate::impl_debug_id_type!(RelayId); diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 19f3bf0a3823..ec6e91a2ecb0 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -17,7 +17,8 @@ pub mod diesel_exports { DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, DbPaymentSource as PaymentSource, DbPaymentType as PaymentType, DbPayoutStatus as PayoutStatus, DbPayoutType as PayoutType, DbProcessTrackerStatus as ProcessTrackerStatus, DbReconStatus as ReconStatus, - DbRefundStatus as RefundStatus, DbRefundType as RefundType, + DbRefundStatus as RefundStatus, DbRefundType as RefundType, DbRelayStatus as RelayStatus, + DbRelayType as RelayType, DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, DbRoleScope as RoleScope, DbRoutingAlgorithmKind as RoutingAlgorithmKind, DbScaExemptionType as ScaExemptionType, diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index cc3dc1361545..1369368a8099 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -39,6 +39,7 @@ pub mod payouts; pub mod process_tracker; pub mod query; pub mod refund; +pub mod relay; pub mod reverse_lookup; pub mod role; pub mod routing_algorithm; diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index ab044b5c6e69..8eb0a44f5dd7 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -34,6 +34,7 @@ pub mod payout_attempt; pub mod payouts; pub mod process_tracker; pub mod refund; +pub mod relay; pub mod reverse_lookup; pub mod role; pub mod routing_algorithm; diff --git a/crates/diesel_models/src/query/relay.rs b/crates/diesel_models/src/query/relay.rs new file mode 100644 index 000000000000..034446fe6b51 --- /dev/null +++ b/crates/diesel_models/src/query/relay.rs @@ -0,0 +1,49 @@ +use diesel::{associations::HasTable, ExpressionMethods}; + +use super::generics; +use crate::{ + errors, + relay::{Relay, RelayNew, RelayUpdateInternal}, + schema::relay::dsl, + PgPooledConn, StorageResult, +}; + +impl RelayNew { + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl Relay { + pub async fn update( + self, + conn: &PgPooledConn, + relay: RelayUpdateInternal, + ) -> StorageResult { + match generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >(conn, dsl::id.eq(self.id.to_owned()), relay) + .await + { + Err(error) => match error.current_context() { + errors::DatabaseError::NoFieldsToUpdate => Ok(self), + _ => Err(error), + }, + result => result, + } + } + + pub async fn find_by_id( + conn: &PgPooledConn, + id: &common_utils::id_type::RelayId, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::id.eq(id.to_owned()), + ) + .await + } +} diff --git a/crates/diesel_models/src/relay.rs b/crates/diesel_models/src/relay.rs new file mode 100644 index 000000000000..153e06ab17f5 --- /dev/null +++ b/crates/diesel_models/src/relay.rs @@ -0,0 +1,77 @@ +use common_utils::pii; +use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; +use time::PrimitiveDateTime; + +use crate::{enums as storage_enums, schema::relay}; + +#[derive( + Clone, + Debug, + Eq, + Identifiable, + Queryable, + Selectable, + PartialEq, + serde::Serialize, + serde::Deserialize, +)] +#[diesel(table_name = relay)] +pub struct Relay { + pub id: common_utils::id_type::RelayId, + pub connector_resource_id: String, + pub connector_id: common_utils::id_type::MerchantConnectorAccountId, + pub profile_id: common_utils::id_type::ProfileId, + pub merchant_id: common_utils::id_type::MerchantId, + pub relay_type: storage_enums::RelayType, + pub request_data: Option, + pub status: storage_enums::RelayStatus, + pub connector_reference_id: Option, + pub error_code: Option, + pub error_message: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: PrimitiveDateTime, + pub response_data: Option, +} + +#[derive( + Clone, + Debug, + Eq, + PartialEq, + Insertable, + router_derive::DebugAsDisplay, + serde::Serialize, + serde::Deserialize, + router_derive::Setter, +)] +#[diesel(table_name = relay)] +pub struct RelayNew { + pub id: common_utils::id_type::RelayId, + pub connector_resource_id: String, + pub connector_id: common_utils::id_type::MerchantConnectorAccountId, + pub profile_id: common_utils::id_type::ProfileId, + pub merchant_id: common_utils::id_type::MerchantId, + pub relay_type: storage_enums::RelayType, + pub request_data: Option, + pub status: storage_enums::RelayStatus, + pub connector_reference_id: Option, + pub error_code: Option, + pub error_message: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: PrimitiveDateTime, + pub response_data: Option, +} + +#[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] +#[diesel(table_name = relay)] +pub struct RelayUpdateInternal { + pub connector_reference_id: Option, + pub status: Option, + pub error_code: Option, + pub error_message: Option, + pub modified_at: PrimitiveDateTime, +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 178f5600542a..7eaa71cb09fe 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1233,6 +1233,35 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + relay (id) { + #[max_length = 64] + id -> Varchar, + #[max_length = 128] + connector_resource_id -> Varchar, + #[max_length = 64] + connector_id -> Varchar, + #[max_length = 64] + profile_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + relay_type -> RelayType, + request_data -> Nullable, + status -> RelayStatus, + #[max_length = 128] + connector_reference_id -> Nullable, + #[max_length = 64] + error_code -> Nullable, + error_message -> Nullable, + created_at -> Timestamp, + modified_at -> Timestamp, + response_data -> Nullable, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -1477,6 +1506,7 @@ diesel::allow_tables_to_appear_in_same_query!( payouts, process_tracker, refund, + relay, reverse_lookup, roles, routing_algorithm, diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index da2298d934bb..790bf75beda7 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1179,6 +1179,35 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + relay (id) { + #[max_length = 64] + id -> Varchar, + #[max_length = 128] + connector_resource_id -> Varchar, + #[max_length = 64] + connector_id -> Varchar, + #[max_length = 64] + profile_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + relay_type -> RelayType, + request_data -> Nullable, + status -> RelayStatus, + #[max_length = 128] + connector_reference_id -> Nullable, + #[max_length = 64] + error_code -> Nullable, + error_message -> Nullable, + created_at -> Timestamp, + modified_at -> Timestamp, + response_data -> Nullable, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -1425,6 +1454,7 @@ diesel::allow_tables_to_appear_in_same_query!( payouts, process_tracker, refund, + relay, reverse_lookup, roles, routing_algorithm, diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index 0e8722644bd4..15cccaa396a8 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -17,6 +17,7 @@ pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; pub mod refunds; +pub mod relay; pub mod router_data; pub mod router_data_v2; pub mod router_flow_types; diff --git a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs index 51c75113dcda..baec0e59f8a4 100644 --- a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs +++ b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs @@ -1,12 +1,11 @@ #[cfg(feature = "v2")] use api_models::admin; -#[cfg(feature = "v2")] -use common_utils::ext_traits::ValueExt; use common_utils::{ crypto::Encryptable, date_time, encryption::Encryption, errors::{CustomResult, ValidationError}, + ext_traits::ValueExt, id_type, pii, type_name, types::keymanager::{Identifier, KeyManagerState, ToEncryptable}, }; @@ -21,9 +20,10 @@ use serde_json::Value; use super::behaviour; #[cfg(feature = "v2")] use crate::errors::api_error_response::ApiErrorResponse; -#[cfg(feature = "v2")] -use crate::router_data; -use crate::type_encryption::{crypto_operation, CryptoOperation}; +use crate::{ + router_data, + type_encryption::{crypto_operation, CryptoOperation}, +}; #[cfg(feature = "v1")] #[derive(Clone, Debug, router_derive::ToEncryption)] @@ -62,6 +62,19 @@ impl MerchantConnectorAccount { pub fn get_id(&self) -> id_type::MerchantConnectorAccountId { self.merchant_connector_id.clone() } + + pub fn get_connector_account_details( + &self, + ) -> error_stack::Result + { + self.connector_account_details + .get_inner() + .clone() + .parse_value("ConnectorAuthType") + } + pub fn get_connector_test_mode(&self) -> Option { + self.test_mode + } } #[cfg(feature = "v2")] @@ -134,6 +147,9 @@ impl MerchantConnectorAccount { .clone() .parse_value("ConnectorAuthType") } + pub fn get_connector_test_mode(&self) -> Option { + todo!() + } } #[cfg(feature = "v1")] diff --git a/crates/hyperswitch_domain_models/src/relay.rs b/crates/hyperswitch_domain_models/src/relay.rs new file mode 100644 index 000000000000..959ac8e7f612 --- /dev/null +++ b/crates/hyperswitch_domain_models/src/relay.rs @@ -0,0 +1,275 @@ +use common_enums::enums; +use common_utils::{ + self, + errors::{CustomResult, ValidationError}, + id_type::{self, GenerateId}, + pii, + types::{keymanager, MinorUnit}, +}; +use diesel_models::relay::RelayUpdateInternal; +use error_stack::ResultExt; +use masking::{ExposeInterface, Secret}; +use serde::{self, Deserialize, Serialize}; +use time::PrimitiveDateTime; + +use crate::{router_data::ErrorResponse, router_response_types}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Relay { + pub id: id_type::RelayId, + pub connector_resource_id: String, + pub connector_id: id_type::MerchantConnectorAccountId, + pub profile_id: id_type::ProfileId, + pub merchant_id: id_type::MerchantId, + pub relay_type: enums::RelayType, + pub request_data: Option, + pub status: enums::RelayStatus, + pub connector_reference_id: Option, + pub error_code: Option, + pub error_message: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: PrimitiveDateTime, + pub response_data: Option, +} + +impl Relay { + pub fn new( + relay_request: &api_models::relay::RelayRequest, + merchant_id: &id_type::MerchantId, + profile_id: &id_type::ProfileId, + ) -> Self { + let relay_id = id_type::RelayId::generate(); + Self { + id: relay_id.clone(), + connector_resource_id: relay_request.connector_resource_id.clone(), + connector_id: relay_request.connector_id.clone(), + profile_id: profile_id.clone(), + merchant_id: merchant_id.clone(), + relay_type: common_enums::RelayType::Refund, + request_data: relay_request.data.clone().map(From::from), + status: common_enums::RelayStatus::Created, + connector_reference_id: None, + error_code: None, + error_message: None, + created_at: common_utils::date_time::now(), + modified_at: common_utils::date_time::now(), + response_data: None, + } + } +} + +impl From for RelayData { + fn from(relay: api_models::relay::RelayData) -> Self { + match relay { + api_models::relay::RelayData::Refund(relay_refund_request) => { + Self::Refund(RelayRefundData { + amount: relay_refund_request.amount, + currency: relay_refund_request.currency, + reason: relay_refund_request.reason, + }) + } + } + } +} + +impl RelayUpdate { + pub fn from( + response: Result, + ) -> Self { + match response { + Err(error) => Self::ErrorUpdate { + error_code: error.code, + error_message: error.message, + status: common_enums::RelayStatus::Failure, + }, + Ok(response) => Self::StatusUpdate { + connector_reference_id: Some(response.connector_refund_id), + status: common_enums::RelayStatus::from(response.refund_status), + }, + } + } +} + +impl From for api_models::relay::RelayResponse { + fn from(value: Relay) -> Self { + let error = value + .error_code + .zip(value.error_message) + .map( + |(error_code, error_message)| api_models::relay::RelayError { + code: error_code, + message: error_message, + }, + ); + + let data = value.request_data.map(|relay_data| match relay_data { + RelayData::Refund(relay_refund_request) => { + api_models::relay::RelayData::Refund(api_models::relay::RelayRefundRequest { + amount: relay_refund_request.amount, + currency: relay_refund_request.currency, + reason: relay_refund_request.reason, + }) + } + }); + Self { + id: value.id, + status: value.status, + error, + connector_resource_id: value.connector_resource_id, + connector_id: value.connector_id, + profile_id: value.profile_id, + relay_type: value.relay_type, + data, + connector_reference_id: value.connector_reference_id, + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "snake_case", untagged)] +pub enum RelayData { + Refund(RelayRefundData), +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RelayRefundData { + pub amount: MinorUnit, + pub currency: enums::Currency, + pub reason: Option, +} + +#[derive(Debug)] +pub enum RelayUpdate { + ErrorUpdate { + error_code: String, + error_message: String, + status: enums::RelayStatus, + }, + StatusUpdate { + connector_reference_id: Option, + status: common_enums::RelayStatus, + }, +} + +impl From for RelayUpdateInternal { + fn from(value: RelayUpdate) -> Self { + match value { + RelayUpdate::ErrorUpdate { + error_code, + error_message, + status, + } => Self { + error_code: Some(error_code), + error_message: Some(error_message), + connector_reference_id: None, + status: Some(status), + modified_at: common_utils::date_time::now(), + }, + RelayUpdate::StatusUpdate { + connector_reference_id, + status, + } => Self { + connector_reference_id, + status: Some(status), + error_code: None, + error_message: None, + modified_at: common_utils::date_time::now(), + }, + } + } +} + +#[async_trait::async_trait] +impl super::behaviour::Conversion for Relay { + type DstType = diesel_models::relay::Relay; + type NewDstType = diesel_models::relay::RelayNew; + + async fn convert(self) -> CustomResult { + Ok(diesel_models::relay::Relay { + id: self.id, + connector_resource_id: self.connector_resource_id, + connector_id: self.connector_id, + profile_id: self.profile_id, + merchant_id: self.merchant_id, + relay_type: self.relay_type, + request_data: self + .request_data + .map(|data| { + serde_json::to_value(data).change_context(ValidationError::InvalidValue { + message: "Failed while decrypting business profile data".to_string(), + }) + }) + .transpose()? + .map(Secret::new), + status: self.status, + connector_reference_id: self.connector_reference_id, + error_code: self.error_code, + error_message: self.error_message, + created_at: self.created_at, + modified_at: self.modified_at, + response_data: self.response_data, + }) + } + + async fn convert_back( + _state: &keymanager::KeyManagerState, + item: Self::DstType, + _key: &Secret>, + _key_manager_identifier: keymanager::Identifier, + ) -> CustomResult { + Ok(Self { + id: item.id, + connector_resource_id: item.connector_resource_id, + connector_id: item.connector_id, + profile_id: item.profile_id, + merchant_id: item.merchant_id, + relay_type: enums::RelayType::Refund, + request_data: item + .request_data + .map(|data| { + serde_json::from_value(data.expose()).change_context( + ValidationError::InvalidValue { + message: "Failed while decrypting business profile data".to_string(), + }, + ) + }) + .transpose()?, + status: item.status, + connector_reference_id: item.connector_reference_id, + error_code: item.error_code, + error_message: item.error_message, + created_at: item.created_at, + modified_at: item.modified_at, + response_data: item.response_data, + }) + } + + async fn construct_new(self) -> CustomResult { + Ok(diesel_models::relay::RelayNew { + id: self.id, + connector_resource_id: self.connector_resource_id, + connector_id: self.connector_id, + profile_id: self.profile_id, + merchant_id: self.merchant_id, + relay_type: self.relay_type, + request_data: self + .request_data + .map(|data| { + serde_json::to_value(data).change_context(ValidationError::InvalidValue { + message: "Failed while decrypting business profile data".to_string(), + }) + }) + .transpose()? + .map(Secret::new), + status: self.status, + connector_reference_id: self.connector_reference_id, + error_code: self.error_code, + error_message: self.error_message, + created_at: self.created_at, + modified_at: self.modified_at, + response_data: self.response_data, + }) + } +} diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index db0db93109ba..22d355ad4037 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -84,6 +84,10 @@ Never share your secret api keys. Keep them guarded and secure. routes::payments::payments_complete_authorize, routes::payments::payments_post_session_tokens, + // Routes for relay + routes::relay, + routes::relay_retrieve, + // Routes for refunds routes::refunds::refunds_create, routes::refunds::refunds_retrieve, @@ -513,6 +517,13 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::PaymentMethodCollectLinkResponse, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, + api_models::relay::RelayRequest, + api_models::relay::RelayResponse, + api_models::enums::RelayType, + api_models::relay::RelayData, + api_models::relay::RelayRefundRequest, + api_models::enums::RelayStatus, + api_models::relay::RelayError, api_models::payments::AmountFilter, api_models::mandates::MandateRevokedResponse, api_models::mandates::MandateResponse, diff --git a/crates/openapi/src/routes.rs b/crates/openapi/src/routes.rs index 453f7f557d34..2c1c58244124 100644 --- a/crates/openapi/src/routes.rs +++ b/crates/openapi/src/routes.rs @@ -16,10 +16,11 @@ pub mod payouts; pub mod poll; pub mod profile; pub mod refunds; +pub mod relay; pub mod routing; pub mod webhook_events; pub use self::{ customers::*, mandates::*, merchant_account::*, merchant_connector_account::*, organization::*, - payment_method::*, payments::*, poll::*, refunds::*, routing::*, webhook_events::*, + payment_method::*, payments::*, poll::*, refunds::*, relay::*, routing::*, webhook_events::*, }; diff --git a/crates/openapi/src/routes/relay.rs b/crates/openapi/src/routes/relay.rs new file mode 100644 index 000000000000..9100bf47f759 --- /dev/null +++ b/crates/openapi/src/routes/relay.rs @@ -0,0 +1,57 @@ +/// Relay - Create +/// +/// Creates a relay request. +#[utoipa::path( + post, + path = "/relay", + request_body( + content = RelayRequest, + examples(( + "Create a relay request" = ( + value = json!({ + "connector_resource_id": "7256228702616471803954", + "connector_id": "mca_5apGeP94tMts6rg3U3kR", + "type": "refund", + "data": { + "amount": 6540, + "currency": "USD" + } + }) + ) + )) + ), + responses( + (status = 200, description = "Relay request", body = RelayResponse), + (status = 400, description = "Invalid data") + ), + params( + ("X-Profile-Id" = String, Header, description = "Profile ID for authentication"), + ("X-Idempotency-Key" = String, Header, description = "Idempotency Key for relay request") + ), + tag = "Relay", + operation_id = "Relay Request", + security(("api_key" = [])) +)] + +pub async fn relay() {} + +/// Relay - Retrieve +/// +/// Retrieves a relay details. +#[utoipa::path( + get, + path = "/relay/{relay_id}", + params (("id" = String, Path, description = "The unique identifier for the Relay")), + responses( + (status = 200, description = "Relay Retrieved", body = RelayResponse), + (status = 404, description = "Relay details was not found") + ), + params( + ("X-Profile-Id" = String, Header, description = "Profile ID for authentication") + ), + tag = "Relay", + operation_id = "Retrieve a Relay details", + security(("api_key" = []), ("ephemeral_key" = [])) +)] + +pub async fn relay_retrieve() {} diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index f6fe1d85b5de..c22cecc20f9f 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -54,3 +54,5 @@ pub mod verify_connector; pub mod webhooks; pub mod unified_authentication_service; + +pub mod relay; diff --git a/crates/router/src/core/relay.rs b/crates/router/src/core/relay.rs new file mode 100644 index 000000000000..d25296635ad1 --- /dev/null +++ b/crates/router/src/core/relay.rs @@ -0,0 +1,330 @@ +use api_models::relay as relay_models; +use common_enums::RelayStatus; +use common_utils::{self, id_type}; +use error_stack::ResultExt; + +use super::errors::{self, ConnectorErrorExt, RouterResponse, RouterResult, StorageErrorExt}; +use crate::{ + core::payments, + routes::SessionState, + services, + types::{ + api::{self}, + domain, + }, + utils::OptionExt, +}; + +pub mod utils; + +pub async fn relay( + state: SessionState, + merchant_account: domain::MerchantAccount, + profile_id_optional: Option, + key_store: domain::MerchantKeyStore, + req: relay_models::RelayRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + let key_manager_state = &(&state).into(); + let merchant_id = merchant_account.get_id(); + let connector_id = &req.connector_id; + + let profile_id_from_auth_layer = profile_id_optional.get_required_value("ProfileId")?; + + let profile = db + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + merchant_id, + &profile_id_from_auth_layer, + ) + .await + .change_context(errors::ApiErrorResponse::ProfileNotFound { + id: profile_id_from_auth_layer.get_string_repr().to_owned(), + })?; + + #[cfg(feature = "v1")] + let connector_account = db + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + key_manager_state, + merchant_id, + connector_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: connector_id.get_string_repr().to_string(), + })?; + + #[cfg(feature = "v2")] + let connector_account = db + .find_merchant_connector_account_by_id(key_manager_state, connector_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: connector_id.get_string_repr().to_string(), + })?; + + validate_relay_refund_request(&req).attach_printable("Invalid relay refund request")?; + + let relay_domain = + hyperswitch_domain_models::relay::Relay::new(&req, merchant_id, profile.get_id()); + + let relay_record = db + .insert_relay(key_manager_state, &key_store, relay_domain) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to insert a relay record in db")?; + + let relay_response = match req.relay_type { + common_enums::RelayType::Refund => { + Box::pin(relay_refund( + &state, + merchant_account, + connector_account, + &relay_record, + )) + .await? + } + }; + + let relay_update_record = db + .update_relay(key_manager_state, &key_store, relay_record, relay_response) + .await + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + let response = relay_models::RelayResponse::from(relay_update_record); + + Ok(hyperswitch_domain_models::api::ApplicationResponse::Json( + response, + )) +} + +pub async fn relay_refund( + state: &SessionState, + merchant_account: domain::MerchantAccount, + connector_account: domain::MerchantConnectorAccount, + relay_record: &hyperswitch_domain_models::relay::Relay, +) -> RouterResult { + let connector_id = &relay_record.connector_id; + + let merchant_id = merchant_account.get_id(); + + let connector_data = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &connector_account.connector_name, + api::GetToken::Connector, + Some(connector_id.clone()), + )?; + + let connector_integration: services::BoxedRefundConnectorIntegrationInterface< + api::Execute, + hyperswitch_domain_models::router_request_types::RefundsData, + hyperswitch_domain_models::router_response_types::RefundsResponseData, + > = connector_data.connector.get_connector_integration(); + + let router_data = utils::construct_relay_refund_router_data( + state, + &connector_account.connector_name, + merchant_id, + &connector_account, + relay_record, + ) + .await?; + + let router_data_res = services::execute_connector_processing_step( + state, + connector_integration, + &router_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await + .to_refund_failed_response()?; + + let relay_response = + hyperswitch_domain_models::relay::RelayUpdate::from(router_data_res.response); + + Ok(relay_response) +} + +// validate relay request +pub fn validate_relay_refund_request( + relay_request: &relay_models::RelayRequest, +) -> RouterResult<()> { + match (relay_request.relay_type, &relay_request.data) { + (common_enums::RelayType::Refund, Some(relay_models::RelayData::Refund(ref_data))) => { + validate_relay_refund_data(ref_data) + } + (common_enums::RelayType::Refund, None) => { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Relay data is required for refund relay".to_string(), + })? + } + } +} + +pub fn validate_relay_refund_data( + refund_data: &relay_models::RelayRefundRequest, +) -> RouterResult<()> { + if refund_data.amount.get_amount_as_i64() <= 0 { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Amount should be greater than 0".to_string(), + })? + } + Ok(()) +} + +pub async fn relay_retrieve( + state: SessionState, + merchant_account: domain::MerchantAccount, + profile_id_optional: Option, + key_store: domain::MerchantKeyStore, + req: relay_models::RelayRetrieveRequest, +) -> RouterResponse { + let db = state.store.as_ref(); + let key_manager_state = &(&state).into(); + let merchant_id = merchant_account.get_id(); + let relay_id = &req.id; + + let profile_id_from_auth_layer = profile_id_optional.get_required_value("ProfileId")?; + + db.find_business_profile_by_merchant_id_profile_id( + key_manager_state, + &key_store, + merchant_id, + &profile_id_from_auth_layer, + ) + .await + .change_context(errors::ApiErrorResponse::ProfileNotFound { + id: profile_id_from_auth_layer.get_string_repr().to_owned(), + })?; + + let relay_record_result = db + .find_relay_by_id(key_manager_state, &key_store, relay_id) + .await; + + let relay_record = match relay_record_result { + Err(error) => { + if error.current_context().is_db_not_found() { + Err(error).change_context(errors::ApiErrorResponse::GenericNotFoundError { + message: "relay not found".to_string(), + })? + } else { + Err(error) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error while fetch relay record")? + } + } + Ok(relay) => relay, + }; + + #[cfg(feature = "v1")] + let connector_account = db + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + key_manager_state, + merchant_id, + &relay_record.connector_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: relay_record.connector_id.get_string_repr().to_string(), + })?; + + #[cfg(feature = "v2")] + let connector_account = db + .find_merchant_connector_account_by_id( + key_manager_state, + &relay_record.connector_id, + &key_store, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: relay_record.connector_id.get_string_repr().to_string(), + })?; + + let relay_response = match relay_record.relay_type { + common_enums::RelayType::Refund => { + if should_call_connector_for_relay_refund_status(&relay_record, req.force_sync) { + let relay_response = sync_relay_refund_with_gateway( + &state, + &merchant_account, + &relay_record, + connector_account, + ) + .await?; + + db.update_relay(key_manager_state, &key_store, relay_record, relay_response) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update the relay record")? + } else { + relay_record + } + } + }; + + let response = relay_models::RelayResponse::from(relay_response); + + Ok(hyperswitch_domain_models::api::ApplicationResponse::Json( + response, + )) +} + +fn should_call_connector_for_relay_refund_status( + relay: &hyperswitch_domain_models::relay::Relay, + force_sync: bool, +) -> bool { + // This allows refund sync at connector level if force_sync is enabled, or + // check if the refund is in terminal state + !matches!(relay.status, RelayStatus::Failure | RelayStatus::Success) && force_sync +} + +pub async fn sync_relay_refund_with_gateway( + state: &SessionState, + merchant_account: &domain::MerchantAccount, + relay_record: &hyperswitch_domain_models::relay::Relay, + connector_account: domain::MerchantConnectorAccount, +) -> RouterResult { + let connector_id = &relay_record.connector_id; + let merchant_id = merchant_account.get_id(); + + let connector_data: api::ConnectorData = api::ConnectorData::get_connector_by_name( + &state.conf.connectors, + &connector_account.connector_name, + api::GetToken::Connector, + Some(connector_id.clone()), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get the connector")?; + + let router_data = utils::construct_relay_refund_router_data( + state, + &connector_account.connector_name, + merchant_id, + &connector_account, + relay_record, + ) + .await?; + + let connector_integration: services::BoxedRefundConnectorIntegrationInterface< + api::RSync, + hyperswitch_domain_models::router_request_types::RefundsData, + hyperswitch_domain_models::router_response_types::RefundsResponseData, + > = connector_data.connector.get_connector_integration(); + + let router_data_res = services::execute_connector_processing_step( + state, + connector_integration, + &router_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await + .to_refund_failed_response()?; + + let relay_response = + hyperswitch_domain_models::relay::RelayUpdate::from(router_data_res.response); + + Ok(relay_response) +} diff --git a/crates/router/src/core/relay/utils.rs b/crates/router/src/core/relay/utils.rs new file mode 100644 index 000000000000..0753be1ec435 --- /dev/null +++ b/crates/router/src/core/relay/utils.rs @@ -0,0 +1,139 @@ +use std::str::FromStr; + +use common_utils::{ext_traits::OptionExt, id_type}; +use error_stack::ResultExt; +use hyperswitch_domain_models::{router_data::ErrorResponse, types}; + +use crate::{ + core::payments, + db::{ + domain, + errors::{self, RouterResult}, + }, + routes::SessionState, +}; + +const IRRELEVANT_PAYMENT_INTENT_ID: &str = "irrelevant_payment_intent_id"; + +const IRRELEVANT_PAYMENT_ATTEMPT_ID: &str = "irrelevant_payment_attempt_id"; + +pub async fn construct_relay_refund_router_data<'a, F>( + state: &'a SessionState, + connector_name: &str, + merchant_id: &id_type::MerchantId, + connector_account: &domain::MerchantConnectorAccount, + relay_record: &hyperswitch_domain_models::relay::Relay, +) -> RouterResult> { + let connector_auth_type = connector_account + .get_connector_account_details() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while parsing value for ConnectorAuthType")?; + + let webhook_url = Some(payments::helpers::create_webhook_url( + &state.base_url.clone(), + merchant_id, + connector_name, + )); + + let supported_connector = &state + .conf + .multiple_api_version_supported_connectors + .supported_connectors; + + let connector_enum = api_models::enums::Connector::from_str(connector_name) + .change_context(errors::ConnectorError::InvalidConnectorName) + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "connector", + }) + .attach_printable_lazy(|| format!("unable to parse connector name {connector_name:?}"))?; + + let connector_api_version = if supported_connector.contains(&connector_enum) { + state + .store + .find_config_by_key(&format!("connector_api_version_{connector_name}")) + .await + .map(|value| value.config) + .ok() + } else { + None + }; + + let hyperswitch_domain_models::relay::RelayData::Refund(relay_refund_data) = relay_record + .request_data + .clone() + .get_required_value("refund relay data") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to obtain relay data to construct relay refund data")?; + + let relay_id_string = relay_record.id.get_string_repr().to_string(); + + let router_data = hyperswitch_domain_models::router_data::RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_id.clone(), + customer_id: None, + connector: connector_name.to_string(), + payment_id: IRRELEVANT_PAYMENT_INTENT_ID.to_string(), + attempt_id: IRRELEVANT_PAYMENT_ATTEMPT_ID.to_string(), + status: common_enums::AttemptStatus::Charged, + payment_method: common_enums::PaymentMethod::default(), + connector_auth_type, + description: None, + return_url: None, + address: hyperswitch_domain_models::payment_address::PaymentAddress::default(), + auth_type: common_enums::AuthenticationType::default(), + connector_meta_data: None, + connector_wallets_details: None, + amount_captured: None, + payment_method_status: None, + minor_amount_captured: None, + request: hyperswitch_domain_models::router_request_types::RefundsData { + refund_id: relay_id_string.clone(), + connector_transaction_id: relay_record.connector_resource_id.clone(), + refund_amount: relay_refund_data.amount.get_amount_as_i64(), + minor_refund_amount: relay_refund_data.amount, + currency: relay_refund_data.currency, + payment_amount: relay_refund_data.amount.get_amount_as_i64(), + minor_payment_amount: relay_refund_data.amount, + webhook_url, + connector_metadata: None, + reason: relay_refund_data.reason, + connector_refund_id: relay_record.connector_reference_id.clone(), + browser_info: None, + split_refunds: None, + integrity_object: None, + refund_status: common_enums::RefundStatus::from(relay_record.status), + }, + + response: Err(ErrorResponse::default()), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + recurring_mandate_payment_data: None, + preprocessing_id: None, + connector_request_reference_id: relay_id_string.clone(), + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + test_mode: connector_account.get_connector_test_mode(), + payment_method_balance: None, + connector_api_version, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + frm_metadata: None, + refund_id: Some(relay_id_string), + dispute_id: None, + connector_response: None, + integrity_check: Ok(()), + additional_merchant_data: None, + header_payload: None, + connector_mandate_request_reference_id: None, + authentication_id: None, + psd2_sca_exemption_type: None, + }; + + Ok(router_data) +} diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index cc9a1e2f436d..458e1ae85174 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -30,6 +30,7 @@ pub mod organization; pub mod payment_link; pub mod payment_method; pub mod refund; +pub mod relay; pub mod reverse_lookup; pub mod role; pub mod routing_algorithm; @@ -132,6 +133,7 @@ pub trait StorageInterface: + user_authentication_method::UserAuthenticationMethodInterface + authentication::AuthenticationInterface + generic_link::GenericLinkInterface + + relay::RelayInterface + 'static { fn get_scheduler_db(&self) -> Box; diff --git a/crates/router/src/db/relay.rs b/crates/router/src/db/relay.rs new file mode 100644 index 000000000000..46259679c55d --- /dev/null +++ b/crates/router/src/db/relay.rs @@ -0,0 +1,181 @@ +use common_utils::types::keymanager::KeyManagerState; +use diesel_models; +use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::behaviour::{Conversion, ReverseConversion}; +use storage_impl::MockDb; + +use super::domain; +use crate::{ + connection, + core::errors::{self, CustomResult}, + db::kafka_store::KafkaStore, + services::Store, +}; + +#[async_trait::async_trait] +pub trait RelayInterface { + async fn insert_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + new: hyperswitch_domain_models::relay::Relay, + ) -> CustomResult; + + async fn update_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + current_state: hyperswitch_domain_models::relay::Relay, + relay_update: hyperswitch_domain_models::relay::RelayUpdate, + ) -> CustomResult; + + async fn find_relay_by_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + relay_id: &common_utils::id_type::RelayId, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl RelayInterface for Store { + async fn insert_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + new: hyperswitch_domain_models::relay::Relay, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + new.construct_new() + .await + .change_context(errors::StorageError::EncryptionError)? + .insert(&conn) + .await + .map_err(|error| report!(errors::StorageError::from(error)))? + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + } + + async fn update_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + current_state: hyperswitch_domain_models::relay::Relay, + relay_update: hyperswitch_domain_models::relay::RelayUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + Conversion::convert(current_state) + .await + .change_context(errors::StorageError::EncryptionError)? + .update( + &conn, + diesel_models::relay::RelayUpdateInternal::from(relay_update), + ) + .await + .map_err(|error| report!(errors::StorageError::from(error)))? + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + } + + async fn find_relay_by_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + relay_id: &common_utils::id_type::RelayId, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + diesel_models::relay::Relay::find_by_id(&conn, relay_id) + .await + .map_err(|error| report!(errors::StorageError::from(error)))? + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + } +} + +#[async_trait::async_trait] +impl RelayInterface for MockDb { + async fn insert_relay( + &self, + _key_manager_state: &KeyManagerState, + _merchant_key_store: &domain::MerchantKeyStore, + _new: hyperswitch_domain_models::relay::Relay, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn update_relay( + &self, + _key_manager_state: &KeyManagerState, + _merchant_key_store: &domain::MerchantKeyStore, + _current_state: hyperswitch_domain_models::relay::Relay, + _relay_update: hyperswitch_domain_models::relay::RelayUpdate, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_relay_by_id( + &self, + _key_manager_state: &KeyManagerState, + _merchant_key_store: &domain::MerchantKeyStore, + _relay_id: &common_utils::id_type::RelayId, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } +} + +#[async_trait::async_trait] +impl RelayInterface for KafkaStore { + async fn insert_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + new: hyperswitch_domain_models::relay::Relay, + ) -> CustomResult { + self.diesel_store + .insert_relay(key_manager_state, merchant_key_store, new) + .await + } + + async fn update_relay( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + current_state: hyperswitch_domain_models::relay::Relay, + relay_update: hyperswitch_domain_models::relay::RelayUpdate, + ) -> CustomResult { + self.diesel_store + .update_relay( + key_manager_state, + merchant_key_store, + current_state, + relay_update, + ) + .await + } + + async fn find_relay_by_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + relay_id: &common_utils::id_type::RelayId, + ) -> CustomResult { + self.diesel_store + .find_relay_by_id(key_manager_state, merchant_key_store, relay_id) + .await + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index d43fa0543138..9100ea3ebaa5 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -140,7 +140,8 @@ pub fn mk_app( .service(routes::Customers::server(state.clone())) .service(routes::Configs::server(state.clone())) .service(routes::MerchantConnectorAccount::server(state.clone())) - .service(routes::Webhooks::server(state.clone())); + .service(routes::Webhooks::server(state.clone())) + .service(routes::Relay::server(state.clone())); #[cfg(feature = "oltp")] { diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index aaadcaa4eb51..53e5d5aac71d 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -58,6 +58,8 @@ pub mod verify_connector; pub mod webhook_events; pub mod webhooks; +pub mod relay; + #[cfg(feature = "dummy_connector")] pub use self::app::DummyConnector; #[cfg(all(feature = "olap", feature = "recon", feature = "v1"))] @@ -66,7 +68,7 @@ pub use self::app::{ ApiKeys, AppState, ApplePayCertificatesMigration, Cache, Cards, Configs, ConnectorOnboarding, Customers, Disputes, EphemeralKey, Files, Forex, Gsm, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Poll, Profile, ProfileNew, - Refunds, SessionState, User, Webhooks, + Refunds, Relay, SessionState, User, Webhooks, }; #[cfg(feature = "olap")] pub use self::app::{Blocklist, Organization, Routing, Verify, WebhookEvents}; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 766679491c54..88251ffffbcc 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -55,7 +55,7 @@ use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_ve use super::webhooks::*; use super::{ admin, api_keys, cache::*, connector_onboarding, disputes, files, gsm, health::*, profiles, - user, user_role, + relay, user, user_role, }; #[cfg(feature = "v1")] use super::{apple_pay_certificates_migration, blocklist, payment_link, webhook_events}; @@ -582,6 +582,18 @@ impl Payments { } } +pub struct Relay; + +#[cfg(feature = "oltp")] +impl Relay { + pub fn server(state: AppState) -> Scope { + web::scope("/relay") + .app_data(web::Data::new(state)) + .service(web::resource("").route(web::post().to(relay::relay))) + .service(web::resource("/{relay_id}").route(web::get().to(relay::relay_retrieve))) + } +} + #[cfg(feature = "v1")] impl Payments { pub fn server(state: AppState) -> Scope { diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index b904cb343a53..dd0fbcdc08e0 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -37,6 +37,7 @@ pub enum ApiIdentifier { Recon, Poll, ApplePayCertificatesMigration, + Relay, } impl From for ApiIdentifier { @@ -164,6 +165,7 @@ impl From for ApiIdentifier { | Flow::RefundsFilters | Flow::RefundsAggregate | Flow::RefundsManualUpdate => Self::Refunds, + Flow::Relay | Flow::RelayRetrieve => Self::Relay, Flow::FrmFulfillment | Flow::IncomingWebhookReceive diff --git a/crates/router/src/routes/relay.rs b/crates/router/src/routes/relay.rs new file mode 100644 index 000000000000..cfc66253d50b --- /dev/null +++ b/crates/router/src/routes/relay.rs @@ -0,0 +1,76 @@ +use actix_web::{web, Responder}; +use router_env::{instrument, tracing, Flow}; + +use crate::{ + self as app, + core::{api_locking, relay}, + services::{api, authentication as auth}, +}; + +#[instrument(skip_all, fields(flow = ?Flow::Relay))] +#[cfg(feature = "oltp")] +pub async fn relay( + state: web::Data, + req: actix_web::HttpRequest, + payload: web::Json, +) -> impl Responder { + let flow = Flow::Relay; + let payload = payload.into_inner(); + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: auth::AuthenticationData, req, _| { + relay::relay( + state, + auth.merchant_account, + #[cfg(feature = "v1")] + auth.profile_id, + #[cfg(feature = "v2")] + Some(auth.profile.get_id().clone()), + auth.key_store, + req, + ) + }, + &auth::HeaderAuth(auth::ApiKeyAuth), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::RelayRetrieve))] +#[cfg(feature = "oltp")] +pub async fn relay_retrieve( + state: web::Data, + path: web::Path, + req: actix_web::HttpRequest, + query_params: web::Query, +) -> impl Responder { + let flow = Flow::RelayRetrieve; + let relay_retrieve_request = api_models::relay::RelayRetrieveRequest { + force_sync: query_params.force_sync, + id: path.into_inner(), + }; + Box::pin(api::server_wrap( + flow, + state, + &req, + relay_retrieve_request, + |state, auth: auth::AuthenticationData, req, _| { + relay::relay_retrieve( + state, + auth.merchant_account, + #[cfg(feature = "v1")] + auth.profile_id, + #[cfg(feature = "v2")] + Some(auth.profile.get_id().clone()), + auth.key_store, + req, + ) + }, + &auth::HeaderAuth(auth::ApiKeyAuth), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index e8435243ff4d..88608316640a 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use actix_web::http::header::HeaderMap; #[cfg(all( any(feature = "v2", feature = "v1"), @@ -538,6 +540,15 @@ where .change_context(errors::ApiErrorResponse::Unauthorized) .attach_printable("Failed to fetch merchant key store for the merchant id")?; + let profile_id = + get_header_value_by_key(headers::X_PROFILE_ID.to_string(), request_headers)? + .map(id_type::ProfileId::from_str) + .transpose() + .change_context(errors::ValidationError::IncorrectValueProvided { + field_name: "X-Profile-Id", + }) + .change_context(errors::ApiErrorResponse::Unauthorized)?; + let merchant = state .store() .find_merchant_account_by_merchant_id( @@ -551,7 +562,7 @@ where let auth = AuthenticationData { merchant_account: merchant, key_store, - profile_id: None, + profile_id, }; Ok(( auth.clone(), @@ -3109,7 +3120,7 @@ pub fn get_header_value_by_key(key: String, headers: &HeaderMap) -> RouterResult }) .transpose() } -pub fn get_id_type_by_key_from_headers( +pub fn get_id_type_by_key_from_headers( key: String, headers: &HeaderMap, ) -> RouterResult> { diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 84e322872a9c..661375aadbf1 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -529,6 +529,10 @@ pub enum Flow { PaymentStartRedirection, /// Volume split on the routing type VolumeSplitOnRoutingType, + /// Relay flow + Relay, + /// Relay retrieve flow + RelayRetrieve, } /// Trait for providing generic behaviour to flow metric diff --git a/migrations/2024-12-17-141811_add_relay_table/down.sql b/migrations/2024-12-17-141811_add_relay_table/down.sql new file mode 100644 index 000000000000..47b6682c6791 --- /dev/null +++ b/migrations/2024-12-17-141811_add_relay_table/down.sql @@ -0,0 +1,7 @@ +-- This file should undo anything in `up.sql` +DROP TABLE relay; + +DROP TYPE IF EXISTS "RelayStatus"; + +DROP TYPE IF EXISTS "RelayType"; + diff --git a/migrations/2024-12-17-141811_add_relay_table/up.sql b/migrations/2024-12-17-141811_add_relay_table/up.sql new file mode 100644 index 000000000000..e9f0017bd049 --- /dev/null +++ b/migrations/2024-12-17-141811_add_relay_table/up.sql @@ -0,0 +1,22 @@ +-- Your SQL goes here +CREATE TYPE "RelayStatus" AS ENUM ('created', 'pending', 'failure', 'success'); + +CREATE TYPE "RelayType" AS ENUM ('refund'); + +CREATE TABLE relay ( + id VARCHAR(64) PRIMARY KEY, + connector_resource_id VARCHAR(128) NOT NULL, + connector_id VARCHAR(64) NOT NULL, + profile_id VARCHAR(64) NOT NULL, + merchant_id VARCHAR(64) NOT NULL, + relay_type "RelayType" NOT NULL, + request_data JSONB DEFAULT NULL, + status "RelayStatus" NOT NULL, + connector_reference_id VARCHAR(128), + error_code VARCHAR(64), + error_message TEXT, + created_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + modified_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + response_data JSONB DEFAULT NULL +); +