Skip to content

Commit ad44ae7

Browse files
authored
Merge branch 'main' into feat/orca-1117-bulk-add-repairs
2 parents 580348e + 64d4d92 commit ad44ae7

3 files changed

Lines changed: 181 additions & 27 deletions

File tree

admin/backend/src/resource-asset/dto/resource-asset.dto.ts

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,30 @@ export class BaseRecreationAssetDto {
158158
@IsDateString()
159159
@IsOptional()
160160
installation_date?: string | null;
161+
162+
@ApiPropertyOptional({
163+
description: 'Point geometry type code, if this asset has a location',
164+
example: 'PT',
165+
nullable: true,
166+
})
167+
@IsOptional()
168+
geometry_type_code?: string | null;
169+
170+
@ApiPropertyOptional({
171+
description: 'Latitude in WGS84 (derived from recreation_asset_geom)',
172+
example: 49.94212,
173+
nullable: true,
174+
})
175+
@IsOptional()
176+
latitude?: number | null;
177+
178+
@ApiPropertyOptional({
179+
description: 'Longitude in WGS84 (derived from recreation_asset_geom)',
180+
example: -123.03604,
181+
nullable: true,
182+
})
183+
@IsOptional()
184+
longitude?: number | null;
161185
}
162186

163187
/**
@@ -199,33 +223,6 @@ export class RecreationAssetDto extends BaseRecreationAssetDto {
199223
@IsOptional()
200224
updated_at?: Date | string | null;
201225

202-
@ApiPropertyOptional({
203-
description: 'Point geometry type code, if this asset has a location',
204-
example: 'PT',
205-
type: String,
206-
nullable: true,
207-
})
208-
@IsOptional()
209-
geometry_type_code?: string | null;
210-
211-
@ApiPropertyOptional({
212-
description: 'Latitude in WGS84 (derived from recreation_asset_geom)',
213-
example: 49.94212,
214-
type: Number,
215-
nullable: true,
216-
})
217-
@IsOptional()
218-
latitude?: number | null;
219-
220-
@ApiPropertyOptional({
221-
description: 'Longitude in WGS84 (derived from recreation_asset_geom)',
222-
example: -123.03604,
223-
type: Number,
224-
nullable: true,
225-
})
226-
@IsOptional()
227-
longitude?: number | null;
228-
229226
@ApiProperty({
230227
description: 'List of repairs associated with this asset',
231228
type: [RecreationAssetRepairDto],

admin/backend/src/resource-asset/service/resource-asset.service.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ export class RecreationAssetService {
9696
},
9797
});
9898

99+
if (dto.geometry_type_code && dto.latitude && dto.longitude) {
100+
await this.upsertAssetGeometry(
101+
Number(created.asset_id),
102+
dto.geometry_type_code,
103+
dto.latitude,
104+
dto.longitude,
105+
);
106+
}
107+
99108
return this.mapAssetToDto(created);
100109
}
101110

@@ -250,6 +259,15 @@ export class RecreationAssetService {
250259
data,
251260
});
252261

262+
if (dto.geometry_type_code && dto.latitude && dto.longitude) {
263+
await this.upsertAssetGeometry(
264+
Number(updated.asset_id),
265+
dto.geometry_type_code,
266+
dto.latitude,
267+
dto.longitude,
268+
);
269+
}
270+
253271
return this.mapAssetToDto(updated);
254272
}
255273

@@ -614,4 +632,32 @@ export class RecreationAssetService {
614632
updated_at: record.updated_at ? record.updated_at.toISOString() : null,
615633
};
616634
}
635+
636+
/**
637+
* Upsert geometry info for an Asset.
638+
*/
639+
async upsertAssetGeometry(
640+
asset_id: number,
641+
geometry_type_code: string,
642+
latitude: number,
643+
longitude: number,
644+
): Promise<void> {
645+
// SRID 4326 represents standard WGS 84 (GPS latitude/longitude)
646+
await this.prisma.$executeRaw`
647+
INSERT INTO rst.recreation_asset_geom
648+
(asset_id, geometry_type_code, geometry, created_at, updated_at)
649+
VALUES (
650+
${asset_id},
651+
${geometry_type_code},
652+
ST_SetSRID(ST_MakePoint(${longitude}, ${latitude}), 4326),
653+
now(),
654+
now()
655+
)
656+
ON CONFLICT (asset_id) DO UPDATE
657+
SET
658+
geometry_type_code = EXCLUDED.geometry_type_code,
659+
geometry = EXCLUDED.geometry,
660+
updated_at = now();
661+
`;
662+
}
617663
}

