docs: update INDEX.md + add catalog module, Ford/PSA legacy catalog, shared vehicles

- 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>
This commit is contained in:
Sase Dev
2026-03-02 00:31:15 +00:00
parent f2189a014b
commit 72c0de65de
90 changed files with 12043 additions and 419 deletions

View File

@@ -231,6 +231,36 @@ export const oemCodeCopies = pgTable(
],
);
// ─── Catalog Vehicles (VIN-less catalog browse — one record per PL24 vehicleId) ──
export const catalogVehicles = pgTable(
"catalog_vehicles",
{
id: uuid("id").primaryKey().defaultRandom(),
source: varchar("source", { length: 20 }).default("pl24").notNull(),
serviceName: varchar("service_name", { length: 100 }).notNull(),
brandName: varchar("brand_name", { length: 100 }).notNull(),
brandId: uuid("brand_id").references(() => brands.id),
model: varchar("model", { length: 255 }).notNull(),
year: varchar("year", { length: 50 }),
engine: varchar("engine", { length: 255 }),
bodyType: varchar("body_type", { length: 100 }),
transmission: varchar("transmission", { length: 100 }),
market: varchar("market", { length: 100 }),
serviceVehicleId: varchar("service_vehicle_id", { length: 255 }).notNull(),
catalogPath: text("catalog_path"),
architecture: varchar("architecture", { length: 30 }),
metadata: jsonb("metadata"),
categoriesFetched: boolean("categories_fetched").default(false).notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
uniqueIndex("catalog_vehicles_source_vid_idx").on(table.source, table.serviceVehicleId),
index("catalog_vehicles_brand_name_idx").on(table.brandName),
index("catalog_vehicles_service_name_idx").on(table.serviceName),
],
);
// ─── Vehicles (shared config — one record per VIN) ──
export const vehicles = pgTable(
"vehicles",
@@ -280,13 +310,12 @@ export const categories = pgTable(
"categories",
{
id: uuid("id").primaryKey().defaultRandom(),
vehicleId: uuid("vehicle_id")
.notNull()
.references(() => vehicles.id, { onDelete: "cascade" }),
vehicleId: uuid("vehicle_id").references(() => vehicles.id, { onDelete: "cascade" }),
catalogVehicleId: uuid("catalog_vehicle_id").references(() => catalogVehicles.id, { onDelete: "cascade" }),
name: varchar("name", { length: 500 }).notNull(),
nameOriginal: varchar("name_original", { length: 500 }),
parentId: uuid("parent_id"),
externalId: varchar("external_id", { length: 500 }),
externalId: text("external_id"),
linkPath: text("link_path"),
linkWid: varchar("link_wid", { length: 100 }),
unavailable: boolean("unavailable").default(false).notNull(),
@@ -295,8 +324,9 @@ export const categories = pgTable(
},
(table) => [
index("categories_vehicle_id_idx").on(table.vehicleId),
index("categories_catalog_vehicle_id_idx").on(table.catalogVehicleId),
index("categories_parent_id_idx").on(table.parentId),
uniqueIndex("categories_vehicle_name_source_idx").on(table.vehicleId, table.name, table.source),
uniqueIndex("categories_vehicle_name_source_idx").on(table.vehicleId, table.catalogVehicleId, table.name, table.source),
],
);
@@ -305,9 +335,8 @@ export const parts = pgTable(
"parts",
{
id: uuid("id").primaryKey().defaultRandom(),
vehicleId: uuid("vehicle_id")
.notNull()
.references(() => vehicles.id, { onDelete: "cascade" }),
vehicleId: uuid("vehicle_id").references(() => vehicles.id, { onDelete: "cascade" }),
catalogVehicleId: uuid("catalog_vehicle_id").references(() => catalogVehicles.id, { onDelete: "cascade" }),
categoryId: uuid("category_id")
.notNull()
.references(() => categories.id, { onDelete: "cascade" }),
@@ -327,6 +356,7 @@ export const parts = pgTable(
},
(table) => [
index("parts_vehicle_id_idx").on(table.vehicleId),
index("parts_catalog_vehicle_id_idx").on(table.catalogVehicleId),
index("parts_category_id_idx").on(table.categoryId),
index("parts_oem_code_idx").on(table.oemCode),
],

View File

@@ -15,6 +15,7 @@ import {
parts,
schemaPics,
referrals,
catalogVehicles,
} from "./core";
export const usersRelations = relations(users, ({ many }) => ({
@@ -59,6 +60,13 @@ export const userBrandsRelations = relations(userBrands, ({ one }) => ({
export const brandsRelations = relations(brands, ({ many }) => ({
userBrands: many(userBrands),
catalogVehicles: many(catalogVehicles),
}));
export const catalogVehiclesRelations = relations(catalogVehicles, ({ one, many }) => ({
brand: one(brands, { fields: [catalogVehicles.brandId], references: [brands.id] }),
categories: many(categories),
parts: many(parts),
}));
export const paymentsRelations = relations(payments, ({ one }) => ({
@@ -88,6 +96,7 @@ export const userVehiclesRelations = relations(userVehicles, ({ one }) => ({
export const categoriesRelations = relations(categories, ({ one, many }) => ({
vehicle: one(vehicles, { fields: [categories.vehicleId], references: [vehicles.id] }),
catalogVehicle: one(catalogVehicles, { fields: [categories.catalogVehicleId], references: [catalogVehicles.id] }),
parent: one(categories, {
fields: [categories.parentId],
references: [categories.id],
@@ -100,6 +109,7 @@ export const categoriesRelations = relations(categories, ({ one, many }) => ({
export const partsRelations = relations(parts, ({ one }) => ({
vehicle: one(vehicles, { fields: [parts.vehicleId], references: [vehicles.id] }),
catalogVehicle: one(catalogVehicles, { fields: [parts.catalogVehicleId], references: [catalogVehicles.id] }),
category: one(categories, { fields: [parts.categoryId], references: [categories.id] }),
}));

View File

@@ -4,33 +4,43 @@ import postgres from "postgres";
import { brands, plans, users, accounts } from "./schema/core";
const BRANDS_DATA = [
{ name: "BMW", slug: "bmw" },
{ name: "Mercedes-Benz", slug: "mercedes-benz" },
// Mevcut markalar
{ name: "Audi", slug: "audi" },
{ name: "Volkswagen", slug: "volkswagen" },
{ name: "Fiat", slug: "fiat" },
{ name: "Renault", slug: "renault" },
{ name: "Peugeot", slug: "peugeot" },
{ name: "BMW", slug: "bmw" },
{ name: "Citroen", slug: "citroen" },
{ name: "Toyota", slug: "toyota" },
{ name: "Dacia", slug: "dacia" },
{ name: "Fiat", slug: "fiat" },
{ name: "Ford", slug: "ford" },
{ name: "Honda", slug: "honda" },
{ name: "Hyundai", slug: "hyundai" },
{ name: "Kia", slug: "kia" },
{ name: "Ford", slug: "ford" },
{ name: "Opel", slug: "opel" },
{ name: "Skoda", slug: "skoda" },
{ name: "Seat", slug: "seat" },
{ name: "Volvo", slug: "volvo" },
{ name: "Nissan", slug: "nissan" },
{ name: "Mazda", slug: "mazda" },
{ name: "Porsche", slug: "porsche" },
{ name: "Land Rover", slug: "land-rover" },
{ name: "Jaguar", slug: "jaguar" },
{ name: "Kia", slug: "kia" },
{ name: "Land Rover", slug: "land-rover" },
{ name: "Mazda", slug: "mazda" },
{ name: "Mercedes-Benz", slug: "mercedes-benz" },
{ name: "Mini", slug: "mini" },
{ name: "Dacia", slug: "dacia" },
{ name: "Mitsubishi", slug: "mitsubishi" },
{ name: "Nissan", slug: "nissan" },
{ name: "Opel", slug: "opel" },
{ name: "Peugeot", slug: "peugeot" },
{ name: "Porsche", slug: "porsche" },
{ name: "Renault", slug: "renault" },
{ name: "Seat", slug: "seat" },
{ name: "Skoda", slug: "skoda" },
{ name: "Subaru", slug: "subaru" },
{ name: "Suzuki", slug: "suzuki" },
{ name: "Mitsubishi", slug: "mitsubishi" },
{ name: "Toyota", slug: "toyota" },
{ name: "Volkswagen", slug: "volkswagen" },
{ name: "Volvo", slug: "volvo" },
// PL24 kataloglarında olan, sase.tr'ye eklenen markalar
{ name: "Alpine", slug: "alpine" },
{ name: "Bentley", slug: "bentley" },
{ name: "Cupra", slug: "cupra" },
{ name: "Infiniti", slug: "infiniti" },
{ name: "Lexus", slug: "lexus" },
{ name: "MAN", slug: "man" },
{ name: "Polestar", slug: "polestar" },
{ name: "Smart", slug: "smart" },
];
const PLANS_DATA = [