Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api-reference-v2/openapi_spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
},
Expand Down
11 changes: 11 additions & 0 deletions crates/api_models/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8547,6 +8547,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<PrimitiveDateTime>,
}
#[cfg(feature = "v2")]
impl PaymentRevenueRecoveryMetadata {
Expand Down Expand Up @@ -8648,6 +8650,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<u16>, example = 1)]
pub retry_count: Option<u16>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this information here , we already might have this is in intent right?

Copy link
Contributor Author

@NISHANTH1221 NISHANTH1221 Apr 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to update the payment_intent's feature metadata with correct retry count(coming from the webhook or other means from billing processor instead of defaulting it to 1 for first webhook we receive). Since payment intent feature metadata is getting updated in the record payment attempt we need this in PaymentAttemptRecordRequest.


/// 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<PrimitiveDateTime>,
}

/// Error details for the payment
Expand Down
2 changes: 2 additions & 0 deletions crates/diesel_models/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<time::PrimitiveDateTime>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,14 @@ pub struct ChargebeeWebhookContent {
pub transaction: ChargebeeTransactionData,
pub invoice: ChargebeeInvoiceData,
pub customer: Option<ChargebeeCustomer>,
pub subscription: Option<ChargebeeSubscriptionData>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct ChargebeeSubscriptionData {
#[serde(default, with = "common_utils::custom_serde::timestamp::option")]
pub next_billing_at: Option<PrimitiveDateTime>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ChargebeeEventType {
Expand All @@ -290,6 +296,13 @@ pub struct ChargebeeInvoiceData {
pub id: String,
pub total: MinorUnit,
pub currency_code: enums::Currency,
pub billing_address: Option<ChargebeeInvoiceBillingAddress>,
pub linked_payments: Option<Vec<ChargebeeInvoicePayments>>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct ChargebeeInvoicePayments {
pub txn_status: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
Expand Down Expand Up @@ -353,6 +366,17 @@ pub struct ChargebeeCustomer {
pub payment_method: ChargebeePaymentMethod,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct ChargebeeInvoiceBillingAddress {
pub line1: Option<Secret<String>>,
pub line2: Option<Secret<String>>,
pub line3: Option<Secret<String>>,
pub state: Option<Secret<String>>,
pub country: Option<enums::CountryAlpha2>,
pub zip: Option<Secret<String>>,
pub city: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct ChargebeePaymentMethod {
pub reference_id: String,
Expand Down Expand Up @@ -449,6 +473,18 @@ impl TryFrom<ChargebeeWebhookBody> 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,
Expand All @@ -466,6 +502,8 @@ impl TryFrom<ChargebeeWebhookBody> for revenue_recovery::RevenueRecoveryAttemptD
network_advice_code: None,
network_decline_code: None,
network_error_message: None,
retry_count,
invoice_next_billing_time,
})
}
}
Expand Down Expand Up @@ -521,10 +559,41 @@ impl TryFrom<ChargebeeInvoiceBody> 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<ChargebeeInvoiceData> 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<ChargebeeInvoiceBillingAddress> 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]")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ impl TryFrom<StripebillingInvoiceBody> for revenue_recovery::RevenueRecoveryInvo
amount: item.data.object.amount,
currency: item.data.object.currency,
merchant_reference_id,
billing_address: None,
})
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/hyperswitch_domain_models/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ impl ApiModelToDieselModelConvertor<ApiRevenueRecoveryMetadata> 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,
}
}

Expand All @@ -292,6 +293,7 @@ impl ApiModelToDieselModelConvertor<ApiRevenueRecoveryMetadata> 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,
}
}
}
Expand Down
14 changes: 10 additions & 4 deletions crates/hyperswitch_domain_models/src/payments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,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<u16>,
pub invoice_next_billing_time: Option<PrimitiveDateTime>,
}

#[cfg(feature = "v2")]
Expand All @@ -840,10 +842,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(),
Expand All @@ -867,6 +872,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")?,
Expand Down
11 changes: 10 additions & 1 deletion crates/hyperswitch_domain_models/src/revenue_recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ pub struct RevenueRecoveryAttemptData {
pub network_decline_code: Option<String>,
/// 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<String>,
/// Number of attempts made for an invoice
pub retry_count: Option<u16>,
/// 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<PrimitiveDateTime>,
}

/// This is unified struct for Revenue Recovery Invoice Data and it is constructed from billing connectors
Expand All @@ -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<api_payments::Address>,
}

/// type of action that needs to taken after consuming recovery payload
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
}
}
}
Expand All @@ -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,
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions crates/hyperswitch_interfaces/src/webhooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<api_models::payments::Address, errors::ConnectorError> {
Err(errors::ConnectorError::NotImplemented(
"get_billing_address_for_invoice method".to_string(),
)
.into())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ impl<F: Send + Clone + Sync>
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 {
Expand Down
2 changes: 2 additions & 0 deletions crates/router/src/core/payments/transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4557,6 +4557,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
Expand Down
23 changes: 14 additions & 9 deletions crates/router/src/core/webhooks/recovery_incoming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,33 +527,38 @@ impl RevenueRecoveryAttempt {
billing_merchant_connector_account_id: &id_type::MerchantConnectorAccountId,
payment_merchant_connector_account: Option<domain::MerchantConnectorAccount>,
) -> 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::<api_payments::RecordAttemptErrorDetails>::from(&self.0);
let error =
Option::<api_payments::RecordAttemptErrorDetails>::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
}
}

Expand Down
Loading