Skip to content

feat(recovery): add support for custom billing api for v2 #8838

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Aug 8, 2025

Conversation

srujanchikke
Copy link
Contributor

@srujanchikke srujanchikke commented Aug 5, 2025

Type of Change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring
  • Dependency updates
  • Documentation
  • CI/CD

Description

Revenue recovery incoming webhook flow handles events from external 3rd party subscription provider, If merchant has inhouse subscription solution and wants to use revenue recovery, /payments/recovery will consume the events same as incoming webhooks.

Flow :

  • Merchant can integrate with /payments/recovery endpoint and sends failed payments to recovery system.
  • recovery core will schedule the payments in process tracker.

Additional Changes

  • This PR modifies the API contract
  • This PR modifies the database schema
  • This PR modifies application configuration/environment variables

Motivation and Context

How did you test it?

Create payment mca :

curl --location 'http://localhost:8080/v2/connector-accounts' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'x-merchant-id: cloth_seller_j08ByNifXf1Q9nMHaBN6' \
--header 'x-profile-id: pro_QfvcZDEn9OJaUyLTL6Z4' \
--header 'Authorization: admin-api-key=test_admin' \
--data '{
    "connector_type": "payment_processor",
    "connector_name": "worldpayvantiv",
    "connector_account_details": {
        "auth_type": "SignatureKey",
        "api_key": "apikey",
        "key1": "key1",
        "api_secret": "secret"
    },
    "payment_methods_enabled": [
        {
            "payment_method_type": "card",
            "payment_method_subtypes": [
                {
                    "payment_method_subtype": "credit",
                    "payment_experience": null,
                    "card_networks": [
                        "Visa",
                        "Mastercard"
                    ],
                    "accepted_currencies": null,
                    "accepted_countries": null,
                    "minimum_amount": -1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                },
                {
                    "payment_method_subtype": "debit",
                    "payment_experience": null,
                    "card_networks": [
                        "Visa",
                        "Mastercard"
                    ],
                    "accepted_currencies": null,
                    "accepted_countries": null,
                    "minimum_amount": -1,
                    "maximum_amount": 68607706,
                    "recurring_enabled": true,
                    "installment_payment_enabled": true
                }
            ]
        }
    ],
    "frm_configs": null,
    "connector_webhook_details": {
        "merchant_secret": ""
    },
    "metadata": {
        "report_group": "Hello",
        "merchant_config_currency": "USD"
    },
    "profile_id": "pro_QfvcZDEn9OJaUyLTL6Z4"
}'

create custom billing mca :

curl --location 'http://localhost:8080/v2/connector-accounts' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'x-merchant-id: cloth_seller_NjzUfz0ALVkqI8BOBP0P' \
--header 'x-profile-id: pro_HfHD4qr1QNDUSHwMyytb' \
--header 'Authorization: admin-api-key=test_admin' \
--header 'api-key: ••••••' \
--data '{
    "connector_type": "billing_processor",
    "connector_name": "custombilling",
    "connector_account_details": {
        "auth_type": "NoKey"
    },
    "feature_metadata" : {
        "revenue_recovery" : {
            "max_retry_count" : 9,
            "billing_connector_retry_threshold" : 2,
            "billing_account_reference" :{
                "mca_oc9cwYX0KTkSy76ZscY2" : "mca_oc9cwYX0KTkSy76ZscY2"
            },
            "switch_payment_method_config" : {
                "retry_threshold" : 5,
                "time_threshold_after_creation": 1
            }
        }
    },
    "profile_id": "pro_HfHD4qr1QNDUSHwMyytb"
}'

Update the profile revenue_recovery_retry_algorithm_type to cascading.

Create revenue recovery

