# -*- coding: utf-8 -*-
"""Manual Técnico del Sistema (arquitectura, procesos, ficheros y tablas por módulo)."""
import os
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
                                PageBreak, KeepTogether, Flowable)

OUT = os.path.dirname(os.path.abspath(__file__))
MAGENTA=colors.HexColor("#e91e63"); VIOLET=colors.HexColor("#7c3aed")
INK=colors.HexColor("#2b2b33"); MUTED=colors.HexColor("#6f6b88")
LIGHT=colors.HexColor("#f6f2f7"); LIGHTMAG=colors.HexColor("#fdeef4")
LIGHTAMB=colors.HexColor("#fff5e6"); BORDER=colors.HexColor("#e7e2ee")
MONO=colors.HexColor("#0f766e")

styles=getSampleStyleSheet()
def S(n,**k): styles.add(ParagraphStyle(n,**k))
S('H2',fontName='Helvetica-Bold',fontSize=13.5,leading=17,textColor=MAGENTA,spaceBefore=12,spaceAfter=5)
S('H3',fontName='Helvetica-Bold',fontSize=11.5,leading=15,textColor=VIOLET,spaceBefore=8,spaceAfter=2)
S('Body',fontName='Helvetica',fontSize=10,leading=15,textColor=INK,spaceAfter=6)
S('Small',fontName='Helvetica',fontSize=8.5,leading=12,textColor=MUTED)
S('Cell',fontName='Helvetica',fontSize=8.6,leading=12,textColor=INK)
S('CellB',fontName='Helvetica-Bold',fontSize=8.6,leading=12,textColor=INK)
S('CellH',fontName='Helvetica-Bold',fontSize=8.6,leading=12,textColor=colors.white)
S('Mono',fontName='Courier',fontSize=8.4,leading=12,textColor=MONO)
S('Lbl',fontName='Helvetica-Bold',fontSize=8.6,leading=12,textColor=VIOLET)
S('CoverTitle',fontName='Helvetica-Bold',fontSize=28,leading=32,textColor=colors.white,alignment=TA_CENTER)
S('CoverSub',fontName='Helvetica',fontSize=13,leading=18,textColor=colors.white,alignment=TA_CENTER)
S('Callout',fontName='Helvetica',fontSize=9.5,leading=13.5,textColor=INK)

def p(t): return Paragraph(t,styles['Body'])
def h2(t): return Paragraph(t,styles['H2'])
def h3(t): return Paragraph(t,styles['H3'])
def small(t): return Paragraph(t,styles['Small'])

class Band(Flowable):
    def __init__(self,text,w): super().__init__(); self.text=text; self.w=w; self.h=28
    def wrap(self,a,b): return (self.w,self.h)
    def draw(self):
        c=self.canv; steps=60
        for i in range(steps):
            t=i/(steps-1); r=(1-t)*0xe9/255+t*0x7c/255; g=(1-t)*0x1e/255+t*0x3a/255; b=(1-t)*0x63/255+t*0xed/255
            c.setFillColorRGB(r,g,b); c.rect(self.w*i/steps,0,self.w/steps+1,self.h,stroke=0,fill=1)
        c.setFillColor(colors.white); c.setFont('Helvetica-Bold',14); c.drawString(12,9,self.text)

def section(t): return KeepTogether([Spacer(1,8),Band(t,170*mm),Spacer(1,8)])

def table(header,rows,widths):
    data=[[Paragraph(h,styles['CellH']) for h in header]]
    for r in rows: data.append([Paragraph(str(x),styles['Cell']) for x in r])
    t=Table(data,colWidths=widths,repeatRows=1)
    t.setStyle(TableStyle([('BACKGROUND',(0,0),(-1,0),VIOLET),
        ('ROWBACKGROUNDS',(0,1),(-1,-1),[colors.white,LIGHT]),('GRID',(0,0),(-1,-1),0.5,BORDER),
        ('VALIGN',(0,0),(-1,-1),'TOP'),('LEFTPADDING',(0,0),(-1,-1),5),('RIGHTPADDING',(0,0),(-1,-1),5),
        ('TOPPADDING',(0,0),(-1,-1),4),('BOTTOMPADDING',(0,0),(-1,-1),4)]))
    return t

