revert(collections): a saved place carries no price, website or phone again

Takes back the editable price, currency, website and phone on collection
places from 0df82185b and its review follow-up in a910c2a83. Costs are kept
inside a trip only; a collection does not carry them, and #2471 is a
feature request rather than a bug. The collection files are back to how
they were before, including the dimmed save button from f16698bdb, which
only mattered for the price field.
This commit is contained in:
Maurice
2026-09-23 20:32:41 +02:00
parent f16698bdb1
commit a1d6348c5f
40 changed files with 25 additions and 937 deletions
@@ -1,6 +1,5 @@
import {
collectionDeleteManyRequestSchema,
collectionPlaceCurrencySchema,
collectionPlaceUpdateRequestSchema,
collectionReorderRequestSchema,
collectionSavePlaceRequestSchema,
@@ -48,65 +47,6 @@ describe('collectionPlaceUpdateRequestSchema', () => {
});
});
// #2471: price, currency, website and phone were columns the update contract did
// not know, so the validation pipe stripped them and a saved place's cost could
// never be edited.
describe('collectionPlaceUpdateRequestSchema price, currency, website and phone', () => {
it('passes all four through', () => {
const parsed = collectionPlaceUpdateRequestSchema.parse({
price: 12.5,
currency: 'CHF',
website: 'https://museum.example',
phone: '+41 44 123 45 67',
});
expect(parsed).toEqual({
price: 12.5,
currency: 'CHF',
website: 'https://museum.example',
phone: '+41 44 123 45 67',
});
});
it('accepts null on each to clear it, and zero as a price', () => {
const none = { price: null, currency: null, website: null, phone: null };
expect(collectionPlaceUpdateRequestSchema.parse(none)).toEqual(none);
expect(collectionPlaceUpdateRequestSchema.parse({ price: 0 }).price).toBe(0);
});
it('leaves all four absent when an unrelated field changes', () => {
const parsed = collectionPlaceUpdateRequestSchema.parse({ name: 'Kunsthaus' });
for (const key of ['price', 'currency', 'website', 'phone']) expect(key in parsed).toBe(false);
});
it('refuses a negative or non-finite price', () => {
for (const price of [-1, -0.01, Number.NaN, Number.POSITIVE_INFINITY]) {
expect(collectionPlaceUpdateRequestSchema.safeParse({ price }).success).toBe(false);
}
});
it('normalises the currency to a trimmed upper-case code', () => {
expect(collectionPlaceUpdateRequestSchema.parse({ currency: ' usd ' }).currency).toBe('USD');
expect(collectionPlaceCurrencySchema.parse('jpy')).toBe('JPY');
});
it('refuses a currency that is not three letters', () => {
for (const currency of ['EU', 'EURO', '€', '12A', '']) {
expect(collectionPlaceUpdateRequestSchema.safeParse({ currency }).success).toBe(false);
}
});
it('holds the website to http or https, like the save side', () => {
expect(collectionPlaceUpdateRequestSchema.safeParse({ website: 'javascript:alert(1)' }).success).toBe(false);
expect(collectionPlaceUpdateRequestSchema.safeParse({ website: 'http://museum.example' }).success).toBe(true);
});
it('trims the phone and caps it at 60 characters', () => {
expect(collectionPlaceUpdateRequestSchema.parse({ phone: ' +49 30 1234 ' }).phone).toBe('+49 30 1234');
expect(collectionPlaceUpdateRequestSchema.safeParse({ phone: '1'.repeat(60) }).success).toBe(true);
expect(collectionPlaceUpdateRequestSchema.safeParse({ phone: '1'.repeat(61) }).success).toBe(false);
});
});
// The two ratchet schemas replace hand-rolled "array of numbers" checks — they
// must stay exactly that permissive (empty arrays included, no int/positive).
describe('collectionReorderRequestSchema / collectionDeleteManyRequestSchema', () => {
@@ -184,31 +184,10 @@ export const collectionSaveFromTripManyRequestSchema = z.object({
});
export type CollectionSaveFromTripManyRequest = z.infer<typeof collectionSaveFromTripManyRequestSchema>;
/**
* A saved place's currency. A collection has no base currency to fall back on,
* so a price always names its own: three letters, trimmed and upper-cased so
* 'eur ' lands as 'EUR'.
*/
export const collectionPlaceCurrencySchema = z
.string()
.trim()
.regex(/^[A-Za-z]{3}$/, { message: 'must be a three-letter ISO 4217 code' })
.toUpperCase();
export const collectionPlaceUpdateRequestSchema = z.object({
name: z.string().min(1).optional(),
description: z.string().nullable().optional(),
notes: z.string().nullable().optional(),
// A rough cost such as an entry fee (#2471). The columns were always there and
// travelled along on save and on copy into a trip, but nothing could edit them.
// Stricter than the save side on purpose (no negative amount, a real currency
// code); the edit forms only resend a field the user changed, so an older row
// never blocks an unrelated edit.
price: z.number().nonnegative().nullable().optional(),
currency: collectionPlaceCurrencySchema.nullable().optional(),
website: placeWebsiteSchema.nullable().optional(),
// Capped like the file import's phone, which is where most of them come from.
phone: z.string().trim().max(60).nullable().optional(),
// Editable coordinates so a place added by GPS can be corrected later (#1435).
lat: z.number().nullable().optional(),
lng: z.number().nullable().optional(),