Coverage for api/handlers/orders/confirm_orders.py: 17%

181 statements  

« prev     ^ index     » next       coverage.py v7.6.2, created at 2024-10-10 03:02 +0300

1import asyncio 

2import datetime 

3from typing import Any 

4 

5from bson import ObjectId 

6from database.entity import update_etag_and_text_search 

7from database.integration_1c.savers import save_entity_to_updates, update_in_1c 

8from database.orders import get_confirmation_interval, update_status_and_return 

9from errors import log_error 

10from exceptions import ( 

11 BadParameterHTTPError, 

12 NotAcceptableHTTPError, 

13 NotFoundHTTPError, 

14) 

15from fastapi import HTTPException 

16from handlers.authorization.check_role import has_role 

17from handlers.authorization.company_employee_access import ( 

18 assigned_subsidiary_or_logistician_for_order_query, 

19 flexible_company_assertion_query, 

20) 

21from handlers.authorization.confidential import ( 

22 hide_dict_fields_from_carrier, 

23 hide_model_fields_from_carrier, 

24) 

25from handlers.grabbers.orders import orders_data_grabber 

26from mongodb import orders_col 

27from operations.assignment import data_from_assigned, notify_if_assigned 

28from operations.orders import ( 

29 clear_documents, 

30 fill_by_insert_oid_resources_docs, 

31 generate_draft_document, 

32 pre_process_to_exchange_auction_end_time, 

33 update_documents_data, 

34) 

35from pymongo import ReturnDocument 

36from services.notifications.director import notification_api 

37from services.recommendations import suggestion_api 

38from sotrans_models.models.misc.document import RequestDocumentType 

39from sotrans_models.models.orders.order import ( 

40 ConfirmedOrderUpdateModel, 

41 OrderDBModel, 

42 OrderStatus, 

43 OrderUpdateModel, 

44) 

45from sotrans_models.models.responses import ( 

46 AucEndNotificationCall, 

47 GenericGetListResponse, 

48) 

49from sotrans_models.models.roles import SotransRole 

50from sotrans_models.models.users import SotransOIDCUserModel 

51from sotrans_models.utils.text_mappers import get_orders_text_search 

52from starlette import status 

53from utils.check_carriers_resources import ( 

54 check_order_approved, 

55 check_resources_are_same, 

56 check_resources_for_generation, 

57 check_verification, 

58) 

59from utils.data_grabber import ( 

60 BaseGetListQueryParams, 

61 BaseGetOneQueryParams, 

62 adjust_search_query, 

63 update_search_query, 

64) 

65from utils.dt_utils import get_current_datetime 

66from utils.helper import add_prices_to_update, etag_detalizer, get_org_oid 

67 

68 

69async def on_put_to_active( 

70 order_id: ObjectId, 

71 user: SotransOIDCUserModel, 

72 order: OrderUpdateModel | None, 

73) -> OrderDBModel | dict[str, Any]: 

74 restriction_q = assigned_subsidiary_or_logistician_for_order_query(user) 

75 restriction_q |= {OrderDBModel.status: OrderStatus.unverified.value} 

76 etag_q = {"etag": order.etag} if order and order.etag else {} 

77 order_data = await orders_col.collection.find_one( 

78 restriction_q | {"id": order_id} | etag_q 

79 ) 

80 if not order_data: 

81 await etag_detalizer( 

82 orders_col, etag_q, {"id": order_id} | restriction_q 

83 ) 

84 raise NotFoundHTTPError("заказ") 

85 found_order = OrderDBModel(**order_data) 

86 if not check_order_approved(found_order): 

87 raise NotAcceptableHTTPError("order_request отсутствует") 

88 if not (found_order.end_price or (order and order.end_price)): 

89 raise BadParameterHTTPError("нет конечной цены") 

90 if found_order.documents: 

91 for document in found_order.documents: 

92 if document.type == RequestDocumentType.order_request: 

93 continue 

94 check_verification( 

95 document.model_dump(), f'Документ {document.name or ""}' 

96 ) 

97 update_data = await data_from_assigned(order) 

98 if order: 

99 add_prices_to_update( 

100 order_data, update_data, order.start_price, order.end_price 

101 ) 

102 if order.end_price: 

