# Controlador de Facturación 4.0 - facturar40()

## Ubicación
`app/Http/Controllers/FacturacionFinKokController.php` - Método: `facturar40(Request $request)`

## Propósito
Controla el flujo completo de creación, validación, timbrado y almacenamiento de facturas CFDI 4.0.

## Flujo de Ejecución

### 1. Configuración Inicial

```php
ini_set('default_socket_timeout', 300);
ini_set('max_execution_time', 300);
set_time_limit(300);
```

- Aumenta el timeout a 5 minutos para operaciones de timbrado

### 2. Obtención de Datos de Entrada

```php
$factura = $request->input('factura');
$user = $request->input('user');
$proveedor = Proveedor::with('domicilio')->find($user['id_proveedor']);
```

#### Estructura del Request

**user**:
```json
{
    "id_proveedor": 123
}
```

**factura**:
```json
{
    "serie": "A",
    "tipoDocumento": "I",
    "moneda": "MXN",
    "tipoCambio": 1.00,
    "decimales": 2,
    "truncar": 0,
    "subtotal": 100.00,
    "base": 100.00,
    "total": 116.00,
    "descuento": 0.00,
    "formaPago": "01",
    "metodoDePago": "PUE",
    "condicionesPago": "",
    "lugarExpedicion": "12345",
    "usoCFDI": "G03",
    "exportacion": "01",
    "facturasRelacionadas": {
        "tipoRelacion": "",
        "uuids": []
    },
    "id_cliente": {
        "id_cliente": 456,
        "rFCRecep": "XAXX010101000",
        "nmbRecep": "CLIENTE SA",
        "tipoPersona": 1,
        "regimenFiscalReceptor": "601",
        "codigoPostal": "54321"
    },
    "detalles": [
        {
            "clave": "84111506",
            "codigo": "PROD001",
            "nombre": "Descripción producto",
            "unidad": "H87",
            "qty": 1,
            "precioU": 100.00,
            "descuento": 0.00,
            "peso": 0.00,
            "fracArancelaria": "",
            "objetoImp": "02",
            "id_producto": 0,
            "alumno": null
        }
    ],
    "impuestosTrasladados": [
        {
            "impuesto": "002",
            "factor": "Tasa",
            "valor": 0.16,
            "total": 16.00,
            "label": "IVA 16%"
        }
    ],
    "impuestosRetenidos": [],
    "publico": {
        "anio": 2024,
        "meses": "01",
        "periodicidad": "01"
    },
    "isComercio": 0,
    "comercio": null,
    "cartaPorte": null,
    "norma": "",
    "leyenda": "",
    "ine": null,
    "addenda": null,
    "id_addenda": null,
    "comentario": "",
    "guardarProd": false
}
```

### 3. Cálculo de TimeStamp

```php
$textLess = '-125 minutes';
if (in_array($user['id_proveedor'], $this->rfcHorasAntes))
    $textLess = '-70 hours';

$timeStamp = date("Y-m-d H:i:s", strtotime($textLess));
```

**Propósito**: 
- Evita problemas de fecha/hora con el SAT
- Permite antedatar facturas (125 minutos por defecto)
- Proveedores especiales pueden antedatar hasta 70 horas

### 4. Obtención de Folio Consecutivo

```php
$noFactura = DB::select("
    SELECT MAX(noFactura) as noFactura 
    FROM Factura f 
    JOIN IdDoc i ON f.id_factura = i.id_factura 
    WHERE id_proveedor = {$user['id_proveedor']} 
    AND i.serie = '{$factura['serie']}'
")[0];
$noFactura->noFactura++;
```

- Obtiene el último folio de la serie
- Incrementa en 1 para el nuevo folio

### 5. Validación de Certificado CSD

