Barcode · UPC · Check Digit

Estado actual, estándar GS1, análisis competitivo y mockup interactivo

Creado: 14 abril 2026  |  Última revisión: 14 abril 2026  |  Fuente: repo qiiub (branch main)
3
Entidades con barcode
3
Unique indexes
7
Barcode types
6
Gaps

Resumen Ejecutivo

QIIUB tiene el modelo de datos correcto: barcode primario en ProductVariant, aliases en ProductBarcode, clasificación por tipo, indexes para lookup en < 1ms.

Falta la lógica de negocio: auto-detección de tipo (función pura en código, no en DB), validación Mod10, y endpoint de búsqueda por barcode.

Cualquier merchant (supermercado grande o tienda pequeña) siempre tiene mezcla de formatos: UPC/EAN del fabricante + códigos internos + PLU. El sistema debe auto-detectar, nunca forzar un formato único.

Interactivo 1. Mockup: Data Entry de Producto

Haga clic en un ejemplo para ver la auto-detección y validación en tiempo real.

Nuevo Producto — Variante

049000042566UPC-A Coca-Cola 7501000911233EAN-13 Bimbo MX 96385074EAN-8 9780141036144ISBN 1984 4011PLU plátano 2100001003995Interno (queso $3.99) FLETE-001Custom servicio 049000042561UPC-A check digit MALO
UPC-A, EAN-13, EAN-8, ISBN, PLU, o código interno
Nota: hoy 1 categoría por producto. Multi-categoría (ej. BigCommerce) requiere tabla junction.

Decisión BarcodeType: calcular siempre, no almacenar en DB

El tipo de barcode es un atributo derivado — se determina al 100% del valor. Dado "049000042566", SIEMPRE es UPC-A. No hay ambigüedad.

Lo que SI existe

  • Enum C# BarcodeType — tipo de retorno de DetectType()
  • Función pura BarcodeHelper.DetectType(string) — ~15 líneas, sin DB, sin estado
  • Se usa en: validators, UI (badge), label printing (symbology)

Lo que NO se necesita

  • ProductBarcode.BarcodeType column — se elimina
  • ProductVariant.BarcodeType column — no se agrega
  • Tabla ref.BarcodeType — no se necesita seed
  • Sync de ref.BarcodeType al POS — eliminado

El enum vive en código, no en la DB

public enum BarcodeType : byte
{
    UpcA     = 0,
    Ean13    = 1,
    Ean8     = 2,
    Isbn     = 3,
    Plu      = 4,
    Internal = 8,
    Custom   = 9,
}

public static BarcodeType DetectType(string barcode) =>
    barcode switch
    {
        _ when !barcode.All(char.IsDigit)                     => BarcodeType.Custom,
        { Length: 4 or 5 }                                    => BarcodeType.Plu,
        { Length: 13 } when barcode[..3] is "978" or "979"  => BarcodeType.Isbn,
        { Length: 13 } when barcode[..2] is >= "20" and <= "29" => BarcodeType.Internal,
        { Length: 12 } when barcode[0] is '2' or '4'        => BarcodeType.Internal,
        { Length: 8 }                                          => BarcodeType.Ean8,
        { Length: 12 }                                         => BarcodeType.UpcA,
        { Length: 13 }                                         => BarcodeType.Ean13,
        _                                                      => BarcodeType.Custom,
    };
AspectoAlmacenar en DBCalcular siempre
ConsistenciaPuede quedar desincronizado si editan el barcodeSiempre correcto por definición
ComplejidadEnum + seed + migration + syncUna función pura de 15 líneas
PerformanceCero (ya está en la row)Cero (5 comparaciones = microsegundos)
¿Se busca por tipo?Nunca. Se busca por VALOR de barcode, no por tipo.
Implicación: La columna ProductBarcode.BarcodeType que existe hoy en el schema se elimina en la próxima migración. Ni ProductVariant ni ProductBarcode necesitan ese campo. El tipo se muestra en la UI llamando a DetectType() sobre el valor almacenado.

Contexto 2. Realidad del Merchant Pequeño / Supermercado

Un merchant que "solo usa el barcode del fabricante" en la práctica siempre tiene mezcla:

Categoría de producto% típicoTipo de barcodeEjemplos
Empacados del fabricante70-85%UPC-A / EAN-13Coca-Cola, Colgate, Bimbo
Sin barcode estándar10-25%Interno (prefijo 2) o CustomPan local, tortillas, granel, importados, reempacados
Frutas y verduras3-8%PLU (4-5 dígitos)4011 = plátano, 4065 = pimiento
Peso variable (báscula)2-5%Interno (prefijo 2 + precio)2-12345-00399-5 = queso $3.99

Productos que NUNCA traen UPC

ProductoPor qué no tiene UPCQué hace la tienda
Pan de panadería localProductor sin registro GS1Código interno + etiqueta propia
Tortillas artesanalesProductor informalCódigo interno o venta por nombre
Tornillos sueltos (ferretería)Se venden por unidad de bolsa grandeCódigo interno por tipo/tamaño
Importados sin GS1Común en LatamCódigo interno
Comida preparada (deli)Producido en tiendaCódigo interno con precio embebido
Servicios (flete, instalación)No es producto físicoAlfanumérico: FLETE-001
Conclusión: No se puede forzar un formato único por merchant. Auto-detectar por cada barcode individual y validar según el tipo detectado.