curl --location 'http://localhost:8080/v2/payments/recovery' \
--header 'Authorization: api-key=dev_VWOtZtmJoPKjnhD9evLcDDbL6CRTI5oSrVZ6EqMTbDflaWFKJY1gapwrEYouiva4' \
--header 'x-profile-id: pro_a8uCk5nzKhOXxnm68UCv' \
--header 'x-merchant-id: cloth_seller_vMdpVbUDQDxrDB04KB6L' \
--header 'Content-Type: application/json' \
--data-raw '{
    "amount_details": {
        "order_amount": 100,
        "currency": "USD"
    },
    "merchant_reference_id": "merchant_reference_id_20jgfdh19asasd",
    "billing_merchant_connector_id" : "mca_5rHage7LWP2yB2BdNZRO",
    "payment_merchant_connector_id": "mca_86nTDgqPCM8EPXPNkzvh",
    "error": {
        "code": "card_declined",
        "message": "Your card was declined.",
        "network_advice_code": null,
        "network_decline_code": "01",
        "network_error_message": "generic_decline"
    },
    "billing": {
        "address": {
            "first_name": "John",
            "last_name": "Dough"
        },
        "email": "[email protected]"
    },
    "attempt_status": "failure",
    "payment_method_type": "card",
    "payment_method_sub_type": "credit",
    "transaction_created_at": "2025-06-20T10:11:12Z",
    "connector_customer_id": "cus_0001",
    "connector_transaction_id": "connector_transaction_id",
    "primary_processor_payment_method_token": "2541911049890008",
    "action": "schedule_failed_payment",
    "billing_started_at": "2025-07-31T10:11:12Z",
    "payment_method_units": {
        "2541911049890008": {
            "card": {
                "expiry_month": "06",
                "expiry_year": "50",
                "last_four_digits": "0008",
                "card_issuer": "Chase",
                "card_network" :"visa"
            }
        }
    }
}'

response status indicates intent is failed (Success response)

{
    "id": "12345_pay_019883e1dc4971228aa378ab75b738f7",
    "intent_status": "failed",
}

Checklist

  • I formatted the code cargo +nightly fmt --all
  • I addressed lints thrown by cargo clippy
  • I reviewed the submitted code
  • I added unit tests for my changes where possible

@srujanchikke srujanchikke self-assigned this Aug 5, 2025
@srujanchikke srujanchikke changed the title feat(recovery): add support for custom billing api feat(recovery): add support for custom billing api for v2 Aug 5, 2025
@srujanchikke srujanchikke added C-feature Category: Feature request or enhancement api-v2 labels Aug 5, 2025
@srujanchikke srujanchikke marked this pull request as ready for review August 7, 2025 05:09
@srujanchikke srujanchikke requested review from a team as code owners August 7, 2025 05:09
@srujanchikke srujanchikke requested a review from a team as a code owner August 7, 2025 08:02
pub status: enums::AttemptStatus,

/// The billing details of the payment attempt. This address will be used for invoicing.
pub billing: Option<Address>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you improve the naming

pub payment_method_units: CustomBillingPaymentMethodDataWithBilling,

/// recovery action
pub action: common_payments_types::RecoveryAction,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you give a descriptive comment

@@ -4318,6 +4335,12 @@ pub struct PaymentMethodDataResponseWithBilling {
pub billing: Option<Address>,
}

