Skip to content
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

feat(router): add api to migrate card from basilisk to rust #2898

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions config/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ kms_encrypted_recon_admin_api_key = "" # Base64-encoded (KMS encrypted) ciph
# like card details
[locker]
host = "" # Locker host
host_rs = "" # Rust Locker host
mock_locker = true # Emulate a locker locally using Postgres
basilisk_host = "" # Basilisk host
locker_signing_key_id = "1" # Key_id to sign basilisk hs locker
Expand All @@ -130,6 +131,7 @@ locker_encryption_key2 = "" # public key 2 in pem format, corresponding private
locker_decryption_key1 = "" # private key 1 in pem format, corresponding public key in basilisk
locker_decryption_key2 = "" # private key 2 in pem format, corresponding public key in basilisk
vault_encryption_key = "" # public key in pem format, corresponding private key in basilisk-hs
rust_locker_encryption_key = "" # public key in pem format, corresponding private key in rust locker
vault_private_key = "" # private key in pem format, corresponding public key in basilisk-hs


Expand Down
2 changes: 2 additions & 0 deletions config/development.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ applepay_endpoint = "DOMAIN SPECIFIC ENDPOINT"

[locker]
host = ""
host_rs = ""
mock_locker = true
basilisk_host = ""

Expand All @@ -59,6 +60,7 @@ locker_encryption_key2 = ""
locker_decryption_key1 = ""
locker_decryption_key2 = ""
vault_encryption_key = ""
rust_locker_encryption_key = ""
vault_private_key = ""
tunnel_private_key = ""

Expand Down
2 changes: 2 additions & 0 deletions config/docker_compose.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ recon_admin_api_key = "recon_test_admin"

[locker]
host = ""
host_rs = ""
mock_locker = true
basilisk_host = ""

Expand All @@ -55,6 +56,7 @@ locker_encryption_key2 = ""
locker_decryption_key1 = ""
locker_decryption_key2 = ""
vault_encryption_key = ""
rust_locker_encryption_key = ""
vault_private_key = ""

[redis]
Expand Down
6 changes: 6 additions & 0 deletions crates/api_models/src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,3 +562,9 @@ pub enum RetryAction {
/// Denotes that the payment is requeued
Requeue,
}

#[derive(Clone, Copy)]
pub enum LockerChoice {
Basilisk,
Tartarus,
}
1 change: 1 addition & 0 deletions crates/api_models/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod customer;
pub mod gsm;
mod locker_migration;
pub mod payment;
#[cfg(feature = "payouts")]
pub mod payouts;
Expand Down
9 changes: 9 additions & 0 deletions crates/api_models/src/events/locker_migration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use common_utils::events::ApiEventMetric;

use crate::locker_migration::MigrateCardResponse;

impl ApiEventMetric for MigrateCardResponse {
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
Some(common_utils::events::ApiEventsType::RustLocker)
}
}
1 change: 1 addition & 0 deletions crates/api_models/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod errors;
pub mod events;
pub mod files;
pub mod gsm;
pub mod locker_migration;
pub mod mandates;
pub mod organization;
pub mod payment_methods;
Expand Down
8 changes: 8 additions & 0 deletions crates/api_models/src/locker_migration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MigrateCardResponse {
pub status_message: String,
pub status_code: String,
pub customers_moved: usize,
pub cards_moved: usize,
}
1 change: 1 addition & 0 deletions crates/common_utils/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub enum ApiEventsType {
Gsm,
// TODO: This has to be removed once the corresponding apiEventTypes are created
Miscellaneous,
RustLocker,
}

