Skip to main content

Overview

The SAT Catalog service provides access to the official Mexican tax authority (SAT) catalog of products and services. This catalog contains over 50,000 standardized product/service codes (claves) used in CFDI invoicing.
The catalog is stored locally in the database and synchronized periodically with the official SAT catalog. All searches are performed against the local database for fast response times.

Catalog Structure

Each product/service entry contains:
  • id (clave): Unique SAT product/service code (e.g., “10101500”)
  • descripcion: Official description
  • palabras_similares: Similar words for search matching
  • incluir_iva_trasladado: IVA (VAT) applicability (“Sí”, “No”, “Opcional”)
  • incluir_ieps_trasladado: IEPS (special tax) applicability
  • fecha_inicio_vigencia: Start date of validity
  • fecha_fin_vigencia: End date of validity (null if still valid)

Search Functionality

The service provides multiple search methods optimized for different use cases: Search by text, code, or filters:
// From sat-catalog.service.ts:18
async search(
  params: SATProductServiceSearchParams,
  maxResults?: number | null
): Promise<SATProductServiceSearchResponse> {
  const {
    query,
    incluir_iva_trasladado,
    incluir_ieps_trasladado,
    limit = 50,
    offset = 0,
  } = params;

  // Search in description, similar words, or by exact code
  const searchConditions = [
    { descripcion: { [Op.iLike]: `%${searchTerm}%` } },
    { palabras_similares: { [Op.iLike]: `%${searchTerm}%` } },
  ];

  // If query looks like a code (numeric), search by ID
  if (/^\d+$/.test(searchTerm)) {
    searchConditions.push({ id: searchTerm });
  }
}
1

Text Search

Searches in both descripcion and palabras_similares fields using case-insensitive pattern matching.
// sat-catalog.service.ts:36
descripcion: { [Op.iLike]: `%${searchTerm}%` }
2

Code Search

If the query is numeric, searches for exact code match.
// sat-catalog.service.ts:49
if (/^\d+$/.test(searchTerm)) {
  searchConditions.push({ id: searchTerm });
}
3

Filter by Tax Applicability

Optionally filter by IVA or IEPS applicability.
// sat-catalog.service.ts:59
if (incluir_iva_trasladado) {
  whereClause.incluir_iva_trasladado = incluir_iva_trasladado;
}
4

Active Items Only

Automatically filters to show only currently valid items.
// sat-catalog.service.ts:74
[Op.or]: [
  { fecha_fin_vigencia: null },
  { fecha_fin_vigencia: { [Op.gte]: new Date() } },
]

Search Response

interface SATProductServiceSearchResponse {
  items: SATProductServiceAttributes[];  // Array of matching items
  total: number;                         // Total matching items
  limit: number;                         // Applied limit
  offset: number;                        // Offset for pagination
}

Advanced Search Methods

For semantic or AI-powered searches using PostgreSQL’s pg_trgm extension:
// From sat-catalog.service.ts:145
async searchBySimilarity(
  query: string,
  limit: number = 20,
  maxResults?: number | null
): Promise<SATProductServiceAttributes[]> {
  // Use PostgreSQL similarity operator
  const items = await SatProductService.findAll({
    where: {
      [Op.or]: [
        SatProductService.sequelize.literal(
          `descripcion % '${searchTerm}'`
        ),
        SatProductService.sequelize.literal(
          `palabras_similares % '${searchTerm}'`
        ),
      ],
    },
    order: [
      // Sort by similarity score (higher = more similar)
      [
        SatProductService.sequelize.literal(
          `similarity(descripcion, '${searchTerm}')`
        ),
        "DESC",
      ],
    ],
  });
}
Similarity search uses the % operator from PostgreSQL’s pg_trgm extension, which performs trigram-based fuzzy matching. This is ideal for handling typos or finding semantically similar entries.

Autocomplete Suggestions

For search-as-you-type functionality:
// From sat-catalog.service.ts:208
async getSuggestions(
  partialQuery: string,
  limit: number = 10,
  maxResults?: number | null
): Promise<SATProductServiceAttributes[]> {
  const searchTerm = partialQuery.trim().toLowerCase();

  const items = await SatProductService.findAll({
    where: {
      [Op.or]: [
        { descripcion: { [Op.iLike]: `${searchTerm}%` } },  // Starts with
        { id: { [Op.like]: `${searchTerm}%` } },            // Code starts with
      ],
    },
    limit: effectiveLimit,
    order: [
      ["descripcion", "ASC"],
      ["id", "ASC"],
    ],
  });
}
Autocomplete uses prefix matching (searchTerm%) which is faster than full-text search and optimized for real-time suggestions.

Get by ID

Retrieve a specific product/service by its SAT code:
// From sat-catalog.service.ts:133
async getById(id: string): Promise<SATProductServiceAttributes | null> {
  const item = await SatProductService.findByPk(id);
  return item ? (item.toJSON() as SATProductServiceAttributes) : null;
}

Catalog Statistics

