diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index c252c5a6ab..3125f3ac29 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -16678,6 +16678,12 @@ }, "connector": { "$ref": "#/components/schemas/Connector" + }, + "invoice_next_billing_time": { + "type": "string", + "format": "date-time", + "description": "Invoice Next billing time", + "nullable": true } } }, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 2d32ee6593..011fd7e443 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -8565,6 +8565,8 @@ pub struct PaymentRevenueRecoveryMetadata { /// The name of the payment connector through which the payment attempt was made. #[schema(value_type = Connector, example = "stripe")] pub connector: common_enums::connector_enums::Connector, + /// Invoice Next billing time + pub invoice_next_billing_time: Option, } #[cfg(feature = "v2")] impl PaymentRevenueRecoveryMetadata { @@ -8666,6 +8668,15 @@ pub struct PaymentsAttemptRecordRequest { /// customer id at payment connector for which mandate is attached. #[schema(value_type = String, example = "cust_12345")] pub connector_customer_id: String, + + /// Number of attempts made for invoice + #[schema(value_type = Option, example = 1)] + pub retry_count: Option, + + /// Next Billing time of the Invoice + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub invoice_next_billing_time: Option, } /// Error details for the payment diff --git a/crates/common_utils/src/id_type/global_id/payment.rs b/crates/common_utils/src/id_type/global_id/payment.rs index 22ac0d4c0a..e43e33c619 100644 --- a/crates/common_utils/src/id_type/global_id/payment.rs +++ b/crates/common_utils/src/id_type/global_id/payment.rs @@ -34,7 +34,7 @@ impl GlobalPaymentId { task: &str, runner: enums::ProcessTrackerRunner, ) -> String { - format!("{task}_{runner}_{}", self.get_string_repr()) + format!("{runner}_{task}_{}", self.get_string_repr()) } } diff --git a/crates/diesel_models/src/types.rs b/crates/diesel_models/src/types.rs index 2314d2bf4c..bf463616c6 100644 --- a/crates/diesel_models/src/types.rs +++ b/crates/diesel_models/src/types.rs @@ -171,6 +171,8 @@ pub struct PaymentRevenueRecoveryMetadata { pub payment_method_subtype: common_enums::enums::PaymentMethodType, /// The name of the payment connector through which the payment attempt was made. pub connector: common_enums::connector_enums::Connector, + /// Time at which next invoice will be created + pub invoice_next_billing_time: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] diff --git a/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs b/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs index 11e6de3ac6..ff3b6b422c 100644 --- a/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/chargebee/transformers.rs @@ -274,8 +274,14 @@ pub struct ChargebeeWebhookContent { pub transaction: ChargebeeTransactionData, pub invoice: ChargebeeInvoiceData, pub customer: Option, + pub subscription: Option, } +#[derive(Serialize, Deserialize, Debug)] +pub struct ChargebeeSubscriptionData { + #[serde(default, with = "common_utils::custom_serde::timestamp::option")] + pub next_billing_at: Option, +} #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "snake_case")] pub enum ChargebeeEventType { @@ -290,6 +296,13 @@ pub struct ChargebeeInvoiceData { pub id: String, pub total: MinorUnit, pub currency_code: enums::Currency, + pub billing_address: Option, + pub linked_payments: Option>, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ChargebeeInvoicePayments { + pub txn_status: Option, } #[derive(Serialize, Deserialize, Debug)] @@ -353,6 +366,17 @@ pub struct ChargebeeCustomer { pub payment_method: ChargebeePaymentMethod, } +#[derive(Serialize, Deserialize, Debug)] +pub struct ChargebeeInvoiceBillingAddress { + pub line1: Option>, + pub line2: Option>, + pub line3: Option>, + pub state: Option>, + pub country: Option, + pub zip: Option>, + pub city: Option, +} + #[derive(Serialize, Deserialize, Debug)] pub struct ChargebeePaymentMethod { pub reference_id: String, @@ -449,6 +473,18 @@ impl TryFrom for revenue_recovery::RevenueRecoveryAttemptD .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; let payment_method_sub_type = enums::PaymentMethodType::from(payment_method_details.card.funding_type); + // Chargebee retry count will always be less than u16 always. Chargebee can have maximum 12 retry attempts + #[allow(clippy::as_conversions)] + let retry_count = item + .content + .invoice + .linked_payments + .map(|linked_payments| linked_payments.len() as u16); + let invoice_next_billing_time = item + .content + .subscription + .as_ref() + .and_then(|subscription| subscription.next_billing_at); Ok(Self { amount, currency, @@ -466,6 +502,8 @@ impl TryFrom for revenue_recovery::RevenueRecoveryAttemptD network_advice_code: None, network_decline_code: None, network_error_message: None, + retry_count, + invoice_next_billing_time, }) } } @@ -521,10 +559,41 @@ impl TryFrom for revenue_recovery::RevenueRecoveryInvoiceD amount: item.content.invoice.total, currency: item.content.invoice.currency_code, merchant_reference_id, + billing_address: Some(api_models::payments::Address::from(item.content.invoice)), }) } } +#[cfg(all(feature = "revenue_recovery", feature = "v2"))] +impl From for api_models::payments::Address { + fn from(item: ChargebeeInvoiceData) -> Self { + Self { + address: item + .billing_address + .map(api_models::payments::AddressDetails::from), + phone: None, + email: None, + } + } +} + +#[cfg(all(feature = "revenue_recovery", feature = "v2"))] +impl From for api_models::payments::AddressDetails { + fn from(item: ChargebeeInvoiceBillingAddress) -> Self { + Self { + city: item.city, + country: item.country, + state: item.state, + zip: item.zip, + line1: item.line1, + line2: item.line2, + line3: item.line3, + first_name: None, + last_name: None, + } + } +} + #[derive(Debug, Serialize)] pub struct ChargebeeRecordPaymentRequest { #[serde(rename = "transaction[amount]")] diff --git a/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs b/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs index ab81529c48..3ff7d04fde 100644 --- a/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/stripebilling/transformers.rs @@ -340,6 +340,7 @@ impl TryFrom for revenue_recovery::RevenueRecoveryInvo amount: item.data.object.amount, currency: item.data.object.currency, merchant_reference_id, + billing_address: None, }) } } diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index 541c94370c..a231a4b42f 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -277,6 +277,7 @@ impl ApiModelToDieselModelConvertor for PaymentReven payment_method_type: from.payment_method_type, payment_method_subtype: from.payment_method_subtype, connector: from.connector, + invoice_next_billing_time: from.invoice_next_billing_time, } } @@ -292,6 +293,7 @@ impl ApiModelToDieselModelConvertor for PaymentReven payment_method_type: self.payment_method_type, payment_method_subtype: self.payment_method_subtype, connector: self.connector, + invoice_next_billing_time: self.invoice_next_billing_time, } } } diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 02a1622712..90ba708c10 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -830,6 +830,8 @@ pub struct RevenueRecoveryData { pub billing_connector_id: id_type::MerchantConnectorAccountId, pub processor_payment_method_token: String, pub connector_customer_id: String, + pub retry_count: Option, + pub invoice_next_billing_time: Option, } #[cfg(feature = "v2")] @@ -847,10 +849,13 @@ where let payment_revenue_recovery_metadata = match payment_attempt_connector { Some(connector) => Some(diesel_models::types::PaymentRevenueRecoveryMetadata { // Update retry count by one. - total_retry_count: revenue_recovery - .as_ref() - .map_or(1, |data| (data.total_retry_count + 1)), - // Since this is an external system call, marking this payment_connector_transmission to ConnectorCallUnsuccessful. + total_retry_count: revenue_recovery.as_ref().map_or( + self.revenue_recovery_data + .retry_count + .map_or_else(|| 1, |retry_count| retry_count), + |data| (data.total_retry_count + 1), + ), + // Since this is an external system call, marking this payment_connector_transmission to ConnectorCallSucceeded. payment_connector_transmission: common_enums::PaymentConnectorTransmission::ConnectorCallUnsuccessful, billing_connector_id: self.revenue_recovery_data.billing_connector_id.clone(), @@ -874,6 +879,7 @@ where router_env::logger::error!(?err, "Failed to parse connector string to enum"); errors::api_error_response::ApiErrorResponse::InternalServerError })?, + invoice_next_billing_time: self.revenue_recovery_data.invoice_next_billing_time, }), None => Err(errors::api_error_response::ApiErrorResponse::InternalServerError) .attach_printable("Connector not found in payment attempt")?, diff --git a/crates/hyperswitch_domain_models/src/revenue_recovery.rs b/crates/hyperswitch_domain_models/src/revenue_recovery.rs index 24514fa025..81443abbaa 100644 --- a/crates/hyperswitch_domain_models/src/revenue_recovery.rs +++ b/crates/hyperswitch_domain_models/src/revenue_recovery.rs @@ -42,6 +42,10 @@ pub struct RevenueRecoveryAttemptData { pub network_decline_code: Option, /// A string indicating how to proceed with an network error if payment gateway provide one. This is used to understand the network error code better. pub network_error_message: Option, + /// Number of attempts made for an invoice + pub retry_count: Option, + /// Time when next invoice will be generated which will be equal to the end time of the current invoice + pub invoice_next_billing_time: Option, } /// This is unified struct for Revenue Recovery Invoice Data and it is constructed from billing connectors @@ -53,6 +57,8 @@ pub struct RevenueRecoveryInvoiceData { pub currency: common_enums::Currency, /// merchant reference id at billing connector. ex: invoice_id pub merchant_reference_id: id_type::PaymentReferenceId, + /// billing address id of the invoice + pub billing_address: Option, } /// type of action that needs to taken after consuming recovery payload @@ -178,7 +184,7 @@ impl From<&RevenueRecoveryInvoiceData> for api_payments::PaymentsCreateIntentReq // so capture method will be always automatic. capture_method: Some(common_enums::CaptureMethod::Automatic), authentication_type: Some(common_enums::AuthenticationType::NoThreeDs), - billing: None, + billing: data.billing_address.clone(), shipping: None, customer_id: None, customer_present: Some(common_enums::PresenceOfCustomerDuringPayment::Absent), @@ -209,6 +215,7 @@ impl From<&BillingConnectorPaymentsSyncResponse> for RevenueRecoveryInvoiceData amount: data.amount, currency: data.currency, merchant_reference_id: data.merchant_reference_id.clone(), + billing_address: None, } } } @@ -232,6 +239,8 @@ impl From<&BillingConnectorPaymentsSyncResponse> for RevenueRecoveryAttemptData network_advice_code: None, network_decline_code: None, network_error_message: None, + retry_count: None, + invoice_next_billing_time: None, } } } diff --git a/crates/hyperswitch_interfaces/src/connector_integration_interface.rs b/crates/hyperswitch_interfaces/src/connector_integration_interface.rs index b49869519e..8122f6365f 100644 --- a/crates/hyperswitch_interfaces/src/connector_integration_interface.rs +++ b/crates/hyperswitch_interfaces/src/connector_integration_interface.rs @@ -388,6 +388,34 @@ impl IncomingWebhook for ConnectorEnum { Self::New(connector) => connector.get_network_txn_id(request), } } + + #[cfg(all(feature = "revenue_recovery", feature = "v2"))] + fn get_revenue_recovery_invoice_details( + &self, + request: &IncomingWebhookRequestDetails<'_>, + ) -> CustomResult< + hyperswitch_domain_models::revenue_recovery::RevenueRecoveryInvoiceData, + errors::ConnectorError, + > { + match self { + Self::Old(connector) => connector.get_revenue_recovery_invoice_details(request), + Self::New(connector) => connector.get_revenue_recovery_invoice_details(request), + } + } + + #[cfg(all(feature = "revenue_recovery", feature = "v2"))] + fn get_revenue_recovery_attempt_details( + &self, + request: &IncomingWebhookRequestDetails<'_>, + ) -> CustomResult< + hyperswitch_domain_models::revenue_recovery::RevenueRecoveryAttemptData, + errors::ConnectorError, + > { + match self { + Self::Old(connector) => connector.get_revenue_recovery_attempt_details(request), + Self::New(connector) => connector.get_revenue_recovery_attempt_details(request), + } + } } impl ConnectorRedirectResponse for ConnectorEnum { diff --git a/crates/hyperswitch_interfaces/src/webhooks.rs b/crates/hyperswitch_interfaces/src/webhooks.rs index 6a243cf09f..a84c5a02fc 100644 --- a/crates/hyperswitch_interfaces/src/webhooks.rs +++ b/crates/hyperswitch_interfaces/src/webhooks.rs @@ -304,4 +304,16 @@ pub trait IncomingWebhook: ConnectorCommon + Sync { ) .into()) } + + /// get billing address for invoice if present in the webhook + #[cfg(all(feature = "revenue_recovery", feature = "v2"))] + fn get_billing_address_for_invoice( + &self, + _request: &IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented( + "get_billing_address_for_invoice method".to_string(), + ) + .into()) + } } diff --git a/crates/router/src/core/payments/operations/payment_attempt_record.rs b/crates/router/src/core/payments/operations/payment_attempt_record.rs index e9597234f1..4eff44a455 100644 --- a/crates/router/src/core/payments/operations/payment_attempt_record.rs +++ b/crates/router/src/core/payments/operations/payment_attempt_record.rs @@ -192,6 +192,8 @@ impl billing_connector_id: request.billing_connector_id.clone(), processor_payment_method_token: request.processor_payment_method_token.clone(), connector_customer_id: request.connector_customer_id.clone(), + retry_count: request.retry_count, + invoice_next_billing_time: request.invoice_next_billing_time, }; let payment_data = PaymentAttemptRecordData { diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 04102bcfb0..6d039310c8 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -4624,6 +4624,8 @@ impl ForeignFrom<&diesel_models::types::FeatureMetadata> for api_models::payment api_models::payments::BillingConnectorPaymentDetails::foreign_from( &payment_revenue_recovery_metadata.billing_connector_payment_details, ), + invoice_next_billing_time: payment_revenue_recovery_metadata + .invoice_next_billing_time, } }); let apple_pay_details = feature_metadata diff --git a/crates/router/src/core/webhooks/recovery_incoming.rs b/crates/router/src/core/webhooks/recovery_incoming.rs index b38c0321e5..44c99c30b7 100644 --- a/crates/router/src/core/webhooks/recovery_incoming.rs +++ b/crates/router/src/core/webhooks/recovery_incoming.rs @@ -527,33 +527,38 @@ impl RevenueRecoveryAttempt { billing_merchant_connector_account_id: &id_type::MerchantConnectorAccountId, payment_merchant_connector_account: Option, ) -> api_payments::PaymentsAttemptRecordRequest { - let amount_details = api_payments::PaymentAttemptAmountDetails::from(&self.0); + let revenue_recovery_attempt_data = &self.0; + let amount_details = + api_payments::PaymentAttemptAmountDetails::from(revenue_recovery_attempt_data); let feature_metadata = api_payments::PaymentAttemptFeatureMetadata { revenue_recovery: Some(api_payments::PaymentAttemptRevenueRecoveryData { // Since we are recording the external paymenmt attempt, this is hardcoded to External attempt_triggered_by: common_enums::TriggeredBy::External, }), }; - let error = Option::::from(&self.0); + let error = + Option::::from(revenue_recovery_attempt_data); api_payments::PaymentsAttemptRecordRequest { amount_details, - status: self.0.status, + status: revenue_recovery_attempt_data.status, billing: None, shipping: None, connector : payment_merchant_connector_account.as_ref().map(|account| account.connector_name), payment_merchant_connector_id: payment_merchant_connector_account.as_ref().map(|account: &hyperswitch_domain_models::merchant_connector_account::MerchantConnectorAccount| account.id.clone()), error, description: None, - connector_transaction_id: self.0.connector_transaction_id.clone(), - payment_method_type: self.0.payment_method_type, + connector_transaction_id: revenue_recovery_attempt_data.connector_transaction_id.clone(), + payment_method_type: revenue_recovery_attempt_data.payment_method_type, billing_connector_id: billing_merchant_connector_account_id.clone(), - payment_method_subtype: self.0.payment_method_sub_type, + payment_method_subtype: revenue_recovery_attempt_data.payment_method_sub_type, payment_method_data: None, metadata: None, feature_metadata: Some(feature_metadata), - transaction_created_at: self.0.transaction_created_at, - processor_payment_method_token: self.0.processor_payment_method_token.clone(), - connector_customer_id: self.0.connector_customer_id.clone(), + transaction_created_at: revenue_recovery_attempt_data.transaction_created_at, + processor_payment_method_token: revenue_recovery_attempt_data.processor_payment_method_token.clone(), + connector_customer_id: revenue_recovery_attempt_data.connector_customer_id.clone(), + retry_count: revenue_recovery_attempt_data.retry_count, + invoice_next_billing_time: revenue_recovery_attempt_data.invoice_next_billing_time } }