Coverage for src/integrify/azericard/schemas/request/auth.py: 98%
76 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-12-29 00:57 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-12-29 00:57 +0000
1import base64
2import json
3from typing import ClassVar, Literal, Optional, Union
5import rsa
6from pydantic import (
7 AliasGenerator,
8 ConfigDict,
9 Field,
10 computed_field,
11 field_serializer,
12)
13from typing_extensions import TypedDict
15from integrify.azericard import env
16from integrify.azericard.schemas.common import (
17 AzeriCardMinimalDataSchema,
18 AzeriCardMinimalWithAmountDataSchema,
19)
20from integrify.azericard.schemas.enums import (
21 AuthorizationMiscType,
22 AuthorizationResponseType,
23 AuthorizationType,
24)
25from integrify.schemas import PayloadBaseModel
28class BaseRequestSchema(PayloadBaseModel):
29 SIGNATURE_FIELDS: ClassVar[list[str]]
30 """P_SIGN hesablanılması üçün lazım olan field adları"""
32 model_config = ConfigDict(alias_generator=AliasGenerator(serialization_alias=str.upper))
34 @computed_field
35 def p_sign(self) -> Optional[str]:
36 """P_SIGN generasiyası"""
37 if not self.SIGNATURE_FIELDS:
38 return None # pragma: no cover
40 with open(env.AZERICARD_KEY_FILE_PATH, 'rb') as key_file:
41 private_key = rsa.PrivateKey.load_pkcs1(key_file.read())
43 mac_source = self.generate_mac_source().encode('utf-8')
44 return rsa.sign(mac_source, private_key, 'SHA-256').hex()
46 def generate_mac_source(self):
47 """P_SIGN üçün MAC source-un yaradılması"""
48 source = ''
49 for field in self.SIGNATURE_FIELDS:
50 val = getattr(self, field)
52 if val:
53 source += str(len(str(val))) + str(val)
54 else:
55 # So far, no case can reach this state
56 source += '-' # pragma: no cover
58 return source
61class MobilePhone(TypedDict):
62 cc: str
63 subscriber: str
66class MInfo(TypedDict):
67 browserScreenHeight: str
68 browserScreenWidth: str
69 browserTZ: str
70 mobilePhone: MobilePhone
73class AuthRequestSchema(BaseRequestSchema, AzeriCardMinimalWithAmountDataSchema):
74 SIGNATURE_FIELDS: ClassVar[list[str]] = [
75 'amount',
76 'currency',
77 'terminal',
78 'trtype',
79 'timestamp',
80 'nonce',
81 'merch_url',
82 ]
84 # Next three fields can be set either through
85 # functions or environment, but they MUST be set
86 desc: str = Field(default=env.AZERICARD_MERCHANT_NAME, min_length=1, max_length=50) # type: ignore[assignment]
87 merch_name: str = Field(default=env.AZERICARD_MERCHANT_NAME, min_length=1, max_length=50) # type: ignore[assignment]
88 merch_url: str = Field(default=env.AZERICARD_MERCHANT_URL, min_length=1, max_length=250) # type: ignore[assignment]
90 email: Optional[str] = Field(default=env.AZERICARD_MERCHANT_EMAIL, max_length=80)
91 country: Optional[str] = Field(None, max_length=2)
92 merch_gmt: Optional[str] = Field(None, min_length=1, max_length=5)
93 backref: str = Field(default=env.AZERICARD_CALLBACK_URL, min_length=1, max_length=250) # type: ignore[assignment]
94 lang: str = Field(default=env.AZERICARD_INTERFACE_LANG, min_length=2, max_length=2)
95 name: Optional[str] = Field(None, min_length=2, max_length=45)
96 m_info: Optional[MInfo] = None
98 @field_serializer('m_info')
99 def serialize_minfo_to_b64(self, m_info: Optional[MInfo]):
100 """M_INFO dcit-ini base64 encodelaşdırılması"""
101 if not m_info: 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true
102 return None
104 return base64.b64encode(json.dumps(m_info).encode()).decode()
106 @classmethod
107 def get_input_fields(cls):
108 return [
109 'amount',
110 'currency',
111 'order',
112 'desc',
113 'trtype',
114 'merch_name',
115 'merch_url',
116 'terminal',
117 'email',
118 'country',
119 'merch_gmt',
120 'backref',
121 'timestamp',
122 'lang',
123 'name',
124 'm_info',
125 ]
128class AuthConfirmRequestSchema(BaseRequestSchema, AzeriCardMinimalWithAmountDataSchema):
129 SIGNATURE_FIELDS: ClassVar[list[str]] = [
130 'order',
131 'amount',
132 'currency',
133 'terminal',
134 'trtype',
135 'rrn',
136 'int_ref',
137 ]
139 rrn: str = Field(min_length=12, max_length=12)
140 """Müştəri bankının axtarış istinad nömrəsi (ISO-8583 Sahə 37)"""
142 int_ref: str = Field(min_length=1, max_length=128)
143 """Elektron ticarət şlüzünün daxili istinad nömrəsi"""
145 @classmethod
146 def get_input_fields(cls):
147 return [
148 'order',
149 'amount',
150 'currency',
151 'rrn',
152 'int_ref',
153 'trtype',
154 'terminal',
155 'timestamp',
156 ]
159class AuthAndSaveCardRequestSchema(AuthRequestSchema):
160 token_action: Literal['REGISTER'] = 'REGISTER'
163class AuthWithSavedCardRequestSchema(AuthRequestSchema):
164 token: str = Field(min_length=28, max_length=28)
166 @classmethod
167 def get_input_fields(cls):
168 return [
169 'amount',
170 'currency',
171 'order',
172 'desc',
173 'trtype',
174 'token',
175 'merch_name',
176 'merch_url',
177 'terminal',
178 'email',
179 'country',
180 'merch_gmt',
181 'backref',
182 'timestamp',
183 'lang',
184 'name',
185 'm_info',
186 ]
189class GetTransactionStatusRequestSchema(BaseRequestSchema, AzeriCardMinimalDataSchema):
190 SIGNATURE_FIELDS: ClassVar[list[str]] = [
191 'order',
192 'terminal',
193 'trtype',
194 'timestamp',
195 'nonce',
196 ]
197 tran_trtype: Union[AuthorizationType, AuthorizationResponseType] = Field(
198 min_length=1,
199 max_length=2,
200 )
201 trtype: Literal[AuthorizationMiscType.REQUEST_STATUS] = AuthorizationMiscType.REQUEST_STATUS
203 @classmethod
204 def get_input_fields(cls):
205 return ['tran_trtype', 'order', 'terminal', 'timestamp']