Get statistics about the catalog:
// From sat-catalog.service.ts:262
async getStats(): Promise<{
  total: number;      // Total items in catalog
  withIva: number;    // Items with IVA applicable
  withIeps: number;   // Items with IEPS applicable
  active: number;     // Currently valid items
}> {
  const [total, withIva, withIeps, active] = await Promise.all([
    SatProductService.count(),
    SatProductService.count({
      where: {
        incluir_iva_trasladado: { [Op.in]: ["Sí", "Opcional"] },
      },
    }),
    SatProductService.count({
      where: {
        incluir_ieps_trasladado: { [Op.in]: ["Sí", "Opcional"] },
      },
    }),
    SatProductService.count({
      where: {
        [Op.or]: [
          { fecha_fin_vigencia: null },
          { fecha_fin_vigencia: { [Op.gte]: new Date() } },
        ],
      },
    }),
  ]);

  return { total, withIva, withIeps, active };
}

Plan-Based Limits

The service supports result limits based on subscription plans:
// From sat-catalog.service.ts:88
const effectiveLimit = maxResults !== null && maxResults !== undefined
  ? Math.min(limit, maxResults, 100)
  : Math.min(limit, 100);
Free plan: Limited to 10 results per search Pro plan: Limited to 50 results per search Enterprise plan: Limited to 100 results per search

Search Optimization

Prioritizing Exact Matches

The search algorithm prioritizes exact matches at the beginning of descriptions:
// From sat-catalog.service.ts:98
order: [
  // Prioritize matches at the start of description
  query && SatProductService.sequelize
    ? [
        SatProductService.sequelize.literal(
          `CASE WHEN descripcion ILIKE '${query}%' THEN 1 ELSE 2 END`
        ),
        "ASC",
      ]
    : ["descripcion", "ASC"],
  ["id", "ASC"],
]
This ensures that:
  1. Items starting with the search term appear first
  2. Other matches appear second
  3. Within each group, items are sorted alphabetically

Usage Examples

Basic Search

import { SATCatalogService } from './services/sat-catalog.service';

const service = new SATCatalogService();

// Search for "computadora"
const results = await service.search({
  query: 'computadora',
  limit: 20,
  offset: 0,
});

console.log(`Found ${results.total} items`);
console.log(`Showing ${results.items.length} items`);

for (const item of results.items) {
  console.log(`${item.id}: ${item.descripcion}`);
  console.log(`  IVA: ${item.incluir_iva_trasladado}`);
}

Search with Filters

// Search for items with IVA applicable
const results = await service.search({
  query: 'servicio',
  incluir_iva_trasladado: 'Sí',
  limit: 50,
});

Get by SAT Code

// Get specific product by code
const product = await service.getById('43211500');

if (product) {
  console.log(`Code: ${product.id}`);
  console.log(`Description: ${product.descripcion}`);
  console.log(`IVA: ${product.incluir_iva_trasladado}`);
  console.log(`IEPS: ${product.incluir_ieps_trasladado}`);
}

Autocomplete

// Get suggestions for autocomplete
const suggestions = await service.getSuggestions('comp', 10);

for (const suggestion of suggestions) {
  console.log(`${suggestion.id}: ${suggestion.descripcion}`);
}

Similarity Search

// Find similar items (fuzzy search)
const similar = await service.searchBySimilarity('conputadora', 20);
// Will find "computadora" even with typo

Get Statistics

const stats = await service.getStats();

console.log(`Total items: ${stats.total}`);
console.log(`With IVA: ${stats.withIva}`);
console.log(`With IEPS: ${stats.withIeps}`);
console.log(`Active: ${stats.active}`);

Database Schema

CREATE TABLE sat_product_services (
  id VARCHAR(8) PRIMARY KEY,                 -- SAT code (e.g., "10101500")
  descripcion TEXT NOT NULL,                 -- Description
  palabras_similares TEXT,                   -- Similar words for search
  incluir_iva_trasladado VARCHAR(10),        -- "Sí", "No", "Opcional"
  incluir_ieps_trasladado VARCHAR(10),       -- "Sí", "No", "Opcional"
  fecha_inicio_vigencia DATE,                -- Start date
  fecha_fin_vigencia DATE,                   -- End date (null if active)
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Indexes for performance
CREATE INDEX idx_sat_products_descripcion ON sat_product_services USING gin (descripcion gin_trgm_ops);
CREATE INDEX idx_sat_products_palabras ON sat_product_services USING gin (palabras_similares gin_trgm_ops);
CREATE INDEX idx_sat_products_vigencia ON sat_product_services (fecha_fin_vigencia);
The gin_trgm_ops indexes enable fast similarity searches using the pg_trgm extension.

API Integration

The SAT catalog service is exposed through API endpoints:
  • GET /api/sat/catalog/search?query=... - Basic search
  • GET /api/sat/catalog/:code - Get by code
  • GET /api/sat/catalog/suggestions?q=... - Autocomplete
  • GET /api/sat/catalog/stats - Get statistics

Implementation Details

Source Code References

  • Service: src/services/sat-catalog.service.ts
  • Types: src/types/sat.types.ts
  • Model: src/database/models/SatProductService.model.ts

Performance Considerations

  1. Indexing: Full-text indexes on description and similar words
  2. Pagination: All searches support limit/offset pagination
  3. Caching: Consider caching frequently searched terms
  4. Active Items Only: Automatically filters inactive items to reduce result set
The catalog is updated periodically from the official SAT source. The update process should be run during off-peak hours to avoid impacting search performance.

Build docs developers (and LLMs) love