From 7007e733c1bea6576a678501e74bd20087fca582 Mon Sep 17 00:00:00 2001 From: Jiwon <87058411+raipen@users.noreply.github.com> Date: Thu, 19 Oct 2023 18:01:09 +0900 Subject: [PATCH] Feat: mixed stock apis (#60) * feat: implement stock apis * feat: test stock Service * feat: test menu apis * fix: preOrder list default date --- prisma/menuSeed.ts | 2 - .../20231018133609_init/migration.sql | 21 ++ src/DTO/menu.dto.ts | 92 ----- src/DTO/preOrder.dto.ts | 3 +- src/DTO/stock.dto.ts | 345 +++++++++++++++++ src/api/index.ts | 2 + src/api/routes/menu.ts | 46 --- src/api/routes/stock.ts | 159 ++++++++ src/models/Recipe.prisma | 2 - src/models/Stock.prisma | 3 +- src/models/Store.prisma | 2 - src/services/menuService.ts | 133 +++---- src/services/preOrderService.ts | 1 + src/services/stockService.ts | 299 +++++++++++++++ test/integration/menuService.test.ts | 354 +++++++++++++++++- 15 files changed, 1231 insertions(+), 233 deletions(-) create mode 100644 prisma/migrations/20231018133609_init/migration.sql create mode 100644 src/DTO/stock.dto.ts create mode 100644 src/api/routes/stock.ts create mode 100644 src/services/stockService.ts diff --git a/prisma/menuSeed.ts b/prisma/menuSeed.ts index 645b491..d0d1db5 100644 --- a/prisma/menuSeed.ts +++ b/prisma/menuSeed.ts @@ -92,7 +92,6 @@ export default async (prisma: PrismaClient, storeId: number) => { create: material.materials.map((material) => ({ amount: parseInt(material.amount), stockId: material.stockId, - storeId })) } } @@ -145,7 +144,6 @@ export default async (prisma: PrismaClient, storeId: number) => { hotRegularAmount: material.hotRegularAmount, hotSizeUpAmount: material.hotSizeUpAmount, stockId: material.stockId, - storeId })) } } diff --git a/prisma/migrations/20231018133609_init/migration.sql b/prisma/migrations/20231018133609_init/migration.sql new file mode 100644 index 0000000..5ba503d --- /dev/null +++ b/prisma/migrations/20231018133609_init/migration.sql @@ -0,0 +1,21 @@ +/* + Warnings: + + - You are about to drop the column `storeId` on the `Mixing` table. All the data in the column will be lost. + - You are about to drop the column `storeId` on the `Recipe` table. All the data in the column will be lost. + +*/ +-- DropForeignKey +ALTER TABLE `Mixing` DROP FOREIGN KEY `Mixing_storeId_fkey`; + +-- DropForeignKey +ALTER TABLE `Recipe` DROP FOREIGN KEY `Recipe_storeId_fkey`; + +-- AlterTable +ALTER TABLE `Mixing` DROP COLUMN `storeId`; + +-- AlterTable +ALTER TABLE `Recipe` DROP COLUMN `storeId`; + +-- AlterTable +ALTER TABLE `Stock` ADD COLUMN `currentAmount` INTEGER NOT NULL DEFAULT 0; diff --git a/src/DTO/menu.dto.ts b/src/DTO/menu.dto.ts index 5220c3b..a2a02a4 100644 --- a/src/DTO/menu.dto.ts +++ b/src/DTO/menu.dto.ts @@ -275,101 +275,9 @@ export const updateMenuSchema = { }, } as const; -export const createStockSchema = { - tags: ['menu'], - summary: '재료 생성', - headers: StoreAuthorizationHeader, - body: { - type: 'object', - required: ['name'], - properties: { - name: { type: 'string' }, - amount: { type: 'number', nullable: true }, - unit: { type: 'string', nullable: true }, - price: { type: 'number', nullable: true }, - }, - }, - response: { - 201: { - type: 'object', - description: 'success response', - required: ['stockId'], - properties: { - stockId: { type: 'number' }, - }, - }, - ...errorSchema(E.NotFoundError, E.UserAuthorizationError, E.StoreAuthorizationError, E.NoAuthorizationInHeaderError) - }, -} as const; - -export const updateStockSchema = { - tags: ['menu'], - summary: '재료 수정', - headers: StoreAuthorizationHeader, - body: { - type: 'object', - required: ['id', 'name'], - properties: { - id: { type: 'number' }, - name: { type: 'string' }, - amount: { type: 'number', nullable: true }, - unit: { type: 'string', nullable: true }, - price: { type: 'number', nullable: true }, - }, - }, - response: { - 201: { - type: 'object', - description: 'success response', - required: ['stockId'], - properties: { - stockId: { type: 'number' }, - }, - }, - ...errorSchema(E.NotFoundError, E.UserAuthorizationError, E.StoreAuthorizationError, E.NoAuthorizationInHeaderError) - }, -} as const; - -export const searchStockSchema = { - tags: ['menu'], - summary: '재료 검색', - headers: StoreAuthorizationHeader, - querystring: { - type: 'object', - required: ['name'], - properties: { - name: { type: 'string' }, - }, - }, - response: { - 200: { - type: 'object', - description: 'success response', - required: ['stocks'], - properties: { - stocks: { - type: 'array', - items: { - type: 'object', - required: ['id', 'name'], - properties: { - id: { type: 'number' }, - name: { type: 'string' } - }, - }, - }, - }, - }, - ...errorSchema(E.NotFoundError, E.UserAuthorizationError, E.StoreAuthorizationError, E.NoAuthorizationInHeaderError) - }, -} as const; - export type getMenuListInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; export type getMenuInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; export type getOptionListInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; export type createCategoryInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; export type createMenuInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; export type updateMenuInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; -export type createStockInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; -export type updateStockInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; -export type searchStockInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; diff --git a/src/DTO/preOrder.dto.ts b/src/DTO/preOrder.dto.ts index d3e70ad..4a8bca0 100644 --- a/src/DTO/preOrder.dto.ts +++ b/src/DTO/preOrder.dto.ts @@ -135,14 +135,13 @@ export const getPreOrderListSchema = { headers: StoreAuthorizationHeader, querystring: { type: 'object', - required: ['page', 'count', 'date'], + required: ['page', 'count'], properties: { page: { type: 'number', default: 1 }, count: { type: 'number', default: 10 }, date: { type: 'string', format: 'date-time', - default: new Date().toISOString(), }, }, }, diff --git a/src/DTO/stock.dto.ts b/src/DTO/stock.dto.ts new file mode 100644 index 0000000..515b57e --- /dev/null +++ b/src/DTO/stock.dto.ts @@ -0,0 +1,345 @@ +import { + StoreAuthorizationHeader, + errorSchema, + SchemaToInterface, +} from '@DTO/index.dto'; +import * as E from '@errors'; + +export const createStockSchema = { + tags: ['menu'], + summary: '재료 생성', + headers: StoreAuthorizationHeader, + body: { + type: 'object', + required: ['name'], + properties: { + name: { type: 'string' }, + amount: { type: 'number', nullable: true }, + unit: { type: 'string', nullable: true }, + price: { type: 'string', nullable: true }, + currentAmount: { type: 'number' }, + }, + }, + response: { + 201: { + type: 'object', + description: 'success response', + required: ['stockId'], + properties: { + stockId: { type: 'number' }, + }, + }, + ...errorSchema(E.NotFoundError, E.UserAuthorizationError, E.StoreAuthorizationError, E.NoAuthorizationInHeaderError) + }, +} as const; + +export const updateStockSchema = { + tags: ['menu'], + summary: '재료 수정', + headers: StoreAuthorizationHeader, + body: { + type: 'object', + required: ['id', 'name'], + properties: { + id: { type: 'number' }, + name: { type: 'string' }, + amount: { type: 'number', nullable: true }, + unit: { type: 'string', nullable: true }, + price: { type: 'string', nullable: true }, + currentAmount: { type: 'number' }, + }, + }, + response: { + 201: { + type: 'object', + description: 'success response', + required: ['stockId'], + properties: { + stockId: { type: 'number' }, + }, + }, + ...errorSchema(E.NotFoundError, E.UserAuthorizationError, E.StoreAuthorizationError, E.NoAuthorizationInHeaderError) + }, +} as const; + +export const getStockListSchema = { + tags: ['menu'], + summary: '원재료 목록 조회', + headers: StoreAuthorizationHeader, + response: { + 200: { + type: 'object', + description: 'success response', + required: ['stocks'], + properties: { + stocks: { + type: 'array', + items: { + type: 'object', + required: ['id', 'name', 'status', 'usingMenuCount'], + properties: { + id: { type: 'number' }, + name: { type: 'string' }, + status: { type: 'string', enum: ['여유', '주의', '부족', '없음'] }, + usingMenuCount: { type: 'number' }, + }, + }, + }, + }, + }, + ...errorSchema(E.NotFoundError, E.UserAuthorizationError, E.StoreAuthorizationError, E.NoAuthorizationInHeaderError) + }, +} as const; + +export const getStockSchema = { + tags: ['menu'], + summary: '원재료 상세 조회', + headers: StoreAuthorizationHeader, + params: { + type: 'object', + required: ['stockId'], + properties: { + stockId: { type: 'number' }, + }, + }, + response: { + 200: { + type: 'object', + description: 'success response', + required: ['name', 'amount', 'unit', 'price', 'currentAmount'], + properties: { + name: { type: 'string' }, + amount: { type: 'number', nullable: true }, + unit: { type: 'string', nullable: true }, + price: { type: 'string', nullable: true }, + currentAmount: { type: 'number' }, + }, + }, + ...errorSchema(E.NotFoundError, E.UserAuthorizationError, E.StoreAuthorizationError, E.NoAuthorizationInHeaderError) + }, +} as const; + +export const createMixedStockSchema = { + tags: ['menu'], + summary: '혼합 재료 생성', + headers: StoreAuthorizationHeader, + body: { + type: 'object', + required: ['name', 'mixing'], + properties: { + name: { type: 'string' }, + totalAmount: { type: 'number', nullable: true }, + unit: { type: 'string', nullable: true }, + mixing: { + type: 'array', + items: { + type: 'object', + required: ['id', 'unit', 'amount'], + additionalProperties: false, + properties: { + id: { type: 'number' }, + unit: { type: 'string', }, + amount: { type: 'number', }, + }, + }, + }, + }, + }, + response: { + 201: { + type: 'object', + description: 'success response', + required: ['mixedStockId'], + properties: { + mixedStockId: { type: 'number' }, + }, + }, + ...errorSchema(E.NotFoundError, E.UserAuthorizationError, E.StoreAuthorizationError, E.NoAuthorizationInHeaderError) + }, +} as const; + +export const updateMixedStockSchema = { + tags: ['menu'], + summary: '혼합 재료 생성', + headers: StoreAuthorizationHeader, + body: { + type: 'object', + required: ['name', 'mixing', 'id'], + properties: { + id: { type: 'number' }, + name: { type: 'string' }, + totalAmount: { type: 'number', nullable: true }, + unit: { type: 'string', nullable: true }, + mixing: { + type: 'array', + items: { + type: 'object', + required: ['id', 'unit', 'amount'], + additionalProperties: false, + properties: { + id: { type: 'number' }, + unit: { type: 'string', }, + amount: { type: 'number', }, + }, + }, + }, + }, + }, + response: { + 201: { + type: 'object', + description: 'success response', + required: ['mixedStockId'], + properties: { + mixedStockId: { type: 'number' }, + }, + }, + ...errorSchema(E.NotFoundError, E.UserAuthorizationError, E.StoreAuthorizationError, E.NoAuthorizationInHeaderError) + }, +} as const; + +export const getMixedStockListSchema = { + tags: ['menu'], + summary: '혼합 재료 목록 조회', + headers: StoreAuthorizationHeader, + response: { + 200: { + type: 'object', + description: 'success response', + required: ['mixedStocks'], + properties: { + mixedStocks: { + type: 'array', + items: { + type: 'object', + required: ['id', 'name' ], + properties: { + id: { type: 'number' }, + name: { type: 'string' }, + } + } + } + }, + }, + ...errorSchema(E.NotFoundError, E.UserAuthorizationError, E.StoreAuthorizationError, E.NoAuthorizationInHeaderError) + }, +} as const; + +export const getMixedStockSchema = { + tags: ['menu'], + summary: '혼합 재료 상세 조회', + headers: StoreAuthorizationHeader, + params: { + type: 'object', + required: ['mixedStockId'], + properties: { + mixedStockId: { type: 'number' }, + }, + }, + response: { + 200: { + type: 'object', + description: 'success response', + required: ['name', 'totalAmount', 'unit', 'mixing'], + properties: { + name: { type: 'string' }, + totalAmount: { type: 'number', nullable: true }, + unit: { type: 'string', nullable: true }, + mixing: { + type: 'array', + items: { + type: 'object', + required: ['id', 'name', 'unit', 'amount'], + properties: { + id: { type: 'number' }, + name: { type: 'string' }, + unit: { type: 'string', nullable: true }, + amount: { type: 'number'}, + }, + }, + }, + }, + }, + ...errorSchema(E.NotFoundError, E.UserAuthorizationError, E.StoreAuthorizationError, E.NoAuthorizationInHeaderError) + }, +} as const; + +export const searchStockSchema = { + tags: ['menu'], + summary: '재료 검색', + headers: StoreAuthorizationHeader, + querystring: { + type: 'object', + required: ['name'], + properties: { + name: { type: 'string' }, + }, + }, + response: { + 200: { + type: 'object', + description: 'success response', + required: ['stocks'], + properties: { + stocks: { + type: 'array', + items: { + type: 'object', + required: ['id', 'name'], + properties: { + id: { type: 'number' }, + name: { type: 'string' } + }, + }, + }, + }, + }, + ...errorSchema(E.NotFoundError, E.UserAuthorizationError, E.StoreAuthorizationError, E.NoAuthorizationInHeaderError) + }, +} as const; + +export const searchStockAndMixedStockSchema = { + tags: ['menu'], + summary: '재료 검색', + headers: StoreAuthorizationHeader, + querystring: { + type: 'object', + required: ['name'], + properties: { + name: { type: 'string' }, + }, + }, + response: { + 200: { + type: 'object', + description: 'success response', + required: ['stocks'], + properties: { + stocks: { + type: 'array', + items: { + type: 'object', + required: ['id', 'name', 'isMixed'], + properties: { + id: { type: 'number' }, + name: { type: 'string' }, + isMixed: { type: 'boolean' } + }, + }, + }, + }, + }, + ...errorSchema(E.NotFoundError, E.UserAuthorizationError, E.StoreAuthorizationError, E.NoAuthorizationInHeaderError) + }, +} as const; + +export type createStockInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; +export type updateStockInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; +export type getStockListInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; +export type getStockInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; +export type createMixedStockInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; +export type updateMixedStockInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; +export type getMixedStockListInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; +export type getMixedStockInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; +export type searchStockInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; +export type searchStockAndMixedStockInterface = SchemaToInterface&{Body: {storeId: number, userId: number}}; diff --git a/src/api/index.ts b/src/api/index.ts index bdff75e..bd60297 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -5,6 +5,7 @@ import user from './routes/user'; import store from './routes/store'; import mileage from './routes/mileage'; import preOrder from './routes/preOrder'; +import stock from './routes/stock'; import test from './routes/apiTest'; const api: FastifyPluginAsync = async (server: FastifyInstance) => { @@ -14,6 +15,7 @@ const api: FastifyPluginAsync = async (server: FastifyInstance) => { server.register(store, { prefix: '/store' }); server.register(mileage, { prefix: '/mileage' }); server.register(preOrder, { prefix: '/preorder' }); + server.register(stock, { prefix: '/stock' }); server.register(test, { prefix: '/' }); }; diff --git a/src/api/routes/menu.ts b/src/api/routes/menu.ts index e54bf95..3cc1f8a 100644 --- a/src/api/routes/menu.ts +++ b/src/api/routes/menu.ts @@ -79,52 +79,6 @@ const api: FastifyPluginAsync = async (server: FastifyInstance) => { .send(result); } ); - - server.post( - '/stock', - { - schema: Menu.createStockSchema, - onError, - preValidation: checkStoreIdUser - }, - async (request, reply) => { - const result = await menuService.createStock(request.body); - reply - .code(201) - .send(result); - } - ); - - server.put( - '/stock', - { - schema: Menu.updateStockSchema, - onError, - preValidation: checkStoreIdUser - }, - async (request, reply) => { - const result = await menuService.updateStockInfo(request.body); - reply - .code(201) - .send(result); - } - ); - - server.get( - '/stock', - { - schema: Menu.searchStockSchema, - onError, - preValidation: checkStoreIdUser - }, - async (request, reply) => { - const result = await menuService.searchStock(request.body, request.query); - reply - .code(200) - .send(result); - } - ); - server.get( '/option', { diff --git a/src/api/routes/stock.ts b/src/api/routes/stock.ts new file mode 100644 index 0000000..5584bde --- /dev/null +++ b/src/api/routes/stock.ts @@ -0,0 +1,159 @@ +import { FastifyInstance, FastifyPluginAsync } from "fastify"; +import onError from "@hooks/onError"; +import checkStoreIdUser from '@hooks/checkStoreIdUser'; +import * as Stock from "@DTO/stock.dto"; +import stockService from "@services/stockService"; + +const api: FastifyPluginAsync = async (server: FastifyInstance) => { + server.post( + '/', + { + schema: Stock.createStockSchema, + onError, + preValidation: checkStoreIdUser + }, + async (request, reply) => { + const result = await stockService.createStock(request.body); + reply + .code(201) + .send(result); + } + ); + + server.put( + '/', + { + schema: Stock.updateStockSchema, + onError, + preValidation: checkStoreIdUser + }, + async (request, reply) => { + const result = await stockService.updateStock(request.body); + reply + .code(201) + .send(result); + } + ); + + server.get( + '/', + { + schema: Stock.getStockListSchema, + onError, + preValidation: checkStoreIdUser + }, + async (request, reply) => { + const result = await stockService.getStockList(request.body); + reply + .code(200) + .send(result); + } + ); + + server.get( + '/:stockId', + { + schema: Stock.getStockSchema, + onError, + preValidation: checkStoreIdUser + }, + async (request, reply) => { + const result = await stockService.getStock(request.body, request.params); + reply + .code(200) + .send(result); + } + ); + + server.post( + '/mixed', + { + schema: Stock.createMixedStockSchema, + onError, + preValidation: checkStoreIdUser + }, + async (request, reply) => { + const result = await stockService.createMixedStock(request.body); + reply + .code(201) + .send(result); + } + ); + + server.put( + '/mixed', + { + schema: Stock.updateMixedStockSchema, + onError, + preValidation: checkStoreIdUser + }, + async (request, reply) => { + const result = await stockService.updateMixedStock(request.body); + reply + .code(201) + .send(result); + } + ); + + server.get( + '/mixed', + { + schema: Stock.getMixedStockListSchema, + onError, + preValidation: checkStoreIdUser + }, + async (request, reply) => { + const result = await stockService.getMixedStockList(request.body); + reply + .code(200) + .send(result); + } + ); + + server.get( + '/mixed/:mixedStockId', + { + schema: Stock.getMixedStockSchema, + onError, + preValidation: checkStoreIdUser + }, + async (request, reply) => { + const result = await stockService.getMixedStock(request.body, request.params); + reply + .code(200) + .send(result); + } + ); + + server.get( + '/search', + { + schema: Stock.searchStockSchema, + onError, + preValidation: checkStoreIdUser + }, + async (request, reply) => { + const result = await stockService.searchStock(request.body, request.query); + reply + .code(200) + .send(result); + } + ); + + server.get( + '/withMixed/search', + { + schema: Stock.searchStockAndMixedStockSchema, + onError, + preValidation: checkStoreIdUser + }, + async (request, reply) => { + const result = await stockService.searchStockAndMixedStock(request.body, request.query); + reply + .code(200) + .send(result); + } + ); +} + +export default api; diff --git a/src/models/Recipe.prisma b/src/models/Recipe.prisma index a1f9fe3..347cae6 100644 --- a/src/models/Recipe.prisma +++ b/src/models/Recipe.prisma @@ -1,7 +1,5 @@ model Recipe { id Int @id @default(autoincrement()) - store Store @relation(fields: [storeId], references: [id]) - storeId Int menuId Int menu Menu @relation(fields: [menuId], references: [id]) stockId Int? diff --git a/src/models/Stock.prisma b/src/models/Stock.prisma index 74f0108..7660d67 100644 --- a/src/models/Stock.prisma +++ b/src/models/Stock.prisma @@ -8,6 +8,7 @@ model Stock { amount Int? unit String? price Decimal? + currentAmount Int @default(0) } model MixedStock { @@ -23,8 +24,6 @@ model MixedStock { model Mixing { id Int @id @default(autoincrement()) - store Store @relation(fields: [storeId], references: [id]) - storeId Int mixedStockId Int mixedStock MixedStock @relation(fields: [mixedStockId], references: [id]) stockId Int diff --git a/src/models/Store.prisma b/src/models/Store.prisma index 53b20b6..95b719b 100644 --- a/src/models/Store.prisma +++ b/src/models/Store.prisma @@ -12,10 +12,8 @@ model Store{ preOrder PreOrder[] mileage Mileage[] option Option[] - recipe Recipe[] stock Stock[] mixedStocks MixedStock[] - mixings Mixing[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } diff --git a/src/services/menuService.ts b/src/services/menuService.ts index 0d9d6fa..d374378 100644 --- a/src/services/menuService.ts +++ b/src/services/menuService.ts @@ -93,9 +93,7 @@ export default { const recipe = menu.recipes.map(({ stock, mixedStock, coldRegularAmount, coldSizeUpAmount, hotRegularAmount, hotSizeUpAmount }) => { const recipeStock = stock ?? mixedStock; - if(!recipeStock) - throw new NotFoundError('재고가 존재하지 않습니다.', '재고'); - const { id, name, unit } = recipeStock; + const { id, name, unit } = recipeStock!; return { id, isMixed: stock === null, @@ -213,7 +211,6 @@ export default { }, recipes: { create: recipe.map(({ id, isMixed, coldRegularAmount, coldSizeUpAmount, hotRegularAmount, hotSizeUpAmount}) => ({ - storeId, stockId: isMixed ? undefined : id, mixedStockId: isMixed ? id : undefined, coldRegularAmount, @@ -234,23 +231,27 @@ export default { }); //레시피에 대한 재고에 unit 정보가 없는 경우, 재고에 unit 정보를 추가해준다. - await Promise.all(result.recipes.map(async ({ stockId, mixedStockId }) => { - if(stockId) { - const unit = recipe!.find(({ id, isMixed }) => id === stockId&&isMixed===false)?.unit; + await Promise.all(result.recipes.map(async ({ stock, mixedStock }) => { + if(stock) { + if(stock.unit !== null) + return; + const unit = recipe!.find(({ id, isMixed }) => id === stock.id&&isMixed===false)?.unit; await prisma.stock.update({ where: { - id: stockId, + id: stock.id, }, data: { unit, }, }); } - if(mixedStockId) { - const unit = recipe!.find(({ id, isMixed }) => id === mixedStockId&&isMixed===true)?.unit; + if(mixedStock) { + if(mixedStock.unit !== null) + return; + const unit = recipe!.find(({ id, isMixed }) => id === mixedStock.id&&isMixed===true)?.unit; await prisma.mixedStock.update({ where: { - id: mixedStockId, + id: mixedStock.id, }, data: { unit, @@ -275,6 +276,15 @@ export default { id, }: Menu.updateMenuInterface['Body'] ): Promise { + //delete all recipes + await prisma.recipe.deleteMany({ + where: { + menuId: id, + }, + }); + + if(!recipe) + recipe = []; const result = await prisma.menu.update({ where: { id, @@ -283,7 +293,17 @@ export default { data: { name, price, - categoryId + categoryId, + recipes: { + create: recipe.map(({ id, isMixed, coldRegularAmount, coldSizeUpAmount, hotRegularAmount, hotSizeUpAmount}) => ({ + stockId: isMixed ? undefined : id, + mixedStockId: isMixed ? id : undefined, + coldRegularAmount, + coldSizeUpAmount, + hotRegularAmount, + hotSizeUpAmount, + })), + }, }, include: { optionMenu: true, @@ -298,8 +318,6 @@ export default { if(!option) option = []; - if(!recipe) - recipe = []; const optionMenuIds = result.optionMenu.map(({ optionId }) => optionId).sort(); const optionIds = option.sort(); @@ -316,67 +334,38 @@ export default { })), }); } + + await Promise.all(result.recipes.map(async ({ stock, mixedStock }) => { + if(stock) { + if(stock.unit !== null) + return; + const unit = recipe!.find(({ id, isMixed }) => id === stock.id&&isMixed===false)?.unit; + await prisma.stock.update({ + where: { + id: stock.id, + }, + data: { + unit, + }, + }); + } + if(mixedStock) { + if(mixedStock.unit !== null) + return; + const unit = recipe!.find(({ id, isMixed }) => id === mixedStock.id&&isMixed===true)?.unit; + await prisma.mixedStock.update({ + where: { + id: mixedStock.id, + }, + data: { + unit, + }, + }); + } + })); return { menuId: result.id, }; }, - async createStock( - { storeId, name, price, amount, unit }: Menu.createStockInterface['Body'] - ): Promise { - const result = await prisma.stock.create({ - data: { - name, - price, - amount, - unit, - storeId, - }, - }); - - return { - stockId: result.id, - }; - }, - async updateStockInfo( - { storeId, name, price, amount, unit, id }: Menu.updateStockInterface['Body'] - ): Promise { - const result = await prisma.stock.update({ - where: { - id, - storeId, - }, - data: { - name, - price, - amount, - unit, - }, - }); - - return { - stockId: result.id, - }; - }, - - async searchStock( - { storeId }: Menu.searchStockInterface['Body'], - { name }: Menu.searchStockInterface['Querystring'], - ): Promise { - const result = await prisma.stock.findMany({ - where: { - storeId, - name: { - contains: name, - }, - }, - }); - - return { - stocks: result.map((stock) => ({ - id: stock.id, - name: stock.name - })), - }; - } }; diff --git a/src/services/preOrderService.ts b/src/services/preOrderService.ts index 25a88f2..a575ec1 100644 --- a/src/services/preOrderService.ts +++ b/src/services/preOrderService.ts @@ -105,6 +105,7 @@ export default { { storeId }: PreOrder.getPreOrderListInterface['Body'], { page, count, date }: PreOrder.getPreOrderListInterface['Querystring'] ): Promise { + date = date ?? new Date().toISOString().split('T')[0]; const reservationDate = new Date(date); const krDate = new Date(reservationDate.getTime() + 9 * 60 * 60 * 1000); const krDateStr = new Date(krDate.toISOString().split('T')[0]); diff --git a/src/services/stockService.ts b/src/services/stockService.ts new file mode 100644 index 0000000..de6ca09 --- /dev/null +++ b/src/services/stockService.ts @@ -0,0 +1,299 @@ +import { PrismaClient } from '@prisma/client'; +import { NotFoundError } from '@errors'; +import * as Stock from '@DTO/stock.dto'; + +const prisma = new PrismaClient(); + +export default { + async createStock( + { storeId, name, price, amount,currentAmount, unit }: Stock.createStockInterface['Body'] + ): Promise { + const result = await prisma.stock.create({ + data: { + name, + price, + amount, + currentAmount, + unit, + storeId, + }, + }); + + return { + stockId: result.id, + }; + }, + + async updateStock( + { storeId, name, price, amount, currentAmount, unit, id }: Stock.updateStockInterface['Body'] + ): Promise { + const result = await prisma.stock.update({ + where: { + id, + storeId, + }, + data: { + name, + price, + amount, + unit, + currentAmount, + }, + }); + + return { + stockId: result.id, + }; + }, + + async getStockList( + { storeId }: Stock.getStockListInterface['Body'] + ): Promise { + const result = await prisma.stock.findMany({ + where: { + storeId, + } + }); + + return { + stocks: result.map(({ id, name, currentAmount, unit }) => ({ + id, + name, + status: currentAmount === 0 ? '없음' : '여유', // TODO: 재고 상태를 구하는 로직 필요 + usingMenuCount: 0, // TODO: 재고를 사용하는 메뉴 개수를 구하는 로직 필요 + })), + }; + }, + + async getStock( + { storeId }: Stock.getStockInterface['Body'], + { stockId }: Stock.getStockInterface['Params'] + ): Promise { + const result = await prisma.stock.findUnique({ + where: { + id: stockId, + storeId, + }, + }); + if (!result) { + throw new NotFoundError('재고가 존재하지 않습니다.', '재고'); + } + + return { + name: result.name, + price: result.price===null?"0":result.price.toString(), + amount: result.amount, + currentAmount: result.currentAmount, + unit: result.unit, + }; + }, + + async createMixedStock( + { storeId, name, totalAmount, unit, mixing }: Stock.createMixedStockInterface['Body'] + ): Promise { + const result = await prisma.mixedStock.create({ + data: { + name, + storeId, + unit, + totalAmount, + mixings: { + create: mixing.map(({ id, amount}) => ({ + stockId: id, + amount, + })), + }, + }, + include: { + mixings: { + include: { + stock: true, + }, + }, + }, + }); + + //레시피에 대한 재고에 unit 정보가 없는 경우, 재고에 unit 정보를 추가해준다. + await Promise.all(result.mixings.map(async ({ stock }) => { + if(stock.unit !== null) + return; + const unit = mixing!.find(({ id }) => id === stock.id)?.unit; + await prisma.stock.update({ + where: { + id: stock.id, + }, + data: { + unit, + }, + }); + })); + + return { + mixedStockId: result.id, + }; + }, + + async updateMixedStock( + { storeId, id, name, totalAmount, unit, mixing }: Stock.updateMixedStockInterface['Body'] + ): Promise { + //delete all mixings + await prisma.mixing.deleteMany({ + where: { + mixedStockId: id, + }, + }); + + const result = await prisma.mixedStock.update({ + where: { + id, + storeId, + }, + data: { + name, + storeId, + unit, + totalAmount, + mixings: { + create: mixing.map(({ id, amount}) => ({ + stockId: id, + amount, + })), + }, + }, + include: { + mixings: { + include: { + stock: true, + }, + }, + }, + }); + + //레시피에 대한 재고에 unit 정보가 없는 경우, 재고에 unit 정보를 추가해준다. + await Promise.all(result.mixings.map(async ({ stock }) => { + if(stock.unit !== null) + return; + const unit = mixing!.find(({ id }) => id === stock.id)?.unit; + await prisma.stock.update({ + where: { + id: stock.id, + }, + data: { + unit, + }, + }); + })); + + return { + mixedStockId: result.id, + }; + }, + + async getMixedStockList( + { storeId }: Stock.getMixedStockListInterface['Body'] + ): Promise { + const result = await prisma.mixedStock.findMany({ + where: { + storeId, + } + }); + + return { + mixedStocks: result.map(({ id, name }) => ({ + id, + name + })) + }; + }, + + async getMixedStock( + { storeId }: Stock.getMixedStockInterface['Body'], + { mixedStockId }: Stock.getMixedStockInterface['Params'] + ): Promise { + const result = await prisma.mixedStock.findUnique({ + where: { + id: mixedStockId, + storeId, + }, + include: { + mixings: { + include: { + stock: true, + }, + }, + }, + }); + if (!result) { + throw new NotFoundError('재고가 존재하지 않습니다.', '재고'); + } + + return { + name: result.name, + totalAmount: result.totalAmount, + unit: result.unit, + mixing: result.mixings.map(({ stock, amount }) => ({ + id: stock.id, + name: stock.name, + amount, + unit: stock.unit, + })), + }; + }, + + async searchStock( + { storeId }: Stock.searchStockInterface['Body'], + { name }: Stock.searchStockInterface['Querystring'], + ): Promise { + const result = await prisma.stock.findMany({ + where: { + storeId, + name: { + contains: name, + }, + }, + }); + + return { + stocks: result.map((stock) => ({ + id: stock.id, + name: stock.name, + })) + }; + }, + + async searchStockAndMixedStock( + { storeId }: Stock.searchStockAndMixedStockInterface['Body'], + { name }: Stock.searchStockAndMixedStockInterface['Querystring'], + ): Promise { + const [result, mixedResult] = await Promise.all([ + prisma.stock.findMany({ + where: { + storeId, + name: { + contains: name, + }, + }, + }), + prisma.mixedStock.findMany({ + where: { + storeId, + name: { + contains: name, + }, + }, + }) + ]); + + return { + stocks: result.map((stock) => ({ + id: stock.id, + name: stock.name, + isMixed: false, + })).concat(mixedResult.map((stock) => ({ + id: stock.id, + name: stock.name, + isMixed: true, + }))), + }; + } +}; diff --git a/test/integration/menuService.test.ts b/test/integration/menuService.test.ts index ccf2e18..86f2af7 100644 --- a/test/integration/menuService.test.ts +++ b/test/integration/menuService.test.ts @@ -4,6 +4,7 @@ import { afterAll, beforeAll, describe, expect, test } from '@jest/globals'; import { LoginToken } from '@utils/jwt'; import seedValues from './seedValues'; import * as Menu from '@DTO/menu.dto'; +import * as Stock from '@DTO/stock.dto'; import { ErrorInterface } from '@DTO/index.dto'; let app: FastifyInstance; @@ -43,7 +44,7 @@ let mintChoco: number; test('create stock with name only', async () => { const response = await app.inject({ method: 'POST', - url: `/api/menu/stock`, + url: `/api/stock`, headers: { authorization: `Bearer ${accessToken}`, storeid: seedValues.store.id.toString(), @@ -56,7 +57,7 @@ test('create stock with name only', async () => { expect(response.statusCode).toBe(201); const body = JSON.parse( response.body - ) as Menu.createStockInterface['Reply']['201']; + ) as Stock.createStockInterface['Reply']['201']; mintChoco = body.stockId; }); @@ -64,7 +65,7 @@ let cock: number; test('create stock with name and price', async () => { const response = await app.inject({ method: 'POST', - url: `/api/menu/stock`, + url: `/api/stock`, headers: { authorization: `Bearer ${accessToken}`, storeid: seedValues.store.id.toString(), @@ -80,7 +81,7 @@ test('create stock with name and price', async () => { expect(response.statusCode).toBe(201); const body = JSON.parse( response.body - ) as Menu.createStockInterface['Reply']['201']; + ) as Stock.createStockInterface['Reply']['201']; cock = body.stockId; }); @@ -122,21 +123,321 @@ test('new menu', async () => { }); }); +let sugar: number; +test('search stock',async () => { + const response = await app.inject({ + method: 'GET', + url: `/api/stock/search?name=설탕`, + headers: { + authorization: `Bearer ${accessToken}`, + storeid: seedValues.store.id.toString(), + }, + }); + expect(response.statusCode).toBe(200); + const body = JSON.parse(response.body) as Stock.searchStockInterface['Reply']['200']; + sugar = body.stocks[0].id; +}); + +let chocoSyrup:number; +test('search stock',async () => { + const response = await app.inject({ + method: 'GET', + url: `/api/stock/search?name=초코시럽`, + headers: { + authorization: `Bearer ${accessToken}`, + storeid: seedValues.store.id.toString(), + }, + }); + expect(response.statusCode).toBe(200); + const body = JSON.parse(response.body) as Stock.searchStockInterface['Reply']['200']; + chocoSyrup = body.stocks[0].id; +}); + +let mintChocoChung: number; +test('new mixedStock',async () => { + const response = await app.inject({ + method: 'POST', + url: `/api/stock/mixed`, + headers: { + authorization: `Bearer ${accessToken}`, + storeid: seedValues.store.id.toString(), + }, + body: { + name: '민트초코 청', + mixing: [ + { + id: mintChoco, + unit: 'ml', + amount: 1000, + }, + { + id: sugar, + unit: 'g', + amount: 1000, + } + ] + }, + }); + expect(response.statusCode).toBe(201); + const body = JSON.parse( + response.body + ) as Stock.createMixedStockInterface['Reply']['201']; + mintChocoChung = body.mixedStockId; +}); + +test('get stock detail', async () => { + const response = await app.inject({ + method: 'GET', + url: `/api/stock/${mintChoco}`, + headers: { + authorization: `Bearer ${accessToken}`, + storeid: seedValues.store.id.toString(), + }, + }); + expect(response.statusCode).toBe(200); + const body = JSON.parse( + response.body + ) as Stock.getStockInterface['Reply']['200']; + expect(body).toEqual({ + name: '민트초코 시럽', + price: '0', + amount: null, + currentAmount: 0, + unit: 'ml', + }); +}); + +test('update stock', async () => { + const response = await app.inject({ + method: 'PUT', + url: `/api/stock`, + headers: { + authorization: `Bearer ${accessToken}`, + storeid: seedValues.store.id.toString(), + }, + body: { + id: mintChoco, + name: '민트초코 시럽', + price: 3000, + amount: 1000, + unit: 'ml', + currentAmount: 1000, + }, + }); + expect(response.statusCode).toBe(201); + const body = JSON.parse( + response.body + ) as Stock.updateStockInterface['Reply']['201']; + expect(body).toEqual({ + stockId: mintChoco, + }); +}); + +test('get stock list', async () => { + const response = await app.inject({ + method: 'GET', + url: `/api/stock`, + headers: { + authorization: `Bearer ${accessToken}`, + storeid: seedValues.store.id.toString(), + }, + }); + expect(response.statusCode).toBe(200); + const body = JSON.parse( + response.body + ) as Stock.getStockListInterface['Reply']['200']; + const mintChoco = body.stocks.find((stock) => stock.name === '민트초코 시럽'); + expect(mintChoco).toEqual({ + id: expect.any(Number), + name: '민트초코 시럽', + status: '여유', + usingMenuCount: 0, // TODO: make this value to be 1 + }); + const cock = body.stocks.find((stock)=> stock.name === '콜라'); + expect(cock).toEqual({ + id: expect.any(Number), + name: '콜라', + status: '없음', + usingMenuCount: 0, // TODO: make this value to be 1 + }) +}); + +test('get stock detail', async () => { + const response = await app.inject({ + method: 'GET', + url: `/api/stock/${mintChoco}`, + headers: { + authorization: `Bearer ${accessToken}`, + storeid: seedValues.store.id.toString(), + }, + }); + expect(response.statusCode).toBe(200); + const body = JSON.parse( + response.body + ) as Stock.getStockInterface['Reply']['200']; + expect(body).toEqual({ + name: '민트초코 시럽', + price: '3000', + amount: 1000, + currentAmount: 1000, + unit: 'ml', + }); +}); + +test('get stock detail fail', async () => { + const response = await app.inject({ + method: 'GET', + url: `/api/stock/456784353456`, + headers: { + authorization: `Bearer ${accessToken}`, + storeid: seedValues.store.id.toString(), + }, + }); + expect(response.statusCode).toBe(404); +}); + +test('get mixed stock list', async () => { + const response = await app.inject({ + method: 'GET', + url: `/api/stock/mixed`, + headers: { + authorization: `Bearer ${accessToken}`, + storeid: seedValues.store.id.toString(), + }, + }); + expect(response.statusCode).toBe(200); + const body = JSON.parse( + response.body + ) as Stock.getMixedStockListInterface['Reply']['200']; + const mintChocoChung = body.mixedStocks.find((stock) => stock.name === '민트초코 청'); + expect(mintChocoChung).toEqual({ + id: expect.any(Number), + name: '민트초코 청' + }); +}); + +test('update mixed stock', async () => { + const response = await app.inject({ + method: 'PUT', + url: `/api/stock/mixed`, + headers: { + authorization: `Bearer ${accessToken}`, + storeid: seedValues.store.id.toString(), + }, + body: { + id: mintChocoChung, + name: '민트초코 청', + totalAmount: 2200, + unit: 'g', + mixing: [ + { + id: mintChoco, + unit: 'ml', + amount: 1000, + }, + { + id: sugar, + unit: 'g', + amount: 1000, + }, + { + id:chocoSyrup, + unit: 'ml', + amount: 200, + } + ] + }, + }); + expect(response.statusCode).toBe(201); + const body = JSON.parse( + response.body + ) as Stock.updateMixedStockInterface['Reply']['201']; + expect(body).toEqual({ + mixedStockId: mintChocoChung, + }); +}); + +test('get mixed stock detail', async () => { + const response = await app.inject({ + method: 'GET', + url: `/api/stock/mixed/${mintChocoChung}`, + headers: { + authorization: `Bearer ${accessToken}`, + storeid: seedValues.store.id.toString(), + }, + }); + expect(response.statusCode).toBe(200); + const body = JSON.parse( + response.body + ) as Stock.getMixedStockInterface['Reply']['200']; + expect(body).toEqual({ + name: '민트초코 청', + totalAmount: 2200, + unit: 'g', + mixing: [ + { + id: mintChoco, + name: '민트초코 시럽', + amount: 1000, + unit: 'ml' + }, + { + id: sugar, + name: '설탕', + amount: 1000, + unit: 'g' + }, + { + id: chocoSyrup, + name: '초코시럽', + amount: 200, + unit: 'ml' + } + ] + }); +}); + +test('get mixed stock detail fail', async () => { + const response = await app.inject({ + method: 'GET', + url: `/api/stock/mixed/456784353456`, + headers: { + authorization: `Bearer ${accessToken}`, + storeid: seedValues.store.id.toString(), + }, + }); + expect(response.statusCode).toBe(404); +}); + let sparklingWater:number; test('search stock',async () => { const response = await app.inject({ method: 'GET', - url: `/api/menu/stock?name=탄산수`, + url: `/api/stock/withMixed/search?name=탄산수`, headers: { authorization: `Bearer ${accessToken}`, storeid: seedValues.store.id.toString(), }, }); expect(response.statusCode).toBe(200); - const body = JSON.parse(response.body) as Menu.searchStockInterface['Reply']['200']; + const body = JSON.parse(response.body) as Stock.searchStockAndMixedStockInterface['Reply']['200']; sparklingWater = body.stocks[0].id; }); +test('search stock and mixed stock',async () => { + const response = await app.inject({ + method: 'GET', + url: `/api/stock/withMixed/search?name=민트초코`, + headers: { + authorization: `Bearer ${accessToken}`, + storeid: seedValues.store.id.toString(), + }, + }); + expect(response.statusCode).toBe(200); + const body = JSON.parse(response.body) as Stock.searchStockAndMixedStockInterface['Reply']['200']; + expect(body.stocks.length).toBeGreaterThanOrEqual(2); +}); + test('new menu without option', async () => { const response = await app.inject({ method: 'POST', @@ -157,8 +458,8 @@ test('new menu without option', async () => { coldRegularAmount: 150, }, { - id:mintChoco, - isMixed: false, + id:mintChocoChung, + isMixed: true, unit: 'ml', coldRegularAmount: 50, } @@ -353,7 +654,12 @@ test('update menu', async () => { price: 2500, categoryId: 2, option: [1, 3, 5, 6], - recipe: [], + recipe: [{ + id:sparklingWater, + isMixed: false, + unit: 'ml', + coldRegularAmount: 150, + }] }, }); expect(response.statusCode).toBe(201); @@ -365,7 +671,30 @@ test('update menu', async () => { }); }); -test('update menu without option', async () => { +test('create menu without option and recipe', async () => { + const response = await app.inject({ + method: 'POST', + url: `/api/menu`, + headers: { + authorization: `Bearer ${accessToken}`, + storeid: seedValues.store.id.toString(), + }, + body: { + name: '오렌지에이드', + price: 2500, + categoryId: 2, + }, + }); + expect(response.statusCode).toBe(201); + const body = JSON.parse( + response.body + ) as Menu.updateMenuInterface['Reply']['201']; + expect(body).toEqual({ + menuId: 47, + }); +}); + +test('update menu without option and recipe', async () => { const response = await app.inject({ method: 'PUT', url: `/api/menu`, @@ -374,11 +703,10 @@ test('update menu without option', async () => { storeid: seedValues.store.id.toString(), }, body: { - id: 46, + id: 47, name: '오렌지에이드', price: 2500, categoryId: 2, - recipe: [], }, }); expect(response.statusCode).toBe(201); @@ -386,7 +714,7 @@ test('update menu without option', async () => { response.body ) as Menu.updateMenuInterface['Reply']['201']; expect(body).toEqual({ - menuId: 46, + menuId: 47, }); });