Skip to content

Commit

Permalink
Feat: stock change history (#89)
Browse files Browse the repository at this point in the history
* feat: add log at stock, menu detail dto

* test: add stock history

* feat: add stock history model

* feat: implement stock history

* feat: implement menu cost history
  • Loading branch information
raipen authored Nov 22, 2023
1 parent cc8446a commit bd7b4fc
Show file tree
Hide file tree
Showing 12 changed files with 293 additions and 9 deletions.
13 changes: 13 additions & 0 deletions prisma/migrations/20231121112401_init/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE `StockHistory` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`stockId` INTEGER NOT NULL,
`amount` INTEGER NOT NULL,
`price` DECIMAL(65, 30) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),

PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- AddForeignKey
ALTER TABLE `StockHistory` ADD CONSTRAINT `StockHistory_stockId_fkey` FOREIGN KEY (`stockId`) REFERENCES `Stock`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
13 changes: 12 additions & 1 deletion src/DTO/menu.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const getMenuSchema = {
200: {
type: 'object',
description: 'success response',
required: ['category', 'categoryId', 'name', 'price', 'option', 'recipe'],
required: ['category', 'categoryId', 'name', 'price', 'option', 'recipe','history'],
properties: {
category: { type: 'string' },
categoryId: { type: 'number' },
Expand Down Expand Up @@ -124,6 +124,17 @@ export const getMenuSchema = {
},
},
},
history: {
type: 'array',
items: {
type: 'object',
required: ['date', 'price'],
properties: {
date: { type: 'string', format: 'date-time' },
price: { type: 'string' },
},
},
},
},
},
...errorSchema(
Expand Down
18 changes: 17 additions & 1 deletion src/DTO/stock.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export const getStockSchema = {
'currentAmount',
'noticeThreshold',
'updatedAt',
'history'
],
properties: {
name: { type: 'string' },
Expand All @@ -168,6 +169,18 @@ export const getStockSchema = {
currentAmount: { type: 'number', nullable: true },
noticeThreshold: { type: 'number' },
updatedAt: { type: 'string' },
history: {
type: 'array',
items: {
type: 'object',
required: ['amount', 'date', 'price'],
properties: {
amount: { type: 'number' },
date: { type: 'string', format: 'date-time' },
price: { type: 'string' },
},
},
}
},
},
...errorSchema(
Expand Down Expand Up @@ -460,7 +473,10 @@ export type softDeleteStockInterface = SchemaToInterface<
export type getStockListInterface = SchemaToInterface<
typeof getStockListSchema
> & { Body: { storeId: number; userId: number } };
export type getStockInterface = SchemaToInterface<typeof getStockSchema> & {
export type getStockInterface = SchemaToInterface<
typeof getStockSchema,
[{ pattern: { type: 'string'; format: 'date-time' }; output: Date }]
> & {
Body: { storeId: number; userId: number };
};
export type createMixedStockInterface = SchemaToInterface<
Expand Down
2 changes: 0 additions & 2 deletions src/api/hooks/onError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,4 @@ export default (
}

reply.code(500).send();
// test 폴더에 있는 hookTest.test.ts 파일에서 test
// test 이름은 human error
};
10 changes: 10 additions & 0 deletions src/models/Stock.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ model Stock {
storeId Int
recipes Recipe[]
mixings Mixing[]
history StockHistory[]
name String
amount Int?
unit String?
Expand All @@ -14,6 +15,15 @@ model Stock {
deletedAt DateTime?
}

model StockHistory {
id Int @id @default(autoincrement())
stockId Int
stock Stock @relation(fields: [stockId], references: [id])
amount Int
price Decimal
createdAt DateTime @default(now())
}

model MixedStock {
id Int @id @default(autoincrement())
store Store @relation(fields: [storeId], references: [id])
Expand Down
132 changes: 128 additions & 4 deletions src/services/menuService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PrismaClient } from '@prisma/client';
import { Prisma,PrismaClient } from '@prisma/client';
import { NotFoundError } from '@errors';
import * as Menu from '@DTO/menu.dto';
import { STATUS, getStockStatus } from '@utils/stockStatus';
Expand Down Expand Up @@ -90,14 +90,116 @@ export default {
return { categories: result };
},

async getCostHistory(recipes: Prisma.RecipeGetPayload<{
include: {
stock: {
include: {
history: true,
},
},
mixedStock: {
include: {
mixings: {
include: {
stock: {
include: {
history: true,
}
}
}
}
},
}
}
}>[])
:Promise<Menu.getMenuInterface['Reply']['200']['history']> {
const usingStocks = recipes.filter(({ stock }) => stock!==null&&stock!.noticeThreshold>=0);
if(usingStocks.some(({ stock }) => stock!.currentAmount===null || stock!.amount===null || stock!.price===null))
return [];

const usingMixedStocks = recipes.filter(({ mixedStock }) => mixedStock!==null);
if(usingMixedStocks.some(({ mixedStock }) => mixedStock!.totalAmount===null))
return [];

Check warning on line 122 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 122 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

if(usingMixedStocks.some(({ mixedStock }) => mixedStock!.mixings.some(({ stock }) => stock.currentAmount===null || stock.amount===null || stock.price===null)))
return [];

Check warning on line 125 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 125 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

const stockHistory = usingStocks
.map(({ stock,coldRegularAmount }) => ({
id: stock!.id,
history: stock!.history
.map(({ createdAt,amount,price }) => ({
date: createdAt.toISOString().split('T')[0],
price: coldRegularAmount!*price.toNumber()/amount,
})),
}));

const stockInMixedStocksHistory = usingMixedStocks
.flatMap(({ mixedStock,coldRegularAmount }) =>{
if(mixedStock===null) return [];

Check warning on line 139 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 139 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
const totalAmount = mixedStock!.totalAmount!;
return mixedStock!.mixings.map(({ stock,amount }) => (
{
id: stock.id,
history: stock.history.map(({ createdAt,amount: historyAmount,price }) => ({
date: createdAt.toISOString().split('T')[0],
price: coldRegularAmount!*amount*price.toNumber()/historyAmount/totalAmount,
})),
}
));
});

const allHistory = stockHistory.concat(stockInMixedStocksHistory).map(({history})=>history)
.map((history) => history.reduce((acc, {date,price})=>{
const sameDateIndex = acc.findIndex((history)=>history.date===date);

Check warning on line 154 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 154 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
if(sameDateIndex!==-1){
acc[sameDateIndex].price=price;

Check warning on line 156 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
return acc;

Check warning on line 157 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 158 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
acc.push({date,price});
return acc;
},[] as {date:string,price:number}[])
);
const [initPrice,initDate] = allHistory.reduce((acc, history) => {
const initPrice = history[0].price;
const initDate = history[0].date;
return [acc[0]+initPrice,acc[1].localeCompare(initDate)<0?initDate:acc[1]];
}, [0,'1900-01-01'] as [number,string]);
const updateHistory = allHistory.reduce((acc, history) => {
for (let i = 1; i < history.length; i++) {
const currentDate = history[i].date;

Check warning on line 170 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
const currentPrice = history[i].price;

Check warning on line 171 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
const previousPrice = history[i-1].price;

Check warning on line 172 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
const priceDifference = currentPrice - previousPrice;

Check warning on line 173 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
if (acc[currentDate]) {
acc[currentDate] += priceDifference;

Check warning on line 175 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
} else {
acc[currentDate] = priceDifference;

Check warning on line 177 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}

Check warning on line 178 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement

Check warning on line 178 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch

Check warning on line 178 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🌿 Branch is not covered

Warning! Not covered branch
}
return acc;
}, {} as Record<string, number>);
const sortedHistory = Object.entries(updateHistory).sort(([date1], [date2]) => {

Check warning on line 182 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
return date1.localeCompare(date2);

Check warning on line 183 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
})
const accumulatedHistory = sortedHistory.reduce((acc, [date, price]) => {

Check warning on line 185 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🕹️ Function is not covered

Warning! Not covered function
const curPrice = acc[acc.length - 1].price;

Check warning on line 186 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
acc.push({
date,
price: curPrice + price,
});

Check warning on line 190 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
return acc;

Check warning on line 191 in src/services/menuService.ts

View workflow job for this annotation

GitHub Actions / Coverage annotations (🧪 jest-coverage-report-action)

🧾 Statement is not covered

Warning! Not covered statement
}, [{ date: initDate, price: initPrice }]);
return accumulatedHistory.filter(({date}) => date.localeCompare(initDate)>=0).map(({date,price})=>({date,price:price.toFixed(2)}));
},

async getMenu(
{ storeId }: Menu.getMenuInterface['Body'],
{ menuId }: Menu.getMenuInterface['Params']
): Promise<Menu.getMenuInterface['Reply']['200']> {
const menu = await prisma.menu.findUnique({
where: {
id: menuId,
deletedAt: null,
},
include: {
optionMenu: {
Expand All @@ -110,8 +212,29 @@ export default {
category: true,
recipes: {
include: {
stock: true,
mixedStock: true,
stock: {
include: {
history: true,
},
},
mixedStock: {
include: {
mixings: {
include: {
stock: {
include: {
history: true,
}
}
},
where: {
stock: {
deletedAt: null,
},
},
}
},
}
},
where: {
OR: [
Expand Down Expand Up @@ -197,6 +320,7 @@ export default {
})
),
recipe,
history: await this.getCostHistory(menu.recipes),
};
},
async getOptionList({
Expand Down
28 changes: 28 additions & 0 deletions src/services/stockService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ export default {
},
});

if(price !== null&& price !== undefined&& amount !== null&& amount !== undefined){
await prisma.stockHistory.create({
data: {
stockId: result.id,
amount,
price,
},
});
}

return {
stockId: result.id,
};
Expand Down Expand Up @@ -61,6 +71,16 @@ export default {
},
});

if(price !== null&& price !== undefined&& amount !== null&& amount !== undefined){
await prisma.stockHistory.create({
data: {
stockId: result.id,
amount,
price,
},
});
}

return {
stockId: result.id,
};
Expand Down Expand Up @@ -168,6 +188,9 @@ export default {
storeId,
deletedAt: null,
},
include: {
history: true,
},
});
if (!result) {
throw new NotFoundError('재고가 존재하지 않습니다.', '재고');
Expand All @@ -181,6 +204,11 @@ export default {
noticeThreshold: result.noticeThreshold,
unit: result.unit,
updatedAt: result.updatedAt.toISOString(),
history: result.history.map(({ createdAt, amount, price }) => ({
date: createdAt,
amount,
price: price.toString(),
})),
};
},

Expand Down
2 changes: 2 additions & 0 deletions test/integration/0. init/getBeforeRegister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default (app: FastifyInstance) => () => {
currentAmount: null,
noticeThreshold: -1,
updatedAt: expect.any(String),
history: []
}
);
});
Expand Down Expand Up @@ -139,6 +140,7 @@ export default (app: FastifyInstance) => () => {
price: "2500",
category: '커피',
categoryId: testValues.coffeeCategoryId,
history: [],
option: [
{
optionType: "온도",
Expand Down
11 changes: 10 additions & 1 deletion test/integration/1. register/stock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default (app: FastifyInstance) => () => {
currentAmount: 3000,
noticeThreshold: 500,
amount: 2800,
price: 26900,
price: "26900",
},
});
const data = JSON.parse(res.body) as Stock.createStockInterface['Reply']['201'];
Expand Down Expand Up @@ -65,6 +65,8 @@ export default (app: FastifyInstance) => () => {
unit: 'g',
currentAmount: 3000,
noticeThreshold: 500,
amount: 1000,
price: "2400",
},
});
const data = JSON.parse(res.body) as Stock.createStockInterface['Reply']['201'];
Expand Down Expand Up @@ -106,6 +108,13 @@ export default (app: FastifyInstance) => () => {
const data = JSON.parse(res.body) as Stock.getStockInterface['Reply']['200'];
expect(res.statusCode).toEqual(200);
expect(data).toHaveProperty('name', '자몽');
expect(data.history).toEqual([
{
date: expect.any(String),
amount: 2800,
price: "26900",
}
]);
});

test('get stock detail:lemon', async () => {
Expand Down
Loading

0 comments on commit bd7b4fc

Please sign in to comment.