103 update_data |= {OrderDBModel.end_price: order.end_price} 

104 order_updated = await update_status_and_return( 

105 order_id, 

106 OrderStatus.active, 

107 set_=update_data, 

108 restriction=restriction_q | etag_q, 

109 ) 

110 order_resp_model = OrderDBModel(**order_updated) 

111 asyncio.create_task(notification_api.order_is_active(order_resp_model)) 

112 updated_1c = await update_in_1c( 

113 order_id, orders_col.collection_name, order_resp_model 

114 ) 

115 if not updated_1c: 

116 asyncio.create_task( 

117 save_entity_to_updates(orders_col.collection_name, order_updated) 

118 ) 

119 notify_if_assigned(order, order_resp_model) 

120 return order_updated 

121 

122 

123async def on_apply_for_draft( 

124 order_id: ObjectId, 

125 order: ConfirmedOrderUpdateModel, 

126 user: SotransOIDCUserModel, 

127) -> dict[str, Any] | OrderDBModel: 

128 companies_employee = has_role(user, SotransRole.company_logistician) 

129 restriction_q = { 

130 "$or": [ 

131 { 

132 OrderDBModel.status: OrderStatus.confirmed.value, 

133 OrderDBModel.confirmation_end_time: { 

134 "$gte": datetime.datetime.utcnow() 

135 }, 

136 }, 

137 { 

138 OrderDBModel.status: { 

139 "$in": [ 

140 OrderStatus.unverified.value, 

141 OrderStatus.active.value, 

142 ] 

143 } 

144 }, 

145 ] 

146 } 

147 if not companies_employee: 

148 org_id = get_org_oid(user) 

149 restriction_q |= {OrderDBModel.carrier.id: org_id} 

150 order_to_update_data = await fill_by_insert_oid_resources_docs( 

151 {}, order, order_id, with_carrier=True 

152 ) 

153 etag_q = {"etag": order.etag} if order.etag else {} 

154 order_in = await orders_col.collection.find_one( 

155 {"id": order_id} | restriction_q | etag_q, 

156 ) 

157 if order_in is None: 

158 await etag_detalizer( 

159 orders_col, etag_q, {"id": order_id} | restriction_q 

160 ) 

161 raise NotFoundHTTPError("заказ") 

162 if check_resources_are_same(order_in, order_to_update_data): 

163 raise BadParameterHTTPError("ресурсы совпадают") 

164 order_up = OrderDBModel(**order_to_update_data) 

165 model_to_update = OrderDBModel( 

166 **{**order_in, **order_up.model_dump(exclude_unset=True)} 

167 ) 

168 check_resources_for_generation(model_to_update) 

169 documents_draft = await generate_draft_document(model_to_update) 

170 back_to_confirmed = {"documents": None} 

171 await clear_documents(order_in.get("documents")) 

172 if model_to_update.status != OrderStatus.confirmed: 

173 back_to_confirmed |= { 

174 OrderDBModel.status: OrderStatus.confirmed.value, 

175 OrderDBModel.confirmation_end_time: get_current_datetime() 

176 + datetime.timedelta(minutes=await get_confirmation_interval()), 

177 } 

178 updated_path = await orders_col.collection.find_one_and_update( 

179 {"id": order_id}, 

180 { 

181 "$set": order_to_update_data 

182 | back_to_confirmed 

183 | {OrderDBModel.document_draft: documents_draft.model_dump()} 

184 }, 

185 return_document=ReturnDocument.AFTER, 

186 ) 

187 if updated_path is None: 

188 log_error( 

189 f"Провалена попытка закрепить документ [{documents_draft.model_dump(format_ids=False)}] за заказом [{order_id}]" 

190 ) 

191 raise HTTPException(status.HTTP_410_GONE, "закрепление не удалось") 

192 await update_etag_and_text_search( 

193 updated_path, 

194 orders_col, 

195 OrderDBModel, 

196 get_orders_text_search, 

197 ) 

198 if not companies_employee: 

199 hide_dict_fields_from_carrier(updated_path) 

200 return updated_path 

201 

202 

203async def on_draw_up_order( 

204 order_id: ObjectId, 

205 user: SotransOIDCUserModel, 

206 order: ConfirmedOrderUpdateModel, 

207) -> OrderDBModel | dict[str, Any]: 

