michangarrito/backups/docs-backup-2026-01-10/docs/02-especificaciones/ARQUITECTURA-DATABASE.md
rckrdmrd 97f407c661 [MIGRATION-V2] feat: Migrar michangarrito a estructura v2
- Prefijo v2: MCH
- TRACEABILITY-MASTER.yml creado
- Listo para integracion como submodulo

Workspace: v2.0.0 | SIMCO: v4.0.0
2026-01-10 11:28:54 -06:00

30 KiB

MiChangarrito - Arquitectura de Base de Datos

Resumen

  • Motor: PostgreSQL 15
  • Puerto desarrollo: 5432 (instancia compartida del workspace)
  • Base de datos: michangarrito_dev
  • Usuario: michangarrito_dev
  • Arquitectura: Multi-tenant con Row Level Security (RLS)

Schemas

Schema Proposito Tablas Principales
public Tenants y configuracion global tenants, tenant_configs
auth Autenticacion y usuarios users, sessions, otp_codes
catalog Productos y categorias products, categories, product_templates
sales Ventas y pagos sales, sale_items, payments, daily_closures
inventory Stock y movimientos inventory, inventory_movements, stock_alerts
customers Clientes y fiados customers, fiados, fiado_payments
orders Pedidos de clientes orders, order_items, deliveries
subscriptions Planes y tokens IA plans, subscriptions, token_packages, token_usage
messaging WhatsApp y notificaciones conversations, messages, notifications

Schema: public

tenants

Tabla raiz multi-tenant.

CREATE TABLE public.tenants (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Identificacion
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(50) UNIQUE NOT NULL,
    business_type VARCHAR(50) NOT NULL, -- abarrotes, comida, fonda, etc.

    -- Contacto
    phone VARCHAR(20) NOT NULL,
    email VARCHAR(100),
    address TEXT,
    city VARCHAR(50),
    state VARCHAR(50),
    zip_code VARCHAR(10),

    -- Configuracion
    timezone VARCHAR(50) DEFAULT 'America/Mexico_City',
    currency VARCHAR(3) DEFAULT 'MXN',
    tax_rate DECIMAL(5,2) DEFAULT 16.00,
    tax_included BOOLEAN DEFAULT true,

    -- WhatsApp
    whatsapp_number VARCHAR(20),
    whatsapp_verified BOOLEAN DEFAULT false,
    uses_platform_number BOOLEAN DEFAULT true,

    -- Suscripcion (referencia)
    current_plan_id UUID,
    subscription_status VARCHAR(20) DEFAULT 'trial', -- trial, active, past_due, cancelled

    -- Estado
    status VARCHAR(20) DEFAULT 'active',
    onboarding_completed BOOLEAN DEFAULT false,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_tenants_slug ON public.tenants(slug);
CREATE INDEX idx_tenants_phone ON public.tenants(phone);
CREATE INDEX idx_tenants_status ON public.tenants(status);

tenant_configs

Configuraciones adicionales por tenant.

CREATE TABLE public.tenant_configs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,

    -- Horarios
    opening_hour TIME DEFAULT '08:00',
    closing_hour TIME DEFAULT '22:00',
    working_days INTEGER[] DEFAULT ARRAY[1,2,3,4,5,6], -- 0=domingo

    -- Tickets
    ticket_header TEXT,
    ticket_footer TEXT DEFAULT 'Gracias por su compra',
    print_logo BOOLEAN DEFAULT false,

    -- Notificaciones
    daily_summary_enabled BOOLEAN DEFAULT true,
    daily_summary_time TIME DEFAULT '21:00',
    low_stock_alerts BOOLEAN DEFAULT true,

    -- Fiados
    fiados_enabled BOOLEAN DEFAULT true,
    default_fiado_limit DECIMAL(10,2) DEFAULT 500.00,
    fiado_reminder_days INTEGER DEFAULT 7,

    -- Pedidos
    delivery_enabled BOOLEAN DEFAULT false,
    delivery_fee DECIMAL(10,2) DEFAULT 0.00,
    delivery_radius_km DECIMAL(5,2),

    -- Metodos de pago habilitados
    payment_cash BOOLEAN DEFAULT true,
    payment_card_mercadopago BOOLEAN DEFAULT false,
    payment_card_clip BOOLEAN DEFAULT false,
    payment_codi BOOLEAN DEFAULT false,
    payment_transfer BOOLEAN DEFAULT false,

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),

    UNIQUE(tenant_id)
);

