# SPEC-LOCALIZACION-PAISES

## Metadatos
| Campo | Valor |
|-------|-------|
| **Código** | SPEC-TRANS-012 |
| **Versión** | 1.0.0 |
| **Fecha** | 2025-01-15 |
| **Autor** | Requirements-Analyst Agent |
| **Estado** | DRAFT |
| **Prioridad** | P1 |
| **Módulos Afectados** | MGN-002 (Empresas), MGN-004 (Financiero) |
| **Gaps Cubiertos** | GAP-MGN-002-002 |

## Resumen Ejecutivo

Esta especificación define el sistema completo de localización por país, permitiendo configurar automáticamente:

1. **Plantillas contables (CoA)**: Plan de cuentas específico por país
2. **Impuestos por defecto**: IVA, retenciones, impuestos especiales
3. **Posiciones fiscales**: Mapeo de impuestos según tipo de cliente
4. **Tipos de documentos**: Facturas, notas de crédito según normativa local
5. **Tipos de identificación**: RFC, CUIT, NIF según país
6. **Datos maestros**: Bancos, estados/provincias, formatos de dirección
7. **Configuración de empresa**: Campos específicos por legislación

### Referencia Odoo 18

Basado en análisis exhaustivo de módulos `l10n_*` de Odoo 18:
- **221 módulos** de localización disponibles
- **Patrón `@template`** para registro de datos
- **Herencia de plantillas** para variantes regionales
- **Validación externa** con `stdnum` para identificaciones fiscales

---

## Parte 1: Arquitectura del Sistema de Localización

### 1.1 Visión General

```
┌─────────────────────────────────────────────────────────────────┐
│                    SISTEMA DE LOCALIZACIÓN                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐       │
│  │  Plantilla   │    │  Plantilla   │    │  Plantilla   │       │
│  │    l10n_mx   │    │   l10n_ar    │    │   l10n_es    │       │
│  └──────┬───────┘    └──────┬───────┘    └──────┬───────┘       │
│         │                   │                   │                │
│         └───────────────────┼───────────────────┘                │
│                             │                                    │
│                             ▼                                    │
│              ┌──────────────────────────┐                        │
│              │   LocalizationService    │                        │
│              │   ─────────────────────  │                        │
│              │   • loadTemplate()       │                        │
│              │   • getCountryConfig()   │                        │
│              │   • applyToCompany()     │                        │
│              └──────────────┬───────────┘                        │
│                             │                                    │
│         ┌───────────────────┼───────────────────┐                │
│         │                   │                   │                │
│         ▼                   ▼                   ▼                │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐          │
│  │   Empresa   │    │  Impuestos  │    │   Cuentas   │          │
│  │   Config    │    │   Config    │    │   Config    │          │
│  └─────────────┘    └─────────────┘    └─────────────┘          │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
```

### 1.2 Componentes del Sistema

| Componente | Descripción | Datos Incluidos |
|------------|-------------|-----------------|
| **Plantilla Base** | Configuración raíz del país | CoA, impuestos, posiciones fiscales |
| **Datos Maestros** | Catálogos específicos | Bancos, provincias, tipos de ID |
| **Extensiones de Modelo** | Campos específicos país | RFC, CUIT, tipo responsabilidad |
| **Validadores** | Reglas de validación | Formato RFC, checksum CUIT |
| **Formateadores** | Presentación de datos | Formato dirección, moneda |

---

## Parte 2: Modelo de Datos

### 2.1 Tipos Enumerados

```sql
-- Estado de plantilla de localización
CREATE TYPE localization_template_status AS ENUM (
    'draft',       -- En desarrollo
    'active',      -- Disponible para uso
    'deprecated',  -- Obsoleto pero funcional
    'archived'     -- No disponible
);

-- Tipo de dato de localización
CREATE TYPE localization_data_type AS ENUM (
    'chart_of_accounts',     -- Plan de cuentas
    'taxes',                 -- Impuestos
    'fiscal_positions',      -- Posiciones fiscales
    'document_types',        -- Tipos de documento
    'identification_types',  -- Tipos de identificación
    'banks',                 -- Bancos
    'address_format',        -- Formato de dirección
    'company_fields'         -- Campos de empresa
);

-- Tipo de impuesto por uso
CREATE TYPE tax_use_type AS ENUM (
    'sale',       -- Ventas
    'purchase',   -- Compras
    'none'        -- Ninguno (ajustes)
);

-- Tipo de cálculo de impuesto
CREATE TYPE tax_amount_type AS ENUM (
    'percent',    -- Porcentaje
    'fixed',      -- Monto fijo
    'division',   -- División
    'group'       -- Grupo de impuestos
);
```

### 2.2 Tabla de Plantillas de Localización

```sql
-- Plantillas de localización por país
CREATE TABLE localization_templates (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Identificación
    code VARCHAR(20) NOT NULL UNIQUE,           -- Ej: 'mx', 'ar_ri', 'es_pymes'
    name VARCHAR(200) NOT NULL,                 -- Nombre descriptivo
    country_id UUID NOT NULL REFERENCES countries(id),

    -- Jerarquía (para variantes)
    parent_template_id UUID REFERENCES localization_templates(id),
    sequence INTEGER DEFAULT 10,                -- Orden de prioridad

    -- Configuración
    code_digits INTEGER DEFAULT 6,              -- Longitud de códigos de cuenta
    currency_id UUID REFERENCES currencies(id),

    -- Estado
    status localization_template_status DEFAULT 'active',

    -- Metadatos
    version VARCHAR(20),
    description TEXT,

    -- Auditoría
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by UUID NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_by UUID NOT NULL,

    -- Constraints
    CONSTRAINT uq_template_country_code UNIQUE (country_id, code)
);

-- Índices
CREATE INDEX idx_loc_templates_country ON localization_templates(country_id);
CREATE INDEX idx_loc_templates_parent ON localization_templates(parent_template_id);
CREATE INDEX idx_loc_templates_status ON localization_templates(status);

COMMENT ON TABLE localization_templates IS
'Plantillas de configuración por país. Soporta herencia para variantes regionales.';
```

### 2.3 Datos de Plantilla (Configuración)

```sql
-- Configuración específica de cada plantilla
CREATE TABLE localization_template_data (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    template_id UUID NOT NULL REFERENCES localization_templates(id) ON DELETE CASCADE,

    -- Tipo de configuración
    data_type localization_data_type NOT NULL,

    -- Datos en formato JSONB para flexibilidad
    config_data JSONB NOT NULL,

    -- Para ordenamiento y prioridad
    sequence INTEGER DEFAULT 10,

    -- Auditoría
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    -- Constraint único por plantilla y tipo
    CONSTRAINT uq_template_data_type UNIQUE (template_id, data_type)
);

-- Índice para búsqueda por tipo
CREATE INDEX idx_loc_template_data_type ON localization_template_data(data_type);
CREATE INDEX idx_loc_template_data_gin ON localization_template_data USING GIN (config_data);

COMMENT ON TABLE localization_template_data IS
'Datos de configuración por tipo para cada plantilla de localización.';
```

