Skip to content

Commit 32bb7c9

Browse files
authored
Merge pull request #28 from CodelyTV/ts-stock-management
[ts][stock_management-01_base] Add stock management example
2 parents b2876e3 + 82560d6 commit 32bb7c9

File tree

11 files changed

+4696
-0
lines changed

11 files changed

+4696
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
coverage
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Stock management
2+
3+
Code smells:
4+
- Data class
5+
- Shotgun surgery
6+
7+
## Setup
8+
- Install dependencies with `yarn install`
9+
- Execute test with `yarn test`
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
presets: [
3+
['@babel/preset-env', {targets: {node: 'current'}}],
4+
'@babel/preset-typescript',
5+
],
6+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "step_shotgun_surgery",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"scripts": {
7+
"test": "jest",
8+
"test-coverage": "jest --coverage"
9+
},
10+
"devDependencies": {
11+
"@babel/core": "^7.13.15",
12+
"@babel/preset-env": "^7.13.15",
13+
"@babel/preset-typescript": "^7.13.0",
14+
"@types/jest": "^26.0.22",
15+
"babel-jest": "^26.6.3",
16+
"jest": "^26.6.3",
17+
"typescript": "^4.2.4"
18+
}
19+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export class AggregateRoot {
2+
private domainEvents: Array<any>
3+
4+
constructor() {
5+
this.domainEvents = []
6+
}
7+
8+
pullDomainEvents(): Array<any> {
9+
const domainEvents = this.domainEvents.slice()
10+
this.domainEvents = []
11+
12+
return domainEvents
13+
}
14+
15+
record(event: any): void {
16+
this.domainEvents.push(event)
17+
}
18+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { LowStock } from "./LowStock"
2+
import { AggregateRoot } from "./AggregateRoot"
3+
import { NotEnoughStock } from "./NotEnoughStock"
4+
import { OutOfStock } from "./OutOfStock"
5+
6+
export class FlowerStock extends AggregateRoot {
7+
8+
private flowers: Array<string>
9+
10+
constructor(flowers: Array<string> = []) {
11+
super()
12+
const flowerSet = new Set<string>(flowers)
13+
this.flowers = Array.from(flowerSet)
14+
}
15+
16+
total() {
17+
return this.flowers.length
18+
}
19+
20+
add(flowerId: string): void {
21+
const flowerSet = new Set<string>(this.flowers)
22+
flowerSet.add(flowerId)
23+
this.flowers = Array.from(flowerSet)
24+
}
25+
26+
get(): string {
27+
if (this.flowers.length === 0) {
28+
throw new OutOfStock()
29+
}
30+
const currentStock = this.flowers.length
31+
32+
const flowerId = this.flowers.shift()
33+
34+
if (currentStock >= 50 && this.flowers.length < 50) {
35+
this.record(new LowStock('low flower stock'))
36+
}
37+
38+
return flowerId
39+
}
40+
41+
getMultiple(amount: number): Array<string> {
42+
if (this.flowers.length < amount) {
43+
throw new NotEnoughStock()
44+
}
45+
46+
const currentStock = this.flowers.length
47+
48+
const flowerIds: Array<string> = this.flowers.splice(0, amount)
49+
50+
if (currentStock >= 50 && this.flowers.length < 50) {
51+
this.record(new LowStock(`low flower stock produced by high demand: ${amount}`))
52+
}
53+
54+
return flowerIds
55+
}
56+
57+
removeDeadFlowers(deadFlowers: Array<string>): void {
58+
const currentStock = this.flowers.length
59+
60+
this.flowers = this.flowers.filter(flowerId => !deadFlowers.includes(flowerId))
61+
62+
if (currentStock >= 50 && this.flowers.length < 50) {
63+
this.record(new LowStock(`low flower stock produced by ${deadFlowers.length} dead flowers`))
64+
}
65+
}
66+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export class LowStock {
2+
constructor(public readonly reason: string) {}
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export class NotEnoughStock extends Error {
2+
constructor() {
3+
super('Not enough stock')
4+
}
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export class OutOfStock extends Error {
2+
constructor() {
3+
super('Out of stock')
4+
}
5+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { FlowerStock } from "../src/FlowerStock"
2+
import { LowStock } from "../src/LowStock"
3+
import { NotEnoughStock } from "../src/NotEnoughStock"
4+
import { OutOfStock } from "../src/OutOfStock"
5+
6+
describe('flower stock management', () => {
7+
8+
it('should add a flower to the stock', () => {
9+
const stockFlowerIds = ['1', '2']
10+
const stock = new FlowerStock(stockFlowerIds)
11+
12+
stock.add('3')
13+
14+
expect(stock.total()).toBe(3)
15+
})
16+
17+
it('should add the same flower multiple times', () => {
18+
const stock = new FlowerStock()
19+
20+
stock.add('3')
21+
stock.add('3')
22+
23+
expect(stock.total()).toBe(1)
24+
})
25+
26+
it('get a single flower from the stock', () => {
27+
const flowerIds = ['1', '2']
28+
const stock = new FlowerStock(flowerIds)
29+
30+
const flowerId = stock.get()
31+
32+
expect(flowerId).toContain('1')
33+
expect(stock.total()).toBe(1)
34+
})
35+
36+
it('should reach low stock when getting a single flower from a stock of 50', () => {
37+
const stockFlowerIds = makeFlowerIds(50)
38+
const stock = new FlowerStock(stockFlowerIds)
39+
40+
stock.get()
41+
42+
const domainEvents = stock.pullDomainEvents()
43+
expect(domainEvents.length).toBe(1)
44+
const lowStockEvent = domainEvents[0] as LowStock
45+
expect(lowStockEvent.reason).toBe('low flower stock')
46+
})
47+
48+
it('should not reach low stock when getting a single flower from a stock greater than 50', () => {
49+
const flowerIds = makeFlowerIds(51)
50+
const stock = new FlowerStock(flowerIds)
51+
52+
stock.get()
53+
54+
expect(stock.pullDomainEvents().length).toBe(0)
55+
})
56+
57+
it('should throw an error when trying to get a single flower from an empty stock', () => {
58+
const stock = new FlowerStock()
59+
60+
expect(() => stock.get()).toThrow(OutOfStock)
61+
})
62+
63+
it('should get multiple flowers from the stock', () => {
64+
const stockFlowerIds = ['1', '2', '3']
65+
const stock = new FlowerStock(stockFlowerIds)
66+
67+
const flowerIds = stock.getMultiple(2)
68+
69+
expect(flowerIds).toEqual(['1', '2'])
70+
expect(stock.total()).toBe(1)
71+
})
72+
73+
it('should reach low stock when getting multiple flowers and the limit of 50 is exceeded', () => {
74+
const lowStockLevel = 50
75+
const unitsToLowStock = 5
76+
const unitsToGetFromTheStock = 6
77+
const totalStock = lowStockLevel + unitsToLowStock
78+
const stockFlowerIds = makeFlowerIds(totalStock)
79+
const stock = new FlowerStock(stockFlowerIds)
80+
81+
stock.getMultiple(unitsToGetFromTheStock)
82+
83+
const domainEvents = stock.pullDomainEvents()
84+
expect(domainEvents.length).toBe(1)
85+
const lowStockEvent = domainEvents[0] as LowStock
86+
expect(lowStockEvent.reason).toBe('low flower stock produced by high demand: 6')
87+
})
88+
89+
it('should not reach low stock when getting multiple flowers and the limit of 50 is not exceeded', () => {
90+
const lowStockLimit = 50
91+
const unitsToLowStock = 5
92+
const unitsToGetFromTheStock = 5
93+
const totalStock = lowStockLimit + unitsToLowStock
94+
const flowerIds = makeFlowerIds(totalStock)
95+
const stock = new FlowerStock(flowerIds)
96+
97+
stock.getMultiple(unitsToGetFromTheStock)
98+
99+
expect(stock.pullDomainEvents().length).toBe(0)
100+
})
101+
102+
it('should get a multiple flowers from an empty stock', () => {
103+
const stock = new FlowerStock()
104+
105+
expect(() => stock.getMultiple(2)).toThrow(NotEnoughStock)
106+
})
107+
108+
it('remove dead flowers from the stock', () => {
109+
const stockFlowerIds = ['1', '2', '3']
110+
const stock = new FlowerStock(stockFlowerIds)
111+
112+
const deadFlowerIds = ['1', '3']
113+
stock.removeDeadFlowers(deadFlowerIds)
114+
115+
expect(stock.total()).toBe(1)
116+
const aliveFlower = '2'
117+
expect(stock.get()).toBe(aliveFlower)
118+
})
119+
120+
121+
it('should reach low stock when removing dead flowers and the limit of 50 is exceeded', () => {
122+
const lowStockLevel = 50
123+
const unitsToLowStock = 5
124+
const totalDeadFlowers = 6
125+
const totalStock = lowStockLevel + unitsToLowStock
126+
const stockFlowerIds = makeFlowerIds(totalStock)
127+
const stock = new FlowerStock(stockFlowerIds)
128+
129+
const deadFlowers = makeFlowerIds(totalDeadFlowers)
130+
stock.removeDeadFlowers(deadFlowers)
131+
132+
const domainEvents = stock.pullDomainEvents()
133+
expect(domainEvents.length).toBe(1)
134+
const lowStockEvent = domainEvents[0] as LowStock
135+
expect(lowStockEvent.reason).toBe('low flower stock produced by 6 dead flowers')
136+
})
137+
138+
it('should not reach low stock when removing flowers and the limit of 50 is not exceeded', () => {
139+
const lowStockLimit = 50
140+
const unitsToLowStock = 5
141+
const totalDeadFlowers = 5
142+
const totalStock = lowStockLimit + unitsToLowStock
143+
const flowerIds = makeFlowerIds(totalStock)
144+
const stock = new FlowerStock(flowerIds)
145+
146+
const deadFlowers = makeFlowerIds(totalDeadFlowers)
147+
stock.removeDeadFlowers(deadFlowers)
148+
149+
expect(stock.pullDomainEvents().length).toBe(0)
150+
})
151+
152+
function makeFlowerIds(amount: number) {
153+
return Array<string>(amount).fill('1').map((x, y) => (x + y) as string)
154+
}
155+
})

0 commit comments

Comments
 (0)