Skip to content

Commit

Permalink
Merge pull request #257 from fairnesscoop/feat/add-interest-rate
Browse files Browse the repository at this point in the history
Add interest rate to user savings record
  • Loading branch information
mmarchois authored May 4, 2022
2 parents f37313d + 0a999a8 commit e5af60f
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 5 deletions.
3 changes: 3 additions & 0 deletions client/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,9 @@
},
"form": {
"amount": "Montant"
},
"errors": {
"interest_rate_not_found": "Aucun taux d'intérêt n'est applicable."
}
}
},
Expand Down
19 changes: 19 additions & 0 deletions server/migrations/1651674281488-InterestRate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {MigrationInterface, QueryRunner} from "typeorm";

export class InterestRate1651674281488 implements MigrationInterface {
name = 'InterestRate1651674281488'

public async up(queryRunner: QueryRunner): Promise<void> {
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'`);
await queryRunner.query(`ALTER TABLE "user_savings_record" ADD "interestRateId" uuid`);
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`);
await queryRunner.query(`INSERT INTO "interest_rate" VALUES('9ae76df0-2ae6-40f8-a2e2-fad0371bcfa9', '100', '2022-05-04')`, undefined);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_savings_record" DROP CONSTRAINT "FK_b88b218db366c70a2ec33e424a2"`);
await queryRunner.query(`ALTER TABLE "user_savings_record" DROP COLUMN "interestRateId"`);
await queryRunner.query(`DROP TABLE "interest_rate"`);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ import { SavingsRecordType, UserSavingsRecord } from 'src/Domain/HumanResource/S
import { UserRepository } from 'src/Infrastructure/HumanResource/User/Repository/UserRepository';
import { UserSavingsRecordRepository } from 'src/Infrastructure/HumanResource/Savings/Repository/UserSavingsRecordRepository';
import { UserNotFoundException } from 'src/Domain/HumanResource/User/Exception/UserNotFoundException';
import { InterestRateRepository } from 'src/Infrastructure/HumanResource/Savings/Repository/InterestRateRepository';
import { InterestRateNotFoundException } from 'src/Domain/HumanResource/Savings/Exception/InterestRateNotFoundException';
import { InterestRate } from 'src/Domain/HumanResource/Savings/InterestRate.entity';

describe('IncreaseUserSavingsRecordCommandHandler', () => {
let userRepository: UserRepository;
let userSavingsRecordRepository: UserSavingsRecordRepository;
let interestRateRepository: InterestRateRepository;
let handler: IncreaseUserSavingsRecordCommandHandler;

const user = mock(User);
const command = new IncreaseUserSavingsRecordCommand(
5000,
'a58c5253-c097-4f44-b8c1-ccd45aab36e3',
Expand All @@ -20,10 +25,12 @@ describe('IncreaseUserSavingsRecordCommandHandler', () => {
beforeEach(() => {
userRepository = mock(UserRepository);
userSavingsRecordRepository = mock(UserSavingsRecordRepository);
interestRateRepository = mock(InterestRateRepository);

handler = new IncreaseUserSavingsRecordCommandHandler(
instance(userRepository),
instance(userSavingsRecordRepository),
instance(interestRateRepository),
);
});

Expand All @@ -46,17 +53,45 @@ describe('IncreaseUserSavingsRecordCommandHandler', () => {
}
});

it('testInterestRateNotFound', async () => {
when(
userRepository.findOneById('a58c5253-c097-4f44-b8c1-ccd45aab36e3')
).thenResolve(instance(user));
when(
interestRateRepository.findLatestInterestRate()
).thenResolve(null);

try {
expect(await handler.execute(command)).toBeUndefined();
} catch (e) {
expect(e).toBeInstanceOf(InterestRateNotFoundException);
expect(e.message).toBe(
'human_resources.savings_records.errors.interest_rate_not_found'
);
verify(
userRepository.findOneById('a58c5253-c097-4f44-b8c1-ccd45aab36e3')
).once();
verify(
interestRateRepository.findLatestInterestRate()
).once();
verify(userSavingsRecordRepository.save(anything())).never();
}
});

it('testAddSuccessfully', async () => {
const userSavingsRecord = mock(UserSavingsRecord);
const user = mock(User);
const interestRate = mock(InterestRate);

when(userSavingsRecord.getId()).thenReturn('5c97487c-7863-46a2-967d-79eb8c94ecb5');
when(
userRepository.findOneById('a58c5253-c097-4f44-b8c1-ccd45aab36e3')
).thenResolve(instance(user));
when(
interestRateRepository.findLatestInterestRate()
).thenResolve(instance(interestRate));
when(
userSavingsRecordRepository.save(
deepEqual(new UserSavingsRecord(500000, SavingsRecordType.INPUT, instance(user)))
deepEqual(new UserSavingsRecord(500000, SavingsRecordType.INPUT, instance(user), instance(interestRate)))
)
).thenResolve(instance(userSavingsRecord));

Expand All @@ -65,9 +100,12 @@ describe('IncreaseUserSavingsRecordCommandHandler', () => {
verify(
userRepository.findOneById('a58c5253-c097-4f44-b8c1-ccd45aab36e3')
).once();
verify(
interestRateRepository.findLatestInterestRate()
).once();
verify(
userSavingsRecordRepository.save(
deepEqual(new UserSavingsRecord(500000, SavingsRecordType.INPUT, instance(user)))
deepEqual(new UserSavingsRecord(500000, SavingsRecordType.INPUT, instance(user), instance(interestRate)))
)
).once();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { IUserRepository } from 'src/Domain/HumanResource/User/Repository/IUserR
import { SavingsRecordType, UserSavingsRecord } from 'src/Domain/HumanResource/Savings/UserSavingsRecord.entity';
import { UserNotFoundException } from 'src/Domain/HumanResource/User/Exception/UserNotFoundException';
import { IUserSavingsRecordRepository } from 'src/Domain/HumanResource/Savings/Repository/IUserSavingsRecordRepository';
import { IInterestRateRepository } from 'src/Domain/HumanResource/Savings/Repository/IInterestRateRepository';
import { InterestRateNotFoundException } from 'src/Domain/HumanResource/Savings/Exception/InterestRateNotFoundException';

@CommandHandler(IncreaseUserSavingsRecordCommand)
export class IncreaseUserSavingsRecordCommandHandler {
Expand All @@ -13,6 +15,8 @@ export class IncreaseUserSavingsRecordCommandHandler {
private readonly userRepository: IUserRepository,
@Inject('IUserSavingsRecordRepository')
private readonly userSavingsRecordRepository: IUserSavingsRecordRepository,
@Inject('IInterestRateRepository')
private readonly interestRateRepository: IInterestRateRepository,
) {}

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

const interestRate = await this.interestRateRepository.findLatestInterestRate();
if (!interestRate) {
throw new InterestRateNotFoundException();
}

const userSavingsRecord = await this.userSavingsRecordRepository.save(
new UserSavingsRecord(
Math.round(amount * 100),
SavingsRecordType.INPUT,
user,
interestRate
)
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class InterestRateNotFoundException extends Error {
constructor() {
super(
'human_resources.savings_records.errors.interest_rate_not_found'
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { InterestRate } from './InterestRate.entity';

describe('InterestRate.entity', () => {
it('testGetters', () => {
const interestRate = new InterestRate(100);

expect(interestRate.getId()).toBe(undefined);
expect(interestRate.getRate()).toBe(100);
});
});
29 changes: 29 additions & 0 deletions server/src/Domain/HumanResource/Savings/InterestRate.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
Entity,
Column,
PrimaryGeneratedColumn,
} from 'typeorm';

@Entity()
export class InterestRate {
@PrimaryGeneratedColumn('uuid')
private id: string;

@Column({ type: 'integer', nullable: false, comment: 'Stored in base 100' })
private rate: number;

@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
private createdAt: Date;

constructor(rate: number) {
this.rate = rate;
}

public getId(): string {
return this.id;
}

public getRate(): number {
return this.rate;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { InterestRate } from '../InterestRate.entity';

export interface IInterestRateRepository {
findLatestInterestRate(): Promise<InterestRate>;
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { mock, instance } from 'ts-mockito';
import { User } from '../User/User.entity';
import { InterestRate } from './InterestRate.entity';
import { SavingsRecordType, UserSavingsRecord } from './UserSavingsRecord.entity';

describe('UserSavingsRecord.entity', () => {
it('testGetters', () => {
const user = mock(User);
const interestRate = mock(InterestRate);
const userSavingsRecord = new UserSavingsRecord(
100000,
SavingsRecordType.INPUT,
instance(user)
instance(user),
instance(interestRate),
);

expect(userSavingsRecord.getId()).toBe(undefined);
expect(userSavingsRecord.getAmount()).toBe(100000);
expect(userSavingsRecord.getUser()).toBe(instance(user));
expect(userSavingsRecord.getInterestRate()).toBe(instance(interestRate));
expect(userSavingsRecord.getType()).toBe(SavingsRecordType.INPUT);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ManyToOne
} from 'typeorm';
import { User } from '../User/User.entity';
import { InterestRate } from './InterestRate.entity';

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

@ManyToOne(type => InterestRate, { nullable: true })
private interestRate: InterestRate;

@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
private createdAt: Date;

constructor(
amount: number,
type: SavingsRecordType,
user: User,
interestRate?: InterestRate
) {
this.amount = amount;
this.type = type;
this.user = user;
this.interestRate = interestRate;
}

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

public getInterestRate(): InterestRate {
return this.interestRate;
}

public getUser(): User {
return this.user;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { IInterestRateRepository } from 'src/Domain/HumanResource/Savings/Repository/IInterestRateRepository';
import { InterestRate } from 'src/Domain/HumanResource/Savings/InterestRate.entity';

export class InterestRateRepository implements IInterestRateRepository {
constructor(
@InjectRepository(InterestRate)
private readonly repository: Repository<InterestRate>
) {}

public findLatestInterestRate(): Promise<InterestRate> {
return this.repository
.createQueryBuilder('interestRate')
.select(['interestRate.id'])
.limit(1)
.orderBy('interestRate.createdAt', 'DESC')
.getOne();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ import { UserSavingsRecord } from 'src/Domain/HumanResource/Savings/UserSavingsR
import { UserSavingsRecordRepository } from './Savings/Repository/UserSavingsRecordRepository';
import { IncreaseUserSavingsRecordCommandHandler } from 'src/Application/HumanResource/Savings/Command/IncreaseUserSavingsRecordCommandHandler';
import { IncreaseUserSavingsRecordAction } from './Savings/Action/IncreaseUserSavingsRecordAction';
import { InterestRate } from 'src/Domain/HumanResource/Savings/InterestRate.entity';
import { InterestRateRepository } from './Savings/Repository/InterestRateRepository';

@Module({
imports: [
Expand All @@ -80,7 +82,8 @@ import { IncreaseUserSavingsRecordAction } from './Savings/Action/IncreaseUserSa
Event,
Cooperative,
MealTicketRemoval,
UserSavingsRecord
UserSavingsRecord,
InterestRate
])
],
controllers: [
Expand Down Expand Up @@ -114,6 +117,7 @@ import { IncreaseUserSavingsRecordAction } from './Savings/Action/IncreaseUserSa
{ provide: 'IEventRepository', useClass: EventRepository },
{ provide: 'ICooperativeRepository', useClass: CooperativeRepository },
{ provide: 'IUserSavingsRecordRepository', useClass: UserSavingsRecordRepository },
{ provide: 'IInterestRateRepository', useClass: InterestRateRepository },
{
provide: 'IMealTicketRemovalRepository',
useClass: MealTicketRemovalRepository
Expand Down

0 comments on commit e5af60f

Please sign in to comment.