def callout(title,text,kind='tip'):
    bg={'tip':LIGHTMAG,'warn':LIGHTAMB}[kind]; bar={'tip':MAGENTA,'warn':colors.HexColor('#f59e0b')}[kind]
    t=Table([[Paragraph('<b>%s</b> %s'%(title,text),styles['Callout'])]],colWidths=[168*mm])
    t.setStyle(TableStyle([('BACKGROUND',(0,0),(-1,-1),bg),('LEFTPADDING',(0,0),(-1,-1),12),
        ('RIGHTPADDING',(0,0),(-1,-1),12),('TOPPADDING',(0,0),(-1,-1),8),('BOTTOMPADDING',(0,0),(-1,-1),8),
        ('LINEBEFORE',(0,0),(0,-1),3,bar)]))
    return KeepTogether([Spacer(1,3),t,Spacer(1,5)])

def modcard(name, proceso, ctrl, modelos, vistas, tablas, rutas):
    rows=[
        ['Proceso', proceso],
        ['Controladores', '<font face="Courier">%s</font>'%ctrl],
        ['Modelos', '<font face="Courier">%s</font>'%modelos],
        ['Vistas (Blade)', '<font face="Courier">%s</font>'%vistas],
        ['Tablas de datos', '<font face="Courier">%s</font>'%tablas],
        ['Rutas principales', '<font face="Courier">%s</font>'%rutas],
    ]
    data=[[Paragraph(a,styles['Lbl']),Paragraph(b,styles['Cell'])] for a,b in rows]
    t=Table(data,colWidths=[32*mm,136*mm])
    t.setStyle(TableStyle([('GRID',(0,0),(-1,-1),0.5,BORDER),('VALIGN',(0,0),(-1,-1),'TOP'),
        ('BACKGROUND',(0,0),(0,-1),LIGHT),('LEFTPADDING',(0,0),(-1,-1),6),('RIGHTPADDING',(0,0),(-1,-1),6),
        ('TOPPADDING',(0,0),(-1,-1),4),('BOTTOMPADDING',(0,0),(-1,-1),4)]))
    return KeepTogether([h3(name), t, Spacer(1,6)])

def footer(canvas,doc):
    canvas.saveState(); canvas.setStrokeColor(BORDER); canvas.setLineWidth(0.6)
    canvas.line(20*mm,15*mm,190*mm,15*mm); canvas.setFont('Helvetica',8); canvas.setFillColor(MUTED)
    canvas.drawString(20*mm,10*mm,"Suite Integral de Moda SaaS — Manual Técnico del Sistema")
    canvas.drawRightString(190*mm,10*mm,"Página %d"%doc.page); canvas.restoreState()

story=[]
# Portada
story+=[Spacer(1,40)]
band=Table([[Paragraph('Moda<font color="#ffd24a">Suite</font>',ParagraphStyle('b',fontName='Helvetica-Bold',fontSize=22,textColor=colors.white,alignment=TA_CENTER))]],colWidths=[170*mm])
band.setStyle(TableStyle([('BACKGROUND',(0,0),(-1,-1),MAGENTA),('TOPPADDING',(0,0),(-1,-1),18),('BOTTOMPADDING',(0,0),(-1,-1),18)]))
story+=[band,Spacer(1,26),Paragraph('Manual Técnico del Sistema',styles['CoverTitle']),Spacer(1,10),
        Paragraph('Arquitectura, procesos, ficheros y modelo de datos por módulo',styles['CoverSub']),Spacer(1,30)]
box=Table([[Paragraph('Laravel 11 &nbsp;·&nbsp; PHP 8.2 &nbsp;·&nbsp; MySQL 8 &nbsp;·&nbsp; Blade<br/>SaaS multi-negocio (BD única, aislada por empresa)',
    ParagraphStyle('rb',fontName='Helvetica',fontSize=10.5,leading=16,textColor=INK,alignment=TA_CENTER))]],colWidths=[140*mm])