### 2.4 Tipos de Identificación por País

```sql
-- Tipos de identificación fiscal/legal por país
CREATE TABLE identification_types (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Identificación
    code VARCHAR(20) NOT NULL,                  -- Ej: 'rfc', 'cuit', 'nif'
    name VARCHAR(100) NOT NULL,
    description TEXT,

    -- País
    country_id UUID REFERENCES countries(id),   -- NULL = global

    -- Configuración
    is_vat BOOLEAN DEFAULT false,              -- ¿Es identificación fiscal?
    validation_regex VARCHAR(200),              -- Patrón de validación
    validation_module VARCHAR(100),             -- Módulo de validación (ej: 'mx.rfc')
    format_mask VARCHAR(100),                   -- Máscara de formato

    -- Ordenamiento
    sequence INTEGER DEFAULT 10,

    -- Estado
    is_active BOOLEAN DEFAULT true,

    -- Auditoría
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uq_identification_type_code UNIQUE (country_id, code)
);

-- Índices
CREATE INDEX idx_id_types_country ON identification_types(country_id);
CREATE INDEX idx_id_types_vat ON identification_types(is_vat) WHERE is_vat = true;

-- Datos iniciales globales
INSERT INTO identification_types (code, name, is_vat, sequence) VALUES
    ('vat', 'Tax ID / VAT', true, 1),
    ('passport', 'Passport', false, 100),
    ('foreign_id', 'Foreign ID', false, 110);

COMMENT ON TABLE identification_types IS
'Catálogo de tipos de identificación por país (RFC, CUIT, NIF, etc.)';
```

### 2.5 Configuración de Empresa por País

```sql
-- Campos adicionales de empresa por localización
CREATE TABLE company_localization_config (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE,

    -- Localización aplicada
    template_id UUID REFERENCES localization_templates(id),
    country_id UUID NOT NULL REFERENCES countries(id),

    -- Identificación fiscal
    identification_type_id UUID REFERENCES identification_types(id),
    tax_id VARCHAR(50),                         -- RFC, CUIT, NIF
    tax_id_formatted VARCHAR(60),               -- Versión formateada

    -- Configuración contable
    chart_template_loaded BOOLEAN DEFAULT false,
    fiscal_country_id UUID REFERENCES countries(id),

    -- Prefijos de cuentas
    bank_account_code_prefix VARCHAR(20),
    cash_account_code_prefix VARCHAR(20),

    -- Impuestos por defecto
    default_sale_tax_id UUID,                   -- REFERENCES taxes(id)
    default_purchase_tax_id UUID,               -- REFERENCES taxes(id)

    -- Configuración de redondeo
    tax_calculation_rounding VARCHAR(20) DEFAULT 'round_per_line',

    -- Campos específicos por país (JSONB para flexibilidad)
    country_specific_data JSONB DEFAULT '{}',

    -- Auditoría
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by UUID NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_by UUID NOT NULL,

    CONSTRAINT uq_company_localization UNIQUE (company_id)
);

-- Índices
CREATE INDEX idx_company_loc_template ON company_localization_config(template_id);
CREATE INDEX idx_company_loc_country ON company_localization_config(country_id);

COMMENT ON TABLE company_localization_config IS
'Configuración de localización aplicada a cada empresa.';
```

### 2.6 Posiciones Fiscales

```sql
-- Posiciones fiscales (mapeo de impuestos)
CREATE TABLE fiscal_positions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Identificación
    name VARCHAR(200) NOT NULL,
    code VARCHAR(50),

    -- Ámbito
    company_id UUID NOT NULL REFERENCES companies(id),
    country_id UUID REFERENCES countries(id),
    country_group_id UUID,                      -- Para grupos como "EU"

    -- Configuración
    auto_apply BOOLEAN DEFAULT false,           -- Aplicar automáticamente
    vat_required BOOLEAN DEFAULT false,         -- Requiere VAT

    -- Para matching automático
    zip_from VARCHAR(20),
    zip_to VARCHAR(20),

    -- Ordenamiento
    sequence INTEGER DEFAULT 10,

    -- Estado
    is_active BOOLEAN DEFAULT true,

    -- Auditoría
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by UUID NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_by UUID NOT NULL
);

-- Mapeo de impuestos en posición fiscal
CREATE TABLE fiscal_position_tax_mappings (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    fiscal_position_id UUID NOT NULL REFERENCES fiscal_positions(id) ON DELETE CASCADE,

    -- Mapeo
    tax_src_id UUID NOT NULL,                   -- Impuesto origen
    tax_dest_id UUID,                           -- Impuesto destino (NULL = no aplicar)

    CONSTRAINT uq_fp_tax_mapping UNIQUE (fiscal_position_id, tax_src_id)
);

-- Mapeo de cuentas en posición fiscal
CREATE TABLE fiscal_position_account_mappings (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    fiscal_position_id UUID NOT NULL REFERENCES fiscal_positions(id) ON DELETE CASCADE,

    -- Mapeo
    account_src_id UUID NOT NULL,               -- Cuenta origen
    account_dest_id UUID NOT NULL,              -- Cuenta destino

    CONSTRAINT uq_fp_account_mapping UNIQUE (fiscal_position_id, account_src_id)
);

-- Índices
CREATE INDEX idx_fiscal_pos_company ON fiscal_positions(company_id);
CREATE INDEX idx_fiscal_pos_country ON fiscal_positions(country_id);
CREATE INDEX idx_fiscal_pos_auto ON fiscal_positions(auto_apply) WHERE auto_apply = true;

COMMENT ON TABLE fiscal_positions IS
'Posiciones fiscales para mapeo automático de impuestos y cuentas según cliente.';
```

### 2.7 Tipos de Documento (LATAM)

```sql
-- Tipos de documento fiscal (facturas, NC, ND, etc.)
CREATE TABLE document_types (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Identificación
    code VARCHAR(20) NOT NULL,
    name VARCHAR(200) NOT NULL,

    -- País
    country_id UUID REFERENCES countries(id),

    -- Clasificación
    internal_type VARCHAR(50) NOT NULL,         -- invoice, credit_note, debit_note
    doc_code_prefix VARCHAR(10),                -- Prefijo (FA, NC, ND)

    -- Configuración
    active BOOLEAN DEFAULT true,

    -- Ordenamiento
    sequence INTEGER DEFAULT 10,

    -- Campos específicos (JSONB)
    country_specific_data JSONB DEFAULT '{}',

    -- Auditoría
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uq_document_type_code UNIQUE (country_id, code)
);

-- Índices
CREATE INDEX idx_doc_types_country ON document_types(country_id);
CREATE INDEX idx_doc_types_internal ON document_types(internal_type);

COMMENT ON TABLE document_types IS
'Tipos de documento fiscal por país (facturas A/B/C en Argentina, etc.)';
```

### 2.8 Bancos por País

