import lexorank from lexorank import Bucket, middle from sqlalchemy import select, update, insert, delete, desc from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import joinedload from enums.service import ServiceType from models import Service, ServiceCategory, ServicePriceRange, ServicesKit, services_kit_services from schemas.service import * from services.base import BaseService from utils.list_utils import previous_current_next class ServiceService(BaseService): async def get_all(self, with_placeholders: bool) -> ServiceGetAllResponse: query = await ( self.session .scalars( select( Service ) .options( joinedload(Service.category) ) .order_by( Service.rank ) .filter( Service.is_deleted == False ) ) ) services_raw = query.all() services = [] for service in services_raw: services.append(ServiceSchema.model_validate(service)) if not with_placeholders: return ServiceGetAllResponse(services=services) category_ids = list(set([service.category_id for service in services_raw])) rest_categories = await ( self.session .scalars( select( ServiceCategory ) .where( ServiceCategory.id.notin_(category_ids), ServiceCategory.is_deleted == False ) ) ) for category in rest_categories: for service_type in [ServiceType.DEAL_SERVICE, ServiceType.PRODUCT_SERVICE]: services.append(ServiceSchema( id=0, name='Пусто', category=ServiceCategorySchema.model_validate(category), price=0, service_type=service_type, price_ranges=[], cost=0, rank='', is_placeholder=True )) return ServiceGetAllResponse(services=services) async def get_latest_rank_in_category(self, category_id: int, service_type: ServiceType) -> str: stmt = ( select(Service.rank) .where(Service.category_id == category_id) .where(Service.service_type == service_type) .order_by(desc(Service.rank)) .limit(1) ) return await self.session.scalar(stmt) or '' async def create(self, request: ServiceCreateRequest) -> ServiceCreateResponse: try: raw_service = request.service service_dict = raw_service.model_dump() service_dict['category_id'] = raw_service.category.id del service_dict['id'] del service_dict['category'] del service_dict['price_ranges'] del service_dict['is_placeholder'] service_type = ServiceType(raw_service.service_type) category_id = raw_service.category.id latest_rank = await self.get_latest_rank_in_category(category_id, service_type) if not latest_rank: latest_rank = middle(Bucket.BUCEKT_0) else: latest_rank = lexorank.parse(latest_rank) service_rank = str(latest_rank.next()) service_dict['rank'] = service_rank service = Service(**service_dict) self.session.add(service) await self.session.flush() price_ranges = request.service.price_ranges for price_range in price_ranges: price_range_dict = price_range.model_dump() price_range_dict['service_id'] = service.id del price_range_dict['id'] price_range_obj = ServicePriceRange(**price_range_dict) self.session.add(price_range_obj) await self.session.commit() return ServiceCreateResponse(ok=True, message="Услуга успешно создана") except Exception as e: return ServiceCreateResponse(ok=False, message=f"Неудалось создать услугу, ошибка: {e}") async def update(self, request: ServiceUpdateRequest) -> ServiceUpdateResponse: try: raw_service = request.data service = await (self.session.get(Service, raw_service.id)) if not service: return ServiceUpdateResponse(ok=False, message="Услуга не найдена") prev_category_id = service.category_id new_category_id = request.data.category.id service_dict = raw_service.dict() service_dict['category_id'] = raw_service.category.id del service_dict['category'] del service_dict['price_ranges'] del service_dict['is_placeholder'] if prev_category_id != new_category_id: latest_rank = await self.get_latest_rank_in_category(new_category_id, ServiceType(raw_service.service_type)) if not latest_rank: latest_rank = middle(Bucket.BUCEKT_0) else: latest_rank = lexorank.parse(latest_rank) service_dict['rank'] = str(latest_rank.next()) await self.session.execute( update(Service) .where(Service.id == raw_service.id) .values(**service_dict) ) # checking if old price ranges are still in the request request_price_range_ids = [price_range.id for price_range in raw_service.price_ranges if price_range.id] price_ranges_to_delete = [] for price_range in service.price_ranges: if price_range.id not in request_price_range_ids: price_ranges_to_delete.append(price_range) for price_range in price_ranges_to_delete: await self.session.delete(price_range) await self.session.flush() for price_range in raw_service.price_ranges: price_range_dict = price_range.dict() price_range_dict['service_id'] = raw_service.id if price_range.id: await self.session.execute( update(ServicePriceRange) .where(ServicePriceRange.id == price_range.id) .values(**price_range_dict) ) else: del price_range_dict['id'] price_range_obj = ServicePriceRange(**price_range_dict) self.session.add(price_range_obj) await self.session.commit() return ServiceUpdateResponse(ok=True, message="Услуга успешно обновлена") except Exception as e: return ServiceUpdateResponse(ok=False, message=f"Неудалось обновить услугу, ошибка: {e}") async def delete(self, request: ServiceDeleteRequest) -> ServiceDeleteResponse: try: service = await (self.session .scalar(select(Service) .filter(Service.id == request.service_id))) if not service: return ServiceDeleteResponse(ok=False, message="Услуга не найдена") await self.session.delete(service) await self.session.commit() return ServiceDeleteResponse(ok=True, message="Услуга успешно удалена") except IntegrityError: await self.session.rollback() service = await (self.session .scalar(select(Service) .filter(Service.id == request.service_id))) service.is_deleted = True await self.session.commit() return ServiceDeleteResponse(ok=True, message="Услуга успешно удалена") except Exception as e: return ServiceDeleteResponse(ok=False, message=f"Неудалось удалить услугу, ошибка: {e}") async def create_category(self, request: ServiceCreateCategoryRequest) -> ServiceCreateCategoryResponse: try: raw_category = request.category category_dict = raw_category.model_dump() del category_dict['id'] last_deal_service_rank = await self.session.scalar( select(ServiceCategory.deal_service_rank) .order_by(ServiceCategory.deal_service_rank.desc()) .limit(1) ) last_product_service_rank = await self.session.scalar( select(ServiceCategory.product_service_rank) .order_by(ServiceCategory.product_service_rank.desc()) .limit(1) ) last_product_service_rank = lexorank.parse( last_product_service_rank) if last_product_service_rank else middle( Bucket.BUCEKT_0) last_deal_service_rank = lexorank.parse(last_deal_service_rank) if last_deal_service_rank else middle( Bucket.BUCEKT_0) last_deal_service_rank = last_deal_service_rank.next() last_product_service_rank = last_product_service_rank.next() category_dict['deal_service_rank'] = str(last_deal_service_rank) category_dict['product_service_rank'] = str(last_product_service_rank) category = ServiceCategory(**category_dict) self.session.add(category) await self.session.commit() return ServiceCreateCategoryResponse(ok=True, message="Категория успешно создана") except Exception as e: return ServiceCreateCategoryResponse(ok=False, message=f"Неудалось создать категорию, ошибка: {e}") async def get_all_categories(self) -> ServiceGetAllCategoriesResponse: query = await (self.session .scalars(select(ServiceCategory) .order_by(ServiceCategory.id))) categories = [] for category in query.all(): categories.append(ServiceCategorySchema.model_validate(category)) return ServiceGetAllCategoriesResponse(categories=categories) async def get_kit_by_name(self, name: str) -> Optional[ServicesKit]: return await self.session.scalar(select(ServicesKit).where(ServicesKit.name == name)) async def get_kit_by_id(self, kit_id: int) -> Optional[ServicesKit]: return await self.session.scalar(select(ServicesKit).where(ServicesKit.id == kit_id)) async def get_all_kits(self) -> GetAllServicesKitsResponse: stmt = ( select( ServicesKit ) .order_by( ServicesKit.id.desc() ) ) kits = (await self.session.scalars(stmt)).all() kits_schemas = GetServiceKitSchema.from_orm_list(kits) return GetAllServicesKitsResponse(services_kits=kits_schemas) async def create_kit(self, request: CreateServicesKitRequest) -> CreateServicesKitResponse: try: if await self.get_kit_by_name(request.data.name): return CreateServicesKitResponse(ok=False, message='Набор услуг с таким названием уже существует') base_fields = request.data.model_dump_parent() kit = ServicesKit(**base_fields) self.session.add(kit) await self.session.flush() # Appending services insert_data = [] for service_id in request.data.services_ids: insert_data.append({ 'service_id': service_id, 'services_kit_id': kit.id }) if insert_data: await self.session.execute( insert(services_kit_services), insert_data ) await self.session.flush() await self.session.commit() return CreateServicesKitResponse(ok=True, message='Набор услуг успешно создан') except Exception as e: return CreateServicesKitResponse(ok=False, message=str(e)) async def update_kit(self, request: UpdateServicesKitRequest) -> UpdateServicesKitResponse: try: kit = await self.get_kit_by_id(request.data.id) if not kit: return UpdateServicesKitResponse(ok=False, message='Указанный набор услуг не существует') base_fields = request.data.model_dump_parent() stmt = update(ServicesKit).values(**base_fields).where(ServicesKit.id == request.data.id) await self.session.execute(stmt) await self.session.flush() # Deleting previous services stmt = ( delete( services_kit_services ).where( services_kit_services.c.services_kit_id == kit.id ) ) await self.session.execute(stmt) await self.session.flush() insert_data = [] for service_id in request.data.services_ids: insert_data.append({ 'service_id': service_id, 'services_kit_id': kit.id }) if insert_data: await self.session.execute( insert(services_kit_services), insert_data ) await self.session.flush() await self.session.commit() return UpdateServicesKitResponse(ok=True, message='Набор услуг успешно обновлен') except Exception as e: return UpdateServicesKitResponse(ok=False, message=str(e)) async def reorder(self, request: ServiceReorderRequest) -> ServiceReorderResponse: try: draining_service = await (self.session.get(Service, request.draining_service_id)) hovered_service = await (self.session.get(Service, request.hovered_service_id)) if not draining_service or not hovered_service: return ServiceReorderResponse(ok=False, message="Услуга не найдена") if draining_service.category_id == hovered_service.category_id: temp = draining_service.rank draining_service.rank = hovered_service.rank hovered_service.rank = temp else: return ServiceReorderResponse(ok=False, message="Нельзя перемещать услуги между категориями") await self.session.commit() return ServiceReorderResponse(ok=True, message="Услуги успешно пересортированы") except Exception as e: return ServiceReorderResponse(ok=False, message=f"Неудалось найти услугу, ошибка: {e}") async def reorder_category(self, request: ServiceCategoryReorderRequest) -> ServiceCategoryReorderResponse: try: if request.move_down and request.move_up: return ServiceCategoryReorderResponse(ok=False, message="Невозможно выполнить два действия одновременно") if not request.move_down and not request.move_up: return ServiceCategoryReorderResponse(ok=False, message="Не указано действие") if request.service_type == ServiceType.DEAL_SERVICE: order_by = ServiceCategory.deal_service_rank rank_field = 'deal_service_rank' else: order_by = ServiceCategory.product_service_rank rank_field = 'product_service_rank' service_categories = await ( self.session .scalars(select(ServiceCategory) .order_by(order_by)) ) service_categories = service_categories.all() for prv, cur, nxt in previous_current_next(service_categories): if request.category_id == cur.id: if request.move_down: if nxt: temp = getattr(cur, rank_field) # cur.rank = nxt.rank setattr(cur, rank_field, getattr(nxt, rank_field)) # nxt.rank = temp setattr(nxt, rank_field, temp) else: return ServiceCategoryReorderResponse(ok=False, message="Невозможно переместить вниз") elif request.move_up: if prv: temp = getattr(cur, rank_field) # cur.rank = prv.rank setattr(cur, rank_field, getattr(prv, rank_field)) # prv.rank = temp setattr(prv, rank_field, temp) else: return ServiceCategoryReorderResponse(ok=False, message="Невозможно переместить вверх") await self.session.commit() return ServiceCategoryReorderResponse(ok=True, message="Категории успешно пересортированы") except Exception as e: return ServiceCategoryReorderResponse(ok=False, message=f"Неудалось пересортировать категорию, ошибка: {e}") async def update_category(self, request: ServiceUpdateCategoryRequest) -> ServiceUpdateResponse: try: raw_category = request.category category = await (self.session.get(ServiceCategory, raw_category.id)) if not category: return ServiceUpdateResponse(ok=False, message="Категория не найдена") category_dict = raw_category.dict() del category_dict['id'] await self.session.execute( update(ServiceCategory) .where(ServiceCategory.id == raw_category.id) .values(**category_dict) ) await self.session.commit() return ServiceUpdateResponse(ok=True, message="Категория успешно обновлена") except Exception as e: return ServiceUpdateResponse(ok=False, message=f"Неудалось обновить категорию, ошибка: {e}") async def delete_category(self, request: ServiceDeleteCategoryRequest) -> ServiceDeleteResponse: try: active_services = await self.session.scalars( select( Service ) .where( Service.category_id == request.category_id, Service.is_deleted == False ) ) if active_services.first(): return ServiceDeleteResponse(ok=False, message="Категория используется в услугах") # Check if category is used in deleted services deleted_services = await self.session.scalars( select( Service ) .where( Service.category_id == request.category_id, Service.is_deleted == True ) ) category = await self.session.scalar( select( ServiceCategory ) .filter( ServiceCategory.id == request.category_id ) ) if not category: return ServiceDeleteResponse(ok=False, message="Категория не найдена") if deleted_services.first(): category.is_deleted = True else: await self.session.delete(category) await self.session.commit() return ServiceDeleteResponse(ok=True, message="Категория успешно удалена") except Exception as e: return ServiceDeleteResponse(ok=False, message=f"Неудалось удалить категорию, ошибка: {e}")