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
@@ -28,6 +28,36 @@ describe('collectionStatusSchema', () => {
});
describe('collectionPlaceUpdateRequestSchema', () => {
// #2646: the fields saving takes, with the same checks, so a place is corrected in place.
it('takes the provider ids, website, phone and price, and null clears each', () => {
const set = {
google_place_id: 'ChIJseptime',
google_ftid: '0x47e6:0x1b2',
osm_id: 'node/42',
website: 'https://septime-charonne.fr',
phone: '+33 1 43 67 38 29',
price: 55,
currency: 'EUR',
};
expect(collectionPlaceUpdateRequestSchema.parse(set)).toEqual(set);
const cleared = Object.fromEntries(Object.keys(set).map((key) => [key, null]));
expect(collectionPlaceUpdateRequestSchema.parse(cleared)).toEqual(cleared);
});
it('normalises a bare domain as website and refuses a script link, as saving does', () => {
expect(collectionPlaceUpdateRequestSchema.parse({ website: 'septime-charonne.fr' }).website).toBe(
'https://septime-charonne.fr',
);
expect(collectionPlaceUpdateRequestSchema.safeParse({ website: 'javascript:alert(1)' }).success).toBe(false);
});
it('leaves the new fields absent when an update does not name them', () => {
const parsed = collectionPlaceUpdateRequestSchema.parse({ name: 'Septime' });
for (const key of ['google_place_id', 'google_ftid', 'osm_id', 'website', 'phone', 'price', 'currency']) {
expect(key in parsed, key).toBe(false);
}
});
// Regression for #1437: an update that omits `status` must NOT inject a default
// of 'idea', otherwise the server overwrites the stored status on every edit-save.
it('does not inject a status when the field is absent', () => {
@@ -223,6 +223,16 @@ export const collectionPlaceUpdateRequestSchema = z.object({
label_ids: z.array(z.number()).optional(),
// Custom thumbnail (#1136): null clears it (falls back to the auto-fetched photo).
image_url: placeImageUrlSchema.nullable().optional(),
// The fields saving accepts but editing did not (#2646), with the same checks, so
// a place can be corrected in place instead of re-saved, which loses its labels,
// ratings, position and id. Each one is left alone when absent; null clears it.
google_place_id: z.string().nullable().optional(),
google_ftid: z.string().nullable().optional(),
osm_id: z.string().nullable().optional(),
website: placeWebsiteSchema.nullable().optional(),
phone: z.string().nullable().optional(),
price: z.number().nullable().optional(),
currency: z.string().nullable().optional(),
});
export type CollectionPlaceUpdateRequest = z.infer<typeof collectionPlaceUpdateRequestSchema>;