```sql
-- Extensión de tabla de bancos para datos de localización
ALTER TABLE banks ADD COLUMN IF NOT EXISTS
    country_id UUID REFERENCES countries(id);

ALTER TABLE banks ADD COLUMN IF NOT EXISTS
    country_specific_code VARCHAR(50);          -- Código local (ej: código CLABE en México)

ALTER TABLE banks ADD COLUMN IF NOT EXISTS
    country_specific_data JSONB DEFAULT '{}';

-- Índice por país
CREATE INDEX IF NOT EXISTS idx_banks_country ON banks(country_id);

COMMENT ON COLUMN banks.country_specific_code IS
'Código específico del país (ej: código interbancario en México)';
```

### 2.9 Formato de Dirección por País

```sql
-- Configuración de formato de dirección por país
CREATE TABLE address_formats (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    country_id UUID NOT NULL REFERENCES countries(id) UNIQUE,

    -- Formato de dirección
    address_format TEXT NOT NULL,               -- Template con placeholders

    -- Campos requeridos
    street_required BOOLEAN DEFAULT true,
    city_required BOOLEAN DEFAULT true,
    state_required BOOLEAN DEFAULT false,
    zip_required BOOLEAN DEFAULT false,

    -- Configuración adicional
    zip_regex VARCHAR(100),                     -- Validación de código postal
    phone_format VARCHAR(100),                  -- Formato de teléfono

    -- Auditoría
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Datos iniciales
INSERT INTO address_formats (country_id, address_format, state_required, zip_required) VALUES
    ((SELECT id FROM countries WHERE code = 'MX'),
     '%(street)s\n%(street2)s\n%(zip)s %(city)s, %(state_name)s\n%(country_name)s',
     true, true),
    ((SELECT id FROM countries WHERE code = 'US'),
     '%(street)s\n%(street2)s\n%(city)s, %(state_code)s %(zip)s\n%(country_name)s',
     true, true),
    ((SELECT id FROM countries WHERE code = 'ES'),
     '%(street)s\n%(street2)s\n%(zip)s %(city)s\n%(state_name)s\n%(country_name)s',
     false, true);

COMMENT ON TABLE address_formats IS
'Formato de presentación de direcciones por país.';
```

---

## Parte 3: Interfaces de Dominio

### 3.1 Entidades de Localización

```typescript
// src/modules/localization/domain/entities/localization-template.entity.ts

export interface LocalizationTemplateProps {
  id: string;
  code: string;
  name: string;
  countryId: string;
  parentTemplateId?: string;
  sequence: number;
  codeDigits: number;
  currencyId?: string;
  status: LocalizationTemplateStatus;
  version?: string;
  description?: string;
  isActive: boolean;
  createdAt: Date;
  createdBy: string;
  updatedAt: Date;
  updatedBy: string;
}

export enum LocalizationTemplateStatus {
  DRAFT = 'draft',
  ACTIVE = 'active',
  DEPRECATED = 'deprecated',
  ARCHIVED = 'archived'
}

export class LocalizationTemplate extends Entity<LocalizationTemplateProps> {

  get code(): string {
    return this.props.code;
  }

  get countryId(): string {
    return this.props.countryId;
  }

  get hasParent(): boolean {
    return !!this.props.parentTemplateId;
  }

  get isActive(): boolean {
    return this.props.isActive && this.props.status === LocalizationTemplateStatus.ACTIVE;
  }

  public static create(props: Omit<LocalizationTemplateProps, 'id' | 'createdAt' | 'updatedAt'>): LocalizationTemplate {
    return new LocalizationTemplate({
      ...props,
      id: crypto.randomUUID(),
      createdAt: new Date(),
      updatedAt: new Date()
    });
  }
}
```

### 3.2 Value Objects

```typescript
// src/modules/localization/domain/value-objects/identification-number.vo.ts

export class IdentificationNumber {
  private readonly value: string;
  private readonly type: IdentificationType;
  private readonly countryCode: string;

  private constructor(value: string, type: IdentificationType, countryCode: string) {
    this.value = value;
    this.type = type;
    this.countryCode = countryCode;
  }

  get raw(): string {
    return this.value;
  }

  get formatted(): string {
    return this.format();
  }

  get isVat(): boolean {
    return this.type.isVat;
  }

  private format(): string {
    // Formateo según país
    switch (this.countryCode) {
      case 'MX':
        return this.formatMexicanRFC();
      case 'AR':
        return this.formatArgentineCUIT();
      case 'ES':
        return this.formatSpanishNIF();
      default:
        return this.value;
    }
  }

  private formatMexicanRFC(): string {
    // RFC: XXXX-XXXXXX-XXX
    if (this.value.length === 13) {
      return `${this.value.slice(0, 4)}-${this.value.slice(4, 10)}-${this.value.slice(10)}`;
    }
    if (this.value.length === 12) {
      return `${this.value.slice(0, 3)}-${this.value.slice(3, 9)}-${this.value.slice(9)}`;
    }
    return this.value;
  }

  private formatArgentineCUIT(): string {
    // CUIT: XX-XXXXXXXX-X
    if (this.value.length === 11) {
      return `${this.value.slice(0, 2)}-${this.value.slice(2, 10)}-${this.value.slice(10)}`;
    }
    return this.value;
  }

  private formatSpanishNIF(): string {
    // NIF ya viene formateado típicamente
    return this.value;
  }

  public static create(
    value: string,
    type: IdentificationType,
    countryCode: string
  ): IdentificationNumber {
    const cleanValue = value.replace(/[-\s]/g, '').toUpperCase();

    // Validar según tipo
    if (type.validationRegex) {
      const regex = new RegExp(type.validationRegex);
      if (!regex.test(cleanValue)) {
        throw new DomainError(`Invalid ${type.name} format`);
      }
    }

    return new IdentificationNumber(cleanValue, type, countryCode);
  }
}

interface IdentificationType {
  code: string;
  name: string;
  isVat: boolean;
  validationRegex?: string;
}
```

### 3.3 Configuración de Plantilla

```typescript
// src/modules/localization/domain/value-objects/template-config.vo.ts

export interface ChartOfAccountsConfig {
  codeDigits: number;
  accounts: AccountTemplateData[];
  defaultReceivableAccountCode: string;
  defaultPayableAccountCode: string;
  defaultIncomeAccountCode: string;
  defaultExpenseAccountCode: string;
  bankAccountPrefix: string;
  cashAccountPrefix: string;
}

export interface TaxConfig {
  taxes: TaxTemplateData[];
  defaultSaleTaxCode: string;
  defaultPurchaseTaxCode: string;
  calculationRounding: 'round_per_line' | 'round_globally';
}

export interface FiscalPositionConfig {
  positions: FiscalPositionData[];
}

export interface CompanyConfig {
  angloSaxonAccounting: boolean;
  fiscalCountryId: string;
  additionalFields: Record<string, any>;
}

export interface TemplateConfig {
  chartOfAccounts: ChartOfAccountsConfig;
  taxes: TaxConfig;
  fiscalPositions: FiscalPositionConfig;
  company: CompanyConfig;
  documentTypes?: DocumentTypeData[];
  identificationTypes?: IdentificationTypeData[];
  banks?: BankData[];
}

// Datos de plantilla de cuenta
interface AccountTemplateData {
  code: string;
  name: string;
  accountType: string;
  reconcile: boolean;
  tags?: string[];
}

// Datos de plantilla de impuesto
interface TaxTemplateData {
  code: string;
  name: string;
  description?: string;
  amountType: 'percent' | 'fixed' | 'division' | 'group';
  amount: number;
  taxUse: 'sale' | 'purchase' | 'none';
  tags?: string[];
  countrySpecificData?: Record<string, any>;
}
```

