# 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. ```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) ); ``` --- ## 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) ); ``` --- ## 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) ); ``` --- ## 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); ``` --- ## 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(); ``` --- ## 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); ``` --- **Version:** 1.0.0 **Fecha:** 2026-01-04