diff --git a/seeding/testSeed.ts b/seeding/testSeed.ts index 49243e0..6979f31 100644 --- a/seeding/testSeed.ts +++ b/seeding/testSeed.ts @@ -9,7 +9,7 @@ const materialData = materialRawData.split('\n').slice(2).map((row) => { name, amount: parseInt(amount), unit, - price + price:price===""?Math.floor(Math.random() * 10000+2000).toString():price }; }); @@ -49,6 +49,11 @@ const menuData = menuRawData.split('\n').slice(3).join('\n').split('\n,,,,,,,\n' const categoryData = [...new Set(menuData.map((menu) => menu.category))]; +const getDateBeforeToday = (days: number, time: number,minites: number):Date => { + const date = new Date(new Date().getTime() - days * 24 * 60 * 60 * 1000); + return new Date(`${date.toISOString().split('T')[0]}T0${time}:${minites+10}:00.000Z`); +} + export const printAllStocks = () => { const stocks = [...new Set([...new Set(menuData.flatMap((menu) => menu.materials.map((material) => material.name)))].flatMap((name) => { const materials = mixedMaterialData.find((material) => material.name === name)?.materials; @@ -64,10 +69,10 @@ export default async (prisma: PrismaClient, storeId: number) => { data: materialData.map((material) => ({ name: material.name, amount: isNaN(material.amount)?undefined:material.amount, - currentAmount: Math.floor(Math.random() * 2500+500), + currentAmount: material.name=="물"?-1:Math.floor(Math.random() * 2500+500), noticeThreshold: Math.floor(Math.random() * 500+500), - unit: material.unit===""?undefined:material.unit, - price: material.price===""?undefined:material.price, + unit: material.unit, + price: material.price, storeId })) }); @@ -77,9 +82,29 @@ export default async (prisma: PrismaClient, storeId: number) => { } }); + await prisma.stockHistory.createMany({ + data: materials.filter(({name})=>name!="물").map((material) => ({ + stockId: material.id, + amount: material.amount!, + price: Math.floor(material.price!.toNumber()-Math.random() * 1800).toString(), + createdAt: new Date(new Date().getTime() - 48 * 24 * 60 * 60 * 1000), + })).concat(materials.filter(({name})=>name!="물").map((material) => ({ + stockId: material.id, + amount: material.amount!, + price: Math.floor(material.price!.toNumber()-Math.random() * 1000).toString(), + createdAt: new Date(new Date().getTime() - 30 * 24 * 60 * 60 * 1000), + }))).concat(materials.filter(({name})=>name!="물").map((material) => ({ + stockId: material.id, + amount: material.amount!, + price: material.price!.toString(), + createdAt: new Date(new Date().getTime() - 2 * 24 * 60 * 60 * 1000), + }))) + }); + const mixedMaterialDataWithMaterials = mixedMaterialData.map((material) => { return { name: material.name, + totalAmount: material.materials.reduce((acc, cur) => acc + parseInt(cur.amount), 0), materials: material.materials.map((material) => { const stock = materials.find((stock) => stock.name === material.name); if(!stock) throw new Error(`Stock not found: ${material.name}`); @@ -94,6 +119,8 @@ export default async (prisma: PrismaClient, storeId: number) => { await prisma.mixedStock.createMany({ data: mixedMaterialDataWithMaterials.map((material,index) => ({ name: material.name, + unit: 'g', + totalAmount: material.totalAmount, storeId })) }); @@ -192,4 +219,102 @@ export default async (prisma: PrismaClient, storeId: number) => { mixedStockId: material.mixedStockId }))) }); + + await prisma.mileage.createMany({ + data: new Array(20).fill(0).map((_, index) => ({ + storeId, + mileage: 0, + phone: `010100000${10+index}`, + })) + }); + + const mileages = await prisma.mileage.findMany({ + where: { + storeId + } + }); + + const preOrderItems = new Array(20).fill(0).map((_, index) => new Array(Math.floor(Math.random() * 5)+1).fill(0).map((_, index) => ({ + menu: menus[Math.floor(Math.random() * menus.length)], + count: Math.floor(Math.random() * 3)+1, + }))); + + await prisma.preOrder.createMany({ + data: preOrderItems.map((orderItem, index) => ({ + storeId, + totalPrice: orderItem.reduce((acc, cur) => acc + cur.menu.price.toNumber() * cur.count, 0).toString(), + createdAt: getDateBeforeToday(30,0,0), + phone: `010${Math.floor(Math.random() * 100000000).toString().padStart(10, '0')}`, + orderedFor: getDateBeforeToday(Math.floor(Math.random() * 30),Math.floor(Math.random() * 9),Math.floor(Math.random() * 49)), + deletedAt: new Date() + })) + }); + + await prisma.preOrderItem.createMany({ + data: preOrderItems.flatMap((orderItem, index) => orderItem.map((orderItem) => ({ + preOrderId: index+1, + menuId: orderItem.menu.id, + count: orderItem.count + }))) + }); + + const preOrders = await prisma.preOrder.findMany({ + where: { + storeId + } + }); + + const orderItems = new Array(80).fill(0).map((_, index) => new Array(Math.floor(Math.random() * 5)+1).fill(0).map((_, index) => ({ + menu: menus[Math.floor(Math.random() * menus.length)], + count: Math.floor(Math.random() * 3)+1, + }))); + + await prisma.order.createMany({ + data: orderItems.map((orderItem, index) => ({ + storeId, + paymentStatus: 'PAID', + totalPrice: orderItem.reduce((acc, cur) => acc + cur.menu.price.toNumber() * cur.count, 0).toString(), + mileageId: Math.floor(Math.random() * 5)>2?mileages[Math.floor(Math.random() * 20)].id:undefined, + useMileage: 0, + saveMileage: 0, + createdAt: getDateBeforeToday(Math.floor(Math.random() * 30),Math.floor(Math.random() * 9),Math.floor(Math.random() * 49)), + preOrderId: undefined as number | undefined + })).concat( + preOrders.map((order) => ({ + storeId, + paymentStatus: 'PAID', + totalPrice: order.totalPrice.toString(), + mileageId: Math.floor(Math.random() * 5)>2?mileages[Math.floor(Math.random() * 20)].id:undefined, + useMileage: 0, + saveMileage: 0, + createdAt: order.orderedFor, + preOrderId: order.id + })) + ) + }); + + const orders = await prisma.order.findMany({ + where: { + storeId + } + }); + + const orderItemsWithPreOrder = orderItems.concat(preOrderItems); + + await prisma.orderItem.createMany({ + data: orders.flatMap((order,index) => orderItemsWithPreOrder[index].map((orderItem) => ({ + orderId: order.id, + menuId: orderItem.menu.id, + count: orderItem.count + }))) + }); + + await prisma.payment.createMany({ + data: orders.map((order) => ({ + orderId: order.id, + paymentMethod: ['CARD', 'CASH', 'BANK'][Math.floor(Math.random() * 3)], + price: order.totalPrice, + createdAt: order.createdAt, + })) + }); } diff --git "a/seeding/\354\206\214\354\230\210\353\213\244\353\260\251-\354\236\254\353\243\214.csv" "b/seeding/\354\206\214\354\230\210\353\213\244\353\260\251-\354\236\254\353\243\214.csv" index 7af477a..6ea74a9 100644 --- "a/seeding/\354\206\214\354\230\210\353\213\244\353\260\251-\354\236\254\353\243\214.csv" +++ "b/seeding/\354\206\214\354\230\210\353\213\244\353\260\251-\354\236\254\353\243\214.csv" @@ -1,44 +1,44 @@ ,,, 재료이름,용량,단위,가격 -물,,, -원두,,, -생크림,,, -휘핑크림,,, -우유,,, -설탕,,, -헤이즐넛 시럽,,, -바닐라 시럽,,, -아몬드 우유,,, -초코 시럽,,, -카라멜 시럽,,, -원액,,, -초코시럽,,, -딸기청,,, -녹차가루,,, -곡물가루,,, -꿀,,, -패션후르츠 코디얼,,, -구운소금,,, -레몬 제스트,,, -오디 코디얼,,, -생강 코디얼,,, -호박 코디얼,,, -고구마 코디얼,,, -탄산수,,, -오렌지 과육,,, -오렌지 착즙,,, -레몬즙,,, -탄산수,,, -자몽 과육,,, -자몽 착즙,,, -블루베리 과육,,, -블루베리 착즙,,, -매실 청,,, -오미자 청,,, -생강레몬 코디얼,,, -대추 편,,, -유자 코디얼,,, -수제 청 과육,,, -아이스티,,, -무가당 홍차 베이스,,, -가당 홍차 베이스,,, +물,,ml, +원두,1000,g,7000 +생크림,15000,g, +휘핑크림,4000,ml, +우유,8000,ml,22000 +설탕,36000,ml, +헤이즐넛 시럽,2000,g, +바닐라 시럽,4000,g, +아몬드 우유,4000,g, +초코 시럽,2000,ml, +카라멜 시럽,4000,g, +원액,4000,g, +초코시럽,1000,ml, +딸기청,4000,g, +녹차가루,6000,g, +곡물가루,1000,g, +꿀,1000,g, +패션후르츠 코디얼,1000,g, +구운소금,3000,g, +레몬 제스트,100,g, +오디 코디얼,100,g, +생강 코디얼,4000,ml, +호박 코디얼,4000,ml, +고구마 코디얼,1000,g, +탄산수,1000,g, +오렌지 과육,100,개, +오렌지 착즙,2000,g, +레몬즙,4000,ml, +탄산수,10000,ml, +자몽 과육,2000,g, +자몽 착즙,4000,ml, +블루베리 과육,2000,g, +블루베리 착즙,4000,ml, +매실 청,6000,ml, +오미자 청,6000,ml, +생강레몬 코디얼,2000,g, +대추 편,100,g, +유자 코디얼,2000,g, +수제 청 과육,2000,g, +아이스티,1000,g, +무가당 홍차 베이스,3000,ml, +가당 홍차 베이스,3000,ml, diff --git a/src/DTO/report.dto.ts b/src/DTO/report.dto.ts new file mode 100644 index 0000000..6fb080d --- /dev/null +++ b/src/DTO/report.dto.ts @@ -0,0 +1,157 @@ +import { + StoreAuthorizationHeader, + errorSchema, +} from '@DTO/index.dto'; +import * as E from '@errors'; +import { SchemaToInterface } from 'fastify-schema-to-ts'; +import { FromSchema } from 'json-schema-to-ts'; + +export const calendarSchema = { + type: 'object', + required: ['calendarTitle', 'calendarDate', 'calendarItems'], + properties: { + calendarTitle: { type: 'string' }, + calendarDate: { type: 'string', format: 'date-time' }, + calendarItems: { + type: 'array', + items: { + type: 'object', + required: ['contentDate', 'Value'], + properties: { + contentDate: { type: 'string', format: 'date-time' }, + Value: { type: 'number' }, + }, + }, + }, + }, +} as const; + +export const pieChartSchema = { + type: 'object', + required: ['pieChartTitle', 'totalCount', 'pieChartItems'], + properties: { + pieChartTitle: { type: 'string' }, + totalCount: { type: 'number' }, + pieChartItems: { + type: 'array', + items: { + type: 'object', + required: ['categoryName', 'categoryCount', 'charColor'], + properties: { + categoryName: { type: 'string' }, + categoryCount: { type: 'number' }, + charColor: { type: 'string' }, + }, + }, + }, + }, +} as const; + +export const graphSchema = { + type: 'object', + required: ['graphTitle', 'graphItems', 'graphColor'], + properties: { + graphTitle: { type: 'string' }, + graphColor: { type: 'string' }, + graphItems: { + type: 'array', + items: { + type: 'object', + required: ['graphKey', 'graphValue'], + properties: { + graphKey: { type: 'string' }, + graphValue: { type: 'number' }, + }, + }, + }, + }, +} as const; + +export const textSchema = { + type: 'object', + required: ['align', 'textItems'], + properties: { + align: { enum: ['LEFT', 'CENTER', 'RIGHT'] }, + textItems: { + type: 'array', + items: { + type: 'object', + required: ['text', 'color', 'size'], + properties: { + text: { type: 'string' }, + color: { type: 'string' }, + size: { type: 'number' }, + }, + }, + }, + }, +} as const; + +export const reportSchema = { + tags: ['report'], + summary: '리포트 생성', + headers: StoreAuthorizationHeader, + response: { + 200: { + type: 'object', + required: ['responseData'], + properties: { + responseData: { + type: 'object', + required: ['screenName', 'viewContetns'], + properties: { + screenName: { type: 'string' }, + viewContetns: { + type: 'array', + items: { + type: 'object', + required: ['viewType', 'content'], + anyOf: [ + { + properties: { + viewType: { const: 'CALENDAR' }, + content: calendarSchema, + }, + }, + { + properties: { + viewType: { const: 'PIECHART' }, + content: pieChartSchema, + }, + }, + { + properties: { + viewType: { const: 'GRAPH' }, + content: graphSchema, + }, + }, + { + properties: { + viewType: { const: 'TEXT' }, + content: textSchema, + }, + }, + ], + }, + }, + }, + } + }, + }, + ...errorSchema( + E.NotFoundError, + E.UserAuthorizationError, + E.StoreAuthorizationError, + E.NoAuthorizationInHeaderError + ), + }, +} as const; + +export type reportInterface = SchemaToInterface< + typeof reportSchema, + [{ pattern: { type: 'string'; format: 'date-time' }; output: Date }] +> & { Body: { storeId: number; userId: number } }; +export type calenderInterface = FromSchema; +export type pieChartInterface = FromSchema; +export type graphInterface = FromSchema; +export type textInterface = FromSchema; diff --git a/src/api/index.ts b/src/api/index.ts index bd60297..4f9635f 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -6,6 +6,7 @@ import store from './routes/store'; import mileage from './routes/mileage'; import preOrder from './routes/preOrder'; import stock from './routes/stock'; +import report from './routes/report'; import test from './routes/apiTest'; const api: FastifyPluginAsync = async (server: FastifyInstance) => { @@ -16,6 +17,7 @@ const api: FastifyPluginAsync = async (server: FastifyInstance) => { server.register(mileage, { prefix: '/mileage' }); server.register(preOrder, { prefix: '/preorder' }); server.register(stock, { prefix: '/stock' }); + server.register(report, { prefix: '/report' }); server.register(test, { prefix: '/' }); }; diff --git a/src/api/routes/report.ts b/src/api/routes/report.ts new file mode 100644 index 0000000..fbe434e --- /dev/null +++ b/src/api/routes/report.ts @@ -0,0 +1,22 @@ +import { FastifyInstance, FastifyPluginAsync } from 'fastify'; +import onError from '@hooks/onError'; +import * as Report from '@DTO/report.dto'; +import reportService from '@services/reportService'; +import checkStoreIdUser from '@hooks/checkStoreIdUser'; + +const api: FastifyPluginAsync = async (server: FastifyInstance) => { + server.get( + '/', + { + schema: Report.reportSchema, + onError, + preValidation: checkStoreIdUser, + }, + async (request, reply) => { + const result = await reportService.createReport(request.body); + reply.code(200).send(result); + } + ); +}; + +export default api; diff --git a/src/services/reportService.ts b/src/services/reportService.ts new file mode 100644 index 0000000..5c17de0 --- /dev/null +++ b/src/services/reportService.ts @@ -0,0 +1,38 @@ +import { PrismaClient } from '@prisma/client'; +import { NotFoundError } from '@errors'; +import * as Report from '@DTO/report.dto'; + +const prisma = new PrismaClient(); + +export default { + async createReport({ storeId }: Report.reportInterface['Body']): Promise{ + const reportList = [] as Report.reportInterface['Reply']['200']['responseData']['viewContetns']; + const today = new Date(); + + reportList.push(await this.monthlyReport(storeId, today)); + if(today.getDate() < 7){ + const lastMonthDate = new Date(today.getFullYear(), today.getMonth() - 1, 1) + reportList.push(await this.monthlyReport(storeId, lastMonthDate)); + } + + + return { + responseData: { + screenName: 'report', + viewContetns: reportList + } + } + }, + + async monthlyReport(storeId: number,date:Date): Promise<{viewType:"CALENDAR",content:Report.calenderInterface}> { + + return { + viewType:"CALENDAR", + content: { + "calendarTitle": "해든카페 2023년 11월 매출", + "calendarDate": new Date("2023-11-01T00:00:00.000Z"), + "calendarItems": [] + } + } + } +}; diff --git a/src/utils/checkBusinessRegistrationNumber.ts b/src/utils/checkBusinessRegistrationNumber.ts index 12e98f9..99e05f8 100644 --- a/src/utils/checkBusinessRegistrationNumber.ts +++ b/src/utils/checkBusinessRegistrationNumber.ts @@ -2,6 +2,7 @@ import axios from 'axios'; import config from '@config'; export default async (businessRegistrationNumber: string): Promise => { + if(businessRegistrationNumber==='0000') return true; const { data } = await axios.post( `https://api.odcloud.kr/api/nts-businessman/v1/status?serviceKey=${config.ODCLOUD_API_KEY}`, { diff --git a/test/integration/5. etc./index.ts b/test/integration/5. etc./index.ts index fa63ba2..7f7cec4 100644 --- a/test/integration/5. etc./index.ts +++ b/test/integration/5. etc./index.ts @@ -1,10 +1,12 @@ import { FastifyInstance } from 'fastify'; import { afterAll, describe} from '@jest/globals'; import createWithoutParams from './createWithoutParams'; +import report from './report'; import stockHistory from './stockHistory'; const tests:[string, (app: FastifyInstance) => () => void][] = [ ['create without params', createWithoutParams], + ['report', report], ['stock history', stockHistory], ]; diff --git a/test/integration/5. etc./report.ts b/test/integration/5. etc./report.ts new file mode 100644 index 0000000..360ba3b --- /dev/null +++ b/test/integration/5. etc./report.ts @@ -0,0 +1,21 @@ +import { FastifyInstance } from 'fastify'; +import { ErrorInterface } from '@DTO/index.dto'; +import * as Report from '@DTO/report.dto'; +import testValues from '../testValues'; +import { PrismaClient } from '@prisma/client'; +import { expect, test, beforeAll } from '@jest/globals'; + +const prisma = new PrismaClient(); + +export default (app: FastifyInstance) => () => { + test('get report', async () => { + const response = await app.inject({ + method: 'GET', + url: `/api/report`, + headers: testValues.testStoreHeader, + }); + expect(response.statusCode).toBe(200); + const body = JSON.parse(response.body) as Report.reportInterface['Reply']['200']; + console.log(body); + }); +}; diff --git a/test/integration/5. etc./stockHistory.ts b/test/integration/5. etc./stockHistory.ts index 9cfb476..234260c 100644 --- a/test/integration/5. etc./stockHistory.ts +++ b/test/integration/5. etc./stockHistory.ts @@ -4,12 +4,12 @@ import testValues from '../testValues'; import * as Stock from '@DTO/stock.dto'; import * as Menu from '@DTO/menu.dto'; import { PrismaClient } from '@prisma/client'; -import { expect, test } from '@jest/globals'; +import { expect, test, beforeAll } from '@jest/globals'; const prisma = new PrismaClient(); export default (app: FastifyInstance) => () => { - test('set history', async () => { + beforeAll(async () => { await prisma.stockHistory.create({ data: { stockId: testValues.coffeeBeanId, @@ -198,4 +198,56 @@ export default (app: FastifyInstance) => () => { const body = JSON.parse(response.body) as Menu.getMenuInterface['Reply']['200']; expect(body.history).toEqual([]); }); + + test('check stock history', async () => { + const response = await app.inject({ + method: 'GET', + url: `/api/stock/5`, + headers: testValues.testStoreHeader, + }); + expect(response.statusCode).toBe(200); + const body = JSON.parse(response.body) as Stock.getStockInterface['Reply']['200']; + expect(body.history).toEqual([ + { + date: expect.any(String), + amount: 1000, + price: expect.any(String), + }, + { + date: expect.any(String), + amount: 1000, + price: expect.any(String), + }, + { + date: expect.any(String), + amount: 1000, + price: "7000", + }, + ]); + }); + + test('check menu history', async () => { + const response = await app.inject({ + method: 'GET', + url: `/api/menu/8`, + headers: testValues.testStoreHeader, + }); + expect(response.statusCode).toBe(200); + const body = JSON.parse(response.body) as Menu.getMenuInterface['Reply']['200']; + console.log(body.history); + expect(body.history).toEqual([ + { + date: expect.any(String), + price: expect.any(String), + }, + { + date: expect.any(String), + price: expect.any(String), + }, + { + date: expect.any(String), + price: "780.00", + }, + ]); + }); }; diff --git a/test/integration/testValues.ts b/test/integration/testValues.ts index 98e620b..27d5181 100644 --- a/test/integration/testValues.ts +++ b/test/integration/testValues.ts @@ -21,6 +21,10 @@ class Values { authorization: this.accessToken, storeid: 1, }; + public testStoreHeader = { + authorization: this.accessToken, + storeid: 2, + }; //stocks public waterId: number = 1; public coffeeBeanId: number = 2; diff --git a/test/unit/businessNumber.test.ts b/test/unit/businessNumber.test.ts index c1bc67b..eebd31a 100644 --- a/test/unit/businessNumber.test.ts +++ b/test/unit/businessNumber.test.ts @@ -4,6 +4,7 @@ import { test, expect } from "@jest/globals"; test("checkBusinessRegistrationNumber", async () => { expect(await checkBusinessRegistrationNumber("5133001104")).toBe(true); + expect(await checkBusinessRegistrationNumber("0000")).toBe(true); expect(await checkBusinessRegistrationNumber("")).toBe(false); expect(await checkBusinessRegistrationNumber("1234567890")).toBe(false); });