### 3.4 Servicio de Localización

```typescript
// src/modules/localization/domain/services/localization.service.ts

import { Injectable } from '@nestjs/common';

@Injectable()
export class LocalizationService {

  constructor(
    private readonly templateRepository: LocalizationTemplateRepository,
    private readonly companyRepository: CompanyRepository,
    private readonly accountService: AccountService,
    private readonly taxService: TaxService,
    private readonly fiscalPositionService: FiscalPositionService
  ) {}

  /**
   * Obtener plantillas disponibles para un país
   */
  async getTemplatesForCountry(countryId: string): Promise<LocalizationTemplate[]> {
    return this.templateRepository.findByCountry(countryId);
  }

  /**
   * Obtener la mejor plantilla para un país
   */
  async guessTemplate(countryCode: string): Promise<LocalizationTemplate | null> {
    const templates = await this.templateRepository.findByCountryCode(countryCode);

    // Ordenar por secuencia y retornar la primera
    return templates.sort((a, b) => a.sequence - b.sequence)[0] || null;
  }

  /**
   * Cargar plantilla de localización en una empresa
   */
  async loadTemplate(
    templateCode: string,
    companyId: string,
    options?: LoadTemplateOptions
  ): Promise<LocalizationResult> {
    const template = await this.templateRepository.findByCode(templateCode);
    if (!template) {
      throw new NotFoundException(`Template ${templateCode} not found`);
    }

    const company = await this.companyRepository.findById(companyId);
    if (!company) {
      throw new NotFoundException(`Company ${companyId} not found`);
    }

    // Verificar si ya tiene contabilidad
    if (options?.forceCreate !== true) {
      const hasAccounting = await this.accountService.companyHasAccounting(companyId);
      if (hasAccounting) {
        throw new ConflictError('Company already has accounting data. Use forceCreate to override.');
      }
    }

    // Obtener configuración completa (con herencia)
    const config = await this.getTemplateConfig(template);

    // Cargar en orden de dependencias
    const result: LocalizationResult = {
      templateCode,
      companyId,
      accountsCreated: 0,
      taxesCreated: 0,
      fiscalPositionsCreated: 0,
      errors: []
    };

    try {
      // 1. Pre-procesamiento
      await this.preLoadData(company, config);

      // 2. Cargar cuentas
      result.accountsCreated = await this.loadAccounts(company, config.chartOfAccounts);

      // 3. Cargar impuestos
      result.taxesCreated = await this.loadTaxes(company, config.taxes);

      // 4. Cargar posiciones fiscales
      result.fiscalPositionsCreated = await this.loadFiscalPositions(company, config.fiscalPositions);

      // 5. Configurar empresa
      await this.configureCompany(company, template, config.company);

      // 6. Post-procesamiento
      await this.postLoadData(company, config);

      // 7. Cargar traducciones
      if (options?.loadTranslations !== false) {
        await this.loadTranslations(template, company);
      }

    } catch (error) {
      result.errors.push(error.message);
      throw error;
    }

    return result;
  }

  /**
   * Obtener configuración completa de plantilla (resolviendo herencia)
   */
  private async getTemplateConfig(template: LocalizationTemplate): Promise<TemplateConfig> {
    const configs: TemplateConfig[] = [];

    // Construir cadena de herencia
    let current: LocalizationTemplate | null = template;
    while (current) {
      const templateData = await this.templateRepository.getTemplateData(current.id);
      configs.unshift(this.parseTemplateData(templateData));

      if (current.hasParent) {
        current = await this.templateRepository.findById(current.props.parentTemplateId!);
      } else {
        current = null;
      }
    }

    // Merge de configuraciones (hijo sobrescribe padre)
    return configs.reduce((merged, config) => this.mergeConfigs(merged, config), {} as TemplateConfig);
  }

  /**
   * Pre-procesamiento antes de cargar datos
   */
  private async preLoadData(company: Company, config: TemplateConfig): Promise<void> {
    // Normalizar códigos de cuenta según code_digits
    config.chartOfAccounts.accounts = config.chartOfAccounts.accounts.map(account => ({
      ...account,
      code: this.normalizeAccountCode(account.code, config.chartOfAccounts.codeDigits)
    }));

    // Establecer país fiscal
    if (!company.fiscalCountryId) {
      await this.companyRepository.updateFiscalCountry(company.id, company.countryId);
    }
  }

  /**
   * Cargar cuentas contables
   */
  private async loadAccounts(company: Company, config: ChartOfAccountsConfig): Promise<number> {
    let created = 0;

    for (const accountData of config.accounts) {
      try {
        await this.accountService.createFromTemplate(company.id, accountData);
        created++;
      } catch (error) {
        // Log pero continuar
        console.warn(`Error creating account ${accountData.code}: ${error.message}`);
      }
    }

    return created;
  }

  /**
   * Cargar impuestos
   */
  private async loadTaxes(company: Company, config: TaxConfig): Promise<number> {
    let created = 0;

    for (const taxData of config.taxes) {
      try {
        await this.taxService.createFromTemplate(company.id, taxData);
        created++;
      } catch (error) {
        console.warn(`Error creating tax ${taxData.code}: ${error.message}`);
      }
    }

    return created;
  }

  /**
   * Cargar posiciones fiscales
   */
  private async loadFiscalPositions(company: Company, config: FiscalPositionConfig): Promise<number> {
    let created = 0;

    for (const fpData of config.positions) {
      try {
        await this.fiscalPositionService.createFromTemplate(company.id, fpData);
        created++;
      } catch (error) {
        console.warn(`Error creating fiscal position ${fpData.name}: ${error.message}`);
      }
    }

    return created;
  }

  /**
   * Configurar empresa con datos de localización
   */
  private async configureCompany(
    company: Company,
    template: LocalizationTemplate,
    config: CompanyConfig
  ): Promise<void> {
    const updateData: Partial<CompanyLocalizationConfig> = {
      templateId: template.id,
      chartTemplateLoaded: true,
      taxCalculationRounding: config.angloSaxonAccounting ? 'round_globally' : 'round_per_line'
    };

    // Asignar cuentas por defecto
    // ... (lógica de asignación)

    await this.companyRepository.updateLocalizationConfig(company.id, updateData);
  }

  /**
   * Post-procesamiento después de cargar datos
   */
  private async postLoadData(company: Company, config: TemplateConfig): Promise<void> {
    // Crear cuenta de ganancias no distribuidas si no existe
    await this.accountService.ensureUnaffectedEarningsAccount(company.id);

    // Configurar diarios con impuestos por defecto
    await this.taxService.assignDefaultTaxesToJournals(company.id);
  }

  /**
   * Normalizar código de cuenta
   */
  private normalizeAccountCode(code: string, digits: number): string {
    return code.padEnd(digits, '0');
  }

  /**
   * Merge de configuraciones
   */
  private mergeConfigs(base: TemplateConfig, override: TemplateConfig): TemplateConfig {
    return {
      chartOfAccounts: {
        ...base.chartOfAccounts,
        ...override.chartOfAccounts,
        accounts: [
          ...(base.chartOfAccounts?.accounts || []),
          ...(override.chartOfAccounts?.accounts || [])
        ]
      },
      taxes: {
        ...base.taxes,
        ...override.taxes,
        taxes: [
          ...(base.taxes?.taxes || []),
          ...(override.taxes?.taxes || [])
        ]
      },
      fiscalPositions: {
        positions: [
          ...(base.fiscalPositions?.positions || []),
          ...(override.fiscalPositions?.positions || [])
        ]
      },
      company: {
        ...base.company,
        ...override.company
      }
    };
  }
}

interface LoadTemplateOptions {
  forceCreate?: boolean;
  loadTranslations?: boolean;
  installDemo?: boolean;
}

interface LocalizationResult {
  templateCode: string;
  companyId: string;
  accountsCreated: number;
  taxesCreated: number;
  fiscalPositionsCreated: number;
  errors: string[];
}
```

