CROP Catalog Architecture Refactoring
> Date: 2026-01-23 > Status: Planning > Author: Claude + Vova
CROP Catalog Architecture Refactoring
Date: 2026-01-23 Status: Planning Author: Claude + Vova
Executive Summary
This document outlines the architectural refactoring needed to properly separate parts and tires in the CROP catalog system, fix terminology issues, and ensure clean data model.
1. Current State Analysis
What Works Well
✅ Two separate ES indices (parts_current, tires_current)
✅ Smart index resolver with automatic routing
✅ K&M Tires isolated in separate collection (parts_kmt)
✅ Search works across both indices
✅ Category hierarchy for partsCritical Issues
❌ KMT called "manufacturer" but it's a reseller/vendor
❌ Catalog page shows ALL products including tires
❌ No explicit productType field
❌ Inconsistent category structure (hierarchical vs flat)
❌ manufacturer field overloaded (means different things)2. Terminology Clarification
Current (Confused)
| Term | Current Usage | Problem |
|---|---|---|
manufacturer | KMT, NHL, BNS, KUH... | KMT is NOT a manufacturer |
vendorCode | Same as manufacturer | Duplicate concept |
category | "parts", "Wheels & Tires" | No productType distinction |
Proposed (Clear)
| Term | Definition | Examples |
|---|---|---|
productType | High-level product classification | part, tire, accessory |
manufacturer | Company that MAKES the product | New Holland, Kuhn, Bridgestone |
vendor | Company we BUY from (supplier/reseller) | K&M Tire, Clinton Tractor DIS |
brandCode | Our internal code for the brand | NHL, KUH, KMT, BNS |
category | Product category hierarchy | Filters > Oil Filters |
Entity Relationships
┌─────────────────────────────────────────────────────────────────┐
│ PRODUCT │
├─────────────────────────────────────────────────────────────────┤
│ productType: 'part' | 'tire' | 'accessory' │
│ │
│ brand: { // Who MAKES it │
│ code: string // NHL, KUH, BRG (Bridgestone) │
│ name: string // New Holland, Kuhn, Bridgestone │
│ type: 'oem' | 'aftermarket' │
│ } │
│ │
│ vendor: { // Who we BUY from │
│ code: string // DIS, KMT │
│ name: string // Clinton Tractor DIS, K&M Tire │
│ type: 'dealer' | 'distributor' | 'reseller' │
│ } │
│ │
│ category: { // Product classification │
│ path: string[] // ['Filters', 'Oil Filters'] │
│ leaf: string // 'Oil Filters' │
│ } │
└─────────────────────────────────────────────────────────────────┘3. Proposed Architecture
3.1 Data Model
// packages/shared-catalog/src/types/product.ts
/**
* Product type - highest level classification
*/
export type ProductType = 'part' | 'tire' | 'accessory';
/**
* Brand - the company that MANUFACTURES the product
*/
export interface Brand {
code: string; // NHL, KUH, BRG, MCH, etc.
name: string; // New Holland, Kuhn, Bridgestone
slug: string; // new-holland, kuhn, bridgestone
type: 'oem' | 'aftermarket' | 'private-label';
}
/**
* Vendor - the company we PURCHASE from
*/
export interface Vendor {
code: string; // DIS, KMT
name: string; // Clinton Tractor DIS, K&M Tire
type: 'dealer' | 'distributor' | 'wholesaler';
}
/**
* Category - product classification hierarchy
*/
export interface Category {
id: string;
name: string;
slug: string;
path: string[]; // ['Filters', 'Oil Filters']
pathString: string; // 'Filters > Oil Filters'
level: number;
isLeaf: boolean;
}
/**
* Unified Product Schema
*/
export interface Product {
// Identity
id: string;
sku: string;
partNumber: string;
slug: string;
// Classification (NEW)
productType: ProductType;
// Brand & Vendor (REFACTORED)
brand: Brand; // Who makes it
vendor: Vendor; // Who we buy from
// Legacy compatibility (deprecated, remove later)
/** @deprecated Use brand.code instead */
manufacturer?: { code: string; name: string };
// Content
title: string;
description: string;
// Category
category: Category;
categories: Category[]; // Multiple categories allowed
// Pricing & Inventory
pricing: ProductPricing;
inventory: ProductInventory;
// Media
media: ProductMedia;
// Tire-specific (only for productType='tire')
tireSpecs?: TireSpecifications;
// Part-specific (only for productType='part')
partSpecs?: PartSpecifications;
// Metadata
status: 'active' | 'discontinued' | 'superseded';
createdAt: Date;
updatedAt: Date;
}
/**
* Tire-specific specifications
*/
export interface TireSpecifications {
tireSize: string; // "225/65R17"
width: number; // 225
aspectRatio: number; // 65
rimDiameter: number; // 17
loadIndex?: number;
speedRating?: string;
tireBrand: string; // Bridgestone, Michelin (actual tire manufacturer)
}
/**
* Part-specific specifications
*/
export interface PartSpecifications {
weight?: { value: number; unit: string };
dimensions?: { l: number; w: number; h: number; unit: string };
equipmentFitment?: string[];
crossReferences?: CrossReference[];
}3.2 Vendor Configuration
// packages/shared-catalog/src/config/vendors.ts
export const VENDORS = {
// Clinton Tractor DIS - Dealer Information System
DIS: {
code: 'DIS',
name: 'Clinton Tractor DIS',
type: 'dealer',
description: 'Primary dealer inventory system',
brands: ['NHL', 'KUH', 'BNS', 'VNT', 'HOT', 'HAR', 'MAR', 'KIN', 'MCH'],
},
// K&M Tire - Wholesale tire distributor
KMT: {
code: 'KMT',
name: 'K&M Tire',
type: 'wholesaler',
description: 'Wholesale tire distributor',
brands: ['BRG', 'FST', 'GYR', 'MCH', 'CPR'], // Bridgestone, Firestone, Goodyear, Michelin, Cooper
},
} as const;
export const BRANDS = {
// DIS Brands (Parts)
NHL: { code: 'NHL', name: 'New Holland', type: 'oem' },
KUH: { code: 'KUH', name: 'Kuhn', type: 'oem' },
BNS: { code: 'BNS', name: 'Briggs & Stratton', type: 'oem' },
VNT: { code: 'VNT', name: 'Ventrac', type: 'oem' },
HOT: { code: 'HOT', name: 'Hotsy', type: 'oem' },
HAR: { code: 'HAR', name: 'Harvest Tech', type: 'oem' },
MAR: { code: 'MAR', name: 'Marcrest', type: 'oem' },
KIN: { code: 'KIN', name: 'Kinze', type: 'oem' },
MCH: { code: 'MCH', name: 'McHale', type: 'oem' },
// Tire Brands (via K&M)
BRG: { code: 'BRG', name: 'Bridgestone', type: 'oem' },
FST: { code: 'FST', name: 'Firestone', type: 'oem' },
GYR: { code: 'GYR', name: 'Goodyear', type: 'oem' },
MIC: { code: 'MIC', name: 'Michelin', type: 'oem' },
CPR: { code: 'CPR', name: 'Cooper', type: 'oem' },
TTN: { code: 'TTN', name: 'Titan', type: 'oem' },
} as const;3.3 Search API Changes
// services/search/src/schemas/search.ts
export const SearchParamsSchema = z.object({
// Text search
q: z.string().optional(),
// Product type filter (NEW - primary filter)
productType: z.enum(['part', 'tire', 'accessory']).optional(),
// Brand filter (replaces manufacturer)
brand: z.union([z.string(), z.array(z.string())]).optional(),
// Vendor filter (NEW)
vendor: z.union([z.string(), z.array(z.string())]).optional(),
// Category filter
category: z.union([z.string(), z.array(z.string())]).optional(),
categoryPath: z.union([z.string(), z.array(z.string())]).optional(),
// Legacy compatibility (deprecated)
/** @deprecated Use brand instead */
manufacturer: z.union([z.string(), z.array(z.string())]).optional(),
// Tire-specific filters
tireBrand: z.union([z.string(), z.array(z.string())]).optional(),
tireSize: z.union([z.string(), z.array(z.string())]).optional(),
// ... other existing filters
});3.4 Index Resolver Refactor
// services/search/src/utils/index-resolver.ts
export function resolveSearchIndex(params: SearchParams): string {
// Priority 1: Explicit productType
if (params.productType === 'tire') {
return env.TIRES_INDEX_NAME;
}
if (params.productType === 'part') {
return env.SEARCH_INDEX_NAME;
}
// Priority 2: Vendor-based (KMT = tires)
if (isKMTVendor(params.vendor)) {
return env.TIRES_INDEX_NAME;
}
// Priority 3: Tire-specific filters
if (params.tireBrand || params.tireSize) {
return env.TIRES_INDEX_NAME;
}
// Priority 4: Category-based (legacy compatibility)
if (isTireCategory(params.category)) {
return env.TIRES_INDEX_NAME;
}
// Default: parts
return env.SEARCH_INDEX_NAME;
}3.5 Frontend Changes
// CROP-front/app/catalog/page.tsx
function buildSearchParams(parsed: ParsedParams): SearchParams {
return {
// Explicit product type filter
productType: 'part', // <-- KEY CHANGE: Only parts on catalog page
// User-selected filters
brand: parsed.brand,
category: parsed.category,
// ... other filters
};
}// CROP-front/app/tires/page.tsx
function buildSearchParams(parsed: ParsedParams): SearchParams {
return {
// Explicit product type filter
productType: 'tire', // <-- KEY CHANGE: Only tires on tires page
// Tire-specific filters
tireBrand: parsed.tireBrand,
tireSize: parsed.tireSize,
// ... other filters
};
}4. Migration Plan
Phase 1: Add productType (Non-Breaking)
Goal: Add productType field without breaking existing functionality
Changes:
- Add
productTypefield to ES mapping - Add
productTypeto MongoDB schema - Update sync scripts to populate productType
- Add productType filter to Search API (optional param)
Migration Script:
// MongoDB migration
db.parts_kmt.updateMany({}, { $set: { productType: 'tire' } });
db.parts_nhl.updateMany({}, { $set: { productType: 'part' } });
db.parts_bns.updateMany({}, { $set: { productType: 'part' } });
// ... other collectionsES Mapping Addition:
{
"productType": {
"type": "keyword"
}
}Phase 2: Add vendor/brand separation (Non-Breaking)
Goal: Add vendor and brand fields alongside existing manufacturer
Changes:
- Add
vendorandbrandfields to schema - Update transformers to populate both
- Keep
manufacturerfor backward compatibility - Add deprecation warnings
Data Mapping:
KMT parts:
- vendor: { code: 'KMT', name: 'K&M Tire', type: 'wholesaler' }
- brand: { code: 'BRG', name: 'Bridgestone' } // from specifications.Brand
- manufacturer: { code: 'KMT', name: 'K&M Tire' } // legacy
DIS parts:
- vendor: { code: 'DIS', name: 'Clinton Tractor DIS', type: 'dealer' }
- brand: { code: 'NHL', name: 'New Holland' }
- manufacturer: { code: 'NHL', name: 'New Holland' } // legacyPhase 3: Frontend Migration
Goal: Update frontend to use new fields
Changes:
- Update catalog page to use
productType: 'part' - Update tires page to use
productType: 'tire' - Update filters to use
brandinstead ofmanufacturer - Add vendor filter (optional, for admin)
Phase 4: Deprecation & Cleanup
Goal: Remove legacy fields and complete migration
Changes:
- Remove
manufacturerfield from schema - Update all code to use
brand/vendor - Update documentation
- Rebuild ES indices with clean schema
5. Implementation Checklist
Backend (CROP-parts-services)
-
shared-catalog package
- Add
ProductTypetype - Add
Brandinterface - Add
Vendorinterface - Update
IndexedParttype - Add validation for new fields
- Update transformers
- Add
-
search service
- Add
productTypeto ES mapping - Add
productTypefilter to query builder - Update index resolver
- Add
brandandvendorfields to mapping - Update facet aggregations
- Add deprecation warnings for
manufacturer
- Add
-
catalog service
- Update DIS sync to set
productType: 'part' - Update K&M sync to set
productType: 'tire' - Add vendor/brand population
- Update CT photos sync
- Update DIS sync to set
-
sync scripts
- Migration script for productType
- Migration script for vendor/brand
- ES reindex script
Frontend (CROP-front)
-
catalog page
- Add
productType: 'part'to search params - Update manufacturer filter → brand filter
- Test parts-only display
- Add
-
tires page
- Add
productType: 'tire'to search params - Ensure tire-specific filters work
- Test tires-only display
- Add
-
search page
- Support both productTypes
- Add productType facet (optional)
- Test cross-search
-
types
- Update SearchParams type
- Add ProductType type
- Update Product type
Documentation
- Update API documentation
- Update data model documentation
- Add migration guide
- Update CLAUDE.md files
6. Risks & Mitigations
| Risk | Probability | Impact | Mitigation |
|---|---|---|---|
| Breaking existing API consumers | Medium | High | Keep manufacturer field for backward compatibility |
| ES reindex downtime | Low | Medium | Use alias switching for zero-downtime reindex |
| Data inconsistency during migration | Medium | Medium | Run migration in stages with validation |
| Frontend regression | Medium | High | Comprehensive testing before deploy |
7. Success Criteria
- Catalog page shows ONLY parts (no tires)
- Tires page shows ONLY tires (no parts)
- Search finds both with clear distinction
- Terminology is correct (vendor vs brand vs manufacturer)
- Backward compatibility maintained during migration
- No performance degradation
8. Timeline Estimate
| Phase | Duration | Dependencies |
|---|---|---|
| Phase 1: Add productType | 2-3 days | None |
| Phase 2: Add vendor/brand | 3-4 days | Phase 1 |
| Phase 3: Frontend migration | 2-3 days | Phase 2 |
| Phase 4: Cleanup | 1-2 days | Phase 3 + validation period |
| Total | 8-12 days |
Appendix A: Field Mapping Reference
| Current Field | New Field | Notes |
|---|---|---|
manufacturer.code | brand.code | Brand = who makes it |
manufacturer.name | brand.name | |
| - | vendor.code | NEW: Who we buy from |
| - | vendor.name | NEW |
| - | productType | NEW: part/tire/accessory |
category | category | Keep, but ensure hierarchical |
specifications.Brand (tires) | tireSpecs.tireBrand | Tire manufacturer brand |
Appendix B: Query Examples
Catalog page (parts only):
{
"productType": "part",
"brand": ["NHL", "KUH"],
"category": "Filters"
}Tires page:
{
"productType": "tire",
"tireBrand": "Bridgestone",
"tireSize": "225/65R17"
}Search (both):
{
"q": "oil filter",
"productType": null // or omit - search both indices
}