Skip to content

Commit

Permalink
Merge pull request #30 from LucasPereiraMiranda/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
LucasPereiraMiranda authored Jan 8, 2025
2 parents c46757e + 6b11225 commit 2af51f4
Show file tree
Hide file tree
Showing 15 changed files with 387 additions and 4 deletions.
Binary file modified .github/image/diagram-entity-relationship.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/image/swagger-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/image/tests-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ $ npm run migration:generate

## Swagger

A aplicação contém um Swagger, que inclui os contratos e especificações para comunicação com a API. Ao executar a aplicação localmente com o Docker, o Swagger pode ser acessado por meio da seguinte URI:
A aplicação contém um Swagger, que inclui os contratos e especificações para comunicação com a API. Ao executar a aplicação localmente, o Swagger pode ser acessado por meio da seguinte URI:

```bash
$ http://localhost:3000/api#/
Expand All @@ -163,6 +163,7 @@ Para garantir que o sistema seja capaz de suportar um grande número de usuário

- **Adição de Paginação**: Foi implementada a paginação nos endpoints que retornam muitos registros (associados a findAll). Isso foi feito para evitar consultas que retornem grandes volumes de dados de uma só vez, ajudando a reduzir o tráfego de dados e melhorando a performance ao acessar as informações de forma mais controlada.


### Estratégias para observabilidade

A observabilidade é um fator importante para garantir o bom funcionamento do sistema produtivo. Para isso, foram implementadas as seguintes estratégias no desafio:
Expand Down
18 changes: 18 additions & 0 deletions src/migrations/1736369286911-migration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class Migration1736369286911 implements MigrationInterface {
name = 'Migration1736369286911'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "harvest" DROP CONSTRAINT "FK_246d631160ff530434f0380a0ec"`);
await queryRunner.query(`ALTER TABLE "harvest" RENAME COLUMN "harvest_id" TO "agricultural_property_id"`);
await queryRunner.query(`ALTER TABLE "harvest" ADD CONSTRAINT "FK_95308fdd7e5d65bce65a6d9fc2d" FOREIGN KEY ("agricultural_property_id") REFERENCES "agricultural_property"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "harvest" DROP CONSTRAINT "FK_95308fdd7e5d65bce65a6d9fc2d"`);
await queryRunner.query(`ALTER TABLE "harvest" RENAME COLUMN "agricultural_property_id" TO "harvest_id"`);
await queryRunner.query(`ALTER TABLE "harvest" ADD CONSTRAINT "FK_246d631160ff530434f0380a0ec" FOREIGN KEY ("harvest_id") REFERENCES "agricultural_property"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Repository } from 'typeorm';
import { Repository, SelectQueryBuilder } from 'typeorm';

import {
MockRepository,
Expand All @@ -12,12 +12,13 @@ import { agriculturalPropertyMock } from './mocks/agricultural-property.mock';
import { GrowerService } from '../../grower/grower.service';
import { Grower } from '../../../modules/grower/grower.entity';
import { growerCpfMock } from '../../../modules/grower/__tests__/mocks/grower.mock';
import { selectQueryBuilderMock } from '../../common/tests/select-query-builder.mock';

describe('AgriculturalPropertyService', () => {
let agriculturalPropertyService: AgriculturalPropertyService;
let growerService: GrowerService;
let repositoryMock: MockRepository<Repository<AgriculturalProperty>>;

let selectQueryBuilder: SelectQueryBuilder<any>;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
Expand All @@ -37,8 +38,12 @@ describe('AgriculturalPropertyService', () => {
agriculturalPropertyService = module.get<AgriculturalPropertyService>(
AgriculturalPropertyService,
);
selectQueryBuilder = selectQueryBuilderMock();
growerService = module.get<GrowerService>(GrowerService);
repositoryMock = module.get(getRepositoryToken(AgriculturalProperty));
jest
.spyOn(repositoryMock, 'createQueryBuilder')
.mockReturnValue(selectQueryBuilder);
});

beforeEach(() => jest.clearAllMocks());
Expand Down Expand Up @@ -115,4 +120,124 @@ describe('AgriculturalPropertyService', () => {
expect(result.count).toEqual(1);
});
});

describe('dashboard total properties', () => {
it('should successfully find total agricultural properties', async () => {
repositoryMock.count = jest.fn().mockResolvedValue(3);
repositoryMock.sum = jest.fn().mockResolvedValue(480);

const result =
await agriculturalPropertyService.dashboardTotalProperties();

expect(result).toEqual({
count: 3,
sumTotalArea: 480,
});
});

it('should successfully find total agricultural properties as 0, if values are nullable', async () => {
repositoryMock.count = jest.fn().mockResolvedValue(null);
repositoryMock.sum = jest.fn().mockResolvedValue(null);

const result =
await agriculturalPropertyService.dashboardTotalProperties();

expect(result).toEqual({
count: 0,
sumTotalArea: 0,
});
});
});

describe('dashboard properties by state', () => {
it('should successfully find properties by state', async () => {
jest.spyOn(selectQueryBuilder, 'getRawMany').mockReturnValue([
{
state: 'MG',
count: '2',
},
{
state: 'SP',
count: '1',
},
] as any);

const result =
await agriculturalPropertyService.dashboardPropertiesByState();

expect(result).toEqual([
{
state: 'MG',
count: 2,
},
{
state: 'SP',
count: 1,
},
]);
});
});

describe('dashboard properties by crop', () => {
it('should successfully find properties by crop', async () => {
jest.spyOn(selectQueryBuilder, 'getRawMany').mockReturnValue([
{
cropName: 'Mandioca',
count: '1',
sumTotalArea: '180',
},
{
cropName: 'Milho',
count: '1',
sumTotalArea: '150',
},
] as any);

const result =
await agriculturalPropertyService.dashboardPropertiesByCrop();

expect(result).toEqual([
{
cropName: 'Mandioca',
count: 1,
sumTotalArea: 180,
},
{
cropName: 'Milho',
count: 1,
sumTotalArea: 150,
},
]);
});
});

describe('dashboard land use', () => {
it('should successfully find land use', async () => {
repositoryMock.sum = jest
.fn()
.mockResolvedValueOnce(150)
.mockResolvedValueOnce(330);

const result = await agriculturalPropertyService.dashboardLandUse();

expect(result).toEqual({
sumVegetationArea: 150,
sumArableArea: 330,
});
});

it('should successfully find land use as 0, if values are nullable', async () => {
repositoryMock.sum = jest
.fn()
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(null);

const result = await agriculturalPropertyService.dashboardLandUse();

expect(result).toEqual({
sumVegetationArea: 0,
sumArableArea: 0,
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { FindOneByIdAgriculturalPropertyRequestDto } from './dto/request/findOne
import { FindOneByIdAgriculturalPropertyResponseDto } from './dto/response/findOneById-agricultural-property.response.dto';
import { FindAllAgriculturalPropertyResponseDto } from './dto/response/findAll-agricultural-property.response.dto';
import { FindAllAgriculturalPropertyQueryRequestDto } from './dto/request/findAll-agricultural-property.request.dto';
import { DashboardTotalPropertiesResponseDto } from './dto/response/dashboardTotalProperties.response.dto';
import { DashboardPropertiesByStateItemResponseDto } from './dto/response/dashboardPropertiesByState.response.dto copy';
import { DashboardPropertiesByCropItemResponseDto } from './dto/response/dashboardPropertiesByCrop.response.dto';
import { DashboardLandUseResponseDto } from './dto/response/dashboardLandUse.response.dto';
@Controller('agricultural-property')
@ApiTags('Propriedades Agrícola - Agricultural Property')
export class AgriculturalPropertyController {
Expand Down Expand Up @@ -86,4 +90,64 @@ export class AgriculturalPropertyController {
): Promise<FindAllAgriculturalPropertyResponseDto> {
return this.agriculturalPropertyService.findAll(input);
}

@ApiOperation({
summary:
'[Dashboard] Indica o total de fazendas e a soma de todas as areas totais',
})
@ApiResponse({
status: HttpStatus.OK,
description:
'Total de fazendas e a soma de todas as areas totais obtidas com sucesso',
type: DashboardTotalPropertiesResponseDto,
})
@Get('/dashboard/total')
async dashboardTotalProperties(): Promise<DashboardTotalPropertiesResponseDto> {
return this.agriculturalPropertyService.dashboardTotalProperties();
}

@ApiOperation({
summary: '[Dashboard] Indica o total de fazendas por estado',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Total de fazendas por estado obtidas com sucesso',
type: [DashboardPropertiesByStateItemResponseDto],
})
@Get('/dashboard/total-by-state')
async dashboardPropertiesByState(): Promise<
DashboardPropertiesByStateItemResponseDto[]
> {
return this.agriculturalPropertyService.dashboardPropertiesByState();
}

@ApiOperation({
summary:
'[Dashboard] Indica as métricas das propriedades rurais por cultura',
})
@ApiResponse({
status: HttpStatus.OK,
description:
'Métricas das propriedades rurais por cultura obtidas com sucesso',
type: [DashboardPropertiesByCropItemResponseDto],
})
@Get('/dashboard/total-by-crop')
async dashboardPropertiesByCrop(): Promise<
DashboardPropertiesByCropItemResponseDto[]
> {
return this.agriculturalPropertyService.dashboardPropertiesByCrop();
}

@ApiOperation({
summary: '[Dashboard] Indica as metricas de uso do solo',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Metricas de uso do solo obtidas com sucesso',
type: DashboardLandUseResponseDto,
})
@Get('/dashboard/land-use')
async dashboardLandUse(): Promise<DashboardLandUseResponseDto> {
return this.agriculturalPropertyService.dashboardLandUse();
}
}
65 changes: 65 additions & 0 deletions src/modules/agricutural-property/agricultural-property.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import { GrowerService } from '../grower/grower.service';
import { FindOneByIdAgriculturalPropertyResponseDto } from './dto/response/findOneById-agricultural-property.response.dto';
import { FindAllAgriculturalPropertyQueryRequestDto } from './dto/request/findAll-agricultural-property.request.dto';
import { FindAllAgriculturalPropertyResponseDto } from './dto/response/findAll-agricultural-property.response.dto';
import { DashboardPropertiesByStateItemResponseDto } from './dto/response/dashboardPropertiesByState.response.dto copy';
import { DashboardTotalPropertiesResponseDto } from './dto/response/dashboardTotalProperties.response.dto';
import { DashboardPropertiesByCropItemResponseDto } from './dto/response/dashboardPropertiesByCrop.response.dto';
import { DashboardLandUseResponseDto } from './dto/response/dashboardLandUse.response.dto';

@Injectable()
export class AgriculturalPropertyService {
Expand Down Expand Up @@ -86,4 +90,65 @@ export class AgriculturalPropertyService {
count,
};
}

async dashboardTotalProperties(): Promise<DashboardTotalPropertiesResponseDto> {
const [count, sumTotalArea] = await Promise.all([
this.agriculturalPropertyRepository.count(),
this.agriculturalPropertyRepository.sum('totalArea'),
]);
return {
count: count || 0,
sumTotalArea: sumTotalArea || 0,
};
}

async dashboardPropertiesByState(): Promise<
DashboardPropertiesByStateItemResponseDto[]
> {
const result = await this.agriculturalPropertyRepository
.createQueryBuilder('agriculturalProperty')
.select('agriculturalProperty.state', 'state')
.addSelect('COUNT(agriculturalProperty.id)', 'count')
.groupBy('agriculturalProperty.state')
.getRawMany();

const formattedResult = result.map((row) => ({
state: row.state,
count: parseInt(row.count, 10),
}));
return formattedResult;
}

async dashboardPropertiesByCrop(): Promise<
DashboardPropertiesByCropItemResponseDto[]
> {
const result = await this.agriculturalPropertyRepository
.createQueryBuilder('ap')
.innerJoin('ap.harverts', 'h')
.innerJoin('h.harvestToCrops', 'htc')
.innerJoin('htc.crop', 'c')
.select('c.name', 'cropName')
.addSelect('COUNT(DISTINCT ap.id)', 'count')
.addSelect('SUM(ap."total_area")', 'sumTotalArea')
.groupBy('c.name')
.getRawMany();

const formattedResult = result.map((row) => ({
cropName: row.cropName,
count: parseInt(row.count, 10),
sumTotalArea: parseFloat(row.sumTotalArea),
}));
return formattedResult;
}

async dashboardLandUse(): Promise<DashboardLandUseResponseDto> {
const [sumVegetationArea, sumArableArea] = await Promise.all([
this.agriculturalPropertyRepository.sum('vegetationArea'),
this.agriculturalPropertyRepository.sum('arableArea'),
]);
return {
sumVegetationArea: sumVegetationArea || 0,
sumArableArea: sumArableArea || 0,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';

export class DashboardLandUseResponseDto {
@ApiProperty({
type: Number,
description: 'Soma da área de vegetação de todas as propriedades rurais',
example: 150,
})
sumVegetationArea: number;

@ApiProperty({
type: Number,
description: 'Soma da área agricultável de todas as propriedades rurais',
example: 180,
})
sumArableArea: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';

export class DashboardPropertiesByCropItemResponseDto {
@ApiProperty({
type: Number,
description: 'Soma da área total em hectares associado a cultura',
example: 1050,
})
sumTotalArea: number;

@ApiProperty({
type: String,
description: 'Nome da cultura',
example: 'Milho',
})
cropName: string;
}
Loading

0 comments on commit 2af51f4

Please sign in to comment.