mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-11-01 02:57:02 +08:00 
			
		
		
		
	feat(router): add api to migrate card from basilisk to rust (#2853)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
		| @ -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(), | ||||
|  | ||||
| @ -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!( | ||||
| @ -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), | ||||
|         )?; | ||||
|  | ||||
| @ -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, | ||||
| @ -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, | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
							
								
								
									
										131
									
								
								crates/router/src/core/locker_migration.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								crates/router/src/core/locker_migration.rs
									
									
									
									
									
										Normal file
									
								
							| @ -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) | ||||
| } | ||||
| @ -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, | ||||
|         &[], | ||||
| @ -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(), | ||||
| @ -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, | ||||
| @ -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"))] | ||||
| @ -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); | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| use std::str::FromStr; | ||||
|  | ||||
| use api_models::enums as api_enums; | ||||
| use common_utils::{ext_traits::StringExt, pii::Email}; | ||||
| use error_stack::ResultExt; | ||||
| use josekit::jwe; | ||||
| @ -26,6 +27,8 @@ pub enum StoreLockerReq<'a> { | ||||
| pub struct StoreCardReq<'a> { | ||||
|     pub merchant_id: &'a str, | ||||
|     pub merchant_customer_id: String, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub card_reference: Option<String>, | ||||
|     pub card: Card, | ||||
| } | ||||
|  | ||||
| @ -224,6 +227,7 @@ pub async fn mk_basilisk_req( | ||||
|     #[cfg(feature = "kms")] jwekey: &settings::ActiveKmsSecrets, | ||||
|     #[cfg(not(feature = "kms"))] jwekey: &settings::Jwekey, | ||||
|     jws: &str, | ||||
|     locker_choice: api_enums::LockerChoice, | ||||
| ) -> CustomResult<encryption::JweBody, errors::VaultError> { | ||||
|     let jws_payload: Vec<&str> = jws.split('.').collect(); | ||||
|  | ||||
| @ -241,10 +245,18 @@ pub async fn mk_basilisk_req( | ||||
|         .change_context(errors::VaultError::SaveCardFailed)?; | ||||
|  | ||||
|     #[cfg(feature = "kms")] | ||||
|     let public_key = jwekey.jwekey.peek().vault_encryption_key.as_bytes(); | ||||
|     let public_key = match locker_choice { | ||||
|         api_enums::LockerChoice::Basilisk => jwekey.jwekey.peek().vault_encryption_key.as_bytes(), | ||||
|         api_enums::LockerChoice::Tartarus => { | ||||
|             jwekey.jwekey.peek().rust_locker_encryption_key.as_bytes() | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     #[cfg(not(feature = "kms"))] | ||||
|     let public_key = jwekey.vault_encryption_key.as_bytes(); | ||||
|     let public_key = match locker_choice { | ||||
|         api_enums::LockerChoice::Basilisk => jwekey.vault_encryption_key.as_bytes(), | ||||
|         api_enums::LockerChoice::Tartarus => jwekey.rust_locker_encryption_key.as_bytes(), | ||||
|     }; | ||||
|  | ||||
|     let jwe_encrypted = encryption::encrypt_jwe(&payload, public_key) | ||||
|         .await | ||||
| @ -272,6 +284,7 @@ pub async fn mk_add_locker_request_hs<'a>( | ||||
|     #[cfg(feature = "kms")] jwekey: &settings::ActiveKmsSecrets, | ||||
|     locker: &settings::Locker, | ||||
|     payload: &StoreLockerReq<'a>, | ||||
|     locker_choice: api_enums::LockerChoice, | ||||
| ) -> CustomResult<services::Request, errors::VaultError> { | ||||
|     let payload = utils::Encode::<StoreCardReq<'_>>::encode_to_vec(&payload) | ||||
|         .change_context(errors::VaultError::RequestEncodingFailed)?; | ||||
| @ -286,11 +299,14 @@ pub async fn mk_add_locker_request_hs<'a>( | ||||
|         .await | ||||
|         .change_context(errors::VaultError::RequestEncodingFailed)?; | ||||
|  | ||||
|     let jwe_payload = mk_basilisk_req(jwekey, &jws).await?; | ||||
|     let jwe_payload = mk_basilisk_req(jwekey, &jws, locker_choice).await?; | ||||
|  | ||||
|     let body = utils::Encode::<encryption::JweBody>::encode_to_value(&jwe_payload) | ||||
|         .change_context(errors::VaultError::RequestEncodingFailed)?; | ||||
|     let mut url = locker.host.to_owned(); | ||||
|     let mut url = match locker_choice { | ||||
|         api_enums::LockerChoice::Basilisk => locker.host.to_owned(), | ||||
|         api_enums::LockerChoice::Tartarus => locker.host_rs.to_owned(), | ||||
|     }; | ||||
|     url.push_str("/cards/add"); | ||||
|     let mut request = services::Request::new(services::Method::Post, &url); | ||||
|     request.add_header(headers::CONTENT_TYPE, "application/json".into()); | ||||
| @ -432,7 +448,7 @@ pub async fn mk_get_card_request_hs( | ||||
|         .await | ||||
|         .change_context(errors::VaultError::RequestEncodingFailed)?; | ||||
|  | ||||
|     let jwe_payload = mk_basilisk_req(jwekey, &jws).await?; | ||||
|     let jwe_payload = mk_basilisk_req(jwekey, &jws, api_enums::LockerChoice::Basilisk).await?; | ||||
|  | ||||
|     let body = utils::Encode::<encryption::JweBody>::encode_to_value(&jwe_payload) | ||||
|         .change_context(errors::VaultError::RequestEncodingFailed)?; | ||||
| @ -512,7 +528,7 @@ pub async fn mk_delete_card_request_hs( | ||||
|         .await | ||||
|         .change_context(errors::VaultError::RequestEncodingFailed)?; | ||||
|  | ||||
|     let jwe_payload = mk_basilisk_req(jwekey, &jws).await?; | ||||
|     let jwe_payload = mk_basilisk_req(jwekey, &jws, api_enums::LockerChoice::Basilisk).await?; | ||||
|  | ||||
|     let body = utils::Encode::<encryption::JweBody>::encode_to_value(&jwe_payload) | ||||
|         .change_context(errors::VaultError::RequestEncodingFailed)?; | ||||
|  | ||||
| @ -152,6 +152,7 @@ pub async fn save_payout_data_to_locker( | ||||
|                     card_isin: None, | ||||
|                     nick_name: None, | ||||
|                 }, | ||||
|                 card_reference: None, | ||||
|             }); | ||||
|             ( | ||||
|                 payload, | ||||
| @ -195,9 +196,14 @@ pub async fn save_payout_data_to_locker( | ||||
|         } | ||||
|     }; | ||||
|     // Store payout method in locker | ||||
|     let stored_resp = cards::call_to_locker_hs(state, &locker_req, &payout_attempt.customer_id) | ||||
|         .await | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError)?; | ||||
|     let stored_resp = cards::call_to_locker_hs( | ||||
|         state, | ||||
|         &locker_req, | ||||
|         &payout_attempt.customer_id, | ||||
|         api_enums::LockerChoice::Basilisk, | ||||
|     ) | ||||
|     .await | ||||
|     .change_context(errors::ApiErrorResponse::InternalServerError)?; | ||||
|  | ||||
|     // Store card_reference in payouts table | ||||
|     let db = &*state.store; | ||||
|  | ||||
| @ -145,6 +145,7 @@ pub fn mk_app( | ||||
|             .service(routes::Disputes::server(state.clone())) | ||||
|             .service(routes::Analytics::server(state.clone())) | ||||
|             .service(routes::Routing::server(state.clone())) | ||||
|             .service(routes::LockerMigrate::server(state.clone())) | ||||
|             .service(routes::Gsm::server(state.clone())) | ||||
|             .service(routes::User::server(state.clone())) | ||||
|     } | ||||
|  | ||||
| @ -29,6 +29,7 @@ pub mod user; | ||||
| pub mod verification; | ||||
| pub mod webhooks; | ||||
|  | ||||
| pub mod locker_migration; | ||||
| #[cfg(feature = "dummy_connector")] | ||||
| pub use self::app::DummyConnector; | ||||
| #[cfg(feature = "payouts")] | ||||
| @ -39,8 +40,8 @@ pub use self::app::Routing; | ||||
| pub use self::app::Verify; | ||||
| pub use self::app::{ | ||||
|     ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, Customers, Disputes, EphemeralKey, | ||||
|     Files, Gsm, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, | ||||
|     PaymentMethods, Payments, Refunds, User, Webhooks, | ||||
|     Files, Gsm, Health, LockerMigrate, Mandates, MerchantAccount, MerchantConnectorAccount, | ||||
|     PaymentLink, PaymentMethods, Payments, Refunds, User, Webhooks, | ||||
| }; | ||||
| #[cfg(feature = "stripe")] | ||||
| pub use super::compatibility::stripe::StripeApis; | ||||
|  | ||||
| @ -19,7 +19,7 @@ use super::routing as cloud_routing; | ||||
| #[cfg(all(feature = "olap", feature = "kms"))] | ||||
| use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; | ||||
| #[cfg(feature = "olap")] | ||||
| use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*, user::*}; | ||||
| use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*, locker_migration, user::*}; | ||||
| use super::{cache::*, health::*, payment_link::*}; | ||||
| #[cfg(any(feature = "olap", feature = "oltp"))] | ||||
| use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; | ||||
| @ -743,3 +743,16 @@ impl User { | ||||
|             .service(web::resource("/v2/signup").route(web::post().to(user_connect_account))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct LockerMigrate; | ||||
|  | ||||
| #[cfg(feature = "olap")] | ||||
| impl LockerMigrate { | ||||
|     pub fn server(state: AppState) -> Scope { | ||||
|         web::scope("locker_migration/{merchant_id}") | ||||
|             .app_data(web::Data::new(state)) | ||||
|             .service( | ||||
|                 web::resource("").route(web::post().to(locker_migration::rust_locker_migration)), | ||||
|             ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -23,6 +23,7 @@ pub enum ApiIdentifier { | ||||
|     ApiKeys, | ||||
|     PaymentLink, | ||||
|     Routing, | ||||
|     RustLockerMigration, | ||||
|     Gsm, | ||||
|     User, | ||||
| } | ||||
| @ -131,6 +132,7 @@ impl From<Flow> for ApiIdentifier { | ||||
|             Flow::Verification => Self::Verification, | ||||
|  | ||||
|             Flow::PaymentLinkInitiate | Flow::PaymentLinkRetrieve => Self::PaymentLink, | ||||
|             Flow::RustLockerMigration => Self::RustLockerMigration, | ||||
|             Flow::GsmRuleCreate | ||||
|             | Flow::GsmRuleRetrieve | ||||
|             | Flow::GsmRuleUpdate | ||||
|  | ||||
							
								
								
									
										27
									
								
								crates/router/src/routes/locker_migration.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								crates/router/src/routes/locker_migration.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| use actix_web::{web, HttpRequest, HttpResponse}; | ||||
| use router_env::Flow; | ||||
|  | ||||
| use super::AppState; | ||||
| use crate::{ | ||||
|     core::{api_locking, locker_migration}, | ||||
|     services::{api, authentication as auth}, | ||||
| }; | ||||
|  | ||||
| pub async fn rust_locker_migration( | ||||
|     state: web::Data<AppState>, | ||||
|     req: HttpRequest, | ||||
|     path: web::Path<String>, | ||||
| ) -> HttpResponse { | ||||
|     let flow = Flow::RustLockerMigration; | ||||
|     let merchant_id = path.into_inner(); | ||||
|     api::server_wrap( | ||||
|         flow, | ||||
|         state, | ||||
|         &req, | ||||
|         &merchant_id, | ||||
|         |state, _, _| locker_migration::rust_locker_migration(state, &merchant_id), | ||||
|         &auth::AdminApiAuth, | ||||
|         api_locking::LockAction::NotApplicable, | ||||
|     ) | ||||
|     .await | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Shankar Singh C
					Shankar Singh C