box.setStyle(TableStyle([('BACKGROUND',(0,0),(-1,-1),LIGHTMAG),('BOX',(0,0),(-1,-1),1,MAGENTA),
    ('TOPPADDING',(0,0),(-1,-1),14),('BOTTOMPADDING',(0,0),(-1,-1),14),('ALIGN',(0,0),(-1,-1),'CENTER')]))
story+=[box,Spacer(1,40),Paragraph('Base de datos: suite_saas_modas_integral',styles['CoverSub']),PageBreak()]

# 1. Introducción
story+=[section('1. Introducción'),
 p('Este manual técnico describe la <b>Suite Integral de Moda SaaS</b>: su arquitectura, la organización del código, '
   'el modelo de datos y, para cada módulo, el <b>proceso funcional</b> y los <b>ficheros y tablas</b> que participan. '
   'Está dirigido a desarrolladores y personal técnico que dará mantenimiento o ampliará el sistema.')]

# 2. Arquitectura
story+=[section('2. Arquitectura y stack')]
story+=[p('Aplicación web construida con el patrón <b>MVC</b> sobre Laravel 11. Renderizado del lado del servidor con '
 'plantillas <b>Blade</b>; estilos en un único CSS (<font face="Courier">public/css/app.css</font>, tema oscuro + magenta). '
 'Persistencia en <b>MySQL</b> mediante Eloquent ORM.')]
story+=[table(['Capa','Tecnología / Ubicación'],[
 ['Framework','Laravel 11 (PHP 8.2+)'],
 ['Rutas','routes/web.php (nombradas, agrupadas por middleware de rol)'],
 ['Controladores','app/Http/Controllers/*'],
 ['Modelos (ORM)','app/Models/* (Eloquent)'],
 ['Vistas','resources/views/* (Blade) + layout resources/views/layouts/app.blade.php'],
 ['Lógica de apoyo','app/Support/* (ModuleCatalog, MenuBuilder, motores y servicios)'],
 ['Middleware','app/Http/Middleware/* (EnsureCompanyContext, EnsureRole)'],
 ['Migraciones/semillas','database/migrations/*, database/seeders/*'],
 ['Estilos','public/css/app.css'],
],[38*mm,130*mm])]
story+=[p('<b>Modelo multi-negocio (multi-tenant lógico):</b> una sola base de datos. Cada registro de negocio lleva '
 '<font face="Courier">company_id</font> y las consultas se filtran por empresa mediante el <i>trait</i> '
 '<font face="Courier">HasCompanyScope</font>. El <b>tipo de negocio</b> (business_types) y el <b>plan</b> definen qué '
 'módulos ve cada empresa.')]

# 3. Seguridad
story+=[section('3. Seguridad y control de acceso')]
story+=[table(['Elemento','Función'],[
 ['auth (middleware)','Exige sesión iniciada.'],
 ['company (EnsureCompanyContext)','Verifica que el usuario de empresa tenga una empresa activa; superadmin exento.'],
 ['role:.. (EnsureRole)','Restringe rutas por rol (superadmin, admin, manager, cashier, seller).'],
 ['HasCompanyScope (trait)','scopeCompany(), requireCompanyOrRedirect(), assertSameCompany() para aislar datos por empresa.'],
 ['Contraseñas','Hash bcrypt (cast hashed en el modelo User).'],
 ['Límites de plan','Company::canAddUsers()/canAddProducts() aplican los máximos del plan.'],
],[52*mm,116*mm])]

# 4. Multi-negocio
story+=[section('4. Motor multi-negocio')]
story+=[table(['Fichero','Rol'],[
 ['app/Support/ModuleCatalog.php','Catálogo central de módulos (slug, etiqueta, ícono, grupo) y mapa de rutas implementadas.'],
 ['app/Support/MenuBuilder.php','Arma el menú lateral según rol y módulos efectivos de la empresa.'],
 ['Company::effectiveModules()','Módulos visibles = override de la empresa ∩ módulos del plan (siempre incluye dashboard).'],
 ['business_types.modules (JSON)','Módulos típicos por rubro (definidos en el seeder).'],
 ['plans.modules (JSON)','Módulos que habilita cada plan.'],
 ['companies.modules (JSON)','Override por empresa (marcado desde “Editar empresa”).'],
],[62*mm,106*mm])]

