Skip to content

Commit

Permalink
Voucher activation (#71)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmarchois authored Apr 5, 2021
1 parent c7fcf08 commit 9c8f41d
Show file tree
Hide file tree
Showing 31 changed files with 491 additions and 217 deletions.
5 changes: 1 addition & 4 deletions api/src/Application/School/Query/GetSchoolsQuery.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { IQuery } from 'src/Application/IQuery';
import { UserRole } from 'src/Domain/User/User.entity';

export class GetSchoolsQuery implements IQuery {
constructor(
public readonly page: number,
public readonly userId: string,
public readonly userRole: UserRole,
public readonly page: number
) {}
}
49 changes: 2 additions & 47 deletions api/src/Application/School/Query/GetSchoolsQueryHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import { GetSchoolsQuery } from 'src/Application/School/Query/GetSchoolsQuery';
import { School } from 'src/Domain/School/School.entity';
import { SchoolView } from 'src/Application/School/View/SchoolView';
import { Pagination } from 'src/Application/Common/Pagination';
import { UserRole } from 'src/Domain/User/User.entity';
import { Status, Type } from 'src/Domain/School/AbstractSchool';

describe('GetSchoolsQueryHandler', () => {
it('testGetSchoolsWithPhotographerRole', async () => {
it('testGetSchools', async () => {
const schoolRepository = mock(SchoolRepository);

const school1 = mock(School);
Expand Down Expand Up @@ -65,51 +64,7 @@ describe('GetSchoolsQueryHandler', () => {
);

expect(
await queryHandler.execute(
new GetSchoolsQuery(1, '2eefa0ec-484b-4c13-ad8f-e7dbce14be64', UserRole.PHOTOGRAPHER)
)).toMatchObject(expectedResult);
verify(schoolRepository.findSchools(1)).once();
});

it('testGetSchoolsWithDirectorRole', async () => {
const schoolRepository = mock(SchoolRepository);

const school3 = mock(School);
when(school3.getId()).thenReturn('eb9e1d9b-dce2-48a9-b64f-f0872f3157d2');
when(school3.getName()).thenReturn('Ecole élementaire Belliard');
when(school3.getReference()).thenReturn('xLKJSs');
when(school3.getAddress()).thenReturn('127 Rue Belliard');
when(school3.getCity()).thenReturn('Paris');
when(school3.getZipCode()).thenReturn('75010');
when(school3.getType()).thenReturn(Type.ELEMENTARY);
when(school3.getStatus()).thenReturn(Status.PUBLIC);

when(schoolRepository.findSchools(1)).thenResolve([
[instance(school3)],
1
]);

const queryHandler = new GetSchoolsQueryHandler(instance(schoolRepository));
const expectedResult = new Pagination<SchoolView>(
[
new SchoolView(
'eb9e1d9b-dce2-48a9-b64f-f0872f3157d2',
'Ecole élementaire Belliard',
'xLKJSs',
'127 Rue Belliard',
'Paris',
'75010',
Status.PUBLIC,
Type.ELEMENTARY
)
],
1
);

expect(
await queryHandler.execute(
new GetSchoolsQuery(1, '2eefa0ec-484b-4c13-ad8f-e7dbce14be64', UserRole.DIRECTOR)
)).toMatchObject(expectedResult);
await queryHandler.execute(new GetSchoolsQuery(1))).toMatchObject(expectedResult);
verify(schoolRepository.findSchools(1)).once();
});
});
2 changes: 1 addition & 1 deletion api/src/Application/School/Query/GetSchoolsQueryHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class GetSchoolsQueryHandler {
public async execute(
query: GetSchoolsQuery
): Promise<Pagination<SchoolView>> {
const { page, userId, userRole } = query;
const { page } = query;
const schoolViews: SchoolView[] = [];
const [ schools, total ] = await this.schoolRepository.findSchools(page);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import { ProductSummaryView } from 'src/Application/Product/View/ProductSummaryV
export class GetSchoolProductByIdQueryHandler {
constructor(
@Inject('ISchoolProductRepository')
private readonly schoolproductRepository: ISchoolProductRepository
private readonly schoolProductRepository: ISchoolProductRepository
) {}

public async execute(query: GetSchoolProductByIdQuery): Promise<SchoolProductView> {
const schoolProduct = await this.schoolproductRepository.findOneById(query.id);
const schoolProduct = await this.schoolProductRepository.findOneById(query.id);

if (!schoolProduct) {
throw new SchoolProductNotFoundException();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { IQuery } from 'src/Application/IQuery';

export class GetVoucherByCodeQuery implements IQuery {
constructor(public readonly code: string) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { VoucherNotFoundException } from 'src/Domain/School/Exception/VoucherNotFoundException';
import { Voucher } from 'src/Domain/School/Voucher.entity';
import { VoucherRepository } from 'src/Infrastructure/School/Repository/VoucherRepository';
import { mock, instance, when, verify } from 'ts-mockito';
import { VoucherView } from '../../View/VoucherView';
import { GetVoucherByCodeQuery } from './GetVoucherByCodeQuery';
import { GetVoucherByCodeQueryHandler } from './GetVoucherByCodeQueryHandler';

describe('GetVoucherByCodeQueryHandler', () => {
const query = new GetVoucherByCodeQuery('eb9e1d9b-dce2-48a9-b64f-f0872f3157d2');

it('testGetVoucher', async () => {
const voucherRepository = mock(VoucherRepository);
const queryHandler = new GetVoucherByCodeQueryHandler(instance(voucherRepository));
const expectedResult = new VoucherView(
'eb9e1d9b-dce2-48a9-B64F-f0872f3157d2',
'xLKJS',
'mathieu@fairness.coop'
);

const voucher = mock(Voucher);
when(voucher.getId()).thenReturn('eb9e1d9b-dce2-48a9-B64F-f0872f3157d2');
when(voucher.getCode()).thenReturn('xLKJS');
when(voucher.getEmail()).thenReturn('mathieu@fairness.coop');
when(
voucherRepository.findOneByCode('eb9e1d9b-dce2-48a9-b64f-f0872f3157d2')
).thenResolve(instance(voucher));

expect(await queryHandler.execute(query)).toMatchObject(expectedResult);

verify(
voucherRepository.findOneByCode('eb9e1d9b-dce2-48a9-b64f-f0872f3157d2')
).once();
});

it('testGetVoucherNotFound', async () => {
const voucherRepository = mock(VoucherRepository);
const queryHandler = new GetVoucherByCodeQueryHandler(instance(voucherRepository));
when(
voucherRepository.findOneByCode('eb9e1d9b-dce2-48a9-b64f-f0872f3157d2')
).thenResolve(null);

try {
expect(await queryHandler.execute(query)).toBeUndefined();
} catch (e) {
expect(e).toBeInstanceOf(VoucherNotFoundException);
expect(e.message).toBe('schools.errors.voucher_not_found');
verify(
voucherRepository.findOneByCode('eb9e1d9b-dce2-48a9-b64f-f0872f3157d2')
).once();
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { QueryHandler } from '@nestjs/cqrs';
import { Inject } from '@nestjs/common';
import { IVoucherRepository } from 'src/Domain/School/Repository/IVoucherRepository';
import { VoucherNotFoundException } from 'src/Domain/School/Exception/VoucherNotFoundException';
import { GetVoucherByCodeQuery } from './GetVoucherByCodeQuery';
import { VoucherView } from '../../View/VoucherView';

@QueryHandler(GetVoucherByCodeQuery)
export class GetVoucherByCodeQueryHandler {
constructor(
@Inject('IVoucherRepository')
private readonly voucherRepository: IVoucherRepository
) {}

public async execute({ code }: GetVoucherByCodeQuery): Promise<VoucherView> {
const voucher = await this.voucherRepository.findOneByCode(code);

if (!voucher) {
throw new VoucherNotFoundException();
}

return new VoucherView(
voucher.getId(),
voucher.getCode(),
voucher.getEmail(),
);
}
}
1 change: 0 additions & 1 deletion api/src/Application/School/View/SchoolDetailView.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { UserSummaryView } from 'src/Application/User/View/UserSummaryView';
import { Status, Type } from 'src/Domain/School/AbstractSchool';

export class SchoolDetailView {
Expand Down
7 changes: 7 additions & 0 deletions api/src/Application/School/View/VoucherView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class VoucherView {
constructor(
public readonly id: string,
public readonly code: string,
public readonly email: string,
) {}
}
1 change: 1 addition & 0 deletions api/src/Domain/School/Repository/IVoucherRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ export interface IVoucherRepository {
remove(voucher: Voucher): void;
findOneByEmailAndSchool(email: string, school: School): Promise<Voucher | undefined>;
findOneById(id: string): Promise<Voucher | undefined>;
findOneByCode(code: string): Promise<Voucher | undefined>;
findBySchool(schoolId: string): Promise<Voucher[]>;
}
9 changes: 3 additions & 6 deletions api/src/Infrastructure/School/Action/GetSchoolsAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import { PaginationDTO } from 'src/Infrastructure/Common/DTO/PaginationDTO';
import { RolesGuard } from 'src/Infrastructure/User/Security/RolesGuard';
import { Roles } from 'src/Infrastructure/User/Decorator/Roles';
import { UserRole } from 'src/Domain/User/User.entity';
import { LoggedUser } from 'src/Infrastructure/User/Decorator/LoggedUser';
import { UserAuthView } from 'src/Infrastructure/User/Security/UserAuthView';

@Controller('schools')
@ApiTags('School')
Expand All @@ -23,12 +21,11 @@ export class GetSchoolsAction {
) {}

@Get()
@Roles(UserRole.PHOTOGRAPHER, UserRole.DIRECTOR)
@Roles(UserRole.PHOTOGRAPHER)
@ApiOperation({summary: 'Get all schools'})
public async index(
@Query() { page }: PaginationDTO,
@LoggedUser() { id, role }: UserAuthView
@Query() { page }: PaginationDTO
): Promise<Pagination<SchoolView>> {
return await this.queryBus.execute(new GetSchoolsQuery(page, id, role));
return await this.queryBus.execute(new GetSchoolsQuery(page));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
Controller,
Inject,
Post,
Body,
BadRequestException,
Param
} from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import { ICommandBus } from 'src/Application/ICommandBus';
import { CreateUserCommand } from 'src/Application/User/Command/CreateUserCommand';
import { UserView } from 'src/Application/User/View/UserView';
import { IQueryBus } from 'src/Application/IQueryBus';
import { GetVoucherByCodeQuery } from 'src/Application/School/Query/Voucher/GetVoucherByCodeQuery';
import { VoucherView } from 'src/Application/School/View/VoucherView';
import { ConsumeVoucherDTO } from '../../DTO/ConsumeVoucherDTO';
import { UserRole } from 'src/Domain/User/User.entity';
import { RemoveVoucherCommand } from 'src/Application/School/Command/Voucher/RemoveVoucherCommand';

@Controller('vouchers')
@ApiTags('School voucher')
export class ConsumeVoucherAction {
constructor(
@Inject('ICommandBus')
private readonly commandBus: ICommandBus,
@Inject('IQueryBus')
private readonly queryBus: IQueryBus
) {}

@Post(':code/consume')
@ApiOperation({ summary: 'Consume voucher' })
public async index(
@Param() code: string,
@Body() { firstName, lastName, password }: ConsumeVoucherDTO
): Promise<UserView> {
try {
const voucher: VoucherView = await this.queryBus.execute(new GetVoucherByCodeQuery(code));
if (!voucher) {
return;
}

await this.commandBus.execute(
new CreateUserCommand(
firstName,
lastName,
voucher.email,
password,
UserRole.DIRECTOR
)
);

await this.commandBus.execute(new RemoveVoucherCommand(voucher.id));
} catch (e) {
throw new BadRequestException(e.message);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { IdDTO } from 'src/Infrastructure/Common/DTO/IdDTO';
import { Roles } from 'src/Infrastructure/User/Decorator/Roles';
import { RolesGuard } from 'src/Infrastructure/User/Security/RolesGuard';

@Controller('schools')
@Controller('vouchers')
@ApiTags('School voucher')
@ApiBearerAuth()
@UseGuards(AuthGuard('bearer'), RolesGuard)
Expand All @@ -25,7 +25,7 @@ export class RemoveVoucherAction {
private readonly commandBus: ICommandBus
) {}

@Delete('/:schoolId/vouchers/:id')
@Delete(':id')
@Roles(UserRole.PHOTOGRAPHER)
@ApiOperation({ summary: 'Remove voucher' })
public async index(@Param() { id }: IdDTO) {
Expand Down
31 changes: 31 additions & 0 deletions api/src/Infrastructure/School/DTO/ConsumeVoucherDTO.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ConsumeVoucherDTO } from './ConsumeVoucherDTO';
import { validate } from 'class-validator';

describe('ConsumeVoucherDTO', () => {
it('testValidDTO', async () => {
const dto = new ConsumeVoucherDTO();

dto.firstName = 'Mathieu';
dto.lastName = 'MARCHOIS';
dto.password = 'password';

const validation = await validate(dto);
expect(validation).toHaveLength(0);
});

it('testInvalidDTO', async () => {
const dto = new ConsumeVoucherDTO();

const validation = await validate(dto);
expect(validation).toHaveLength(3);
expect(validation[0].constraints).toMatchObject({
isNotEmpty: 'firstName should not be empty'
});
expect(validation[1].constraints).toMatchObject({
isNotEmpty: 'lastName should not be empty'
});
expect(validation[2].constraints).toMatchObject({
isNotEmpty: 'password should not be empty'
});
});
});
16 changes: 16 additions & 0 deletions api/src/Infrastructure/School/DTO/ConsumeVoucherDTO.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';

export class ConsumeVoucherDTO {
@IsNotEmpty()
@ApiProperty()
public firstName: string;

@IsNotEmpty()
@ApiProperty()
public lastName: string;

@IsNotEmpty()
@ApiProperty()
public password: string;
}
8 changes: 8 additions & 0 deletions api/src/Infrastructure/School/Repository/VoucherRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ export class VoucherRepository implements IVoucherRepository {
.getOne();
}

public findOneByCode(code: string): Promise<Voucher | undefined> {
return this.repository
.createQueryBuilder('voucher')
.select([ 'voucher.id', 'voucher.code', 'voucher.email' ])
.where('voucher.code = :code', { code })
.getOne();
}

public findOneByEmailAndSchool(email: string, school: School): Promise<Voucher | undefined> {
return this.repository
.createQueryBuilder('voucher')
Expand Down
Loading

0 comments on commit 9c8f41d

Please sign in to comment.