```php
$certificadoHaUsar = CertificadoProveedor::where('id_proveedor', '=', $user['id_proveedor'])
    ->where('status', '=', 1)
    ->first();

if(!is_object($certificadoHaUsar)){
    return Response::json([
        'success' => false,
        'errorCode' => 1009,
        'errortext' => "El certificado CSD no fue configurado de forma correcta",
        'facturaResult' => "",
        'nombreArchivo' => ""
    ]);
}
```

### 6. Construcción del Objeto $datosFactura

#### Datos Básicos
```php
$datosFactura = new \stdClass();
$datosFactura->timeStamp = $timeStamp;
$datosFactura->tipoDeComprobante = $factura['tipoDocumento'];
$datosFactura->serie = $factura['serie'];
$datosFactura->folio = $noFactura->noFactura;
$datosFactura->noCertificado = $certificadoHaUsar->noCertificado;
$datosFactura->certificado = $certificadoHaUsar->certificado;
$datosFactura->exportacion = $factura['exportacion'];
// ... más campos
```

#### Procesamiento de Conceptos

Para cada concepto en `$factura['detalles']`:

1. **Crear objeto concepto**:
```php
$concepto = new \stdClass();
$concepto->clave = $detalle['clave'];
$concepto->cdgItem = Utils::deleteExtraSpaces($detalle['codigo']);
$concepto->dscItem = Utils::deleteExtraSpaces($detalle['nombre']);
$concepto->unmdItem = $detalle['unidad'];
$concepto->qtyItem = $detalle['qty'];
$concepto->montoNetoItem = $detalle['precioU'];
$concepto->prcNetoItem = $detalle['qty'] * $detalle['precioU'];
$concepto->descuento = $detalle['descuento'];
$concepto->objetoImp = $detalle['objetoImp'];
$concepto->peso = $detalle['peso'];
$concepto->fracArancelaria = $detalle['fracArancelaria'];
```

2. **Calcular base gravable**:
```php
$base = Utils::formatNumber(
    (($detalle['qty'] * $detalle['precioU']) - $detalle['descuento']), 
    2
);
```

3. **Procesar impuestos trasladados**:
```php
foreach ($factura['impuestosTrasladados'] as $traslado) {
    if ($traslado['impuesto'] == 'XXX') {
        // Impuesto local
        $impuestoLcl = new \stdClass();
        $impuestoLcl->tipoImp = $traslado['label'];
        $impuestoLcl->tasaImp = $traslado['valor'] * 100;
        $impuestoLcl->montoImp = Utils::formatNumber($traslado['total'], 2);
        $datosFactura->impuestosTrasladadosLcl[] = $impuestoLcl;
    } else {
        // Impuesto federal (IVA, IEPS)
        $impuesto = new \stdClass();
        $impuesto->base = $base;
        $impuesto->factor = $traslado['factor'];
        $impuesto->tipoImp = $traslado['impuesto'];
        $impuesto->tasaImp = $traslado['valor'];
        
        if ($traslado['factor'] == 'Tasa') {
            $importeImp = $traslado['valor'] * $base;
        } else if ($traslado['factor'] == 'Cuota') {
            $importeImp = $traslado['valor'];
        }
        
        $roundDown = $factura['truncar'] == 1;
        $importeImp = Utils::formatNumber($importeImp, 2, $roundDown);
        $impuesto->montoImp = $importeImp;
        
        $concepto->impuestosTrasladados[] = $impuesto;
        
        // Agregar a resumen general
        $this->agregarImpuestoGeneral($datosFactura->impuestosTrasladados, $impuesto);
    }
}
```

4. **Procesar impuestos retenidos**: Similar a trasladados

5. **Complemento Alumno (IEDU)**:
```php
if(isset($detalle['alumno']) && $detalle['alumno'] != null){
    $alumnoObj = new \stdClass();
    $alumnoObj->curp = $detalle['alumno']['curp'];
    $alumnoObj->nombreAlumno = $detalle['alumno']['nombreAlumno'];
    $alumnoObj->autRVOE = $detalle['alumno']['autRVOE'];
    $alumnoObj->nivelEducativo = $detalle['alumno']['nivelEducativo'];
    $alumnoObj->rfcPago = $detalle['alumno']['rfcPago'];
    $concepto->alumno = $alumnoObj;
}
```