### 3.5 Servicio de Validación de Identificación

```typescript
// src/modules/localization/domain/services/identification-validator.service.ts

import { Injectable } from '@nestjs/common';

@Injectable()
export class IdentificationValidatorService {

  /**
   * Validar número de identificación según país y tipo
   */
  validate(value: string, countryCode: string, typeCode: string): ValidationResult {
    const cleanValue = this.clean(value);

    switch (countryCode) {
      case 'MX':
        return this.validateMexican(cleanValue, typeCode);
      case 'AR':
        return this.validateArgentine(cleanValue, typeCode);
      case 'ES':
        return this.validateSpanish(cleanValue, typeCode);
      default:
        return { isValid: true, value: cleanValue };
    }
  }

  /**
   * Validar RFC mexicano
   */
  private validateMexican(value: string, typeCode: string): ValidationResult {
    if (typeCode !== 'rfc') {
      return { isValid: true, value };
    }

    // RFC persona moral: 3 letras + 6 dígitos + 3 homoclave
    // RFC persona física: 4 letras + 6 dígitos + 3 homoclave
    const rfcRegex = /^[A-Z&Ñ]{3,4}[0-9]{6}[A-Z0-9]{3}$/;

    if (!rfcRegex.test(value)) {
      return {
        isValid: false,
        value,
        error: 'Invalid RFC format'
      };
    }

    // Validar fecha de nacimiento/constitución
    const dateStr = value.slice(-9, -3);
    if (!this.isValidDate(dateStr)) {
      return {
        isValid: false,
        value,
        error: 'Invalid date in RFC'
      };
    }

    return { isValid: true, value };
  }

  /**
   * Validar CUIT argentino
   */
  private validateArgentine(value: string, typeCode: string): ValidationResult {
    if (typeCode !== 'cuit' && typeCode !== 'cuil') {
      return { isValid: true, value };
    }

    // CUIT/CUIL: 11 dígitos con dígito verificador
    if (!/^\d{11}$/.test(value)) {
      return {
        isValid: false,
        value,
        error: 'CUIT must be 11 digits'
      };
    }

    // Validar dígito verificador
    const multipliers = [5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
    let sum = 0;
    for (let i = 0; i < 10; i++) {
      sum += parseInt(value[i]) * multipliers[i];
    }
    const checkDigit = 11 - (sum % 11);
    const expectedCheck = checkDigit === 11 ? 0 : checkDigit === 10 ? 9 : checkDigit;

    if (parseInt(value[10]) !== expectedCheck) {
      return {
        isValid: false,
        value,
        error: 'Invalid CUIT check digit'
      };
    }

    return { isValid: true, value };
  }

  /**
   * Validar NIF/CIF español
   */
  private validateSpanish(value: string, typeCode: string): ValidationResult {
    if (typeCode !== 'nif' && typeCode !== 'cif') {
      return { isValid: true, value };
    }

    // NIF: 8 dígitos + letra
    const nifRegex = /^[0-9]{8}[A-Z]$/;
    // NIE: X/Y/Z + 7 dígitos + letra
    const nieRegex = /^[XYZ][0-9]{7}[A-Z]$/;
    // CIF: letra + 7 dígitos + letra/dígito
    const cifRegex = /^[ABCDEFGHJNPQRSUVW][0-9]{7}[A-J0-9]$/;

    if (!nifRegex.test(value) && !nieRegex.test(value) && !cifRegex.test(value)) {
      return {
        isValid: false,
        value,
        error: 'Invalid NIF/NIE/CIF format'
      };
    }

    // Validar letra de control para NIF
    if (nifRegex.test(value)) {
      const letters = 'TRWAGMYFPDXBNJZSQVHLCKE';
      const number = parseInt(value.slice(0, 8));
      const expectedLetter = letters[number % 23];

      if (value[8] !== expectedLetter) {
        return {
          isValid: false,
          value,
          error: 'Invalid NIF control letter'
        };
      }
    }

    return { isValid: true, value };
  }

  private clean(value: string): string {
    return value.replace(/[-\s.]/g, '').toUpperCase();
  }

  private isValidDate(dateStr: string): boolean {
    // Formato YYMMDD
    const year = parseInt(dateStr.slice(0, 2));
    const month = parseInt(dateStr.slice(2, 4));
    const day = parseInt(dateStr.slice(4, 6));

    if (month < 1 || month > 12) return false;
    if (day < 1 || day > 31) return false;

    return true;
  }
}

interface ValidationResult {
  isValid: boolean;
  value: string;
  error?: string;
}
```

---

## Parte 4: API REST

### 4.1 Controlador de Localización

