Skip to content

Commit 9e9a922

Browse files
feat(process_tracker): Task implementation for psync payments (#7178)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
1 parent b04ccaf commit 9e9a922

File tree

19 files changed

+1115
-417
lines changed

19 files changed

+1115
-417
lines changed

crates/api_models/src/payments.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8705,6 +8705,10 @@ pub struct PaymentsAttemptRecordRequest {
87058705
#[schema(example = "2022-09-10T10:11:12Z")]
87068706
#[serde(default, with = "common_utils::custom_serde::iso8601::option")]
87078707
pub invoice_next_billing_time: Option<PrimitiveDateTime>,
8708+
8709+
/// source where the payment was triggered by
8710+
#[schema(value_type = TriggeredBy, example = "internal" )]
8711+
pub triggered_by: common_enums::TriggeredBy,
87088712
}
87098713

87108714
/// Error details for the payment

crates/diesel_models/src/process_tracker.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,18 @@ pub mod business_status {
262262
pub const EXECUTE_WORKFLOW_COMPLETE_FOR_REVIEW: &str =
263263
"COMPLETED_EXECUTE_TASK_TO_TRIGGER_REVIEW";
264264

265+
/// This status indicates that the requeue was triggered for execute task
266+
pub const EXECUTE_WORKFLOW_REQUEUE: &str = "TRIGGER_REQUEUE_FOR_EXECUTE_WORKFLOW";
267+
265268
/// This status indicates the completion of a psync task
266269
pub const PSYNC_WORKFLOW_COMPLETE: &str = "COMPLETED_PSYNC_TASK";
270+
271+
/// This status indicates that the psync task was completed to trigger the review task
272+
pub const PSYNC_WORKFLOW_COMPLETE_FOR_REVIEW: &str = "COMPLETED_PSYNC_TASK_TO_TRIGGER_REVIEW";
273+
274+
/// This status indicates that the requeue was triggered for psync task
275+
pub const PSYNC_WORKFLOW_REQUEUE: &str = "TRIGGER_REQUEUE_FOR_PSYNC_WORKFLOW";
276+
277+
/// This status indicates the completion of a review task
278+
pub const REVIEW_WORKFLOW_COMPLETE: &str = "COMPLETED_REVIEW_TASK";
267279
}

crates/hyperswitch_domain_models/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,22 @@ impl From<&api_models::payments::PaymentAttemptAmountDetails>
628628
}
629629
}
630630
#[cfg(feature = "v2")]
631+
impl From<&payments::payment_attempt::AttemptAmountDetailsSetter>
632+
for api_models::payments::PaymentAttemptAmountDetails
633+
{
634+
fn from(amount: &payments::payment_attempt::AttemptAmountDetailsSetter) -> Self {
635+
Self {
636+
net_amount: amount.net_amount,
637+
amount_to_capture: amount.amount_to_capture,
638+
surcharge_amount: amount.surcharge_amount,
639+
tax_on_surcharge: amount.tax_on_surcharge,
640+
amount_capturable: amount.amount_capturable,
641+
shipping_cost: amount.shipping_cost,
642+
order_tax_amount: amount.order_tax_amount,
643+
}
644+
}
645+
}
646+
#[cfg(feature = "v2")]
631647
impl From<&api_models::payments::RecordAttemptErrorDetails>
632648
for payments::payment_attempt::ErrorDetails
633649
{

crates/hyperswitch_domain_models/src/merchant_connector_account.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,20 @@ impl MerchantConnectorAccount {
181181
})
182182
})
183183
}
184+
pub fn get_account_reference_id_using_payment_merchant_connector_account_id(
185+
&self,
186+
payment_merchant_connector_account_id: id_type::MerchantConnectorAccountId,
187+
) -> Option<String> {
188+
self.feature_metadata.as_ref().and_then(|metadata| {
189+
metadata.revenue_recovery.as_ref().and_then(|recovery| {
190+
recovery
191+
.mca_reference
192+
.recovery_to_billing
193+
.get(&payment_merchant_connector_account_id)
194+
.cloned()
195+
})
196+
})
197+
}
184198
}
185199

186200
#[cfg(feature = "v2")]

crates/hyperswitch_domain_models/src/payments.rs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ use self::payment_attempt::PaymentAttempt;
4242
#[cfg(feature = "v2")]
4343
use crate::{
4444
address::Address, business_profile, customer, errors, merchant_account,
45-
merchant_connector_account, merchant_context, payment_address, payment_method_data, routing,
46-
ApiModelToDieselModelConvertor,
45+
merchant_connector_account, merchant_context, payment_address, payment_method_data,
46+
revenue_recovery, routing, ApiModelToDieselModelConvertor,
4747
};
4848
#[cfg(feature = "v1")]
4949
use crate::{payment_method_data, RemoteStorageObject};
@@ -683,6 +683,64 @@ impl PaymentIntent {
683683
self.feature_metadata.clone()
684684
}
685685