#### Facturas Relacionadas

```php
$datosFactura->facturasRelacionadas = [];
if (count($factura['facturasRelacionadas']['uuids']))
    foreach ($factura['facturasRelacionadas']['uuids'] as $facturaRelacionada) {
        $relacionObj = new \stdClass();
        $relacionObj->uuid = $facturaRelacionada['uUID'];
        $relacionObj->tipoRelacion = $facturaRelacionada['tipoRelacion'] ?? null;
        $datosFactura->facturasRelacionadas[] = $relacionObj;
    }
```

#### Información Global (Público en General)

```php
$datosFactura->infGlobal = null;
if($datosFactura->rFCRecep == "XAXX010101000" && $datosFactura->nmbRecep == 'PUBLICO EN GENERAL'){
    $informacionGlobal = new \stdClass();
    $informacionGlobal->anio = $factura['publico']['anio'];
    $informacionGlobal->meses = $factura['publico']['meses'];
    $informacionGlobal->periodicidad = $factura['publico']['periodicidad'];
    $datosFactura->infGlobal = $informacionGlobal;
}
```

#### Comercio Exterior

```php
if(isset($factura['isComercio']) && $factura['isComercio'] == 1){
    $datosFactura->comercio = new \stdClass();
    $datosFactura->mercanciasComercio = $mercanciasComercio;
    // Procesar datos de comercio exterior
    // Emisor, receptor, mercancías
}
```

#### Carta Porte

```php
if(isset($factura['cartaPorte']) || !empty($factura['cartaPorte']['transpInternac'])){
    $idccp = Utils::getIDCCP($proveedor);
    $cartaPorte = new \stdClass();
    // Procesar ubicaciones, mercancías, autotransporte, figuras
}
```

#### Leyendas Fiscales

```php
if(!empty($factura['norma'])){
    $datosFactura->norma = trim($factura['norma']);
    $datosFactura->leyenda = trim($factura['leyenda']);
}
```

#### INE

```php
if(!empty($factura['ine']) && !empty($factura['ine']['tipoProceso'])){
    $ineObj = new \stdClass();
    $ineObj->tipoProceso = $ine['tipoProceso'];
    // ... más campos
    $datosFactura->ine = $ineObj;
}
```

### 7. Generación del XML

```php
$xml = XMLUtils::createXML40($datosFactura, $proveedor);
$sello = $xml->query('//cfdi:Comprobante')[0]->getAttribute('Sello');
```

### 8. Guardado Temporal del XML

```php
$nombreArchivo = "$proveedor->rFCEmisor-" . date('YmdHis');
file_put_contents("tmp/$nombreArchivo.xml", $xml);
```

### 9. Timbrado con Finkok

```php
$urlTimbrado = "https://facturacion.finkok.com/servicios/soap/stamp.wsdl";
$usuarioInt = "saul.flores@soliat.com";
$passwordInt = "S3rgio.Fl0res";

if ($proveedor->id_proveedor == 1089) {
    $urlTimbrado = "https://demo-facturacion.finkok.com/servicios/soap/stamp.wsdl";
}

$params = [
    'xml' => $xml,
    'username' => $usuarioInt,
    'password' => $passwordInt
];

$client = new SoapClient($urlTimbrado, [
    'encoding' => 'UTF-8',
    "connection_timeout" => 250
]);
$client->soap_defencoding = "UTF-8";
$resultado = $client->__soapCall('stamp', [$params]);
```

### 10. Logging del Resultado

