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:
Basic Search
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 });
}
}
Text Search
Searches in both descripcion and palabras_similares fields using case-insensitive pattern matching.// sat-catalog.service.ts:36
descripcion: { [Op.iLike]: `%${searchTerm}%` }
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 });
}
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;
}
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
Similarity Search
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:
- Items starting with the search term appear first
- Other matches appear second
- 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
- Indexing: Full-text indexes on description and similar words
- Pagination: All searches support limit/offset pagination
- Caching: Consider caching frequently searched terms
- 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.