-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #256 from fairnesscoop/feat/add-user-savings-record
Add user savings record
- Loading branch information
Showing
16 changed files
with
354 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
38 changes: 38 additions & 0 deletions
38
client/src/routes/human_resources/savings_records/_Form.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<script> | ||
import { createEventDispatcher, onMount } from 'svelte'; | ||
import { _ } from 'svelte-i18n'; | ||
import { get } from 'utils/axios'; | ||
import Button from 'components/inputs/Button.svelte'; | ||
import Input from 'components/inputs/Input.svelte'; | ||
import UsersInput from 'components/inputs/UsersInput.svelte'; | ||
const dispatch = createEventDispatcher(); | ||
export let userId; | ||
export let amount; | ||
export let loading; | ||
let users = []; | ||
onMount(async () => { | ||
users = (await get('users', { params: {activeOnly: true} })).data; | ||
}); | ||
const submit = () => { | ||
dispatch('save', { userId, amount }); | ||
}; | ||
</script> | ||
|
||
<form | ||
on:submit|preventDefault={submit} | ||
class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800"> | ||
<UsersInput {users} bind:userId /> | ||
<Input | ||
type={'money'} | ||
label={$_('human_resources.savings_records.form.amount')} | ||
bind:value={amount} /> | ||
<Button | ||
value={$_('common.form.save')} | ||
{loading} | ||
disabled={!userId || !amount || loading} /> | ||
</form> |
44 changes: 44 additions & 0 deletions
44
client/src/routes/human_resources/savings_records/add.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<script> | ||
import { goto } from '@sapper/app'; | ||
import { _ } from 'svelte-i18n'; | ||
import Breadcrumb from 'components/Breadcrumb.svelte'; | ||
import { post } from 'utils/axios'; | ||
import { errorNormalizer } from 'normalizer/errors'; | ||
import ServerErrors from 'components/ServerErrors.svelte'; | ||
import H4Title from 'components/H4Title.svelte'; | ||
import Form from './_Form.svelte'; | ||
const title = $_('human_resources.savings_records.add.title'); | ||
let loading = false; | ||
let errors = []; | ||
const onSave = async (e) => { | ||
try { | ||
loading = true; | ||
await post('users/savings-records/increase', { | ||
amount: e.detail.amount, | ||
userId: e.detail.userId, | ||
}); | ||
goto('/human_resources/savings_records'); | ||
} catch (e) { | ||
errors = errorNormalizer(e); | ||
} finally { | ||
loading = false; | ||
} | ||
}; | ||
</script> | ||
|
||
<svelte:head> | ||
<title>{title} - {$_('app')}</title> | ||
</svelte:head> | ||
|
||
<Breadcrumb | ||
items={[ | ||
{ title: $_('human_resources.breadcrumb') }, | ||
{ title: $_('human_resources.savings_records.title'), path: '/human_resources/savings_records' }, | ||
{ title } | ||
]} | ||
/> | ||
<H4Title {title} /> | ||
<ServerErrors {errors} /> | ||
<Form {loading} on:save={onSave} /> |
18 changes: 18 additions & 0 deletions
18
client/src/routes/human_resources/savings_records/index.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<script> | ||
import { _ } from 'svelte-i18n'; | ||
import Breadcrumb from 'components/Breadcrumb.svelte'; | ||
import H4Title from 'components/H4Title.svelte'; | ||
import AddLink from 'components/links/AddLink.svelte'; | ||
const title = $_('human_resources.savings_records.title'); | ||
</script> | ||
|
||
<svelte:head> | ||
<title>{title} - {$_('app')}</title> | ||
</svelte:head> | ||
|
||
<Breadcrumb items={[{ title: $_('human_resources.breadcrumb') }, { title }]} /> | ||
<div class="inline-flex items-center"> | ||
<H4Title {title} /> | ||
<AddLink href={'/human_resources/savings_records/add'} value={$_('human_resources.savings_records.add.title')} /> | ||
</div> |
8 changes: 8 additions & 0 deletions
8
server/src/Application/HumanResource/Savings/Command/IncreaseUserSavingsRecordCommand.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { ICommand } from 'src/Application/ICommand'; | ||
|
||
export class IncreaseUserSavingsRecordCommand implements ICommand { | ||
constructor( | ||
public readonly amount: number, | ||
public readonly userId: string, | ||
) {} | ||
} |
74 changes: 74 additions & 0 deletions
74
...Application/HumanResource/Savings/Command/IncreaseUserSavingsRecordCommandHandler.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { mock, instance, when, verify, deepEqual, anything } from 'ts-mockito'; | ||
import { User } from 'src/Domain/HumanResource/User/User.entity'; | ||
import { IncreaseUserSavingsRecordCommandHandler } from './IncreaseUserSavingsRecordCommandHandler'; | ||
import { IncreaseUserSavingsRecordCommand } from './IncreaseUserSavingsRecordCommand'; | ||
import { SavingsRecordType, UserSavingsRecord } from 'src/Domain/HumanResource/Savings/UserSavingsRecord.entity'; | ||
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'; | ||
|
||
describe('IncreaseUserSavingsRecordCommandHandler', () => { | ||
let userRepository: UserRepository; | ||
let userSavingsRecordRepository: UserSavingsRecordRepository; | ||
let handler: IncreaseUserSavingsRecordCommandHandler; | ||
|
||
const command = new IncreaseUserSavingsRecordCommand( | ||
5000, | ||
'a58c5253-c097-4f44-b8c1-ccd45aab36e3', | ||
); | ||
|
||
beforeEach(() => { | ||
userRepository = mock(UserRepository); | ||
userSavingsRecordRepository = mock(UserSavingsRecordRepository); | ||
|
||
handler = new IncreaseUserSavingsRecordCommandHandler( | ||
instance(userRepository), | ||
instance(userSavingsRecordRepository), | ||
); | ||
}); | ||
|
||
it('testUserNotFound', async () => { | ||
when( | ||
userRepository.findOneById('a58c5253-c097-4f44-b8c1-ccd45aab36e3') | ||
).thenResolve(null); | ||
|
||
try { | ||
expect(await handler.execute(command)).toBeUndefined(); | ||
} catch (e) { | ||
expect(e).toBeInstanceOf(UserNotFoundException); | ||
expect(e.message).toBe( | ||
'human_resources.users.errors.not_found' | ||
); | ||
verify( | ||
userRepository.findOneById('a58c5253-c097-4f44-b8c1-ccd45aab36e3') | ||
).once(); | ||
verify(userSavingsRecordRepository.save(anything())).never(); | ||
} | ||
}); | ||
|
||
it('testAddSuccessfully', async () => { | ||
const userSavingsRecord = mock(UserSavingsRecord); | ||
const user = mock(User); | ||
|
||
when(userSavingsRecord.getId()).thenReturn('5c97487c-7863-46a2-967d-79eb8c94ecb5'); | ||
when( | ||
userRepository.findOneById('a58c5253-c097-4f44-b8c1-ccd45aab36e3') | ||
).thenResolve(instance(user)); | ||
when( | ||
userSavingsRecordRepository.save( | ||
deepEqual(new UserSavingsRecord(500000, SavingsRecordType.INPUT, instance(user))) | ||
) | ||
).thenResolve(instance(userSavingsRecord)); | ||
|
||
expect(await handler.execute(command)).toBe('5c97487c-7863-46a2-967d-79eb8c94ecb5'); | ||
|
||
verify( | ||
userRepository.findOneById('a58c5253-c097-4f44-b8c1-ccd45aab36e3') | ||
).once(); | ||
verify( | ||
userSavingsRecordRepository.save( | ||
deepEqual(new UserSavingsRecord(500000, SavingsRecordType.INPUT, instance(user))) | ||
) | ||
).once(); | ||
}); | ||
}); |
36 changes: 36 additions & 0 deletions
36
.../src/Application/HumanResource/Savings/Command/IncreaseUserSavingsRecordCommandHandler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { CommandHandler } from '@nestjs/cqrs'; | ||
import { Inject } from '@nestjs/common'; | ||
import { IncreaseUserSavingsRecordCommand } from './IncreaseUserSavingsRecordCommand'; | ||
import { IUserRepository } from 'src/Domain/HumanResource/User/Repository/IUserRepository'; | ||
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'; | ||
|
||
@CommandHandler(IncreaseUserSavingsRecordCommand) | ||
export class IncreaseUserSavingsRecordCommandHandler { | ||
constructor( | ||
@Inject('IUserRepository') | ||
private readonly userRepository: IUserRepository, | ||
@Inject('IUserSavingsRecordRepository') | ||
private readonly userSavingsRecordRepository: IUserSavingsRecordRepository, | ||
) {} | ||
|
||
public async execute(command: IncreaseUserSavingsRecordCommand): Promise<string> { | ||
const { userId, amount } = command; | ||
|
||
const user = await this.userRepository.findOneById(userId); | ||
if (!user) { | ||
throw new UserNotFoundException(); | ||
} | ||
|
||
const userSavingsRecord = await this.userSavingsRecordRepository.save( | ||
new UserSavingsRecord( | ||
Math.round(amount * 100), | ||
SavingsRecordType.INPUT, | ||
user, | ||
) | ||
); | ||
|
||
return userSavingsRecord.getId(); | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
server/src/Domain/HumanResource/Savings/Repository/IUserSavingsRecordRepository.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { UserSavingsRecord } from '../UserSavingsRecord.entity'; | ||
|
||
export interface IUserSavingsRecordRepository { | ||
save(userSavingsRecord: UserSavingsRecord): Promise<UserSavingsRecord>; | ||
} |
44 changes: 44 additions & 0 deletions
44
server/src/Infrastructure/HumanResource/Savings/Action/IncreaseUserSavingsRecordAction.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { | ||
Body, | ||
Post, | ||
Controller, | ||
Inject, | ||
BadRequestException, | ||
UseGuards | ||
} from '@nestjs/common'; | ||
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; | ||
import { AuthGuard } from '@nestjs/passport'; | ||
import { ICommandBus } from 'src/Application/ICommandBus'; | ||
import { UserRole } from 'src/Domain/HumanResource/User/User.entity'; | ||
import { RolesGuard } from 'src/Infrastructure/HumanResource/User/Security/RolesGuard'; | ||
import { Roles } from 'src/Infrastructure/HumanResource/User/Decorator/Roles'; | ||
import { IncreaseUserSavingsRecordCommand } from 'src/Application/HumanResource/Savings/Command/IncreaseUserSavingsRecordCommand'; | ||
import { UserSavingsRecordDTO } from '../DTO/UserSavingsRecordDTO'; | ||
|
||
@Controller('users/savings-records') | ||
@ApiTags('Human Resource') | ||
@ApiBearerAuth() | ||
@UseGuards(AuthGuard('bearer'), RolesGuard) | ||
export class IncreaseUserSavingsRecordAction { | ||
constructor( | ||
@Inject('ICommandBus') | ||
private readonly commandBus: ICommandBus | ||
) {} | ||
|
||
@Post('increase') | ||
@Roles(UserRole.COOPERATOR, UserRole.EMPLOYEE) | ||
@ApiOperation({ summary: 'Increase user savings record' }) | ||
public async index( | ||
@Body() { userId, amount }: UserSavingsRecordDTO, | ||
) { | ||
try { | ||
const id = await this.commandBus.execute( | ||
new IncreaseUserSavingsRecordCommand(amount, userId) | ||
); | ||
|
||
return { id }; | ||
} catch (e) { | ||
throw new BadRequestException(e.message); | ||
} | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
server/src/Infrastructure/HumanResource/Savings/DTO/UserSavingsRecordDTO.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { UserSavingsRecordDTO } from './UserSavingsRecordDTO'; | ||
import { validate } from 'class-validator'; | ||
|
||
describe('UserSavingsRecordDTO', () => { | ||
it('testValidDTO', async () => { | ||
const dto = new UserSavingsRecordDTO(); | ||
dto.amount = -5000; | ||
dto.userId = 'e0884737-2a01-4f12-ac0e-c4d0ccc48d59'; | ||
|
||
const validation = await validate(dto); | ||
expect(validation).toHaveLength(0); | ||
}); | ||
|
||
it('testInvalidDTO', async () => { | ||
const dto = new UserSavingsRecordDTO(); | ||
const validation = await validate(dto); | ||
|
||
expect(validation).toHaveLength(2); | ||
expect(validation[0].constraints).toMatchObject({ | ||
isNotEmpty: "userId should not be empty", | ||
isUuid: "userId must be an UUID" | ||
}); | ||
expect(validation[1].constraints).toMatchObject({ | ||
isNotEmpty: "amount should not be empty", | ||
isNumber: "amount must be a number conforming to the specified constraints" | ||
}); | ||
}); | ||
}); |
18 changes: 18 additions & 0 deletions
18
server/src/Infrastructure/HumanResource/Savings/DTO/UserSavingsRecordDTO.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { ApiProperty } from '@nestjs/swagger'; | ||
import { | ||
IsNotEmpty, | ||
IsUUID, | ||
IsNumber, | ||
} from 'class-validator'; | ||
|
||
export class UserSavingsRecordDTO { | ||
@IsNotEmpty() | ||
@IsUUID() | ||
@ApiProperty() | ||
public userId: string; | ||
|
||
@IsNotEmpty() | ||
@IsNumber() | ||
@ApiProperty() | ||
public amount: number; | ||
} |
15 changes: 15 additions & 0 deletions
15
server/src/Infrastructure/HumanResource/Savings/Repository/UserSavingsRecordRepository.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { InjectRepository } from '@nestjs/typeorm'; | ||
import { Repository } from 'typeorm'; | ||
import { IUserSavingsRecordRepository } from 'src/Domain/HumanResource/Savings/Repository/IUserSavingsRecordRepository'; | ||
import { UserSavingsRecord } from 'src/Domain/HumanResource/Savings/UserSavingsRecord.entity'; | ||
|
||
export class UserSavingsRecordRepository implements IUserSavingsRecordRepository { | ||
constructor( | ||
@InjectRepository(UserSavingsRecord) | ||
private readonly repository: Repository<UserSavingsRecord> | ||
) {} | ||
|
||
public save(userSavingsRecord: UserSavingsRecord): Promise<UserSavingsRecord> { | ||
return this.repository.save(userSavingsRecord); | ||
} | ||
} |
Oops, something went wrong.