mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feat: cards info api (#749)
Co-authored-by: Jagan <jaganelavarasan@gmail.com> Co-authored-by: Kartikeya Hegde <karthikey.hegde@juspay.in> Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
@ -1 +0,0 @@
|
||||
|
||||
31
crates/api_models/src/cards_info.rs
Normal file
31
crates/api_models/src/cards_info.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(serde::Deserialize, ToSchema)]
|
||||
pub struct CardsInfoRequestParams {
|
||||
#[schema(example = "pay_OSERgeV9qAy7tlK7aKpc_secret_TuDUoh11Msxh12sXn3Yp")]
|
||||
pub client_secret: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug)]
|
||||
pub struct CardsInfoRequest {
|
||||
pub client_secret: Option<String>,
|
||||
pub card_iin: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize, Debug, ToSchema)]
|
||||
pub struct CardInfoResponse {
|
||||
#[schema(example = "374431")]
|
||||
pub card_iin: String,
|
||||
#[schema(example = "AMEX")]
|
||||
pub card_issuer: Option<String>,
|
||||
#[schema(example = "AMEX")]
|
||||
pub card_network: Option<String>,
|
||||
#[schema(example = "CREDIT")]
|
||||
pub card_type: Option<String>,
|
||||
#[schema(example = "CLASSIC")]
|
||||
pub card_sub_type: Option<String>,
|
||||
#[schema(example = "INDIA")]
|
||||
pub card_issuing_country: Option<String>,
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
pub mod admin;
|
||||
pub mod api_keys;
|
||||
pub mod bank_accounts;
|
||||
pub mod cards;
|
||||
pub mod cards_info;
|
||||
pub mod customers;
|
||||
pub mod disputes;
|
||||
pub mod enums;
|
||||
|
||||
@ -347,7 +347,9 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
|
||||
| errors::ApiErrorResponse::GenericUnauthorized { .. }
|
||||
| errors::ApiErrorResponse::InvalidEphemeralKey => Self::Unauthorized,
|
||||
errors::ApiErrorResponse::InvalidRequestUrl
|
||||
| errors::ApiErrorResponse::InvalidHttpMethod => Self::InvalidRequestUrl,
|
||||
| errors::ApiErrorResponse::InvalidHttpMethod
|
||||
| errors::ApiErrorResponse::InvalidCardIin
|
||||
| errors::ApiErrorResponse::InvalidCardIinLength => Self::InvalidRequestUrl,
|
||||
errors::ApiErrorResponse::MissingRequiredField { field_name } => {
|
||||
Self::ParameterMissing {
|
||||
field_name,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
pub mod admin;
|
||||
pub mod api_keys;
|
||||
pub mod cards_info;
|
||||
pub mod configs;
|
||||
pub mod customers;
|
||||
pub mod errors;
|
||||
|
||||
49
crates/router/src/core/cards_info.rs
Normal file
49
crates/router/src/core/cards_info.rs
Normal file
@ -0,0 +1,49 @@
|
||||
use common_utils::fp_utils::when;
|
||||
use error_stack::{report, ResultExt};
|
||||
use router_env::{instrument, tracing};
|
||||
|
||||
use crate::{
|
||||
core::{
|
||||
errors::{self, RouterResponse},
|
||||
payments::helpers,
|
||||
},
|
||||
routes,
|
||||
services::ApplicationResponse,
|
||||
types::{storage, transformers::ForeignFrom},
|
||||
};
|
||||
|
||||
fn verify_iin_length(card_iin: &str) -> Result<(), errors::ApiErrorResponse> {
|
||||
let is_bin_length_in_range = card_iin.len() == 6 || card_iin.len() == 8;
|
||||
when(!is_bin_length_in_range, || {
|
||||
Err(errors::ApiErrorResponse::InvalidCardIinLength)
|
||||
})
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn retrieve_card_info(
|
||||
state: &routes::AppState,
|
||||
merchant_account: storage::MerchantAccount,
|
||||
request: api_models::cards_info::CardsInfoRequest,
|
||||
) -> RouterResponse<api_models::cards_info::CardInfoResponse> {
|
||||
let db = &*state.store;
|
||||
|
||||
verify_iin_length(&request.card_iin)?;
|
||||
helpers::verify_client_secret(
|
||||
db,
|
||||
merchant_account.storage_scheme,
|
||||
request.client_secret,
|
||||
&merchant_account.merchant_id,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let card_info = db
|
||||
.get_card_info(&request.card_iin)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to retrieve card information")?
|
||||
.ok_or(report!(errors::ApiErrorResponse::InvalidCardIin))?;
|
||||
|
||||
Ok(ApplicationResponse::Json(
|
||||
api_models::cards_info::CardInfoResponse::foreign_from(card_info),
|
||||
))
|
||||
}
|
||||
@ -158,6 +158,10 @@ pub enum ApiErrorResponse {
|
||||
IncorrectConnectorNameGiven,
|
||||
#[error(error_type = ErrorType::ObjectNotFound, code = "HE_04", message = "Address does not exist in our records")]
|
||||
AddressNotFound,
|
||||
#[error(error_type = ErrorType::InvalidRequestError, code = "HE_04", message = "Card with the provided iin does not exist")]
|
||||
InvalidCardIin,
|
||||
#[error(error_type = ErrorType::InvalidRequestError, code = "HE_04", message = "The provided card IIN length is invalid, please provide an iin with 6 or 8 digits")]
|
||||
InvalidCardIinLength,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -202,9 +206,10 @@ impl actix_web::ResponseError for ApiErrorResponse {
|
||||
}
|
||||
Self::InvalidRequestUrl => StatusCode::NOT_FOUND, // 404
|
||||
Self::InvalidHttpMethod => StatusCode::METHOD_NOT_ALLOWED, // 405
|
||||
Self::MissingRequiredField { .. } | Self::InvalidDataValue { .. } => {
|
||||
StatusCode::BAD_REQUEST
|
||||
} // 400
|
||||
Self::MissingRequiredField { .. }
|
||||
| Self::InvalidDataValue { .. }
|
||||
| Self::InvalidCardIin
|
||||
| Self::InvalidCardIinLength => StatusCode::BAD_REQUEST, // 400
|
||||
Self::InvalidDataFormat { .. } | Self::InvalidRequestData { .. } => {
|
||||
StatusCode::UNPROCESSABLE_ENTITY
|
||||
} // 422
|
||||
@ -437,9 +442,11 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
|
||||
}
|
||||
Self::NotSupported { message } => {
|
||||
AER::BadRequest(ApiError::new("HE", 3, "Payment method type not supported", Some(Extra {reason: Some(message.to_owned()), ..Default::default()})))
|
||||
}
|
||||
},
|
||||
Self::InvalidCardIin => AER::BadRequest(ApiError::new("HE", 3, "The provided card IIN does not exist", None)),
|
||||
Self::InvalidCardIinLength => AER::BadRequest(ApiError::new("HE", 3, "The provided card IIN length is invalid, please provide an IIN with 6 digits", None)),
|
||||
Self::FlowNotSupported { flow, connector } => {
|
||||
AER::BadRequest(ApiError::new("IR", 20, format!("{flow} flow not supported"), Some(Extra {connector: Some(connector.to_owned()), ..Default::default()})))
|
||||
AER::BadRequest(ApiError::new("IR", 20, format!("{flow} flow not supported"), Some(Extra {connector: Some(connector.to_owned()), ..Default::default()}))) //FIXME: error message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1244,8 +1244,7 @@ pub(crate) async fn verify_client_secret(
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
|
||||
authenticate_client_secret(Some(&cs), payment_intent.client_secret.as_ref())
|
||||
.map_err(errors::ApiErrorResponse::from)?;
|
||||
authenticate_client_secret(Some(&cs), payment_intent.client_secret.as_ref())?;
|
||||
Ok(payment_intent)
|
||||
})
|
||||
.await
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
pub mod address;
|
||||
pub mod api_keys;
|
||||
pub mod cache;
|
||||
pub mod cards_info;
|
||||
pub mod configs;
|
||||
pub mod connector_response;
|
||||
pub mod customers;
|
||||
@ -55,6 +56,7 @@ pub trait StorageInterface:
|
||||
+ queue::QueueInterface
|
||||
+ refund::RefundInterface
|
||||
+ reverse_lookup::ReverseLookupInterface
|
||||
+ cards_info::CardsInfoInterface
|
||||
+ 'static
|
||||
{
|
||||
async fn close(&mut self) {}
|
||||
|
||||
41
crates/router/src/db/cards_info.rs
Normal file
41
crates/router/src/db/cards_info.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use error_stack::IntoReport;
|
||||
|
||||
use crate::{
|
||||
connection,
|
||||
core::errors::{self, CustomResult},
|
||||
db::MockDb,
|
||||
services::Store,
|
||||
types::storage::cards_info::CardInfo,
|
||||
};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait CardsInfoInterface {
|
||||
async fn get_card_info(
|
||||
&self,
|
||||
_card_iin: &str,
|
||||
) -> CustomResult<Option<CardInfo>, errors::StorageError>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl CardsInfoInterface for Store {
|
||||
async fn get_card_info(
|
||||
&self,
|
||||
card_iin: &str,
|
||||
) -> CustomResult<Option<CardInfo>, errors::StorageError> {
|
||||
let conn = connection::pg_connection_read(self).await?;
|
||||
CardInfo::find_by_iin(&conn, card_iin)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.into_report()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl CardsInfoInterface for MockDb {
|
||||
async fn get_card_info(
|
||||
&self,
|
||||
_card_iin: &str,
|
||||
) -> CustomResult<Option<CardInfo>, errors::StorageError> {
|
||||
Err(errors::StorageError::MockDbError)?
|
||||
}
|
||||
}
|
||||
@ -121,6 +121,7 @@ pub fn mk_app(
|
||||
{
|
||||
server_app = server_app.service(routes::StripeApis::server(state.clone()));
|
||||
}
|
||||
server_app = server_app.service(routes::Cards::server(state.clone()));
|
||||
server_app = server_app.service(routes::Health::server(state));
|
||||
server_app
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
pub mod admin;
|
||||
pub mod api_keys;
|
||||
pub mod app;
|
||||
pub mod cards_info;
|
||||
pub mod configs;
|
||||
pub mod customers;
|
||||
pub mod ephemeral_key;
|
||||
@ -14,7 +15,7 @@ pub mod refunds;
|
||||
pub mod webhooks;
|
||||
|
||||
pub use self::app::{
|
||||
ApiKeys, AppState, Configs, Customers, EphemeralKey, Health, Mandates, MerchantAccount,
|
||||
ApiKeys, AppState, Cards, Configs, Customers, EphemeralKey, Health, Mandates, MerchantAccount,
|
||||
MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds, Webhooks,
|
||||
};
|
||||
#[cfg(feature = "stripe")]
|
||||
|
||||
@ -10,6 +10,7 @@ use super::{ephemeral_key::*, payment_methods::*, webhooks::*};
|
||||
use crate::{
|
||||
configs::settings::Settings,
|
||||
db::{MockDb, StorageImpl, StorageInterface},
|
||||
routes::cards_info::card_iin_info,
|
||||
services::Store,
|
||||
};
|
||||
|
||||
@ -377,3 +378,13 @@ impl ApiKeys {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Cards;
|
||||
|
||||
impl Cards {
|
||||
pub fn server(state: AppState) -> Scope {
|
||||
web::scope("/cards")
|
||||
.app_data(web::Data::new(state))
|
||||
.service(web::resource("/{bin}").route(web::get().to(card_iin_info)))
|
||||
}
|
||||
}
|
||||
|
||||
53
crates/router/src/routes/cards_info.rs
Normal file
53
crates/router/src/routes/cards_info.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use actix_web::{web, HttpRequest, Responder};
|
||||
use router_env::{instrument, tracing, Flow};
|
||||
|
||||
use super::app::AppState;
|
||||
use crate::{
|
||||
core::cards_info,
|
||||
services::{api, authentication as auth},
|
||||
};
|
||||
|
||||
/// Cards Info - Retrieve
|
||||
///
|
||||
/// Retrieve the card information given the card bin
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/cards/{bin}",
|
||||
params(("bin" = String, Path, description = "The first 6 or 9 digits of card")),
|
||||
responses(
|
||||
(status = 200, description = "Card iin data found", body = CardInfoResponse),
|
||||
(status = 404, description = "Card iin data not found")
|
||||
),
|
||||
operation_id = "Retrieve card information",
|
||||
security(("api_key" = []), ("publishable_key" = []))
|
||||
)]
|
||||
#[instrument(skip_all, fields(flow = ?Flow::CardsInfo))]
|
||||
pub async fn card_iin_info(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
path: web::Path<String>,
|
||||
payload: web::Query<api_models::cards_info::CardsInfoRequestParams>,
|
||||
) -> impl Responder {
|
||||
let card_iin = path.into_inner();
|
||||
let request_params = payload.into_inner();
|
||||
|
||||
let payload = api_models::cards_info::CardsInfoRequest {
|
||||
client_secret: request_params.client_secret,
|
||||
card_iin,
|
||||
};
|
||||
|
||||
let (auth, _) = match auth::check_client_secret_and_get_auth(req.headers(), &payload) {
|
||||
Ok((auth, _auth_flow)) => (auth, _auth_flow),
|
||||
Err(e) => return api::log_and_return_error_response(e),
|
||||
};
|
||||
|
||||
api::server_wrap(
|
||||
Flow::CardsInfo,
|
||||
state.as_ref(),
|
||||
&req,
|
||||
payload,
|
||||
cards_info::retrieve_card_info,
|
||||
&*auth,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@ -33,6 +33,22 @@ where
|
||||
#[derive(Debug)]
|
||||
pub struct ApiKeyAuth;
|
||||
|
||||
pub struct NoAuth;
|
||||
|
||||
#[async_trait]
|
||||
impl<A> AuthenticateAndFetch<(), A> for NoAuth
|
||||
where
|
||||
A: AppStateInfo + Sync,
|
||||
{
|
||||
async fn authenticate_and_fetch(
|
||||
&self,
|
||||
_request_headers: &HeaderMap,
|
||||
_state: &A,
|
||||
) -> RouterResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<A> AuthenticateAndFetch<storage::MerchantAccount, A> for ApiKeyAuth
|
||||
where
|
||||
@ -243,6 +259,12 @@ impl ClientSecretFetch for PaymentMethodListRequest {
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientSecretFetch for api_models::cards_info::CardsInfoRequest {
|
||||
fn get_client_secret(&self) -> Option<&String> {
|
||||
self.client_secret.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn jwt_auth_or<'a, T, A: AppStateInfo>(
|
||||
default_auth: &'a dyn AuthenticateAndFetch<T, A>,
|
||||
headers: &HeaderMap,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
pub mod address;
|
||||
pub mod api_keys;
|
||||
pub mod cards_info;
|
||||
pub mod configs;
|
||||
pub mod connector_response;
|
||||
pub mod customers;
|
||||
@ -23,8 +24,8 @@ pub mod refund;
|
||||
pub mod kv;
|
||||
|
||||
pub use self::{
|
||||
address::*, api_keys::*, configs::*, connector_response::*, customers::*, events::*,
|
||||
locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*,
|
||||
address::*, api_keys::*, cards_info::*, configs::*, connector_response::*, customers::*,
|
||||
events::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*,
|
||||
payment_attempt::*, payment_intent::*, payment_method::*, process_tracker::*, refund::*,
|
||||
reverse_lookup::*,
|
||||
};
|
||||
|
||||
1
crates/router/src/types/storage/cards_info.rs
Normal file
1
crates/router/src/types/storage/cards_info.rs
Normal file
@ -0,0 +1 @@
|
||||
pub use storage_models::cards_info::CardInfo;
|
||||
@ -435,3 +435,18 @@ impl ForeignFrom<storage_enums::AttemptStatus> for api_enums::AttemptStatus {
|
||||
frunk::labelled_convert_from(status)
|
||||
}
|
||||
}
|
||||
|
||||
impl ForeignFrom<storage_models::cards_info::CardInfo>
|
||||
for api_models::cards_info::CardInfoResponse
|
||||
{
|
||||
fn foreign_from(item: storage_models::cards_info::CardInfo) -> Self {
|
||||
Self {
|
||||
card_iin: item.card_iin,
|
||||
card_type: item.card_type,
|
||||
card_sub_type: item.card_subtype,
|
||||
card_network: item.card_network,
|
||||
card_issuer: item.card_issuer,
|
||||
card_issuing_country: item.card_issuing_country,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,6 +162,8 @@ pub enum Flow {
|
||||
ApiKeyRevoke,
|
||||
/// API Key list flow
|
||||
ApiKeyList,
|
||||
/// Cards Info flow
|
||||
CardsInfo,
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
17
crates/storage_models/src/cards_info.rs
Normal file
17
crates/storage_models/src/cards_info.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use diesel::{Identifiable, Queryable};
|
||||
|
||||
use crate::schema::cards_info;
|
||||
|
||||
#[derive(Clone, Debug, Queryable, Identifiable, serde::Deserialize, serde::Serialize)]
|
||||
#[diesel(table_name = cards_info, primary_key(card_iin))]
|
||||
pub struct CardInfo {
|
||||
pub card_iin: String,
|
||||
pub card_issuer: Option<String>,
|
||||
pub card_network: Option<String>,
|
||||
pub card_type: Option<String>,
|
||||
pub card_subtype: Option<String>,
|
||||
pub card_issuing_country: Option<String>,
|
||||
pub bank_code_id: Option<String>,
|
||||
pub bank_code: Option<String>,
|
||||
pub country_code: Option<String>,
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
pub mod address;
|
||||
pub mod api_keys;
|
||||
pub mod cards_info;
|
||||
pub mod configs;
|
||||
pub mod connector_response;
|
||||
pub mod customers;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
pub mod address;
|
||||
pub mod api_keys;
|
||||
pub mod cards_info;
|
||||
pub mod configs;
|
||||
pub mod connector_response;
|
||||
pub mod customers;
|
||||
|
||||
13
crates/storage_models/src/query/cards_info.rs
Normal file
13
crates/storage_models/src/query/cards_info.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use diesel::associations::HasTable;
|
||||
|
||||
use crate::{cards_info::CardInfo, query::generics, PgPooledConn, StorageResult};
|
||||
|
||||
impl CardInfo {
|
||||
pub async fn find_by_iin(conn: &PgPooledConn, card_iin: &str) -> StorageResult<Option<Self>> {
|
||||
generics::generic_find_by_id_optional::<<Self as HasTable>::Table, _, _>(
|
||||
conn,
|
||||
card_iin.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@ -42,6 +42,23 @@ diesel::table! {
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use crate::enums::diesel_exports::*;
|
||||
|
||||
cards_info (card_iin) {
|
||||
card_iin -> Varchar,
|
||||
card_issuer -> Nullable<Text>,
|
||||
card_network -> Nullable<Text>,
|
||||
card_type -> Nullable<Text>,
|
||||
card_subtype -> Nullable<Text>,
|
||||
card_issuing_country -> Nullable<Text>,
|
||||
bank_code_id -> Nullable<Varchar>,
|
||||
bank_code -> Nullable<Varchar>,
|
||||
country_code -> Nullable<Varchar>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use crate::enums::diesel_exports::*;
|
||||
@ -366,6 +383,7 @@ diesel::table! {
|
||||
diesel::allow_tables_to_appear_in_same_query!(
|
||||
address,
|
||||
api_keys,
|
||||
cards_info,
|
||||
configs,
|
||||
connector_response,
|
||||
customers,
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE IF EXISTS cards_info;
|
||||
15
migrations/2023-03-14-123541_add_cards_info_table/up.sql
Normal file
15
migrations/2023-03-14-123541_add_cards_info_table/up.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- Your SQL goes here
|
||||
CREATE TABLE cards_info (
|
||||
card_iin VARCHAR(16) PRIMARY KEY,
|
||||
card_issuer TEXT,
|
||||
card_network TEXT,
|
||||
card_type TEXT,
|
||||
card_subtype TEXT,
|
||||
card_issuing_country TEXT,
|
||||
bank_code_id VARCHAR(32),
|
||||
bank_code VARCHAR(32),
|
||||
country_code VARCHAR(32),
|
||||
date_created TIMESTAMP NOT NULL,
|
||||
last_updated TIMESTAMP,
|
||||
last_updated_provider TEXT
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user