refactor(p): complete TecDoc → P content rename
Finishes 3b14431 (which only captured the file renames): apply the
identifier/endpoint/env/UI changes so the code matches the new paths
— PModule/PController/PSourceDbService, @Controller("p"), /p/oem,
config key `p`, P_DB_ENABLED/P_DB_URL, "P kataloğundan…" copy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@ import { DemoModule } from "./demo/demo.module";
|
||||
import { EmailModule } from "./email/email.module";
|
||||
import { HealthController } from "./health.controller";
|
||||
import { EmexModule } from "./integrations/emex/emex.module";
|
||||
import { TecdocModule } from "./integrations/tecdoc/tecdoc.module";
|
||||
import { PModule } from "./integrations/p/p.module";
|
||||
import { InternalAdminModule } from "./internal-admin/internal-admin.module";
|
||||
import { JobsModule } from "./jobs/jobs.module";
|
||||
import { MetaCapiModule } from "./meta-capi/meta-capi.module";
|
||||
@@ -87,7 +87,7 @@ import { VehiclesModule } from "./vehicles/vehicles.module";
|
||||
CategoriesModule,
|
||||
DemoModule,
|
||||
PartsModule,
|
||||
TecdocModule,
|
||||
PModule,
|
||||
JobsModule,
|
||||
EmexModule,
|
||||
TranslationsModule,
|
||||
|
||||
@@ -88,13 +88,13 @@ export default () => ({
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
},
|
||||
tecdoc: {
|
||||
// Read-only lookup against the imported TecDoc snapshot (db `td`). When
|
||||
p: {
|
||||
// Read-only lookup against the imported P snapshot (db `td`). When
|
||||
// enabled + url set, the OEM detail page resolves a part's OEM code to
|
||||
// TecDoc aftermarket equivalents + OE cross-references. Disabled → endpoint
|
||||
// P aftermarket equivalents + OE cross-references. Disabled → endpoint
|
||||
// returns { matched: false } and the UI shows an empty state.
|
||||
enabled: process.env.TECDOC_DB_ENABLED === "true",
|
||||
url: process.env.TECDOC_DB_URL,
|
||||
enabled: process.env.P_DB_ENABLED === "true",
|
||||
url: process.env.P_DB_URL,
|
||||
},
|
||||
otel: {
|
||||
enabled: process.env.OTEL_ENABLED === "true",
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
import { Injectable, Logger, type OnModuleDestroy, type OnModuleInit } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import postgres, { type Sql } from "postgres";
|
||||
import type {
|
||||
TecdocArticle,
|
||||
TecdocCompatible,
|
||||
TecdocOeNumber,
|
||||
TecdocOemResult,
|
||||
} from "./tecdoc.types";
|
||||
import type { PArticle, PCompatible, POeNumber, POemResult } from "./p.types";
|
||||
|
||||
/**
|
||||
* Read-only lookup against the imported TecDoc snapshot (db `td` — a selective
|
||||
* Read-only lookup against the imported P snapshot (db `td` — a selective
|
||||
* copy of articles + OE numbers + aftermarket compatibility + images/eans; the
|
||||
* 29 GB vehicle-fitment table is intentionally excluded). Given an OEM code from
|
||||
* the sase catalog, returns the TecDoc articles that carry it as an OE number,
|
||||
* the sase catalog, returns the P articles that carry it as an OE number,
|
||||
* with their aftermarket equivalents and OE cross-references.
|
||||
*
|
||||
* Matching is normalisation-based, not exact: TecDoc stores OE codes with
|
||||
* Matching is normalisation-based, not exact: P stores OE codes with
|
||||
* spaces/dashes (`1J0 973 702`) while the catalog gives `1J0973702`, so both
|
||||
* sides are reduced to `[A-Z0-9]` uppercase before comparison (a precomputed
|
||||
* `code_norm` column, indexed, holds the TecDoc side). Exact matching recovers
|
||||
* `code_norm` column, indexed, holds the P side). Exact matching recovers
|
||||
* almost nothing — verified ~1/10 vs normalised ~5/10 on real catalog codes.
|
||||
*
|
||||
* Never throws: disabled feature, too-short code, connection blip or no match
|
||||
* all collapse to `matched: false` so the UI has a single empty-state path.
|
||||
*/
|
||||
@Injectable()
|
||||
export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(TecdocSourceDbService.name);
|
||||
export class PSourceDbService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly logger = new Logger(PSourceDbService.name);
|
||||
private sql: Sql | null = null;
|
||||
private enabled = false;
|
||||
|
||||
@@ -39,10 +34,10 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy {
|
||||
constructor(private readonly config: ConfigService) {}
|
||||
|
||||
onModuleInit() {
|
||||
const enabled = this.config.get<boolean>("tecdoc.enabled");
|
||||
const url = this.config.get<string>("tecdoc.url");
|
||||
const enabled = this.config.get<boolean>("p.enabled");
|
||||
const url = this.config.get<string>("p.url");
|
||||
if (!enabled || !url) {
|
||||
this.logger.log(`[tecdoc] disabled (enabled=${enabled}, urlSet=${Boolean(url)})`);
|
||||
this.logger.log(`[p] disabled (enabled=${enabled}, urlSet=${Boolean(url)})`);
|
||||
return;
|
||||
}
|
||||
this.sql = postgres(url, {
|
||||
@@ -52,7 +47,7 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy {
|
||||
prepare: false,
|
||||
});
|
||||
this.enabled = true;
|
||||
this.logger.log("[tecdoc] connected, OEM cross-reference lookup enabled");
|
||||
this.logger.log("[p] connected, OEM cross-reference lookup enabled");
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
@@ -63,16 +58,16 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy {
|
||||
}
|
||||
|
||||
/** `1J0 973 702` / `1j0-973-702` → `1J0973702`. Used for both the input code
|
||||
* and JS-side dedupe; the TecDoc side is matched against the stored
|
||||
* and JS-side dedupe; the P side is matched against the stored
|
||||
* `code_norm` (built with the identical rule at import time). */
|
||||
private static norm(code: string): string {
|
||||
return code.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
||||
}
|
||||
|
||||
async lookupByOem(rawCode: string): Promise<TecdocOemResult | null> {
|
||||
async lookupByOem(rawCode: string): Promise<POemResult | null> {
|
||||
const query = (rawCode ?? "").trim();
|
||||
const queryNorm = TecdocSourceDbService.norm(query);
|
||||
const miss: TecdocOemResult = {
|
||||
const queryNorm = PSourceDbService.norm(query);
|
||||
const miss: POemResult = {
|
||||
query,
|
||||
queryNorm,
|
||||
matched: false,
|
||||
@@ -83,7 +78,7 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy {
|
||||
};
|
||||
|
||||
if (!this.enabled || !this.sql) return miss;
|
||||
if (queryNorm.length < TecdocSourceDbService.MIN_NORM_LEN) return miss;
|
||||
if (queryNorm.length < PSourceDbService.MIN_NORM_LEN) return miss;
|
||||
|
||||
try {
|
||||
const rows = await this.sql<
|
||||
@@ -93,8 +88,8 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy {
|
||||
article_number: string;
|
||||
name: string | null;
|
||||
spare_info: string | null;
|
||||
oe_numbers: TecdocOeNumber[];
|
||||
compatible: TecdocCompatible[];
|
||||
oe_numbers: POeNumber[];
|
||||
compatible: PCompatible[];
|
||||
images: Array<{ url: string; thumb: string | null }>;
|
||||
eans: string[];
|
||||
}>
|
||||
@@ -103,7 +98,7 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy {
|
||||
SELECT DISTINCT article_id
|
||||
FROM article_oe_numbers
|
||||
WHERE code_norm = ${queryNorm}
|
||||
LIMIT ${TecdocSourceDbService.MAX_ARTICLES}
|
||||
LIMIT ${PSourceDbService.MAX_ARTICLES}
|
||||
)
|
||||
SELECT
|
||||
a.id::text AS id,
|
||||
@@ -150,9 +145,9 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
if (rows.length === 0) return miss;
|
||||
|
||||
let truncated = rows.length >= TecdocSourceDbService.MAX_ARTICLES;
|
||||
let truncated = rows.length >= PSourceDbService.MAX_ARTICLES;
|
||||
|
||||
const articles: TecdocArticle[] = rows.map((r) => {
|
||||
const articles: PArticle[] = rows.map((r) => {
|
||||
if (r.oe_numbers.length >= 200 || r.compatible.length >= 200) truncated = true;
|
||||
return {
|
||||
id: r.id,
|
||||
@@ -171,12 +166,12 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy {
|
||||
// The matched articles are themselves aftermarket parts; their
|
||||
// compatibility rows add equivalent numbers from other supplier brands.
|
||||
const afterSeen = new Set<string>();
|
||||
const aftermarketParts: TecdocOemResult["aftermarketParts"] = [];
|
||||
const aftermarketParts: POemResult["aftermarketParts"] = [];
|
||||
const pushAfter = (brand: string, articleNumber: string, thumb: string | null) => {
|
||||
const key = `${brand.toUpperCase().trim()}␟${TecdocSourceDbService.norm(articleNumber)}`;
|
||||
const key = `${brand.toUpperCase().trim()}␟${PSourceDbService.norm(articleNumber)}`;
|
||||
if (afterSeen.has(key) || !articleNumber.trim()) return;
|
||||
afterSeen.add(key);
|
||||
if (aftermarketParts.length < TecdocSourceDbService.MAX_AGG) {
|
||||
if (aftermarketParts.length < PSourceDbService.MAX_AGG) {
|
||||
aftermarketParts.push({ brand, articleNumber, thumb });
|
||||
} else {
|
||||
truncated = true;
|
||||
@@ -192,15 +187,15 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy {
|
||||
// ── Aggregate: OE cross-references (same part, other makes) ───────────
|
||||
// Exclude restatements of the queried code itself (same normalised code).
|
||||
const oeSeen = new Set<string>();
|
||||
const oeCrossReferences: TecdocOeNumber[] = [];
|
||||
const oeCrossReferences: POeNumber[] = [];
|
||||
for (const a of articles) {
|
||||
for (const oe of a.oeNumbers) {
|
||||
const codeNorm = TecdocSourceDbService.norm(oe.code);
|
||||
const codeNorm = PSourceDbService.norm(oe.code);
|
||||
if (codeNorm === queryNorm) continue;
|
||||
const key = `${oe.brand.toUpperCase().trim()}␟${codeNorm}`;
|
||||
if (oeSeen.has(key)) continue;
|
||||
oeSeen.add(key);
|
||||
if (oeCrossReferences.length < TecdocSourceDbService.MAX_AGG) {
|
||||
if (oeCrossReferences.length < PSourceDbService.MAX_AGG) {
|
||||
oeCrossReferences.push(oe);
|
||||
} else {
|
||||
truncated = true;
|
||||
@@ -218,7 +213,7 @@ export class TecdocSourceDbService implements OnModuleInit, OnModuleDestroy {
|
||||
truncated,
|
||||
};
|
||||
} catch (err) {
|
||||
this.logger.warn(`[tecdoc] lookup failed (oem=${query}): ${(err as Error).message}`);
|
||||
this.logger.warn(`[p] lookup failed (oem=${query}): ${(err as Error).message}`);
|
||||
return miss;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Controller, Get, Query } from "@nestjs/common";
|
||||
import { TecdocSourceDbService } from "./tecdoc-source-db.service";
|
||||
import { PSourceDbService } from "./p-source-db.service";
|
||||
|
||||
@Controller("tecdoc")
|
||||
export class TecdocController {
|
||||
constructor(private readonly tecdoc: TecdocSourceDbService) {}
|
||||
@Controller("p")
|
||||
export class PController {
|
||||
constructor(private readonly p: PSourceDbService) {}
|
||||
|
||||
/**
|
||||
* Resolve an OEM code from the catalog to its TecDoc equivalents.
|
||||
* `GET /tecdoc/oem?code=1J0973702` → { matched, articles, aftermarketParts,
|
||||
* Resolve an OEM code from the catalog to its P equivalents.
|
||||
* `GET /p/oem?code=1J0973702` → { matched, articles, aftermarketParts,
|
||||
* oeCrossReferences }. Always 200 with `matched: false` on any miss.
|
||||
*/
|
||||
@Get("oem")
|
||||
async oem(@Query("code") code: string) {
|
||||
return this.tecdoc.lookupByOem(code ?? "");
|
||||
return this.p.lookupByOem(code ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { TecdocSourceDbService } from "./tecdoc-source-db.service";
|
||||
import { TecdocController } from "./tecdoc.controller";
|
||||
import { PSourceDbService } from "./p-source-db.service";
|
||||
import { PController } from "./p.controller";
|
||||
|
||||
/**
|
||||
* OEM cross-reference lookup against the imported TecDoc snapshot (db `td`).
|
||||
* OEM cross-reference lookup against the imported P snapshot (db `td`).
|
||||
* Raw read-only queries — intentionally no Drizzle schema modelling, mirroring
|
||||
* CatalogSourceDbModule. Self-disables when TECDOC_DB_* env is unset.
|
||||
* CatalogSourceDbModule. Self-disables when P_DB_* env is unset.
|
||||
*/
|
||||
@Module({
|
||||
controllers: [TecdocController],
|
||||
providers: [TecdocSourceDbService],
|
||||
exports: [TecdocSourceDbService],
|
||||
controllers: [PController],
|
||||
providers: [PSourceDbService],
|
||||
exports: [PSourceDbService],
|
||||
})
|
||||
export class TecdocModule {}
|
||||
export class PModule {}
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
/** An OE (original-equipment) number cross-reference: the same physical part as
|
||||
* catalogued by a vehicle manufacturer (e.g. VAG `1J0 973 702`). */
|
||||
export interface TecdocOeNumber {
|
||||
export interface POeNumber {
|
||||
brand: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
/** An aftermarket equivalent: a buyable part number from a supplier brand
|
||||
* (e.g. FEBI BILSTEIN `171903`). */
|
||||
export interface TecdocCompatible {
|
||||
export interface PCompatible {
|
||||
brand: string;
|
||||
article: string;
|
||||
}
|
||||
|
||||
export interface TecdocImage {
|
||||
export interface PImage {
|
||||
url: string;
|
||||
thumb: string | null;
|
||||
}
|
||||
|
||||
/** One TecDoc article whose OE number list contains the queried OEM code. */
|
||||
export interface TecdocArticle {
|
||||
/** One P article whose OE number list contains the queried OEM code. */
|
||||
export interface PArticle {
|
||||
id: string;
|
||||
brand: string;
|
||||
articleNumber: string;
|
||||
name: string | null;
|
||||
spareInfo: string | null;
|
||||
images: TecdocImage[];
|
||||
images: PImage[];
|
||||
eans: string[];
|
||||
oeNumbers: TecdocOeNumber[];
|
||||
compatible: TecdocCompatible[];
|
||||
oeNumbers: POeNumber[];
|
||||
compatible: PCompatible[];
|
||||
}
|
||||
|
||||
/** Response of the OEM detail lookup. `matched: false` covers every miss —
|
||||
* feature disabled, code too short, or no TecDoc article carries that OE
|
||||
* feature disabled, code too short, or no P article carries that OE
|
||||
* number — so the UI has a single empty-state path. */
|
||||
export interface TecdocOemResult {
|
||||
export interface POemResult {
|
||||
query: string;
|
||||
queryNorm: string;
|
||||
matched: boolean;
|
||||
/** Distinct articles whose OE list contains the queried code. */
|
||||
articles: TecdocArticle[];
|
||||
articles: PArticle[];
|
||||
/** Deduped buyable aftermarket part numbers across all matched articles
|
||||
* (the matched articles themselves + their compatibility entries). */
|
||||
aftermarketParts: Array<{ brand: string; articleNumber: string; thumb: string | null }>;
|
||||
/** Deduped OE cross-references across all matched articles, excluding the
|
||||
* queried code itself — i.e. the same part's numbers under other makes. */
|
||||
oeCrossReferences: TecdocOeNumber[];
|
||||
oeCrossReferences: POeNumber[];
|
||||
/** True when any per-article list or the article set hit its cap. */
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user