Coverage for packages/kapitalbank/src/integrify/kapitalbank/handlers.py: 88%

98 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-10-19 02:39 +0000

1import base64 

2import json 

3from functools import cached_property 

4from typing import ClassVar 

5 

6import httpx 

7from integrify.api import APIPayloadHandler, APIResponse, _ResponseT 

8from integrify.kapitalbank.env import KAPITAL_PASSWORD, KAPITAL_USERNAME 

9from integrify.kapitalbank.schemas.request import ( 

10 ClearingOrderRequestSchema, 

11 CreateOrderRequestSchema, 

12 FullReverseOrderRequestSchema, 

13 LinkCardTokenRequestSchema, 

14 OrderInformationRequestSchema, 

15 OrderWithSavedCardRequestSchema, 

16 PartialReverseOrderRequestSchema, 

17 PayAndSaveCardRequestSchema, 

18 ProcessPaymentWithSavedCardRequestSchema, 

19 RefundOrderRequestSchema, 

20 SaveCardRequestSchema, 

21) 

22from integrify.kapitalbank.schemas.response import ( 

23 BaseResponseSchema, 

24 ClearingOrderResponseSchema, 

25 CreateOrderResponseSchema, 

26 DetailedOrderInformationResponseSchema, 

27 ErrorResponseBodySchema, 

28 FullReverseOrderResponseSchema, 

29 LinkCardTokenResponseSchema, 

30 OrderInformationResponseSchema, 

31 PartialReverseOrderResponseSchema, 

32 ProcessPaymentWithSavedCardResponseSchema, 

33 RefundOrderResponseSchema, 

34) 

35from pydantic import BaseModel 

36 

37 

38def _safe_json(resp: httpx.Response) -> dict: 

39 """Cavabı JSON kimi parse etmək; JSON deyilsə (gateway xətası, boş body) 

40 exception atmaq əvəzinə boş dict qaytarır.""" 

41 try: 

42 data = resp.json() 

43 except (json.JSONDecodeError, ValueError): 

44 return {} 

45 

46 return data if isinstance(data, dict) else {} 

47 

48 

49class BasePayloadHandler(APIPayloadHandler): 

50 """Kapitalbank üçün baza handler. `req_model`/`resp_model`/`data_key` 

51 alt class-larda ClassVar kimi təyin olunur.""" 

52 

53 data_key: ClassVar[str | None] = None 

54 

55 @cached_property 

56 def headers(self): 

57 credentials = f'{KAPITAL_USERNAME}:{KAPITAL_PASSWORD}' 

58 encoded_credentials = base64.b64encode(credentials.encode()).decode() 

59 return { 

60 'Authorization': f'Basic {encoded_credentials}', 

61 'Content-Type': 'application/json', 

62 } 

63 

64 def post_handle_payload(self, data): 

65 if self.data_key: 65 ↛ 68line 65 didn't jump to line 68 because the condition on line 65 was always true

66 return {self.data_key: data} 

67 

68 return data 

69 

70 def handle_response(self, resp: httpx.Response) -> APIResponse[_ResponseT]: 

71 """ 

72 Bu funksiya API-dən gələn cavabı status koduna görə tənzimləyir. 

73 Əgər status kodu 200-dürsə, gələn cavabı modelə uyğunlaşdırır və APIResponse obyektini qaytarır. 

74 200-dən fərqli status kodu gələrsə, gələn cavabı modelə uyğunlaşdırır və error obyektini APIResponse obyektinə əlavə edir. 

75 """ # noqa: E501 

76 

77 api_resp = APIResponse[BaseResponseSchema].model_validate(resp, from_attributes=True) 

78 body = _safe_json(resp) 

79 

80 if resp.status_code == 200: 80 ↛ 91line 80 didn't jump to line 91 because the condition on line 80 was always true

81 if not self.resp_model: 81 ↛ 82line 81 didn't jump to line 82 because the condition on line 81 was never true

82 raise ValueError('Response model is not set for this handler.') 

83 

84 data = self.get_response_data(body) 

85 

86 assert issubclass(self.resp_model, BaseModel) 

87 api_resp.body.data = self.resp_model.model_validate(data, from_attributes=True) 

