Skip to content

Commit e17ffd1

Browse files
srujanchikkeChikke Srujanhyperswitch-bot[bot]
authored
feat(core): Add support for v2 payments get intent using merchant reference id (#7123)
Co-authored-by: Chikke Srujan <[email protected]> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
1 parent 97e9270 commit e17ffd1

File tree

10 files changed

+245
-1
lines changed

10 files changed

+245
-1
lines changed

crates/diesel_models/src/query/payment_intent.rs

+17
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,23 @@ impl PaymentIntent {
8787
.await
8888
}
8989

90+
// This query should be removed in the future because direct queries to the intent table without an intent ID are not allowed.
91+
// In an active-active setup, a lookup table should be implemented, and the merchant reference ID will serve as the idempotency key.
92+
#[cfg(feature = "v2")]
93+
pub async fn find_by_merchant_reference_id_profile_id(
94+
conn: &PgPooledConn,
95+
merchant_reference_id: &common_utils::id_type::PaymentReferenceId,
96+
profile_id: &common_utils::id_type::ProfileId,
97+
) -> StorageResult<Self> {
98+
generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
99+
conn,
100+
dsl::profile_id
101+
.eq(profile_id.to_owned())
102+
.and(dsl::merchant_reference_id.eq(merchant_reference_id.to_owned())),
103+
)
104+
.await
105+
}
106+
90107
#[cfg(feature = "v1")]
91108
pub async fn find_by_payment_id_merchant_id(
92109
conn: &PgPooledConn,

crates/hyperswitch_domain_models/src/payments/payment_intent.rs

+9
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ pub trait PaymentIntentInterface {
6363
merchant_key_store: &MerchantKeyStore,
6464
storage_scheme: common_enums::MerchantStorageScheme,
6565
) -> error_stack::Result<PaymentIntent, errors::StorageError>;
66+
#[cfg(feature = "v2")]
67+
async fn find_payment_intent_by_merchant_reference_id_profile_id(
68+
&self,
69+
state: &KeyManagerState,
70+
merchant_reference_id: &id_type::PaymentReferenceId,
71+
profile_id: &id_type::ProfileId,
72+
merchant_key_store: &MerchantKeyStore,
73+
storage_scheme: &common_enums::MerchantStorageScheme,
74+
) -> error_stack::Result<PaymentIntent, errors::StorageError>;
6675

6776
#[cfg(feature = "v2")]
6877
async fn find_payment_intent_by_id(

crates/router/src/core/payments.rs

+64
Original file line numberDiff line numberDiff line change
@@ -1525,6 +1525,70 @@ where
15251525
)
15261526
}
15271527

1528+
#[cfg(feature = "v2")]
1529+
#[allow(clippy::too_many_arguments)]
1530+
pub async fn payments_get_intent_using_merchant_reference(
1531+
state: SessionState,
1532+
merchant_account: domain::MerchantAccount,
1533+
profile: domain::Profile,
1534+
key_store: domain::MerchantKeyStore,
1535+
req_state: ReqState,
1536+
merchant_reference_id: &id_type::PaymentReferenceId,
1537+
header_payload: HeaderPayload,
1538+
platform_merchant_account: Option<domain::MerchantAccount>,
1539+
) -> RouterResponse<api::PaymentsIntentResponse> {
1540+
let db = state.store.as_ref();
1541+
let storage_scheme = merchant_account.storage_scheme;
1542+
let key_manager_state = &(&state).into();
1543+
let payment_intent = db
1544+
.find_payment_intent_by_merchant_reference_id_profile_id(
1545+
key_manager_state,
1546+
merchant_reference_id,
1547+
profile.get_id(),
1548+
&key_store,
1549+
&storage_scheme,
1550+
)
1551+
.await
1552+
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
1553+
1554+
let (payment_data, _req, customer) = Box::pin(payments_intent_operation_core::<
1555+
api::PaymentGetIntent,
1556+
_,
1557+
_,
1558+
PaymentIntentData<api::PaymentGetIntent>,
1559+
>(
1560+
&state,
1561+
req_state,
1562+
merchant_account.clone(),
1563+
profile.clone(),
1564+
key_store.clone(),
1565+
operations::PaymentGetIntent,
1566+
api_models::payments::PaymentsGetIntentRequest {
1567+
id: payment_intent.get_id().clone(),
1568+
},
1569+
payment_intent.get_id().clone(),
1570+
header_payload.clone(),
1571+
platform_merchant_account,
1572+
))
1573+
.await?;
1574+
1575+
transformers::ToResponse::<
1576+
api::PaymentGetIntent,
1577+
PaymentIntentData<api::PaymentGetIntent>,
1578+
operations::PaymentGetIntent,
1579+
>::generate_response(
1580+
payment_data,
1581+
customer,
1582+
&state.base_url,
1583+
operations::PaymentGetIntent,
1584+
&state.conf.connector_request_reference_id_config,
1585+
None,
1586+
None,
1587+
header_payload.x_hs_latency,
1588+
&merchant_account,
1589+
)
1590+
}
1591+
15281592
#[cfg(feature = "v2")]
15291593
#[allow(clippy::too_many_arguments)]
15301594
pub async fn payments_core<F, Res, Req, Op, FData, D>(

crates/router/src/db/kafka_store.rs

+23
Original file line numberDiff line numberDiff line change
@@ -1904,6 +1904,29 @@ impl PaymentIntentInterface for KafkaStore {
19041904
)
19051905
.await
19061906
}
1907+
1908+
#[cfg(feature = "v2")]
1909+
async fn find_payment_intent_by_merchant_reference_id_profile_id(
1910+
&self,
1911+
state: &KeyManagerState,
1912+
merchant_reference_id: &id_type::PaymentReferenceId,
1913+
profile_id: &id_type::ProfileId,
1914+
merchant_key_store: &domain::MerchantKeyStore,
1915+
storage_scheme: &MerchantStorageScheme,
1916+
) -> error_stack::Result<
1917+
hyperswitch_domain_models::payments::PaymentIntent,
1918+
errors::DataStorageError,
1919+
> {
1920+
self.diesel_store
1921+
.find_payment_intent_by_merchant_reference_id_profile_id(
1922+
state,
1923+
merchant_reference_id,
1924+
profile_id,
1925+
merchant_key_store,
1926+
storage_scheme,
1927+
)
1928+
.await
1929+
}
19071930
}
19081931