impl ApiEventMetric for serde_json::Value {}
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/configs/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ impl Default for super::settings::Locker {
fn default() -> Self {
Self {
host: "localhost".into(),
host_rs: "localhost".into(),
mock_locker: true,
basilisk_host: "localhost".into(),
locker_signing_key_id: "1".into(),
Expand Down
2 changes: 2 additions & 0 deletions crates/router/src/configs/kms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ impl KmsDecrypt for settings::Jwekey {
self.locker_decryption_key1,
self.locker_decryption_key2,
self.vault_encryption_key,
self.rust_locker_encryption_key,
self.vault_private_key,
self.tunnel_private_key,
) = tokio::try_join!(
Expand All @@ -26,6 +27,7 @@ impl KmsDecrypt for settings::Jwekey {
kms_client.decrypt(self.locker_decryption_key1),
kms_client.decrypt(self.locker_decryption_key2),
kms_client.decrypt(self.vault_encryption_key),
kms_client.decrypt(self.rust_locker_encryption_key),
kms_client.decrypt(self.vault_private_key),
kms_client.decrypt(self.tunnel_private_key),
)?;
Expand Down
2 changes: 2 additions & 0 deletions crates/router/src/configs/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ pub struct Secrets {
#[serde(default)]
pub struct Locker {
pub host: String,
pub host_rs: String,
pub mock_locker: bool,
pub basilisk_host: String,
pub locker_signing_key_id: String,
Expand Down Expand Up @@ -448,6 +449,7 @@ pub struct Jwekey {
pub locker_decryption_key1: String,
pub locker_decryption_key2: String,
pub vault_encryption_key: String,
pub rust_locker_encryption_key: String,
pub vault_private_key: String,
pub tunnel_private_key: String,
}
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod disputes;
pub mod errors;
pub mod files;
pub mod gsm;
pub mod locker_migration;
pub mod mandate;
pub mod metrics;
pub mod payment_link;
Expand Down
131 changes: 131 additions & 0 deletions crates/router/src/core/locker_migration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use api_models::{enums as api_enums, locker_migration::MigrateCardResponse};
use common_utils::errors::CustomResult;
use diesel_models::PaymentMethod;
use error_stack::{FutureExt, ResultExt};
use futures::TryFutureExt;

use super::{errors::StorageErrorExt, payment_methods::cards};
use crate::{
errors,
routes::AppState,
services::{self, logger},
types::{api, domain},
};

pub async fn rust_locker_migration(
state: AppState,
merchant_id: &str,
) -> CustomResult<services::ApplicationResponse<MigrateCardResponse>, errors::ApiErrorResponse> {
let db = state.store.as_ref();

let key_store = state
.store
.get_merchant_key_store_by_merchant_id(
merchant_id,
&state.store.get_master_key().to_vec().into(),
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;

let merchant_account = db
.find_merchant_account_by_merchant_id(merchant_id, &key_store)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
.change_context(errors::ApiErrorResponse::InternalServerError)?;

let domain_customers = db
.list_customers_by_merchant_id(merchant_id, &key_store)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;

let mut customers_moved = 0;
let mut cards_moved = 0;

for customer in domain_customers {
let result = db
.find_payment_method_by_customer_id_merchant_id_list(&customer.customer_id, merchant_id)
.change_context(errors::ApiErrorResponse::InternalServerError)
.and_then(|pm| {
call_to_locker(
&state,
pm,
&customer.customer_id,
merchant_id,
&merchant_account,
)
})
.await?;

customers_moved += 1;
cards_moved += result;
}

Ok(services::api::ApplicationResponse::Json(
MigrateCardResponse {
status_code: "200".to_string(),
status_message: "Card migration completed".to_string(),
customers_moved,
cards_moved,
},
))
}

pub async fn call_to_locker(
state: &AppState,
payment_methods: Vec<PaymentMethod>,
customer_id: &String,
merchant_id: &str,
merchant_account: &domain::MerchantAccount,
) -> CustomResult<usize, errors::ApiErrorResponse> {
let mut cards_moved = 0;

for pm in payment_methods {
let card =
cards::get_card_from_locker(state, customer_id, merchant_id, &pm.payment_method_id)
.await?;

let card_details = api::CardDetail {
card_number: card.card_number,
card_exp_month: card.card_exp_month,
card_exp_year: card.card_exp_year,
card_holder_name: card.name_on_card,
nick_name: card.nick_name.map(masking::Secret::new),
};

let pm_create = api::PaymentMethodCreate {
payment_method: pm.payment_method,
payment_method_type: pm.payment_method_type,
payment_method_issuer: pm.payment_method_issuer,
payment_method_issuer_code: pm.payment_method_issuer_code,
card: Some(card_details.clone()),
metadata: pm.metadata,
customer_id: Some(pm.customer_id),
card_network: card.card_brand,
};

let (_add_card_rs_resp, _is_duplicate) = cards::add_card_hs(
state,
pm_create,
card_details,
customer_id.to_string(),
merchant_account,
api_enums::LockerChoice::Tartarus,
Some(&pm.payment_method_id),
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable(format!(
"Card migration failed for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ",
pm.payment_method_id
))?;

cards_moved += 1;

logger::info!(
"Card migrated for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ",
pm.payment_method_id
);
}

Ok(cards_moved)
}
31 changes: 23 additions & 8 deletions crates/router/src/core/payment_methods/cards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,20 @@ pub async fn add_card_to_locker(
metrics::STORED_TO_LOCKER.add(&metrics::CONTEXT, 1, &[]);
request::record_operation_time(
async {
add_card_hs(state, req, card, customer_id, merchant_account)
.await
.map_err(|error| {
metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]);
error
})
add_card_hs(
state,
req,
card,
customer_id,
merchant_account,
api_enums::LockerChoice::Basilisk,
None,
)
.await
.map_err(|error| {
metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]);
error
})
},
&metrics::CARD_ADD_TIME,
&[],
Expand Down Expand Up @@ -282,10 +290,13 @@ pub async fn add_card_hs(
card: api::CardDetail,
customer_id: String,
merchant_account: &domain::MerchantAccount,
locker_choice: api_enums::LockerChoice,
card_reference: Option<&str>,
) -> errors::CustomResult<(api::PaymentMethodResponse, bool), errors::VaultError> {
let payload = payment_methods::StoreLockerReq::LockerCard(payment_methods::StoreCardReq {
merchant_id: &merchant_account.merchant_id,
merchant_customer_id: customer_id.to_owned(),
card_reference: card_reference.map(str::to_string),
card: payment_methods::Card {
card_number: card.card_number.to_owned(),
name_on_card: card.card_holder_name.to_owned(),
Expand All @@ -296,7 +307,8 @@ pub async fn add_card_hs(
nick_name: card.nick_name.as_ref().map(masking::Secret::peek).cloned(),
},
});
let store_card_payload = call_to_locker_hs(state, &payload, &customer_id).await?;
let store_card_payload =
call_to_locker_hs(state, &payload, &customer_id, locker_choice).await?;

let payment_method_resp = payment_methods::mk_add_card_response_hs(
card,
Expand Down Expand Up @@ -394,6 +406,7 @@ pub async fn call_to_locker_hs<'a>(
state: &routes::AppState,
payload: &payment_methods::StoreLockerReq<'a>,
customer_id: &str,
locker_choice: api_enums::LockerChoice,
) -> errors::CustomResult<payment_methods::StoreCardRespPayload, errors::VaultError> {
let locker = &state.conf.locker;
#[cfg(not(feature = "kms"))]
Expand All @@ -402,7 +415,9 @@ pub async fn call_to_locker_hs<'a>(
let jwekey = &state.kms_secrets;
let db = &*state.store;
let stored_card_response = if !locker.mock_locker {
let request = payment_methods::mk_add_locker_request_hs(jwekey, locker, payload).await?;
let request =
payment_methods::mk_add_locker_request_hs(jwekey, locker, payload, locker_choice)
.await?;
let response = services::call_connector_api(state, request)
.await
.change_context(errors::VaultError::SaveCardFailed);
Expand Down
Loading
Loading