208 if not order.documents: 

209 raise BadParameterHTTPError("документы не приложены") 

210 companies_employee = has_role(user, SotransRole.company_logistician) 

211 restriction_q = { 

212 OrderDBModel.confirmation_end_time: { 

213 "$gte": datetime.datetime.utcnow() 

214 } 

215 } 

216 if not companies_employee: 

217 org_id = get_org_oid(user) 

218 restriction_q |= {OrderDBModel.carrier.id: org_id} 

219 etag_q = {} if order.etag is None else {"etag": order.etag} 

220 update_data = await data_from_assigned(order) 

221 update_data |= {OrderDBModel.status: OrderStatus.unverified.value} 

222 await update_documents_data(order_id, order, update_data, True) 

223 updated = await orders_col.collection.find_one_and_update( 

224 {"id": order_id} | restriction_q | etag_q, 

225 {"$set": update_data}, 

226 return_document=ReturnDocument.AFTER, 

227 ) 

228 if not updated: 

229 await etag_detalizer( 

230 orders_col, etag_q, {"id": order_id} | restriction_q 

231 ) 

232 raise NotFoundHTTPError("order") 

233 await update_etag_and_text_search( 

234 updated, 

235 orders_col, 

236 OrderDBModel, 

237 get_orders_text_search, 

238 ) 

239 up_model = OrderDBModel(**updated) 

240 notify_if_assigned(order, up_model) 

241 notification_api.company_order_unverified(up_model) 

242 if not companies_employee: 

243 hide_model_fields_from_carrier(up_model) 

244 return up_model 

245 

246 

247async def on_put_back_to_auc( 

248 order_id: ObjectId, 

249 user: SotransOIDCUserModel, 

250 order: OrderUpdateModel, 

251) -> OrderDBModel | dict[str, Any]: 

252 if not order.start_price: 

253 raise BadParameterHTTPError("Нет начальной цены") 

254 restriction_q = await flexible_company_assertion_query(user) 

255 etag_q = {} if order.etag is None else {"etag": order.etag} 

256 order_data = await orders_col.collection.find_one( 

257 {"id": order_id} | restriction_q | etag_q 

258 ) 

259 if not order_data: 

260 await etag_detalizer( 

261 orders_col, etag_q, {"id": order_id} | restriction_q 

262 ) 

263 raise NotFoundHTTPError("заказ") 

264 

265 update_data = await data_from_assigned(order) 

266 

267 pre_process_to_exchange_auction_end_time(order_data, order, update_data) 

268 add_prices_to_update( 

269 order_data, update_data, order.start_price, order.end_price 

270 ) 

271 update_data[OrderDBModel.start_price] = order.start_price 

272 if order.auction_end_time: 

273 update_data[OrderDBModel.auction_end_time] = order.auction_end_time 

274 restriction_q |= { 

275 OrderDBModel.status: { 

276 "$in": [ 

277 s.value 

278 for s in ( 

279 OrderStatus.confirmed, 

280 OrderStatus.unverified, 

281 OrderStatus.reserved, 

282 ) 

283 ] 

284 } 

285 } 

286 payload = update_data | { 

287 OrderDBModel.status: OrderStatus.exchange.value, 

288 **{ 

289 k: None 

290 for k in ( 

291 OrderDBModel.carrier, 

292 OrderDBModel.truck, 

293 OrderDBModel.trailer, 

294 OrderDBModel.driver, 

295 OrderDBModel.documents[0], 

296 OrderDBModel.best_bid, 

297 ) 

298 }, 

299 } 

300 updated_order = await orders_col.collection.find_one_and_update( 

301 restriction_q | {"id": order_id}, 

302 {"$set": payload}, 

303 return_document=ReturnDocument.AFTER, 

304 ) 

305 if not updated_order: 

306 raise NotFoundHTTPError("order") 

307 om = OrderDBModel(**updated_order) 

308 await update_etag_and_text_search( 

309 updated_order, orders_col, OrderDBModel, get_orders_text_search 

310 ) 

311 asyncio.create_task( 

312 suggestion_api.create_target_and_recommendation_order(om, target=False) 

313 ) 

