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:
Narayan Bhat
2023-03-28 19:04:58 +05:30
committed by GitHub
parent 20b4372bfe
commit b15b8f7b43
27 changed files with 1336 additions and 299 deletions

View File

@ -1 +0,0 @@

View 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>,
}

View File

@ -2,7 +2,7 @@
pub mod admin; pub mod admin;
pub mod api_keys; pub mod api_keys;
pub mod bank_accounts; pub mod bank_accounts;
pub mod cards; pub mod cards_info;
pub mod customers; pub mod customers;
pub mod disputes; pub mod disputes;
pub mod enums; pub mod enums;

View File

@ -347,7 +347,9 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
| errors::ApiErrorResponse::GenericUnauthorized { .. } | errors::ApiErrorResponse::GenericUnauthorized { .. }
| errors::ApiErrorResponse::InvalidEphemeralKey => Self::Unauthorized, | errors::ApiErrorResponse::InvalidEphemeralKey => Self::Unauthorized,
errors::ApiErrorResponse::InvalidRequestUrl errors::ApiErrorResponse::InvalidRequestUrl
| errors::ApiErrorResponse::InvalidHttpMethod => Self::InvalidRequestUrl, | errors::ApiErrorResponse::InvalidHttpMethod
| errors::ApiErrorResponse::InvalidCardIin
| errors::ApiErrorResponse::InvalidCardIinLength => Self::InvalidRequestUrl,
errors::ApiErrorResponse::MissingRequiredField { field_name } => { errors::ApiErrorResponse::MissingRequiredField { field_name } => {
Self::ParameterMissing { Self::ParameterMissing {
field_name, field_name,

View File

@ -1,5 +1,6 @@
pub mod admin; pub mod admin;
pub mod api_keys; pub mod api_keys;
pub mod cards_info;
pub mod configs; pub mod configs;
pub mod customers; pub mod customers;
pub mod errors; pub mod errors;

View 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),
))
}

View File

@ -158,6 +158,10 @@ pub enum ApiErrorResponse {
IncorrectConnectorNameGiven, IncorrectConnectorNameGiven,
#[error(error_type = ErrorType::ObjectNotFound, code = "HE_04", message = "Address does not exist in our records")] #[error(error_type = ErrorType::ObjectNotFound, code = "HE_04", message = "Address does not exist in our records")]
AddressNotFound, 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)] #[derive(Clone)]
@ -202,9 +206,10 @@ impl actix_web::ResponseError for ApiErrorResponse {
} }
Self::InvalidRequestUrl => StatusCode::NOT_FOUND, // 404 Self::InvalidRequestUrl => StatusCode::NOT_FOUND, // 404
Self::InvalidHttpMethod => StatusCode::METHOD_NOT_ALLOWED, // 405 Self::InvalidHttpMethod => StatusCode::METHOD_NOT_ALLOWED, // 405
Self::MissingRequiredField { .. } | Self::InvalidDataValue { .. } => { Self::MissingRequiredField { .. }
StatusCode::BAD_REQUEST | Self::InvalidDataValue { .. }
} // 400 | Self::InvalidCardIin
| Self::InvalidCardIinLength => StatusCode::BAD_REQUEST, // 400
Self::InvalidDataFormat { .. } | Self::InvalidRequestData { .. } => { Self::InvalidDataFormat { .. } | Self::InvalidRequestData { .. } => {
StatusCode::UNPROCESSABLE_ENTITY StatusCode::UNPROCESSABLE_ENTITY
} // 422 } // 422
@ -437,9 +442,11 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
} }
Self::NotSupported { message } => { Self::NotSupported { message } => {
AER::BadRequest(ApiError::new("HE", 3, "Payment method type not supported", Some(Extra {reason: Some(message.to_owned()), ..Default::default()}))) 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 } => { 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
} }
} }
} }

View File

@ -1244,8 +1244,7 @@ pub(crate) async fn verify_client_secret(
.await .await
.change_context(errors::ApiErrorResponse::PaymentNotFound)?; .change_context(errors::ApiErrorResponse::PaymentNotFound)?;
authenticate_client_secret(Some(&cs), payment_intent.client_secret.as_ref()) authenticate_client_secret(Some(&cs), payment_intent.client_secret.as_ref())?;
.map_err(errors::ApiErrorResponse::from)?;
Ok(payment_intent) Ok(payment_intent)
}) })
.await .await

View File

@ -1,6 +1,7 @@
pub mod address; pub mod address;
pub mod api_keys; pub mod api_keys;
pub mod cache; pub mod cache;
pub mod cards_info;
pub mod configs; pub mod configs;
pub mod connector_response; pub mod connector_response;
pub mod customers; pub mod customers;
@ -55,6 +56,7 @@ pub trait StorageInterface:
+ queue::QueueInterface + queue::QueueInterface
+ refund::RefundInterface + refund::RefundInterface
+ reverse_lookup::ReverseLookupInterface + reverse_lookup::ReverseLookupInterface
+ cards_info::CardsInfoInterface
+ 'static + 'static
{ {
async fn close(&mut self) {} async fn close(&mut self) {}

View 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)?
}
}