```typescript
// src/modules/localization/interfaces/http/localization.controller.ts

@Controller('localization')
@ApiTags('Localization')
export class LocalizationController {

  constructor(
    private readonly localizationService: LocalizationService
  ) {}

  @Get('templates')
  @ApiOperation({ summary: 'List available localization templates' })
  @ApiQuery({ name: 'countryId', required: false })
  async getTemplates(
    @Query('countryId') countryId?: string
  ): Promise<LocalizationTemplateDto[]> {
    if (countryId) {
      return this.localizationService.getTemplatesForCountry(countryId);
    }
    return this.localizationService.getAllTemplates();
  }

  @Get('templates/:code')
  @ApiOperation({ summary: 'Get template details' })
  async getTemplate(
    @Param('code') code: string
  ): Promise<LocalizationTemplateDetailDto> {
    return this.localizationService.getTemplateByCode(code);
  }

  @Post('templates/:code/load')
  @ApiOperation({ summary: 'Load localization template into company' })
  async loadTemplate(
    @Param('code') code: string,
    @Body() dto: LoadTemplateDto,
    @CurrentUser() user: User
  ): Promise<LocalizationResultDto> {
    return this.localizationService.loadTemplate(code, dto.companyId, {
      forceCreate: dto.forceCreate,
      loadTranslations: dto.loadTranslations,
      installDemo: dto.installDemo
    });
  }

  @Get('guess/:countryCode')
  @ApiOperation({ summary: 'Guess best template for country' })
  async guessTemplate(
    @Param('countryCode') countryCode: string
  ): Promise<LocalizationTemplateDto | null> {
    return this.localizationService.guessTemplate(countryCode);
  }

  @Get('identification-types')
  @ApiOperation({ summary: 'List identification types' })
  @ApiQuery({ name: 'countryId', required: false })
  async getIdentificationTypes(
    @Query('countryId') countryId?: string
  ): Promise<IdentificationTypeDto[]> {
    return this.localizationService.getIdentificationTypes(countryId);
  }

  @Post('validate-identification')
  @ApiOperation({ summary: 'Validate identification number' })
  async validateIdentification(
    @Body() dto: ValidateIdentificationDto
  ): Promise<IdentificationValidationResult> {
    return this.localizationService.validateIdentification(
      dto.value,
      dto.countryCode,
      dto.typeCode
    );
  }

  @Get('fiscal-positions')
  @ApiOperation({ summary: 'List fiscal positions for company' })
  async getFiscalPositions(
    @Query('companyId') companyId: string
  ): Promise<FiscalPositionDto[]> {
    return this.localizationService.getFiscalPositions(companyId);
  }

  @Get('document-types')
  @ApiOperation({ summary: 'List document types for country' })
  async getDocumentTypes(
    @Query('countryId') countryId: string
  ): Promise<DocumentTypeDto[]> {
    return this.localizationService.getDocumentTypes(countryId);
  }
}

// DTOs
class LoadTemplateDto {
  @IsUUID()
  companyId: string;

  @IsOptional()
  @IsBoolean()
  forceCreate?: boolean;

  @IsOptional()
  @IsBoolean()
  loadTranslations?: boolean;

  @IsOptional()
  @IsBoolean()
  installDemo?: boolean;
}

class ValidateIdentificationDto {
  @IsString()
  value: string;

  @IsString()
  @Length(2, 2)
  countryCode: string;

  @IsString()
  typeCode: string;
}
```

---

## Parte 5: Datos de Localización Iniciales

### 5.1 México (l10n_mx)

```typescript
// src/modules/localization/data/templates/mx.template.ts

export const mexicoTemplate: TemplateDefinition = {
  code: 'mx',
  name: 'Mexico - Plan de Cuentas General',
  countryCode: 'MX',
  codeDigits: 9,
  sequence: 1,

  chartOfAccounts: {
    defaultReceivableAccountCode: '105.01',
    defaultPayableAccountCode: '201.01',
    defaultIncomeAccountCode: '401.01',
    defaultExpenseAccountCode: '601.84',
    bankAccountPrefix: '102.01.0',
    cashAccountPrefix: '101.01.0',
    accounts: [
      // Activo
      { code: '101', name: 'Caja', accountType: 'asset_cash', reconcile: false },
      { code: '101.01', name: 'Caja y Efectivo', accountType: 'asset_cash', reconcile: false },
      { code: '102', name: 'Bancos', accountType: 'asset_cash', reconcile: true },
      { code: '102.01', name: 'Bancos Nacionales', accountType: 'asset_cash', reconcile: true },
      { code: '105', name: 'Clientes', accountType: 'asset_receivable', reconcile: true },
      { code: '105.01', name: 'Clientes Nacionales', accountType: 'asset_receivable', reconcile: true },
      // ... más cuentas

      // Pasivo
      { code: '201', name: 'Proveedores', accountType: 'liability_payable', reconcile: true },
      { code: '201.01', name: 'Proveedores Nacionales', accountType: 'liability_payable', reconcile: true },
      { code: '205', name: 'Impuestos por Pagar', accountType: 'liability_current', reconcile: false },
      { code: '205.01', name: 'IVA por Pagar', accountType: 'liability_current', reconcile: false },
      // ... más cuentas

      // Ingresos
      { code: '401', name: 'Ingresos', accountType: 'income', reconcile: false },
      { code: '401.01', name: 'Ventas Nacionales', accountType: 'income', reconcile: false },

      // Gastos
      { code: '601', name: 'Gastos Generales', accountType: 'expense', reconcile: false },
      { code: '601.84', name: 'Gastos de Operación', accountType: 'expense', reconcile: false },
    ]
  },

  taxes: {
    defaultSaleTaxCode: 'tax_iva_16_venta',
    defaultPurchaseTaxCode: 'tax_iva_16_compra',
    calculationRounding: 'round_globally',
    taxes: [
      {
        code: 'tax_iva_16_venta',
        name: 'IVA 16% Venta',
        amountType: 'percent',
        amount: 16,
        taxUse: 'sale',
        countrySpecificData: {
          l10n_mx_factor_type: 'Tasa',
          l10n_mx_tax_type: 'iva'
        }
      },
      {
        code: 'tax_iva_16_compra',
        name: 'IVA 16% Compra',
        amountType: 'percent',
        amount: 16,
        taxUse: 'purchase',
        countrySpecificData: {
          l10n_mx_factor_type: 'Tasa',
          l10n_mx_tax_type: 'iva'
        }
      },
      {
        code: 'tax_iva_0_venta',
        name: 'IVA 0% Venta',
        amountType: 'percent',
        amount: 0,
        taxUse: 'sale',
        countrySpecificData: {
          l10n_mx_factor_type: 'Tasa',
          l10n_mx_tax_type: 'iva'
        }
      },
      {
        code: 'tax_exento',
        name: 'Exento de IVA',
        amountType: 'percent',
        amount: 0,
        taxUse: 'sale',
        countrySpecificData: {
          l10n_mx_factor_type: 'Exento',
          l10n_mx_tax_type: 'iva'
        }
      },
      {
        code: 'tax_ret_isr_10',
        name: 'Retención ISR 10%',
        amountType: 'percent',
        amount: -10,
        taxUse: 'purchase',
        countrySpecificData: {
          l10n_mx_factor_type: 'Tasa',
          l10n_mx_tax_type: 'isr'
        }
      }
    ]
  },

  fiscalPositions: {
    positions: [
      {
        name: 'Régimen Nacional',
        code: 'fp_nacional',
        autoApply: true,
        sequence: 1
      },
      {
        name: 'Exportación',
        code: 'fp_export',
        autoApply: false,
        vatRequired: false,
        sequence: 10,
        taxMappings: [
          { srcCode: 'tax_iva_16_venta', destCode: 'tax_iva_0_venta' }
        ]
      }
    ]
  },

  identificationTypes: [
    {
      code: 'rfc',
      name: 'RFC',
      description: 'Registro Federal de Contribuyentes',
      isVat: true,
      validationRegex: '^[A-Z&Ñ]{3,4}[0-9]{6}[A-Z0-9]{3}$',
      sequence: 1
    },
    {
      code: 'curp',
      name: 'CURP',
      description: 'Clave Única de Registro de Población',
      isVat: false,
      validationRegex: '^[A-Z]{4}[0-9]{6}[HM][A-Z]{5}[0-9A-Z]{2}$',
      sequence: 2
    }
  ],

  company: {
    angloSaxonAccounting: true,
    additionalFields: {
      l10n_mx_edi_pac: null,
      l10n_mx_edi_certificate: null
    }
  }
};
```

