diff --git a/api-reference/v1/openapi_spec_v1.json b/api-reference/v1/openapi_spec_v1.json index 9e18f37167..32c47be831 100644 --- a/api-reference/v1/openapi_spec_v1.json +++ b/api-reference/v1/openapi_spec_v1.json @@ -19208,6 +19208,15 @@ "format": "int64", "description": "This Unit struct represents MinorUnit in which core amount works" }, + "MitCategory": { + "type": "string", + "enum": [ + "installment", + "unscheduled", + "recurring", + "resubmission" + ] + }, "MobilePayRedirection": { "type": "object" }, @@ -23150,6 +23159,14 @@ "description": "Boolean flag indicating whether this payment method is stored and has been previously used for payments", "example": true, "nullable": true + }, + "mit_category": { + "allOf": [ + { + "$ref": "#/components/schemas/MitCategory" + } + ], + "nullable": true } } }, @@ -23636,6 +23653,14 @@ "description": "Boolean flag indicating whether this payment method is stored and has been previously used for payments", "example": true, "nullable": true + }, + "mit_category": { + "allOf": [ + { + "$ref": "#/components/schemas/MitCategory" + } + ], + "nullable": true } } }, @@ -24273,6 +24298,14 @@ "description": "Boolean flag indicating whether this payment method is stored and has been previously used for payments", "example": true, "nullable": true + }, + "mit_category": { + "allOf": [ + { + "$ref": "#/components/schemas/MitCategory" + } + ], + "nullable": true } } }, @@ -25054,6 +25087,14 @@ "description": "Boolean flag indicating whether this payment method is stored and has been previously used for payments", "example": true, "nullable": true + }, + "mit_category": { + "allOf": [ + { + "$ref": "#/components/schemas/MitCategory" + } + ], + "nullable": true } }, "additionalProperties": false @@ -25717,6 +25758,14 @@ "description": "Boolean flag indicating whether this payment method is stored and has been previously used for payments", "example": true, "nullable": true + }, + "mit_category": { + "allOf": [ + { + "$ref": "#/components/schemas/MitCategory" + } + ], + "nullable": true } } }, @@ -26336,6 +26385,14 @@ "description": "Boolean flag indicating whether this payment method is stored and has been previously used for payments", "example": true, "nullable": true + }, + "mit_category": { + "allOf": [ + { + "$ref": "#/components/schemas/MitCategory" + } + ], + "nullable": true } } }, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index f0fa9ab13a..840cdf7a1d 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1295,6 +1295,10 @@ pub struct PaymentsRequest { /// Boolean flag indicating whether this payment method is stored and has been previously used for payments #[schema(value_type = Option, example = true)] pub is_stored_credential: Option, + + /// The category of the MIT transaction + #[schema(value_type = Option, example = "recurring")] + pub mit_category: Option, } #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] @@ -1467,6 +1471,19 @@ impl PaymentsRequest { Ok(()) } } + + pub fn validate_mit_request(&self) -> common_utils::errors::CustomResult<(), ValidationError> { + if self.mit_category.is_some() + && (!matches!(self.off_session, Some(true)) || self.recurring_details.is_none()) + { + return Err(ValidationError::InvalidValue { + message: "`mit_category` requires both: (1) `off_session = true`, and (2) `recurring_details`.".to_string(), + } + .into()); + } + + Ok(()) + } } #[cfg(feature = "v1")] @@ -5726,6 +5743,10 @@ pub struct PaymentsResponse { /// Boolean flag indicating whether this payment method is stored and has been previously used for payments #[schema(value_type = Option, example = true)] pub is_stored_credential: Option, + + /// The category of the MIT transaction + #[schema(value_type = Option, example = "recurring")] + pub mit_category: Option, } #[cfg(feature = "v2")] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index e6e71abff7..83aeda0dfb 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -9353,6 +9353,33 @@ pub enum TriggeredBy { External, } +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + ToSchema, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum MitCategory { + /// A fixed purchase amount split into multiple scheduled payments until the total is paid. + Installment, + /// Merchant-initiated transaction using stored credentials, but not tied to a fixed schedule + Unscheduled, + /// Merchant-initiated payments that happen at regular intervals (usually the same amount each time). + Recurring, + /// A retried MIT after a previous transaction failed or was declined. + Resubmission, +} + #[derive( Clone, Copy, diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index e1640435b1..7079c720a3 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -80,6 +80,7 @@ pub struct PaymentIntent { pub order_date: Option, pub enable_partial_authorization: Option, pub enable_overcapture: Option, + pub mit_category: Option, pub merchant_reference_id: Option, pub billing_address: Option, pub shipping_address: Option, @@ -184,6 +185,7 @@ pub struct PaymentIntent { pub order_date: Option, pub enable_partial_authorization: Option, pub enable_overcapture: Option, + pub mit_category: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, diesel::AsExpression, PartialEq)] @@ -390,6 +392,7 @@ pub struct PaymentIntentNew { pub shipping_amount_tax: Option, pub duty_amount: Option, pub order_date: Option, + pub mit_category: Option, } #[cfg(feature = "v1")] @@ -474,6 +477,7 @@ pub struct PaymentIntentNew { pub duty_amount: Option, pub enable_partial_authorization: Option, pub enable_overcapture: Option, + pub mit_category: Option, } #[cfg(feature = "v2")] @@ -816,6 +820,7 @@ impl PaymentIntentUpdateInternal { enable_overcapture: None, active_attempt_id_type: source.active_attempt_id_type, active_attempts_group_id: source.active_attempts_group_id, + mit_category: None, } } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index f11f9318f1..bd91c7843e 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1189,6 +1189,8 @@ diesel::table! { order_date -> Nullable, enable_partial_authorization -> Nullable, enable_overcapture -> Nullable, + #[max_length = 64] + mit_category -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index b57a192bfe..560d1fc646 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1124,6 +1124,8 @@ diesel::table! { enable_partial_authorization -> Nullable, enable_overcapture -> Nullable, #[max_length = 64] + mit_category -> Nullable, + #[max_length = 64] merchant_reference_id -> Nullable, billing_address -> Nullable, shipping_address -> Nullable, diff --git a/crates/hyperswitch_connectors/src/connectors/checkout/transformers.rs b/crates/hyperswitch_connectors/src/connectors/checkout/transformers.rs index 0b9aa148e1..44d1a7637e 100644 --- a/crates/hyperswitch_connectors/src/connectors/checkout/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/checkout/transformers.rs @@ -12,6 +12,7 @@ use common_utils::{ use error_stack::ResultExt; use hyperswitch_domain_models::{ payment_method_data::{PaymentMethodData, WalletData}, + payment_methods::storage_enums::MitCategory, router_data::{ AdditionalPaymentMethodConnectorResponse, ConnectorAuthType, ConnectorResponseData, ErrorResponse, PaymentMethodToken, RouterData, @@ -273,6 +274,8 @@ pub enum CheckoutPaymentType { Unscheduled, #[serde(rename = "MOTO")] Moto, + Installment, + Recurring, } pub struct CheckoutAuthType { @@ -585,7 +588,12 @@ impl TryFrom<&CheckoutRouterData<&PaymentsAuthorizeRouterData>> for PaymentsRequ .request .get_connector_mandate_request_reference_id()?, ); - let p_type = CheckoutPaymentType::Unscheduled; + let p_type = match item.router_data.request.mit_category { + Some(MitCategory::Installment) => CheckoutPaymentType::Installment, + Some(MitCategory::Recurring) => CheckoutPaymentType::Recurring, + Some(MitCategory::Unscheduled) | None => CheckoutPaymentType::Unscheduled, + _ => CheckoutPaymentType::Unscheduled, + }; Ok((mandate_source, previous_id, Some(true), p_type, None)) } PaymentMethodData::CardDetailsForNetworkTransactionId(ccard) => { @@ -605,8 +613,12 @@ impl TryFrom<&CheckoutRouterData<&PaymentsAuthorizeRouterData>> for PaymentsRequ .attach_printable("Checkout unable to find NTID for MIT")?, ); - let p_type = CheckoutPaymentType::Unscheduled; - + let p_type = match item.router_data.request.mit_category { + Some(MitCategory::Installment) => CheckoutPaymentType::Installment, + Some(MitCategory::Recurring) => CheckoutPaymentType::Recurring, + Some(MitCategory::Unscheduled) | None => CheckoutPaymentType::Unscheduled, + _ => CheckoutPaymentType::Unscheduled, + }; Ok((payment_source, previous_id, Some(true), p_type, None)) } _ => Err(errors::ConnectorError::NotImplemented( diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index 9846ba5d5a..486c2f4d8e 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -6768,6 +6768,7 @@ pub(crate) fn convert_setup_mandate_router_data_to_authorize_router_data( enable_partial_authorization: data.request.enable_partial_authorization, enable_overcapture: None, is_stored_credential: data.request.is_stored_credential, + mit_category: None, } } diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 2fc41ee558..b1f7a4745e 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -125,6 +125,7 @@ pub struct PaymentIntent { pub duty_amount: Option, pub enable_partial_authorization: Option, pub enable_overcapture: Option, + pub mit_category: Option, } impl PaymentIntent { diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index 8da2bc446f..d3d4f891e6 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -1850,6 +1850,7 @@ impl behaviour::Conversion for PaymentIntent { order_date: None, enable_partial_authorization, enable_overcapture: None, + mit_category: None, }) } async fn convert_back( @@ -2088,6 +2089,7 @@ impl behaviour::Conversion for PaymentIntent { payment_channel: None, tax_status: None, discount_amount: None, + mit_category: None, shipping_amount_tax: None, duty_amount: None, order_date: None, @@ -2175,6 +2177,7 @@ impl behaviour::Conversion for PaymentIntent { duty_amount: self.duty_amount, enable_partial_authorization: self.enable_partial_authorization, enable_overcapture: self.enable_overcapture, + mit_category: self.mit_category, }) } @@ -2284,6 +2287,7 @@ impl behaviour::Conversion for PaymentIntent { order_date: storage_model.order_date, enable_partial_authorization: storage_model.enable_partial_authorization, enable_overcapture: storage_model.enable_overcapture, + mit_category: storage_model.mit_category, }) } .await @@ -2365,6 +2369,7 @@ impl behaviour::Conversion for PaymentIntent { duty_amount: self.duty_amount, enable_partial_authorization: self.enable_partial_authorization, enable_overcapture: self.enable_overcapture, + mit_category: self.mit_category, }) } } diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index 415b97d1af..0eee01af34 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -89,6 +89,7 @@ pub struct PaymentsAuthorizeData { Option, pub enable_overcapture: Option, pub is_stored_credential: Option, + pub mit_category: Option, } #[derive(Debug, Clone)] diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 2759bfc6e1..e20f64b6f3 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -325,6 +325,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::admin::AcceptedCurrencies, api_models::enums::AdyenSplitType, api_models::enums::PaymentType, + api_models::enums::MitCategory, api_models::enums::ScaExemptionType, api_models::enums::PaymentMethod, api_models::enums::TriggeredBy, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 8cfa5e3236..bd8ecb8253 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3969,6 +3969,7 @@ mod tests { connector_id: None, shipping_address_id: None, billing_address_id: None, + mit_category: None, statement_descriptor_name: None, statement_descriptor_suffix: None, created_at: common_utils::date_time::now(), @@ -4054,6 +4055,7 @@ mod tests { metadata: None, connector_id: None, shipping_address_id: None, + mit_category: None, billing_address_id: None, statement_descriptor_name: None, statement_descriptor_suffix: None, @@ -4135,6 +4137,7 @@ mod tests { return_url: None, metadata: None, connector_id: None, + mit_category: None, shipping_address_id: None, billing_address_id: None, statement_descriptor_name: None, diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 985450ad1a..854659a61f 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1041,6 +1041,13 @@ impl ValidateRequest( connector_testing_data: None, order_id: None, locale: None, + mit_category: None, payment_channel: None, enable_partial_authorization: payment_data.payment_intent.enable_partial_authorization, enable_overcapture: None, @@ -3655,6 +3656,7 @@ where merchant_order_reference_id: payment_intent.merchant_order_reference_id, order_tax_amount, connector_mandate_id, + mit_category: payment_intent.mit_category, shipping_cost: payment_intent.shipping_cost, capture_before: payment_attempt.capture_before, extended_authorization_applied: payment_attempt.extended_authorization_applied, @@ -3962,6 +3964,7 @@ impl ForeignFrom<(storage::PaymentIntent, storage::PaymentAttempt)> for api::Pay connector_mandate_id:None, shipping_cost: None, card_discovery: pa.card_discovery, + mit_category: pi.mit_category, force_3ds_challenge: pi.force_3ds_challenge, force_3ds_challenge_trigger: pi.force_3ds_challenge_trigger, whole_connector_response: None, @@ -4304,6 +4307,7 @@ impl TryFrom> for types::PaymentsAuthoriz merchant_config_currency: None, connector_testing_data: None, order_id: None, + mit_category: None, locale: None, payment_channel: None, enable_partial_authorization: None, @@ -4538,6 +4542,7 @@ impl TryFrom> for types::PaymentsAuthoriz merchant_account_id, merchant_config_currency, connector_testing_data, + mit_category: payment_data.payment_intent.mit_category, order_id: None, locale: Some(additional_data.state.locale.clone()), payment_channel: payment_data.payment_intent.payment_channel, diff --git a/crates/router/src/db/events.rs b/crates/router/src/db/events.rs index c31b106dbf..c80e9a24f9 100644 --- a/crates/router/src/db/events.rs +++ b/crates/router/src/db/events.rs @@ -1399,6 +1399,7 @@ mod tests { connector_mandate_id: None, shipping_cost: None, card_discovery: None, + mit_category: None, force_3ds_challenge: None, force_3ds_challenge_trigger: None, issuer_error_code: None, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 7e257a71a9..8205f7c7b9 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -1243,6 +1243,7 @@ impl ForeignFrom<&SetupMandateRouterData> for PaymentsAuthorizeData { enable_partial_authorization: data.request.enable_partial_authorization, enable_overcapture: None, is_stored_credential: data.request.is_stored_credential, + mit_category: None, } } } diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index 76dfb72b76..fae9d3ed19 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -70,6 +70,7 @@ impl VerifyConnectorData { enable_partial_authorization: None, enable_overcapture: None, is_stored_credential: None, + mit_category: None, } } diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 47f73daa3d..b3bff2d182 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -298,6 +298,7 @@ pub async fn generate_sample_data( shipping_amount_tax: None, enable_partial_authorization: None, enable_overcapture: None, + mit_category: None, }; let (connector_transaction_id, processor_transaction_data) = ConnectorTransactionId::form_id_and_data(attempt_id.clone()); diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 2d28cd2008..f548245d17 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -1010,6 +1010,7 @@ impl Default for PaymentAuthorizeType { enable_partial_authorization: None, enable_overcapture: None, is_stored_credential: None, + mit_category: None, }; Self(data) } diff --git a/crates/router/tests/payments.rs b/crates/router/tests/payments.rs index 85801152bd..75917f667b 100644 --- a/crates/router/tests/payments.rs +++ b/crates/router/tests/payments.rs @@ -446,6 +446,7 @@ async fn payments_create_core() { external_3ds_authentication_attempted: None, expires_on: None, fingerprint: None, + mit_category: None, browser_info: None, payment_method_id: None, payment_method_status: None, @@ -733,6 +734,7 @@ async fn payments_create_core_adyen_no_redirect() { expires_on: None, fingerprint: None, browser_info: None, + mit_category: None, payment_method_id: None, payment_method_status: None, updated: None, diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index 1378fac942..676082cb05 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -219,6 +219,7 @@ async fn payments_create_core() { extended_authorization_applied: None, order_tax_amount: None, connector_mandate_id: None, + mit_category: None, shipping_cost: None, card_discovery: None, force_3ds_challenge: None, @@ -512,6 +513,7 @@ async fn payments_create_core_adyen_no_redirect() { capture_before: None, extended_authorization_applied: None, order_tax_amount: None, + mit_category: None, connector_mandate_id: None, shipping_cost: None, card_discovery: None, diff --git a/migrations/2025-09-25-075008_add_mit_category_in_payment_intnet/down.sql b/migrations/2025-09-25-075008_add_mit_category_in_payment_intnet/down.sql new file mode 100644 index 0000000000..332492ecdc --- /dev/null +++ b/migrations/2025-09-25-075008_add_mit_category_in_payment_intnet/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` + +ALTER TABLE payment_intent +DROP COLUMN IF EXISTS mit_category; diff --git a/migrations/2025-09-25-075008_add_mit_category_in_payment_intnet/up.sql b/migrations/2025-09-25-075008_add_mit_category_in_payment_intnet/up.sql new file mode 100644 index 0000000000..086eec7842 --- /dev/null +++ b/migrations/2025-09-25-075008_add_mit_category_in_payment_intnet/up.sql @@ -0,0 +1,3 @@ +-- Add mit_category to payment_intent table +ALTER TABLE payment_intent +ADD COLUMN IF NOT EXISTS mit_category VARCHAR(64);