Schema: auth

users

Usuarios del sistema (duenos y empleados).

CREATE TABLE auth.users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,

    -- Identificacion
    phone VARCHAR(20) NOT NULL,
    email VARCHAR(100),
    name VARCHAR(100) NOT NULL,

    -- Autenticacion
    pin_hash VARCHAR(255), -- PIN de 4 digitos hasheado
    biometric_enabled BOOLEAN DEFAULT false,
    biometric_key TEXT,

    -- Rol
    role VARCHAR(20) NOT NULL DEFAULT 'owner', -- owner, employee, viewer
    permissions JSONB DEFAULT '{}',

    -- Estado
    status VARCHAR(20) DEFAULT 'active',
    last_login_at TIMESTAMPTZ,
    failed_attempts INTEGER DEFAULT 0,
    locked_until TIMESTAMPTZ,

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),

    UNIQUE(tenant_id, phone)
);

CREATE INDEX idx_users_tenant ON auth.users(tenant_id);
CREATE INDEX idx_users_phone ON auth.users(phone);

sessions

Sesiones activas.

CREATE TABLE auth.sessions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,

    -- Token
    token_hash VARCHAR(255) NOT NULL,
    refresh_token_hash VARCHAR(255),

    -- Metadata
    device_type VARCHAR(20), -- mobile, web
    device_info JSONB,
    ip_address VARCHAR(45),

    -- Expiracion
    expires_at TIMESTAMPTZ NOT NULL,
    refresh_expires_at TIMESTAMPTZ,

    created_at TIMESTAMPTZ DEFAULT NOW(),
    last_activity_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_sessions_user ON auth.sessions(user_id);
CREATE INDEX idx_sessions_token ON auth.sessions(token_hash);

otp_codes

Codigos OTP para verificacion.

CREATE TABLE auth.otp_codes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    phone VARCHAR(20) NOT NULL,

    code VARCHAR(6) NOT NULL,
    purpose VARCHAR(20) NOT NULL, -- login, verify_phone, reset_pin

    attempts INTEGER DEFAULT 0,
    max_attempts INTEGER DEFAULT 3,

    expires_at TIMESTAMPTZ NOT NULL,
    used_at TIMESTAMPTZ,

    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_otp_phone ON auth.otp_codes(phone, purpose);

Schema: catalog

categories

Categorias de productos.

CREATE TABLE catalog.categories (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,

    name VARCHAR(50) NOT NULL,
    description TEXT,
    icon VARCHAR(50),
    color VARCHAR(7), -- hex color
    sort_order INTEGER DEFAULT 0,

    status VARCHAR(20) DEFAULT 'active',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),

    UNIQUE(tenant_id, name)
);

-- RLS
ALTER TABLE catalog.categories ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON catalog.categories
    USING (tenant_id = current_setting('app.current_tenant')::UUID);

products

Catalogo de productos.