### 5.2 Argentina (l10n_ar)

```typescript
// src/modules/localization/data/templates/ar.template.ts

// Plantilla base para Monotributista
export const argentinaBaseTemplate: TemplateDefinition = {
  code: 'ar_base',
  name: 'Argentina - Plan General Monotributista',
  countryCode: 'AR',
  codeDigits: 12,
  sequence: 2,

  chartOfAccounts: {
    defaultReceivableAccountCode: '1.1.03.01.001',
    defaultPayableAccountCode: '2.1.01.01.001',
    // ... cuentas base
  },

  taxes: {
    defaultSaleTaxCode: null, // Monotributista no tiene IVA
    defaultPurchaseTaxCode: null,
    taxes: []
  },

  identificationTypes: [
    {
      code: 'cuit',
      name: 'CUIT',
      description: 'Clave Única de Identificación Tributaria',
      isVat: true,
      validationRegex: '^[0-9]{11}$',
      sequence: 1
    },
    {
      code: 'cuil',
      name: 'CUIL',
      description: 'Código Único de Identificación Laboral',
      isVat: false,
      validationRegex: '^[0-9]{11}$',
      sequence: 2
    },
    {
      code: 'dni',
      name: 'DNI',
      description: 'Documento Nacional de Identidad',
      isVat: false,
      sequence: 3
    }
  ],

  company: {
    angloSaxonAccounting: false,
    additionalFields: {
      l10n_ar_afip_responsibility_type: 'monotributo',
      l10n_ar_gross_income_type: null
    }
  }
};

// Plantilla para Responsable Inscripto (extiende ar_base)
export const argentinaRITemplate: TemplateDefinition = {
  code: 'ar_ri',
  name: 'Argentina - Plan General Responsable Inscripto',
  countryCode: 'AR',
  parentCode: 'ar_base',
  codeDigits: 12,
  sequence: 1,

  taxes: {
    defaultSaleTaxCode: 'tax_iva_21_venta',
    defaultPurchaseTaxCode: 'tax_iva_21_compra',
    calculationRounding: 'round_globally',
    taxes: [
      {
        code: 'tax_iva_21_venta',
        name: 'IVA 21% Venta',
        amountType: 'percent',
        amount: 21,
        taxUse: 'sale'
      },
      {
        code: 'tax_iva_21_compra',
        name: 'IVA 21% Compra',
        amountType: 'percent',
        amount: 21,
        taxUse: 'purchase'
      },
      {
        code: 'tax_iva_10_5',
        name: 'IVA 10.5%',
        amountType: 'percent',
        amount: 10.5,
        taxUse: 'sale'
      },
      {
        code: 'tax_iva_27',
        name: 'IVA 27%',
        amountType: 'percent',
        amount: 27,
        taxUse: 'sale'
      }
    ]
  },

  documentTypes: [
    {
      code: 'FA-A',
      name: 'Factura A',
      internalType: 'invoice',
      docCodePrefix: 'FA-A',
      countrySpecificData: { l10n_ar_letter: 'A' }
    },
    {
      code: 'NC-A',
      name: 'Nota de Crédito A',
      internalType: 'credit_note',
      docCodePrefix: 'NC-A',
      countrySpecificData: { l10n_ar_letter: 'A' }
    },
    {
      code: 'FA-B',
      name: 'Factura B',
      internalType: 'invoice',
      docCodePrefix: 'FA-B',
      countrySpecificData: { l10n_ar_letter: 'B' }
    },
    {
      code: 'NC-B',
      name: 'Nota de Crédito B',
      internalType: 'credit_note',
      docCodePrefix: 'NC-B',
      countrySpecificData: { l10n_ar_letter: 'B' }
    }
  ],

  company: {
    angloSaxonAccounting: false,
    additionalFields: {
      l10n_ar_afip_responsibility_type: 'responsable_inscripto'
    }
  }
};
```

---

## Parte 6: Migraciones

### 6.1 Migración Principal

