CROP
ProjectsParts Services

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 parts

Critical 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)

TermCurrent UsageProblem
manufacturerKMT, NHL, BNS, KUH...KMT is NOT a manufacturer
vendorCodeSame as manufacturerDuplicate concept
category"parts", "Wheels & Tires"No productType distinction

Proposed (Clear)

TermDefinitionExamples
productTypeHigh-level product classificationpart, tire, accessory
manufacturerCompany that MAKES the productNew Holland, Kuhn, Bridgestone
vendorCompany we BUY from (supplier/reseller)K&M Tire, Clinton Tractor DIS
brandCodeOur internal code for the brandNHL, KUH, KMT, BNS
categoryProduct category hierarchyFilters > 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:

  1. Add productType field to ES mapping
  2. Add productType to MongoDB schema
  3. Update sync scripts to populate productType
  4. 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 collections

ES Mapping Addition:

{
  "productType": {
    "type": "keyword"
  }
}

Phase 2: Add vendor/brand separation (Non-Breaking)

Goal: Add vendor and brand fields alongside existing manufacturer

Changes:

  1. Add vendor and brand fields to schema
  2. Update transformers to populate both
  3. Keep manufacturer for backward compatibility
  4. 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' } // legacy

Phase 3: Frontend Migration

Goal: Update frontend to use new fields

Changes:

  1. Update catalog page to use productType: 'part'
  2. Update tires page to use productType: 'tire'
  3. Update filters to use brand instead of manufacturer
  4. Add vendor filter (optional, for admin)

Phase 4: Deprecation & Cleanup

Goal: Remove legacy fields and complete migration

Changes:

  1. Remove manufacturer field from schema
  2. Update all code to use brand/vendor
  3. Update documentation
  4. Rebuild ES indices with clean schema

5. Implementation Checklist

Backend (CROP-parts-services)

  • shared-catalog package

    • Add ProductType type
    • Add Brand interface
    • Add Vendor interface
    • Update IndexedPart type
    • Add validation for new fields
    • Update transformers
  • search service

    • Add productType to ES mapping
    • Add productType filter to query builder
    • Update index resolver
    • Add brand and vendor fields to mapping
    • Update facet aggregations
    • Add deprecation warnings for manufacturer
  • 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
  • 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
  • tires page

    • Add productType: 'tire' to search params
    • Ensure tire-specific filters work
    • Test tires-only display
  • 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

RiskProbabilityImpactMitigation
Breaking existing API consumersMediumHighKeep manufacturer field for backward compatibility
ES reindex downtimeLowMediumUse alias switching for zero-downtime reindex
Data inconsistency during migrationMediumMediumRun migration in stages with validation
Frontend regressionMediumHighComprehensive testing before deploy

7. Success Criteria

  1. Catalog page shows ONLY parts (no tires)
  2. Tires page shows ONLY tires (no parts)
  3. Search finds both with clear distinction
  4. Terminology is correct (vendor vs brand vs manufacturer)
  5. Backward compatibility maintained during migration
  6. No performance degradation

8. Timeline Estimate

PhaseDurationDependencies
Phase 1: Add productType2-3 daysNone
Phase 2: Add vendor/brand3-4 daysPhase 1
Phase 3: Frontend migration2-3 daysPhase 2
Phase 4: Cleanup1-2 daysPhase 3 + validation period
Total8-12 days

Appendix A: Field Mapping Reference

Current FieldNew FieldNotes
manufacturer.codebrand.codeBrand = who makes it
manufacturer.namebrand.name
-vendor.codeNEW: Who we buy from
-vendor.nameNEW
-productTypeNEW: part/tire/accessory
categorycategoryKeep, but ensure hierarchical
specifications.Brand (tires)tireSpecs.tireBrandTire 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
}

On this page