# 5. Modelo de datos
story+=[PageBreak(),section('5. Modelo de datos (tablas)')]
story+=[p('La base <font face="Courier">suite_saas_modas_integral</font> contiene las siguientes tablas principales:')]
story+=[table(['Tabla','Propósito'],[
 ['business_types','Rubros (7): módulos, color e ícono por tipo de negocio.'],
 ['plans','Planes de suscripción: precio, límites y módulos incluidos.'],
 ['companies','Empresas-cliente: rubro, plan, branding, preferencias, vencimiento.'],
 ['users','Usuarios: pertenecen a una empresa (o superadmin), rol y estado.'],
 ['subscriptions','Suscripciones/pagos por empresa (ticket, vigencia, total).'],
 ['categories / brands','Categorías y marcas del catálogo (por empresa).'],
 ['collections','Colecciones/temporadas (por empresa).'],
 ['attributes','Opciones reutilizables: tallas, colores, numeración, materiales.'],
 ['products','Productos: categoría, marca, colección, costo y precio.'],
 ['product_variants','Variantes (talla/color/numeración): SKU, stock, mín., precio.'],
 ['stock_movements','Kardex: entradas/salidas/ajustes con saldo resultante.'],
 ['suppliers / purchases / purchase_items','Proveedores y compras (suman stock).'],
 ['customers','Clientes (con saldo de puntos de fidelización).'],
 ['sales / sale_items','Ventas y sus líneas (POS).'],
 ['sale_returns / sale_return_items','Devoluciones contra ventas.'],
 ['cash_sessions / cash_movements','Aperturas de caja y movimientos; arqueo.'],
 ['promotions','Promociones (%, monto) por producto/categoría/todo, con vigencia.'],
 ['loyalty_transactions','Historial de puntos (acumulación, canje, ajuste).'],
 ['expenses','Gastos/egresos por empresa.'],
 ['stores / store_stocks','Tiendas/locales y stock por ubicación.'],
 ['transfers / transfer_items','Traslados de mercadería entre tiendas.'],
 ['appointments','Citas y probadores (boutique).'],
 ['repair_orders','Órdenes de reparación (joyería).'],
 ['certificates','Certificados de autenticidad y garantías (joyería).'],
 ['insurance_policies','Pólizas de seguro / alto valor (joyería).'],
],[58*mm,110*mm])]

# 6. Módulos
story+=[PageBreak(),section('6. Módulos: proceso, ficheros y tablas')]

