Skip to content

Commit e5af60f

Browse files
authored
Merge pull request #257 from fairnesscoop/feat/add-interest-rate
Add interest rate to user savings record
2 parents f37313d + 0a999a8 commit e5af60f

12 files changed

+164
-5
lines changed

client/i18n/fr.json

+3
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,9 @@
368368
},
369369
"form": {
370370
"amount": "Montant"
371+
},
372+
"errors": {
373+
"interest_rate_not_found": "Aucun taux d'intérêt n'est applicable."
371374
}
372375
}
373376
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {MigrationInterface, QueryRunner} from "typeorm";
2+
3+
export class InterestRate1651674281488 implements MigrationInterface {
4+
name = 'InterestRate1651674281488'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`CREATE TABLE "interest_rate" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "rate" integer NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "PK_e0dc77d8cda169497a847de0f8b" PRIMARY KEY ("id")); COMMENT ON COLUMN "interest_rate"."rate" IS 'Stored in base 100'`);
8+
await queryRunner.query(`ALTER TABLE "user_savings_record" ADD "interestRateId" uuid`);
9+
await queryRunner.query(`ALTER TABLE "user_savings_record" ADD CONSTRAINT "FK_b88b218db366c70a2ec33e424a2" FOREIGN KEY ("interestRateId") REFERENCES "interest_rate"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
10+
await queryRunner.query(`INSERT INTO "interest_rate" VALUES('9ae76df0-2ae6-40f8-a2e2-fad0371bcfa9', '100', '2022-05-04')`, undefined);
11+
}
12+
13+
public async down(queryRunner: QueryRunner): Promise<void> {
14+
await queryRunner.query(`ALTER TABLE "user_savings_record" DROP CONSTRAINT "FK_b88b218db366c70a2ec33e424a2"`);
15+
await queryRunner.query(`ALTER TABLE "user_savings_record" DROP COLUMN "interestRateId"`);
16+
await queryRunner.query(`DROP TABLE "interest_rate"`);
17+
}
18+
19+
}

server/src/Application/HumanResource/Savings/Command/IncreaseUserSavingsRecordCommandHandler.spec.ts

+41-3
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@ import { SavingsRecordType, UserSavingsRecord } from 'src/Domain/HumanResource/S
66
import { UserRepository } from 'src/Infrastructure/HumanResource/User/Repository/UserRepository';
77
import { UserSavingsRecordRepository } from 'src/Infrastructure/HumanResource/Savings/Repository/UserSavingsRecordRepository';
88
import { UserNotFoundException } from 'src/Domain/HumanResource/User/Exception/UserNotFoundException';
9+
import { InterestRateRepository } from 'src/Infrastructure/HumanResource/Savings/Repository/InterestRateRepository';
10+
import { InterestRateNotFoundException } from 'src/Domain/HumanResource/Savings/Exception/InterestRateNotFoundException';
11+
import { InterestRate } from 'src/Domain/HumanResource/Savings/InterestRate.entity';
912

1013
describe('IncreaseUserSavingsRecordCommandHandler', () => {
1114
let userRepository: UserRepository;
1215
let userSavingsRecordRepository: UserSavingsRecordRepository;
16+
let interestRateRepository: InterestRateRepository;
1317
let handler: IncreaseUserSavingsRecordCommandHandler;
1418

19+
const user = mock(User);
1520
const command = new IncreaseUserSavingsRecordCommand(
1621
5000,
1722
'a58c5253-c097-4f44-b8c1-ccd45aab36e3',
@@ -20,10 +25,12 @@ describe('IncreaseUserSavingsRecordCommandHandler', () => {
2025
beforeEach(() => {
2126
userRepository = mock(UserRepository);
2227
userSavingsRecordRepository = mock(UserSavingsRecordRepository);
28+
interestRateRepository = mock(InterestRateRepository);
2329

2430
handler = new IncreaseUserSavingsRecordCommandHandler(
2531
instance(userRepository),
2632
instance(userSavingsRecordRepository),
33+
instance(interestRateRepository),
2734
);
2835
});
2936

@@ -46,17 +53,45 @@ describe('IncreaseUserSavingsRecordCommandHandler', () => {
4653
}
4754
});
4855

56+
it('testInterestRateNotFound', async () => {
57+
when(
58+
userRepository.findOneById('a58c5253-c097-4f44-b8c1-ccd45aab36e3')
59+
).thenResolve(instance(user));
60+
when(
61+
interestRateRepository.findLatestInterestRate()
62+
).thenResolve(null);
63+
64+
try {
65+
expect(await handler.execute(command)).toBeUndefined();
66+
} catch (e) {
67+
expect(e).toBeInstanceOf(InterestRateNotFoundException);
68+
expect(e.message).toBe(
69+
'human_resources.savings_records.errors.interest_rate_not_found'
70+
);
71+
verify(
72+
userRepository.findOneById('a58c5253-c097-4f44-b8c1-ccd45aab36e3')
73+
).once();
74+
verify(
75+
interestRateRepository.findLatestInterestRate()
76+
).once();
77+
verify(userSavingsRecordRepository.save(anything())).never();
78+
}
79+
});
80+
4981
it('testAddSuccessfully', async () => {
5082
const userSavingsRecord = mock(UserSavingsRecord);
51-
const user = mock(User);
83+
const interestRate = mock(InterestRate);
5284

5385
when(userSavingsRecord.getId()).thenReturn('5c97487c-7863-46a2-967d-79eb8c94ecb5');
5486
when(
5587
userRepository.findOneById('a58c5253-c097-4f44-b8c1-ccd45aab36e3')
5688
).thenResolve(instance(user));
89+
when(
90+
interestRateRepository.findLatestInterestRate()
91+
).thenResolve(instance(interestRate));
5792
when(
5893
userSavingsRecordRepository.save(
59-
deepEqual(new UserSavingsRecord(500000, SavingsRecordType.INPUT, instance(user)))
94+
deepEqual(new UserSavingsRecord(500000, SavingsRecordType.INPUT, instance(user), instance(interestRate)))
6095
)
6196
).thenResolve(instance(userSavingsRecord));
6297

@@ -65,9 +100,12 @@ describe('IncreaseUserSavingsRecordCommandHandler', () => {
65100
verify(
66101
userRepository.findOneById('a58c5253-c097-4f44-b8c1-ccd45aab36e3')
67102
).once();
103+
verify(
104+
interestRateRepository.findLatestInterestRate()
105+
).once();
68106
verify(
69107
userSavingsRecordRepository.save(
70-
deepEqual(new UserSavingsRecord(500000, SavingsRecordType.INPUT, instance(user)))
108+
deepEqual(new UserSavingsRecord(500000, SavingsRecordType.INPUT, instance(user), instance(interestRate)))
71109
)
72110
).once();
73111
});

server/src/Application/HumanResource/Savings/Command/IncreaseUserSavingsRecordCommandHandler.ts

+10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { IUserRepository } from 'src/Domain/HumanResource/User/Repository/IUserR
55
import { SavingsRecordType, UserSavingsRecord } from 'src/Domain/HumanResource/Savings/UserSavingsRecord.entity';
66
import { UserNotFoundException } from 'src/Domain/HumanResource/User/Exception/UserNotFoundException';
77
import { IUserSavingsRecordRepository } from 'src/Domain/HumanResource/Savings/Repository/IUserSavingsRecordRepository';
8+
import { IInterestRateRepository } from 'src/Domain/HumanResource/Savings/Repository/IInterestRateRepository';
9+
import { InterestRateNotFoundException } from 'src/Domain/HumanResource/Savings/Exception/InterestRateNotFoundException';
810

911
@CommandHandler(IncreaseUserSavingsRecordCommand)
1012
export class IncreaseUserSavingsRecordCommandHandler {
@@ -13,6 +15,8 @@ export class IncreaseUserSavingsRecordCommandHandler {
1315
private readonly userRepository: IUserRepository,
1416
@Inject('IUserSavingsRecordRepository')
1517
private readonly userSavingsRecordRepository: IUserSavingsRecordRepository,
18+
@Inject('IInterestRateRepository')
19+
private readonly interestRateRepository: IInterestRateRepository,
1620
) {}
1721

1822
public async execute(command: IncreaseUserSavingsRecordCommand): Promise<string> {
@@ -23,11 +27,17 @@ export class IncreaseUserSavingsRecordCommandHandler {
2327
throw new UserNotFoundException();
2428
}
2529

30+
const interestRate = await this.interestRateRepository.findLatestInterestRate();
31+
if (!interestRate) {
32+
throw new InterestRateNotFoundException();
33+
}
34+
2635
const userSavingsRecord = await this.userSavingsRecordRepository.save(
2736
new UserSavingsRecord(
2837
Math.round(amount * 100),
2938
SavingsRecordType.INPUT,
3039
user,
40+
interestRate
3141
)
3242
);
3343

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export class InterestRateNotFoundException extends Error {
2+
constructor() {
3+
super(
4+
'human_resources.savings_records.errors.interest_rate_not_found'
5+
);
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { InterestRate } from './InterestRate.entity';
2+
3+
describe('InterestRate.entity', () => {
4+
it('testGetters', () => {
5+
const interestRate = new InterestRate(100);
6+
7+
expect(interestRate.getId()).toBe(undefined);
8+
expect(interestRate.getRate()).toBe(100);
9+
});
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
Entity,
3+
Column,
4+
PrimaryGeneratedColumn,
5+
} from 'typeorm';
6+
7+
@Entity()
8+
export class InterestRate {
9+
@PrimaryGeneratedColumn('uuid')
10+
private id: string;
11+
12+
@Column({ type: 'integer', nullable: false, comment: 'Stored in base 100' })
13+
private rate: number;
14+
15+
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
16+
private createdAt: Date;
17+
18+
constructor(rate: number) {
19+
this.rate = rate;
20+
}
21+
22+
public getId(): string {
23+
return this.id;
24+
}
25+
26+
public getRate(): number {
27+
return this.rate;
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { InterestRate } from '../InterestRate.entity';
2+
3+
export interface IInterestRateRepository {
4+
findLatestInterestRate(): Promise<InterestRate>;
5+
}
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
import { mock, instance } from 'ts-mockito';
22
import { User } from '../User/User.entity';
3+
import { InterestRate } from './InterestRate.entity';
34
import { SavingsRecordType, UserSavingsRecord } from './UserSavingsRecord.entity';
45

56
describe('UserSavingsRecord.entity', () => {
67
it('testGetters', () => {
78
const user = mock(User);
9+
const interestRate = mock(InterestRate);
810
const userSavingsRecord = new UserSavingsRecord(
911
100000,
1012
SavingsRecordType.INPUT,
11-
instance(user)
13+
instance(user),
14+
instance(interestRate),
1215
);
1316

1417
expect(userSavingsRecord.getId()).toBe(undefined);
1518
expect(userSavingsRecord.getAmount()).toBe(100000);
1619
expect(userSavingsRecord.getUser()).toBe(instance(user));
20+
expect(userSavingsRecord.getInterestRate()).toBe(instance(interestRate));
1721
expect(userSavingsRecord.getType()).toBe(SavingsRecordType.INPUT);
1822
});
1923
});

server/src/Domain/HumanResource/Savings/UserSavingsRecord.entity.ts

+10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ManyToOne
66
} from 'typeorm';
77
import { User } from '../User/User.entity';
8+
import { InterestRate } from './InterestRate.entity';
89

910
export enum SavingsRecordType {
1011
INPUT = 'input',
@@ -25,17 +26,22 @@ export class UserSavingsRecord {
2526
@ManyToOne(type => User, { nullable: false, onDelete: 'CASCADE' })
2627
private user: User;
2728

29+
@ManyToOne(type => InterestRate, { nullable: true })
30+
private interestRate: InterestRate;
31+
2832
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
2933
private createdAt: Date;
3034

3135
constructor(
3236
amount: number,
3337
type: SavingsRecordType,
3438
user: User,
39+
interestRate?: InterestRate
3540
) {
3641
this.amount = amount;
3742
this.type = type;
3843
this.user = user;
44+
this.interestRate = interestRate;
3945
}
4046

4147
public getId(): string {
@@ -50,6 +56,10 @@ export class UserSavingsRecord {
5056
return this.type;
5157
}
5258

59+
public getInterestRate(): InterestRate {
60+
return this.interestRate;
61+
}
62+
5363
public getUser(): User {
5464
return this.user;
5565
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { InjectRepository } from '@nestjs/typeorm';
2+
import { Repository } from 'typeorm';
3+
import { IInterestRateRepository } from 'src/Domain/HumanResource/Savings/Repository/IInterestRateRepository';
4+
import { InterestRate } from 'src/Domain/HumanResource/Savings/InterestRate.entity';
5+
6+
export class InterestRateRepository implements IInterestRateRepository {
7+
constructor(
8+
@InjectRepository(InterestRate)
9+
private readonly repository: Repository<InterestRate>
10+
) {}
11+
12+
public findLatestInterestRate(): Promise<InterestRate> {
13+
return this.repository
14+
.createQueryBuilder('interestRate')
15+
.select(['interestRate.id'])
16+
.limit(1)
17+
.orderBy('interestRate.createdAt', 'DESC')
18+
.getOne();
19+
}
20+
}

server/src/Infrastructure/HumanResource/humanResource.module.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ import { UserSavingsRecord } from 'src/Domain/HumanResource/Savings/UserSavingsR
6666
import { UserSavingsRecordRepository } from './Savings/Repository/UserSavingsRecordRepository';
6767
import { IncreaseUserSavingsRecordCommandHandler } from 'src/Application/HumanResource/Savings/Command/IncreaseUserSavingsRecordCommandHandler';
6868
import { IncreaseUserSavingsRecordAction } from './Savings/Action/IncreaseUserSavingsRecordAction';
69+
import { InterestRate } from 'src/Domain/HumanResource/Savings/InterestRate.entity';
70+
import { InterestRateRepository } from './Savings/Repository/InterestRateRepository';
6971

7072
@Module({
7173
imports: [
@@ -80,7 +82,8 @@ import { IncreaseUserSavingsRecordAction } from './Savings/Action/IncreaseUserSa
8082
Event,
8183
Cooperative,
8284
MealTicketRemoval,
83-
UserSavingsRecord
85+
UserSavingsRecord,
86+
InterestRate
8487
])
8588
],
8689
controllers: [
@@ -114,6 +117,7 @@ import { IncreaseUserSavingsRecordAction } from './Savings/Action/IncreaseUserSa
114117
{ provide: 'IEventRepository', useClass: EventRepository },
115118
{ provide: 'ICooperativeRepository', useClass: CooperativeRepository },
116119
{ provide: 'IUserSavingsRecordRepository', useClass: UserSavingsRecordRepository },
120+
{ provide: 'IInterestRateRepository', useClass: InterestRateRepository },
117121
{
118122
provide: 'IMealTicketRemovalRepository',
119123
useClass: MealTicketRemovalRepository

0 commit comments

Comments
 (0)