--- id: SPEC-ARQUITECTURA-DATABASE type: Specification title: "MiChangarrito - Arquitectura de Base de Datos" status: Published created_at: 2026-01-04 updated_at: 2026-01-10 simco_version: "3.8.0" author: "Equipo MiChangarrito" tags: - database - postgresql - arquitectura - multi-tenant - rls --- # 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, tenant_integration_credentials | | `auth` | Autenticacion y usuarios | users, sessions, otp_codes | | `catalog` | Productos y categorias | products, categories, product_templates | | `sales` | Ventas, pagos y CoDi/SPEI | sales, sale_items, payments, daily_closures, codi_transactions | | `inventory` | Stock y movimientos | inventory_movements, stock_alerts | | `customers` | Clientes y fiados | customers, fiados, fiado_payments | | `orders` | Pedidos de clientes | orders, order_items | | `subscriptions` | Planes, tokens y referidos | plans, subscriptions, token_usage, referral_codes | | `messaging` | WhatsApp y notificaciones | conversations, messages, notifications | | `billing` | Facturacion electronica SAT | tax_configs, invoices, invoice_items | | `marketplace` | Marketplace B2B proveedores | suppliers, supplier_products, supplier_orders | | `integrations` | Configuracion de integraciones | integration_configs, integration_logs | **Total: 12 schemas, ~49 tablas** --- ## Schema: public ### tenants Tabla raiz multi-tenant. ```sql 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. ```sql 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) ); ``` ### tenant_integration_credentials Credenciales de integraciones externas por tenant. ```sql CREATE TABLE public.tenant_integration_credentials ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, -- Tipo de integracion integration_type VARCHAR(20) NOT NULL, -- whatsapp, llm, stripe, mercadopago, clip provider VARCHAR(50) NOT NULL, -- meta, openai, openrouter, anthropic, ollama, azure_openai -- Credenciales (encriptadas) credentials JSONB NOT NULL, -- api_key, access_token, etc. -- Estado is_active BOOLEAN DEFAULT true, last_validated_at TIMESTAMPTZ, validation_error TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(tenant_id, integration_type, provider) ); CREATE INDEX idx_integration_credentials_tenant ON public.tenant_integration_credentials(tenant_id); CREATE INDEX idx_integration_credentials_type ON public.tenant_integration_credentials(integration_type); ``` ### tenant_whatsapp_numbers Numeros de WhatsApp configurados por tenant. ```sql CREATE TABLE public.tenant_whatsapp_numbers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, -- Numero phone_number VARCHAR(20) NOT NULL, phone_number_id VARCHAR(50), -- ID de Meta display_name VARCHAR(100), -- Estado quality_rating VARCHAR(20), -- green, yellow, red is_verified BOOLEAN DEFAULT false, is_primary BOOLEAN DEFAULT false, -- Configuracion webhook_url TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(tenant_id, phone_number) ); CREATE INDEX idx_whatsapp_numbers_tenant ON public.tenant_whatsapp_numbers(tenant_id); ``` --- ## Schema: auth ### users Usuarios del sistema (duenos y empleados). ```sql 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. ```sql 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. ```sql 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. ```sql 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. ```sql 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.). ```sql 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. ```sql 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. ```sql 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). ```sql 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. ```sql 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) ); ``` ### virtual_accounts Cuentas CLABE virtuales para recibir SPEI. ```sql CREATE TABLE sales.virtual_accounts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, -- CLABE clabe VARCHAR(18) NOT NULL UNIQUE, bank_name VARCHAR(50), holder_name VARCHAR(100), -- Proveedor provider VARCHAR(50) NOT NULL, -- stp, arcus, openpay -- Estado is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_virtual_accounts_tenant ON sales.virtual_accounts(tenant_id); CREATE INDEX idx_virtual_accounts_clabe ON sales.virtual_accounts(clabe); ``` ### codi_transactions Transacciones de pago CoDi. ```sql CREATE TABLE sales.codi_transactions ( 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), -- Referencia reference VARCHAR(50) NOT NULL UNIQUE, amount DECIMAL(10,2) NOT NULL, description VARCHAR(100), -- QR qr_data TEXT NOT NULL, qr_url TEXT, -- Estado status VARCHAR(20) DEFAULT 'pending', -- pending, completed, expired, failed -- Fechas completed_at TIMESTAMPTZ, expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_codi_transactions_tenant ON sales.codi_transactions(tenant_id); CREATE INDEX idx_codi_transactions_reference ON sales.codi_transactions(reference); CREATE INDEX idx_codi_transactions_status ON sales.codi_transactions(status); ``` ### spei_transactions Transacciones SPEI recibidas. ```sql CREATE TABLE sales.spei_transactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, virtual_account_id UUID REFERENCES sales.virtual_accounts(id), sale_id UUID REFERENCES sales.sales(id), -- Datos SPEI clave_rastreo VARCHAR(30) NOT NULL UNIQUE, sender_clabe VARCHAR(18), sender_name VARCHAR(100), sender_rfc VARCHAR(13), amount DECIMAL(10,2) NOT NULL, concept VARCHAR(100), -- Estado status VARCHAR(20) DEFAULT 'received', -- received, processed, returned -- Fechas received_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_spei_transactions_tenant ON sales.spei_transactions(tenant_id); CREATE INDEX idx_spei_transactions_clave ON sales.spei_transactions(clave_rastreo); ``` ### payment_config Configuracion de pagos CoDi/SPEI por tenant. ```sql CREATE TABLE sales.payment_config ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, -- Habilitacion codi_enabled BOOLEAN DEFAULT false, spei_enabled BOOLEAN DEFAULT false, -- Proveedor provider VARCHAR(50), -- stp, arcus, openpay credentials JSONB, -- Credenciales encriptadas created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(tenant_id) ); ``` --- ## Schema: inventory ### inventory_movements Movimientos de inventario. ```sql 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. ```sql 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. ```sql 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). ```sql 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. ```sql 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). ```sql 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. ```sql 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. ```sql 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. ```sql 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. ```sql 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. ```sql 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. ```sql 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) ); ``` ### referral_codes Codigos de referido por tenant. ```sql CREATE TABLE subscriptions.referral_codes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, -- Codigo code VARCHAR(10) NOT NULL UNIQUE, -- Beneficios discount_percent INTEGER DEFAULT 10, -- % descuento para referido reward_months INTEGER DEFAULT 1, -- Meses gratis para referidor -- Limites max_uses INTEGER, -- null = sin limite current_uses INTEGER DEFAULT 0, -- Estado is_active BOOLEAN DEFAULT true, expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_referral_codes_tenant ON subscriptions.referral_codes(tenant_id); CREATE INDEX idx_referral_codes_code ON subscriptions.referral_codes(code); ``` ### referrals Registro de referidos. ```sql CREATE TABLE subscriptions.referrals ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Relacion referrer_tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, referred_tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, code_id UUID NOT NULL REFERENCES subscriptions.referral_codes(id), -- Estado status VARCHAR(20) DEFAULT 'pending', -- pending, active, expired, rewarded -- Fechas referred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), activated_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(referred_tenant_id) -- Un tenant solo puede ser referido una vez ); CREATE INDEX idx_referrals_referrer ON subscriptions.referrals(referrer_tenant_id); CREATE INDEX idx_referrals_status ON subscriptions.referrals(status); ``` ### referral_rewards Recompensas otorgadas por referidos. ```sql CREATE TABLE subscriptions.referral_rewards ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), referral_id UUID NOT NULL REFERENCES subscriptions.referrals(id) ON DELETE CASCADE, tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, -- Tipo de recompensa reward_type VARCHAR(20) NOT NULL, -- free_month, discount -- Valor value DECIMAL(10,2) NOT NULL, -- Meses o porcentaje -- Estado status VARCHAR(20) DEFAULT 'pending', -- pending, applied, expired -- Fechas applied_at TIMESTAMPTZ, expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_referral_rewards_tenant ON subscriptions.referral_rewards(tenant_id); CREATE INDEX idx_referral_rewards_status ON subscriptions.referral_rewards(status); ``` --- ## Schema: messaging ### conversations Conversaciones de WhatsApp. ```sql 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. ```sql 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. ```sql 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); ``` --- ## Schema: billing ### tax_configs Configuracion fiscal por tenant (RFC, CSD, PAC). ```sql CREATE TABLE billing.tax_configs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, -- Datos fiscales rfc VARCHAR(13) NOT NULL, razon_social VARCHAR(200) NOT NULL, regimen_fiscal VARCHAR(10) NOT NULL, -- Codigo SAT codigo_postal VARCHAR(5) NOT NULL, -- Certificado de Sello Digital (CSD) csd_cer TEXT, -- Certificado en base64 csd_key TEXT, -- Llave privada encriptada csd_password TEXT, -- Password encriptado -- PAC pac_provider VARCHAR(50), -- facturapi, sw_sapien, etc pac_credentials JSONB, -- Credenciales del PAC -- Estado is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(tenant_id) ); ``` ### invoices Facturas CFDI emitidas. ```sql CREATE TABLE billing.invoices ( 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), -- Folio folio VARCHAR(20) NOT NULL, serie VARCHAR(10), uuid_fiscal VARCHAR(36), -- UUID del SAT -- Tipo tipo_comprobante VARCHAR(1) NOT NULL DEFAULT 'I', -- I=Ingreso, E=Egreso, T=Traslado -- Receptor receptor_rfc VARCHAR(13) NOT NULL, receptor_nombre VARCHAR(200) NOT NULL, receptor_regimen VARCHAR(10), receptor_uso_cfdi VARCHAR(10) NOT NULL, -- G03, etc -- Montos subtotal DECIMAL(12,2) NOT NULL, total_impuestos DECIMAL(12,2) DEFAULT 0, total DECIMAL(12,2) NOT NULL, -- Pago moneda VARCHAR(3) DEFAULT 'MXN', tipo_cambio DECIMAL(10,4) DEFAULT 1, forma_pago VARCHAR(2) NOT NULL, -- 01=Efectivo, 04=Tarjeta, 03=Transferencia metodo_pago VARCHAR(3) NOT NULL, -- PUE o PPD -- XML xml_content TEXT, pdf_url TEXT, -- Estado status VARCHAR(20) DEFAULT 'draft', -- draft, stamped, cancelled, sent -- Cancelacion cancelled_at TIMESTAMPTZ, cancellation_reason VARCHAR(2), cancellation_uuid VARCHAR(36), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(tenant_id, serie, folio) ); CREATE INDEX idx_invoices_tenant ON billing.invoices(tenant_id); CREATE INDEX idx_invoices_uuid ON billing.invoices(uuid_fiscal); CREATE INDEX idx_invoices_status ON billing.invoices(status); ``` ### invoice_items Items de factura. ```sql CREATE TABLE billing.invoice_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), invoice_id UUID NOT NULL REFERENCES billing.invoices(id) ON DELETE CASCADE, -- Producto SAT clave_prod_serv VARCHAR(8) NOT NULL, -- Clave SAT clave_unidad VARCHAR(3) NOT NULL, -- H87=Pieza, KGM=Kg, etc descripcion VARCHAR(500) NOT NULL, -- Cantidades cantidad DECIMAL(10,3) NOT NULL, valor_unitario DECIMAL(12,4) NOT NULL, importe DECIMAL(12,2) NOT NULL, -- Descuento descuento DECIMAL(12,2) DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_invoice_items_invoice ON billing.invoice_items(invoice_id); ``` ### invoice_item_taxes Impuestos por item de factura. ```sql CREATE TABLE billing.invoice_item_taxes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), invoice_item_id UUID NOT NULL REFERENCES billing.invoice_items(id) ON DELETE CASCADE, -- Tipo tipo VARCHAR(10) NOT NULL, -- traslado o retencion impuesto VARCHAR(3) NOT NULL, -- 002=IVA, 003=IEPS tipo_factor VARCHAR(10) NOT NULL, -- Tasa, Cuota, Exento -- Valores tasa_o_cuota DECIMAL(6,4) NOT NULL, -- 0.16 para IVA 16% importe DECIMAL(12,2) NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); ``` ### invoice_history Historial de cambios en facturas (auditoria). ```sql CREATE TABLE billing.invoice_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), invoice_id UUID NOT NULL REFERENCES billing.invoices(id) ON DELETE CASCADE, -- Accion action VARCHAR(20) NOT NULL, -- created, stamped, cancelled, sent -- Detalles details JSONB, -- Usuario performed_by UUID REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_invoice_history_invoice ON billing.invoice_history(invoice_id); ``` --- ## Schema: marketplace ### suppliers Proveedores/distribuidores del marketplace B2B. ```sql CREATE TABLE marketplace.suppliers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Identificacion name VARCHAR(200) NOT NULL, slug VARCHAR(100) UNIQUE NOT NULL, description TEXT, logo_url TEXT, -- Contacto contact_phone VARCHAR(20), contact_email VARCHAR(100), contact_name VARCHAR(100), -- Direccion address TEXT, city VARCHAR(50), state VARCHAR(50), zip_codes_served TEXT[], -- CPs que atiende -- Condiciones min_order_amount DECIMAL(10,2) DEFAULT 0, delivery_days INTEGER DEFAULT 3, delivery_fee DECIMAL(10,2) DEFAULT 0, -- Metricas rating DECIMAL(3,2) DEFAULT 0, total_orders INTEGER DEFAULT 0, total_reviews INTEGER DEFAULT 0, -- Estado is_verified BOOLEAN DEFAULT false, is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_suppliers_slug ON marketplace.suppliers(slug); CREATE INDEX idx_suppliers_active ON marketplace.suppliers(is_active); CREATE INDEX idx_suppliers_zip ON marketplace.suppliers USING gin(zip_codes_served); ``` ### supplier_products Catalogo de productos de proveedores. ```sql CREATE TABLE marketplace.supplier_products ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), supplier_id UUID NOT NULL REFERENCES marketplace.suppliers(id) ON DELETE CASCADE, -- Producto name VARCHAR(200) NOT NULL, description TEXT, sku VARCHAR(50), barcode VARCHAR(50), -- Precios price DECIMAL(10,2) NOT NULL, -- Precio mayoreo suggested_retail_price DECIMAL(10,2), -- Precio sugerido -- Presentacion unit VARCHAR(20) DEFAULT 'pieza', min_quantity INTEGER DEFAULT 1, -- Categoria category VARCHAR(50), -- Imagen image_url TEXT, -- Estado is_available BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_supplier_products_supplier ON marketplace.supplier_products(supplier_id); CREATE INDEX idx_supplier_products_barcode ON marketplace.supplier_products(barcode); CREATE INDEX idx_supplier_products_available ON marketplace.supplier_products(is_available); ``` ### supplier_orders Pedidos a proveedores. ```sql CREATE TABLE marketplace.supplier_orders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, supplier_id UUID NOT NULL REFERENCES marketplace.suppliers(id), -- Numero order_number VARCHAR(20) NOT NULL, -- Montos subtotal DECIMAL(12,2) NOT NULL, delivery_fee DECIMAL(10,2) DEFAULT 0, total DECIMAL(12,2) NOT NULL, -- Estado status VARCHAR(20) DEFAULT 'pending', -- pending, confirmed, shipped, delivered, cancelled -- Entrega shipping_address TEXT NOT NULL, shipping_notes TEXT, estimated_delivery DATE, delivered_at TIMESTAMPTZ, -- Notas notes TEXT, -- Cancelacion cancelled_at TIMESTAMPTZ, cancellation_reason TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_supplier_orders_tenant ON marketplace.supplier_orders(tenant_id); CREATE INDEX idx_supplier_orders_supplier ON marketplace.supplier_orders(supplier_id); CREATE INDEX idx_supplier_orders_status ON marketplace.supplier_orders(status); ``` ### supplier_order_items Items de pedidos a proveedores. ```sql CREATE TABLE marketplace.supplier_order_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), order_id UUID NOT NULL REFERENCES marketplace.supplier_orders(id) ON DELETE CASCADE, product_id UUID REFERENCES marketplace.supplier_products(id), -- Producto (snapshot) product_name VARCHAR(200) NOT NULL, product_sku VARCHAR(50), -- Cantidades quantity INTEGER NOT NULL, unit_price DECIMAL(10,2) NOT NULL, subtotal DECIMAL(12,2) NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_supplier_order_items_order ON marketplace.supplier_order_items(order_id); ``` ### supplier_reviews Resenas de proveedores. ```sql CREATE TABLE marketplace.supplier_reviews ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, supplier_id UUID NOT NULL REFERENCES marketplace.suppliers(id) ON DELETE CASCADE, order_id UUID REFERENCES marketplace.supplier_orders(id), -- Calificacion rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5), comment TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(tenant_id, order_id) -- Una resena por orden ); CREATE INDEX idx_supplier_reviews_supplier ON marketplace.supplier_reviews(supplier_id); ``` ### supplier_favorites Proveedores favoritos por tenant. ```sql CREATE TABLE marketplace.supplier_favorites ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, supplier_id UUID NOT NULL REFERENCES marketplace.suppliers(id) ON DELETE CASCADE, created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(tenant_id, supplier_id) ); CREATE INDEX idx_supplier_favorites_tenant ON marketplace.supplier_favorites(tenant_id); ``` --- ## Schema: integrations ### integration_configs Configuraciones de integraciones por tenant. ```sql CREATE TABLE integrations.integration_configs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, -- Tipo de integracion integration_type VARCHAR(50) NOT NULL, -- llm, payments, whatsapp, sat, etc. provider VARCHAR(50) NOT NULL, -- openrouter, stripe, meta, etc. -- Configuracion config JSONB NOT NULL DEFAULT '{}', is_active BOOLEAN DEFAULT true, priority INTEGER DEFAULT 0, -- para fallback ordering -- Limites y uso rate_limit_per_minute INTEGER, daily_limit INTEGER, monthly_limit INTEGER, current_daily_usage INTEGER DEFAULT 0, current_monthly_usage INTEGER DEFAULT 0, -- Timestamps last_used_at TIMESTAMPTZ, last_reset_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(tenant_id, integration_type, provider) ); CREATE INDEX idx_integration_configs_tenant ON integrations.integration_configs(tenant_id); CREATE INDEX idx_integration_configs_type ON integrations.integration_configs(integration_type); ``` ### integration_logs Logs de uso de integraciones para auditoria. ```sql CREATE TABLE integrations.integration_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, -- Referencia a config integration_config_id UUID REFERENCES integrations.integration_configs(id), integration_type VARCHAR(50) NOT NULL, provider VARCHAR(50) NOT NULL, -- Detalles de la llamada request_type VARCHAR(100), -- chat_completion, send_message, create_invoice, etc. request_data JSONB, response_data JSONB, -- Resultado status VARCHAR(20) NOT NULL, -- success, error, timeout, rate_limited error_message TEXT, -- Metricas latency_ms INTEGER, tokens_used INTEGER, -- para LLM cost_cents INTEGER, -- costo estimado created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_integration_logs_tenant ON integrations.integration_logs(tenant_id); CREATE INDEX idx_integration_logs_type ON integrations.integration_logs(integration_type); CREATE INDEX idx_integration_logs_created ON integrations.integration_logs(created_at); ``` --- ## Funciones Utiles ### Generador de numeros de ticket ```sql 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 ```sql 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 ```sql 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(); ``` ### Funciones de Sistema de Referidos ```sql -- Generador de codigo de referido unico CREATE OR REPLACE FUNCTION subscriptions.generate_referral_code(p_tenant_id UUID) RETURNS VARCHAR(10) AS $$ DECLARE v_code VARCHAR(10); v_exists BOOLEAN; BEGIN LOOP -- Genera codigo alfanumerico de 8 caracteres v_code := UPPER(SUBSTRING(MD5(RANDOM()::TEXT || CLOCK_TIMESTAMP()::TEXT) FROM 1 FOR 8)); -- Verifica que no exista SELECT EXISTS( SELECT 1 FROM subscriptions.referral_codes WHERE code = v_code ) INTO v_exists; EXIT WHEN NOT v_exists; END LOOP; RETURN v_code; END; $$ LANGUAGE plpgsql; -- Estadisticas de referidos por tenant CREATE OR REPLACE FUNCTION subscriptions.get_referral_stats(p_tenant_id UUID) RETURNS TABLE( total_codes INTEGER, active_codes INTEGER, total_referrals INTEGER, successful_referrals INTEGER, total_rewards_pending DECIMAL, total_rewards_paid DECIMAL ) AS $$ BEGIN RETURN QUERY SELECT COUNT(DISTINCT rc.id)::INTEGER as total_codes, COUNT(DISTINCT rc.id) FILTER (WHERE rc.is_active)::INTEGER as active_codes, COUNT(DISTINCT r.id)::INTEGER as total_referrals, COUNT(DISTINCT r.id) FILTER (WHERE r.status = 'converted')::INTEGER as successful_referrals, COALESCE(SUM(rr.reward_amount) FILTER (WHERE rr.status = 'pending'), 0) as total_rewards_pending, COALESCE(SUM(rr.reward_amount) FILTER (WHERE rr.status = 'paid'), 0) as total_rewards_paid FROM subscriptions.referral_codes rc LEFT JOIN subscriptions.referrals r ON r.referral_code_id = rc.id LEFT JOIN subscriptions.referral_rewards rr ON rr.referral_id = r.id WHERE rc.tenant_id = p_tenant_id; END; $$ LANGUAGE plpgsql; ``` ### Funciones de CoDi/SPEI ```sql -- Generador de referencia CoDi CREATE OR REPLACE FUNCTION sales.generate_codi_reference(p_tenant_id UUID) RETURNS VARCHAR(20) AS $$ DECLARE v_date TEXT; v_sequence INTEGER; v_reference VARCHAR(20); BEGIN v_date := TO_CHAR(NOW(), 'YYYYMMDD'); SELECT COALESCE(MAX( CAST(SUBSTRING(codi_reference FROM 10) AS INTEGER) ), 0) + 1 INTO v_sequence FROM sales.codi_transactions WHERE tenant_id = p_tenant_id AND codi_reference LIKE 'CODI' || v_date || '%'; v_reference := 'CODI' || v_date || LPAD(v_sequence::TEXT, 6, '0'); RETURN v_reference; END; $$ LANGUAGE plpgsql; -- Resumen de transacciones CoDi/SPEI CREATE OR REPLACE FUNCTION sales.get_codi_spei_summary( p_tenant_id UUID, p_start_date TIMESTAMPTZ DEFAULT NOW() - INTERVAL '30 days', p_end_date TIMESTAMPTZ DEFAULT NOW() ) RETURNS TABLE( total_codi_transactions INTEGER, total_codi_amount DECIMAL, total_spei_transactions INTEGER, total_spei_amount DECIMAL, pending_transactions INTEGER ) AS $$ BEGIN RETURN QUERY SELECT COUNT(ct.id)::INTEGER as total_codi_transactions, COALESCE(SUM(ct.amount) FILTER (WHERE ct.status = 'completed'), 0) as total_codi_amount, COUNT(st.id)::INTEGER as total_spei_transactions, COALESCE(SUM(st.amount) FILTER (WHERE st.status = 'completed'), 0) as total_spei_amount, (COUNT(ct.id) FILTER (WHERE ct.status = 'pending') + COUNT(st.id) FILTER (WHERE st.status = 'pending'))::INTEGER as pending_transactions FROM sales.codi_transactions ct FULL OUTER JOIN sales.spei_transactions st ON st.tenant_id = ct.tenant_id WHERE (ct.tenant_id = p_tenant_id OR st.tenant_id = p_tenant_id) AND (ct.created_at BETWEEN p_start_date AND p_end_date OR st.created_at BETWEEN p_start_date AND p_end_date); END; $$ LANGUAGE plpgsql; ``` ### Funciones de Facturacion (CFDI 4.0) ```sql -- Obtener siguiente folio de factura CREATE OR REPLACE FUNCTION billing.get_next_invoice_folio( p_tenant_id UUID, p_serie VARCHAR DEFAULT 'A' ) RETURNS VARCHAR(20) AS $$ DECLARE v_sequence INTEGER; v_folio VARCHAR(20); BEGIN SELECT COALESCE(MAX( CAST(SUBSTRING(folio FROM 2) AS INTEGER) ), 0) + 1 INTO v_sequence FROM billing.invoices WHERE tenant_id = p_tenant_id AND serie = p_serie; v_folio := p_serie || LPAD(v_sequence::TEXT, 8, '0'); RETURN v_folio; END; $$ LANGUAGE plpgsql; -- Resumen de facturacion por periodo CREATE OR REPLACE FUNCTION billing.get_invoice_summary( p_tenant_id UUID, p_start_date TIMESTAMPTZ DEFAULT DATE_TRUNC('month', NOW()), p_end_date TIMESTAMPTZ DEFAULT NOW() ) RETURNS TABLE( total_invoices INTEGER, total_subtotal DECIMAL, total_iva DECIMAL, total_amount DECIMAL, cancelled_invoices INTEGER, pending_timbrado INTEGER ) AS $$ BEGIN RETURN QUERY SELECT COUNT(*)::INTEGER as total_invoices, COALESCE(SUM(subtotal), 0) as total_subtotal, COALESCE(SUM(total_tax), 0) as total_iva, COALESCE(SUM(total_amount), 0) as total_amount, COUNT(*) FILTER (WHERE status = 'cancelled')::INTEGER as cancelled_invoices, COUNT(*) FILTER (WHERE status = 'pending')::INTEGER as pending_timbrado FROM billing.invoices WHERE tenant_id = p_tenant_id AND created_at BETWEEN p_start_date AND p_end_date; END; $$ LANGUAGE plpgsql; ``` ### Funciones de Marketplace ```sql -- Actualizar rating de proveedor CREATE OR REPLACE FUNCTION marketplace.update_supplier_rating() RETURNS TRIGGER AS $$ BEGIN UPDATE marketplace.suppliers SET rating = ( SELECT COALESCE(AVG(sr.rating), 0) FROM marketplace.supplier_reviews sr WHERE sr.supplier_id = COALESCE(NEW.supplier_id, OLD.supplier_id) ), reviews_count = ( SELECT COUNT(*) FROM marketplace.supplier_reviews sr WHERE sr.supplier_id = COALESCE(NEW.supplier_id, OLD.supplier_id) ) WHERE id = COALESCE(NEW.supplier_id, OLD.supplier_id); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trigger_update_supplier_rating AFTER INSERT OR UPDATE OR DELETE ON marketplace.supplier_reviews FOR EACH ROW EXECUTE FUNCTION marketplace.update_supplier_rating(); -- Actualizar contador de ordenes de proveedor CREATE OR REPLACE FUNCTION marketplace.update_supplier_orders_count() RETURNS TRIGGER AS $$ BEGIN UPDATE marketplace.suppliers SET orders_count = ( SELECT COUNT(*) FROM marketplace.supplier_orders so WHERE so.supplier_id = COALESCE(NEW.supplier_id, OLD.supplier_id) AND so.status = 'completed' ) WHERE id = COALESCE(NEW.supplier_id, OLD.supplier_id); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trigger_update_supplier_orders_count AFTER INSERT OR UPDATE ON marketplace.supplier_orders FOR EACH ROW EXECUTE FUNCTION marketplace.update_supplier_orders_count(); -- Buscar proveedores por zona geografica CREATE OR REPLACE FUNCTION marketplace.find_suppliers_by_zone( p_state VARCHAR, p_city VARCHAR DEFAULT NULL, p_category VARCHAR DEFAULT NULL ) RETURNS TABLE( supplier_id UUID, business_name VARCHAR, category VARCHAR, rating DECIMAL, delivery_days INTEGER, minimum_order DECIMAL ) AS $$ BEGIN RETURN QUERY SELECT s.id as supplier_id, s.business_name, s.category, s.rating, s.delivery_days, s.minimum_order FROM marketplace.suppliers s WHERE s.is_active = TRUE AND s.is_verified = TRUE AND (p_state = ANY(s.delivery_zones) OR 'Nacional' = ANY(s.delivery_zones)) AND (p_category IS NULL OR s.category = p_category) ORDER BY s.rating DESC, s.orders_count DESC; END; $$ LANGUAGE plpgsql; -- Estadisticas generales del marketplace CREATE OR REPLACE FUNCTION marketplace.get_marketplace_stats() RETURNS TABLE( total_suppliers INTEGER, verified_suppliers INTEGER, total_products INTEGER, total_orders INTEGER, total_volume DECIMAL, avg_rating DECIMAL ) AS $$ BEGIN RETURN QUERY SELECT (SELECT COUNT(*)::INTEGER FROM marketplace.suppliers WHERE is_active), (SELECT COUNT(*)::INTEGER FROM marketplace.suppliers WHERE is_verified), (SELECT COUNT(*)::INTEGER FROM marketplace.supplier_products WHERE is_active), (SELECT COUNT(*)::INTEGER FROM marketplace.supplier_orders), (SELECT COALESCE(SUM(total_amount), 0) FROM marketplace.supplier_orders WHERE status = 'completed'), (SELECT COALESCE(AVG(rating), 0) FROM marketplace.suppliers WHERE is_verified); END; $$ LANGUAGE plpgsql; ``` --- ## Indices Adicionales para Performance ```sql -- 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. ```sql -- 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); ``` --- ## Extensiones PostgreSQL | Extension | Proposito | |-----------|-----------| | `uuid-ossp` | Generacion de UUIDs | | `pgcrypto` | Encriptacion de datos sensibles | | `pg_trgm` | Busqueda por similitud (fuzzy search) | | `btree_gin` | Indices GIN para busquedas combinadas | --- ## Convenciones ### Columnas Comunes Todas las tablas siguen estas convenciones: | Columna | Tipo | Descripcion | |---------|------|-------------| | `id` | UUID PRIMARY KEY | Identificador unico | | `tenant_id` | UUID NOT NULL | Referencia al tenant (multi-tenant) | | `created_at` | TIMESTAMPTZ DEFAULT NOW() | Fecha de creacion | | `updated_at` | TIMESTAMPTZ DEFAULT NOW() | Fecha de ultima modificacion | | `is_active` | BOOLEAN DEFAULT TRUE | Estado activo/inactivo | ### Indices Estandar - PK en `id` - Indice en `tenant_id` para todas las tablas multi-tenant - Indices en Foreign Keys - Indices en columnas de busqueda frecuente - Indices GIN para busqueda full-text ### Triggers Estandar - `set_updated_at`: Actualiza `updated_at` automaticamente en cada UPDATE - `update_customer_fiado_balance`: Recalcula balance de fiados del cliente --- ## Conexion ### Desarrollo ``` Host: localhost Puerto: 5432 Database: michangarrito_dev Usuario: michangarrito_dev Password: (ver .env) ``` ### Produccion ``` Host: (configurar en .env) Puerto: 5432 Database: michangarrito Usuario: michangarrito SSL: Requerido ``` --- ## Scripts de Base de Datos | Script | Ubicacion | Descripcion | |--------|-----------|-------------| | `create-database.sh` | database/scripts/ | Crear BD desde cero | | `recreate-database.sh` | database/scripts/ | Recrear BD (destructivo) | | `drop-and-recreate-database.sh` | database/scripts/ | Eliminar y recrear BD | ### Orden de Ejecucion de Schemas ``` 00-extensions.sql -> Extensiones PostgreSQL 01-schemas.sql -> Creacion de schemas (11 schemas) 02-functions.sql -> Funciones utilitarias 03-public.sql -> Schema public (tenants, tenant_settings, tenant_integration_credentials, tenant_whatsapp_numbers) 04-auth.sql -> Schema auth 05-catalog.sql -> Schema catalog 06-sales.sql -> Schema sales (incluye virtual_accounts, codi_transactions, spei_transactions, payment_config) 07-inventory.sql -> Schema inventory 08-customers.sql -> Schema customers 09-orders.sql -> Schema orders 10-subscriptions.sql -> Schema subscriptions (incluye referral_codes, referrals, referral_rewards) 11-messaging.sql -> Schema messaging 12-integrations.sql -> Schema integrations 13-billing.sql -> Schema billing (tax_configs, invoices, invoice_items, invoice_item_taxes, invoice_history) 14-marketplace.sql -> Schema marketplace (suppliers, supplier_products, supplier_orders, supplier_order_items, supplier_reviews, supplier_favorites) 15-rls-policies.sql -> Politicas RLS para todos los schemas 16-seed-data.sql -> Datos iniciales (planes, configuraciones) ``` --- ## Referencias - [Environment Inventory](../../orchestration/environment/ENVIRONMENT-INVENTORY.yml) - [Contexto del Proyecto](../../orchestration/00-guidelines/CONTEXTO-PROYECTO.md) - [Database Inventory](../../orchestration/inventarios/DATABASE_INVENTORY.yml) - [Especificacion de Componentes](./ESPECIFICACION-COMPONENTES.md) --- **Version:** 2.2.0 **Fecha:** 2026-01-10 **Actualizado:** Schema integrations agregado - 12 schemas, ~49 tablas completas