19091932
#[async_trait::async_trait]

crates/router/src/routes/app.rs

+6
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,12 @@ impl Payments {
561561
.route(web::post().to(payments::payments_create_intent)),
562562
);
563563

564+
route =
565+
route
566+
.service(web::resource("/ref/{merchant_reference_id}").route(
567+
web::get().to(payments::payment_get_intent_using_merchant_reference_id),
568+
));
569+
564570
route = route.service(
565571
web::scope("/{payment_id}")
566572
.service(

crates/router/src/routes/lock_utils.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ impl From<Flow> for ApiIdentifier {
147147
| Flow::PaymentsPostSessionTokens
148148
| Flow::PaymentsUpdateIntent
149149
| Flow::PaymentsCreateAndConfirmIntent
150-
| Flow::PaymentStartRedirection => Self::Payments,
150+
| Flow::PaymentStartRedirection
151+
| Flow::PaymentsRetrieveUsingMerchantReferenceId => Self::Payments,
151152

152153
Flow::PayoutsCreate
153154
| Flow::PayoutsRetrieve

crates/router/src/routes/payments.rs

+41
Original file line numberDiff line numberDiff line change
@@ -2470,6 +2470,47 @@ pub async fn payment_status(
24702470
.await
24712471
}
24722472

2473+
#[cfg(feature = "v2")]
2474+
#[instrument(skip(state, req), fields(flow, payment_id))]
2475+
pub async fn payment_get_intent_using_merchant_reference_id(
2476+
state: web::Data<app::AppState>,
2477+
req: actix_web::HttpRequest,
2478+
path: web::Path<common_utils::id_type::PaymentReferenceId>,
2479+
) -> impl Responder {
2480+
let flow = Flow::PaymentsRetrieveUsingMerchantReferenceId;
2481+
let header_payload = match HeaderPayload::foreign_try_from(req.headers()) {
2482+
Ok(headers) => headers,
2483+
Err(err) => {
2484+
return api::log_and_return_error_response(err);
2485+
}
2486+
};
2487+
2488+
let merchant_reference_id = path.into_inner();
2489+
2490+
Box::pin(api::server_wrap(
2491+
flow,
2492+
state,
2493+
&req,
2494+
(),
2495+
|state, auth: auth::AuthenticationData, _, req_state| async {
2496+
Box::pin(payments::payments_get_intent_using_merchant_reference(
2497+
state,
2498+
auth.merchant_account,
2499+
auth.profile,
2500+
auth.key_store,
2501+
req_state,
2502+
&merchant_reference_id,
2503+
header_payload.clone(),
2504+
auth.platform_merchant_account,
2505+
))
2506+
.await
2507+
},
2508+
&auth::HeaderAuth(auth::ApiKeyAuth),
2509+
api_locking::LockAction::NotApplicable,
2510+
))
2511+
.await
2512+
}
2513+
24732514
#[cfg(feature = "v2")]
24742515
#[instrument(skip_all, fields(flow = ?Flow::PaymentsRedirect, payment_id))]
24752516
pub async fn payments_finish_redirection(

crates/router_env/src/logger/types.rs

+2
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ pub enum Flow {
144144
PaymentsRetrieve,
145145
/// Payments Retrieve force sync flow.
146146
PaymentsRetrieveForceSync,
147+
/// Payments Retrieve using merchant reference id
148+
PaymentsRetrieveUsingMerchantReferenceId,
147149
/// Payments update flow.
148150
PaymentsUpdate,
149151
/// Payments confirm flow.

crates/storage_impl/src/mock_db/payment_intent.rs

+22
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,26 @@ impl PaymentIntentInterface for MockDb {
185185

186186
Ok(payment_intent.clone())
187187
}
188+
#[cfg(feature = "v2")]
189+
async fn find_payment_intent_by_merchant_reference_id_profile_id(
190+
&self,
191+
_state: &KeyManagerState,
192+
merchant_reference_id: &common_utils::id_type::PaymentReferenceId,
193+
profile_id: &common_utils::id_type::ProfileId,
194+
_merchant_key_store: &MerchantKeyStore,
195+
_storage_scheme: &common_enums::MerchantStorageScheme,
196+
) -> error_stack::Result<PaymentIntent, StorageError> {
197+
let payment_intents = self.payment_intents.lock().await;
198+
let payment_intent = payment_intents
199+
.iter()
200+
.find(|payment_intent| {
201+
payment_intent.merchant_reference_id.as_ref() == Some(merchant_reference_id)
202+
&& payment_intent.profile_id.eq(profile_id)
203+
})
204+
.ok_or(StorageError::ValueNotFound(
205+
"PaymentIntent not found".to_string(),
206+
))?;
207+
208+
Ok(payment_intent.clone())
209+
}
188210
}

crates/storage_impl/src/payments/payment_intent.rs

+59
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,32 @@ impl<T: DatabaseStore> PaymentIntentInterface for KVRouterStore<T> {
459459
)
460460
.await
461461
}
462+
#[cfg(feature = "v2")]
463+
async fn find_payment_intent_by_merchant_reference_id_profile_id(
464+
&self,
465+
state: &KeyManagerState,
466+
merchant_reference_id: &common_utils::id_type::PaymentReferenceId,
467+
profile_id: &common_utils::id_type::ProfileId,
468+
merchant_key_store: &MerchantKeyStore,
469+
storage_scheme: &MerchantStorageScheme,
470+
) -> error_stack::Result<PaymentIntent, StorageError> {
471+
match storage_scheme {
472+
MerchantStorageScheme::PostgresOnly => {
473+
self.router_store
474+
.find_payment_intent_by_merchant_reference_id_profile_id(
475+
state,
476+
merchant_reference_id,
477+
profile_id,
478+
merchant_key_store,
479+
storage_scheme,
480+
)
481+
.await
482+
}
483+
MerchantStorageScheme::RedisKv => {
484+
todo!()
485+
}
486+
}
487+
}
462488
}
463489

464490
#[async_trait::async_trait]
@@ -622,6 +648,39 @@ impl<T: DatabaseStore> PaymentIntentInterface for crate::RouterStore<T> {
622648
.change_context(StorageError::DecryptionError)
623649
}
624650

651+
#[cfg(feature = "v2")]
652+
#[instrument(skip_all)]
653+
async fn find_payment_intent_by_merchant_reference_id_profile_id(
654+
&self,
655+
state: &KeyManagerState,
656+
merchant_reference_id: &common_utils::id_type::PaymentReferenceId,
657+
profile_id: &common_utils::id_type::ProfileId,
658+
merchant_key_store: &MerchantKeyStore,
659+
_storage_scheme: &MerchantStorageScheme,
660+
) -> error_stack::Result<PaymentIntent, StorageError> {
661+
let conn = pg_connection_read(self).await?;
662+
let diesel_payment_intent = DieselPaymentIntent::find_by_merchant_reference_id_profile_id(
663+
&conn,
664+
merchant_reference_id,
665+
profile_id,
666+
)
667+
.await
668+
.map_err(|er| {
669+
let new_err = diesel_error_to_data_error(*er.current_context());
670+
er.change_context(new_err)
671+
})?;
672+
let merchant_id = diesel_payment_intent.merchant_id.clone();
673+
674+
PaymentIntent::convert_back(
675+
state,
676+
diesel_payment_intent,
677+
merchant_key_store.key.get_inner(),
678+
merchant_id.to_owned().into(),
679+
)
680+
.await
681+
.change_context(StorageError::DecryptionError)
682+
}
683+
625684
#[cfg(all(feature = "v1", feature = "olap"))]
626685
#[instrument(skip_all)]
627686
async fn filter_payment_intent_by_constraints(

0 commit comments

Comments
 (0)