Skip to content

Commit

Permalink
Merge pull request #5 from hectorgomezv/1-expose-companies-last-state
Browse files Browse the repository at this point in the history
1 expose companies last state
  • Loading branch information
hectorgomezv authored Oct 16, 2022
2 parents d6d5336 + d52599d commit aac2d0a
Show file tree
Hide file tree
Showing 14 changed files with 197 additions and 38 deletions.
60 changes: 55 additions & 5 deletions src/companies/domain/companies.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CompaniesRepository } from '../repositories/companies.repository';
import { CompaniesService } from './companies.service';
import { CompanyStatesService } from './company-states.service';
import { CreateCompanyDto } from './dto/create-company.dto';
import { companyStateFactory } from './entities/__tests__/company-state.factory';
import { companyFactory } from './entities/__tests__/company.factory';

describe('CompaniesService', () => {
Expand All @@ -25,6 +26,8 @@ describe('CompaniesService', () => {
const mockedCompanyStateService = jest.mocked({
createCompanyState: jest.fn(),
deleteByCompanyUuid: jest.fn(),
getLastStateByCompanyUuid: jest.fn(),
getLastStateByCompanyUuids: jest.fn(),
} as unknown as CompanyStatesService);

const service = new CompaniesService(
Expand All @@ -51,16 +54,18 @@ describe('CompaniesService', () => {

it('should create a CompanyState when creating a company', async () => {
const company = companyFactory();
const state = companyStateFactory();
const dto = <CreateCompanyDto>{
symbol: faker.finance.currencyCode(),
name: faker.company.name(),
};
mockedCompaniesRepository.findBySymbol.mockResolvedValue(null);
mockedCompaniesRepository.create.mockResolvedValue(company);
mockedCompanyStateService.createCompanyState.mockResolvedValue(state);

const created = await service.create(dto);
const actual = await service.create(dto);

expect(created).toEqual(company);
expect(actual).toEqual({ ...company, state });
expect(mockedCompanyStateService.createCompanyState).toBeCalledTimes(1);
expect(mockedCompanyStateService.createCompanyState).toBeCalledWith(
company,
Expand All @@ -69,18 +74,63 @@ describe('CompaniesService', () => {
});

describe('retrieving', () => {
it('should call repository for retrieving all the companies', async () => {
await service.findAll();
it('should retrieve all the companies and merge their states', async () => {
const companies = [companyFactory(), companyFactory()];
const states = [
companyStateFactory(
faker.datatype.uuid(),
Date.now(),
faker.datatype.number(),
faker.datatype.number(),
companies[1].uuid,
),
companyStateFactory(
faker.datatype.uuid(),
Date.now(),
faker.datatype.number(),
faker.datatype.number(),
companies[0].uuid,
),
];
mockedCompaniesRepository.findAll.mockResolvedValue(companies);
mockedCompanyStateService.getLastStateByCompanyUuids.mockResolvedValue(
states,
);

const actual = await service.findAll();

const expected = [
{
...companies[0],
state: states[1],
},
{
...companies[1],
state: states[0],
},
];
expect(actual).toEqual(expected);
expect(mockedCompaniesRepository.findAll).toHaveBeenCalledTimes(1);
expect(
mockedCompanyStateService.getLastStateByCompanyUuids,
).toHaveBeenCalledTimes(1);
});

it('should call repository for retrieving a company', async () => {
const company = companyFactory();
const state = companyStateFactory();
mockedCompaniesRepository.findOne.mockResolvedValue(company);
mockedCompanyStateService.getLastStateByCompanyUuid.mockResolvedValue(
state,
);

await service.findOne(company.uuid);
const actual = await service.findOne(company.uuid);

expect(actual).toEqual({ ...company, state });
expect(mockedCompaniesRepository.findOne).toHaveBeenCalledTimes(1);
expect(
mockedCompanyStateService.getLastStateByCompanyUuid,
).toHaveBeenCalledTimes(1);
});

it('should fail if a company cannot be found by uuid', async () => {
Expand Down
32 changes: 24 additions & 8 deletions src/companies/domain/companies.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid';
import { CompaniesRepository } from '../repositories/companies.repository';
import { Company } from './entities/company.entity';
import { Company, CompanyWithState } from './entities/company.entity';
import { CompanyStatesService } from './company-states.service';
import { PositionsRepository } from '../../portfolios/repositories/positions.repository';
import { Cron } from '@nestjs/schedule';
Expand All @@ -22,7 +22,7 @@ export class CompaniesService {
private readonly companyStatesService: CompanyStatesService,
) {}

async create(createCompanyDto: CreateCompanyDto): Promise<Company> {
async create(createCompanyDto: CreateCompanyDto): Promise<CompanyWithState> {
const exists = await this.repository.findBySymbol(createCompanyDto.symbol);

if (exists) {
Expand All @@ -34,23 +34,38 @@ export class CompaniesService {
uuid: uuidv4(),
});

await this.companyStatesService.createCompanyState(company);
const state = await this.companyStatesService.createCompanyState(company);

return company;
return <CompanyWithState>{ ...company, state };
}

findAll(): Promise<Company[]> {
return this.repository.findAll();
async findAll(): Promise<CompanyWithState[]> {
const companies = await this.repository.findAll();
const states = await this.companyStatesService.getLastStateByCompanyUuids(
companies.map((company) => company.uuid),
);

return companies.map(
(company) =>
<CompanyWithState>{
...company,
state: states.find((state) => state.companyUuid === company.uuid),
},
);
}

async findOne(uuid: string): Promise<Company> {
async findOne(uuid: string): Promise<CompanyWithState> {
const company = await this.repository.findOne(uuid);

if (!company) {
throw new NotFoundException('Company not found');
}

return company;
const state = await this.companyStatesService.getLastStateByCompanyUuid(
uuid,
);

return <CompanyWithState>{ ...company, state };
}

async remove(uuid: string) {
Expand Down Expand Up @@ -83,6 +98,7 @@ export class CompaniesService {
private refreshAllStatesAtMidday() {
return this.refreshAllStates();
}

@Cron('0 02 4 * * *', { timeZone: 'America/New_York' })
private refreshAllStatesAtMarketClose() {
return this.refreshAllStates();
Expand Down
16 changes: 16 additions & 0 deletions src/companies/domain/company-states.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('CompanyStatesService', () => {
const mockedCompanyStatesRepository = jest.mocked({
create: jest.fn(),
deleteByCompanyUuid: jest.fn(),
getLastByCompanyUuids: jest.fn(),
} as unknown as CompanyStatesRepository);

const mockedFinancialDataClient = jest.mocked({
Expand Down Expand Up @@ -39,6 +40,21 @@ describe('CompanyStatesService', () => {
});
});

describe('retrieving', () => {
it('should call repository to obtain the last states for an array of company uuids', async () => {
const companyUuids = [faker.datatype.uuid(), faker.datatype.uuid()];

await service.getLastStateByCompanyUuids(companyUuids);

expect(
mockedCompanyStatesRepository.getLastByCompanyUuids,
).toBeCalledTimes(1);
expect(
mockedCompanyStatesRepository.getLastByCompanyUuids,
).toHaveBeenCalledWith(companyUuids);
});
});

describe('deletion', () => {
it('should call repository for deletion', async () => {
const companyUuid = faker.datatype.uuid();
Expand Down
10 changes: 10 additions & 0 deletions src/companies/domain/company-states.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ export class CompanyStatesService {
private readonly financialDataClient: IFinancialDataClient,
) {}

async getLastStateByCompanyUuid(companyUuid: string): Promise<CompanyState> {
return this.companyStatesRepository.getLastByCompanyUuid(companyUuid);
}

async getLastStateByCompanyUuids(
companyUuids: string[],
): Promise<CompanyState[]> {
return this.companyStatesRepository.getLastByCompanyUuids(companyUuids);
}

async createCompanyState(company: Company): Promise<CompanyState> {
const quoteSummary: QuoteSummary =
await this.financialDataClient.getQuoteSummary(company.symbol);
Expand Down
6 changes: 6 additions & 0 deletions src/companies/domain/entities/company.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { CompanyState } from './company-state.entity';

export class Company {
uuid: string;
name: string;
symbol: string;
}

export class CompanyWithState extends Company {
state: CompanyState;
}
10 changes: 5 additions & 5 deletions src/companies/repositories/companies.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,27 @@ export class CompaniesRepository {

async create(company: Company): Promise<Company> {
const created = (await this.model.create(company)).toObject();
return plainToInstance(Company, created);
return plainToInstance(Company, created, { excludePrefixes: ['_', '__'] });
}

async findAll(): Promise<Company[]> {
const result = await this.model.find().lean();
return plainToInstance(Company, result);
return plainToInstance(Company, result, { excludePrefixes: ['_', '__'] });
}

async findByUuidIn(uuid: string[]): Promise<Company[]> {
const result = await this.model.find({ uuid: { $in: uuid } }).lean();
return plainToInstance(Company, result);
return plainToInstance(Company, result, { excludePrefixes: ['_', '__'] });
}

async findOne(uuid: string): Promise<Company> {
const result = await this.model.findOne({ uuid }).lean();
return plainToInstance(Company, result);
return plainToInstance(Company, result, { excludePrefixes: ['_', '__'] });
}

async findBySymbol(symbol: string): Promise<Company> {
const result = await this.model.findOne({ symbol }).lean();
return plainToInstance(Company, result);
return plainToInstance(Company, result, { excludePrefixes: ['_', '__'] });
}

async deleteOne(uuid: string): Promise<void> {
Expand Down
28 changes: 26 additions & 2 deletions src/companies/repositories/company-states.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ export class CompanyStatesRepository {

async create(companyState: CompanyState): Promise<CompanyState> {
const created = (await this.model.create(companyState)).toObject();
return plainToInstance(CompanyState, created);
return plainToInstance(CompanyState, created, {
excludePrefixes: ['_', '__'],
});
}

async deleteByCompanyUuid(companyUuid: string): Promise<void> {
Expand All @@ -31,6 +33,28 @@ export class CompanyStatesRepository {
.limit(1)
.lean();

return plainToInstance(CompanyState, result);
return plainToInstance(CompanyState, result, {
excludePrefixes: ['_', '__'],
});
}

async getLastByCompanyUuids(companyUuids: string[]): Promise<CompanyState[]> {
const result = await this.model
.aggregate()
.match({ companyUuid: { $in: companyUuids } })
.group({ _id: '$companyUuid', state: { $last: '$$ROOT' } })
.lookup({
from: 'companies',
localField: '_id',
foreignField: 'uuid',
as: 'company',
})
.exec();

return plainToInstance(
CompanyState,
result.map((i) => i.state),
{ excludePrefixes: ['_', '__'] },
);
}
}
8 changes: 4 additions & 4 deletions src/companies/routes/companies.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { MainExceptionFilter } from '../../common/routes/filters/main-exception.
import { DataInterceptor } from '../../common/routes/interceptors/data.interceptor';
import { CompaniesService } from '../domain/companies.service';
import { CreateCompanyDto } from '../domain/dto/create-company.dto';
import { Company } from './entities/company.entity';
import { Company, CompanyWithState } from './entities/company.entity';

@UseInterceptors(DataInterceptor)
@UseFilters(MainExceptionFilter)
Expand All @@ -33,20 +33,20 @@ export class CompaniesController {
constructor(private readonly companiesService: CompaniesService) {}

@Post()
@CreatedResponse(Company)
@CreatedResponse(CompanyWithState)
@ApiBadRequestResponse()
create(@Body() createCompanyDto: CreateCompanyDto) {
return this.companiesService.create(createCompanyDto);
}

@Get()
@OkArrayResponse(Company)
@OkArrayResponse(CompanyWithState)
findAll() {
return this.companiesService.findAll();
}

@Get(':uuid')
@OkResponse(Company)
@OkResponse(CompanyWithState)
@ApiNotFoundResponse()
findOne(@Param('uuid') uuid: string) {
return this.companiesService.findOne(uuid);
Expand Down
15 changes: 15 additions & 0 deletions src/companies/routes/entities/company-state.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { CompanyState as DomainCompanyState } from '../../domain/entities/company-state.entity';

export class CompanyState implements DomainCompanyState {
@ApiProperty()
uuid: string;
@ApiProperty()
timestamp: number;
@ApiProperty()
price: number;
@ApiProperty()
peg: number;
@ApiProperty()
companyUuid: string;
}
14 changes: 13 additions & 1 deletion src/companies/routes/entities/company.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { Company as DomainCompany } from '../../domain/entities/company.entity';
import {
Company as DomainCompany,
CompanyWithState as DomainCompanyWithState,
} from '../../domain/entities/company.entity';
import { CompanyState } from './company-state.entity';

export class Company implements DomainCompany {
@ApiProperty()
Expand All @@ -9,3 +13,11 @@ export class Company implements DomainCompany {
@ApiProperty()
symbol: string;
}

export class CompanyWithState
extends Company
implements DomainCompanyWithState
{
@ApiProperty()
state: CompanyState;
}
2 changes: 2 additions & 0 deletions src/companies/test/companies.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe('Companies e2e tests', () => {
uuid: createdUuid,
name: dto.name,
symbol: dto.symbol,
state: expect.anything(),
}),
});
});
Expand All @@ -64,6 +65,7 @@ describe('Companies e2e tests', () => {
uuid: createdUuid,
name: dto.name,
symbol: dto.symbol,
state: expect.anything(),
}),
});
});
Expand Down
Loading

0 comments on commit aac2d0a

Please sign in to comment.