```php
$logToSave = print_r($resultado, true);
$log = env('DIR_LOGS') . "/" . date("Y-m-d") . ".log";
$fp = fopen($log, "a");
fwrite($fp, "\n\n===" . date("Y-m-d:H:i:s") . "\n\n");
fwrite($fp, "\n\n$nombreArchivo\n\n");
fwrite($fp, $logToSave);
fwrite($fp, "\n\n===");
fclose($fp);
```

### 11. Procesamiento de Respuesta Exitosa

```php
if (is_object($resultado) && 
    isset($resultado->stampResult) && 
    isset($resultado->stampResult->UUID) && 
    !empty($resultado->stampResult->xml)) {
    
    $success = true;
    $xmlTimbrado = new \DOMDocument("1.0", "uft-8");
    $xmlTimbrado->loadXML($resultado->stampResult->xml);
    
    // Extraer TimbreFiscalDigital
    $timbreFiscal = $xmlTimbrado->getElementsByTagName('TimbreFiscalDigital');
    foreach ($timbreFiscal as $timbre) {
        $idDoc->version = $timbre->getAttribute('Version');
        $idDoc->fechaTimbrado = $timbre->getAttribute('FechaTimbrado');
        $idDoc->selloCFD = $timbre->getAttribute('SelloCFD');
        $idDoc->noCertificadoSAT = $timbre->getAttribute('NoCertificadoSAT');
        $idDoc->selloSAT = $timbre->getAttribute('SelloSAT');
        $idDoc->uUID = $timbre->getAttribute('UUID');
        $idDoc->sello = $sello;
        $idDoc->rfcProvCertif = $timbre->getAttribute('RfcProvCertif');
    }
}
```

### 12. Persistencia en Base de Datos

#### Actualizar Cliente
```php
$clienteObj = Cliente::with('domicilio')->find($factura['id_cliente']['id_cliente']);
$domicilioObj = $clienteObj->domicilio;
if(!is_object($domicilioObj)){
    $domicilioObj = new DomFiscalRcp();
    $domicilioObj->id_cliente = $factura['id_cliente']['id_cliente'];
}
$clienteObj->tipoPersona = $factura['id_cliente']['tipoPersona'];
$clienteObj->regimenFiscalReceptor = $factura['id_cliente']['regimenFiscalReceptor'];
$clienteObj->nmbRecep = $factura['id_cliente']['nmbRecep'];
$clienteObj->metodoPago = $factura['formaPago'];
$clienteObj->save();

$domicilioObj->codigoPostal = $factura['id_cliente']['codigoPostal'];
$domicilioObj->save();
```

#### Guardar Factura
```php
$facturaObj = new Factura();
$facturaObj->id_proveedor = $user['id_proveedor'];
$facturaObj->id_cliente = $factura['id_cliente']['id_cliente'];
$facturaObj->id_certificadoProveedor = $certificadoHaUsar->id;
$facturaObj->nmbEmisor = $proveedor->nmbEmisor;
$facturaObj->rFCEmisor = $proveedor->rFCEmisor;
$facturaObj->nmbRecep = $factura['id_cliente']['nmbRecep'];
$facturaObj->rFCRecep = $factura['id_cliente']['rFCRecep'];
$facturaObj->regimenFiscal = $proveedor->regimenFiscal33;
$facturaObj->timeStamp = $timeStamp;
$facturaObj->noFactura = $noFactura->noFactura;
$facturaObj->version = "4.0";
$facturaObj->generadoPor = 4;
$facturaObj->nota = isset($factura['comentario']) ? Utils::deleteExtraSpaces($factura['comentario']) : '';
$facturaObj->tipoDeComprobante = $factura['tipoDocumento'];
$facturaObj->precission = $factura['decimales'];
$facturaObj->status = 1;
$facturaObj->isPagado = ($factura['metodoDePago'] == 'PUE') ? 1 : 0;
$facturaObj->numRegIdTrib = $datosFactura->numRegIdTrib;
$facturaObj->pais = $datosFactura->pais;
$facturaObj->regimenFiscalReceptor = $datosFactura->regimenFiscalReceptor;
$facturaObj->exportacion = $datosFactura->exportacion;
$facturaObj->domicilioFiscalReceptor = $datosFactura->domicilioFiscalReceptor;
$facturaObj->save();
```