```sql
-- migrations/YYYYMMDD_add_localization_system.sql

-- Tipos enumerados
DO $$ BEGIN
    CREATE TYPE localization_template_status AS ENUM ('draft', 'active', 'deprecated', 'archived');
EXCEPTION WHEN duplicate_object THEN null;
END $$;

DO $$ BEGIN
    CREATE TYPE localization_data_type AS ENUM (
        'chart_of_accounts', 'taxes', 'fiscal_positions',
        'document_types', 'identification_types', 'banks',
        'address_format', 'company_fields'
    );
EXCEPTION WHEN duplicate_object THEN null;
END $$;

-- Tabla de plantillas de localización
CREATE TABLE IF NOT EXISTS localization_templates (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    code VARCHAR(20) NOT NULL UNIQUE,
    name VARCHAR(200) NOT NULL,
    country_id UUID NOT NULL REFERENCES countries(id),
    parent_template_id UUID REFERENCES localization_templates(id),
    sequence INTEGER DEFAULT 10,
    code_digits INTEGER DEFAULT 6,
    currency_id UUID REFERENCES currencies(id),
    status localization_template_status DEFAULT 'active',
    version VARCHAR(20),
    description TEXT,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by UUID NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_by UUID NOT NULL,
    CONSTRAINT uq_template_country_code UNIQUE (country_id, code)
);

-- Datos de plantilla
CREATE TABLE IF NOT EXISTS localization_template_data (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    template_id UUID NOT NULL REFERENCES localization_templates(id) ON DELETE CASCADE,
    data_type localization_data_type NOT NULL,
    config_data JSONB NOT NULL,
    sequence INTEGER DEFAULT 10,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT uq_template_data_type UNIQUE (template_id, data_type)
);

-- Tipos de identificación
CREATE TABLE IF NOT EXISTS identification_types (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    code VARCHAR(20) NOT NULL,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    country_id UUID REFERENCES countries(id),
    is_vat BOOLEAN DEFAULT false,
    validation_regex VARCHAR(200),
    validation_module VARCHAR(100),
    format_mask VARCHAR(100),
    sequence INTEGER DEFAULT 10,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT uq_identification_type_code UNIQUE (country_id, code)
);

-- Configuración de empresa por localización
CREATE TABLE IF NOT EXISTS company_localization_config (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE UNIQUE,
    template_id UUID REFERENCES localization_templates(id),
    country_id UUID NOT NULL REFERENCES countries(id),
    identification_type_id UUID REFERENCES identification_types(id),
    tax_id VARCHAR(50),
    tax_id_formatted VARCHAR(60),
    chart_template_loaded BOOLEAN DEFAULT false,
    fiscal_country_id UUID REFERENCES countries(id),
    bank_account_code_prefix VARCHAR(20),
    cash_account_code_prefix VARCHAR(20),
    default_sale_tax_id UUID,
    default_purchase_tax_id UUID,
    tax_calculation_rounding VARCHAR(20) DEFAULT 'round_per_line',
    country_specific_data JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by UUID NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_by UUID NOT NULL
);

-- Posiciones fiscales
CREATE TABLE IF NOT EXISTS fiscal_positions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(200) NOT NULL,
    code VARCHAR(50),
    company_id UUID NOT NULL REFERENCES companies(id),
    country_id UUID REFERENCES countries(id),
    country_group_id UUID,
    auto_apply BOOLEAN DEFAULT false,
    vat_required BOOLEAN DEFAULT false,
    zip_from VARCHAR(20),
    zip_to VARCHAR(20),
    sequence INTEGER DEFAULT 10,
    is_active BOOLEAN DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by UUID NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_by UUID NOT NULL
);

CREATE TABLE IF NOT EXISTS fiscal_position_tax_mappings (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    fiscal_position_id UUID NOT NULL REFERENCES fiscal_positions(id) ON DELETE CASCADE,
    tax_src_id UUID NOT NULL,
    tax_dest_id UUID,
    CONSTRAINT uq_fp_tax_mapping UNIQUE (fiscal_position_id, tax_src_id)
);

CREATE TABLE IF NOT EXISTS fiscal_position_account_mappings (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    fiscal_position_id UUID NOT NULL REFERENCES fiscal_positions(id) ON DELETE CASCADE,
    account_src_id UUID NOT NULL,
    account_dest_id UUID NOT NULL,
    CONSTRAINT uq_fp_account_mapping UNIQUE (fiscal_position_id, account_src_id)
);

-- Tipos de documento
CREATE TABLE IF NOT EXISTS document_types (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    code VARCHAR(20) NOT NULL,
    name VARCHAR(200) NOT NULL,
    country_id UUID REFERENCES countries(id),
    internal_type VARCHAR(50) NOT NULL,
    doc_code_prefix VARCHAR(10),
    active BOOLEAN DEFAULT true,
    sequence INTEGER DEFAULT 10,
    country_specific_data JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    CONSTRAINT uq_document_type_code UNIQUE (country_id, code)
);

-- Formato de dirección
CREATE TABLE IF NOT EXISTS address_formats (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    country_id UUID NOT NULL REFERENCES countries(id) UNIQUE,
    address_format TEXT NOT NULL,
    street_required BOOLEAN DEFAULT true,
    city_required BOOLEAN DEFAULT true,
    state_required BOOLEAN DEFAULT false,
    zip_required BOOLEAN DEFAULT false,
    zip_regex VARCHAR(100),
    phone_format VARCHAR(100),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Índices
CREATE INDEX IF NOT EXISTS idx_loc_templates_country ON localization_templates(country_id);
CREATE INDEX IF NOT EXISTS idx_loc_templates_parent ON localization_templates(parent_template_id);
CREATE INDEX IF NOT EXISTS idx_loc_template_data_type ON localization_template_data(data_type);
CREATE INDEX IF NOT EXISTS idx_id_types_country ON identification_types(country_id);
CREATE INDEX IF NOT EXISTS idx_company_loc_template ON company_localization_config(template_id);
CREATE INDEX IF NOT EXISTS idx_fiscal_pos_company ON fiscal_positions(company_id);
CREATE INDEX IF NOT EXISTS idx_doc_types_country ON document_types(country_id);

-- Datos iniciales globales
INSERT INTO identification_types (code, name, is_vat, sequence) VALUES
    ('vat', 'Tax ID / VAT', true, 1),
    ('passport', 'Passport', false, 100),
    ('foreign_id', 'Foreign ID', false, 110)
ON CONFLICT DO NOTHING;

COMMENT ON TABLE localization_templates IS 'Plantillas de localización por país';
COMMENT ON TABLE identification_types IS 'Tipos de identificación fiscal por país';
COMMENT ON TABLE fiscal_positions IS 'Posiciones fiscales para mapeo de impuestos';
COMMENT ON TABLE document_types IS 'Tipos de documento fiscal por país';
```

---

## Parte 7: Resumen de Implementación

### 7.1 Gap Cubierto

| Gap ID | Descripción | Estado |
|--------|-------------|--------|
| GAP-MGN-002-002 | Configuración de plantillas por país (l10n_*) | ✅ Especificado |

### 7.2 Componentes Definidos

| Componente | Tipo | Descripción |
|------------|------|-------------|
| `localization_templates` | Tabla | Plantillas de localización |
| `localization_template_data` | Tabla | Configuración por tipo |
| `identification_types` | Tabla | Tipos de ID por país |
| `company_localization_config` | Tabla | Config de empresa |
| `fiscal_positions` | Tabla | Posiciones fiscales |
| `document_types` | Tabla | Tipos de documento |
| `address_formats` | Tabla | Formatos de dirección |
| `LocalizationService` | Service | Carga de plantillas |
| `IdentificationValidatorService` | Service | Validación de IDs |
| `LocalizationController` | API | Endpoints REST |

### 7.3 Países Inicialmente Soportados

| País | Código | Plantillas | Identificación |
|------|--------|------------|----------------|
| México | MX | `mx` | RFC |
| Argentina | AR | `ar_base`, `ar_ri`, `ar_ex` | CUIT, CUIL, DNI |
| España | ES | `es_common`, `es_pymes` | NIF, NIE, CIF |
| Colombia | CO | `co` | NIT, CC, CE |
| Chile | CL | `cl` | RUT |

### 7.4 Integración con Otros Módulos

- **MGN-004 (Financiero)**: Plan de cuentas, impuestos, posiciones fiscales
- **MGN-003 (Catálogos)**: Bancos, monedas, países
- **MGN-006/007 (Compras/Ventas)**: Tipos de documento, posiciones fiscales

---

## Referencias

- Odoo 18 `l10n_mx`, `l10n_ar`, `l10n_es` - Módulos de localización
- Odoo 18 `account/models/chart_template.py` - Patrón @template
- SPEC-PLANTILLAS-CUENTAS.md - Especificación de chart templates
- SPEC-IMPUESTOS-AVANZADOS.md - Sistema de impuestos
- ISO 3166-1 - Códigos de país