Referencia 3. Cómo lo Hacen los Competidores

CaracterísticaSquareShopify POSLightspeedCloverQIIUB (meta)
SKU y Barcode separadosSiSiSiSiSi
Ambos opcionalesSiSiSKU autoSiSKU req.
Múltiples barcodes / itemNoNoSiNoSi
Auto-detección formatoNoNoSiNoDiseñado
Validación check digitNoWarningBloqueaNoWarning
Barcode interno (gen.)NoNoSiParcialDiseñado
Soporte PLUNoNoSiNoPropuesto
Offline barcode scanLimitadoNoSiLimitadoSi (SQLite)
Patrón de la industria: La mayoría no valida check digits (confían en el escáner físico). La validación importa cuando el barcode se digita manualmente. Lightspeed es el benchmark; QIIUB apunta a igualarlo.

Recomendación: Warning (como Shopify), no error bloqueante. Override manual para importaciones y casos legítimos.

Referencia 4. Glosario de Términos

Todos estos términos están documentados en docs/GLOSSARY.md. Referencia rápida inline:

GS1Global Standards One
Organización sin fines de lucro que administra los estándares globales de códigos de barras. Fundada en 2005 (fusión de EAN International + UCC). Opera en ~115 países.
GTINGlobal Trade Item Number
Término paraguas de GS1 que abarca UPC-A (12 dígitos), EAN-13 (13), EAN-8 (8). Todo UPC es un GTIN, pero no todo GTIN es un UPC.
UPC-AUniversal Product Code
Barcode de 12 dígitos numéricos usado en retail USA/Canadá. El último dígito es un check digit Modulo 10.
EAN-13European Article Number
Barcode de 13 dígitos numéricos. Estándar internacional fuera de USA. Compatible con UPC-A (un UPC-A es un EAN-13 con prefijo 0).
EAN-8
Versión compacta de EAN para productos pequeños. 8 dígitos numéricos con check digit Modulo 10.
ISBNInternational Standard Book Number
Identificador de libros. ISBN-13 usa formato EAN-13 con prefijo 978 o 979. Check digit Modulo 10.
PLUPrice Look-Up Code
Código de 4-5 dígitos para frutas y verduras. Estándar IFPS (International Federation for Produce Standards). Sin check digit. Ej: 4011 = plátano.
Check Digit
Último dígito de un UPC/EAN calculado con algoritmo Modulo 10. Detecta errores de digitación: si cambias un dígito, el check digit no cuadra.
Modulo 10(Mod 10 / Luhn variant)
Algoritmo GS1: suma ponderada alternando ×1 y ×3 de derecha a izquierda, check = (10 - suma%10) % 10.
Prefijo 2GS1 In-Store
GS1 reserva prefijo 2 (UPC-A) y 20-29 (EAN-13) para uso interno de tienda. Estos códigos los inventa el merchant, no son globalmente únicos.
IFPSInternational Federation for Produce Standards
Organización que administra los códigos PLU para frutas y verduras a nivel mundial.
SKUStock Keeping Unit
Código interno del merchant para identificar un producto. No es estándar global — cada tienda define los suyos. En QIIUB: ProductVariant.Sku.

Referencia 5. Estándar GS1: Por qué Prefijo "2"

El primer dígito de un UPC-A indica su propósito. Esto es un estándar global de GS1, no una convención arbitraria:

0UPC regular
1UPC regular
2Uso interno
3Pharma (NDC)
4Interno (no-food)
5Cupones
6UPC regular
7UPC regular
8Reservado
9Reservado

Por qué "2" y no otro

Ejemplo: peso variable en báscula

Barcode generado por la báscula del supermercado:

  2 12345 00399 5
  │ │     │     └─ Check digit
  │ │     └─ Precio: $3.99
  │ └─ Código interno del producto (queso gouda)
  └─ Prefijo 2 = "este código lo generé yo"

Prefijos EAN-13 relevantes (Latam)

PrefijoAsignado a
20-29Uso interno de tienda (reservado por GS1)
00-09USA / Canadá
740Guatemala
741El Salvador
742Honduras
743Nicaragua
744Costa Rica
745Panamá
750México
770Colombia
978-979ISBN (libros)

No implementada 6. Lógica de Auto-Detección (función pura, no DB)

// BarcodeHelper.DetectType() — enum en código, NO columna en DB
// Orden: más específico → más general. El usuario NUNCA elige tipo manualmente.