M=[
 ('6.1 Autenticación y Registro',
  'Login/logout con sesión; registro público self-service que crea empresa + usuario administrador e inicia sesión.',
  'Auth\\LoginController, Auth\\RegisterController','User, Company, Plan, BusinessType',
  'auth/login, auth/register','users, companies, plans, business_types','login, logout, register'),
 ('6.2 Dashboard / Panel global',
  'Empresa: KPIs (ventas hoy/mes, ticket, stock bajo), gráficos y top productos. Superadmin: panel global agregando todas las empresas.',
  'DashboardController','Sale, SaleItem, Product, ProductVariant, Company, Plan, BusinessType, User',
  'dashboard, dashboard_admin','sales, sale_items, products, product_variants, companies','dashboard'),
 ('6.3 Empresas Cliente (superadmin)',
  'Alta/edición de empresas, override de módulos visibles, branding, gestión de suscripción con ticket y activación.',
  'CompanyController','Company, BusinessType, Plan, Subscription',
  'companies/index, companies/edit, companies/form, companies/subscription','companies, subscriptions, plans, business_types',
  'companies.*, companies.subscription(.store), companies.toggle'),
 ('6.4 Planes',
  'CRUD de planes: precio, ciclo, límites (usuarios/productos) y módulos incluidos.',
  'PlanController','Plan, Company','plans/index, plans/form','plans (companies.plan_id)','plans.*'),
 ('6.5 Pagos / Suscripción',
  'Historial global de suscripciones/pagos de todas las empresas, con ticket imprimible.',
  'PaymentController','Subscription, Company, Plan','payments/index, payments/ticket','subscriptions','payments.index, payments.ticket'),
 ('6.6 Usuarios y Roles',
  'CRUD de usuarios; superadmin gestiona todos, admin solo los de su empresa; asignación de rol y contraseña.',
  'UserController','User, Company','users/index, users/form','users','users.*, users.toggle'),
 ('6.7 Configuración de empresa',
  'Perfil de la empresa, logo (storage público), pie de comprobante, tasa de puntos, umbral de stock y color de marca.',
  'SettingsController','Company','settings/index','companies','settings.index/update, settings.logo.remove'),
 ('6.8 Mantenimiento (por empresa)',
  'Copia de seguridad (export JSON), restauración (import con remapeo de IDs) y reinicio de datos de la empresa; copia previa automática.',
  'BackupController + Support\\CompanyBackupService','(todas las de negocio de la empresa)',
  'maintenance/index','todas las tablas de negocio (filtradas por company_id)','backup.index/create/download/destroy/restore/reset'),
 ('6.9 Categorías y Marcas',
  'Catálogos base para clasificar productos; alta/edición en modal.',
  'CategoryController, BrandController','Category, Brand','categories/index, brands/index','categories, brands','categories.*, brands.*'),
 ('6.10 Colecciones y Temporadas',
  'Agrupa productos por temporada/colección; se enlaza en el formulario de producto.',
  'CollectionController','Collection, Product','collections/index, collections/form','collections','collections.*'),
 ('6.11 Productos y Variantes',
  'Crea producto y sus variantes (talla/color/numeración) con precio y stock; el stock inicial genera movimiento de entrada. Respeta límites del plan.',
  'ProductController','Product, ProductVariant, Category, Brand, Collection, StockMovement',
  'products/index, products/form','products, product_variants, stock_movements','products.*, products.toggle'),
 ('6.12 Inventario / Stock',
  'Lista stock por variante con semáforo; ajustes (entrada/salida/ajuste) que actualizan stock y registran el movimiento con saldo.',
  'InventoryController','ProductVariant, StockMovement, Product','inventory/index, inventory/movements',
  'product_variants, stock_movements','inventory.index/adjust/movements'),
 ('6.13 Atributos (Tallas/Colores/Numeración/Materiales)',
  'Catálogos reutilizables de opciones para agilizar la carga de variantes.',
  'AttributeController','Attribute','attributes/index','attributes','variants.index, shoe_sizes.index, materials.index, attributes.store/destroy'),
 ('6.14 Compras y Proveedores',
  'Registro de mercadería: suma stock, actualiza el último costo y crea movimiento de entrada; anulación revierte. Directorio de proveedores.',
  'PurchaseController, SupplierController','Purchase, PurchaseItem, Supplier, ProductVariant, StockMovement',
  'purchases/index,create,show; suppliers/index','purchases, purchase_items, suppliers, product_variants, stock_movements',
  'purchases.*, purchases.search/void, suppliers.*'),
 ('6.15 Punto de Venta (POS)',
  'Búsqueda AJAX con precio y promoción aplicada; carrito; al cobrar crea la venta, descuenta stock (movimiento de salida) y otorga puntos si hay cliente.',
  'PosController + Support\\PromotionEngine + Support\\LoyaltyService','Sale, SaleItem, ProductVariant, Customer, StockMovement',
  'pos/index','sales, sale_items, product_variants, stock_movements, promotions, loyalty_transactions','pos.index/search/store'),
 ('6.16 Ventas / Facturación',
  'Listado y comprobante imprimible; anulación que repone stock y revierte los puntos otorgados.',
  'SalesController + Support\\LoyaltyService','Sale, SaleItem, StockMovement','sales/index, sales/show',
  'sales, sale_items, stock_movements, loyalty_transactions','sales.index/show/void'),
 ('6.17 Devoluciones',
  'Devolución contra una venta con control de cantidad devolvible; repone stock (movimiento) y calcula el reembolso.',
  'ReturnController','SaleReturn, SaleReturnItem, Sale, ProductVariant, StockMovement',
  'returns/index,create,show','sale_returns, sale_return_items, stock_movements','returns.index/create/store/show'),
 ('6.18 Caja y Arqueo',
  'Apertura con monto inicial; ingresos/egresos; al cerrar calcula esperado (apertura + ventas efectivo + ingresos − egresos) vs contado y la diferencia.',
  'CashController','CashSession, CashMovement, Sale','cash/index, cash/show','cash_sessions, cash_movements, sales',
  'cash.index/open/movement/close/show'),
 ('6.19 Promociones',
  'Descuentos por producto/categoría/todo (%, monto) con vigencia; el motor elige el mejor descuento vigente en el POS.',
  'PromotionController + Support\\PromotionEngine','Promotion, Category, Product','promotions/index, promotions/form',
  'promotions','promotions.*, promotions.toggle'),
 ('6.20 Fidelización / Puntos',
  'Acumulación automática de puntos por venta con cliente; historial; ajuste y canje manual.',
  'LoyaltyController + Support\\LoyaltyService','Customer, LoyaltyTransaction','loyalty/index, loyalty/show',
  'customers (points_balance), loyalty_transactions','loyalty.index/show/adjust'),
 ('6.21 Clientes',
  'Base de clientes (documento, contacto); usada por POS, fidelización, citas, reparaciones y seguros.',
  'CustomerController','Customer','customers/index','customers','customers.*'),
 ('6.22 Reportes',
  'Ventas por período (por día, medio de pago, vendedor, top productos) y stock valorizado; exportación a CSV (streaming).',
  'ReportController','Sale, SaleItem, Purchase, ProductVariant','reports/sales, reports/inventory',
  'sales, sale_items, purchases, product_variants','reports.sales(.export), reports.inventory(.export)'),
 ('6.23 Tiendas y Traslados (multi-local)',
  'Locales con stock por ubicación; “Cargar stock” inicializa la tienda principal; traslados mueven stock entre tiendas validando disponibilidad.',
  'StoreController, TransferController','Store, StoreStock, Transfer, TransferItem, ProductVariant',
  'stores/index,show; transfers/index,create,show','stores, store_stocks, transfers, transfer_items',
  'stores.*, transfers.*'),
 ('6.24 Citas y Probadores (boutique)',
  'Agenda de citas de clientes con asesor, duración y estados (programada/atendida/cancelada/no asistió).',
  'AppointmentController','Appointment, Customer, User','appointments/index, appointments/form','appointments','appointments.*, appointments.status'),
 ('6.25 Órdenes de Reparación (joyería)',
  'Recepción de piezas (material, costo, adelanto, saldo) y seguimiento de estado con comprobante.',
  'RepairOrderController','RepairOrder, Customer','repairs/index,form,show','repair_orders','repairs.* (param repair), repairs.status'),
 ('6.26 Certificados y Garantías (joyería)',
  'Emisión de certificados de autenticidad y garantías por pieza; documento imprimible; anulación.',
  'CertificateController','Certificate, Customer, Product','certificates/index,form,show','certificates','certificates.*, certificates.void'),
 ('6.27 Seguros / Alto Valor (joyería)',
  'Pólizas de piezas aseguradas (aseguradora, valor, prima, vigencia) con estado vigente/vencida/cancelada.',
  'InsuranceController','InsurancePolicy, Customer, Product','insurance/index,form,show','insurance_policies','insurance.* (param insurance), insurance.cancel'),
 ('6.28 Gastos (finanzas)',
  'Registro de egresos por categoría con total del mes.',
  'ExpenseController','Expense','expenses/index','expenses','expenses.index/store/destroy'),
]
for m in M: story+=[modcard(*m)]

