From 31fb9e0b1fd4e6db4ecc739328c8d28d6d70c434 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Wed, 8 Oct 2025 10:41:53 -0400 Subject: [PATCH 01/11] [TM-2517] enable approval frow for disturbance report --- .../entities/dto/disturbance-report.dto.ts | 6 + .../disturbance-report.processor.ts | 163 +++++++++++++++++- .../entities/processors/entity-processor.ts | 10 +- .../financial-report.processor.spec.ts | 26 +-- .../processors/financial-report.processor.ts | 2 +- .../disturbance-report-entry.entity.ts | 11 +- .../lib/entities/disturbance-report.entity.ts | 3 + 7 files changed, 200 insertions(+), 21 deletions(-) diff --git a/apps/entity-service/src/entities/dto/disturbance-report.dto.ts b/apps/entity-service/src/entities/dto/disturbance-report.dto.ts index 93298950..2c4d86d7 100644 --- a/apps/entity-service/src/entities/dto/disturbance-report.dto.ts +++ b/apps/entity-service/src/entities/dto/disturbance-report.dto.ts @@ -5,6 +5,7 @@ import { populateDto } from "@terramatch-microservices/common/dto/json-api-attri import { ApiProperty } from "@nestjs/swagger"; import { HybridSupportProps } from "@terramatch-microservices/common/dto/hybrid-support.dto"; import { DisturbanceReportEntryDto } from "./disturbance-report-entry.dto"; +import { MediaDto } from "./media.dto"; @JsonApiDto({ type: "disturbanceReports" }) export class DisturbanceReportLightDto extends EntityDto { @@ -52,6 +53,8 @@ export class DisturbanceReportLightDto extends EntityDto { entries: DisturbanceReportEntryDto[] | null; } +export type DisturbanceReportMedia = Pick; + export class DisturbanceReportFullDto extends DisturbanceReportLightDto { constructor( disturbanceReport: DisturbanceReport, @@ -101,4 +104,7 @@ export class DisturbanceReportFullDto extends DisturbanceReportLightDto { @ApiProperty({ nullable: true, type: String }) actionDescription: string | null; + + @ApiProperty({ type: () => MediaDto, isArray: true }) + media: MediaDto[]; } diff --git a/apps/entity-service/src/entities/processors/disturbance-report.processor.ts b/apps/entity-service/src/entities/processors/disturbance-report.processor.ts index 907e5e85..ecf6cd2f 100644 --- a/apps/entity-service/src/entities/processors/disturbance-report.processor.ts +++ b/apps/entity-service/src/entities/processors/disturbance-report.processor.ts @@ -2,16 +2,24 @@ import { DisturbanceReport, DisturbanceReportEntry, Project, - ProjectUser + ProjectUser, + SitePolygon, + Disturbance, + Media } from "@terramatch-microservices/database/entities"; import { ReportProcessor } from "./entity-processor"; import { EntityQueryDto } from "../dto/entity-query.dto"; import { BadRequestException } from "@nestjs/common"; import { Op, Includeable } from "sequelize"; import { ReportUpdateAttributes } from "../dto/entity-update.dto"; -import { DisturbanceReportFullDto, DisturbanceReportLightDto } from "../dto/disturbance-report.dto"; +import { + DisturbanceReportFullDto, + DisturbanceReportLightDto, + DisturbanceReportMedia +} from "../dto/disturbance-report.dto"; import { DisturbanceReportEntryDto } from "../dto/disturbance-report-entry.dto"; import { FrameworkKey } from "@terramatch-microservices/database/constants/framework"; +import { TMLogger } from "@terramatch-microservices/common/util/tm-logger"; const SIMPLE_FILTERS: (keyof EntityQueryDto)[] = [ "status", @@ -36,6 +44,148 @@ export class DisturbanceReportProcessor extends ReportProcessor< > { readonly LIGHT_DTO = DisturbanceReportLightDto; readonly FULL_DTO = DisturbanceReportFullDto; + private logger = new TMLogger(DisturbanceReportProcessor.name); + /** + * Specific method for DisturbanceReport custom logic. This is called automatically when the report is approved. + * + * This method implements the workflow described in the task: + * - When a polygon is identified as affected in a disturbance report, the site_polygon.disturbance_id field + * is populated with the v2_disturbances.id + * - All new versions of the polygon will carry the disturbance_id with it, as occurs with other attributes + * - Each polygon should only be identified in 1 disturbance + * - Extracts disturbance details (intensity, extent, type, subtype, peopleAffected, monetaryDamage, propertyAffected) + * from disturbance report entries and populates the disturbance record + */ + protected async processReportSpecificLogic(model: DisturbanceReport): Promise { + const entries = await DisturbanceReportEntry.report(model.id).findAll(); + + const affectedPolygonUuids = new Set(); + const disturbanceData: Partial = {}; + + for (const entry of entries) { + // Look for entries that contain affected polygon UUIDs + // Based on the task requirements, this should identify which polygons have been impacted + const polygonFieldNames = ["polygon-affected"]; + + if (polygonFieldNames.includes(entry.name) && entry.value) { + this.logger.debug(`Processing polygon field: ${entry.name} with value: ${entry.value}`); + try { + // Parse as JSON array of arrays containing polygon objects + const parsedValue = JSON.parse(entry.value); + if (Array.isArray(parsedValue)) { + parsedValue.forEach((polygonGroup, groupIndex) => { + if (Array.isArray(polygonGroup)) { + // Handle array of arrays format + polygonGroup.forEach(polygonObj => { + if (polygonObj && typeof polygonObj === "object" && polygonObj.polyUuid) { + this.logger.debug( + `Adding polygon UUID: ${polygonObj.polyUuid} (${polygonObj.polyName}) from group ${groupIndex}` + ); + affectedPolygonUuids.add(polygonObj.polyUuid); + } + }); + } else if (polygonGroup && typeof polygonGroup === "object" && polygonGroup.polyUuid) { + // Handle direct object format (fallback) + this.logger.debug(`Adding polygon UUID: ${polygonGroup.polyUuid} (${polygonGroup.polyName})`); + affectedPolygonUuids.add(polygonGroup.polyUuid); + } else if (typeof polygonGroup === "string" && polygonGroup.trim()) { + // Fallback for simple string UUIDs + this.logger.debug(`Adding polygon UUID (string): ${polygonGroup.trim()}`); + affectedPolygonUuids.add(polygonGroup.trim()); + } + }); + } + } catch (error) { + this.logger.warn(`Failed to parse polygon JSON: ${error.message}, trying comma-separated values`); + // If JSON parsing fails, try comma-separated values + const uuids = entry.value + .split(",") + .map(uuid => uuid.trim()) + .filter(uuid => uuid); + uuids.forEach(uuid => affectedPolygonUuids.add(uuid)); + } + } + + switch (entry.name) { + case "intensity": + if (entry.value) disturbanceData.intensity = entry.value; + break; + case "extent": + if (entry.value) disturbanceData.extent = entry.value; + break; + case "disturbance-type": + if (entry.value) disturbanceData.type = entry.value; + break; + case "disturbance-subtype": + if (entry.value) disturbanceData.subtype = entry.value ? JSON.parse(entry.value) : []; + break; + case "people-affected": + if (entry.value) disturbanceData.peopleAffected = entry.value ? Number(entry.value) : null; + break; + case "monetary-damage": + if (entry.value) disturbanceData.monetaryDamage = entry.value ? Number(entry.value) : null; + break; + case "property-affected": + if (entry.value) disturbanceData.propertyAffected = entry.value ? JSON.parse(entry.value) : []; + break; + case "date-of-disturbance": + if (entry.value) disturbanceData.disturbanceDate = entry.value ? new Date(entry.value) : null; + break; + } + } + + if (affectedPolygonUuids.size === 0) { + return; + } + + const disturbanceCreateData = { + disturbanceableType: DisturbanceReport.LARAVEL_TYPE, + disturbanceableId: model.id, + disturbanceDate: disturbanceData.disturbanceDate, + type: disturbanceData.type, + subtype: disturbanceData.subtype, + intensity: disturbanceData.intensity, + extent: disturbanceData.extent, + peopleAffected: disturbanceData.peopleAffected, + monetaryDamage: disturbanceData.monetaryDamage, + propertyAffected: disturbanceData.propertyAffected, + description: model.description, + actionDescription: model.actionDescription, + hidden: 0 + } as Disturbance; + + const disturbance = await Disturbance.create(disturbanceCreateData); + + // Find all affected site polygons and validate they're not already affected by another disturbance + const affectedPolygons = await SitePolygon.findAll({ + where: { + uuid: { [Op.in]: Array.from(affectedPolygonUuids) }, + isActive: true + } + }); + + // Check for polygons that are already affected by another disturbance + const alreadyAffectedPolygons = affectedPolygons.filter(polygon => polygon.disturbanceId != null); + if (alreadyAffectedPolygons.length > 0) { + this.logger.warn( + `The following polygons are already affected by another disturbance: ${alreadyAffectedPolygons + .map(p => p.uuid) + .join(", ")}` + ); + } + + // Update all affected polygons with the disturbance_id, where they are not already affected by another disturbance + await SitePolygon.update( + { disturbanceId: disturbance.id }, + { + where: { + uuid: { [Op.in]: Array.from(affectedPolygonUuids) }, + isActive: true, + disturbanceId: null + } + } + ); + } async findOne(uuid: string) { return await DisturbanceReport.findOne({ @@ -116,10 +266,17 @@ export class DisturbanceReportProcessor extends ReportProcessor< const entries = await this.getDisturbanceReportEntries(disturbanceReport); const intensity = entries.find(entry => entry.name === "intensity")?.value ?? null; const dateOfDisturbance = entries.find(entry => entry.name === "date-of-disturbance")?.value; + const mediaCollection = await Media.for(disturbanceReport).findAll(); const dto = new DisturbanceReportFullDto(disturbanceReport, { entries, intensity, - dateOfDisturbance: dateOfDisturbance != null ? new Date(dateOfDisturbance) : null + dateOfDisturbance: dateOfDisturbance != null ? new Date(dateOfDisturbance) : null, + ...(this.entitiesService.mapMediaCollection( + mediaCollection, + DisturbanceReport.MEDIA, + "disturbanceReports", + disturbanceReport.uuid + ) as DisturbanceReportMedia) }); return { id: disturbanceReport.uuid, dto }; diff --git a/apps/entity-service/src/entities/processors/entity-processor.ts b/apps/entity-service/src/entities/processors/entity-processor.ts index 1f62f59b..43eec44a 100644 --- a/apps/entity-service/src/entities/processors/entity-processor.ts +++ b/apps/entity-service/src/entities/processors/entity-processor.ts @@ -13,7 +13,7 @@ import { NEEDS_MORE_INFORMATION, RESTORATION_IN_PROGRESS } from "@terramatch-microservices/database/constants/status"; -import { FinancialReport, ProjectReport } from "@terramatch-microservices/database/entities"; +import { DisturbanceReport, FinancialReport, ProjectReport } from "@terramatch-microservices/database/entities"; export type Aggregate> = { func: string; @@ -205,7 +205,11 @@ export abstract class ReportProcessor< } if (model instanceof FinancialReport && update.status === "approved") { - await this.processFinancialReportSpecificLogic(model); + await this.processReportSpecificLogic(model); + } + + if (model instanceof DisturbanceReport && update.status === "approved") { + await this.processReportSpecificLogic(model); } await super.update(model, update); @@ -224,7 +228,7 @@ export abstract class ReportProcessor< * @param model The report model being processed */ // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected async processFinancialReportSpecificLogic(model: FinancialReport): Promise { + protected async processReportSpecificLogic(model: ReportModel): Promise { // This method will be overridden in FinancialReportProcessor } } diff --git a/apps/entity-service/src/entities/processors/financial-report.processor.spec.ts b/apps/entity-service/src/entities/processors/financial-report.processor.spec.ts index 7687cc94..8e6e1dc1 100644 --- a/apps/entity-service/src/entities/processors/financial-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/financial-report.processor.spec.ts @@ -246,7 +246,7 @@ describe("FinancialReportProcessor", () => { }); }); - describe("processFinancialReportSpecificLogic", () => { + describe("processReportSpecificLogic", () => { it("should update organisation fields when financial report is approved", async () => { const organisation = await OrganisationFactory.create({ finStartMonth: null, @@ -260,9 +260,9 @@ describe("FinancialReportProcessor", () => { await ( processor as unknown as { - processFinancialReportSpecificLogic: (report: FinancialReport) => Promise; + processReportSpecificLogic: (report: FinancialReport) => Promise; } - ).processFinancialReportSpecificLogic(financialReport); + ).processReportSpecificLogic(financialReport); await organisation.reload(); expect(organisation.finStartMonth).toBe(3); @@ -284,9 +284,9 @@ describe("FinancialReportProcessor", () => { await ( processor as unknown as { - processFinancialReportSpecificLogic: (report: FinancialReport) => Promise; + processReportSpecificLogic: (report: FinancialReport) => Promise; } - ).processFinancialReportSpecificLogic(financialReport); + ).processReportSpecificLogic(financialReport); expect(mockWarn).toHaveBeenCalledWith(`Organisation not found for FinancialReport ${financialReport.uuid}`); }); @@ -322,9 +322,9 @@ describe("FinancialReportProcessor", () => { await ( processor as unknown as { - processFinancialReportSpecificLogic: (report: FinancialReport) => Promise; + processReportSpecificLogic: (report: FinancialReport) => Promise; } - ).processFinancialReportSpecificLogic(financialReport); + ).processReportSpecificLogic(financialReport); expect(mockBulkCreate).toHaveBeenCalledWith([ { @@ -400,9 +400,9 @@ describe("FinancialReportProcessor", () => { await ( processor as unknown as { - processFinancialReportSpecificLogic: (report: FinancialReport) => Promise; + processReportSpecificLogic: (report: FinancialReport) => Promise; } - ).processFinancialReportSpecificLogic(financialReport); + ).processReportSpecificLogic(financialReport); expect(mockUpdate).toHaveBeenCalledTimes(2); expect(mockUpdate).toHaveBeenCalledWith( @@ -468,9 +468,9 @@ describe("FinancialReportProcessor", () => { await ( processor as unknown as { - processFinancialReportSpecificLogic: (report: FinancialReport) => Promise; + processReportSpecificLogic: (report: FinancialReport) => Promise; } - ).processFinancialReportSpecificLogic(financialReport); + ).processReportSpecificLogic(financialReport); expect(mockBulkCreate).toHaveBeenCalledWith([ { @@ -508,9 +508,9 @@ describe("FinancialReportProcessor", () => { await ( processor as unknown as { - processFinancialReportSpecificLogic: (report: FinancialReport) => Promise; + processReportSpecificLogic: (report: FinancialReport) => Promise; } - ).processFinancialReportSpecificLogic(financialReport); + ).processReportSpecificLogic(financialReport); expect(mockBulkCreate).not.toHaveBeenCalled(); expect(mockUpdate).not.toHaveBeenCalled(); diff --git a/apps/entity-service/src/entities/processors/financial-report.processor.ts b/apps/entity-service/src/entities/processors/financial-report.processor.ts index 452d3b56..5ef902cf 100644 --- a/apps/entity-service/src/entities/processors/financial-report.processor.ts +++ b/apps/entity-service/src/entities/processors/financial-report.processor.ts @@ -34,7 +34,7 @@ export class FinancialReportProcessor extends ReportProcessor< /** * Specific method for FinancialReport custom logic. This is called automatically when nothingToReport is updated */ - protected async processFinancialReportSpecificLogic(model: FinancialReport): Promise { + protected async processReportSpecificLogic(model: FinancialReport): Promise { const organisation = await Organisation.findByPk(model.organisationId); if (organisation == null) { this.logger.warn(`Organisation not found for FinancialReport ${model.uuid}`); diff --git a/libs/database/src/lib/entities/disturbance-report-entry.entity.ts b/libs/database/src/lib/entities/disturbance-report-entry.entity.ts index b4d711e1..fde73062 100644 --- a/libs/database/src/lib/entities/disturbance-report-entry.entity.ts +++ b/libs/database/src/lib/entities/disturbance-report-entry.entity.ts @@ -7,19 +7,28 @@ import { ForeignKey, BelongsTo, Model, - Index + Index, + Scopes } from "sequelize-typescript"; import { BIGINT, STRING, TEXT, UUID, UUIDV4 } from "sequelize"; import { DisturbanceReport } from "./disturbance-report.entity"; +import { chainScope } from "../util/chain-scope"; @Table({ tableName: "disturbance_report_entries", underscored: true, paranoid: true }) +@Scopes(() => ({ + report: (id: number) => ({ where: { disturbanceReportId: id } }) +})) export class DisturbanceReportEntry extends Model { static readonly LARAVEL_TYPE = "App\\Models\\V2\\DisturbanceReportEntry"; + static report(id: number) { + return chainScope(this, "report", id) as typeof DisturbanceReportEntry; + } + @PrimaryKey @AutoIncrement @Column(BIGINT.UNSIGNED) diff --git a/libs/database/src/lib/entities/disturbance-report.entity.ts b/libs/database/src/lib/entities/disturbance-report.entity.ts index 5fbd8ec5..d53ab421 100644 --- a/libs/database/src/lib/entities/disturbance-report.entity.ts +++ b/libs/database/src/lib/entities/disturbance-report.entity.ts @@ -30,6 +30,9 @@ import { Project } from "./project.entity"; }) export class DisturbanceReport extends Model { static readonly LARAVEL_TYPE = "App\\Models\\V2\\DisturbanceReport"; + static readonly MEDIA = { + media: { dbCollection: "media", multiple: true, validation: "general-documents" } + } as const; static project(id: number) { return chainScope(this, "project", id) as typeof DisturbanceReport; From 1922a7a2ba77ea6506f6da8a4fe72213dd6864b2 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Wed, 8 Oct 2025 15:55:41 -0400 Subject: [PATCH 02/11] [TM-2517] remove comments --- .../src/entities/processors/disturbance-report.processor.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/entity-service/src/entities/processors/disturbance-report.processor.ts b/apps/entity-service/src/entities/processors/disturbance-report.processor.ts index ecf6cd2f..11bfbd5c 100644 --- a/apps/entity-service/src/entities/processors/disturbance-report.processor.ts +++ b/apps/entity-service/src/entities/processors/disturbance-report.processor.ts @@ -70,7 +70,6 @@ export class DisturbanceReportProcessor extends ReportProcessor< if (polygonFieldNames.includes(entry.name) && entry.value) { this.logger.debug(`Processing polygon field: ${entry.name} with value: ${entry.value}`); try { - // Parse as JSON array of arrays containing polygon objects const parsedValue = JSON.parse(entry.value); if (Array.isArray(parsedValue)) { parsedValue.forEach((polygonGroup, groupIndex) => { @@ -88,10 +87,6 @@ export class DisturbanceReportProcessor extends ReportProcessor< // Handle direct object format (fallback) this.logger.debug(`Adding polygon UUID: ${polygonGroup.polyUuid} (${polygonGroup.polyName})`); affectedPolygonUuids.add(polygonGroup.polyUuid); - } else if (typeof polygonGroup === "string" && polygonGroup.trim()) { - // Fallback for simple string UUIDs - this.logger.debug(`Adding polygon UUID (string): ${polygonGroup.trim()}`); - affectedPolygonUuids.add(polygonGroup.trim()); } }); } From 4d788f0f29635b4a3ec31fbb5a82ec3116a04735 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Thu, 9 Oct 2025 10:24:54 -0400 Subject: [PATCH 03/11] [TM-2517] update logic for approval flow and enable send email --- .../entities/dto/disturbance-report.dto.ts | 3 + .../disturbance-report.processor.ts | 178 +++++++++++------- .../src/site-polygons/dto/site-polygon.dto.ts | 8 + .../site-polygon-query.builder.ts | 8 +- 4 files changed, 128 insertions(+), 69 deletions(-) diff --git a/apps/entity-service/src/entities/dto/disturbance-report.dto.ts b/apps/entity-service/src/entities/dto/disturbance-report.dto.ts index 2c4d86d7..f8e8e121 100644 --- a/apps/entity-service/src/entities/dto/disturbance-report.dto.ts +++ b/apps/entity-service/src/entities/dto/disturbance-report.dto.ts @@ -51,6 +51,9 @@ export class DisturbanceReportLightDto extends EntityDto { @ApiProperty({ nullable: true, type: DisturbanceReportEntryDto, isArray: true }) entries: DisturbanceReportEntryDto[] | null; + + @ApiProperty({ type: Number }) + disturbanceableId: number; } export type DisturbanceReportMedia = Pick; diff --git a/apps/entity-service/src/entities/processors/disturbance-report.processor.ts b/apps/entity-service/src/entities/processors/disturbance-report.processor.ts index 11bfbd5c..8a8c30cf 100644 --- a/apps/entity-service/src/entities/processors/disturbance-report.processor.ts +++ b/apps/entity-service/src/entities/processors/disturbance-report.processor.ts @@ -62,78 +62,14 @@ export class DisturbanceReportProcessor extends ReportProcessor< const affectedPolygonUuids = new Set(); const disturbanceData: Partial = {}; - for (const entry of entries) { - // Look for entries that contain affected polygon UUIDs - // Based on the task requirements, this should identify which polygons have been impacted - const polygonFieldNames = ["polygon-affected"]; - - if (polygonFieldNames.includes(entry.name) && entry.value) { - this.logger.debug(`Processing polygon field: ${entry.name} with value: ${entry.value}`); - try { - const parsedValue = JSON.parse(entry.value); - if (Array.isArray(parsedValue)) { - parsedValue.forEach((polygonGroup, groupIndex) => { - if (Array.isArray(polygonGroup)) { - // Handle array of arrays format - polygonGroup.forEach(polygonObj => { - if (polygonObj && typeof polygonObj === "object" && polygonObj.polyUuid) { - this.logger.debug( - `Adding polygon UUID: ${polygonObj.polyUuid} (${polygonObj.polyName}) from group ${groupIndex}` - ); - affectedPolygonUuids.add(polygonObj.polyUuid); - } - }); - } else if (polygonGroup && typeof polygonGroup === "object" && polygonGroup.polyUuid) { - // Handle direct object format (fallback) - this.logger.debug(`Adding polygon UUID: ${polygonGroup.polyUuid} (${polygonGroup.polyName})`); - affectedPolygonUuids.add(polygonGroup.polyUuid); - } - }); - } - } catch (error) { - this.logger.warn(`Failed to parse polygon JSON: ${error.message}, trying comma-separated values`); - // If JSON parsing fails, try comma-separated values - const uuids = entry.value - .split(",") - .map(uuid => uuid.trim()) - .filter(uuid => uuid); - uuids.forEach(uuid => affectedPolygonUuids.add(uuid)); - } - } - - switch (entry.name) { - case "intensity": - if (entry.value) disturbanceData.intensity = entry.value; - break; - case "extent": - if (entry.value) disturbanceData.extent = entry.value; - break; - case "disturbance-type": - if (entry.value) disturbanceData.type = entry.value; - break; - case "disturbance-subtype": - if (entry.value) disturbanceData.subtype = entry.value ? JSON.parse(entry.value) : []; - break; - case "people-affected": - if (entry.value) disturbanceData.peopleAffected = entry.value ? Number(entry.value) : null; - break; - case "monetary-damage": - if (entry.value) disturbanceData.monetaryDamage = entry.value ? Number(entry.value) : null; - break; - case "property-affected": - if (entry.value) disturbanceData.propertyAffected = entry.value ? JSON.parse(entry.value) : []; - break; - case "date-of-disturbance": - if (entry.value) disturbanceData.disturbanceDate = entry.value ? new Date(entry.value) : null; - break; - } - } + this.processPolygonEntry(entries, affectedPolygonUuids, disturbanceData); if (affectedPolygonUuids.size === 0) { return; } - const disturbanceCreateData = { + // Upsert disturbance for this report (align with PHP logic) + const disturbanceUpsertData = { disturbanceableType: DisturbanceReport.LARAVEL_TYPE, disturbanceableId: model.id, disturbanceDate: disturbanceData.disturbanceDate, @@ -149,7 +85,14 @@ export class DisturbanceReportProcessor extends ReportProcessor< hidden: 0 } as Disturbance; - const disturbance = await Disturbance.create(disturbanceCreateData); + let disturbance = await Disturbance.findOne({ + where: { disturbanceableType: DisturbanceReport.LARAVEL_TYPE, disturbanceableId: model.id } + }); + if (disturbance != null) { + await disturbance.update(disturbanceUpsertData); + } else { + disturbance = await Disturbance.create(disturbanceUpsertData); + } // Find all affected site polygons and validate they're not already affected by another disturbance const affectedPolygons = await SitePolygon.findAll({ @@ -182,6 +125,103 @@ export class DisturbanceReportProcessor extends ReportProcessor< ); } + private processPolygonEntry( + entries: DisturbanceReportEntry[], + affectedPolygonUuids: Set, + disturbanceData: Partial + ): void { + for (const entry of entries) { + // Look for entries that contain affected polygon UUIDs + // Based on the task requirements, this should identify which polygons have been impacted + if (entry.name == "polygon-affected" && entry.value !== null) { + this.logger.log(`Processing polygon field: ${entry.name} with value: ${entry.value}`); + try { + const parsedValue = JSON.parse(entry.value); + if (Array.isArray(parsedValue)) { + parsedValue.forEach((polygonGroup, groupIndex) => { + if (Array.isArray(polygonGroup)) { + // Handle array of arrays format + polygonGroup.forEach(polygonObj => { + if (polygonObj !== null && typeof polygonObj === "object" && polygonObj.polyUuid !== null) { + this.logger.log( + `Adding polygon UUID: ${polygonObj.polyUuid} (${polygonObj.polyName}) from group ${groupIndex}` + ); + affectedPolygonUuids.add(polygonObj.polyUuid); + } + }); + } else if (polygonGroup !== null && typeof polygonGroup === "object" && polygonGroup.polyUuid !== null) { + // Handle direct object format (fallback) + this.logger.log(`Adding polygon UUID: ${polygonGroup.polyUuid} (${polygonGroup.polyName})`); + affectedPolygonUuids.add(polygonGroup.polyUuid); + } + }); + } + } catch (error) { + this.logger.warn(`Failed to parse polygon JSON: ${error.message}, trying comma-separated values`); + // If JSON parsing fails, try comma-separated values + const uuids = entry.value + .split(",") + .map(uuid => uuid.trim()) + .filter(uuid => uuid !== null); + uuids.forEach(uuid => affectedPolygonUuids.add(uuid)); + } + } + + this.processDisturbanceDataEntry(entry, disturbanceData); + } + } + + private processDisturbanceDataEntry(entry: DisturbanceReportEntry, disturbanceData: Partial): void { + if (entry.value === null) return; + + switch (entry.name) { + case "intensity": + disturbanceData.intensity = entry.value; + break; + case "extent": + disturbanceData.extent = entry.value; + break; + case "disturbance-type": + disturbanceData.type = entry.value; + break; + case "disturbance-subtype": + try { + disturbanceData.subtype = JSON.parse(entry.value); + } catch { + this.logger.warn(`Failed to parse subtype JSON: ${entry.value}`); + } + break; + case "people-affected": { + const peopleAffected = Number(entry.value); + if (!isNaN(peopleAffected)) { + disturbanceData.peopleAffected = peopleAffected; + } + break; + } + case "monetary-damage": { + const monetaryDamage = Number(entry.value); + if (!isNaN(monetaryDamage)) { + disturbanceData.monetaryDamage = monetaryDamage; + } + break; + } + case "property-affected": + try { + disturbanceData.propertyAffected = JSON.parse(entry.value); + } catch { + this.logger.warn(`Failed to parse propertyAffected JSON: ${entry.value}`); + } + break; + case "date-of-disturbance": { + const date = new Date(entry.value); + if (!isNaN(date.getTime())) { + disturbanceData.disturbanceDate = date; + } + break; + } + } + } + async findOne(uuid: string) { return await DisturbanceReport.findOne({ where: { uuid }, @@ -263,6 +303,7 @@ export class DisturbanceReportProcessor extends ReportProcessor< const dateOfDisturbance = entries.find(entry => entry.name === "date-of-disturbance")?.value; const mediaCollection = await Media.for(disturbanceReport).findAll(); const dto = new DisturbanceReportFullDto(disturbanceReport, { + disturbanceableId: disturbanceReport.id, entries, intensity, dateOfDisturbance: dateOfDisturbance != null ? new Date(dateOfDisturbance) : null, @@ -298,6 +339,7 @@ export class DisturbanceReportProcessor extends ReportProcessor< return { id: disturbanceReport.uuid, dto: new DisturbanceReportLightDto(disturbanceReport, { + disturbanceableId: disturbanceReport.id, entries, intensity, dateOfDisturbance: dateOfDisturbance != null ? new Date(dateOfDisturbance) : null diff --git a/apps/research-service/src/site-polygons/dto/site-polygon.dto.ts b/apps/research-service/src/site-polygons/dto/site-polygon.dto.ts index ab4a537a..8267c64b 100644 --- a/apps/research-service/src/site-polygons/dto/site-polygon.dto.ts +++ b/apps/research-service/src/site-polygons/dto/site-polygon.dto.ts @@ -57,6 +57,7 @@ export class SitePolygonLightDto extends HybridSupportDto { projectShortName: sitePolygon.site?.project?.shortName, indicators: indicators ?? [], siteName: sitePolygon.site?.name, + disturbanceableId: sitePolygon?.disturbance?.disturbanceableId ?? null, lightResource: true }); } @@ -169,6 +170,12 @@ export class SitePolygonLightDto extends HybridSupportDto { description: "UUID of the site polygon" }) uuid: string; + + @ApiProperty({ + type: Number, + nullable: true + }) + disturbanceableId: number | null; } @JsonApiDto({ type: "sitePolygons" }) @@ -191,6 +198,7 @@ export class SitePolygonFullDto extends SitePolygonLightDto { geometry: sitePolygon.polygon?.polygon, establishmentTreeSpecies: establishmentTreeSpecies ?? [], reportingPeriods: reportingPeriods ?? [], + disturbanceableId: sitePolygon.disturbance?.disturbanceableId ?? null, lightResource: false }); } diff --git a/apps/research-service/src/site-polygons/site-polygon-query.builder.ts b/apps/research-service/src/site-polygons/site-polygon-query.builder.ts index 2a0eb376..3403cedb 100644 --- a/apps/research-service/src/site-polygons/site-polygon-query.builder.ts +++ b/apps/research-service/src/site-polygons/site-polygon-query.builder.ts @@ -10,7 +10,8 @@ import { PolygonGeometry, Project, Site, - SitePolygon + SitePolygon, + Disturbance } from "@terramatch-microservices/database/entities"; import { IndicatorSlug, PolygonStatus } from "@terramatch-microservices/database/constants"; import { uniq } from "lodash"; @@ -63,6 +64,11 @@ export class SitePolygonQueryBuilder extends PaginatedQueryBuilder model: PolygonGeometry, attributes: ["polygon"] }, + { + model: Disturbance, + attributes: ["disturbanceableId"], + required: false + }, this.siteJoin ]; From b9e29eb843a54e5ff6b1d55ef8f2303fc143ca01 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Thu, 9 Oct 2025 11:43:11 -0400 Subject: [PATCH 04/11] [TM-2517] fix lint --- .../disturbance-report.processor.ts | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/apps/entity-service/src/entities/processors/disturbance-report.processor.ts b/apps/entity-service/src/entities/processors/disturbance-report.processor.ts index 8a8c30cf..3d6190ee 100644 --- a/apps/entity-service/src/entities/processors/disturbance-report.processor.ts +++ b/apps/entity-service/src/entities/processors/disturbance-report.processor.ts @@ -133,7 +133,7 @@ export class DisturbanceReportProcessor extends ReportProcessor< for (const entry of entries) { // Look for entries that contain affected polygon UUIDs // Based on the task requirements, this should identify which polygons have been impacted - if (entry.name == "polygon-affected" && entry.value !== null) { + if (entry.name === "polygon-affected" && entry.value != null) { this.logger.log(`Processing polygon field: ${entry.name} with value: ${entry.value}`); try { const parsedValue = JSON.parse(entry.value); @@ -142,14 +142,14 @@ export class DisturbanceReportProcessor extends ReportProcessor< if (Array.isArray(polygonGroup)) { // Handle array of arrays format polygonGroup.forEach(polygonObj => { - if (polygonObj !== null && typeof polygonObj === "object" && polygonObj.polyUuid !== null) { + if (polygonObj != null && typeof polygonObj === "object" && polygonObj.polyUuid != null) { this.logger.log( `Adding polygon UUID: ${polygonObj.polyUuid} (${polygonObj.polyName}) from group ${groupIndex}` ); affectedPolygonUuids.add(polygonObj.polyUuid); } }); - } else if (polygonGroup !== null && typeof polygonGroup === "object" && polygonGroup.polyUuid !== null) { + } else if (polygonGroup != null && typeof polygonGroup === "object" && polygonGroup.polyUuid != null) { // Handle direct object format (fallback) this.logger.log(`Adding polygon UUID: ${polygonGroup.polyUuid} (${polygonGroup.polyName})`); affectedPolygonUuids.add(polygonGroup.polyUuid); @@ -162,7 +162,7 @@ export class DisturbanceReportProcessor extends ReportProcessor< const uuids = entry.value .split(",") .map(uuid => uuid.trim()) - .filter(uuid => uuid !== null); + .filter(uuid => uuid != null && uuid !== ""); uuids.forEach(uuid => affectedPolygonUuids.add(uuid)); } } @@ -172,7 +172,7 @@ export class DisturbanceReportProcessor extends ReportProcessor< } private processDisturbanceDataEntry(entry: DisturbanceReportEntry, disturbanceData: Partial): void { - if (entry.value === null) return; + if (entry.value == null) return; switch (entry.name) { case "intensity": @@ -186,35 +186,41 @@ export class DisturbanceReportProcessor extends ReportProcessor< break; case "disturbance-subtype": try { - disturbanceData.subtype = JSON.parse(entry.value); + const parsed = JSON.parse(entry.value); + if (parsed != null) { + disturbanceData.subtype = parsed; + } } catch { this.logger.warn(`Failed to parse subtype JSON: ${entry.value}`); } break; case "people-affected": { const peopleAffected = Number(entry.value); - if (!isNaN(peopleAffected)) { + if (!isNaN(peopleAffected) && peopleAffected > 0) { disturbanceData.peopleAffected = peopleAffected; } break; } case "monetary-damage": { const monetaryDamage = Number(entry.value); - if (!isNaN(monetaryDamage)) { + if (!isNaN(monetaryDamage) && monetaryDamage >= 0) { disturbanceData.monetaryDamage = monetaryDamage; } break; } case "property-affected": try { - disturbanceData.propertyAffected = JSON.parse(entry.value); + const parsed = JSON.parse(entry.value); + if (parsed != null) { + disturbanceData.propertyAffected = parsed; + } } catch { this.logger.warn(`Failed to parse propertyAffected JSON: ${entry.value}`); } break; case "date-of-disturbance": { const date = new Date(entry.value); - if (!isNaN(date.getTime())) { + if (!isNaN(date.getTime()) && date.getTime() > 0) { disturbanceData.disturbanceDate = date; } break; From 0cdac4016154c84f9c50babbc3bfec93ded7c25b Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 13 Oct 2025 10:37:53 -0400 Subject: [PATCH 05/11] [TM-2517] improve code --- .../disturbance-report.processor.ts | 23 ++++--------------- .../src/lib/entities/site-polygon.entity.ts | 7 +++++- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/apps/entity-service/src/entities/processors/disturbance-report.processor.ts b/apps/entity-service/src/entities/processors/disturbance-report.processor.ts index 3d6190ee..7f892ca7 100644 --- a/apps/entity-service/src/entities/processors/disturbance-report.processor.ts +++ b/apps/entity-service/src/entities/processors/disturbance-report.processor.ts @@ -95,12 +95,7 @@ export class DisturbanceReportProcessor extends ReportProcessor< } // Find all affected site polygons and validate they're not already affected by another disturbance - const affectedPolygons = await SitePolygon.findAll({ - where: { - uuid: { [Op.in]: Array.from(affectedPolygonUuids) }, - isActive: true - } - }); + const affectedPolygons = await SitePolygon.active().forUuids(Array.from(affectedPolygonUuids)).findAll(); // Check for polygons that are already affected by another disturbance const alreadyAffectedPolygons = affectedPolygons.filter(polygon => polygon.disturbanceId != null); @@ -112,13 +107,11 @@ export class DisturbanceReportProcessor extends ReportProcessor< ); } - // Update all affected polygons with the disturbance_id, where they are not already affected by another disturbance await SitePolygon.update( { disturbanceId: disturbance.id }, { where: { uuid: { [Op.in]: Array.from(affectedPolygonUuids) }, - isActive: true, disturbanceId: null } } @@ -134,31 +127,23 @@ export class DisturbanceReportProcessor extends ReportProcessor< // Look for entries that contain affected polygon UUIDs // Based on the task requirements, this should identify which polygons have been impacted if (entry.name === "polygon-affected" && entry.value != null) { - this.logger.log(`Processing polygon field: ${entry.name} with value: ${entry.value}`); try { const parsedValue = JSON.parse(entry.value); if (Array.isArray(parsedValue)) { - parsedValue.forEach((polygonGroup, groupIndex) => { + parsedValue.forEach(polygonGroup => { if (Array.isArray(polygonGroup)) { - // Handle array of arrays format polygonGroup.forEach(polygonObj => { if (polygonObj != null && typeof polygonObj === "object" && polygonObj.polyUuid != null) { - this.logger.log( - `Adding polygon UUID: ${polygonObj.polyUuid} (${polygonObj.polyName}) from group ${groupIndex}` - ); affectedPolygonUuids.add(polygonObj.polyUuid); } }); } else if (polygonGroup != null && typeof polygonGroup === "object" && polygonGroup.polyUuid != null) { - // Handle direct object format (fallback) - this.logger.log(`Adding polygon UUID: ${polygonGroup.polyUuid} (${polygonGroup.polyName})`); affectedPolygonUuids.add(polygonGroup.polyUuid); } }); } } catch (error) { this.logger.warn(`Failed to parse polygon JSON: ${error.message}, trying comma-separated values`); - // If JSON parsing fails, try comma-separated values const uuids = entry.value .split(",") .map(uuid => uuid.trim()) @@ -196,14 +181,14 @@ export class DisturbanceReportProcessor extends ReportProcessor< break; case "people-affected": { const peopleAffected = Number(entry.value); - if (!isNaN(peopleAffected) && peopleAffected > 0) { + if (!isNaN(peopleAffected)) { disturbanceData.peopleAffected = peopleAffected; } break; } case "monetary-damage": { const monetaryDamage = Number(entry.value); - if (!isNaN(monetaryDamage) && monetaryDamage >= 0) { + if (!isNaN(monetaryDamage)) { disturbanceData.monetaryDamage = monetaryDamage; } break; diff --git a/libs/database/src/lib/entities/site-polygon.entity.ts b/libs/database/src/lib/entities/site-polygon.entity.ts index c41cb5bd..b3442b95 100644 --- a/libs/database/src/lib/entities/site-polygon.entity.ts +++ b/libs/database/src/lib/entities/site-polygon.entity.ts @@ -41,7 +41,8 @@ export type Indicator = @Scopes(() => ({ active: { where: { isActive: true } }, approved: { where: { status: "approved" } }, - sites: (uuids: string[] | Literal) => ({ where: { siteUuid: { [Op.in]: uuids } } }) + sites: (uuids: string[] | Literal) => ({ where: { siteUuid: { [Op.in]: uuids } } }), + forUuids: (uuids: string[] | Literal) => ({ where: { uuid: { [Op.in]: uuids } } }) })) @Table({ tableName: "site_polygon", @@ -66,6 +67,10 @@ export class SitePolygon extends Model { return chainScope(this, "sites", uuids) as typeof SitePolygon; } + static forUuids(uuids: string[] | Literal) { + return chainScope(this, "forUuids", uuids) as typeof SitePolygon; + } + @PrimaryKey @AutoIncrement @Column(BIGINT.UNSIGNED) From cd95545f61257323a37c893d7ee97a4c1e71aa1d Mon Sep 17 00:00:00 2001 From: LimberHope Date: Mon, 13 Oct 2025 10:51:25 -0400 Subject: [PATCH 06/11] [TM-2517] update unit test --- .../disturbance-report.processor.spec.ts | 257 +++++++++++++++++- 1 file changed, 255 insertions(+), 2 deletions(-) diff --git a/apps/entity-service/src/entities/processors/disturbance-report.processor.spec.ts b/apps/entity-service/src/entities/processors/disturbance-report.processor.spec.ts index 14e282b5..ecb7954a 100644 --- a/apps/entity-service/src/entities/processors/disturbance-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/disturbance-report.processor.spec.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { DisturbanceReport } from "@terramatch-microservices/database/entities"; +import { DisturbanceReport, Disturbance, SitePolygon } from "@terramatch-microservices/database/entities"; import { Test } from "@nestjs/testing"; import { MediaService } from "@terramatch-microservices/common/media/media.service"; import { createMock, DeepMocked } from "@golevelup/ts-jest"; @@ -11,7 +11,8 @@ import { DisturbanceReportEntryFactory, ProjectFactory, ProjectUserFactory, - UserFactory + UserFactory, + SitePolygonFactory } from "@terramatch-microservices/database/factories"; import { BadRequestException } from "@nestjs/common/exceptions/bad-request.exception"; import { DisturbanceReportProcessor } from "./disturbance-report.processor"; @@ -323,4 +324,256 @@ describe("DisturbanceReportProcessor", () => { expect(entries).toHaveLength(0); }); }); + + describe("processReportSpecificLogic", () => { + it("upserts disturbance and sets disturbanceId on polygons parsed from JSON", async () => { + const project = await ProjectFactory.create(); + const disturbanceReport = await DisturbanceReportFactory.create({ + projectId: project.id, + description: "desc", + actionDescription: "act" + }); + + const poly1 = await SitePolygonFactory.create(); + const poly2 = await SitePolygonFactory.create(); + + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "polygon-affected", + value: JSON.stringify([{ polyUuid: poly1.uuid }, { polyUuid: poly2.uuid }]), + inputType: "text" + }); + + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "intensity", + value: "high", + inputType: "select" + }); + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "extent", + value: "large", + inputType: "text" + }); + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "disturbance-type", + value: "fire", + inputType: "text" + }); + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "disturbance-subtype", + value: JSON.stringify({ code: "wild" }), + inputType: "text" + }); + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "people-affected", + value: "12", + inputType: "number" + }); + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "monetary-damage", + value: "345.6", + inputType: "number" + }); + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "property-affected", + value: JSON.stringify({ houses: 3 }), + inputType: "text" + }); + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "date-of-disturbance", + value: "2024-01-05", + inputType: "date" + }); + + await ( + processor as unknown as { processReportSpecificLogic: (report: DisturbanceReport) => Promise } + ).processReportSpecificLogic(disturbanceReport); + + const disturbance = await Disturbance.findOne({ + where: { disturbanceableType: DisturbanceReport.LARAVEL_TYPE, disturbanceableId: disturbanceReport.id } + }); + expect(disturbance).toBeTruthy(); + expect(disturbance?.intensity).toBe("high"); + expect(disturbance?.extent).toBe("large"); + expect(disturbance?.type).toBe("fire"); + expect(disturbance?.peopleAffected).toBe(12); + expect(disturbance?.monetaryDamage).toBe(345.6); + expect(disturbance?.description).toBe("desc"); + expect(disturbance?.actionDescription).toBe("act"); + expect(disturbance?.disturbanceDate?.toISOString()).toBe(new Date("2024-01-05").toISOString()); + + const updatedPoly1 = await SitePolygon.findOne({ where: { uuid: poly1.uuid } }); + const updatedPoly2 = await SitePolygon.findOne({ where: { uuid: poly2.uuid } }); + expect(updatedPoly1?.disturbanceId).toBe(disturbance?.id); + expect(updatedPoly2?.disturbanceId).toBe(disturbance?.id); + }); + + it("does not overwrite existing polygon disturbanceId and updates only null ones", async () => { + const project = await ProjectFactory.create(); + const disturbanceReport = await DisturbanceReportFactory.create({ projectId: project.id }); + + const preExistingDisturbance = await Disturbance.create({ + disturbanceableType: DisturbanceReport.LARAVEL_TYPE, + disturbanceableId: disturbanceReport.id, + hidden: 0 + } as Disturbance); + + const polyWithDist = await SitePolygonFactory.create(); + await polyWithDist.update({ disturbanceId: preExistingDisturbance.id }); + const polyWithoutDist = await SitePolygonFactory.create(); + + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "polygon-affected", + value: JSON.stringify([{ polyUuid: polyWithDist.uuid }, { polyUuid: polyWithoutDist.uuid }]), + inputType: "text" + }); + + await ( + processor as unknown as { processReportSpecificLogic: (report: DisturbanceReport) => Promise } + ).processReportSpecificLogic(disturbanceReport); + + const refreshedWith = await SitePolygon.findOne({ where: { uuid: polyWithDist.uuid } }); + const refreshedWithout = await SitePolygon.findOne({ where: { uuid: polyWithoutDist.uuid } }); + + expect(refreshedWith?.disturbanceId).toBe(preExistingDisturbance.id); + expect(refreshedWithout?.disturbanceId).toBeTruthy(); + }); + + it("parses CSV fallback for polygon-affected when JSON fails", async () => { + const project = await ProjectFactory.create(); + const disturbanceReport = await DisturbanceReportFactory.create({ projectId: project.id }); + + const p1 = await SitePolygonFactory.create(); + const p2 = await SitePolygonFactory.create(); + + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "polygon-affected", + value: `${p1.uuid}, ${p2.uuid}`, + inputType: "text" + }); + + await ( + processor as unknown as { processReportSpecificLogic: (report: DisturbanceReport) => Promise } + ).processReportSpecificLogic(disturbanceReport); + + const disturbance = await Disturbance.findOne({ + where: { disturbanceableType: DisturbanceReport.LARAVEL_TYPE, disturbanceableId: disturbanceReport.id } + }); + expect(disturbance).toBeTruthy(); + + const up1 = await SitePolygon.findOne({ where: { uuid: p1.uuid } }); + const up2 = await SitePolygon.findOne({ where: { uuid: p2.uuid } }); + expect(up1?.disturbanceId).toBe(disturbance?.id); + expect(up2?.disturbanceId).toBe(disturbance?.id); + }); + + it("returns early and does not create disturbance when no polygons provided", async () => { + const project = await ProjectFactory.create(); + const disturbanceReport = await DisturbanceReportFactory.create({ projectId: project.id }); + + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "intensity", + value: "low", + inputType: "select" + }); + + await ( + processor as unknown as { processReportSpecificLogic: (report: DisturbanceReport) => Promise } + ).processReportSpecificLogic(disturbanceReport); + + const disturbance = await Disturbance.findOne({ + where: { disturbanceableType: DisturbanceReport.LARAVEL_TYPE, disturbanceableId: disturbanceReport.id } + }); + expect(disturbance).toBeNull(); + }); + + it("handles nested arrays in polygon-affected JSON", async () => { + const project = await ProjectFactory.create(); + const disturbanceReport = await DisturbanceReportFactory.create({ projectId: project.id }); + + const p1 = await SitePolygonFactory.create(); + const p2 = await SitePolygonFactory.create(); + + const nested = [[{ polyUuid: p1.uuid }], [{ polyUuid: p2.uuid }]]; + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "polygon-affected", + value: JSON.stringify(nested), + inputType: "text" + }); + + await ( + processor as unknown as { processReportSpecificLogic: (report: DisturbanceReport) => Promise } + ).processReportSpecificLogic(disturbanceReport); + + const disturbance = await Disturbance.findOne({ + where: { disturbanceableType: DisturbanceReport.LARAVEL_TYPE, disturbanceableId: disturbanceReport.id } + }); + expect(disturbance).toBeTruthy(); + + const up1 = await SitePolygon.findOne({ where: { uuid: p1.uuid } }); + const up2 = await SitePolygon.findOne({ where: { uuid: p2.uuid } }); + expect(up1?.disturbanceId).toBe(disturbance?.id); + expect(up2?.disturbanceId).toBe(disturbance?.id); + }); + + it("warns on invalid JSON for subtype and property-affected and ignores invalid date", async () => { + const project = await ProjectFactory.create(); + const disturbanceReport = await DisturbanceReportFactory.create({ projectId: project.id }); + + const p = await SitePolygonFactory.create(); + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "polygon-affected", + value: JSON.stringify([{ polyUuid: p.uuid }]), + inputType: "text" + }); + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "disturbance-subtype", + value: "{invalid}", + inputType: "text" + }); + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "property-affected", + value: "{invalid}", + inputType: "text" + }); + await DisturbanceReportEntryFactory.create({ + disturbanceReportId: disturbanceReport.id, + name: "date-of-disturbance", + value: "not-a-date", + inputType: "date" + }); + + const warnSpy = jest.spyOn((processor as unknown as { logger: { warn: (m: string) => void } }).logger, "warn"); + + await ( + processor as unknown as { processReportSpecificLogic: (report: DisturbanceReport) => Promise } + ).processReportSpecificLogic(disturbanceReport); + + const disturbance = await Disturbance.findOne({ + where: { disturbanceableType: DisturbanceReport.LARAVEL_TYPE, disturbanceableId: disturbanceReport.id } + }); + expect(disturbance).toBeTruthy(); + expect(disturbance?.subtype).toBeNull(); + expect(disturbance?.propertyAffected).toBeNull(); + expect(disturbance?.disturbanceDate).toBeNull(); + + expect(warnSpy).toHaveBeenCalled(); + warnSpy.mockRestore(); + }); + }); }); From 4639b8b2dcff7ee352a06304f838ce3c14704242 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Tue, 14 Oct 2025 11:10:21 -0400 Subject: [PATCH 07/11] [TM-2517] add disturbance report for audit status update --- libs/database/src/lib/entities/audit-status.entity.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/database/src/lib/entities/audit-status.entity.ts b/libs/database/src/lib/entities/audit-status.entity.ts index 339118ab..df461147 100644 --- a/libs/database/src/lib/entities/audit-status.entity.ts +++ b/libs/database/src/lib/entities/audit-status.entity.ts @@ -10,6 +10,7 @@ import { ProjectReport } from "./project-report.entity"; import { SiteReport } from "./site-report.entity"; import { NurseryReport } from "./nursery-report.entity"; import { SitePolygon } from "./site-polygon.entity"; +import { DisturbanceReport } from "./disturbance-report.entity"; const TYPES = ["change-request", "status", "submission", "comment", "change-request-updated", "reminder-sent"] as const; type AuditStatusType = (typeof TYPES)[number]; @@ -42,7 +43,8 @@ export class AuditStatus extends Model { ProjectReport.LARAVEL_TYPE, SiteReport.LARAVEL_TYPE, NurseryReport.LARAVEL_TYPE, - SitePolygon.LARAVEL_TYPE + SitePolygon.LARAVEL_TYPE, + DisturbanceReport.LARAVEL_TYPE ]; static for(auditable: LaravelModel) { From 0c131487be1ca7b4bb2e5c29bf030939841060b4 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Tue, 14 Oct 2025 12:39:13 -0400 Subject: [PATCH 08/11] [TM-2517] resolved comments --- .../entities/dto/disturbance-report.dto.ts | 4 +-- .../disturbance-report.processor.spec.ts | 24 ++++--------- .../disturbance-report.processor.ts | 14 +++++++- .../entities/processors/entity-processor.ts | 17 --------- .../financial-report.processor.spec.ts | 36 ++++--------------- .../processors/financial-report.processor.ts | 12 +++++-- 6 files changed, 37 insertions(+), 70 deletions(-) diff --git a/apps/entity-service/src/entities/dto/disturbance-report.dto.ts b/apps/entity-service/src/entities/dto/disturbance-report.dto.ts index f8e8e121..5040c33f 100644 --- a/apps/entity-service/src/entities/dto/disturbance-report.dto.ts +++ b/apps/entity-service/src/entities/dto/disturbance-report.dto.ts @@ -52,8 +52,8 @@ export class DisturbanceReportLightDto extends EntityDto { @ApiProperty({ nullable: true, type: DisturbanceReportEntryDto, isArray: true }) entries: DisturbanceReportEntryDto[] | null; - @ApiProperty({ type: Number }) - disturbanceableId: number; + @ApiProperty({ nullable: true, type: Number }) + disturbanceableId: number | null; } export type DisturbanceReportMedia = Pick; diff --git a/apps/entity-service/src/entities/processors/disturbance-report.processor.spec.ts b/apps/entity-service/src/entities/processors/disturbance-report.processor.spec.ts index ecb7954a..2e3cd6d5 100644 --- a/apps/entity-service/src/entities/processors/disturbance-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/disturbance-report.processor.spec.ts @@ -393,9 +393,7 @@ describe("DisturbanceReportProcessor", () => { inputType: "date" }); - await ( - processor as unknown as { processReportSpecificLogic: (report: DisturbanceReport) => Promise } - ).processReportSpecificLogic(disturbanceReport); + await processor.update(disturbanceReport, { status: "approved" }); const disturbance = await Disturbance.findOne({ where: { disturbanceableType: DisturbanceReport.LARAVEL_TYPE, disturbanceableId: disturbanceReport.id } @@ -437,9 +435,7 @@ describe("DisturbanceReportProcessor", () => { inputType: "text" }); - await ( - processor as unknown as { processReportSpecificLogic: (report: DisturbanceReport) => Promise } - ).processReportSpecificLogic(disturbanceReport); + await processor.update(disturbanceReport, { status: "approved" }); const refreshedWith = await SitePolygon.findOne({ where: { uuid: polyWithDist.uuid } }); const refreshedWithout = await SitePolygon.findOne({ where: { uuid: polyWithoutDist.uuid } }); @@ -462,9 +458,7 @@ describe("DisturbanceReportProcessor", () => { inputType: "text" }); - await ( - processor as unknown as { processReportSpecificLogic: (report: DisturbanceReport) => Promise } - ).processReportSpecificLogic(disturbanceReport); + await processor.update(disturbanceReport, { status: "approved" }); const disturbance = await Disturbance.findOne({ where: { disturbanceableType: DisturbanceReport.LARAVEL_TYPE, disturbanceableId: disturbanceReport.id } @@ -488,9 +482,7 @@ describe("DisturbanceReportProcessor", () => { inputType: "select" }); - await ( - processor as unknown as { processReportSpecificLogic: (report: DisturbanceReport) => Promise } - ).processReportSpecificLogic(disturbanceReport); + await processor.update(disturbanceReport, { status: "approved" }); const disturbance = await Disturbance.findOne({ where: { disturbanceableType: DisturbanceReport.LARAVEL_TYPE, disturbanceableId: disturbanceReport.id } @@ -513,9 +505,7 @@ describe("DisturbanceReportProcessor", () => { inputType: "text" }); - await ( - processor as unknown as { processReportSpecificLogic: (report: DisturbanceReport) => Promise } - ).processReportSpecificLogic(disturbanceReport); + await processor.update(disturbanceReport, { status: "approved" }); const disturbance = await Disturbance.findOne({ where: { disturbanceableType: DisturbanceReport.LARAVEL_TYPE, disturbanceableId: disturbanceReport.id } @@ -560,9 +550,7 @@ describe("DisturbanceReportProcessor", () => { const warnSpy = jest.spyOn((processor as unknown as { logger: { warn: (m: string) => void } }).logger, "warn"); - await ( - processor as unknown as { processReportSpecificLogic: (report: DisturbanceReport) => Promise } - ).processReportSpecificLogic(disturbanceReport); + await processor.update(disturbanceReport, { status: "approved" }); const disturbance = await Disturbance.findOne({ where: { disturbanceableType: DisturbanceReport.LARAVEL_TYPE, disturbanceableId: disturbanceReport.id } diff --git a/apps/entity-service/src/entities/processors/disturbance-report.processor.ts b/apps/entity-service/src/entities/processors/disturbance-report.processor.ts index 7f892ca7..aa15a59b 100644 --- a/apps/entity-service/src/entities/processors/disturbance-report.processor.ts +++ b/apps/entity-service/src/entities/processors/disturbance-report.processor.ts @@ -45,6 +45,15 @@ export class DisturbanceReportProcessor extends ReportProcessor< readonly LIGHT_DTO = DisturbanceReportLightDto; readonly FULL_DTO = DisturbanceReportFullDto; private logger = new TMLogger(DisturbanceReportProcessor.name); + + async update(model: DisturbanceReport, update: ReportUpdateAttributes) { + await super.update(model, update); + + if (update.status === "approved") { + await this.processReportSpecificLogic(model); + } + } + /** * Specific method for DisturbanceReport custom logic. This is called automatically when the report is approved. * @@ -56,7 +65,7 @@ export class DisturbanceReportProcessor extends ReportProcessor< * - Extracts disturbance details (intensity, extent, type, subtype, peopleAffected, monetaryDamage, propertyAffected) * from disturbance report entries and populates the disturbance record */ - protected async processReportSpecificLogic(model: DisturbanceReport): Promise { + private async processReportSpecificLogic(model: DisturbanceReport): Promise { const entries = await DisturbanceReportEntry.report(model.id).findAll(); const affectedPolygonUuids = new Set(); @@ -210,6 +219,9 @@ export class DisturbanceReportProcessor extends ReportProcessor< } break; } + default: + this.logger.error(`Unknown disturbance report entry name: ${entry.name}`); + break; } } diff --git a/apps/entity-service/src/entities/processors/entity-processor.ts b/apps/entity-service/src/entities/processors/entity-processor.ts index 43eec44a..f3359621 100644 --- a/apps/entity-service/src/entities/processors/entity-processor.ts +++ b/apps/entity-service/src/entities/processors/entity-processor.ts @@ -204,14 +204,6 @@ export abstract class ReportProcessor< } } - if (model instanceof FinancialReport && update.status === "approved") { - await this.processReportSpecificLogic(model); - } - - if (model instanceof DisturbanceReport && update.status === "approved") { - await this.processReportSpecificLogic(model); - } - await super.update(model, update); } @@ -222,13 +214,4 @@ export abstract class ReportProcessor< return { [Op.or]: [null, false] }; } }; - - /** - * Override this method in specific report processors to add custom logic - * @param model The report model being processed - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected async processReportSpecificLogic(model: ReportModel): Promise { - // This method will be overridden in FinancialReportProcessor - } } diff --git a/apps/entity-service/src/entities/processors/financial-report.processor.spec.ts b/apps/entity-service/src/entities/processors/financial-report.processor.spec.ts index 8e6e1dc1..7b739d88 100644 --- a/apps/entity-service/src/entities/processors/financial-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/financial-report.processor.spec.ts @@ -258,11 +258,7 @@ describe("FinancialReportProcessor", () => { currency: "USD" }); - await ( - processor as unknown as { - processReportSpecificLogic: (report: FinancialReport) => Promise; - } - ).processReportSpecificLogic(financialReport); + await processor.update(financialReport, { status: "approved" }); await organisation.reload(); expect(organisation.finStartMonth).toBe(3); @@ -282,11 +278,7 @@ describe("FinancialReportProcessor", () => { warn: mockWarn }; - await ( - processor as unknown as { - processReportSpecificLogic: (report: FinancialReport) => Promise; - } - ).processReportSpecificLogic(financialReport); + await processor.update(financialReport, { status: "approved" }); expect(mockWarn).toHaveBeenCalledWith(`Organisation not found for FinancialReport ${financialReport.uuid}`); }); @@ -320,11 +312,7 @@ describe("FinancialReportProcessor", () => { const mockBulkCreate = jest.spyOn(FinancialIndicator, "bulkCreate").mockResolvedValue([]); const mockUpdate = jest.spyOn(FinancialIndicator, "update").mockResolvedValue([1]); - await ( - processor as unknown as { - processReportSpecificLogic: (report: FinancialReport) => Promise; - } - ).processReportSpecificLogic(financialReport); + await processor.update(financialReport, { status: "approved" }); expect(mockBulkCreate).toHaveBeenCalledWith([ { @@ -398,11 +386,7 @@ describe("FinancialReportProcessor", () => { const mockBulkCreate = jest.spyOn(FinancialIndicator, "bulkCreate").mockResolvedValue([]); const mockUpdate = jest.spyOn(FinancialIndicator, "update").mockResolvedValue([1]); - await ( - processor as unknown as { - processReportSpecificLogic: (report: FinancialReport) => Promise; - } - ).processReportSpecificLogic(financialReport); + await processor.update(financialReport, { status: "approved" }); expect(mockUpdate).toHaveBeenCalledTimes(2); expect(mockUpdate).toHaveBeenCalledWith( @@ -466,11 +450,7 @@ describe("FinancialReportProcessor", () => { const mockBulkCreate = jest.spyOn(FinancialIndicator, "bulkCreate").mockResolvedValue([]); const mockUpdate = jest.spyOn(FinancialIndicator, "update").mockResolvedValue([1]); - await ( - processor as unknown as { - processReportSpecificLogic: (report: FinancialReport) => Promise; - } - ).processReportSpecificLogic(financialReport); + await processor.update(financialReport, { status: "approved" }); expect(mockBulkCreate).toHaveBeenCalledWith([ { @@ -506,11 +486,7 @@ describe("FinancialReportProcessor", () => { const mockBulkCreate = jest.spyOn(FinancialIndicator, "bulkCreate").mockResolvedValue([]); const mockUpdate = jest.spyOn(FinancialIndicator, "update").mockResolvedValue([1]); - await ( - processor as unknown as { - processReportSpecificLogic: (report: FinancialReport) => Promise; - } - ).processReportSpecificLogic(financialReport); + await processor.update(financialReport, { status: "approved" }); expect(mockBulkCreate).not.toHaveBeenCalled(); expect(mockUpdate).not.toHaveBeenCalled(); diff --git a/apps/entity-service/src/entities/processors/financial-report.processor.ts b/apps/entity-service/src/entities/processors/financial-report.processor.ts index 5ef902cf..79eb7121 100644 --- a/apps/entity-service/src/entities/processors/financial-report.processor.ts +++ b/apps/entity-service/src/entities/processors/financial-report.processor.ts @@ -31,10 +31,18 @@ export class FinancialReportProcessor extends ReportProcessor< readonly FULL_DTO = FinancialReportFullDto; private logger = new TMLogger(FinancialReportProcessor.name); + async update(model: FinancialReport, update: ReportUpdateAttributes) { + await super.update(model, update); + + if (update.status === "approved") { + await this.processReportSpecificLogic(model); + } + } + /** - * Specific method for FinancialReport custom logic. This is called automatically when nothingToReport is updated + * Specific method for FinancialReport custom logic. This is called automatically when the report is approved */ - protected async processReportSpecificLogic(model: FinancialReport): Promise { + private async processReportSpecificLogic(model: FinancialReport): Promise { const organisation = await Organisation.findByPk(model.organisationId); if (organisation == null) { this.logger.warn(`Organisation not found for FinancialReport ${model.uuid}`); From 994f307591ef3d6cccd8c2d59c4e0d9743c506fe Mon Sep 17 00:00:00 2001 From: LimberHope Date: Tue, 14 Oct 2025 12:42:33 -0400 Subject: [PATCH 09/11] [TM-2517] fix lint --- apps/entity-service/src/entities/processors/entity-processor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/entity-service/src/entities/processors/entity-processor.ts b/apps/entity-service/src/entities/processors/entity-processor.ts index f3359621..e7a10354 100644 --- a/apps/entity-service/src/entities/processors/entity-processor.ts +++ b/apps/entity-service/src/entities/processors/entity-processor.ts @@ -13,7 +13,7 @@ import { NEEDS_MORE_INFORMATION, RESTORATION_IN_PROGRESS } from "@terramatch-microservices/database/constants/status"; -import { DisturbanceReport, FinancialReport, ProjectReport } from "@terramatch-microservices/database/entities"; +import { ProjectReport } from "@terramatch-microservices/database/entities"; export type Aggregate> = { func: string; From 61a179e6eed12c1cbc04784fbe138ca2b8b5a713 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Tue, 14 Oct 2025 14:12:51 -0400 Subject: [PATCH 10/11] [TM-2517] update unit test\ --- .../disturbance-report.processor.spec.ts | 28 ++++++++++++---- .../financial-report.processor.spec.ts | 33 ++++++------------- 2 files changed, 32 insertions(+), 29 deletions(-) diff --git a/apps/entity-service/src/entities/processors/disturbance-report.processor.spec.ts b/apps/entity-service/src/entities/processors/disturbance-report.processor.spec.ts index 2e3cd6d5..a89aad64 100644 --- a/apps/entity-service/src/entities/processors/disturbance-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/disturbance-report.processor.spec.ts @@ -331,7 +331,8 @@ describe("DisturbanceReportProcessor", () => { const disturbanceReport = await DisturbanceReportFactory.create({ projectId: project.id, description: "desc", - actionDescription: "act" + actionDescription: "act", + status: "awaiting-approval" }); const poly1 = await SitePolygonFactory.create(); @@ -416,7 +417,10 @@ describe("DisturbanceReportProcessor", () => { it("does not overwrite existing polygon disturbanceId and updates only null ones", async () => { const project = await ProjectFactory.create(); - const disturbanceReport = await DisturbanceReportFactory.create({ projectId: project.id }); + const disturbanceReport = await DisturbanceReportFactory.create({ + projectId: project.id, + status: "awaiting-approval" + }); const preExistingDisturbance = await Disturbance.create({ disturbanceableType: DisturbanceReport.LARAVEL_TYPE, @@ -446,7 +450,10 @@ describe("DisturbanceReportProcessor", () => { it("parses CSV fallback for polygon-affected when JSON fails", async () => { const project = await ProjectFactory.create(); - const disturbanceReport = await DisturbanceReportFactory.create({ projectId: project.id }); + const disturbanceReport = await DisturbanceReportFactory.create({ + projectId: project.id, + status: "awaiting-approval" + }); const p1 = await SitePolygonFactory.create(); const p2 = await SitePolygonFactory.create(); @@ -473,7 +480,10 @@ describe("DisturbanceReportProcessor", () => { it("returns early and does not create disturbance when no polygons provided", async () => { const project = await ProjectFactory.create(); - const disturbanceReport = await DisturbanceReportFactory.create({ projectId: project.id }); + const disturbanceReport = await DisturbanceReportFactory.create({ + projectId: project.id, + status: "awaiting-approval" + }); await DisturbanceReportEntryFactory.create({ disturbanceReportId: disturbanceReport.id, @@ -492,7 +502,10 @@ describe("DisturbanceReportProcessor", () => { it("handles nested arrays in polygon-affected JSON", async () => { const project = await ProjectFactory.create(); - const disturbanceReport = await DisturbanceReportFactory.create({ projectId: project.id }); + const disturbanceReport = await DisturbanceReportFactory.create({ + projectId: project.id, + status: "awaiting-approval" + }); const p1 = await SitePolygonFactory.create(); const p2 = await SitePolygonFactory.create(); @@ -520,7 +533,10 @@ describe("DisturbanceReportProcessor", () => { it("warns on invalid JSON for subtype and property-affected and ignores invalid date", async () => { const project = await ProjectFactory.create(); - const disturbanceReport = await DisturbanceReportFactory.create({ projectId: project.id }); + const disturbanceReport = await DisturbanceReportFactory.create({ + projectId: project.id, + status: "awaiting-approval" + }); const p = await SitePolygonFactory.create(); await DisturbanceReportEntryFactory.create({ diff --git a/apps/entity-service/src/entities/processors/financial-report.processor.spec.ts b/apps/entity-service/src/entities/processors/financial-report.processor.spec.ts index 7b739d88..0af0ed56 100644 --- a/apps/entity-service/src/entities/processors/financial-report.processor.spec.ts +++ b/apps/entity-service/src/entities/processors/financial-report.processor.spec.ts @@ -255,7 +255,8 @@ describe("FinancialReportProcessor", () => { const financialReport = await FinancialReportFactory.create({ organisationId: organisation.id, finStartMonth: 3, - currency: "USD" + currency: "USD", + status: "awaiting-approval" }); await processor.update(financialReport, { status: "approved" }); @@ -265,28 +266,11 @@ describe("FinancialReportProcessor", () => { expect(organisation.currency).toBe("USD"); }); - it("should log warning and return early when organisation is not found", async () => { - const organisation = await OrganisationFactory.create(); - const financialReport = await FinancialReportFactory.create({ - organisationId: organisation.id - }); - - financialReport.organisationId = 99999; - - const mockWarn = jest.fn(); - (processor as unknown as { logger: { warn: jest.Mock } }).logger = { - warn: mockWarn - }; - - await processor.update(financialReport, { status: "approved" }); - - expect(mockWarn).toHaveBeenCalledWith(`Organisation not found for FinancialReport ${financialReport.uuid}`); - }); - it("should create new financial indicators when none exist in organisation", async () => { const organisation = await OrganisationFactory.create(); const financialReport = await FinancialReportFactory.create({ - organisationId: organisation.id + organisationId: organisation.id, + status: "awaiting-approval" }); await FinancialIndicatorFactory.create({ @@ -342,7 +326,8 @@ describe("FinancialReportProcessor", () => { it("should update existing financial indicators when they exist in organisation", async () => { const organisation = await OrganisationFactory.create(); const financialReport = await FinancialReportFactory.create({ - organisationId: organisation.id + organisationId: organisation.id, + status: "awaiting-approval" }); const existingIndicator1 = await FinancialIndicatorFactory.create({ @@ -415,7 +400,8 @@ describe("FinancialReportProcessor", () => { it("should handle mixed scenario with both creating and updating indicators", async () => { const organisation = await OrganisationFactory.create(); const financialReport = await FinancialReportFactory.create({ - organisationId: organisation.id + organisationId: organisation.id, + status: "awaiting-approval" }); const existingIndicator = await FinancialIndicatorFactory.create({ @@ -480,7 +466,8 @@ describe("FinancialReportProcessor", () => { it("should handle case when no financial indicators exist in report", async () => { const organisation = await OrganisationFactory.create(); const financialReport = await FinancialReportFactory.create({ - organisationId: organisation.id + organisationId: organisation.id, + status: "awaiting-approval" }); const mockBulkCreate = jest.spyOn(FinancialIndicator, "bulkCreate").mockResolvedValue([]); From 04254435986ad60c3e0c07c3e31f374e3b8d67d2 Mon Sep 17 00:00:00 2001 From: LimberHope Date: Tue, 14 Oct 2025 16:23:19 -0400 Subject: [PATCH 11/11] [TM-2517] rename property disturbanceableId -> reportId --- .../entity-service/src/entities/dto/disturbance-report.dto.ts | 4 ++-- .../src/entities/processors/disturbance-report.processor.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/entity-service/src/entities/dto/disturbance-report.dto.ts b/apps/entity-service/src/entities/dto/disturbance-report.dto.ts index 5040c33f..6c48e367 100644 --- a/apps/entity-service/src/entities/dto/disturbance-report.dto.ts +++ b/apps/entity-service/src/entities/dto/disturbance-report.dto.ts @@ -52,8 +52,8 @@ export class DisturbanceReportLightDto extends EntityDto { @ApiProperty({ nullable: true, type: DisturbanceReportEntryDto, isArray: true }) entries: DisturbanceReportEntryDto[] | null; - @ApiProperty({ nullable: true, type: Number }) - disturbanceableId: number | null; + @ApiProperty({ type: Number }) + reportId: number; } export type DisturbanceReportMedia = Pick; diff --git a/apps/entity-service/src/entities/processors/disturbance-report.processor.ts b/apps/entity-service/src/entities/processors/disturbance-report.processor.ts index aa15a59b..7029b720 100644 --- a/apps/entity-service/src/entities/processors/disturbance-report.processor.ts +++ b/apps/entity-service/src/entities/processors/disturbance-report.processor.ts @@ -306,7 +306,7 @@ export class DisturbanceReportProcessor extends ReportProcessor< const dateOfDisturbance = entries.find(entry => entry.name === "date-of-disturbance")?.value; const mediaCollection = await Media.for(disturbanceReport).findAll(); const dto = new DisturbanceReportFullDto(disturbanceReport, { - disturbanceableId: disturbanceReport.id, + reportId: disturbanceReport.id, entries, intensity, dateOfDisturbance: dateOfDisturbance != null ? new Date(dateOfDisturbance) : null, @@ -342,7 +342,7 @@ export class DisturbanceReportProcessor extends ReportProcessor< return { id: disturbanceReport.uuid, dto: new DisturbanceReportLightDto(disturbanceReport, { - disturbanceableId: disturbanceReport.id, + reportId: disturbanceReport.id, entries, intensity, dateOfDisturbance: dateOfDisturbance != null ? new Date(dateOfDisturbance) : null