if (4-5 dígitos numéricos)             → PLU         // frutas/verduras
else if (13 dig, 978|979)              → ISBN        // libros
else if (13 dig, prefijo 20-29)        → Internal    // generado por tienda (EAN)
else if (12 dig, prefijo 2 o 4)        → Internal    // generado por tienda (UPC)
else if (8 dígitos numéricos)           → EAN-8       // validar Mod10
else if (12 dígitos numéricos)          → UPC-A       // validar Mod10
else if (13 dígitos numéricos)          → EAN-13      // validar Mod10
elseCustom      // sin validación
TipoValidar Mod10Razón
UPC-A, EAN-13, EAN-8, ISBNSi (warning)Estándar GS1 — detecta errores de digitación
Internal (prefijo 2)NoGenerado por tienda, formato libre
PLUNo4-5 dígitos IFPS, sin check digit
CustomNoFormato libre

No existe 7. Validación Check Digit (Modulo 10)

// Ejemplo: UPC-A "012345678905"
// Posiciones desde la derecha (sin check digit), pesos alternados 3,1,3,1...
// 0×1 + 1×3 + 2×1 + 3×3 + 4×1 + 5×3 + 6×1 + 7×3 + 8×1 + 9×3 + 0×1 = 85
// Check = (10 - 85%10) % 10 = 5  ✓

public static bool IsValidCheckDigit(string barcode)
{
    if (!barcode.All(char.IsDigit)) return false;
    if (barcode.Length is not (8 or 12 or 13)) return true;

    var sum = 0;
    for (var i = 0; i < barcode.Length - 1; i++)
    {
        var w = ((barcode.Length - 1 - i) % 2 == 0) ? 1 : 3;
        sum += (barcode[i] - '0') * w;
    }
    var expected = (10 - (sum % 10)) % 10;
    return (barcode[^1] - '0') == expected;
}

Implementado 8. Modelo de Datos (Schema Actual)

Product
PK Id, MerchantId
    Name, Description
FK CategoryId (primary)
    + ProductCategory (N:M)
    MSRP, IsActive
1 : N
ProductVariant
PK Id, MerchantId
FK ProductId
UQ Sku varchar(50)
UQ Barcode varchar(50) NULL
    Price, Cost, IsActive
1 : N
ProductBarcode
PK Id, MerchantId
FK ProductVariantId
UQ Barcode varchar(50)
    IsPrimary bit
    BarcodeType eliminado (se calcula)

Item Code vs Barcode

CampoEntidadPropósitoEjemplo
SkuProductVariantCódigo interno del merchantCAM-BLK-XL
BarcodeProductVariantBarcode primario (inline)012345678905
BarcodeProductBarcodeAliases adicionales (N por variante)4006381333931
Categorías (multi-asignación): Product.CategoryId es la categoría primaria (usada por DailySales y reportes). La tabla junction ProductCategory(MerchantId, ProductId, CategoryId, SortOrder) contiene todas las asignaciones — incluyendo la primaria — para merchandising (“New Arrivals”), cross-selling y browsing del portal web. Ver ADR-0044.

Implementado 9. Indexes & Performance

SQL Server (API cloud)

IndexPropósito
UQ_..._SkuLookup por SKU
UQ_..._Barcode (Variant)Barcode primario
UQ_..._Barcode (ProdBarcode)Aliases

SQLite (POS offline)

IndexRendimiento
ProductBarcode.Barcode< 0.1 ms
ProductVariant.Sku< 0.1 ms
FTS5 Product.Name1-5 ms

Parcial 10. Flujo de Búsqueda

POS Terminal (offline — todo local)

Cashier escanea
SQLite
Variant.Barcode
ProductBarcode
Resultado

Portal Web / Mobile

Usuario busca
Endpoint NO existe
GET /products/by-barcode/{code}

11. Gap Analysis

1
BarcodeHelper.DetectType() + enum BarcodeType
Función pura de auto-detección + enum C# (7 valores). No se almacena en DB — se calcula del valor. Eliminar columna ProductBarcode.BarcodeType existente.
2
Check digit Modulo 10
No hay validación GS1. Warning (no bloqueante) recomendado. Solo para tipos detectados como UPC-A, EAN-13, EAN-8, ISBN.
3
Setting catalog.barcode.policy
barcode_as_sku (supermercados) vs sku_and_barcode (tiendas con código propio).
4
Endpoint GET /products/by-barcode/{code}
No existe. Necesario para portal web y app mobile.
5
CRUD de ProductBarcode (alias)
No hay API para barcodes adicionales.
6
POS client + SQLite
Trabajo futuro planificado.

12. Orden de Implementación

PasoTrabajoEsfuerzo
1Enum BarcodeType (7 valores) + BarcodeHelper.DetectType() (función pura). Migración: eliminar columna ProductBarcode.BarcodeType.Bajo
2Validación Mod10 (warning) + formato (numérico, longitud) en FluentValidation, usando DetectType()Bajo
3MerchantSetting catalog.barcode.policy + lógica barcode→SKU autoMedio
4Endpoint GET /products/by-barcode/{code}Medio
5CRUD endpoints ProductBarcode (alias)Medio
6POS client SQLite (indexes + FTS5 + scan offline)Alto
Pasos 1-2: quick wins (< 1 día). No hay seed data ni ref table — el tipo se calcula. Paso 3: wiring con settings cascade. Pasos 4-5: endpoints estándar con tests. Paso 6: parte del POS client.