From 30001b993ef5dd32261418fe21c181871c006f86 Mon Sep 17 00:00:00 2001 From: vchikalkin Date: Fri, 16 May 2025 19:14:00 +0300 Subject: [PATCH] refactor orders api --- apps/web/actions/api/lib/service.ts | 8 -- apps/web/actions/api/orders.ts | 18 ++++ apps/web/actions/orders.ts | 77 -------------- .../orders/components/back-button.tsx | 2 +- .../orders/components/submit-button.tsx | 12 ++- .../schedule/components/order-card.tsx | 5 +- apps/web/hooks/api/orders.ts | 15 +++ apps/web/hooks/orders/index.ts | 24 ----- packages/graphql/api/index.ts | 5 - packages/graphql/api/order.ts | 21 ---- packages/graphql/api/orders.ts | 100 ++++++++++++++++++ packages/graphql/config/token.ts | 2 +- 12 files changed, 147 insertions(+), 142 deletions(-) create mode 100644 apps/web/actions/api/orders.ts delete mode 100644 apps/web/actions/orders.ts create mode 100644 apps/web/hooks/api/orders.ts delete mode 100644 apps/web/hooks/orders/index.ts delete mode 100644 packages/graphql/api/index.ts delete mode 100644 packages/graphql/api/order.ts create mode 100644 packages/graphql/api/orders.ts diff --git a/apps/web/actions/api/lib/service.ts b/apps/web/actions/api/lib/service.ts index 9f99685..ca34466 100644 --- a/apps/web/actions/api/lib/service.ts +++ b/apps/web/actions/api/lib/service.ts @@ -1,15 +1,7 @@ -/* eslint-disable canonical/id-match */ import { authOptions } from '@/config/auth'; import { type BaseService } from '@repo/graphql/api/base'; import { getServerSession } from 'next-auth'; -export async function _temporaryGetCustomer() { - const session = await getServerSession(authOptions); - if (!session?.user?.telegramId) throw new Error('Unauthorized'); - - return { telegramId: session.user.telegramId }; -} - export function useService(service: T) { return async function () { const session = await getServerSession(authOptions); diff --git a/apps/web/actions/api/orders.ts b/apps/web/actions/api/orders.ts new file mode 100644 index 0000000..286a719 --- /dev/null +++ b/apps/web/actions/api/orders.ts @@ -0,0 +1,18 @@ +'use server'; + +import { useService } from './lib/service'; +import { OrdersService } from '@repo/graphql/api/orders'; + +const getServicesService = useService(OrdersService); + +export async function createOrder(...variables: Parameters) { + const service = await getServicesService(); + + return service.createOrder(...variables); +} + +export async function getOrder(...variables: Parameters) { + const service = await getServicesService(); + + return service.getOrder(...variables); +} diff --git a/apps/web/actions/orders.ts b/apps/web/actions/orders.ts deleted file mode 100644 index 1ed6d25..0000000 --- a/apps/web/actions/orders.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* eslint-disable canonical/id-match */ -'use server'; -// eslint-disable-next-line sonarjs/no-internal-api-use -import type * as ApolloTypes from '../../../packages/graphql/node_modules/@apollo/client/core'; -import { getCustomer, getCustomerMasters } from './api/customers'; -import { _temporaryGetCustomer } from './api/lib/service'; -import { getService } from './api/services'; -import { getSlot } from './api/slots'; -import * as api from '@repo/graphql/api'; -import { Enum_Customer_Role, Enum_Slot_State } from '@repo/graphql/types'; -import { formatDate, formatTime, sumTime } from '@repo/graphql/utils/datetime-format'; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type FixTypescriptCringe = ApolloTypes.FetchResult; - -export const getOrder = api.getOrder; - -type OrderInput = { - clientId: string; - date: Date; - serviceId: string; - slotId: string; - time: string; -}; - -export async function createOrder(input: OrderInput) { - const variables = await _temporaryGetCustomer(); - const { customer } = await getCustomer(variables); - - if (!customer) { - throw new Error('Missing customer'); - } - - if (!input.slotId) { - throw new Error('Missing slot'); - } - - const { slot } = await getSlot({ documentId: input.slotId }); - - if (slot?.state === Enum_Slot_State.Closed) { - throw new Error('Slot is closed'); - } - - if (customer.role === Enum_Customer_Role.Client) { - if (customer.documentId !== input.clientId) { - throw new Error('Invalid client'); - } - - const masterId = slot?.master?.documentId; - - const masters = await getCustomerMasters(customer); - - if (!masters.customers.some((master) => master?.documentId === masterId)) { - throw new Error('Invalid master'); - } - } - - if ( - customer.role === Enum_Customer_Role.Master && - slot?.master?.documentId !== customer.documentId - ) { - throw new Error('Invalid master'); - } - - const { service } = await getService({ documentId: input.serviceId }); - - const endTime = sumTime(input.time, service?.duration); - const payload = { - client: input.clientId, - date: formatDate(input.date).db(), - services: [input.serviceId], - slot: input.slotId, - time_end: formatTime(endTime).db(), - time_start: formatTime(input.time).db(), - }; - return api.createOrder(payload); -} diff --git a/apps/web/components/orders/components/back-button.tsx b/apps/web/components/orders/components/back-button.tsx index df5f0f7..0253a7a 100644 --- a/apps/web/components/orders/components/back-button.tsx +++ b/apps/web/components/orders/components/back-button.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useOrderCreate } from '@/hooks/orders'; +import { useOrderCreate } from '@/hooks/api/orders'; import { useOrderStore } from '@/stores/order'; import { Button } from '@repo/ui/components/ui/button'; diff --git a/apps/web/components/orders/components/submit-button.tsx b/apps/web/components/orders/components/submit-button.tsx index 35a660f..fa3878b 100644 --- a/apps/web/components/orders/components/submit-button.tsx +++ b/apps/web/components/orders/components/submit-button.tsx @@ -1,5 +1,5 @@ 'use client'; -import { useOrderCreate } from '@/hooks/orders'; +import { useOrderCreate } from '@/hooks/api/orders'; import { useOrderStore } from '@/stores/order'; import { Button } from '@repo/ui/components/ui/button'; import { LoadingSpinner } from '@repo/ui/components/ui/spinner'; @@ -14,7 +14,15 @@ export function SubmitButton() { const handleSubmit = () => { if (isDisabled) return; - createOrder({ clientId, date, serviceId, slotId, time }); + createOrder({ + input: { + client: clientId, + date, + services: [serviceId], + slot: slotId, + time_start: time, + }, + }); }; useEffect(() => { diff --git a/apps/web/components/schedule/components/order-card.tsx b/apps/web/components/schedule/components/order-card.tsx index c75c39f..284366f 100644 --- a/apps/web/components/schedule/components/order-card.tsx +++ b/apps/web/components/schedule/components/order-card.tsx @@ -2,7 +2,7 @@ 'use client'; import { type OrderClient, type OrderComponentProps } from '../types'; import { ReadonlyTimeRange } from './time-range'; -import { useOrderQuery } from '@/hooks/orders'; +import { useOrderQuery } from '@/hooks/api/orders'; import { Enum_Order_State } from '@repo/graphql/types'; import { Avatar, AvatarFallback, AvatarImage } from '@repo/ui/components/ui/avatar'; import { Badge } from '@repo/ui/components/ui/badge'; @@ -10,8 +10,7 @@ import { cn } from '@repo/ui/lib/utils'; import Link from 'next/link'; export function OrderCard({ documentId }: Readonly) { - const { data } = useOrderQuery({ documentId }); - const order = data?.data?.order; + const { data: { order } = {} } = useOrderQuery({ documentId }); if (!order) return null; diff --git a/apps/web/hooks/api/orders.ts b/apps/web/hooks/api/orders.ts new file mode 100644 index 0000000..b25bd15 --- /dev/null +++ b/apps/web/hooks/api/orders.ts @@ -0,0 +1,15 @@ +'use client'; +import { createOrder, getOrder } from '@/actions/api/orders'; +import { useMutation, useQuery } from '@tanstack/react-query'; + +export const useOrderQuery = ({ documentId }: Parameters[0]) => + useQuery({ + queryFn: () => getOrder({ documentId }), + queryKey: ['order', documentId], + }); + +export const useOrderCreate = () => + useMutation({ + mutationFn: createOrder, + mutationKey: ['order', 'create'], + }); diff --git a/apps/web/hooks/orders/index.ts b/apps/web/hooks/orders/index.ts deleted file mode 100644 index bdd0957..0000000 --- a/apps/web/hooks/orders/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -'use client'; -import { createOrder, getOrder } from '@/actions/orders'; -// eslint-disable-next-line sonarjs/no-internal-api-use -import type * as ApolloTypes from '@repo/graphql/node_modules/@apollo/client/core'; -import { useMutation, useQuery } from '@tanstack/react-query'; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -type FixTypescriptCringe = ApolloTypes.FetchResult; - -type Props = { - documentId: string; -}; - -export const useOrderQuery = ({ documentId }: Props) => - useQuery({ - queryFn: () => getOrder({ documentId }), - queryKey: ['orders', 'get', documentId], - }); - -export const useOrderCreate = () => - useMutation({ - mutationFn: createOrder, - mutationKey: ['orders', 'create'], - }); diff --git a/packages/graphql/api/index.ts b/packages/graphql/api/index.ts deleted file mode 100644 index 00a4b07..0000000 --- a/packages/graphql/api/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './auth'; -export * from './customers'; -export * from './order'; -export * from './services'; -export * from './slots'; diff --git a/packages/graphql/api/order.ts b/packages/graphql/api/order.ts deleted file mode 100644 index 80298f7..0000000 --- a/packages/graphql/api/order.ts +++ /dev/null @@ -1,21 +0,0 @@ -'use server'; -import { getClientWithToken } from '../apollo/client'; -import * as GQL from '../types'; - -export async function getOrder(variables: GQL.GetOrderQueryVariables) { - const { query } = await getClientWithToken(); - - return query({ - query: GQL.GetOrderDocument, - variables, - }); -} - -export async function createOrder(input: GQL.CreateOrderMutationVariables['input']) { - const { mutate } = await getClientWithToken(); - - return mutate({ - mutation: GQL.CreateOrderDocument, - variables: { input }, - }); -} diff --git a/packages/graphql/api/orders.ts b/packages/graphql/api/orders.ts new file mode 100644 index 0000000..9f66bc6 --- /dev/null +++ b/packages/graphql/api/orders.ts @@ -0,0 +1,100 @@ +/* eslint-disable canonical/id-match */ +import { getClientWithToken } from '../apollo/client'; +import * as GQL from '../types'; +import { Enum_Customer_Role, Enum_Slot_State } from '../types'; +import { formatDate, formatTime, sumTime } from '../utils/datetime-format'; +import { BaseService } from './base'; +import { CustomersService } from './customers'; +import { ServicesService } from './services'; +import { SlotsService } from './slots'; +import { type VariablesOf } from '@graphql-typed-document-node/core'; + +const ERRORS = { + INVALID_CLIENT: 'Invalid client', + INVALID_MASTER: 'Invalid master', + MISSING_CLIENT: 'Missing client id', + MISSING_SERVICE_ID: 'Missing service id', + MISSING_SERVICES: 'Missing services', + MISSING_SLOT: 'Missing slot id', + MISSING_START_TIME: 'Missing time start', + SLOT_CLOSED: 'Slot is closed', +}; + +export class OrdersService extends BaseService { + async createOrder(variables: { + input: Omit['input'], 'time_end'>; + }) { + if (!variables.input.slot) throw new Error(ERRORS.MISSING_SLOT); + if (!variables.input.client) throw new Error(ERRORS.MISSING_CLIENT); + if (!variables.input.services?.length) throw new Error(ERRORS.MISSING_SERVICES); + if (!variables.input.services[0]) throw new Error(ERRORS.MISSING_SERVICE_ID); + if (!variables.input.time_start) throw new Error(ERRORS.MISSING_START_TIME); + + const customersService = new CustomersService(this.customer); + const slotsService = new SlotsService(this.customer); + const servicesService = new ServicesService(this.customer); + + const { customer } = await customersService.getCustomer(this.customer); + const { slot } = await slotsService.getSlot({ documentId: variables.input.slot }); + + if (slot?.state === Enum_Slot_State.Closed) { + throw new Error(ERRORS.SLOT_CLOSED); + } + + if (customer?.role === Enum_Customer_Role.Client) { + if (customer.documentId !== variables.input.client) { + throw new Error(ERRORS.INVALID_CLIENT); + } + + const masters = await customersService.getCustomerMasters(this.customer); + const masterId = slot?.master?.documentId; + if (!masters.customers.some((master) => master?.documentId === masterId)) { + throw new Error(ERRORS.INVALID_MASTER); + } + } + + if ( + customer?.role === Enum_Customer_Role.Master && + slot?.master?.documentId !== customer.documentId + ) { + throw new Error(ERRORS.INVALID_MASTER); + } + + const { service } = await servicesService.getService({ + documentId: variables.input.services[0], + }); + const endTime = sumTime(variables.input.time_start, service?.duration); + const { mutate } = await getClientWithToken(); + const mutationResult = await mutate({ + mutation: GQL.CreateOrderDocument, + variables: { + input: { + client: variables.input.client, + date: formatDate(variables.input.date).db(), + services: variables.input.services, + slot: variables.input.slot, + time_end: formatTime(endTime).db(), + time_start: formatTime(variables.input.time_start).db(), + }, + }, + }); + + const error = mutationResult.errors?.at(0); + if (error) throw new Error(error.message); + + return mutationResult.data; + } + + async getOrder(variables: VariablesOf) { + const { query } = await getClientWithToken(); + + const result = await query({ + query: GQL.GetOrderDocument, + variables, + }); + + if (result.error) throw new Error(result.error.message); + + return result.data; + } +} diff --git a/packages/graphql/config/token.ts b/packages/graphql/config/token.ts index deb26c2..e61bfc6 100644 --- a/packages/graphql/config/token.ts +++ b/packages/graphql/config/token.ts @@ -1,4 +1,4 @@ -import { login } from '../api'; +import { login } from '../api/auth'; import { isTokenExpired } from '../utils/jwt'; export const token: null | string = null;