Skip to content

Commit

Permalink
Merge branch 'main' into issue/118
Browse files Browse the repository at this point in the history
  • Loading branch information
toririm committed Sep 27, 2024
2 parents fe62a15 + 7d9c220 commit ab1b979
Show file tree
Hide file tree
Showing 13 changed files with 301 additions and 70 deletions.
10 changes: 5 additions & 5 deletions app/models/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const itemSchema = z.object({
required_error: "種類が未選択です",
invalid_type_error: "不正な種類です",
}),
assignee: z.string().nullable(),
});

export type Item = z.infer<typeof itemSchema>;
Expand All @@ -25,18 +26,16 @@ export const type2label = {
} as const satisfies Record<ItemType, string>;

export class ItemEntity implements Item {
// TODO(toririm)
// ゲッターやセッターを使う際にはすべてのプロパティにアンスコをつけてprivateにする
// 実装の詳細は OrderEntity を参照
private constructor(
public readonly id: string | undefined,
public readonly name: string,
public readonly price: number,
public readonly type: ItemType,
public assignee: string | null,
) {}

static createNew({ name, price, type }: Item): ItemEntity {
return new ItemEntity(undefined, name, price, type);
static createNew({ name, price, type }: Omit<Item, "assignee">): ItemEntity {
return new ItemEntity(undefined, name, price, type, null);
}

static fromItem(item: WithId<Item>): WithId<ItemEntity> {
Expand All @@ -45,6 +44,7 @@ export class ItemEntity implements Item {
item.name,
item.price,
item.type,
item.assignee,
) as WithId<ItemEntity>;
}
}
69 changes: 65 additions & 4 deletions app/models/order.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { describe, expect, test } from "bun:test";
import type { WithId } from "~/lib/typeguard";
import type { ItemEntity } from "./item";
import { type Item, ItemEntity } from "./item";
import { OrderEntity } from "./order";

describe("[unit] order entity", () => {
test("order total auto calc", () => {
test("total auto calc", () => {
const order = OrderEntity.createNew({ orderId: 2024 });
expect(order.total).toBe(0);

Expand All @@ -18,12 +18,14 @@ describe("[unit] order entity", () => {
name: "item1",
price: 100,
type: "hot",
assignee: null,
},
{
id: "2",
name: "item2",
price: 341,
type: "ice",
assignee: null,
},
];
order.items.push(...items);
Expand All @@ -34,24 +36,83 @@ describe("[unit] order entity", () => {
name: "item3",
price: 100,
type: "ore",
assignee: null,
});
expect(order.total).toBe(541);
});

test("order beReady", () => {
test("beReady", () => {
const order = OrderEntity.createNew({ orderId: 2024 });
expect(order.orderReady).toBe(false);

order.beReady();
expect(order.orderReady).toBe(true);
});

test("order beServed", () => {
test("beServed", () => {
const order = OrderEntity.createNew({ orderId: 2024 });
expect(order.servedAt).toBe(null);

order.beServed();
expect(order.servedAt).not.toBe(null);
expect(order.servedAt).toBeInstanceOf(Date);
});

test("billingAmount", () => {
const order = OrderEntity.createNew({ orderId: 2024 });
expect(order.billingAmount).toBe(0);

const items: WithId<Item>[] = [
{
id: "1",
name: "item1",
price: 400,
type: "hot",
assignee: null,
},
{
id: "2",
name: "item2",
price: 500,
type: "ice",
assignee: null,
},
];
const itemEntities = items.map((item) => ItemEntity.fromItem(item));

order.items = itemEntities;
expect(order.billingAmount).toBe(900);

const previousOrder = OrderEntity.fromOrder({
id: "1",
orderId: 99999,
createdAt: new Date(),
servedAt: null,
items: itemEntities.slice(0, 1),
total: 900,
orderReady: false,
description: null,
billingAmount: 900,
received: 0,
discountInfo: {
previousOrderId: null,
validCups: 0,
discount: 0,
},
});

order.applyDiscount(previousOrder);
expect(order.discountInfo.previousOrderId).toBe(99999);
expect(order.discountInfo.validCups).toBe(1);
expect(order.discountInfo.discount).toBe(100);
expect(order.billingAmount).toBe(800);
});

test("received", () => {
const order = OrderEntity.createNew({ orderId: 2024 });
expect(order.received).toBe(0);

order.received = 1000;
expect(order.received).toBe(1000);
});
});
106 changes: 93 additions & 13 deletions app/models/order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,39 @@ export const orderSchema = z.object({
createdAt: z.date(),
servedAt: z.date().nullable(),
items: z.array(itemSchema.required()),
assignee: z.string().nullable(),
total: z.number(),
total: z.number(), // sum of item.price
orderReady: z.boolean(),
description: z.string().nullable(),
billingAmount: z.number(), // total - discount
received: z.number(), // お預かり金額
discountInfo: z.object({
previousOrderId: z.number().nullable(),
validCups: z.number(), // min(this.items.length, previousOrder.items.length)
discount: z.number(), // validCups * 100
}),
});

export type Order = z.infer<typeof orderSchema>;
type DiscountInfo = Order["discountInfo"];

const DISCOUNT_RATE_PER_CUP = 100;

// OrderEntity の内部でのみ使うクラス
class DiscountInfoEntity implements DiscountInfo {
constructor(
readonly previousOrderId: number | null,
readonly validCups: number,
readonly discount: number,
) {}

static fromDiscountInfo(discountInfo: DiscountInfo): DiscountInfoEntity {
return new DiscountInfoEntity(
discountInfo.previousOrderId,
discountInfo.validCups,
discountInfo.discount,
);
}
}

export class OrderEntity implements Order {
// 全てのプロパティを private にして外部からの直接アクセスを禁止
Expand All @@ -23,9 +50,16 @@ export class OrderEntity implements Order {
private readonly _createdAt: Date,
private _servedAt: Date | null,
private _items: WithId<ItemEntity>[],
private _assignee: string | null,
private _total: number,
private _orderReady: boolean,
private _description: string | null,
private _billingAmount: number,
private _received: number,
private _discountInfo: DiscountInfoEntity = new DiscountInfoEntity(
null,
0,
0,
),
) {}

static createNew({ orderId }: { orderId: number }): OrderEntity {
Expand All @@ -35,9 +69,11 @@ export class OrderEntity implements Order {
new Date(),
null,
[],
null,
0,
false,
null,
0,
0,
);
}

Expand All @@ -48,9 +84,12 @@ export class OrderEntity implements Order {
order.createdAt,
order.servedAt,
order.items,
order.assignee,
order.total,
order.orderReady,
order.description,
order.billingAmount,
order.received,
DiscountInfoEntity.fromDiscountInfo(order.discountInfo),
) as WithId<OrderEntity>;
}

Expand Down Expand Up @@ -81,13 +120,6 @@ export class OrderEntity implements Order {
this._items = items;
}

get assignee() {
return this._assignee;
}
set assignee(assignee: string | null) {
this._assignee = assignee;
}

get total() {
// items の更新に合わせて total を自動で計算する
// その代わり total は直接更新できない
Expand All @@ -100,10 +132,39 @@ export class OrderEntity implements Order {
return this._orderReady;
}

get description() {
return this._description;
}
set description(description: string | null) {
this._description = description;
}

get billingAmount() {
this._billingAmount = this.total - this._discountInfo.discount;
return this._billingAmount;
}

get received() {
return this._received;
}
set received(received: number) {
this._received = received;
}

get discountInfo() {
return this._discountInfo;
}

// --------------------------------------------------
// methods
// --------------------------------------------------

_getCoffeeCount() {
// milk 以外のアイテムの数を返す
// TODO(toririm): このメソッドは items が変更された時だけでいい
return this.items.filter((item) => item.type !== "milk").length;
}

beReady() {
// orderReady は false -> true にしか変更できないようにする
this._orderReady = true;
Expand All @@ -114,16 +175,35 @@ export class OrderEntity implements Order {
this._servedAt = new Date();
}

/* このメソッドのみで discountInfo を更新する */
applyDiscount(previousOrder: OrderEntity) {
const validCups = Math.min(
this._getCoffeeCount(),
previousOrder._getCoffeeCount(),
);
const discount = validCups * DISCOUNT_RATE_PER_CUP;

this._discountInfo = DiscountInfoEntity.fromDiscountInfo({
previousOrderId: previousOrder.orderId,
validCups,
discount,
});
return this._discountInfo;
}

toOrder(): Order {
return {
id: this.id,
orderId: this.orderId,
createdAt: this.createdAt,
servedAt: this.servedAt,
items: this.items,
assignee: this.assignee,
total: this.total,
orderReady: this.orderReady,
description: this.description,
billingAmount: this.billingAmount,
received: this.received,
discountInfo: this.discountInfo,
};
}
}
2 changes: 2 additions & 0 deletions app/repositories/item.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ describe("[db] itemRepository", async () => {
});

test("itemRepository.save (update)", async () => {
savedItemHoge.assignee = "toririm";
const savedItem = await itemRepository.save(savedItemHoge);
expect(savedItem.id).toEqual(savedItemHoge.id);
expect(savedItem.assignee).toEqual("toririm");
});

test("itemRepository.findById", async () => {
Expand Down
20 changes: 13 additions & 7 deletions app/repositories/order.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const isEmulatorRunning = async (): Promise<boolean> => {
describe("[db] orderRepository", async () => {
// To use this environment, firebase emulator must be running.

let savedOrderHoge: WithId<OrderEntity>;
let savedOrderChange: WithId<OrderEntity>;
let orderRepository: OrderRepository;

beforeAll(async () => {
Expand All @@ -47,15 +47,21 @@ describe("[db] orderRepository", async () => {

test("orderRepository.save (create)", async () => {
const order = OrderEntity.createNew({ orderId: 2024 });
savedOrderHoge = await orderRepository.save(order);
expect(savedOrderHoge.id).toBeDefined();
savedOrderChange = await orderRepository.save(order);
expect(savedOrderChange.id).toBeDefined();
});

test("orderRepository.save (update)", async () => {
savedOrderHoge.assignee = "hoge";
const savedOrder = await orderRepository.save(savedOrderHoge);
expect(savedOrder.id).toEqual(savedOrderHoge.id);
expect(savedOrder.assignee).toEqual("hoge");
savedOrderChange.items.push({
id: "1",
name: "item1",
price: 100,
type: "hot",
assignee: null,
});
const savedOrder = await orderRepository.save(savedOrderChange);
expect(savedOrder.id).toEqual(savedOrderChange.id);
expect(savedOrder.items).toEqual(savedOrderChange.items);
});

test("orderRepository.findById", async () => {
Expand Down
Loading

0 comments on commit ab1b979

Please sign in to comment.