View File

@ -121,6 +121,7 @@ pub fn mk_app(
{ {
server_app = server_app.service(routes::StripeApis::server(state.clone())); 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 = server_app.service(routes::Health::server(state));
server_app server_app
} }

View File

@ -1,6 +1,7 @@
pub mod admin; pub mod admin;
pub mod api_keys; pub mod api_keys;
pub mod app; pub mod app;
pub mod cards_info;
pub mod configs; pub mod configs;
pub mod customers; pub mod customers;
pub mod ephemeral_key; pub mod ephemeral_key;
@ -14,7 +15,7 @@ pub mod refunds;
pub mod webhooks; pub mod webhooks;
pub use self::app::{ 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, MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds, Webhooks,
}; };
#[cfg(feature = "stripe")] #[cfg(feature = "stripe")]

View File

@ -10,6 +10,7 @@ use super::{ephemeral_key::*, payment_methods::*, webhooks::*};
use crate::{ use crate::{
configs::settings::Settings, configs::settings::Settings,
db::{MockDb, StorageImpl, StorageInterface}, db::{MockDb, StorageImpl, StorageInterface},
routes::cards_info::card_iin_info,
services::Store, 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)))
}
}

View 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
}

View File

@ -33,6 +33,22 @@ where
#[derive(Debug)] #[derive(Debug)]
pub struct ApiKeyAuth; 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] #[async_trait]
impl<A> AuthenticateAndFetch<storage::MerchantAccount, A> for ApiKeyAuth impl<A> AuthenticateAndFetch<storage::MerchantAccount, A> for ApiKeyAuth
where 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>( pub fn jwt_auth_or<'a, T, A: AppStateInfo>(
default_auth: &'a dyn AuthenticateAndFetch<T, A>, default_auth: &'a dyn AuthenticateAndFetch<T, A>,
headers: &HeaderMap, headers: &HeaderMap,

View File

@ -1,5 +1,6 @@
pub mod address; pub mod address;
pub mod api_keys; pub mod api_keys;
pub mod cards_info;
pub mod configs; pub mod configs;
pub mod connector_response; pub mod connector_response;
pub mod customers; pub mod customers;
@ -23,8 +24,8 @@ pub mod refund;
pub mod kv; pub mod kv;
pub use self::{ pub use self::{
address::*, api_keys::*, configs::*, connector_response::*, customers::*, events::*, address::*, api_keys::*, cards_info::*, configs::*, connector_response::*, customers::*,
locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, events::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*,
payment_attempt::*, payment_intent::*, payment_method::*, process_tracker::*, refund::*, payment_attempt::*, payment_intent::*, payment_method::*, process_tracker::*, refund::*,
reverse_lookup::*, reverse_lookup::*,
}; };

View File

@ -0,0 +1 @@
pub use storage_models::cards_info::CardInfo;

View File

@ -435,3 +435,18 @@ impl ForeignFrom<storage_enums::AttemptStatus> for api_enums::AttemptStatus {
frunk::labelled_convert_from(status) 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,
}
}
}

View File

@ -162,6 +162,8 @@ pub enum Flow {
ApiKeyRevoke, ApiKeyRevoke,
/// API Key list flow /// API Key list flow
ApiKeyList, ApiKeyList,
/// Cards Info flow
CardsInfo,
} }
/// ///

View 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>,
}

View File

@ -1,5 +1,6 @@
pub mod address; pub mod address;
pub mod api_keys; pub mod api_keys;
pub mod cards_info;
pub mod configs; pub mod configs;
pub mod connector_response; pub mod connector_response;
pub mod customers; pub mod customers;

View File

@ -1,5 +1,6 @@
pub mod address; pub mod address;
pub mod api_keys; pub mod api_keys;
pub mod cards_info;
pub mod configs; pub mod configs;
pub mod connector_response; pub mod connector_response;
pub mod customers; pub mod customers;

View 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
}
}

View File

@ -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! { diesel::table! {
use diesel::sql_types::*; use diesel::sql_types::*;
use crate::enums::diesel_exports::*; use crate::enums::diesel_exports::*;
@ -366,6 +383,7 @@ diesel::table! {
diesel::allow_tables_to_appear_in_same_query!( diesel::allow_tables_to_appear_in_same_query!(
address, address,
api_keys, api_keys,
cards_info,
configs, configs,
connector_response, connector_response,
customers, customers,

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE IF EXISTS cards_info;

View 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