#### Guardar IdDoc (Documento)
```php
$idDoc = new IdDoc();
$idDoc->id_factura = $facturaObj->id_factura;
$idDoc->serie = $factura['serie'];
$idDoc->folio = $noFactura->noFactura;
$idDoc->formaPago = $factura['formaPago'];
$idDoc->condicionesDePago = isset($factura['condicionesPago']) ? Utils::deleteExtraSpaces($factura['condicionesPago']) : '';
$idDoc->lugarExpedicion = Utils::deleteExtraSpaces($factura['lugarExpedicion']);
$idDoc->usoCFDI = $factura['usoCFDI'];
$idDoc->metodoDePago = $factura['metodoDePago'];
$idDoc->sello = $sello;
$idDoc->tipoFacturasRelacionadas = $datosFactura->tipoFacturasRelacionadas;
// Datos del timbrado
$idDoc->version = $timbreFiscal->getAttribute('Version');
$idDoc->fechaTimbrado = $timbreFiscal->getAttribute('FechaTimbrado');
$idDoc->selloCFD = $timbreFiscal->getAttribute('SelloCFD');
$idDoc->noCertificadoSAT = $timbreFiscal->getAttribute('NoCertificadoSAT');
$idDoc->selloSAT = $timbreFiscal->getAttribute('SelloSAT');
$idDoc->uUID = $timbreFiscal->getAttribute('UUID');
$idDoc->rfcProvCertif = $timbreFiscal->getAttribute('RfcProvCertif');
$idDoc->save();
```

#### Guardar Totales
```php
$totales = new Totales();
$totales->id_factura = $facturaObj->id_factura;
$totales->moneda = $factura['moneda'];
$totales->tipoCambio = $factura['tipoCambio'];
$totales->subTotal = $factura['subtotal'];
$totales->mntBase = $factura['base'];
$totales->vlrPagar = $factura['total'];
$totales->descuento = $factura['descuento'];
$totales->save();
```

#### Guardar Conceptos
```php
foreach ($datosFactura->conceptos as $concepto) {
    $conceptoObj = new Concepto();
    $conceptoObj->clave = $concepto->clave;
    $conceptoObj->cdgItem = $concepto->cdgItem;
    $conceptoObj->dscItem = $concepto->dscItem;
    $conceptoObj->unmdItem = $concepto->unmdItem;
    $conceptoObj->qtyItem = $concepto->qtyItem;
    $conceptoObj->montoNetoItem = $concepto->montoNetoItem;
    $conceptoObj->prcNetoItem = $concepto->prcNetoItem;
    $conceptoObj->descuento = $concepto->descuento;
    $conceptoObj->peso = $concepto->peso;
    $conceptoObj->fracArancelaria = $concepto->fracArancelaria;
    $conceptoObj->objetoImp = $concepto->objetoImp;
    $conceptoObj->id_factura = $facturaObj->id_factura;
    $conceptoObj->save();
    
    // Guardar impuestos del concepto
    foreach ($concepto->impuestosTrasladados as $impuesto) {
        $impuestoObj = new ExImpuestos();
        $impuestoObj->base = $impuesto->base;
        $impuestoObj->factor = $impuesto->factor;
        $impuestoObj->tipoImp = $impuesto->tipoImp;
        $impuestoObj->tasaImp = $impuesto->tasaImp;
        $impuestoObj->tipo = 1; // Traslado
        $impuestoObj->montoImp = $impuesto->montoImp;
        $impuestoObj->id_detalle = $conceptoObj->id_detalle;
        $impuestoObj->save();
    }
    
    // Guardar complemento alumno si existe
    if(isset($concepto->alumno) && $concepto->alumno != null){
        $alumnoObj = new AlumnoDetalle();
        $alumnoObj->id_detalle = $conceptoObj->id_detalle;
        $alumnoObj->curp = $concepto->alumno->curp;
        $alumnoObj->nombreAlumno = $concepto->alumno->nombreAlumno;
        // ... más campos
        $alumnoObj->save();
    }
    
    // Guardar producto si guardarProd = true
    if (isset($factura['guardarProd']) && $factura['guardarProd'] == true) {
        // Guardar/actualizar producto en catálogo
    }
}
```