# 7. Procesos clave
story+=[PageBreak(),section('7. Procesos clave (flujos)')]
story+=[h3('7.1 Venta en el POS')]
story+=[p('POS busca variantes (con precio y promoción) → arma carrito → <b>Cobrar</b>: dentro de una transacción se crea '
 '<font face="Courier">sales</font> + <font face="Courier">sale_items</font>, se descuenta el stock de cada '
 '<font face="Courier">product_variants</font>, se registra <font face="Courier">stock_movements</font> (salida) y, si hay '
 'cliente, <font face="Courier">LoyaltyService</font> suma puntos en <font face="Courier">loyalty_transactions</font>.')]
story+=[h3('7.2 Compra')]
story+=[p('Compra recibida → suma stock a la variante, actualiza su último costo y crea movimiento de entrada; la anulación revierte todo.')]
story+=[h3('7.3 Devolución')]
story+=[p('Se elige la venta y las cantidades devolvibles (vendido − ya devuelto) → repone stock con movimiento de entrada y calcula el reembolso.')]
story+=[h3('7.4 Caja / Arqueo')]
story+=[p('Apertura → movimientos → cierre: esperado = apertura + ventas en efectivo del período + ingresos − egresos; se compara con lo contado y se guarda la diferencia.')]
story+=[h3('7.5 Suscripción (superadmin)')]
story+=[p('Se elige plan y duración → calcula subtotal/descuento/total y nuevo vencimiento → crea <font face="Courier">subscriptions</font>, '
 'actualiza <font face="Courier">companies.plan_id</font> y <font face="Courier">subscription_ends_at</font>, y genera el ticket.')]