88 else: 

89 # Error body gözlənilən formatda olmaya bilər (məs., HTML xəta səhifəsi); 

90 # bu halda status kodu və `ok` field-i xətanı bildirir. 

91 try: 

92 api_resp.body.error = ErrorResponseBodySchema.model_validate( 

93 body, 

94 from_attributes=True, 

95 ) 

96 except (ValueError, TypeError): 

97 api_resp.body.error = None 

98 

99 return api_resp 

100 

101 def get_response_data(self, response_json: dict) -> dict: 

102 """`self.data_key` varsa, o key-dəki datanı götürmək""" 

103 if not self.data_key: 

104 raise NotImplementedError("Subclasses must define 'data_key'") 

105 

106 return response_json.get(self.data_key, {}) 

107 

108 

109class CreateOrderPayloadHandler(BasePayloadHandler): 

110 req_model = CreateOrderRequestSchema 

111 resp_model = CreateOrderResponseSchema 

112 data_key = 'order' 

113 

114 

115class OrderInformationPayloadHandler(BasePayloadHandler): 

116 req_model = OrderInformationRequestSchema 

117 resp_model = OrderInformationResponseSchema 

118 data_key = 'order' 

119 

120 def post_handle_payload(self, data): 

121 return data 

122 

123 

124class DetailedOrderInformationPayloadHandler(BasePayloadHandler): 

125 req_model = OrderInformationRequestSchema 

126 resp_model = DetailedOrderInformationResponseSchema 

127 data_key = 'order' 

128 

129 def post_handle_payload(self, data): 

130 return data 

131 

132 

133class RefundOrderPayloadHandler(BasePayloadHandler): 

134 req_model = RefundOrderRequestSchema 

135 resp_model = RefundOrderResponseSchema 

136 data_key = 'tran' 

137 

138 

139class SaveCardPayloadHandler(BasePayloadHandler): 

140 req_model = SaveCardRequestSchema 

141 resp_model = CreateOrderResponseSchema 

142 data_key = 'order' 

143 

144 

145class PayAndSaveCardPayloadHandler(BasePayloadHandler): 

146 req_model = PayAndSaveCardRequestSchema 

147 resp_model = CreateOrderResponseSchema 

148 data_key = 'order' 

149 

150 

151class FullReverseOrderPayloadHandler(BasePayloadHandler): 

152 req_model = FullReverseOrderRequestSchema 

153 resp_model = FullReverseOrderResponseSchema 

154 data_key = 'tran' 

155 

156 

157class ClearingOrderPayloadHandler(BasePayloadHandler): 

158 req_model = ClearingOrderRequestSchema 

159 resp_model = ClearingOrderResponseSchema 

160 data_key = 'tran' 

161 

162 

163class PartialReverseOrderPayloadHandler(BasePayloadHandler): 

164 req_model = PartialReverseOrderRequestSchema 

165 resp_model = PartialReverseOrderResponseSchema 

166 data_key = 'tran' 

167 

168 

169class OrderWithSavedCardPayloadHandler(BasePayloadHandler): 

170 req_model = OrderWithSavedCardRequestSchema 

171 resp_model = CreateOrderResponseSchema 

172 data_key = 'order' 

173 

174 

175class LinkCardTokenPayloadHandler(BasePayloadHandler): 

176 req_model = LinkCardTokenRequestSchema 

177 resp_model = LinkCardTokenResponseSchema 

178 data_key = 'order' 

179 

180 def post_handle_payload(self, data): 

181 # `dict` qaytarırıq (əvvəlki `json.dumps(...)` string qaytarırdı, bu da httpx 

182 # tərəfindən ikiqat JSON-encode olunub, body-ni korlayırdı). 

183 return { 

184 'order': {'initiationEnvKind': 'Server'}, 

185 'token': {'storedId': data['token']}, 

186 } 

187 

188 

189class ProcessPaymentWithSavedCardPayloadHandler(BasePayloadHandler): 

190 req_model = ProcessPaymentWithSavedCardRequestSchema 

191 resp_model = ProcessPaymentWithSavedCardResponseSchema 

192 data_key = 'tran'