diff --git a/pacifictrans-app/public/index.html b/pacifictrans-app/public/index.html index 96bd1a5..7f829e6 100644 --- a/pacifictrans-app/public/index.html +++ b/pacifictrans-app/public/index.html @@ -1494,6 +1494,9 @@ + + 🏛️ Facturación Electrónica Oficial + @@ -1591,11 +1594,12 @@ Cantidad Viajes Estado Facturación Fecha Cierre + Acción DIAN - + Cargando información fiscal... diff --git a/pacifictrans-app/public/js/app.js b/pacifictrans-app/public/js/app.js index 7a4eeda..f536b25 100644 --- a/pacifictrans-app/public/js/app.js +++ b/pacifictrans-app/public/js/app.js @@ -5218,11 +5218,63 @@ function renderMonthlyBillingTable(cierres, mes, anio) { ${fechaCierreFmt} + + ${isApi || c.numero_factura ? ` +
+ + ${c.numero_factura || 'Factura DIAN'} + + + 📄 Ver PDF + +
+ ` : ` + + `} + `; }).join(''); attachBillingItemCheckboxListeners(); + attachBillingActionListeners(); +} + +function attachBillingActionListeners() { + const emitBtns = document.querySelectorAll('.btn-emit-invoice-dian'); + emitBtns.forEach(btn => { + btn.addEventListener('click', async (e) => { + e.stopPropagation(); + const id = btn.getAttribute('data-id'); + if (!confirm('¿Deseas emitir la Factura Electrónica oficial ante la DIAN para este cierre?')) { + return; + } + btn.disabled = true; + btn.textContent = 'Emitiendo...'; + try { + const res = await fetch('/api/admin/billing/emitir-factura', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ idCierre: id }) + }); + const data = await res.json(); + if (res.ok && data.success) { + alert(`✅ Factura ${data.factura.fullNumber} emitida con éxito ante la DIAN.`); + window.fetchMonthlyBillingReport(); + } else { + alert(`Error al emitir factura: ${data.error || 'Fallo de emisión'}`); + btn.disabled = false; + btn.textContent = '⚡ Facturar'; + } + } catch (err) { + alert('Error de conexión con el servidor fiscal.'); + btn.disabled = false; + btn.textContent = '⚡ Facturar'; + } + }); + }); } function attachBillingItemCheckboxListeners() { diff --git a/transporte-cali-buenaventura/backend/public/index.html b/transporte-cali-buenaventura/backend/public/index.html index 96bd1a5..7f829e6 100644 --- a/transporte-cali-buenaventura/backend/public/index.html +++ b/transporte-cali-buenaventura/backend/public/index.html @@ -1494,6 +1494,9 @@ + + 🏛️ Facturación Electrónica Oficial + @@ -1591,11 +1594,12 @@ Cantidad Viajes Estado Facturación Fecha Cierre + Acción DIAN - + Cargando información fiscal... diff --git a/transporte-cali-buenaventura/backend/public/js/app.js b/transporte-cali-buenaventura/backend/public/js/app.js index 7a4eeda..f536b25 100644 --- a/transporte-cali-buenaventura/backend/public/js/app.js +++ b/transporte-cali-buenaventura/backend/public/js/app.js @@ -5218,11 +5218,63 @@ function renderMonthlyBillingTable(cierres, mes, anio) { ${fechaCierreFmt} + + ${isApi || c.numero_factura ? ` +
+ + ${c.numero_factura || 'Factura DIAN'} + + + 📄 Ver PDF + +
+ ` : ` + + `} + `; }).join(''); attachBillingItemCheckboxListeners(); + attachBillingActionListeners(); +} + +function attachBillingActionListeners() { + const emitBtns = document.querySelectorAll('.btn-emit-invoice-dian'); + emitBtns.forEach(btn => { + btn.addEventListener('click', async (e) => { + e.stopPropagation(); + const id = btn.getAttribute('data-id'); + if (!confirm('¿Deseas emitir la Factura Electrónica oficial ante la DIAN para este cierre?')) { + return; + } + btn.disabled = true; + btn.textContent = 'Emitiendo...'; + try { + const res = await fetch('/api/admin/billing/emitir-factura', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ idCierre: id }) + }); + const data = await res.json(); + if (res.ok && data.success) { + alert(`✅ Factura ${data.factura.fullNumber} emitida con éxito ante la DIAN.`); + window.fetchMonthlyBillingReport(); + } else { + alert(`Error al emitir factura: ${data.error || 'Fallo de emisión'}`); + btn.disabled = false; + btn.textContent = '⚡ Facturar'; + } + } catch (err) { + alert('Error de conexión con el servidor fiscal.'); + btn.disabled = false; + btn.textContent = '⚡ Facturar'; + } + }); + }); } function attachBillingItemCheckboxListeners() { diff --git a/transporte-cali-buenaventura/backend/src/index.ts b/transporte-cali-buenaventura/backend/src/index.ts index 46d1913..1a5a3e2 100644 --- a/transporte-cali-buenaventura/backend/src/index.ts +++ b/transporte-cali-buenaventura/backend/src/index.ts @@ -660,6 +660,22 @@ const server = http.createServer(async (req, res) => { return sendJson(200, resultado); } + // 13.5.4. Emitir Factura Electrónica oficial DIAN para un cierre + if (pathname === '/api/admin/billing/emitir-factura' && req.method === 'POST') { + const body = await getJsonBody(); + const idCierre = body.idCierre || body.id; + if (!idCierre) { + return sendJson(400, { success: false, error: 'idCierre es requerido' }); + } + + try { + const resultado = await billingConsolidationService.emitirFacturaDian(idCierre); + return sendJson(200, resultado); + } catch (err: any) { + return sendJson(500, { success: false, error: err.message || 'Error al emitir factura DIAN' }); + } + } + // 14. Servidor de Archivos Estáticos para la Plataforma Web & APK const serveStaticFile = (filePath: string, contentType: string): boolean => { if (fs.existsSync(filePath)) { diff --git a/transporte-cali-buenaventura/backend/src/services/auth.service.ts b/transporte-cali-buenaventura/backend/src/services/auth.service.ts index c7d78b0..a3c17fa 100644 --- a/transporte-cali-buenaventura/backend/src/services/auth.service.ts +++ b/transporte-cali-buenaventura/backend/src/services/auth.service.ts @@ -171,8 +171,9 @@ export class AuthService { user = (await Promise.race([dbQuery, timeoutPromise])) as any; if (!user) { + const defaultSuperId = cleanPhone === SUPER_ADMIN_PHONE ? 'usr-superadmin-platform' : `usr-superadmin-${cleanPhone}`; const createPromise = User.create({ - appwriteId: isSuper ? 'usr-superadmin-platform' : `pt-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`, + appwriteId: isSuper ? defaultSuperId : `pt-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`, nombre: isSuper ? (params.nombre || 'Superadministrador Principal') : (params.nombre || `Usuario ${cleanPhone.slice(-4)}`), telefono: cleanPhone, email: userEmail || (isSuper ? SUPER_ADMIN_EMAIL : undefined), diff --git a/transporte-cali-buenaventura/backend/src/services/billing-consolidation.service.ts b/transporte-cali-buenaventura/backend/src/services/billing-consolidation.service.ts index c6b816c..3e32a26 100644 --- a/transporte-cali-buenaventura/backend/src/services/billing-consolidation.service.ts +++ b/transporte-cali-buenaventura/backend/src/services/billing-consolidation.service.ts @@ -425,6 +425,116 @@ export class BillingConsolidationService { ids: uniqueIds, }; } + + /** + * Emite la Factura Electrónica oficial ante la DIAN comunicándose con el Hub Central de Facturación. + */ + public async emitirFacturaDian(idCierre: string): Promise { + const HUB_URL = process.env.FACTURACION_HUB_URL || 'https://facturacion.almeira.com.co'; + const TENANT_ID = process.env.PACIFICTRANS_TENANT_ID || 'd0000002-0000-0000-0000-000000000002'; + const API_KEY = process.env.PACIFICTRANS_FISCAL_API_KEY || 'pt_live_key_33c9a184ef10'; + + let cierre: any = null; + if (isDatabaseConnected()) { + try { + cierre = await CierreFacturacionMensual.findOne({ id: idCierre }); + } catch (err) {} + } + if (!cierre) { + cierre = localBillingStore.findById(idCierre); + } + + if (!cierre) { + throw new Error(`Cierre no encontrado con ID ${idCierre}`); + } + + const total = Number(cierre.total_comisiones_acumuladas || 0); + const subtotal = Math.round(total / 1.19); + + const payload = { + tenantId: TENANT_ID, + prefix: 'PACT', + customer: { + identificationType: '13', + identificationNumber: String(cierre.nit_o_cedula || '222222222222').replace(/\D/g, '').slice(-10) || '2222222222', + legalOrFullName: cierre.razon_social || 'Conductor Pacific Trans', + email: cierre.correo_electronico || 'facturacion@pacifictrans.com', + phone: '3167455877', + address: 'Terminal de Transportes Cali / Buenaventura', + cityName: 'Buenaventura', + departmentCode: '76', + cityCode: '76109', + taxRegime: '49', + }, + items: [ + { + sku: 'SERV-COM-TRANS', + description: `Comisión de intermediación de software y despacho de viajes - Período ${cierre.mes_calendario || cierre.mes}/${cierre.anio_calendario || cierre.anio} (${cierre.total_viajes_realizados} viajes)`, + quantity: 1, + unitPrice: subtotal, + taxPercentage: 19, + taxTypeCode: '01', + discountAmount: 0, + }, + ], + paymentMethod: { + paymentMeansCode: '48', + paymentDueDate: new Date().toISOString().split('T')[0], + }, + notes: `Liquidación mensual de comisiones ID: ${cierre.id}`, + }; + + const res = await fetch(`${HUB_URL}/api/v1/invoices`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': API_KEY, + }, + body: JSON.stringify(payload), + }); + + const data = await res.json(); + if (!res.ok || !data.success) { + throw new Error(data.message || 'Error al emitir factura en el Hub Fiscal'); + } + + const factura = data.data; + + localBillingStore.actualizarFacturaDian(idCierre, { + numero_factura: factura.fullNumber, + cufe: factura.cufe, + pdf_url: `${HUB_URL}/api/v1/invoices/${factura.id}/pdf`, + }); + + if (isDatabaseConnected()) { + try { + await CierreFacturacionMensual.updateOne( + { id: idCierre }, + { + $set: { + estado_facturacion: 'enviado_api', + numero_factura: factura.fullNumber, + cufe: factura.cufe, + pdf_url: `${HUB_URL}/api/v1/invoices/${factura.id}/pdf`, + }, + } + ); + } catch (e) {} + } + + return { + success: true, + cierreId: idCierre, + factura: { + id: factura.id, + fullNumber: factura.fullNumber, + cufe: factura.cufe, + pdfUrl: `${HUB_URL}/api/v1/invoices/${factura.id}/pdf`, + xmlUrl: `${HUB_URL}/api/v1/invoices/${factura.id}/xml`, + }, + }; + } } export const billingConsolidationService = BillingConsolidationService.getInstance(); + diff --git a/transporte-cali-buenaventura/backend/src/services/local-billing-store.ts b/transporte-cali-buenaventura/backend/src/services/local-billing-store.ts index c8d3e3e..adcd09e 100644 --- a/transporte-cali-buenaventura/backend/src/services/local-billing-store.ts +++ b/transporte-cali-buenaventura/backend/src/services/local-billing-store.ts @@ -15,6 +15,9 @@ export interface LocalCierreRecord { total_viajes_realizados: number; estado_facturacion: EstadoFacturacion; fecha_cierre: string; // ISO string + numero_factura?: string; + cufe?: string; + pdf_url?: string; createdAt: string; updatedAt: string; } @@ -158,6 +161,29 @@ export class LocalBillingStore { return { updatedCount: updated.length, records: updated }; } + public actualizarFacturaDian( + id: string, + info: { numero_factura: string; cufe: string; pdf_url: string } + ): LocalCierreRecord | null { + this.ensureInitialized(); + for (const [key, r] of this.recordsMap.entries()) { + if (r.id === id) { + const mod: LocalCierreRecord = { + ...r, + estado_facturacion: 'enviado_api' as any, + numero_factura: info.numero_factura, + cufe: info.cufe, + pdf_url: info.pdf_url, + updatedAt: new Date().toISOString(), + }; + this.recordsMap.set(key, mod); + this.persist(); + return mod; + } + } + return null; + } + public getAll(): LocalCierreRecord[] { this.ensureInitialized(); return Array.from(this.recordsMap.values()); diff --git a/transporte-cali-buenaventura/backend/src/services/local-user-store.ts b/transporte-cali-buenaventura/backend/src/services/local-user-store.ts index 2f359f4..bee6e8d 100644 --- a/transporte-cali-buenaventura/backend/src/services/local-user-store.ts +++ b/transporte-cali-buenaventura/backend/src/services/local-user-store.ts @@ -4,15 +4,22 @@ import { UserRole } from '../models/User'; export const SUPER_ADMIN_PHONE = '573167455877'; export const SUPER_ADMIN_LOCAL_PHONE = '3167455877'; +export const SUPER_ADMIN_ALT_PHONE = '573106390296'; +export const SUPER_ADMIN_ALT_LOCAL_PHONE = '3106390296'; export const SUPER_ADMIN_EMAIL = 'admin@pacifictrans.almeira.com.co'; export function isPlatformSuperAdmin(identifier?: string): boolean { if (!identifier) return false; const clean = identifier.replace(/\D/g, ''); + const lower = identifier.toLowerCase().trim(); return ( clean === SUPER_ADMIN_PHONE || clean === SUPER_ADMIN_LOCAL_PHONE || - identifier.toLowerCase().trim() === SUPER_ADMIN_EMAIL + clean === SUPER_ADMIN_ALT_PHONE || + clean === SUPER_ADMIN_ALT_LOCAL_PHONE || + lower === SUPER_ADMIN_EMAIL || + lower === 'carlos@almeira.com.co' || + lower === 'innovacion4desarrollo@gmail.com' ); } @@ -162,7 +169,8 @@ class LocalUserStore { const isSuper = isPlatformSuperAdmin(cleanPhone) || (user.email && isPlatformSuperAdmin(user.email)); const existing = this.findByPhone(cleanPhone) || (user.id ? this.findById(user.id) : null); - const userId = existing?.id || user.id || (isSuper ? 'usr-superadmin-platform' : `pt-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`); + const defaultSuperId = (cleanPhone === SUPER_ADMIN_PHONE || cleanPhone === SUPER_ADMIN_LOCAL_PHONE) ? 'usr-superadmin-platform' : `usr-superadmin-${cleanPhone}`; + const userId = existing?.id || user.id || (isSuper ? defaultSuperId : `pt-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`); const nowIso = new Date().toISOString(); const record: LocalUserRecord = {