Source code for sslcommerz_client.dataclasses

from pydantic import BaseModel, ValidationError, validator, AnyHttpUrl
from decimal import Decimal
from typing import List, Optional, Any, Union
from enum import Enum
from datetime import datetime
from hashlib import md5


[docs]class MultiCardNamesEnum(str, Enum): BRAC_VISA = "brac_visa" DBBL_VISA = "dbbl_visa" CITY_VISA = "city_visa" EBL_VISA = "ebl_visa" SBL_VISA = "sbl_visa" BRAC_MASTER = "brac_master" DBBL_MASTER = "dbbl_master" CITY_MASTER = "city_master" EBL_MASTER = "ebl_master" SBL_MASTER = "sbl_master" CITY_AMEX = "city_amex" QCASH = "qcash" DBBL_NEXUS = "dbbl_nexus" BANK_ASIA = "bankasia" ABBANK = "abbank" IBBL = "ibbl" MTBL = "mtbl" BKASH = ("bkash",) DBBL_MOBILE_BANKING = "dbblmobilebanking" CITY = "city" UPAY = "upay" TAPNPAY = "tapnpay" INTERNET_BANK = "internetbank" MOBILE_BANK = "mobilebank" OTHER_CARD = "othercard" VISA_CARD = "visacard" MASTER_CARD = "mastercard" AMEX_CARD = "amexcard"
[docs]class EMIOptionsEnum(int, Enum): THREE_MONTHS = 3 SIX_MONTHS = 6 NINE_MONTHS = 9
[docs]class ShippingMethodEnum(str, Enum): YES = "YES" NO = "NO" COURIER = "COURIER"
[docs]class BooleanIntEnum(int, Enum): TRUE = 1 FALSE = 0
[docs]class ProductProfileEnum(str, Enum): GENERAL = "general" PHYSICAL_GOODS = "physical-goods" NON_PHYSICAL_GOODS = "non-physical-goods" AIRLINE_TICKETS = "airline-tickets" TRAVEL_VERTICAL = "travel-vertical" TELECOM_VERTICAL = "telecom-vertical"
[docs]class Credential(BaseModel): store_id: str store_passwd: str
[docs]class CartItem(BaseModel): """Dataclass for cart items in PaymentInitPostData.""" product: str quantity: int amount: Decimal
[docs] @validator("product") def not_more_than_255(cls, v, field): if len(v) > 255: raise ValueError(f"{field} can't be more than 255 characters") return v
[docs] @validator("amount") def valid_decimal(cls, v, field): val = str(float(v)).split(".") if len(val[0]) > 12 or len(val[1]) > 2: raise ValueError(f"{field} must have a decimal maximum of (12,2).") return v
[docs]class PaymentInitPostData(BaseModel): """Dataclass for session initiation post data.""" # Basic Fields total_amount: Decimal currency: str tran_id: str product_category: str success_url: AnyHttpUrl fail_url: AnyHttpUrl cancel_url: AnyHttpUrl # EMI Fields emi_option: BooleanIntEnum = BooleanIntEnum.FALSE # Customer Information cus_name: str cus_email: str cus_add1: str cus_city: str cus_country: str cus_phone: str # Shipping Method shipping_method: ShippingMethodEnum = ShippingMethodEnum.YES num_of_item: int # Product Information product_name: str product_category: str product_profile: ProductProfileEnum # Basic Fields Optional ipn_url: Optional[str] multi_card_name: Optional[MultiCardNamesEnum] allowed_bin: Optional[str] # EMI Optional emi_max_inst_option: Optional[EMIOptionsEnum] emi_selected_inst: Optional[EMIOptionsEnum] emi_allow_only: Optional[int] # Customer Optional cus_add2: Optional[str] cus_postcode: Optional[str] cus_state: Optional[str] cus_fax: Optional[str] # Shipping Method Optional ship_name: Optional[str] ship_add1: Optional[str] ship_add2: Optional[str] ship_city: Optional[str] ship_postcode: Optional[str] ship_country: Optional[str] ship_phone: Optional[str] ship_state: Optional[str] # Product Information Optional hours_till_departure: Optional[str] flight_type: Optional[str] pnr: Optional[str] journey_from_to: Optional[str] third_party_booking: Optional[str] hotel_name: Optional[str] length_of_stay: Optional[str] check_in_time: Optional[str] hotel_city: Optional[str] product_type: Optional[str] topup_number: Optional[str] country_topup: Optional[str] cart: Optional[List[CartItem]] product_amount: Optional[Decimal] vat: Optional[Decimal] discount_amount: Optional[Decimal] convenience_fee: Optional[Decimal] # Additional Optional value_a: Optional[str] value_b: Optional[str] value_c: Optional[str] value_d: Optional[str]
[docs] class Config: arbitrary_types_allowed = True
[docs] @validator( "currency", ) def not_more_than_three(cls, v, field): if len(v) > 3: raise ValueError(f"{field} can't be more than 3 characters") return v
[docs] @validator( "tran_id", "cus_postcode", "hours_till_departure", "length_of_stay", "check_in_time", "product_type", "country_topup", ) def not_more_than_thirty(cls, v, field): if len(v) > 30: raise ValueError(f"{field} can't be more than 30 characters") return v
[docs] @validator( "product_category", "cus_name", "cus_email", "cus_add1", "cus_add2", "cus_city", "cus_state", "cus_country", "ship_name", "ship_add1", "ship_add2", "ship_city", "ship_state", "ship_country", "ship_postcode", "pnr", "hotel_city", ) def not_more_than_fifty(cls, v, field): if len(v) > 50: raise ValueError(f"{field} can't be more than 50 characters") return v
[docs] @validator("product_profile", "product_category") def not_more_than_hundred(cls, v, field): if len(v) > 100: raise ValueError(f"{field} can't be more than 100 characters") return v
[docs] @validator("topup_number") def not_more_than_hundred_fifty(cls, v, field): if len(v) > 150: raise ValueError(f"{field} can't be more than 150 characters") return v
[docs] @validator( "success_url", "fail_url", "cancel_url", "ipn_url", "allowed_bin", "product_name", "journey_from_to", "hotel_name", "value_a", "value_b", "value_c", "value_d", ) def not_more_than_255(cls, v, field): if len(v) > 255: raise ValueError(f"{field} can't be more than 255 characters") return v
[docs] @validator( "total_amount", "product_amount", "vat", "discount_amount", "convenience_fee", ) def valid_decimal(cls, v, field): val = str(float(v)).split(".") if len(val[0]) > 10 or len(val[1]) > 2: raise ValueError(f"{field} must have a decimal maximum of (10,2).") return v
[docs] @validator("emi_allow_only", always=True) def valid_emi_allow_only(cls, v, values, field): emi = False if "emi_option" in values and values["emi_option"] == BooleanIntEnum.TRUE: emi = True if not emi and v == 1: raise ValidationError("emi_option should be enabled to use this field") return v
[docs] @validator("num_of_item") def validate_num_of_item(cls, v): if v > 99 or v < 0: raise ValueError( "num_of_item should be of maximum two digits and a positive integer." ) return v
[docs] @validator( "ship_name", "ship_add1", "ship_city", "ship_postcode", "ship_country", always=True, ) def validate_based_on_shipping_method(cls, v, field, values): shipping = values["shipping_method"] == ShippingMethodEnum.YES if shipping and not v: raise ValueError( f"{field} should be provided if shipping_method set to 'YES'" ) if not shipping and v: raise ValueError( f"{field} should be omitted if shipping_method not set to 'YES'" ) return v
[docs] @validator( "hours_till_departure", "flight_type", "pnr", "journey_from_to", "third_party_booking", ) def mandatory_if_airline_tickets(cls, v, field, values): if values["product_profile"] == ProductProfileEnum.AIRLINE_TICKETS and not v: raise ValueError( f"{field} is required if product_profile is {ProductProfileEnum.AIRLINE_TICKETS}" ) if values["product_profile"] != ProductProfileEnum.AIRLINE_TICKETS and v: raise ValueError( f"{field} should be omitted if product_profile is {ProductProfileEnum.AIRLINE_TICKETS}" ) return v
[docs] @validator( "hotel_name", "length_of_stay", "check_in_time", "hotel_city", ) def mandatory_if_travel_vertical(cls, v, field, values): if values["product_profile"] == ProductProfileEnum.TRAVEL_VERTICAL and not v: raise ValueError( f"{field} is required if product_profile is {ProductProfileEnum.TRAVEL_VERTICAL}" ) if values["product_profile"] != ProductProfileEnum.TRAVEL_VERTICAL and v: raise ValueError( f"{field} should be omitted if product_profile is {ProductProfileEnum.TRAVEL_VERTICAL}" ) return v
[docs] @validator( "product_type", "topup_number", "country_topup", ) def mandatory_if_telecom_vertical(cls, v, field, values): if values["product_profile"] == ProductProfileEnum.TELECOM_VERTICAL and not v: raise ValueError( f"{field} is required if product_profile is {ProductProfileEnum.TELECOM_VERTICAL}" ) if values["product_profile"] != ProductProfileEnum.TRAVEL_VERTICAL and v: raise ValueError( f"{field} should be omitted if product_profile is {ProductProfileEnum.TELECOM_VERTICAL}" ) return v
[docs] @validator("cart") def check_cart_items(cls, v): for item in v: v.validate() return v
[docs]class ResponseStatusEnum(str, Enum): SUCCESS = "SUCCESS" FAILED = "FAILED"
[docs]class Gateway(BaseModel): name: str type: str logo: Optional[str] gw: Optional[str] r_flag: Optional[str] redirectGatewayURL: Optional[str]
[docs]class PaymentInitResponse(BaseModel): """Payment initiation response as a dataclass.""" status: ResponseStatusEnum failedreason: Optional[str] sessionkey: Optional[str] gw: Optional[Any] redirectGatewayURL: Optional[str] directPaymentURLBank: Optional[str] directPaymentURLCard: Optional[str] directPaymentURL: Optional[str] redirectGatewayURLFailed: Optional[str] GatewayPageURL: Optional[str] storeBanner: Optional[str] storeLogo: Optional[str] desc: Optional[List[Gateway]]
[docs] class Config: arbitrary_types_allowed = True
[docs]class IPNOrderStatusEnum(str, Enum): VALID = "VALID" FAILED = "FAILED" CANCELLED = "CANCELLED" UNATTEMPTED = "UNATTEMPTED" EXPIRED = "EXPIRED"
[docs]class OrderStatusEnum(str, Enum): VALID = "VALID" VALIDATED = "VALIDATED" INVALID_TRANSACTION = "INVALID_TRANSACTION"
[docs]class CardBrandEnum(str, Enum): VISA = "VISA" MASTER = "MASTER" AMEX = "AMEX" IB = "IB" MOBILE_BANKING = "MOBILE BANKING"
[docs]class RiskLevelEnum(int, Enum): HIGH = 1 LOW = 0
[docs]class BaseOrderResponse(BaseModel): """Base dataclass for Order and IPN.""" tran_date: datetime tran_id: str val_id: str amount: Decimal store_amount: Decimal card_type: str card_no: str currency: str bank_tran_id: str card_issuer: str card_brand: CardBrandEnum card_issuer_country: str card_issuer_country_code: str currency_type: str currency_amount: Decimal risk_level: RiskLevelEnum risk_title: str value_a: Optional[str] value_b: Optional[str] value_c: Optional[str] value_d: Optional[str]
[docs]class IPNResponse(BaseOrderResponse): """IPN response dataclass with validation""" status: IPNOrderStatusEnum verify_sign: str verify_key: str
[docs] def validate_against_credential(self, credential: Union[Credential, dict]): if not isinstance(credential, Credential): credential = Credential(**credential) keys = self.verify_key.split(",") keys.append("store_passwd") keys = sorted(keys) data = [] for key in keys: if key == "store_passwd": data.append((key, md5(credential.store_passwd).hexdigest)) else: data.append((key, getattr(self, key))) hash_string = "&".join([v.join("=") for v in data]) hash_string = md5(hash_string.encode()).hexdigest() return hash_string == self.verify_sign
[docs]class IPNValidationStatus(BaseModel): """IPN validation result's dataclass.""" status: bool response: IPNResponse
[docs] class Config: arbitrary_types_allowed = True
[docs]class OrderValidationPostData(BaseModel): """Dataclass for Order validation API post data.""" val_id: str v: Optional[int]
[docs] @validator("val_id") def not_more_than_fifty(cls, v, field): if len(v) > 50: raise ValueError(f"{field} can't be more than 50 characters") return v
[docs] @validator("v") def validate_v(cls, v): if v < 0 or v > 9: raise ValueError("v must be an one digit positive integer")
[docs]class OrderValidationResponse(BaseOrderResponse): """Order validation response.""" status: OrderStatusEnum emi_instalment: EMIOptionsEnum emi_amount: Decimal discount_amount: Decimal discount_percentage: Decimal discount_remarks: str
[docs]class RefundRequestPostData(BaseModel): """Dataclass for Refund API post data.""" bank_tran_id: str refund_amount: str refund_remarks: str refe_id: str
[docs] @validator( "refe_id", ) def not_more_than_fifty(cls, v, field): if len(v) > 50: raise ValueError(f"{field} can't be more than 50 characters") return v
[docs] @validator("bank_tran_id") def not_more_than_eighty(cls, v, field): if len(v) > 80: raise ValueError(f"{field} can't be more than 80 characters") return v
[docs] @validator("refund_remarks") def not_more_than_255(cls, v, field): if len(v) > 255: raise ValueError(f"{field} can't be more than 255 characters") return v
[docs] @validator("refund_amount") def valid_decimal(cls, v, field): val = str(float(v)).split(".") if len(val[0]) > 10 or len(val[1]) > 2: raise ValueError(f"{field} must have a decimal maximum of (10,2).") return v
[docs]class APIConnectEnum(str, Enum): INVALID_REQUEST = "INVALID_REQUEST" FAILED = "FAILED" INACTIVE = "INACTIVE" DONE = "DONE"
[docs]class RefundStatusEnum(str, Enum): SUCCESS = "success" FAILED = "failed" PROCESSING = "processing"
[docs]class RefundInitateResponse(BaseModel): """Refund initiation response.""" APIConnect: APIConnectEnum bank_tran_id: str trans_id: Optional[str] refund_ref_id: Optional[str] status: RefundStatusEnum errorReason: Optional[str]
[docs]class RefundResponse(RefundInitateResponse): """Refund response.""" initiated_on: datetime refunded_on: datetime
[docs]class Session(BaseModel): """Dataclass for transaction session.""" status: str sessionkey: str tran_date: datetime tran_id: str val_id: str amount: Decimal store_amount: Decimal card_type: str card_no: str currency: str bank_tran_id: str card_issuer: str card_brand: CardBrandEnum card_issuer_country: str card_issuer_country_code: str currency_type: str currency_amount: Decimal emi_instalment: int emi_amount: Decimal discount_percentage: Decimal discount_remarks: str value_a: Optional[str] value_b: Optional[str] value_c: Optional[str] value_d: Optional[str] risk_level: RiskLevelEnum risk_title: str
[docs]class TransactionBySessionResponse(Session): """Dataclass for transaction by session query.""" APIConnect: APIConnectEnum
[docs]class TransactionsByIDResponse(BaseModel): """Dataclass for transactions by ID query.""" APIConnect: APIConnectEnum no_of_trans_found: int element: List[Session]
[docs]class APIResponse(BaseModel): """dataclass for api response complete with raw response data, status_code and one of response objects for easy introspection.""" raw_data: Any status_code: int response: Optional[ Union[ OrderValidationResponse, IPNResponse, PaymentInitResponse, RefundResponse, RefundInitateResponse, TransactionBySessionResponse, TransactionsByIDResponse, ] ]
[docs] class Config: arbitrary_types_allowed = True