fix(budget): name the category parking and file parking bookings in it

The key was 'parkings', which is not a word and does not match the 'parking'
the reservation type uses everywhere else. The shared trip page prints the
stored category verbatim, so a share link would have shown "parkings" in every
language.

The category also had no relationship to the booking type of the same name:
RESERVATION_TYPE_TO_COST_CATEGORY still sent a parking booking's auto-expense
to Transport, so the new bucket sat empty next to the costs that belong in it
while a manually typed "parking" resolved into it. Both paths agree now.
This commit is contained in:
Maurice Böe
2026-08-21 14:14:23 +02:00
committed by Maurice
parent 999236f944
commit 942e1932c9
27 changed files with 53 additions and 35 deletions
@@ -1,9 +1,9 @@
import { describe, it, expect } from 'vitest' import { describe, it, expect } from 'vitest'
import { COST_CATEGORIES } from '@trek/shared' import { COST_CATEGORIES, typeToCostCategory } from '@trek/shared'
import { COST_CAT_META, COST_CATEGORY_LIST, catMeta } from './costsCategories' import { COST_CAT_META, COST_CATEGORY_LIST, catMeta } from './costsCategories'
describe('COST_CAT_META', () => { describe('COST_CAT_META', () => {
it('has metadata for every fixed category, including fuel and parkings', () => { it('has metadata for every fixed category, including fuel and parking', () => {
for (const key of COST_CATEGORIES) { for (const key of COST_CATEGORIES) {
expect(COST_CAT_META[key]).toBeDefined() expect(COST_CAT_META[key]).toBeDefined()
expect(COST_CAT_META[key].key).toBe(key) expect(COST_CAT_META[key].key).toBe(key)
@@ -16,15 +16,21 @@ describe('COST_CAT_META', () => {
}) })
describe('catMeta', () => { describe('catMeta', () => {
it('resolves fuel and parkings directly', () => { it('resolves fuel and parking directly', () => {
expect(catMeta('fuel').key).toBe('fuel') expect(catMeta('fuel').key).toBe('fuel')
expect(catMeta('parkings').key).toBe('parkings') expect(catMeta('parking').key).toBe('parking')
}) })
it('maps legacy/free-text aliases to the new categories', () => { it('maps legacy/free-text aliases to the new categories', () => {
expect(catMeta('gas').key).toBe('fuel') expect(catMeta('gas').key).toBe('fuel')
expect(catMeta('petrol').key).toBe('fuel') expect(catMeta('petrol').key).toBe('fuel')
expect(catMeta('parking').key).toBe('parkings') expect(catMeta('Parking').key).toBe('parking')
expect(catMeta('car park').key).toBe('parking')
})
it('resolves a parking booking the same way whichever path created the expense', () => {
expect(catMeta(typeToCostCategory('parking')).key).toBe('parking')
expect(catMeta('parking').key).toBe(typeToCostCategory('parking'))
}) })
it('falls back to other for unknown categories', () => { it('falls back to other for unknown categories', () => {
@@ -28,7 +28,7 @@ export const COST_CAT_META: Record<CostCategory, CostCategoryMeta> = {
health: { key: 'health', labelKey: 'costs.cat.health', Icon: HeartPulse, color: '#dc2626' }, health: { key: 'health', labelKey: 'costs.cat.health', Icon: HeartPulse, color: '#dc2626' },
tips: { key: 'tips', labelKey: 'costs.cat.tips', Icon: Coins, color: '#d97706' }, tips: { key: 'tips', labelKey: 'costs.cat.tips', Icon: Coins, color: '#d97706' },
fuel: { key: 'fuel', labelKey: 'costs.cat.fuel', Icon: Fuel, color: '#f59e0b' }, fuel: { key: 'fuel', labelKey: 'costs.cat.fuel', Icon: Fuel, color: '#f59e0b' },
parkings: { key: 'parkings', labelKey: 'costs.cat.parkings', Icon: ParkingCircle, color: '#3b82f6' }, parking: { key: 'parking', labelKey: 'costs.cat.parking', Icon: ParkingCircle, color: '#3b82f6' },
other: { key: 'other', labelKey: 'costs.cat.other', Icon: MoreHorizontal, color: '#6b7280' }, other: { key: 'other', labelKey: 'costs.cat.other', Icon: MoreHorizontal, color: '#6b7280' },
} }
@@ -54,7 +54,7 @@ const LEGACY_CATEGORY_MAP: Record<string, CostCategory> = {
health: 'health', medical: 'health', health: 'health', medical: 'health',
tip: 'tips', tips: 'tips', tip: 'tips', tips: 'tips',
gas: 'fuel', fuel: 'fuel', petrol: 'fuel', gas: 'fuel', fuel: 'fuel', petrol: 'fuel',
parking: 'parkings', parkings: 'parkings', parking: 'parking', parkings: 'parking', 'car park': 'parking',
other: 'other', misc: 'other', other: 'other', misc: 'other',
} }
+14 -2
View File
@@ -4,6 +4,7 @@ import {
budgetToggleMemberPaidRequestSchema, budgetToggleMemberPaidRequestSchema,
budgetReorderItemsRequestSchema, budgetReorderItemsRequestSchema,
COST_CATEGORIES, COST_CATEGORIES,
typeToCostCategory,
} from './budget.schema'; } from './budget.schema';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
@@ -44,8 +45,19 @@ describe('budgetReorderItemsRequestSchema', () => {
}); });
describe('COST_CATEGORIES', () => { describe('COST_CATEGORIES', () => {
it('includes fuel and parkings alongside the existing fixed categories', () => { it('includes fuel and parking alongside the existing fixed categories', () => {
expect(COST_CATEGORIES).toContain('fuel'); expect(COST_CATEGORIES).toContain('fuel');
expect(COST_CATEGORIES).toContain('parkings'); expect(COST_CATEGORIES).toContain('parking');
});
});
describe('typeToCostCategory', () => {
it('files a parking booking under parking, not transport', () => {
expect(typeToCostCategory('parking')).toBe('parking');
});
it('leaves the other vehicle types on transport', () => {
expect(typeToCostCategory('car-rental')).toBe('transport');
expect(typeToCostCategory('taxi')).toBe('transport');
}); });
}); });
+2 -2
View File
@@ -47,7 +47,7 @@ export const COST_CATEGORIES = [
'health', 'health',
'tips', 'tips',
'fuel', 'fuel',
'parkings', 'parking',
'other', 'other',
] as const; ] as const;
export type CostCategory = (typeof COST_CATEGORIES)[number]; export type CostCategory = (typeof COST_CATEGORIES)[number];
@@ -72,7 +72,7 @@ const RESERVATION_TYPE_TO_COST_CATEGORY: Record<string, CostCategory> = {
hotel: 'accommodation', hotel: 'accommodation',
accommodation: 'accommodation', accommodation: 'accommodation',
lodging: 'accommodation', lodging: 'accommodation',
parking: 'transport', parking: 'parking',
restaurant: 'food', restaurant: 'food',
activity: 'activities', activity: 'activities',
}; };
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'الصحة', 'costs.cat.health': 'الصحة',
'costs.cat.tips': 'البقشيش', 'costs.cat.tips': 'البقشيش',
'costs.cat.fuel': 'وقود', 'costs.cat.fuel': 'وقود',
'costs.cat.parkings': 'موقف سيارات', 'costs.cat.parking': 'موقف سيارات',
'costs.cat.other': 'أخرى', 'costs.cat.other': 'أخرى',
'costs.daysCount': '{count} أيام', 'costs.daysCount': '{count} أيام',
'costs.travelers': '{count} مسافرين', 'costs.travelers': '{count} مسافرين',
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Saúde', 'costs.cat.health': 'Saúde',
'costs.cat.tips': 'Gorjetas', 'costs.cat.tips': 'Gorjetas',
'costs.cat.fuel': 'Combustível', 'costs.cat.fuel': 'Combustível',
'costs.cat.parkings': 'Estacionamento', 'costs.cat.parking': 'Estacionamento',
'costs.cat.other': 'Outros', 'costs.cat.other': 'Outros',
'costs.daysCount': '{count} dias', 'costs.daysCount': '{count} dias',
'costs.travelers': '{count} viajantes', 'costs.travelers': '{count} viajantes',
+1 -1
View File
@@ -117,7 +117,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Salut', 'costs.cat.health': 'Salut',
'costs.cat.tips': 'Propines', 'costs.cat.tips': 'Propines',
'costs.cat.fuel': 'Combustible', 'costs.cat.fuel': 'Combustible',
'costs.cat.parkings': 'Aparcament', 'costs.cat.parking': 'Aparcament',
'costs.cat.other': 'Altres', 'costs.cat.other': 'Altres',
'costs.daysCount': '{count} dies', 'costs.daysCount': '{count} dies',
'costs.travelers': '{count} viatgers', 'costs.travelers': '{count} viatgers',
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Zdraví', 'costs.cat.health': 'Zdraví',
'costs.cat.tips': 'Spropitné', 'costs.cat.tips': 'Spropitné',
'costs.cat.fuel': 'Palivo', 'costs.cat.fuel': 'Palivo',
'costs.cat.parkings': 'Parkování', 'costs.cat.parking': 'Parkování',
'costs.cat.other': 'Ostatní', 'costs.cat.other': 'Ostatní',
'costs.daysCount': '{count} dní', 'costs.daysCount': '{count} dní',
'costs.travelers': '{count} cestovatelů', 'costs.travelers': '{count} cestovatelů',
+1 -1
View File
@@ -121,7 +121,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Gesundheit', 'costs.cat.health': 'Gesundheit',
'costs.cat.tips': 'Trinkgeld', 'costs.cat.tips': 'Trinkgeld',
'costs.cat.fuel': 'Kraftstoff', 'costs.cat.fuel': 'Kraftstoff',
'costs.cat.parkings': 'Parken', 'costs.cat.parking': 'Parken',
'costs.cat.other': 'Sonstiges', 'costs.cat.other': 'Sonstiges',
'costs.daysCount': '{count} Tage', 'costs.daysCount': '{count} Tage',
'costs.travelers': '{count} Reisende', 'costs.travelers': '{count} Reisende',
+1 -1
View File
@@ -125,7 +125,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Health', 'costs.cat.health': 'Health',
'costs.cat.tips': 'Tips', 'costs.cat.tips': 'Tips',
'costs.cat.fuel': 'Fuel', 'costs.cat.fuel': 'Fuel',
'costs.cat.parkings': 'Parking', 'costs.cat.parking': 'Parking',
'costs.cat.other': 'Other', 'costs.cat.other': 'Other',
'costs.daysCount': '{count} days', 'costs.daysCount': '{count} days',
'costs.travelers': '{count} travelers', 'costs.travelers': '{count} travelers',
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Salud', 'costs.cat.health': 'Salud',
'costs.cat.tips': 'Propinas', 'costs.cat.tips': 'Propinas',
'costs.cat.fuel': 'Combustible', 'costs.cat.fuel': 'Combustible',
'costs.cat.parkings': 'Aparcamiento', 'costs.cat.parking': 'Aparcamiento',
'costs.cat.other': 'Otros', 'costs.cat.other': 'Otros',
'costs.daysCount': '{count} días', 'costs.daysCount': '{count} días',
'costs.travelers': '{count} viajeros', 'costs.travelers': '{count} viajeros',
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Santé', 'costs.cat.health': 'Santé',
'costs.cat.tips': 'Pourboires', 'costs.cat.tips': 'Pourboires',
'costs.cat.fuel': 'Carburant', 'costs.cat.fuel': 'Carburant',
'costs.cat.parkings': 'Parking', 'costs.cat.parking': 'Parking',
'costs.cat.other': 'Autre', 'costs.cat.other': 'Autre',
'costs.daysCount': '{count} jours', 'costs.daysCount': '{count} jours',
'costs.travelers': '{count} voyageurs', 'costs.travelers': '{count} voyageurs',
+1 -1
View File
@@ -119,7 +119,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Υγεία', 'costs.cat.health': 'Υγεία',
'costs.cat.tips': 'Φιλοδωρήματα', 'costs.cat.tips': 'Φιλοδωρήματα',
'costs.cat.fuel': 'Καύσιμα', 'costs.cat.fuel': 'Καύσιμα',
'costs.cat.parkings': 'Στάθμευση', 'costs.cat.parking': 'Στάθμευση',
'costs.cat.other': 'Άλλα', 'costs.cat.other': 'Άλλα',
'costs.daysCount': '{count} ημέρες', 'costs.daysCount': '{count} ημέρες',
'costs.travelers': '{count} ταξιδιώτες', 'costs.travelers': '{count} ταξιδιώτες',
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Egészség', 'costs.cat.health': 'Egészség',
'costs.cat.tips': 'Borravaló', 'costs.cat.tips': 'Borravaló',
'costs.cat.fuel': 'Üzemanyag', 'costs.cat.fuel': 'Üzemanyag',
'costs.cat.parkings': 'Parkolás', 'costs.cat.parking': 'Parkolás',
'costs.cat.other': 'Egyéb', 'costs.cat.other': 'Egyéb',
'costs.daysCount': '{count} nap', 'costs.daysCount': '{count} nap',
'costs.travelers': '{count} utazó', 'costs.travelers': '{count} utazó',
+2 -2
View File
@@ -117,8 +117,8 @@ const budget: TranslationStrings = {
'costs.cat.fees': 'Biaya & tiket', 'costs.cat.fees': 'Biaya & tiket',
'costs.cat.health': 'Kesehatan', 'costs.cat.health': 'Kesehatan',
'costs.cat.tips': 'Tip', 'costs.cat.tips': 'Tip',
'costs.cat.fuel': 'Bahan Bakar', 'costs.cat.fuel': 'Bahan bakar',
'costs.cat.parkings': 'Parkir', 'costs.cat.parking': 'Parkir',
'costs.cat.other': 'Lainnya', 'costs.cat.other': 'Lainnya',
'costs.daysCount': '{count} hari', 'costs.daysCount': '{count} hari',
'costs.travelers': '{count} pelancong', 'costs.travelers': '{count} pelancong',
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Salute', 'costs.cat.health': 'Salute',
'costs.cat.tips': 'Mance', 'costs.cat.tips': 'Mance',
'costs.cat.fuel': 'Carburante', 'costs.cat.fuel': 'Carburante',
'costs.cat.parkings': 'Parcheggio', 'costs.cat.parking': 'Parcheggio',
'costs.cat.other': 'Altro', 'costs.cat.other': 'Altro',
'costs.daysCount': '{count} giorni', 'costs.daysCount': '{count} giorni',
'costs.travelers': '{count} viaggiatori', 'costs.travelers': '{count} viaggiatori',
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': '健康', 'costs.cat.health': '健康',
'costs.cat.tips': 'チップ', 'costs.cat.tips': 'チップ',
'costs.cat.fuel': '燃料', 'costs.cat.fuel': '燃料',
'costs.cat.parkings': '駐車場', 'costs.cat.parking': '駐車場',
'costs.cat.other': 'その他', 'costs.cat.other': 'その他',
'costs.daysCount': '{count}日間', 'costs.daysCount': '{count}日間',
'costs.travelers': '{count}人の旅行者', 'costs.travelers': '{count}人の旅行者',
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': '건강', 'costs.cat.health': '건강',
'costs.cat.tips': '팁', 'costs.cat.tips': '팁',
'costs.cat.fuel': '연료', 'costs.cat.fuel': '연료',
'costs.cat.parkings': '주차', 'costs.cat.parking': '주차',
'costs.cat.other': '기타', 'costs.cat.other': '기타',
'costs.daysCount': '{count}일', 'costs.daysCount': '{count}일',
'costs.travelers': '여행자 {count}명', 'costs.travelers': '여행자 {count}명',
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Gezondheid', 'costs.cat.health': 'Gezondheid',
'costs.cat.tips': 'Fooien', 'costs.cat.tips': 'Fooien',
'costs.cat.fuel': 'Brandstof', 'costs.cat.fuel': 'Brandstof',
'costs.cat.parkings': 'Parkeren', 'costs.cat.parking': 'Parkeren',
'costs.cat.other': 'Overig', 'costs.cat.other': 'Overig',
'costs.daysCount': '{count} dagen', 'costs.daysCount': '{count} dagen',
'costs.travelers': '{count} reizigers', 'costs.travelers': '{count} reizigers',
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Zdrowie', 'costs.cat.health': 'Zdrowie',
'costs.cat.tips': 'Napiwki', 'costs.cat.tips': 'Napiwki',
'costs.cat.fuel': 'Paliwo', 'costs.cat.fuel': 'Paliwo',
'costs.cat.parkings': 'Parking', 'costs.cat.parking': 'Parking',
'costs.cat.other': 'Inne', 'costs.cat.other': 'Inne',
'costs.daysCount': 'Dni: {count}', 'costs.daysCount': 'Dni: {count}',
'costs.travelers': 'Podróżnych: {count}', 'costs.travelers': 'Podróżnych: {count}',
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Здоровье', 'costs.cat.health': 'Здоровье',
'costs.cat.tips': 'Чаевые', 'costs.cat.tips': 'Чаевые',
'costs.cat.fuel': 'Топливо', 'costs.cat.fuel': 'Топливо',
'costs.cat.parkings': 'Парковка', 'costs.cat.parking': 'Парковка',
'costs.cat.other': 'Прочее', 'costs.cat.other': 'Прочее',
'costs.daysCount': '{count} дней', 'costs.daysCount': '{count} дней',
'costs.travelers': '{count} путешественников', 'costs.travelers': '{count} путешественников',
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Hälsa', 'costs.cat.health': 'Hälsa',
'costs.cat.tips': 'Dricks', 'costs.cat.tips': 'Dricks',
'costs.cat.fuel': 'Bränsle', 'costs.cat.fuel': 'Bränsle',
'costs.cat.parkings': 'Parkering', 'costs.cat.parking': 'Parkering',
'costs.cat.other': 'Annat', 'costs.cat.other': 'Annat',
'costs.daysCount': '{count} dagar', 'costs.daysCount': '{count} dagar',
'costs.travelers': '{count} resenärer', 'costs.travelers': '{count} resenärer',
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Sağlık', 'costs.cat.health': 'Sağlık',
'costs.cat.tips': 'Bahşişler', 'costs.cat.tips': 'Bahşişler',
'costs.cat.fuel': 'Yakıt', 'costs.cat.fuel': 'Yakıt',
'costs.cat.parkings': 'Otopark', 'costs.cat.parking': 'Otopark',
'costs.cat.other': 'Diğer', 'costs.cat.other': 'Diğer',
'costs.daysCount': '{count} gün', 'costs.daysCount': '{count} gün',
'costs.travelers': '{count} yolcu', 'costs.travelers': '{count} yolcu',
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Здоров’я', 'costs.cat.health': 'Здоров’я',
'costs.cat.tips': 'Чайові', 'costs.cat.tips': 'Чайові',
'costs.cat.fuel': 'Пальне', 'costs.cat.fuel': 'Пальне',
'costs.cat.parkings': 'Паркування', 'costs.cat.parking': 'Паркування',
'costs.cat.other': 'Інше', 'costs.cat.other': 'Інше',
'costs.daysCount': '{count} днів', 'costs.daysCount': '{count} днів',
'costs.travelers': '{count} мандрівників', 'costs.travelers': '{count} мандрівників',
+1 -1
View File
@@ -118,7 +118,7 @@ const budget: TranslationStrings = {
'costs.cat.health': 'Sức khỏe', 'costs.cat.health': 'Sức khỏe',
'costs.cat.tips': 'Mẹo', 'costs.cat.tips': 'Mẹo',
'costs.cat.fuel': 'Nhiên liệu', 'costs.cat.fuel': 'Nhiên liệu',
'costs.cat.parkings': 'Bãi đỗ xe', 'costs.cat.parking': 'Bãi đỗ xe',
'costs.cat.other': 'Khác', 'costs.cat.other': 'Khác',
'costs.daysCount': '{count} ngày', 'costs.daysCount': '{count} ngày',
'costs.travelers': '{count} người', 'costs.travelers': '{count} người',
+1 -1
View File
@@ -117,7 +117,7 @@ const budget: TranslationStrings = {
'costs.cat.health': '健康', 'costs.cat.health': '健康',
'costs.cat.tips': '小費', 'costs.cat.tips': '小費',
'costs.cat.fuel': '燃油', 'costs.cat.fuel': '燃油',
'costs.cat.parkings': '停車', 'costs.cat.parking': '停車',
'costs.cat.other': '其他', 'costs.cat.other': '其他',
'costs.daysCount': '{count} 天', 'costs.daysCount': '{count} 天',
'costs.travelers': '{count} 位旅伴', 'costs.travelers': '{count} 位旅伴',
+1 -1
View File
@@ -117,7 +117,7 @@ const budget: TranslationStrings = {
'costs.cat.health': '健康', 'costs.cat.health': '健康',
'costs.cat.tips': '小费', 'costs.cat.tips': '小费',
'costs.cat.fuel': '燃油', 'costs.cat.fuel': '燃油',
'costs.cat.parkings': '停车', 'costs.cat.parking': '停车',
'costs.cat.other': '其他', 'costs.cat.other': '其他',
'costs.daysCount': '{count} 天', 'costs.daysCount': '{count} 天',
'costs.travelers': '{count} 位同行者', 'costs.travelers': '{count} 位同行者',