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

1import base64 

2import json 

3from typing import ClassVar, Literal, Optional, Union 

4 

5import rsa 

6from pydantic import ( 

7 AliasGenerator, 

8 ConfigDict, 

9 Field, 

10 computed_field, 

11 field_serializer, 

12) 

13from typing_extensions import TypedDict 

14 

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 

26 

27 

28class BaseRequestSchema(PayloadBaseModel): 

29 SIGNATURE_FIELDS: ClassVar[list[str]] 

30 """P_SIGN hesablanılması üçün lazım olan field adları""" 

31 

32 model_config = ConfigDict(alias_generator=AliasGenerator(serialization_alias=str.upper)) 

33 

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 

39 

40 with open(env.AZERICARD_KEY_FILE_PATH, 'rb') as key_file: 

41 private_key = rsa.PrivateKey.load_pkcs1(key_file.read()) 

42 

43 mac_source = self.generate_mac_source().encode('utf-8') 

44 return rsa.sign(mac_source, private_key, 'SHA-256').hex() 

45 

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) 

51 

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 

57 

58 return source 

59 

60 

61class MobilePhone(TypedDict): 

62 cc: str 

63 subscriber: str 

64 

65 

66class MInfo(TypedDict): 

67 browserScreenHeight: str 

68 browserScreenWidth: str 

69 browserTZ: str 

70 mobilePhone: MobilePhone 

71 

72 

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 ] 

83 

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] 

89 

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 

97 

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 

103 

104 return base64.b64encode(json.dumps(m_info).encode()).decode() 

105 

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 ] 

126 

127 

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 ] 

138 

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)""" 

141 

142 int_ref: str = Field(min_length=1, max_length=128) 

143 """Elektron ticarət şlüzünün daxili istinad nömrəsi""" 

144 

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 ] 

157 

158 

159class AuthAndSaveCardRequestSchema(AuthRequestSchema): 

160 token_action: Literal['REGISTER'] = 'REGISTER' 

161 

162 

163class AuthWithSavedCardRequestSchema(AuthRequestSchema): 

164 token: str = Field(min_length=28, max_length=28) 

165 

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 ] 

187 

188 

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 

202 

203 @classmethod 

204 def get_input_fields(cls): 

205 return ['tran_trtype', 'order', 'terminal', 'timestamp']