feat(collections): edit a saved place's provider ids, website, phone and price (#2646)

The update contract takes google_place_id, google_ftid, osm_id, website,
phone, price and currency like saving does, website with the same
normalisation and refusal. REST and update_collection_place share it, so an
agent can correct a place instead of re-saving it and losing its labels,
ratings, position and id. The assistant's change card names the new fields.
This commit is contained in:
Maurice
2026-11-13 09:33:27 +01:00
parent 72e8b1f8d9
commit cae04e83dd
44 changed files with 289 additions and 13 deletions
@@ -171,6 +171,13 @@ export class CollectionPlacesRepository extends TrekRepository<CollectionPlaces>
category_id: number | null;
image_url: string | null;
links: string | null;
google_place_id: string | null;
google_ftid: string | null;
osm_id: string | null;
website: string | null;
phone: string | null;
price: number | null;
currency: string | null;
collection_id: number;
owner_id: number;
}>,
@@ -77,6 +77,13 @@ const FIELD_OF: Readonly<Record<string, (typeof COLLECTION_CARD_FIELDS)[number]>
category_id: 'category',
links: 'link',
image_url: 'photo',
website: 'website',
phone: 'phone',
price: 'price',
currency: 'price',
google_place_id: 'mapEntry',
google_ftid: 'mapEntry',
osm_id: 'mapEntry',
};
function fieldsOf(args: ToolArgs): CollectionCardDelta['fields'] {
@@ -1,4 +1,4 @@
import type { CollectionLink } from '@trek/shared';
import type { CollectionLink, CollectionPlaceUpdateRequest } from '@trek/shared';
/**
* Pure helpers of the collections domain, moved verbatim out of
@@ -20,3 +20,31 @@ export function parseLinks(raw: unknown): CollectionLink[] | undefined {
export function serializeLinks(links: CollectionLink[] | undefined): string | null {
return links && links.length ? JSON.stringify(links) : null;
}
/** The provider ids, contact and price columns of a saved place, as an update writes them. */
type ListingWrite = Partial<{
google_place_id: string | null;
google_ftid: string | null;
osm_id: string | null;
website: string | null;
phone: string | null;
price: number | null;
currency: string | null;
}>;
/**
* The fields saving a place accepts and editing it did not until #2646: each one
* the update names, null clearing it, the rest left out so the stored value stays.
* The website arrives normalised and checked by the contract, as on save.
*/
export function listingWriteOf(body: CollectionPlaceUpdateRequest): ListingWrite {
const write: ListingWrite = {};
if (body.google_place_id !== undefined) write.google_place_id = body.google_place_id ?? null;
if (body.google_ftid !== undefined) write.google_ftid = body.google_ftid ?? null;
if (body.osm_id !== undefined) write.osm_id = body.osm_id ?? null;
if (body.website !== undefined) write.website = body.website ?? null;
if (body.phone !== undefined) write.phone = body.phone ?? null;
if (body.price !== undefined) write.price = body.price ?? null;
if (body.currency !== undefined) write.currency = body.currency ?? null;
return write;
}
@@ -327,7 +327,7 @@ export class CollectionsMcp {
@Tool({
name: 'update_collection_place',
description:
"Update a saved place's name, address, coordinates (lat/lng), description, notes, status, category, links, tags, labels, image, or move it to another collection (set collection_id). Moving a place out of a list needs the same role there as deleting it (owner or admin).",
"Update a saved place's name, address, coordinates (lat/lng), description, notes, status, category, links, tags, labels, image, provider ids (google_place_id, google_ftid, osm_id), website, phone, price and currency, or move it to another collection (set collection_id). Only the fields you pass change; null clears one. A bare domain as website gets https://, any other scheme is refused. Moving a place out of a list needs the same role there as deleting it (owner or admin).",
inputSchema: { placeId: idSchema, ...collectionPlaceUpdateRequestSchema.shape, links: linksInput },
annotations: TOOL_ANNOTATIONS_WRITE,
when: collectionsAddonOn,
@@ -75,7 +75,7 @@ import type {
import { NotificationsService } from '../notifications/notifications.service';
import { PlaceImportService, type ExportedCollectionFile } from '../place-import/place-import.service';
import { UnitOfWork } from '../database/unit-of-work';
import { parseLinks, serializeLinks } from './collections.helpers';
import { listingWriteOf, parseLinks, serializeLinks } from './collections.helpers';
// Errors: a DomainError carrying a status; TrekExceptionFilter maps it to that code with an `{ error }` body.
@@ -872,7 +872,7 @@ export class CollectionsService {
await this.assertCanEdit(userId, currentCollection);
const prevImage = body.image_url !== undefined ? (await this.collectionPlaces.imageUrl(placeId)) ?? null : null;
const write: Parameters<CollectionPlacesRepository['updateFields']>[1] = {};
const write: Parameters<CollectionPlacesRepository['updateFields']>[1] = listingWriteOf(body);
if (body.name !== undefined) write.name = body.name;
if (body.description !== undefined) write.description = body.description ?? null;
if (body.notes !== undefined) write.notes = body.notes ?? null;