admin/backend/test/resource-asset/service/resource-asset.service.spec.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ describe('RecreationAssetService', () => {
3535
};
3636
$transaction: ReturnType<typeof vi.fn>;
3737
$queryRawTyped: ReturnType<typeof vi.fn>;
38+
$executeRaw: ReturnType<typeof vi.fn>;
3839
};
3940

4041
beforeEach(async () => {
@@ -66,6 +67,7 @@ describe('RecreationAssetService', () => {
6667
},
6768
$transaction: vi.fn((cb) => cb(prismaMock)),
6869
$queryRawTyped: vi.fn().mockResolvedValue([]),
70+
$executeRaw: vi.fn(),
6971
};
7072

7173
const module: TestingModule = await Test.createTestingModule({
@@ -290,6 +292,66 @@ describe('RecreationAssetService', () => {
290292

291293
expect(result.installation_date).toBeNull();
292294
});
295+
296+
it('should create an asset with geometry object', async () => {
297+
(
298+
prismaMock.$executeRaw as unknown as ReturnType<typeof vi.fn>
299+
).mockResolvedValue(undefined);
300+
vi.spyOn(service, 'findAssetCodeById').mockResolvedValue({
301+
asset_code: 10,
302+
description: 'Test Code',
303+
});
304+
305+
prismaMock.recreation_asset.create.mockResolvedValue({
306+
asset_id: 101n,
307+
parent_id: null,
308+
asset_tag: null,
309+
rec_resource_id: 'REC-1',
310+
asset_code: 10,
311+
asset_name: null,
312+
asset_comment: null,
313+
legacy_structure_id: null,
314+
asset_length: null,
315+
asset_width: null,
316+
asset_area: null,
317+
actual_value: null,
318+
default_value: null,
319+
installation_date: null,
320+
geometry_type_code: null,
321+
latitude: null,
322+
longitude: null,
323+
});
324+
325+
const dto = {
326+
asset_code: 10,
327+
rec_resource_id: 'REC-1',
328+
geometry_type_code: 'P',
329+
latitude: 50.281403,
330+
longitude: -125.648533,
331+
};
332+
333+
await service.createAsset(dto);
334+
335+
expect(prismaMock.$executeRaw).toHaveBeenCalled();
336+
337+
expect(prismaMock.recreation_asset.create).toHaveBeenCalledWith({
338+
data: {
339+
parent_id: null,
340+
asset_tag: null,
341+
rec_resource_id: 'REC-1',
342+
asset_code: 10,
343+
asset_name: null,
344+
asset_comment: null,
345+
legacy_structure_id: null,
346+
asset_length: null,
347+
asset_width: null,
348+
asset_area: null,
349+
actual_value: null,
350+
default_value: null,
351+
installation_date: null,
352+
},
353+
});
354+
});
293355
});
294356

295357
describe('RecreationAssetService - findAllAssets', () => {
@@ -826,6 +888,55 @@ describe('RecreationAssetService', () => {
826888
expect(result.installation_date).toBe('2023-05-15');
827889
});
828890

891+
it('should update an asset with geometry object', async () => {
892+
(
893+
prismaMock.$executeRaw as unknown as ReturnType<typeof vi.fn>
894+
).mockResolvedValue(undefined);
895+
prismaMock.recreation_asset.findUnique.mockResolvedValue({
896+
asset_id: 1n,
897+
});
898+
899+
prismaMock.recreation_asset.update.mockResolvedValue({
900+
asset_id: 1n,
901+
parent_id: null,
902+
asset_tag: null,
903+
rec_resource_id: 'REC-1',
904+
asset_code: 10,
905+
asset_name: null,
906+
asset_comment: null,
907+
legacy_structure_id: null,
908+
asset_length: null,
909+
asset_width: null,
910+
asset_area: null,
911+
actual_value: null,
912+
default_value: null,
913+
installation_date: null,
914+
geometry_type_code: null,
915+
latitude: null,
916+
longitude: null,
917+
});
918+
919+
const dto = {
920+
asset_code: 10,
921+
rec_resource_id: 'REC-1',
922+
geometry_type_code: 'P',
923+
latitude: 50.281403,
924+
longitude: -125.648533,
925+
};
926+
927+
await service.updateAsset(1, dto);
928+
929+
expect(prismaMock.$executeRaw).toHaveBeenCalled();
930+
931+
expect(prismaMock.recreation_asset.update).toHaveBeenCalledWith({
932+
where: { asset_id: 1n },
933+
data: {
934+
rec_resource_id: 'REC-1',
935+
asset_code: 10,
936+
},
937+
});
938+
});
939+
829940
it('should handle updating parent_id as number and clearing installation_date', async () => {
830941
prismaMock.recreation_asset.findUnique.mockResolvedValue({
831942
asset_id: 1n,

0 commit comments

Comments
 (0)