fix(vehicles): canonicalise brand casing (case-insensitive brand match)

Decode persist looked brands up with a case-sensitive eq(), so an
uppercase decode string ("FORD") missed canonical "Ford" → brand_id
NULL + raw uppercase stored as brand_name, splitting one brand across
casing variants in analytics/catalog. Now matches brands
case-insensitively and stores the canonical name. Migration
0013_fix_brand_casing backfills existing rows (60 on prod, 1 on dev).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:47:33 +03:00
parent 597391bbdf
commit f955bb7ef8
3 changed files with 43 additions and 2 deletions

View File

@@ -193,16 +193,22 @@ export class VehiclesService {
// 3. Brand access check
let brandId: string | null = null;
const brandName = resolved.brandName;
// Match the brands table case-insensitively and adopt the CANONICAL name:
// decode sources emit the brand with inconsistent casing ("FORD" vs "Ford"),
// and a case-sensitive lookup left brand_id NULL + stored the raw uppercase
// string, splitting one brand across casing variants. See migration
// 0013_fix_brand_casing for the backfill of pre-existing rows.
let brandName = resolved.brandName;
if (brandName) {
const [brand] = await this.db
.select()
.from(brands)
.where(eq(brands.name, brandName))
.where(sql`lower(${brands.name}) = lower(${brandName})`)
.limit(1);
if (brand) {
brandId = brand.id;
brandName = brand.name;
await this.checkBrandAccess(userId, brandId);
}
}