Skip to content

[MERGE] main -> staging post GG release #204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Jun 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c2320f4
[TM-2088] add loadAssociationData method to entity processors
Scriptmatico Jun 2, 2025
cf1b65f
[TM-2088] feat: update loadAssociationData method to accept a single …
Scriptmatico Jun 2, 2025
6b58454
[TM-2088] feat: update loadAssociationData method to accept an array …
Scriptmatico Jun 2, 2025
de06ea2
[TM-2088] feat: update loadAssociationData method signatures to retur…
Scriptmatico Jun 3, 2025
e41d04f
[TM-2088] feat: add mock implementation for loadAssociationData in En…
Scriptmatico Jun 4, 2025
fb32944
[TM-2088] feat: implement loadAssociationData method in multiple proc…
Scriptmatico Jun 4, 2025
145f045
[TM-2088] feat: refactor loadAssociationData to return association da…
Scriptmatico Jun 10, 2025
dc946fa
[TM-2088] feat: enhance loadAssociationData method to support string …
Scriptmatico Jun 10, 2025
ba9c36b
[TM-2088] feat: update loadAssociationData to handle empty site and s…
Scriptmatico Jun 10, 2025
86998c0
[TM-2088] feat: add mock implementation for loadAssociationData in En…
Scriptmatico Jun 10, 2025
2a5c033
[TM-2088] feat: add placeholder for loadAssociationData method in ent…
Scriptmatico Jun 10, 2025
7c95fec
[TM-2088] feat: update Project DTO to allow nullable treesPlantedCoun…
Scriptmatico Jun 10, 2025
9de624e
[TM-2088] feat: update getLightDto method in ProjectProcessor to acce…
Scriptmatico Jun 10, 2025
db8175b
[TM-2088] feat: remove unused Project import and add eslint directive…
Scriptmatico Jun 10, 2025
c75141d
[TM-2088] feat: enhance loadAssociationData method with comprehensive…
Scriptmatico Jun 11, 2025
6725b6c
[TM-2088] update test for loadAssociationData to use ts-expect-error …
Scriptmatico Jun 11, 2025
6ae0f2e
[TM-2088] test: update loadAssociationData test to handle nullable tr…
Scriptmatico Jun 11, 2025
9cb988d
[TM-2088] ignore method for unit test
Scriptmatico Jun 11, 2025
3d00ec3
[TM-2068] Add the missing "year" field.
roguenet Jun 11, 2025
d2ba455
Merge pull request #202 from wri/feat/TM-2068-financial-indicator-year
roguenet Jun 11, 2025
c6f033b
Merge pull request #191 from wri/feat/TM-2088-Add-support-for-trees-p…
Scriptmatico Jun 11, 2025
75723a5
[TM-2150] change expiration for token in reset password
pachonjcl Jun 12, 2025
43593f6
Merge pull request #203 from wri/fix/TM-2150_change_reset_password_ex…
pachonjcl Jun 12, 2025
ef3f585
Merge pull request #200 from wri/release/gleaming-garnet
roguenet Jun 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/entity-service/src/entities/dto/project.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export class ProjectLightDto extends EntityDto {

@ApiProperty()
updatedAt: Date;

@ApiProperty({ nullable: true, type: Number })
treesPlantedCount: number | null;
}

export type ProjectMedia = Pick<ProjectFullDto, keyof typeof Project.MEDIA>;
Expand Down
8 changes: 7 additions & 1 deletion apps/entity-service/src/entities/entities.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class StubProcessor extends EntityProcessor<Project, ProjectLightDto, ProjectFul
getLightDto = jest.fn(() => Promise.resolve({ id: faker.string.uuid(), dto: new ProjectLightDto() }));
delete = jest.fn(() => Promise.resolve());
update = jest.fn(() => Promise.resolve());
loadAssociationData = jest.fn(() => Promise.resolve({} as Record<number, ProjectLightDto>));
}

