first commit
This commit is contained in:
212
Services/ConstanciaFiscalService.php
Normal file
212
Services/ConstanciaFiscalService.php
Normal file
@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyContacts\Services;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Spatie\PdfToText\Pdf;
|
||||
use Koneko\SatCatalogs\Models\{Estado,Localidad,Municipio,Colonia,RegimenFiscal};
|
||||
|
||||
class ConstanciaFiscalService
|
||||
{
|
||||
protected $data = [];
|
||||
protected $actividades = [];
|
||||
protected $regimenes = [];
|
||||
|
||||
/**
|
||||
* Procesa el PDF de la Constancia de Situación Fiscal y extrae la información relevante.
|
||||
*
|
||||
* @param UploadedFile $file
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public function extractData(UploadedFile $file): array
|
||||
{
|
||||
if ($file->getClientOriginalExtension() !== 'pdf') {
|
||||
throw new Exception('El archivo debe ser un PDF.');
|
||||
}
|
||||
|
||||
$text = Pdf::getText($file->getRealPath());
|
||||
$text_array = array_values(array_filter(explode("\n", $text), 'trim')); // Limpia líneas vacías
|
||||
|
||||
// **Estructura base con NULL en todos los campos**
|
||||
$this->data = array_fill_keys([
|
||||
"rfc", "curp", "name", "last_name", "second_last_name", "nombre_comercial", "telefono",
|
||||
"fecha_inicio_operaciones", "estatus_padron", "fecha_ultimo_cambio_estado",
|
||||
"c_codigo_postal", "c_estado", "estado", "c_localidad", "localidad",
|
||||
"c_municipio", "municipio", "c_colonia", "colonia",
|
||||
"vialidad", "entre_calle", "num_ext", "num_int", "regimenes"
|
||||
], null);
|
||||
|
||||
$this->data['regimenes'] = []; // Siempre inicializar como array
|
||||
|
||||
$this->procesarDatosInline($text_array);
|
||||
|
||||
$this->procesarRegimenes($text_array);
|
||||
|
||||
if (empty($this->data['rfc'])) {
|
||||
throw new Exception('No se encontró RFC en el documento.');
|
||||
}
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesa los datos generales hasta encontrar "Regímenes:".
|
||||
*/
|
||||
protected function procesarDatosInline(array $text_array)
|
||||
{
|
||||
foreach ($text_array as $index => $linea) {
|
||||
$linea = trim($linea);
|
||||
|
||||
if ($linea === "Regímenes:") {
|
||||
return; // Detener la iteración
|
||||
}
|
||||
|
||||
// RFC, CURP, Nombre y Apellidos
|
||||
if (str_contains($linea, "RFC:")) {
|
||||
$this->data['rfc'] = trim($text_array[$index + 1]);
|
||||
|
||||
} elseif (str_contains($linea, "CURP:")) {
|
||||
$this->data['curp'] = trim($text_array[$index + 1]);
|
||||
|
||||
} elseif (str_contains($linea, "Nombre (s):")) {
|
||||
$this->data['name'] = trim($text_array[$index + 1]);
|
||||
|
||||
} elseif (str_contains($linea, "Primer Apellido:")) {
|
||||
$this->data['last_name'] = trim($text_array[$index + 1]);
|
||||
|
||||
} elseif (str_contains($linea, "Segundo Apellido:")) {
|
||||
$this->data['second_last_name'] = trim($text_array[$index + 1]);
|
||||
}
|
||||
|
||||
// Fechas y estatus
|
||||
elseif (str_contains($linea, "Fecha inicio de operaciones:")) {
|
||||
$this->data['fecha_inicio_operaciones'] = trim($text_array[$index + 1]);
|
||||
|
||||
} elseif (str_contains($linea, "Estatus en el padrón:")) {
|
||||
$this->data['estatus_padron'] = trim($text_array[$index + 1]);
|
||||
|
||||
} elseif (str_contains($linea, "Fecha de último cambio de estado:")) {
|
||||
$this->data['fecha_ultimo_cambio_estado'] = trim($text_array[$index + 1]);
|
||||
}
|
||||
|
||||
// Nombre Comercial
|
||||
elseif (str_contains($linea, "Nombre Comercial:")) {
|
||||
$this->data['nombre_comercial'] = trim($text_array[$index + 1]);
|
||||
}
|
||||
|
||||
// **Teléfono (Dos Líneas)**
|
||||
elseif (str_contains($linea, "Tel. Fijo Lada:")) {
|
||||
$lada = trim(str_replace("Tel. Fijo Lada:", "", $linea));
|
||||
|
||||
if (isset($text_array[$index + 1]) && str_contains($text_array[$index + 1], "Número:")) {
|
||||
$numero = trim(str_replace("Número:", "", $text_array[$index + 1]));
|
||||
$this->data['telefono'] = $lada . " " . $numero;
|
||||
}
|
||||
}
|
||||
|
||||
// **Código Postal**
|
||||
elseif (str_contains($linea, "Código Postal:")) {
|
||||
$this->data['c_codigo_postal'] = trim(str_replace("Código Postal:", "", $linea));
|
||||
}
|
||||
|
||||
// **Estado**
|
||||
elseif (str_contains($linea, "Nombre de la Entidad Federativa:")) {
|
||||
$estado_nombre = trim(str_replace("Nombre de la Entidad Federativa:", "", $linea));
|
||||
$estado = Estado::where('nombre_del_estado', 'like', "%{$estado_nombre}%")->first();
|
||||
|
||||
$this->data['c_estado'] = $estado->c_estado ?? null;
|
||||
$this->data['estado'] = $estado_nombre;
|
||||
}
|
||||
|
||||
// **Municipio**
|
||||
elseif (str_contains($linea, "Nombre del Municipio o Demarcación Territorial:")) {
|
||||
$municipio_nombre = trim(str_replace("Nombre del Municipio o Demarcación Territorial:", "", $linea));
|
||||
$municipio = Municipio::where('descripcion', 'like', "%{$municipio_nombre}%")->first();
|
||||
|
||||
$this->data['c_municipio'] = $municipio->c_municipio ?? null;
|
||||
$this->data['municipio'] = $municipio_nombre;
|
||||
}
|
||||
|
||||
// **Colonia**
|
||||
elseif (str_contains($linea, "Nombre de la Colonia:")) {
|
||||
$colonia_nombre = trim(str_replace("Nombre de la Colonia:", "", $linea));
|
||||
$colonia = Colonia::where('nombre_del_asentamiento', 'like', "%{$colonia_nombre}%")->first();
|
||||
|
||||
$this->data['c_colonia'] = $colonia->c_colonia ?? null;
|
||||
$this->data['colonia'] = $colonia_nombre;
|
||||
}
|
||||
|
||||
// **Localidad** (Nueva implementación)
|
||||
elseif (str_contains($linea, "Nombre de la Localidad:")) {
|
||||
$localidad_nombre = trim(str_replace("Nombre de la Localidad:", "", $linea));
|
||||
$localidad = Localidad::where('descripcion', 'like', "%{$localidad_nombre}%")->first();
|
||||
|
||||
$this->data['c_localidad'] = $localidad->c_localidad ?? null;
|
||||
$this->data['localidad'] = $localidad_nombre;
|
||||
}
|
||||
|
||||
// **Entre Calle**
|
||||
elseif (str_contains($linea, "Entre Calle:")) {
|
||||
$this->data['entre_calle'] = trim(str_replace("Entre Calle:", "", $linea));
|
||||
}
|
||||
|
||||
// **Dirección**
|
||||
elseif (str_contains($linea, "Nombre de Vialidad:")) {
|
||||
$this->data['vialidad'] = trim(str_replace("Nombre de Vialidad:", "", $linea));
|
||||
|
||||
} elseif (str_contains($linea, "Número Exterior:")) {
|
||||
$this->data['num_ext'] = trim(str_replace("Número Exterior:", "", $linea));
|
||||
|
||||
} elseif (str_contains($linea, "Número Interior:") && !str_contains($text_array[$index + 1], "Nombre de la Colonia:")) {
|
||||
$this->data['num_int'] = trim(str_replace("Número Interior:", "", $linea));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Procesa los regímenes fiscales hasta encontrar "Obligaciones:".
|
||||
*/
|
||||
protected function procesarRegimenes(array $text_array)
|
||||
{
|
||||
$procesando = false;
|
||||
|
||||
foreach ($text_array as $index => $linea) {
|
||||
if (trim($linea) === "Regímenes:") {
|
||||
$procesando = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trim($linea) === "Obligaciones:") {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($procesando) {
|
||||
// **Filtrar líneas no relevantes**
|
||||
if (in_array($linea, ["Régimen", "Fecha Inicio", "Fecha Fin", "Obligaciones:"])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// **Si la línea es una fecha, asociarla al régimen anterior**
|
||||
if (preg_match('/\d{2}\/\d{2}\/\d{4}/', $linea)) {
|
||||
$ultimo_regimen = &$this->data['regimenes'][count($this->data['regimenes']) - 1];
|
||||
if (isset($ultimo_regimen)) {
|
||||
$ultimo_regimen['fecha_inicio'] = trim($linea);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($linea)) {
|
||||
$regimenFiscal = RegimenFiscal::where('descripcion', 'like', "%{$linea}%")->first();
|
||||
$this->data['regimenes'][] = [
|
||||
'regimen_fiscal' => trim($linea),
|
||||
'c_regimen_fiscal' => $regimenFiscal->c_regimen_fiscal ?? null,
|
||||
'fecha_inicio' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
101
Services/ContactCatalogService.php
Normal file
101
Services/ContactCatalogService.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyContacts\Services;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ContactCatalogService
|
||||
{
|
||||
protected $catalogs = [
|
||||
'users' => [
|
||||
'table' => 'users',
|
||||
'key' => 'id',
|
||||
'value' => "CONCAT(name, ' ', COALESCE(last_name, ''), ' - ', email) as item",
|
||||
'search_columns' => ['name', 'last_name', 'email', 'rfc'],
|
||||
'order_by' => 'name',
|
||||
'limit' => 20,
|
||||
],
|
||||
'contacts' => [
|
||||
'table' => 'users',
|
||||
'key' => 'id',
|
||||
'value' => "CONCAT(name, ' ', COALESCE(last_name, '')) as item",
|
||||
'search_columns' => ['name', 'last_name', 'rfc', 'company'],
|
||||
'extra_conditions' => ['is_customer' => true],
|
||||
'order_by' => 'name',
|
||||
'limit' => 20,
|
||||
],
|
||||
'providers' => [
|
||||
'table' => 'users',
|
||||
'key' => 'id',
|
||||
'value' => "CONCAT(name, ' ', COALESCE(last_name, '')) as item",
|
||||
'search_columns' => ['name', 'last_name', 'rfc', 'company'],
|
||||
'extra_conditions' => ['is_provider' => true],
|
||||
'order_by' => 'name',
|
||||
'limit' => 20,
|
||||
],
|
||||
'addresses' => [
|
||||
'table' => 'contactable_addresses',
|
||||
'key' => 'id',
|
||||
'value' => "CONCAT(direccion, ' #', num_ext, ' ', COALESCE(num_int, ''), ', ', c_codigo_postal) as item",
|
||||
'search_columns' => ['direccion', 'num_ext', 'c_codigo_postal'],
|
||||
'extra_conditions' => ['contactable_id', 'contactable_type'],
|
||||
'order_by' => 'direccion',
|
||||
'limit' => 10,
|
||||
],
|
||||
'emails' => [
|
||||
'table' => 'contactable_items',
|
||||
'key' => 'id',
|
||||
'value' => 'data_contact as item',
|
||||
'search_columns' => ['data_contact'],
|
||||
'extra_conditions' => ['contactable_id', 'contactable_type', 'type' => 1], // 1 = email
|
||||
'order_by' => 'data_contact',
|
||||
'limit' => 10,
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Método para obtener datos de catálogos con filtrado flexible.
|
||||
*
|
||||
* @param string $catalog
|
||||
* @param string|null $searchTerm
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public function searchCatalog(string $catalog, ?string $searchTerm = '', array $options = []): array
|
||||
{
|
||||
if (!isset($this->catalogs[$catalog])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$config = $this->catalogs[$catalog];
|
||||
$query = DB::table($config['table']);
|
||||
|
||||
// Selección de columnas
|
||||
$query->selectRaw("{$config['key']}, {$config['value']}");
|
||||
|
||||
// Aplicar condiciones extra
|
||||
if (!empty($config['extra_conditions'])) {
|
||||
foreach ($config['extra_conditions'] as $column => $value) {
|
||||
if (isset($options[$column])) {
|
||||
$query->where($column, $options[$column]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Búsqueda
|
||||
if ($searchTerm) {
|
||||
$query->where(function ($subQ) use ($config, $searchTerm) {
|
||||
foreach ($config['search_columns'] as $column) {
|
||||
$subQ->orWhere($column, 'LIKE', "%{$searchTerm}%");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Orden y límite
|
||||
$query->orderBy($config['order_by'] ?? $config['key'], 'asc');
|
||||
$query->limit($options['limit'] ?? $config['limit'] ?? 20);
|
||||
|
||||
return $query->pluck('item', $config['key'])->toArray();
|
||||
}
|
||||
}
|
48
Services/ContactableItemService.php
Normal file
48
Services/ContactableItemService.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyContacts\Services;
|
||||
|
||||
use Koneko\VuexyAdmin\Models\Setting;
|
||||
|
||||
class ContactableItemService
|
||||
{
|
||||
protected $settingsKey = 'contactable_item_types';
|
||||
|
||||
/**
|
||||
* Obtiene la lista de tipos de medios de contacto desde settings.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getContactableTypes(): array
|
||||
{
|
||||
$setting = Setting::where('key', $this->settingsKey)->first();
|
||||
|
||||
return $setting ? json_decode($setting->value, true) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda una lista de tipos de medios de contacto.
|
||||
*
|
||||
* @param array $types
|
||||
* @return bool
|
||||
*/
|
||||
public function saveContactableTypes(array $types): bool
|
||||
{
|
||||
return Setting::updateOrCreate(
|
||||
['key' => $this->settingsKey],
|
||||
['value' => json_encode($types)]
|
||||
) ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene un tipo de contacto específico por ID.
|
||||
*
|
||||
* @param int $id
|
||||
* @return array|null
|
||||
*/
|
||||
public function getTypeById(int $id): ?array
|
||||
{
|
||||
$types = $this->getContactableTypes();
|
||||
return collect($types)->firstWhere('id', $id);
|
||||
}
|
||||
}
|
159
Services/FacturaXmlService.php
Normal file
159
Services/FacturaXmlService.php
Normal file
@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
namespace Koneko\VuexyContacts\Services;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use SimpleXMLElement;
|
||||
use Exception;
|
||||
|
||||
class FacturaXmlService
|
||||
{
|
||||
protected $xml;
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* Procesa un archivo XML subido y extrae los datos.
|
||||
*
|
||||
* @param UploadedFile $file
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public function processUploadedFile(UploadedFile $file): array
|
||||
{
|
||||
if (!$file->isValid()) {
|
||||
throw new Exception("El archivo no es válido o está corrupto.");
|
||||
}
|
||||
|
||||
// Validamos que sea XML
|
||||
if ($file->getClientOriginalExtension() !== 'xml') {
|
||||
throw new Exception("Solo se permiten archivos XML.");
|
||||
}
|
||||
|
||||
// Cargar XML desde el contenido del archivo
|
||||
$xmlContent = file_get_contents($file->getRealPath());
|
||||
$this->loadXml($xmlContent);
|
||||
|
||||
// Extraer datos del XML
|
||||
return $this->extractData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Carga el XML y lo convierte en un objeto SimpleXMLElement
|
||||
*
|
||||
* @param string $xmlContent
|
||||
* @throws Exception
|
||||
*/
|
||||
public function loadXml(string $xmlContent): void
|
||||
{
|
||||
try {
|
||||
$this->xml = new SimpleXMLElement($xmlContent);
|
||||
|
||||
// Registrar espacios de nombres
|
||||
$this->xml->registerXPathNamespace('cfdi', 'http://www.sat.gob.mx/cfd/4');
|
||||
$this->xml->registerXPathNamespace('tfd', 'http://www.sat.gob.mx/TimbreFiscalDigital');
|
||||
|
||||
} catch (Exception $e) {
|
||||
throw new Exception('Error al cargar el XML: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extrae los datos clave de la factura XML
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function extractData(): array
|
||||
{
|
||||
if (!$this->xml) {
|
||||
throw new Exception('No se ha cargado un XML válido.');
|
||||
}
|
||||
|
||||
$this->data['comprobante'] = $this->extractComprobante();
|
||||
$this->data['emisor'] = $this->extractEmisor();
|
||||
$this->data['receptor'] = $this->extractReceptor();
|
||||
$this->data['conceptos'] = $this->extractConceptos();
|
||||
$this->data['impuestos'] = $this->extractImpuestos();
|
||||
$this->data['timbreFiscal'] = $this->extractTimbreFiscal();
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
protected function extractComprobante(): array
|
||||
{
|
||||
return [
|
||||
'fecha' => (string) $this->xml['Fecha'] ?? null,
|
||||
'total' => (float) $this->xml['Total'] ?? 0.0,
|
||||
'moneda' => (string) $this->xml['Moneda'] ?? 'MXN',
|
||||
'tipoDeComprobante' => (string) $this->xml['TipoDeComprobante'] ?? null,
|
||||
'metodoPago' => (string) $this->xml['MetodoPago'] ?? null,
|
||||
'lugarExpedicion' => (string) $this->xml['LugarExpedicion'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
protected function extractEmisor(): array
|
||||
{
|
||||
$emisor = $this->xml->xpath('//cfdi:Emisor')[0] ?? null;
|
||||
if (!$emisor) return [];
|
||||
|
||||
return [
|
||||
'rfc' => (string) $emisor['Rfc'] ?? null,
|
||||
'nombre' => (string) $emisor['Nombre'] ?? null,
|
||||
'regimenFiscal' => (string) $emisor['RegimenFiscal'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
protected function extractReceptor(): array
|
||||
{
|
||||
$receptor = $this->xml->xpath('//cfdi:Receptor')[0] ?? null;
|
||||
if (!$receptor) return [];
|
||||
|
||||
return [
|
||||
'rfc' => (string) $receptor['Rfc'] ?? null,
|
||||
'nombre' => (string) $receptor['Nombre'] ?? null,
|
||||
'usoCFDI' => (string) $receptor['UsoCFDI'] ?? null,
|
||||
'regimenFiscal' => (string) $receptor['RegimenFiscalReceptor'] ?? null,
|
||||
'domicilioFiscal' => (string) $receptor['DomicilioFiscalReceptor'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
protected function extractConceptos(): array
|
||||
{
|
||||
$conceptos = [];
|
||||
foreach ($this->xml->xpath('//cfdi:Concepto') as $concepto) {
|
||||
$conceptos[] = [
|
||||
'descripcion' => (string) $concepto['Descripcion'] ?? null,
|
||||
'cantidad' => (float) $concepto['Cantidad'] ?? 1.0,
|
||||
'valorUnitario' => (float) $concepto['ValorUnitario'] ?? 0.0,
|
||||
'importe' => (float) $concepto['Importe'] ?? 0.0,
|
||||
'claveProdServ' => (string) $concepto['ClaveProdServ'] ?? null,
|
||||
'claveUnidad' => (string) $concepto['ClaveUnidad'] ?? null,
|
||||
];
|
||||
}
|
||||
return $conceptos;
|
||||
}
|
||||
|
||||
protected function extractImpuestos(): array
|
||||
{
|
||||
$impuestos = $this->xml->xpath('//cfdi:Impuestos')[0] ?? null;
|
||||
if (!$impuestos) return [];
|
||||
|
||||
return [
|
||||
'totalImpuestosTrasladados' => (float) $impuestos['TotalImpuestosTrasladados'] ?? 0.0,
|
||||
'totalImpuestosRetenidos' => (float) $impuestos['TotalImpuestosRetenidos'] ?? 0.0,
|
||||
];
|
||||
}
|
||||
|
||||
protected function extractTimbreFiscal(): array
|
||||
{
|
||||
$timbre = $this->xml->xpath('//tfd:TimbreFiscalDigital')[0] ?? null;
|
||||
if (!$timbre) return [];
|
||||
|
||||
return [
|
||||
'uuid' => (string) $timbre['UUID'] ?? null,
|
||||
'fechaTimbrado' => (string) $timbre['FechaTimbrado'] ?? null,
|
||||
'selloCFD' => (string) $timbre['SelloCFD'] ?? null,
|
||||
'selloSAT' => (string) $timbre['SelloSAT'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user