Skip to content

Commit 03a7bb0

Browse files
authored
Merge pull request #12 from webdevia/homework-10
[10] Реализация AccountService через подход Test Driven Development подход
2 parents e550008 + 053b3c8 commit 03a7bb0

File tree

5 files changed

+317
-0
lines changed

5 files changed

+317
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { IDatabase } from './Database';
2+
import { IDiscountService, DiscountService } from './DiscountService';
3+
import { AccountService } from './AccountService';
4+
import { Product, User } from './Entities';
5+
6+
class MockDatabase implements IDatabase {
7+
private data: Map<string, unknown> = new Map();
8+
9+
async save<T>(key: string, value: T): Promise<boolean> {
10+
this.data.set(key, value);
11+
return true;
12+
}
13+
14+
async load<T>(key: string): Promise<T> {
15+
const value = this.data.get(key);
16+
if (value === undefined) {
17+
throw new Error('Table not found');
18+
}
19+
return { ...(value as T) };
20+
}
21+
}
22+
23+
describe('AccountService', () => {
24+
let discountService: IDiscountService;
25+
let accountService: AccountService;
26+
27+
beforeEach(() => {
28+
const db = new MockDatabase();
29+
discountService = new DiscountService(db);
30+
accountService = new AccountService(discountService);
31+
});
32+
33+
test('should throw table not found error', async () => {
34+
try {
35+
await accountService.userDiscount.loadDiscounts();
36+
} catch (e) {
37+
expect(e.message).toBe('Failed to load user_discount discounts');
38+
}
39+
});
40+
41+
test('should throw table not found error', async () => {
42+
try {
43+
await accountService.userDiscount.loadDiscounts();
44+
} catch (e) {
45+
expect(e.message).toBe('Failed to load user_discount discounts');
46+
}
47+
});
48+
49+
test('should set and get user discount', async () => {
50+
await accountService.userDiscount.setUserDiscount(User.Standard, 10);
51+
await accountService.userDiscount.saveDiscounts();
52+
await accountService.userDiscount.loadDiscounts();
53+
54+
expect(accountService.userDiscount.getUserDiscount(User.Standard)).toBe(10);
55+
});
56+
57+
test('should get user discount = 0 for negative value', async () => {
58+
await accountService.userDiscount.setUserDiscount(User.Standard, -10);
59+
await accountService.userDiscount.saveDiscounts();
60+
await accountService.userDiscount.loadDiscounts();
61+
62+
expect(accountService.userDiscount.getUserDiscount(User.Standard)).toBe(0);
63+
});
64+
65+
test('should calculate user discount = 100 for value > 100', async () => {
66+
await accountService.userDiscount.setUserDiscount(User.Standard, 200);
67+
await accountService.userDiscount.saveDiscounts();
68+
await accountService.userDiscount.loadDiscounts();
69+
70+
expect(accountService.userDiscount.getUserDiscount(User.Standard)).toBe(100);
71+
});
72+
73+
test('should set and get user product discount', async () => {
74+
await accountService.userProductDiscount.setUserProductDiscount(User.Standard, Product.Car, 10);
75+
await accountService.userProductDiscount.saveDiscounts();
76+
await accountService.userProductDiscount.loadDiscounts();
77+
78+
expect(accountService.userProductDiscount.getUserProductDiscount(User.Standard, Product.Car)).toBe(10);
79+
});
80+
81+
test('should get user product discount = 0 for negative value', async () => {
82+
await accountService.userProductDiscount.setUserProductDiscount(User.Standard, Product.Car, -10);
83+
await accountService.userProductDiscount.saveDiscounts();
84+
await accountService.userProductDiscount.loadDiscounts();
85+
86+
expect(accountService.userProductDiscount.getUserProductDiscount(User.Standard, Product.Car)).toBe(0);
87+
});
88+
89+
test('should calculate user product discount = 100 for value > 100', async () => {
90+
await accountService.userProductDiscount.setUserProductDiscount(User.Standard, Product.Car, 200);
91+
await accountService.userProductDiscount.saveDiscounts();
92+
await accountService.userProductDiscount.loadDiscounts();
93+
94+
expect(accountService.userProductDiscount.getUserProductDiscount(User.Standard, Product.Car)).toBe(100);
95+
});
96+
97+
test('should calculate total discount', async () => {
98+
await accountService.userDiscount.setUserDiscount(User.Standard, 10);
99+
await accountService.userProductDiscount.setUserProductDiscount(User.Standard, Product.Car, 20);
100+
await accountService.saveAllDiscounts();
101+
await accountService.loadAllDiscounts();
102+
103+
const totalDiscount = accountService.calculateTotalDiscount(User.Standard, Product.Car);
104+
expect(totalDiscount).toBe(30);
105+
});
106+
107+
test('should calculate total discount = 0 for negative value', async () => {
108+
await accountService.userDiscount.setUserDiscount(User.Standard, -10);
109+
await accountService.userProductDiscount.setUserProductDiscount(User.Standard, Product.Car, -20);
110+
await accountService.saveAllDiscounts();
111+
await accountService.loadAllDiscounts();
112+
113+
const totalDiscount = accountService.calculateTotalDiscount(User.Standard, Product.Car);
114+
expect(totalDiscount).toBe(0);
115+
});
116+
117+
test('should calculate total discount = 100 for value > 100', async () => {
118+
await accountService.userDiscount.setUserDiscount(User.Standard, 200);
119+
await accountService.userProductDiscount.setUserProductDiscount(User.Standard, Product.Car, 10);
120+
await accountService.saveAllDiscounts();
121+
await accountService.loadAllDiscounts();
122+
123+
const totalDiscount = accountService.calculateTotalDiscount(User.Standard, Product.Car);
124+
expect(totalDiscount).toBe(100);
125+
});
126+
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { UserDiscount, UserProductDiscount, IDiscountService } from './DiscountService';
2+
import { Product, User } from './Entities';
3+
4+
export class AccountService {
5+
userDiscount: UserDiscount;
6+
userProductDiscount: UserProductDiscount;
7+
private discountService: IDiscountService;
8+
9+
constructor(discountService: IDiscountService) {
10+
this.discountService = discountService;
11+
this.userDiscount = new UserDiscount(this.discountService);
12+
this.userProductDiscount = new UserProductDiscount(this.discountService);
13+
}
14+
15+
async loadAllDiscounts() {
16+
Promise.all([this.userDiscount.loadDiscounts(), this.userProductDiscount.loadDiscounts()]);
17+
}
18+
19+
async saveAllDiscounts() {
20+
Promise.all([this.userDiscount.saveDiscounts(), this.userProductDiscount.saveDiscounts()]);
21+
}
22+
23+
calculateTotalDiscount(user: User, product: Product) {
24+
const userDiscount = this.userDiscount.getUserDiscount(user);
25+
const userProductDiscount = this.userProductDiscount.getUserProductDiscount(user, product);
26+
return this.discountService.totalDiscount(userDiscount, userProductDiscount);
27+
}
28+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export interface IDatabase {
2+
save<T>(key: string, value: T): Promise<boolean>;
3+
load<T>(key: string): Promise<T>;
4+
}
5+
6+
export class Database implements IDatabase {
7+
private data: Map<string, unknown>;
8+
9+
constructor() {
10+
this.data = new Map();
11+
}
12+
13+
save<T>(key: string, value: T): Promise<boolean> {
14+
return new Promise((resolve) =>
15+
setTimeout(() => {
16+
resolve(!!this.data.set(key, value));
17+
}, 500)
18+
);
19+
}
20+
21+
load<T>(key: string): Promise<T> {
22+
return new Promise((resolve, reject) =>
23+
setTimeout(() => {
24+
const value = this.data.get(key);
25+
if (value === undefined) {
26+
reject(new Error('Table not found'));
27+
} else {
28+
resolve({ ...(value as T) });
29+
}
30+
}, 500)
31+
);
32+
}
33+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { IDatabase } from './Database';
2+
import { User, Product } from './Entities';
3+
4+
export interface IDiscountService {
5+
loadDiscounts(table: string): Promise<void>;
6+
saveDiscounts(table: string): Promise<void>;
7+
getDiscount(table: string, field: string): number;
8+
setDiscount(table: string, field: string, value: number): void;
9+
totalDiscount(...args: number[]): number;
10+
}
11+
12+
type Discount = Record<string, number>;
13+
14+
export class DiscountService implements IDiscountService {
15+
private database: IDatabase;
16+
private discounts: Record<string, Discount> = {};
17+
private maxDiscountValue = 100;
18+
private minDiscountValue = 0;
19+
20+
constructor(db: IDatabase) {
21+
this.database = db;
22+
}
23+
24+
private getValidDiscount(value: number) {
25+
return value < this.minDiscountValue
26+
? this.minDiscountValue
27+
: value > this.maxDiscountValue
28+
? this.maxDiscountValue
29+
: value;
30+
}
31+
32+
async loadDiscounts(table: string) {
33+
try {
34+
this.discounts[table] = await this.database.load<Discount>(table);
35+
} catch (error) {
36+
this.discounts[table] = {};
37+
throw new Error(`Failed to load ${table} discounts`);
38+
}
39+
}
40+
41+
async saveDiscounts(table: string) {
42+
try {
43+
await this.database.save<Discount>(table, this.discounts[table]);
44+
} catch (error) {
45+
console.error(`Failed to save ${table} discounts:`, error);
46+
}
47+
}
48+
49+
getDiscount(table: string, field: string) {
50+
return this.discounts[table]?.[field] ?? 0;
51+
}
52+
53+
setDiscount(table: string, field: string, value: number) {
54+
if (!this.discounts[table]) {
55+
this.discounts[table] = {};
56+
}
57+
this.discounts[table][field] = this.getValidDiscount(value);
58+
}
59+
60+
totalDiscount(...args: number[]) {
61+
return this.getValidDiscount(args.reduce((x, y) => x + y, 0));
62+
}
63+
}
64+
65+
abstract class BaseDiscount {
66+
protected discountService: IDiscountService;
67+
protected discountTable: string;
68+
69+
constructor(discountService: IDiscountService, discountTable: string) {
70+
this.discountService = discountService;
71+
this.discountTable = discountTable;
72+
}
73+
74+
async loadDiscounts() {
75+
await this.discountService.loadDiscounts(this.discountTable);
76+
}
77+
78+
async saveDiscounts() {
79+
await this.discountService.saveDiscounts(this.discountTable);
80+
}
81+
82+
getDiscount(field: string) {
83+
return this.discountService.getDiscount(this.discountTable, field);
84+
}
85+
86+
setDiscount(field: string, value: number) {
87+
this.discountService.setDiscount(this.discountTable, field, value);
88+
}
89+
}
90+
91+
export class UserDiscount extends BaseDiscount {
92+
constructor(discountService: IDiscountService) {
93+
super(discountService, 'user_discount');
94+
}
95+
96+
getUserDiscount(user: User) {
97+
return this.getDiscount(user);
98+
}
99+
100+
setUserDiscount(user: User, value: number) {
101+
this.setDiscount(user, value);
102+
}
103+
}
104+
105+
export class UserProductDiscount extends BaseDiscount {
106+
constructor(discountService: IDiscountService) {
107+
super(discountService, 'user_product_discount');
108+
}
109+
110+
getUserProductDiscount(user: User, product: Product) {
111+
return this.getDiscount(`${user}_${product}`);
112+
}
113+
114+
setUserProductDiscount(user: User, product: Product, value: number) {
115+
this.setDiscount(`${user}_${product}`, value);
116+
}
117+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export enum User {
2+
Standard = 'Standard',
3+
Premium = 'Premium',
4+
Gold = 'Gold',
5+
Free = 'Free',
6+
}
7+
8+
export enum Product {
9+
Car = 'Car',
10+
Toy = 'Toy',
11+
Food = 'Food',
12+
}
13+

0 commit comments

Comments
 (0)