686+
pub fn create_revenue_recovery_attempt_data(
687+
&self,
688+
revenue_recovery_metadata: api_models::payments::PaymentRevenueRecoveryMetadata,
689+
billing_connector_account: &merchant_connector_account::MerchantConnectorAccount,
690+
) -> CustomResult<
691+
revenue_recovery::RevenueRecoveryAttemptData,
692+
errors::api_error_response::ApiErrorResponse,
693+
> {
694+
let merchant_reference_id = self.merchant_reference_id.clone().ok_or_else(|| {
695+
error_stack::report!(
696+
errors::api_error_response::ApiErrorResponse::GenericNotFoundError {
697+
message: "mandate reference id not found".to_string()
698+
}
699+
)
700+
})?;
701+
702+
let connector_account_reference_id = billing_connector_account
703+
.get_account_reference_id_using_payment_merchant_connector_account_id(
704+
revenue_recovery_metadata.active_attempt_payment_connector_id,
705+
)
706+
.ok_or_else(|| {
707+
error_stack::report!(
708+
errors::api_error_response::ApiErrorResponse::GenericNotFoundError {
709+
message: "connector account reference id not found".to_string()
710+
}
711+
)
712+
})?;
713+
714+
Ok(revenue_recovery::RevenueRecoveryAttemptData {
715+
amount: self.amount_details.order_amount,
716+
currency: self.amount_details.currency,
717+
merchant_reference_id,
718+
connector_transaction_id: None, // No connector id
719+
error_code: None,
720+
error_message: None,
721+
processor_payment_method_token: revenue_recovery_metadata
722+
.billing_connector_payment_details
723+
.payment_processor_token,
724+
connector_customer_id: revenue_recovery_metadata
725+
.billing_connector_payment_details
726+
.connector_customer_id,
727+
connector_account_reference_id,
728+
transaction_created_at: None, // would unwrap_or as now
729+
status: common_enums::AttemptStatus::Started,
730+
payment_method_type: self
731+
.get_payment_method_type()
732+
.unwrap_or(revenue_recovery_metadata.payment_method_type),
733+
payment_method_sub_type: self
734+
.get_payment_method_sub_type()
735+
.unwrap_or(revenue_recovery_metadata.payment_method_subtype),
736+
network_advice_code: None,
737+
network_decline_code: None,
738+
network_error_message: None,
739+
retry_count: None,
740+
invoice_next_billing_time: None,
741+
})
742+
}
743+
686744
pub fn get_optional_customer_id(
687745
&self,
688746
) -> CustomResult<Option<id_type::CustomerId>, common_utils::errors::ValidationError> {
@@ -863,6 +921,7 @@ pub struct RevenueRecoveryData {
863921
pub connector_customer_id: String,
864922
pub retry_count: Option<u16>,
865923
pub invoice_next_billing_time: Option<PrimitiveDateTime>,
924+
pub triggered_by: storage_enums::enums::TriggeredBy,
866925
}
867926

868927
#[cfg(feature = "v2")]

crates/hyperswitch_domain_models/src/payments/payment_attempt.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -713,7 +713,7 @@ impl PaymentAttempt {
713713
let feature_metadata = PaymentAttemptFeatureMetadata {
714714
revenue_recovery: Some({
715715
PaymentAttemptRevenueRecoveryData {
716-
attempt_triggered_by: common_enums::TriggeredBy::External,
716+
attempt_triggered_by: request.triggered_by,
717717
}
718718
}),
719719
};

crates/hyperswitch_domain_models/src/payments/payment_intent.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ pub enum PaymentIntentUpdate {
361361
status: common_enums::IntentStatus,
362362
feature_metadata: Box<Option<diesel_models::types::FeatureMetadata>>,
363363
updated_by: String,
364+
active_attempt_id: Option<id_type::GlobalAttemptId>,
364365
},
365366
/// UpdateIntent
366367
UpdateIntent(Box<PaymentIntentUpdateFields>),
@@ -717,10 +718,11 @@ impl TryFrom<PaymentIntentUpdate> for diesel_models::PaymentIntentUpdateInternal
717718
status,
718719
feature_metadata,
719720
updated_by,
721+
active_attempt_id,
720722
} => Ok(Self {
721723
status: Some(status),
722724
amount_captured: None,
723-
active_attempt_id: None,
725+
active_attempt_id: Some(active_attempt_id),
724726
modified_at: common_utils::date_time::now(),
725727
amount: None,
726728
currency: None,

crates/router/src/core/errors.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,4 +520,6 @@ pub enum RevenueRecoveryError {
520520
RetryCountFetchFailed,
521521
#[error("Failed to get the billing threshold retry count")]
522522
BillingThresholdRetryCountFetchFailed,
523+
#[error("Failed to create the revenue recovery attempt data")]
524+
RevenueRecoveryAttemptDataCreateFailed,
523525
}

crates/router/src/core/payments/operations/payment_attempt_record.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ impl<F: Send + Clone + Sync>
197197
connector_customer_id: request.connector_customer_id.clone(),
198198
retry_count: request.retry_count,
199199
invoice_next_billing_time: request.invoice_next_billing_time,
200+
triggered_by: request.triggered_by,
200201
};
201202

202203
let payment_data = PaymentAttemptRecordData {
@@ -236,12 +237,20 @@ impl<F: Clone + Sync> UpdateTracker<F, PaymentAttemptRecordData<F>, PaymentsAtte
236237
F: 'b + Send,
237238
{
238239
let feature_metadata = payment_data.get_updated_feature_metadata()?;
239-
let payment_intent_update = hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::RecordUpdate
240+
let active_attempt_id = match payment_data.revenue_recovery_data.triggered_by {
241+
common_enums::TriggeredBy::Internal => Some(payment_data.payment_attempt.id.clone()),
242+
common_enums::TriggeredBy::External => None,
243+
};
244+
let payment_intent_update =
245+
246+
hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdate::RecordUpdate
240247
{
241248
status: common_enums::IntentStatus::from(payment_data.payment_attempt.status),
242249
feature_metadata: Box::new(feature_metadata),
243250
updated_by: storage_scheme.to_string(),
244-
};
251+
active_attempt_id
252+
}
253+
;
245254
payment_data.payment_intent = state
246255
.store
247256
.update_payment_intent(

crates/router/src/core/payments/operations/proxy_payments_intent.rs

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ impl ValidateStatusForOperation for PaymentProxyIntent {
4040
match intent_status {
4141
//Failed state is included here so that in PCR, retries can be done for failed payments, otherwise for a failed attempt it was asking for new payment_intent
4242
common_enums::IntentStatus::RequiresPaymentMethod
43-
| common_enums::IntentStatus::Failed => Ok(()),
43+
| common_enums::IntentStatus::Failed
44+
| common_enums::IntentStatus::Processing => Ok(()),
4445
//Failed state is included here so that in PCR, retries can be done for failed payments, otherwise for a failed attempt it was asking for new payment_intent
4546
common_enums::IntentStatus::RequiresPaymentMethod
4647
| common_enums::IntentStatus::Failed => Ok(()),
4748
common_enums::IntentStatus::Succeeded
4849
| common_enums::IntentStatus::Cancelled
49-
| common_enums::IntentStatus::Processing
5050
| common_enums::IntentStatus::RequiresCustomerAction
5151
| common_enums::IntentStatus::RequiresMerchantAction
5252
| common_enums::IntentStatus::RequiresCapture
@@ -188,26 +188,40 @@ impl<F: Send + Clone + Sync> GetTracker<F, PaymentConfirmData<F>, ProxyPaymentsR
188188
.change_context(errors::ApiErrorResponse::InternalServerError)
189189
.attach_printable("Failed while encrypting payment intent details")?;
190190

191-
let payment_attempt_domain_model: hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt =
192-
hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt::proxy_create_domain_model(
193-
&payment_intent,
194-
cell_id,
195-
storage_scheme,
196-
request,
197-
encrypted_data
198-
)
199-
.await?;
191+
let payment_attempt = match payment_intent.active_attempt_id.clone() {
192+
Some(ref active_attempt_id) => db
193+
.find_payment_attempt_by_id(
194+
key_manager_state,
195+
merchant_context.get_merchant_key_store(),
196+
active_attempt_id,
197+
storage_scheme,
198+
)
199+
.await
200+
.change_context(errors::ApiErrorResponse::PaymentNotFound)
201+
.attach_printable("Could not find payment attempt")?,
202+
203+
None => {
204+
let payment_attempt_domain_model: hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt =
205+
hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt::proxy_create_domain_model(
206+
&payment_intent,
207+
cell_id,
208+
storage_scheme,
209+
request,
210+
encrypted_data
211+
)
212+
.await?;
213+
db.insert_payment_attempt(
214+
key_manager_state,
215+
merchant_context.get_merchant_key_store(),
216+
payment_attempt_domain_model,
217+
storage_scheme,
218+
)
219+
.await
220+
.change_context(errors::ApiErrorResponse::InternalServerError)
221+
.attach_printable("Could not insert payment attempt")?
222+
}
223+
};
200224

201-
let payment_attempt = db
202-
.insert_payment_attempt(
203-
key_manager_state,
204-
merchant_context.get_merchant_key_store(),
205-
payment_attempt_domain_model,
206-
storage_scheme,
207-
)
208-
.await
209-
.change_context(errors::ApiErrorResponse::InternalServerError)
210-
.attach_printable("Could not insert payment attempt")?;
211225
let processor_payment_token = request.recurring_details.processor_payment_token.clone();
212226

213227
let payment_address = hyperswitch_domain_models::payment_address::PaymentAddress::new(

0 commit comments

Comments
 (0)