#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, ToSchema, serde::Serialize)]
pub struct CustomBillingPaymentMethodDataWithBilling {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
pub struct CustomBillingPaymentMethodDataWithBilling {
pub struct CustomRecoveryPaymentMethodData {

)]
pub id: id_type::GlobalPaymentId,

#[schema(value_type = IntentStatus, example = "failed", default = "requires_confirmation")]
Copy link
Contributor

Choose a reason for hiding this comment

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

add merchant_reference_id in the response to correlate between their input and our response


/// primary payment method token at payment processor end.
#[schema(value_type = String, example = "token_1234")]
pub primary_processor_payment_method_token: String,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
pub primary_processor_payment_method_token: String,
pub primary_processor_payment_method_token: Secret<String>,


/// customer id at payment connector for which mandate is attached.
#[schema(value_type = String, example = "cust_12345")]
pub connector_customer_id: String,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
pub connector_customer_id: String,
pub connector_customer_id: Secret<String>,

pub billing_started_at: Option<PrimitiveDateTime>,

/// Transaction if reference at payment connector end.
pub connector_transaction_id: Option<String>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
pub connector_transaction_id: Option<String>,
pub connector_transaction_id: Option<Secret<String>>,

Comment on lines +55 to +56
retry_count: None,
next_billing_at: None,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
retry_count: None,
next_billing_at: None,
retry_count: None,
next_billing_at: None,

can these be provided from api input? or it will not be allowed always in request?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It will be not allowed in request, it would always be calculated in recovery system with attempt count.

> {
let primary_token = &data.primary_processor_payment_method_token.to_string();
let card_info = data.payment_method_units.units.get(primary_token);
let recovery_attempt = Self(revenue_recovery::RevenueRecoveryAttemptData {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let recovery_attempt = Self(revenue_recovery::RevenueRecoveryAttemptData {
let recovery_attempt = Self(revenue_recovery::RevenueRecoveryAttemptData::from(data)

@@ -906,7 +967,8 @@ impl RevenueRecoveryAttempt {
.change_context(errors::RevenueRecoveryError::ProcessTrackerCreationError)
.attach_printable("Failed to construct process tracker entry")?;

db.insert_process(process_tracker_entry)
let tracker = db
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
let tracker = db
db

remove if not needed

),
) -> CustomResult<webhooks::WebhookResponseTracker, errors::RevenueRecoveryError> {
match self.action {
common_types::payments::RecoveryAction::CancelInvoice => todo!(),
Copy link
Contributor

Choose a reason for hiding this comment

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

is this intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, we need to implment stop flow, which will alter process tracker flow.

}
}
common_types::payments::RecoveryAction::SuccessPaymentExternal => {
router_env::logger::info!("Payment has been succeeded via external system");
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
router_env::logger::info!("Payment has been succeeded via external system");
logger::info!("Payment has been succeeded via external system");

change here and in below places also

jagan-jaya
jagan-jaya previously approved these changes Aug 7, 2025
AkshayaFoiger
AkshayaFoiger previously approved these changes Aug 7, 2025
tsdk02
tsdk02 previously approved these changes Aug 7, 2025
Aprabhat19
Aprabhat19 previously approved these changes Aug 7, 2025
#[serde(default, with = "common_utils::custom_serde::iso8601::option")]
pub billing_started_at: Option<PrimitiveDateTime>,

/// Transaction if reference at payment connector end.
Copy link
Contributor

Choose a reason for hiding this comment

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

This sentence is not clear

pub payment_merchant_connector_id: id_type::MerchantConnectorAccountId,

#[schema(value_type = AttemptStatus, example = "charged")]
pub status: enums::AttemptStatus,
Copy link
Contributor

Choose a reason for hiding this comment

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

status in response type is IntentStatus. Here it is AttemptStatus.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, We depend on transaction status for external payment, But want to share intent status in response.

@@ -267,6 +267,14 @@ pub async fn construct_payment_router_data_for_authorize<'a>(
.and_then(|customer| customer.email.clone())
.map(pii::Email::from);

let additional_payment_method_data: Option<api_models::payments::AdditionalPaymentData> =
Copy link
Contributor

Choose a reason for hiding this comment

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

Where is this additional_payment_method_data being used?

@likhinbopanna likhinbopanna enabled auto-merge August 8, 2025 09:14
@likhinbopanna likhinbopanna added this pull request to the merge queue Aug 8, 2025
Merged via the queue into main with commit 9e8df84 Aug 8, 2025
21 of 26 checks passed
@likhinbopanna likhinbopanna deleted the custombillingapi branch August 8, 2025 09:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-v2 C-feature Category: Feature request or enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants