- CatalogModule: VIN-less PL24 catalog browser (brands, models, categories, parts) - Supports P5 Modern (REST) and P4 Legacy (Ford, PSA) catalog architectures - Ford variant selector (model-year/engine/gearbox), PSA variant selector (body/engine/gearbox) - New API endpoints: ford-config, psa-bodies, psa-engines, psa-gearboxes, brands/:name/catalogs - Shared vehicles: vehicles table decoupled from users via userVehicles junction table - PL24 Ford Legacy service: comprehensive HTML-scraping for Ford/PSA/Hyundai/Kia/Nissan/Opel/Volvo - PL24 types and service updated for P4 Legacy brand support - Categories/parts service updated for dual FK (vehicleId + catalogVehicleId) pattern - Catalog browser frontend routes and components - docs/INDEX.md: updated with all new endpoints, components, hooks, routes (2026-03-02) - docs/pl24-catalog/: per-brand catalog exploration docs - scripts/migration-shared-vehicles.sql, pl24-catalog-explorer.js, posthog-dashboards.sh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
123 lines
4.0 KiB
PL/PgSQL
123 lines
4.0 KiB
PL/PgSQL
-- Migration: Shared Vehicle Config (car-config-centric architecture)
|
|
-- Converts per-user vehicle records to shared vehicle configs with junction table.
|
|
-- IMPORTANT: Take a DB backup before running. Execute inside a transaction.
|
|
|
|
BEGIN;
|
|
|
|
-- 1. Create user_vehicles junction table
|
|
CREATE TABLE user_vehicles (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
vehicle_id UUID NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
last_accessed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- 2. For each VIN, pick the canonical record (most recently updated)
|
|
-- and migrate all userId-vehicleId pairs into the junction table
|
|
INSERT INTO user_vehicles (user_id, vehicle_id, created_at, last_accessed_at)
|
|
SELECT
|
|
v.user_id,
|
|
canonical.id,
|
|
v.created_at,
|
|
v.updated_at
|
|
FROM vehicles v
|
|
JOIN (
|
|
SELECT DISTINCT ON (vin) id, vin
|
|
FROM vehicles
|
|
ORDER BY vin, updated_at DESC NULLS LAST
|
|
) canonical ON canonical.vin = v.vin;
|
|
|
|
-- 3. Delete categories on NON-canonical vehicles that already exist on the
|
|
-- canonical vehicle (same name+source), so the UPDATE in step 4 won't
|
|
-- hit the unique constraint.
|
|
WITH canonical AS (
|
|
SELECT DISTINCT ON (vin) id AS cid, vin
|
|
FROM vehicles ORDER BY vin, updated_at DESC NULLS LAST
|
|
)
|
|
DELETE FROM categories
|
|
WHERE id IN (
|
|
SELECT c_dup.id
|
|
FROM categories c_dup
|
|
JOIN vehicles v ON c_dup.vehicle_id = v.id
|
|
JOIN canonical can ON can.vin = v.vin
|
|
WHERE v.id != can.cid
|
|
AND EXISTS (
|
|
SELECT 1 FROM categories c_can
|
|
WHERE c_can.vehicle_id = can.cid
|
|
AND c_can.name = c_dup.name
|
|
AND c_can.source = c_dup.source
|
|
)
|
|
);
|
|
|
|
-- 4. Move remaining categories from non-canonical vehicles to canonical
|
|
WITH canonical AS (
|
|
SELECT DISTINCT ON (vin) id AS cid, vin
|
|
FROM vehicles ORDER BY vin, updated_at DESC NULLS LAST
|
|
)
|
|
UPDATE categories c
|
|
SET vehicle_id = can.cid
|
|
FROM vehicles v JOIN canonical can ON can.vin = v.vin
|
|
WHERE c.vehicle_id = v.id AND v.id != can.cid;
|
|
|
|
-- 5. Delete parts on non-canonical vehicles that would conflict after move
|
|
-- (parts don't have a unique constraint, but let's keep data clean by
|
|
-- removing duplicates — same oemCode+categoryId on canonical vehicle)
|
|
WITH canonical AS (
|
|
SELECT DISTINCT ON (vin) id AS cid, vin
|
|
FROM vehicles ORDER BY vin, updated_at DESC NULLS LAST
|
|
)
|
|
DELETE FROM parts
|
|
WHERE id IN (
|
|
SELECT p_dup.id
|
|
FROM parts p_dup
|
|
JOIN vehicles v ON p_dup.vehicle_id = v.id
|
|
JOIN canonical can ON can.vin = v.vin
|
|
WHERE v.id != can.cid
|
|
AND EXISTS (
|
|
SELECT 1 FROM parts p_can
|
|
WHERE p_can.vehicle_id = can.cid
|
|
AND p_can.category_id = p_dup.category_id
|
|
AND p_can.oem_code = p_dup.oem_code
|
|
)
|
|
);
|
|
|
|
-- 6. Move remaining parts from non-canonical vehicles to canonical
|
|
WITH canonical AS (
|
|
SELECT DISTINCT ON (vin) id AS cid, vin
|
|
FROM vehicles ORDER BY vin, updated_at DESC NULLS LAST
|
|
)
|
|
UPDATE parts p
|
|
SET vehicle_id = can.cid
|
|
FROM vehicles v JOIN canonical can ON can.vin = v.vin
|
|
WHERE p.vehicle_id = v.id AND v.id != can.cid;
|
|
|
|
-- 7. Delete non-canonical vehicle records (duplicates)
|
|
DELETE FROM vehicles WHERE id NOT IN (
|
|
SELECT DISTINCT ON (vin) id FROM vehicles
|
|
ORDER BY vin, updated_at DESC NULLS LAST
|
|
);
|
|
|
|
-- 8. Drop old user-specific indexes and column
|
|
DROP INDEX IF EXISTS vehicles_user_vin_idx;
|
|
DROP INDEX IF EXISTS vehicles_user_id_idx;
|
|
ALTER TABLE vehicles DROP COLUMN user_id;
|
|
|
|
-- 9. Add unique index on VIN (one record per VIN globally)
|
|
CREATE UNIQUE INDEX vehicles_vin_unique_idx ON vehicles (vin);
|
|
|
|
-- 10. Add FK and indexes on junction table
|
|
ALTER TABLE user_vehicles
|
|
ADD CONSTRAINT user_vehicles_vehicle_id_fkey
|
|
FOREIGN KEY (vehicle_id) REFERENCES vehicles(id) ON DELETE CASCADE;
|
|
CREATE UNIQUE INDEX user_vehicles_user_vehicle_idx ON user_vehicles (user_id, vehicle_id);
|
|
CREATE INDEX user_vehicles_user_id_idx ON user_vehicles (user_id);
|
|
|
|
-- 11. Drop unused vehicle_categories table
|
|
DROP TABLE IF EXISTS vehicle_categories;
|
|
|
|
-- Also drop the old vin-only index if it exists (replaced by unique)
|
|
DROP INDEX IF EXISTS vehicles_vin_idx;
|
|
|
|
COMMIT;
|