CREATE TABLE catalog.products (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
    category_id UUID REFERENCES catalog.categories(id) ON DELETE SET NULL,

    -- Identificacion
    name VARCHAR(100) NOT NULL,
    description TEXT,
    sku VARCHAR(50),
    barcode VARCHAR(50),

    -- Precios
    price DECIMAL(10,2) NOT NULL,
    cost_price DECIMAL(10,2), -- precio de compra
    compare_price DECIMAL(10,2), -- precio anterior/tachado

    -- Inventario
    track_inventory BOOLEAN DEFAULT true,
    stock_quantity INTEGER DEFAULT 0,
    low_stock_threshold INTEGER DEFAULT 5,

    -- Presentacion
    unit VARCHAR(20) DEFAULT 'pieza', -- pieza, kg, litro, etc.

    -- Multimedia
    image_url TEXT,

    -- Estado
    status VARCHAR(20) DEFAULT 'active',
    is_featured BOOLEAN DEFAULT false,

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_products_tenant ON catalog.products(tenant_id);
CREATE INDEX idx_products_category ON catalog.products(category_id);
CREATE INDEX idx_products_barcode ON catalog.products(tenant_id, barcode);
CREATE INDEX idx_products_name ON catalog.products USING gin(to_tsvector('spanish', name));

-- RLS
ALTER TABLE catalog.products ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON catalog.products
    USING (tenant_id = current_setting('app.current_tenant')::UUID);

product_templates

Templates de productos por proveedor (Bimbo, Coca-Cola, etc.).

CREATE TABLE catalog.product_templates (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Proveedor
    provider_name VARCHAR(50) NOT NULL, -- bimbo, cocacola, sabritas, etc.
    provider_logo_url TEXT,

    -- Producto
    name VARCHAR(100) NOT NULL,
    description TEXT,
    barcode VARCHAR(50),
    suggested_price DECIMAL(10,2),
    category_suggestion VARCHAR(50),

    -- Presentaciones
    unit VARCHAR(20) DEFAULT 'pieza',

    -- Multimedia
    image_url TEXT,

    -- Metadata
    business_types TEXT[], -- ['abarrotes', 'tienda']
    popularity INTEGER DEFAULT 0,

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_templates_provider ON catalog.product_templates(provider_name);
CREATE INDEX idx_templates_barcode ON catalog.product_templates(barcode);

Schema: sales

sales

Registro de ventas.

CREATE TABLE sales.sales (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,

    -- Numeracion
    ticket_number VARCHAR(20) NOT NULL,

    -- Montos
    subtotal DECIMAL(10,2) NOT NULL,
    discount_amount DECIMAL(10,2) DEFAULT 0,
    discount_percent DECIMAL(5,2) DEFAULT 0,
    tax_amount DECIMAL(10,2) DEFAULT 0,
    total DECIMAL(10,2) NOT NULL,

    -- Pago
    payment_method VARCHAR(20) NOT NULL, -- cash, card_mercadopago, card_clip, codi, transfer, fiado
    payment_status VARCHAR(20) DEFAULT 'completed', -- pending, completed, refunded
    payment_reference TEXT, -- referencia externa del pago

    -- Efectivo
    cash_received DECIMAL(10,2),
    change_amount DECIMAL(10,2),

    -- Cliente (opcional)
    customer_id UUID REFERENCES customers.customers(id),

    -- Fiado (si aplica)
    is_fiado BOOLEAN DEFAULT false,
    fiado_id UUID,

    -- Usuario que registro
    created_by UUID REFERENCES auth.users(id),

    -- Notas
    notes TEXT,

    -- Estado
    status VARCHAR(20) DEFAULT 'completed', -- completed, cancelled, refunded
    cancelled_at TIMESTAMPTZ,
    cancelled_reason TEXT,

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_sales_tenant ON sales.sales(tenant_id);
CREATE INDEX idx_sales_ticket ON sales.sales(tenant_id, ticket_number);
CREATE INDEX idx_sales_date ON sales.sales(tenant_id, created_at);
CREATE INDEX idx_sales_customer ON sales.sales(customer_id);

-- RLS
ALTER TABLE sales.sales ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON sales.sales
    USING (tenant_id = current_setting('app.current_tenant')::UUID);

sale_items

Detalle de productos vendidos.

CREATE TABLE sales.sale_items (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    sale_id UUID NOT NULL REFERENCES sales.sales(id) ON DELETE CASCADE,
    product_id UUID REFERENCES catalog.products(id),

    -- Producto (snapshot)
    product_name VARCHAR(100) NOT NULL,
    product_sku VARCHAR(50),

    -- Cantidades
    quantity DECIMAL(10,3) NOT NULL,
    unit_price DECIMAL(10,2) NOT NULL,

    -- Descuento por item
    discount_amount DECIMAL(10,2) DEFAULT 0,

    -- Total
    subtotal DECIMAL(10,2) NOT NULL,

    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_sale_items_sale ON sales.sale_items(sale_id);
CREATE INDEX idx_sale_items_product ON sales.sale_items(product_id);

payments

Registro de pagos (para pagos parciales o multiples metodos).

CREATE TABLE sales.payments (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
    sale_id UUID REFERENCES sales.sales(id),
    fiado_id UUID, -- Si es pago de fiado
    subscription_id UUID, -- Si es pago de suscripcion

    -- Metodo
    method VARCHAR(20) NOT NULL,
    provider VARCHAR(20), -- mercadopago, clip, stripe, oxxo

    -- Montos
    amount DECIMAL(10,2) NOT NULL,
    fee_amount DECIMAL(10,2) DEFAULT 0, -- comision del proveedor
    net_amount DECIMAL(10,2), -- monto neto

    -- Referencias
    external_id TEXT, -- ID del proveedor
    external_status VARCHAR(20),
    receipt_url TEXT,

    -- Estado
    status VARCHAR(20) DEFAULT 'pending', -- pending, completed, failed, refunded

    -- Metadata
    metadata JSONB,

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_payments_tenant ON sales.payments(tenant_id);
CREATE INDEX idx_payments_sale ON sales.payments(sale_id);
CREATE INDEX idx_payments_external ON sales.payments(external_id);

daily_closures

Cortes de caja.

CREATE TABLE sales.daily_closures (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,

    -- Periodo
    closure_date DATE NOT NULL,
    opened_at TIMESTAMPTZ,
    closed_at TIMESTAMPTZ,

    -- Montos esperados (calculados)
    expected_cash DECIMAL(10,2) DEFAULT 0,
    expected_card DECIMAL(10,2) DEFAULT 0,
    expected_other DECIMAL(10,2) DEFAULT 0,
    expected_total DECIMAL(10,2) DEFAULT 0,

    -- Montos reales (ingresados)
    actual_cash DECIMAL(10,2),
    actual_card DECIMAL(10,2),
    actual_other DECIMAL(10,2),
    actual_total DECIMAL(10,2),

    -- Diferencia
    cash_difference DECIMAL(10,2),

    -- Resumen
    total_sales INTEGER DEFAULT 0,
    total_cancelled INTEGER DEFAULT 0,
    total_fiados DECIMAL(10,2) DEFAULT 0,

    -- Usuario
    closed_by UUID REFERENCES auth.users(id),
    notes TEXT,

    -- Estado
    status VARCHAR(20) DEFAULT 'open', -- open, closed

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),

    UNIQUE(tenant_id, closure_date)
);

Schema: inventory

inventory_movements

Movimientos de inventario.

CREATE TABLE inventory.inventory_movements (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
    product_id UUID NOT NULL REFERENCES catalog.products(id) ON DELETE CASCADE,

    -- Tipo
    movement_type VARCHAR(20) NOT NULL, -- purchase, sale, adjustment, loss, return

    -- Cantidades
    quantity DECIMAL(10,3) NOT NULL, -- positivo o negativo
    previous_stock DECIMAL(10,3) NOT NULL,
    new_stock DECIMAL(10,3) NOT NULL,

    -- Costo (para compras)
    unit_cost DECIMAL(10,2),
    total_cost DECIMAL(10,2),

    -- Referencia
    reference_type VARCHAR(20), -- sale, purchase_order, manual
    reference_id UUID,

    -- Notas
    notes TEXT,

    -- Usuario
    created_by UUID REFERENCES auth.users(id),

    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_inventory_tenant ON inventory.inventory_movements(tenant_id);
CREATE INDEX idx_inventory_product ON inventory.inventory_movements(product_id);
CREATE INDEX idx_inventory_date ON inventory.inventory_movements(created_at);

stock_alerts

Alertas de stock bajo.

CREATE TABLE inventory.stock_alerts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
    product_id UUID NOT NULL REFERENCES catalog.products(id) ON DELETE CASCADE,

    -- Niveles
    current_stock INTEGER NOT NULL,
    threshold INTEGER NOT NULL,

    -- Estado
    status VARCHAR(20) DEFAULT 'active', -- active, resolved, ignored

    -- Notificacion
    notified_at TIMESTAMPTZ,
    resolved_at TIMESTAMPTZ,

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

Schema: customers

customers

Clientes del negocio.

CREATE TABLE customers.customers (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,

    -- Identificacion
    name VARCHAR(100) NOT NULL,
    phone VARCHAR(20),
    email VARCHAR(100),

    -- Direccion (para entregas)
    address TEXT,
    address_reference TEXT,
    latitude DECIMAL(10,8),
    longitude DECIMAL(11,8),

    -- Fiados
    fiado_enabled BOOLEAN DEFAULT true,
    fiado_limit DECIMAL(10,2),
    current_fiado_balance DECIMAL(10,2) DEFAULT 0,

    -- Estadisticas
    total_purchases DECIMAL(12,2) DEFAULT 0,
    purchase_count INTEGER DEFAULT 0,
    last_purchase_at TIMESTAMPTZ,

    -- WhatsApp
    whatsapp_opt_in BOOLEAN DEFAULT false,

    -- Notas
    notes TEXT,

    -- Estado
    status VARCHAR(20) DEFAULT 'active',

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_customers_tenant ON customers.customers(tenant_id);
CREATE INDEX idx_customers_phone ON customers.customers(tenant_id, phone);
CREATE INDEX idx_customers_name ON customers.customers USING gin(to_tsvector('spanish', name));

-- RLS
ALTER TABLE customers.customers ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON customers.customers
    USING (tenant_id = current_setting('app.current_tenant')::UUID);

fiados

Registro de fiados (creditos a clientes).

CREATE TABLE customers.fiados (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
    customer_id UUID NOT NULL REFERENCES customers.customers(id) ON DELETE CASCADE,
    sale_id UUID REFERENCES sales.sales(id),

    -- Monto
    original_amount DECIMAL(10,2) NOT NULL,
    paid_amount DECIMAL(10,2) DEFAULT 0,
    remaining_amount DECIMAL(10,2) NOT NULL,

    -- Fechas
    due_date DATE,

    -- Estado
    status VARCHAR(20) DEFAULT 'pending', -- pending, partial, paid, overdue, cancelled

    -- Notas
    description TEXT,

    -- Recordatorios
    last_reminder_at TIMESTAMPTZ,
    reminder_count INTEGER DEFAULT 0,

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_fiados_tenant ON customers.fiados(tenant_id);
CREATE INDEX idx_fiados_customer ON customers.fiados(customer_id);
CREATE INDEX idx_fiados_status ON customers.fiados(status);

fiado_payments

Pagos de fiados.

CREATE TABLE customers.fiado_payments (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    fiado_id UUID NOT NULL REFERENCES customers.fiados(id) ON DELETE CASCADE,

    amount DECIMAL(10,2) NOT NULL,
    payment_method VARCHAR(20) NOT NULL,

    notes TEXT,

    created_by UUID REFERENCES auth.users(id),
    created_at TIMESTAMPTZ DEFAULT NOW()
);

Schema: orders

orders

Pedidos de clientes (via WhatsApp u otros).

CREATE TABLE orders.orders (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
    customer_id UUID REFERENCES customers.customers(id),

    -- Numeracion
    order_number VARCHAR(20) NOT NULL,

    -- Canal
    channel VARCHAR(20) NOT NULL, -- whatsapp, app, web

    -- Montos
    subtotal DECIMAL(10,2) NOT NULL,
    delivery_fee DECIMAL(10,2) DEFAULT 0,
    discount_amount DECIMAL(10,2) DEFAULT 0,
    total DECIMAL(10,2) NOT NULL,

    -- Tipo
    order_type VARCHAR(20) NOT NULL, -- pickup, delivery

    -- Entrega
    delivery_address TEXT,
    delivery_notes TEXT,
    estimated_delivery_at TIMESTAMPTZ,

    -- Estado
    status VARCHAR(20) DEFAULT 'pending',
    -- pending, confirmed, preparing, ready, delivering, completed, cancelled

    -- Pago
    payment_status VARCHAR(20) DEFAULT 'pending', -- pending, paid, refunded
    payment_method VARCHAR(20),

    -- Timestamps
    confirmed_at TIMESTAMPTZ,
    preparing_at TIMESTAMPTZ,
    ready_at TIMESTAMPTZ,
    completed_at TIMESTAMPTZ,
    cancelled_at TIMESTAMPTZ,
    cancelled_reason TEXT,

    -- Notas
    customer_notes TEXT,
    internal_notes TEXT,

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_orders_tenant ON orders.orders(tenant_id);
CREATE INDEX idx_orders_customer ON orders.orders(customer_id);
CREATE INDEX idx_orders_status ON orders.orders(status);
CREATE INDEX idx_orders_date ON orders.orders(created_at);

order_items

Items del pedido.

CREATE TABLE orders.order_items (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    order_id UUID NOT NULL REFERENCES orders.orders(id) ON DELETE CASCADE,
    product_id UUID REFERENCES catalog.products(id),

    -- Producto (snapshot)
    product_name VARCHAR(100) NOT NULL,

    -- Cantidades
    quantity DECIMAL(10,3) NOT NULL,
    unit_price DECIMAL(10,2) NOT NULL,
    subtotal DECIMAL(10,2) NOT NULL,

    -- Notas especiales
    notes TEXT,

    created_at TIMESTAMPTZ DEFAULT NOW()
);

Schema: subscriptions

plans

Planes de suscripcion.

CREATE TABLE subscriptions.plans (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Identificacion
    name VARCHAR(50) NOT NULL,
    code VARCHAR(20) UNIQUE NOT NULL, -- changarrito, tiendita
    description TEXT,

    -- Precio
    price_monthly DECIMAL(10,2) NOT NULL,
    price_yearly DECIMAL(10,2),
    currency VARCHAR(3) DEFAULT 'MXN',

    -- Incluido
    included_tokens INTEGER NOT NULL, -- tokens IA incluidos
    features JSONB, -- lista de features

    -- Limites
    max_products INTEGER,
    max_users INTEGER DEFAULT 1,
    whatsapp_own_number BOOLEAN DEFAULT false,

    -- Estado
    status VARCHAR(20) DEFAULT 'active',

    -- Stripe
    stripe_price_id_monthly VARCHAR(100),
    stripe_price_id_yearly VARCHAR(100),

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Insertar planes iniciales
INSERT INTO subscriptions.plans (name, code, price_monthly, included_tokens, max_products, features) VALUES
('Changarrito', 'changarrito', 99.00, 500, 100, '{"pos": true, "inventory": true, "reports_basic": true}'),
('Tiendita', 'tiendita', 199.00, 2000, null, '{"pos": true, "inventory": true, "reports_advanced": true, "whatsapp_own": true, "customers": true, "fiados": true}');

subscriptions

Suscripciones activas.

CREATE TABLE subscriptions.subscriptions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
    plan_id UUID NOT NULL REFERENCES subscriptions.plans(id),

    -- Periodo
    billing_cycle VARCHAR(10) DEFAULT 'monthly', -- monthly, yearly
    current_period_start TIMESTAMPTZ NOT NULL,
    current_period_end TIMESTAMPTZ NOT NULL,

    -- Estado
    status VARCHAR(20) DEFAULT 'active', -- trialing, active, past_due, cancelled, paused
    cancel_at_period_end BOOLEAN DEFAULT false,
    cancelled_at TIMESTAMPTZ,

    -- Pagos
    payment_method VARCHAR(20), -- card, oxxo, iap_ios, iap_android

    -- Stripe
    stripe_subscription_id VARCHAR(100),
    stripe_customer_id VARCHAR(100),

    -- Trial
    trial_ends_at TIMESTAMPTZ,

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_subscriptions_tenant ON subscriptions.subscriptions(tenant_id);
CREATE INDEX idx_subscriptions_stripe ON subscriptions.subscriptions(stripe_subscription_id);

token_packages

Paquetes de tokens para compra.

CREATE TABLE subscriptions.token_packages (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    name VARCHAR(50) NOT NULL,
    tokens INTEGER NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    currency VARCHAR(3) DEFAULT 'MXN',

    -- Bonus
    bonus_tokens INTEGER DEFAULT 0,

    -- Stripe
    stripe_price_id VARCHAR(100),

    -- Estado
    status VARCHAR(20) DEFAULT 'active',

    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Insertar paquetes
INSERT INTO subscriptions.token_packages (name, tokens, price) VALUES
('Recarga Basica', 1000, 29.00),
('Recarga Plus', 3000, 69.00),
('Recarga Pro', 8000, 149.00),
('Recarga Mega', 20000, 299.00);

token_usage

Consumo de tokens.

CREATE TABLE subscriptions.token_usage (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,

    -- Tokens
    tokens_used INTEGER NOT NULL,

    -- Contexto
    action VARCHAR(50) NOT NULL, -- chat, report, ocr, transcription
    description TEXT,

    -- LLM info
    model VARCHAR(50),
    input_tokens INTEGER,
    output_tokens INTEGER,

    -- Referencia
    reference_type VARCHAR(20),
    reference_id UUID,

    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_token_usage_tenant ON subscriptions.token_usage(tenant_id);
CREATE INDEX idx_token_usage_date ON subscriptions.token_usage(created_at);

tenant_token_balance

Balance de tokens por tenant.

CREATE TABLE subscriptions.tenant_token_balance (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,

    -- Balance
    available_tokens INTEGER DEFAULT 0,
    used_tokens INTEGER DEFAULT 0,

    -- Ultimo reset (mensual)
    last_reset_at TIMESTAMPTZ,

    updated_at TIMESTAMPTZ DEFAULT NOW(),

    UNIQUE(tenant_id)
);

Schema: messaging

conversations

Conversaciones de WhatsApp.

CREATE TABLE messaging.conversations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID REFERENCES public.tenants(id), -- null si es plataforma

    -- Participante
    phone_number VARCHAR(20) NOT NULL,
    contact_name VARCHAR(100),

    -- Tipo
    conversation_type VARCHAR(20) NOT NULL, -- owner, customer, support, onboarding

    -- Estado
    status VARCHAR(20) DEFAULT 'active', -- active, archived, blocked

    -- Ultimo mensaje
    last_message_at TIMESTAMPTZ,
    last_message_preview TEXT,
    unread_count INTEGER DEFAULT 0,

    -- WhatsApp
    wa_conversation_id VARCHAR(100),

    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_conversations_tenant ON messaging.conversations(tenant_id);
CREATE INDEX idx_conversations_phone ON messaging.conversations(phone_number);

messages

Mensajes individuales.

CREATE TABLE messaging.messages (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    conversation_id UUID NOT NULL REFERENCES messaging.conversations(id) ON DELETE CASCADE,

    -- Direccion
    direction VARCHAR(10) NOT NULL, -- inbound, outbound

    -- Contenido
    message_type VARCHAR(20) NOT NULL, -- text, image, audio, video, document, location
    content TEXT,
    media_url TEXT,
    media_mime_type VARCHAR(50),

    -- LLM (si fue procesado)
    processed_by_llm BOOLEAN DEFAULT false,
    llm_response_id UUID,
    tokens_used INTEGER,

    -- WhatsApp
    wa_message_id VARCHAR(100),
    wa_status VARCHAR(20), -- sent, delivered, read, failed
    wa_timestamp TIMESTAMPTZ,

    -- Error
    error_code VARCHAR(20),
    error_message TEXT,

    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_messages_conversation ON messaging.messages(conversation_id);
CREATE INDEX idx_messages_wa ON messaging.messages(wa_message_id);

notifications

Notificaciones push y WhatsApp.

CREATE TABLE messaging.notifications (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
    user_id UUID REFERENCES auth.users(id),

    -- Tipo
    notification_type VARCHAR(50) NOT NULL, -- low_stock, new_order, fiado_reminder, daily_summary

    -- Canales
    channels TEXT[] NOT NULL, -- ['push', 'whatsapp']

    -- Contenido
    title VARCHAR(100) NOT NULL,
    body TEXT NOT NULL,
    data JSONB,

    -- Estado por canal
    push_sent BOOLEAN DEFAULT false,
    push_sent_at TIMESTAMPTZ,
    whatsapp_sent BOOLEAN DEFAULT false,
    whatsapp_sent_at TIMESTAMPTZ,

    -- Lectura
    read_at TIMESTAMPTZ,

    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_notifications_tenant ON messaging.notifications(tenant_id);
CREATE INDEX idx_notifications_user ON messaging.notifications(user_id);

Funciones Utiles

Generador de numeros de ticket

CREATE OR REPLACE FUNCTION sales.generate_ticket_number(p_tenant_id UUID)
RETURNS VARCHAR(20) AS $$
DECLARE
    v_date TEXT;
    v_sequence INTEGER;
    v_ticket VARCHAR(20);
BEGIN
    v_date := TO_CHAR(CURRENT_DATE, 'YYMMDD');

    SELECT COALESCE(MAX(
        CAST(SUBSTRING(ticket_number FROM 8) AS INTEGER)
    ), 0) + 1
    INTO v_sequence
    FROM sales.sales
    WHERE tenant_id = p_tenant_id
      AND ticket_number LIKE v_date || '-%';

    v_ticket := v_date || '-' || LPAD(v_sequence::TEXT, 4, '0');

    RETURN v_ticket;
END;
$$ LANGUAGE plpgsql;

Trigger de actualizacion

CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Aplicar a todas las tablas relevantes
CREATE TRIGGER update_tenants_updated_at
    BEFORE UPDATE ON public.tenants
    FOR EACH ROW EXECUTE FUNCTION update_updated_at();

-- (repetir para otras tablas)

Funcion de balance de fiados

CREATE OR REPLACE FUNCTION customers.update_customer_fiado_balance()
RETURNS TRIGGER AS $$
BEGIN
    UPDATE customers.customers
    SET current_fiado_balance = (
        SELECT COALESCE(SUM(remaining_amount), 0)
        FROM customers.fiados
        WHERE customer_id = COALESCE(NEW.customer_id, OLD.customer_id)
          AND status IN ('pending', 'partial', 'overdue')
    )
    WHERE id = COALESCE(NEW.customer_id, OLD.customer_id);

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER update_fiado_balance
    AFTER INSERT OR UPDATE OR DELETE ON customers.fiados
    FOR EACH ROW EXECUTE FUNCTION customers.update_customer_fiado_balance();

Indices Adicionales para Performance

-- Ventas por fecha (reportes)
CREATE INDEX idx_sales_tenant_date ON sales.sales(tenant_id, DATE(created_at));

-- Productos mas vendidos
CREATE INDEX idx_sale_items_product_count ON sales.sale_items(product_id);

-- Fiados vencidos
CREATE INDEX idx_fiados_overdue ON customers.fiados(tenant_id, due_date)
    WHERE status IN ('pending', 'partial');

-- Busqueda de productos por nombre
CREATE INDEX idx_products_search ON catalog.products
    USING gin(to_tsvector('spanish', name || ' ' || COALESCE(description, '')));

Row Level Security (RLS)

Todas las tablas que manejan datos de tenant tienen RLS habilitado.

-- Configurar tenant en cada request
SET app.current_tenant = 'uuid-del-tenant';

-- Ejemplo de policy
CREATE POLICY tenant_isolation ON catalog.products
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant')::UUID)
    WITH CHECK (tenant_id = current_setting('app.current_tenant')::UUID);

Version: 1.0.0 Fecha: 2026-01-04