story+=[h3('7.6 Respaldo / Reinicio por empresa')]
story+=[p('<font face="Courier">CompanyBackupService</font> exporta a JSON los datos de la empresa; el reinicio los borra (con copia previa); '
 'la restauración limpia y reinserta remapeando los IDs y sus relaciones dentro de una transacción.')]

# 8. Instalación
story+=[section('8. Instalación y despliegue')]
story+=[table(['Paso','Comando'],[
 ['1. Dependencias','composer install'],
 ['2. Entorno','copiar .env.example a .env y configurar MySQL'],
 ['3. Clave','php artisan key:generate'],
 ['4. Almacenamiento','php artisan storage:link'],
 ['5. Base de datos','crear BD suite_saas_modas_integral'],
 ['6. Migrar + semillas','php artisan migrate --seed'],
 ['7. Datos demo (opcional)','php artisan db:seed --class=DemoDataSeeder'],
 ['8. Servidor','php artisan serve'],
],[46*mm,122*mm])]
story+=[callout('Configuración MySQL (.env):','DB_DATABASE=suite_saas_modas_integral · DB_USERNAME=root · DB_PASSWORD=(vacío por defecto) · DB_HOST=127.0.0.1 · DB_PORT=3306','tip')]

# 9. Convenciones
story+=[section('9. Convenciones y notas técnicas')]
story+=[p('• Todo dato de negocio se filtra por <font face="Courier">company_id</font> (trait HasCompanyScope).<br/>'
 '• Los <i>slugs</i> de módulos y su enrutado viven en <font face="Courier">ModuleCatalog</font>; el menú se arma en <font face="Courier">MenuBuilder</font>.<br/>'
 '• En Blade, para pasar arreglos a JavaScript se usa <font face="Courier">@js(...)</font> (no @json con arreglos en línea).<br/>'
 '• Precios/decimales con casts <font face="Courier">decimal:2</font>; stock como enteros; el kardex es la fuente de auditoría.<br/>'
 '• El superadmin no tiene empresa: solo ve módulos de administración de la plataforma.')]

doc=SimpleDocTemplate(os.path.join(OUT,'Manual_Tecnico_ModaSuite.pdf'),pagesize=A4,
    leftMargin=20*mm,rightMargin=20*mm,topMargin=18*mm,bottomMargin=20*mm,
    title='Manual Técnico del Sistema — ModaSuite', author='ModaSuite')
doc.build(story, onLaterPages=footer, onFirstPage=lambda c,d:None)
print('OK ->','Manual_Tecnico_ModaSuite.pdf')