314 notify_if_assigned(order, om) 

315 return om 

316 

317 

318async def on_get_confirmed( 

319 user: SotransOIDCUserModel, params: BaseGetListQueryParams 

320) -> GenericGetListResponse[OrderDBModel]: 

321 is_carrier = not has_role( 

322 user, SotransRole.company_logistician 

323 ) and has_role(user, SotransRole.carrier_logistician) 

324 if is_carrier: 

325 carriers_restriction_query = { 

326 OrderDBModel.carrier.id: user.organization_id 

327 } 

328 params.where = update_search_query( 

329 params.where, carriers_restriction_query 

330 ) 

331 if not (params.where and OrderStatus.unverified.value in params.where): 

332 params.where = adjust_search_query( 

333 params.where, OrderDBModel.status, {OrderStatus.confirmed.value} 

334 ) 

335 orders = await orders_data_grabber.get_list(params, user) 

336 if is_carrier: 

337 for order in orders.items: 

338 hide_model_fields_from_carrier(order) 

339 return orders 

340 

341 

342async def on_get_one_confirmed( 

343 order_id: ObjectId, 

344 user: SotransOIDCUserModel, 

345 params: BaseGetOneQueryParams, 

346) -> dict[str, Any] | OrderDBModel: 

347 status_restriction = { 

348 OrderDBModel.status: { 

349 "$in": [OrderStatus.confirmed.value, OrderStatus.unverified.value] 

350 } 

351 } 

352 if has_role(user, SotransRole.company_director): 

353 return await orders_data_grabber.get_one_by_id_with_pattern( 

354 order_id, params, status_restriction 

355 ) 

356 if has_role(user, SotransRole.company_logistician): 

357 restriction_q = await flexible_company_assertion_query(user) 

358 return await orders_data_grabber.get_one_by_id_with_pattern( 

359 order_id, params, status_restriction | restriction_q 

360 ) 

361 # for carriers 

362 org_id = get_org_oid(user) 

363 return await orders_data_grabber.get_one_by_id_with_pattern( 

364 order_id, 

365 params, 

366 status_restriction | {OrderDBModel.carrier.id: org_id}, 

367 [hide_model_fields_from_carrier], 

368 ) 

369 

370 

371async def on_confirmed_to_appointment( 

372 order_id: ObjectId, 

373 user: SotransOIDCUserModel, 

374 order: OrderUpdateModel | None, 

375) -> dict[str, Any] | OrderDBModel: 

376 update_data = await data_from_assigned(order) 

377 restriction_q = await flexible_company_assertion_query(user) 

378 etag_q = {"etag": order.etag} if order and order.etag else {} 

379 updated = await update_status_and_return( 

380 order_id, 

381 OrderStatus.appointment, 

382 set_=update_data, 

383 restriction=restriction_q | etag_q, 

384 ) 

385 asyncio.create_task(suggestion_api.remove_order(order_id, target=True)) 

386 notify_if_assigned(order, updated) 

387 return updated 

388 

389 

390async def on_notify_winner(call_data: AucEndNotificationCall): 

391 orders = await orders_col.find_batch({"_id": {"$in": call_data.order_ids}}) 

392 tasks = [ 

393 notification_api.order_reserved(OrderDBModel(**order)) 

394 for order in orders 

395 ] 

396 await asyncio.gather(*tasks) 

397 

398 

399async def on_warn_confirmation_ends(): 

400 current_time = ( 

401 get_current_datetime() 

402 + datetime.datetime.utcnow().astimezone().tzinfo.utcoffset(None) 

403 ) 

404 ends_soon = current_time + datetime.timedelta(minutes=20) 

405 orders = await orders_col.find_batch( 

406 { 

407 OrderDBModel.confirmation_end_time: { 

408 "$lt": ends_soon, 

409 "$gt": current_time, 

410 } 

411 } 

412 ) 

413 for order in orders: 

414 order_model = OrderDBModel(**order) 

415 minutes_left = int( 

416 ( 

417 order_model.confirmation_end_time 

418 - current_time.replace(tzinfo=None) 

419 ).seconds 

420 / 60 

421 ) 

422 notification_api.order_confirmed_timeout(order_model, minutes_left)