describe("EntitiesController", () => {
Expand Down Expand Up @@ -63,8 +64,13 @@ describe("EntitiesController", () => {
});

it("should add DTOs to the document", async () => {
const projects = await ProjectFactory.createMany(2);
// @ts-expect-error stub processor type issues
processor.findMany.mockResolvedValue({ models: await ProjectFactory.createMany(2), paginationTotal: 2 });
processor.findMany.mockResolvedValue({ models: projects, paginationTotal: 2 });
processor.loadAssociationData.mockResolvedValue({ [projects[0].id]: new ProjectLightDto() } as Record<
number,
ProjectLightDto
>);
policyService.getPermissions.mockResolvedValue(["projects-read"]);
policyService.authorize.mockResolvedValue();

Expand Down
15 changes: 13 additions & 2 deletions apps/entity-service/src/entities/processors/entity-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ export abstract class EntityProcessor<
abstract findMany(query: EntityQueryDto): Promise<PaginatedResult<ModelType>>;

abstract getFullDto(model: ModelType): Promise<DtoResult<FullDto>>;
abstract getLightDto(model: ModelType): Promise<DtoResult<LightDto>>;

abstract getLightDto(model: ModelType, lightResource?: EntityDto): Promise<DtoResult<LightDto>>;

async getFullDtos(models: ModelType[]): Promise<DtoResult<FullDto>[]> {
const results: DtoResult<FullDto>[] = [];
Expand All @@ -86,8 +87,12 @@ export abstract class EntityProcessor<

async getLightDtos(models: ModelType[]): Promise<DtoResult<LightDto>[]> {
const results: DtoResult<LightDto>[] = [];

const associateData = (await this.loadAssociationData(models.map(m => m.id))) as Record<number, LightDto>;

for (const model of models) {
results.push(await this.getLightDto(model));
const dto = await this.getLightDto(model, associateData[model.id]);
results.push(dto);
}
return results;
}
Expand Down Expand Up @@ -158,6 +163,12 @@ export abstract class EntityProcessor<

await model.save();
}

/* istanbul ignore next */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadAssociationData(ids: number[]): Promise<Record<number, object>> {
return Promise.resolve({});
}
}

export abstract class ReportProcessor<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,10 @@ describe("ProjectProcessor", () => {

describe("processSideload", () => {
it("throws if the sideloads includes something unsupported", async () => {
await ProjectFactory.create();
const project = await ProjectFactory.create();
policyService.getPermissions.mockResolvedValue(["projects-read"]);
const document = buildJsonApi(ProjectLightDto);
await processor.loadAssociationData([project.id]);
await expect(
processor.addIndex(document, { sideloads: [{ entity: "siteReports", pageSize: 5 }] })
).rejects.toThrow(BadRequestException);
Expand Down Expand Up @@ -329,7 +330,7 @@ describe("ProjectProcessor", () => {

policyService.getPermissions.mockResolvedValue(["projects-read"]);
const { models } = await processor.findMany({});
const { id, dto } = await processor.getLightDto(models[0]);
const { id, dto } = await processor.getLightDto(models[0], new ProjectLightDto());
expect(id).toEqual(uuid);
expect(dto).toMatchObject({
uuid,
Expand Down
88 changes: 85 additions & 3 deletions apps/entity-service/src/entities/processors/project.processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
TreeSpecies
} from "@terramatch-microservices/database/entities";
import { Dictionary, groupBy, sumBy } from "lodash";
import { Op } from "sequelize";
import { Op, Sequelize } from "sequelize";
import { ANRDto, ProjectApplicationDto, ProjectFullDto, ProjectLightDto, ProjectMedia } from "../dto/project.dto";
import { EntityQueryDto } from "../dto/entity-query.dto";
import { FrameworkKey } from "@terramatch-microservices/database/constants/framework";
Expand All @@ -24,6 +24,7 @@ import { ProcessableEntity } from "../entities.service";
import { DocumentBuilder } from "@terramatch-microservices/common/util";
import { ProjectUpdateAttributes } from "../dto/entity-update.dto";
import { populateDto } from "@terramatch-microservices/common/dto/json-api-attributes";
import { EntityDto } from "../dto/entity.dto";

export class ProjectProcessor extends EntityProcessor<
Project,
Expand Down Expand Up @@ -141,14 +142,17 @@ export class ProjectProcessor extends EntityProcessor<
await processor.addIndex(document, { page: { size: pageSize }, projectUuid: model.uuid }, true);
}

async getLightDto(project: Project) {
async getLightDto(project: Project, associateDto: EntityDto) {
const projectId = project.id;
const totalHectaresRestoredSum =
(await SitePolygon.active().approved().sites(Site.approvedUuidsSubquery(projectId)).sum("calcArea")) ?? 0;

return {
id: project.uuid,
dto: new ProjectLightDto(project, {
totalHectaresRestoredSum
totalHectaresRestoredSum,
treesPlantedCount: 0,
...associateDto
})
};
}
Expand Down Expand Up @@ -292,4 +296,82 @@ export class ProjectProcessor extends EntityProcessor<

return pTotal + sTotal + nTotal;
}

/* istanbul ignore next */
async loadAssociationData(projectIds: number[]): Promise<Record<number, ProjectLightDto>> {
const associationDtos: Record<number, ProjectLightDto> = {};
const sites = await this.getSites(projectIds);

if (sites.length === 0) {
return associationDtos;
}

const siteIdToProjectId = new Map<number, number>();
for (const site of sites) {
siteIdToProjectId.set(site.id, site.projectId);
if (associationDtos[site.projectId] !== undefined) {
associationDtos[site.projectId] = {} as ProjectLightDto; // Initialize with default structure
}
}

const approvedSiteReports = await this.getSiteReports(sites);

if (approvedSiteReports.length === 0) {
return associationDtos;
}

const siteReportIdToProjectId = new Map<number, number>();
for (const report of approvedSiteReports) {
const projectId = siteIdToProjectId.get(report.siteId) ?? null;
if (projectId !== null) {
siteReportIdToProjectId.set(report.id, projectId);
}
}
const treeSpecies = await this.getTreeSpecies(approvedSiteReports);

for (const species of treeSpecies) {
const projectId = siteReportIdToProjectId.get(species.speciesableId);
if (projectId !== undefined) {
const dto = associationDtos[projectId] as ProjectLightDto;

if (dto == null) {
associationDtos[projectId] = { treesPlantedCount: species.amount } as ProjectLightDto;
} else {
dto.treesPlantedCount = (dto.treesPlantedCount ?? 0) + (species.amount ?? 0);
}
}
}

return associationDtos;
}

/* istanbul ignore next */
private async getTreeSpecies(approvedSiteReports: SiteReport[]) {
return await TreeSpecies.visible()
.collection("tree-planted")
.siteReports(approvedSiteReports.map(r => r.id))
.findAll({
attributes: ["speciesableId", [Sequelize.fn("SUM", Sequelize.col("amount")), "amount"]],
group: ["speciesableId"],
raw: true
});
}

/* istanbul ignore next */
private async getSiteReports(sites: Site[]) {
return await SiteReport.findAll({
where: { id: { [Op.in]: SiteReport.approvedIdsSubquery(sites.map(s => s.id)) } },
attributes: ["id", "siteId"],
raw: true
});
}

/* istanbul ignore next */
private async getSites(numericProjectIds: number[]) {
return await Site.findAll({
where: { id: { [Op.in]: Site.approvedIdsProjectsSubquery(numericProjectIds) } },
attributes: ["id", "projectId"],
raw: true
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const COLUMNS: ColumnMapping<FinancialIndicator, FinancialIndicatorAssociations>
"uuid",
"createdAt",
"updatedAt",
"year",
"collection",
"amount",
"description",
Expand Down
2 changes: 1 addition & 1 deletion apps/user-service/src/auth/reset-password.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class ResetPasswordService {
}

const { uuid, locale } = user;
const resetToken = await this.jwtService.signAsync({ sub: uuid }, { expiresIn: "2h" });
const resetToken = await this.jwtService.signAsync({ sub: uuid }, { expiresIn: "7d" });

const resetLink = `${callbackUrl}/${resetToken}`;
await this.emailService.sendI18nTemplateEmail(emailAddress, locale, EMAIL_KEYS, {
Expand Down
5 changes: 4 additions & 1 deletion libs/database/src/lib/entities/financial-indicator.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AllowNull, AutoIncrement, Column, Index, Model, PrimaryKey, Table } from "sequelize-typescript";
import { BIGINT, DECIMAL, STRING, TEXT, UUID, UUIDV4 } from "sequelize";
import { BIGINT, DECIMAL, SMALLINT, STRING, TEXT, UUID, UUIDV4 } from "sequelize";

@Table({ tableName: "financial_indicators", underscored: true, paranoid: true })
export class FinancialIndicator extends Model<FinancialIndicator> {
Expand All @@ -15,6 +15,9 @@ export class FinancialIndicator extends Model<FinancialIndicator> {
@Column(BIGINT.UNSIGNED)
organisationId: number;

@Column(SMALLINT.UNSIGNED)
year: number;

@Column(STRING)
collection: string;

Expand Down
Loading