diff --git a/apps/api/openapi.json b/apps/api/openapi.json index 83071620..46f4ddc9 100644 --- a/apps/api/openapi.json +++ b/apps/api/openapi.json @@ -10413,6 +10413,14 @@ "type": "string", "description": "Nuevo horario. Cadena vacía lo borra. Omitir para no cambiarlo.", "nullable": true + }, + "location": { + "description": "Nueva ubicación (dirección + coordenadas) para corregir la geolocalización. Omitir para no cambiarla.", + "allOf": [ + { + "$ref": "#/components/schemas/LocationDto" + } + ] } }, "required": [ diff --git a/apps/api/src/contexts/resources/application/edit-resource.spec.ts b/apps/api/src/contexts/resources/application/edit-resource.spec.ts index b94223dc..4c2dd267 100644 --- a/apps/api/src/contexts/resources/application/edit-resource.spec.ts +++ b/apps/api/src/contexts/resources/application/edit-resource.spec.ts @@ -80,6 +80,30 @@ describe('EditResource', () => { ]); }); + it('corrects the location in place and reports the coordinate diff', async () => { + const id = await seed(); + + const result = await editResource.execute({ + resourceId: id, + location: { address: 'Av. Bolívar 5', latitude: 10.5, longitude: -66.92 }, + }); + + const resource = await repo.findById(ResourceId.fromString(id)); + expect(resource!.location.toPlain()).toEqual({ + address: 'Av. Bolívar 5', + latitude: 10.5, + longitude: -66.92, + }); + expect(result.changes).toEqual( + expect.arrayContaining([ + { field: 'address', before: 'Caracas', after: 'Av. Bolívar 5' }, + { field: 'latitude', before: 10.48, after: 10.5 }, + { field: 'longitude', before: -66.9, after: -66.92 }, + ]), + ); + expect(result.changes).toHaveLength(3); + }); + it('throws ResourceNotFoundError for an unknown id', async () => { await expect( editResource.execute({ resourceId: OWNER, name: 'x' }), diff --git a/apps/api/src/contexts/resources/application/edit-resource.ts b/apps/api/src/contexts/resources/application/edit-resource.ts index 9f1913e2..8411b36f 100644 --- a/apps/api/src/contexts/resources/application/edit-resource.ts +++ b/apps/api/src/contexts/resources/application/edit-resource.ts @@ -2,6 +2,7 @@ import { ResourceRepository } from '../domain/ports/resource.repository'; import { ResourceId } from '../domain/resource-id'; import { EditResourceProps } from '../domain/resource'; import { ResourceNotFoundError } from './resource-not-found.error'; +import { Location, LocationProps } from '../../../shared/domain/location'; import { MutationAuditResult, diffFields, @@ -13,6 +14,7 @@ export interface EditResourceCommand { description?: string | null; contact?: string | null; schedule?: string | null; + location?: LocationProps; } /** @@ -34,6 +36,9 @@ export class EditResource { description: resource.description, contact: resource.contact, schedule: resource.schedule, + address: resource.location.toPlain().address, + latitude: resource.location.toPlain().latitude, + longitude: resource.location.toPlain().longitude, }; const edit: EditResourceProps = {}; @@ -41,6 +46,8 @@ export class EditResource { if (cmd.description !== undefined) edit.description = cmd.description; if (cmd.contact !== undefined) edit.contact = cmd.contact; if (cmd.schedule !== undefined) edit.schedule = cmd.schedule; + if (cmd.location !== undefined) + edit.location = Location.create(cmd.location); resource.edit(edit); const after = { @@ -48,6 +55,9 @@ export class EditResource { description: resource.description, contact: resource.contact, schedule: resource.schedule, + address: resource.location.toPlain().address, + latitude: resource.location.toPlain().latitude, + longitude: resource.location.toPlain().longitude, }; await this.repo.save(resource); diff --git a/apps/api/src/contexts/resources/domain/resource.ts b/apps/api/src/contexts/resources/domain/resource.ts index 548c7f48..b7d98bd3 100644 --- a/apps/api/src/contexts/resources/domain/resource.ts +++ b/apps/api/src/contexts/resources/domain/resource.ts @@ -68,6 +68,8 @@ export interface EditResourceProps { description?: string | null; contact?: string | null; schedule?: string | null; + /** Correct the point's geolocation (address + coordinates) in place (#—). */ + location?: Location; } // Snapshot used by repositories to rehydrate without going through register(). @@ -117,7 +119,7 @@ export class Resource { public readonly type: ResourceType, private _name: string, private _description: string | null, - public readonly location: Location, + private _location: Location, public readonly ownerUserId: string, public readonly ownerOrganizationId: string | null, private _verificationLevel: VerificationLevel, @@ -213,6 +215,9 @@ export class Resource { get description(): string | null { return this._description; } + get location(): Location { + return this._location; + } get contact(): string | null { return this._contact; } @@ -316,6 +321,9 @@ export class Resource { this._schedule = props.schedule === null ? null : props.schedule.trim() || null; } + if (props.location !== undefined) { + this._location = props.location; + } } /** diff --git a/apps/api/src/contexts/resources/infrastructure/drizzle/drizzle-resource.repository.ts b/apps/api/src/contexts/resources/infrastructure/drizzle/drizzle-resource.repository.ts index 340e447f..2ac22feb 100644 --- a/apps/api/src/contexts/resources/infrastructure/drizzle/drizzle-resource.repository.ts +++ b/apps/api/src/contexts/resources/infrastructure/drizzle/drizzle-resource.repository.ts @@ -268,6 +268,10 @@ export class DrizzleResourceRepository implements ResourceRepository { verificationLevel: s.verificationLevel, publicStatus: s.publicStatus, name: s.name, + description: s.description, + address: s.location.address, + latitude: s.location.latitude, + longitude: s.location.longitude, contact: s.contact, schedule: s.schedule, manager: s.manager, diff --git a/apps/api/src/contexts/resources/infrastructure/http/dto.ts b/apps/api/src/contexts/resources/infrastructure/http/dto.ts index 0498dd87..7dc1a734 100644 --- a/apps/api/src/contexts/resources/infrastructure/http/dto.ts +++ b/apps/api/src/contexts/resources/infrastructure/http/dto.ts @@ -555,6 +555,16 @@ export class EditResourceDto { @IsOptional() @IsString() schedule?: string; + + @ApiPropertyOptional({ + description: + 'Nueva ubicación (dirección + coordenadas) para corregir la geolocalización. Omitir para no cambiarla.', + type: LocationDto, + }) + @IsOptional() + @ValidateNested() + @Type(() => LocationDto) + location?: LocationDto; } export class ReportResourceValidityDto { diff --git a/apps/api/src/contexts/resources/infrastructure/http/resources.controller.ts b/apps/api/src/contexts/resources/infrastructure/http/resources.controller.ts index 7488750c..4ce62f50 100644 --- a/apps/api/src/contexts/resources/infrastructure/http/resources.controller.ts +++ b/apps/api/src/contexts/resources/infrastructure/http/resources.controller.ts @@ -353,6 +353,7 @@ export class ResourcesController { if (dto.description !== undefined) cmd.description = dto.description; if (dto.contact !== undefined) cmd.contact = dto.contact; if (dto.schedule !== undefined) cmd.schedule = dto.schedule; + if (dto.location !== undefined) cmd.location = dto.location; const result = await this.editResource.execute(cmd); setAuditContext(req, { diff --git a/packages/api-client/src/schema.ts b/packages/api-client/src/schema.ts index 49d0704f..e6efcd02 100644 --- a/packages/api-client/src/schema.ts +++ b/packages/api-client/src/schema.ts @@ -3320,6 +3320,8 @@ export interface components { contact?: string | null; /** @description Nuevo horario. Cadena vacía lo borra. Omitir para no cambiarlo. */ schedule?: string | null; + /** @description Nueva ubicación (dirección + coordenadas) para corregir la geolocalización. Omitir para no cambiarla. */ + location?: components["schemas"]["LocationDto"]; }; DiscardResourceDto: { /**