#### Guardar Facturas Relacionadas
```php
foreach ($datosFactura->facturasRelacionadas as $facturaRelacionada) {
    $facturaRelacionadaObj = new FacturasRelacionadas();
    $facturaRelacionadaObj->id_factura = $facturaObj->id_factura;
    $facturaRelacionadaObj->uuid = $facturaRelacionada->uuid;
    $facturaRelacionadaObj->save();
}
```

#### Guardar Complementos Específicos

**Comercio Exterior**:
```php
if($datosFactura->comercio != null){
    $comercioEObjS = new ComercioE();
    $comercioEObjS->id_factura = $facturaObj->id_factura;
    // ... campos
    $comercioEObjS->save();
    
    // Guardar domicilios
    $domicilioEmisorObjS = new DomFiscalFact();
    // ...
    $domicilioReceptorObjS = new DomFiscalRcpFact();
    // ...
}
```

**Carta Porte**:
```php
if($datosFactura->cartaPorte != null){
    $cartaPorteObj = new CartaPorte();
    // ... campos
    $cartaPorteObj->save();
    
    // Guardar ubicaciones, mercancías, autotransporte, figuras
}
```

**INE**:
```php
if($datosFactura->ine != null){
    $ineObj = new Ine();
    // ... campos
    $ineObj->save();
}
```

**Leyendas Fiscales**:
```php
if(!empty($datosFactura->norma)){
    $leyendaObj = new LeyendaFiscal();
    // ... campos
    $leyendaObj->save();
}
```

### 13. Manejo de Errores

```php
if (is_object($resultado) && 
    isset($resultado->stampResult) && 
    isset($resultado->stampResult->Incidencias) &&
    isset($resultado->stampResult->Incidencias->Incidencia)) {
    
    $errorMsg = $resultado->stampResult->Incidencias->Incidencia->MensajeIncidencia;
}

return Response::json([
    'success' => $success,
    'errorCode' => $errorCode,
    'errortext' => $errortext,
    'facturaResult' => $facturaResult,
    'nombreArchivo' => $nombreArchivo,
    'extraInfo' => $extraInfo
]);
```

## Códigos de Error

- **1009**: Certificado CSD no configurado
- **10001**: Error general de timbrado
- Otros códigos según respuesta de Finkok

## Respuesta Exitosa

```json
{
    "success": true,
    "errorCode": 0,
    "errortext": "",
    "facturaResult": "A123",
    "nombreArchivo": "RFC123456789-20240115103000",
    "extraInfo": ""
}
```

## Consideraciones Importantes

1. **Timeout**: Configurado a 5 minutos para operaciones largas
2. **Folio Consecutivo**: Auto-incrementado por serie
3. **Antedatado**: Facturas se antedatan 125 minutos (o 70 horas para ciertos proveedores)
4. **Logs**: Todas las operaciones se registran en archivos diarios
5. **Truncamiento**: Opción para truncar impuestos vs redondear
6. **Productos**: Opción para guardar productos en catálogo
7. **Público en General**: Auto-detecta y agrega información global
8. **Impuestos Locales**: Separados de federales (XXX)
9. **ObjetoImp**: Obligatorio en versión 4.0
10. **Complementos**: Se guardan en tablas separadas relacionadas por id_factura
