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
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-10 03:02 +0300
1import asyncio
2import datetime
3from typing import Any
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
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
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
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
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("заказ")
265 update_data = await data_from_assigned(order)
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
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
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 )
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
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)
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)