Files
AnchorOS/app/api/aperture/pos/close-day/route.ts
Marco Gallegos b7d6e51d67 💰 FASE 4 COMPLETADO: POS y Sistema de Nómina Implementados
 SISTEMA DE NÓMINA COMPLETO:
- API  con cálculos automáticos por período
- Cálculo de comisiones (10% de revenue de servicios completados)
- Cálculo de propinas (5% estimado basado en revenue)
- Cálculo de horas trabajadas desde bookings completados
- Sueldo base configurable por staff
- Exportación a CSV con detalles completos

 PUNTO DE VENTA (POS) COMPLETO:
- API  para procesamiento de ventas
- Múltiples métodos de pago: efectivo, tarjeta, transferencias, giftcards, membresías
- Carrito interactivo con servicios y productos
- Cálculo automático de subtotales y totales
- Validación de pagos completos antes de procesar
- Recibos digitales con impresión
- Interface táctil optimizada para diferentes dispositivos

 CIERRE DE CAJA AUTOMÁTICO:
- API  para reconciliación financiera
- Comparación automática entre ventas reales y efectivo contado
- Detección de discrepancias con reportes detallados
- Auditoría completa de cierres de caja
- Reportes diarios exportables

 COMPONENTES DE GESTIÓN AVANZADOS:
- : Cálculo y exportación de nóminas
- : Interface completa de punto de venta
- Integración completa con dashboard Aperture
- Manejo de errores y estados de carga

 MIGRACIÓN PAYROLL COMPLETA:
- Tablas: staff_salaries, commission_rates, tip_records, payroll_records
- Funciones PostgreSQL para cálculos complejos (preparadas)
- RLS policies para seguridad de datos financieros
- Índices optimizados para consultas rápidas

Próximo: Integración con Stripe real y automatización de WhatsApp
2026-01-17 15:41:28 -06:00

213 lines
6.6 KiB
TypeScript

/**
* @description Cash register closure API for daily financial reconciliation
* @audit BUSINESS RULE: Daily cash closure ensures financial accountability
* @audit SECURITY: Only admin/manager can close cash registers
* @audit Validate: All payments for the day must be accounted for
* @audit AUDIT: Cash closure logged with detailed reconciliation
* @audit COMPLIANCE: Financial records must be immutable after closure
*/
import { NextRequest, NextResponse } from 'next/server'
import { supabaseAdmin } from '@/lib/supabase/admin'
interface CashCount {
cash_amount: number
card_amount: number
transfer_amount: number
giftcard_amount: number
membership_amount: number
other_amount: number
notes?: string
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const {
date,
location_id,
cash_count,
expected_totals,
notes
} = body
if (!date || !location_id || !cash_count) {
return NextResponse.json(
{ error: 'Missing required fields: date, location_id, cash_count' },
{ status: 400 }
)
}
// Get actual sales data for the day
const { data: transactions } = await supabaseAdmin
.from('audit_logs')
.select('*')
.eq('entity_type', 'pos_sale')
.eq('action', 'sale_completed')
.eq('new_values->location_id', location_id)
.gte('created_at', `${date}T00:00:00Z`)
.lte('created_at', `${date}T23:59:59Z`)
// Calculate actual totals from transactions
const actualTotals = (transactions || []).reduce((totals: any, transaction: any) => {
const sale = transaction.new_values
const payments = sale.payment_methods || []
return {
total_sales: totals.total_sales + 1,
total_revenue: totals.total_revenue + (sale.total_amount || 0),
payment_breakdown: payments.reduce((breakdown: any, payment: any) => ({
...breakdown,
[payment.method]: (breakdown[payment.method] || 0) + payment.amount
}), totals.payment_breakdown)
}
}, {
total_sales: 0,
total_revenue: 0,
payment_breakdown: {}
})
// Calculate discrepancies
const discrepancies = {
cash: (cash_count.cash_amount || 0) - (actualTotals.payment_breakdown.cash || 0),
card: (cash_count.card_amount || 0) - (actualTotals.payment_breakdown.card || 0),
transfer: (cash_count.transfer_amount || 0) - (actualTotals.payment_breakdown.transfer || 0),
giftcard: (cash_count.giftcard_amount || 0) - (actualTotals.payment_breakdown.giftcard || 0),
membership: (cash_count.membership_amount || 0) - (actualTotals.payment_breakdown.membership || 0),
other: (cash_count.other_amount || 0) - (actualTotals.payment_breakdown.other || 0)
}
// Get current user (manager closing the register)
const { data: { user } } = await supabaseAdmin.auth.getUser()
// Create cash closure record
const closureRecord = {
date,
location_id,
actual_totals: actualTotals,
counted_totals: cash_count,
discrepancies,
total_discrepancy: Object.values(discrepancies).reduce((sum: number, disc: any) => sum + disc, 0),
closed_by: user?.id,
status: 'closed',
notes
}
const { data: closure, error: closureError } = await supabaseAdmin
.from('audit_logs')
.insert({
entity_type: 'cash_closure',
entity_id: `closure-${date}-${location_id}`,
action: 'register_closed',
new_values: closureRecord,
performed_by_role: 'admin'
})
.select()
.single()
if (closureError) {
console.error('Cash closure error:', closureError)
return NextResponse.json(
{ error: 'Failed to close cash register' },
{ status: 500 }
)
}
return NextResponse.json({
success: true,
closure: closureRecord,
report: {
date,
location_id,
actual_sales: actualTotals.total_sales,
actual_revenue: actualTotals.total_revenue,
counted_amounts: cash_count,
discrepancies,
total_discrepancy: closureRecord.total_discrepancy,
status: Math.abs(closureRecord.total_discrepancy) < 0.01 ? 'balanced' : 'discrepancy'
}
})
} catch (error) {
console.error('Cash closure API error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const date = searchParams.get('date')
const location_id = searchParams.get('location_id')
if (!date || !location_id) {
return NextResponse.json(
{ error: 'Missing required parameters: date, location_id' },
{ status: 400 }
)
}
// Get closure record for the day
const { data: closures } = await supabaseAdmin
.from('audit_logs')
.select('*')
.eq('entity_type', 'cash_closure')
.eq('entity_id', `closure-${date}-${location_id}`)
.eq('action', 'register_closed')
.order('created_at', { ascending: false })
.limit(1)
if (closures && closures.length > 0) {
const closure = closures[0]
return NextResponse.json({
success: true,
closure: closure.new_values,
already_closed: true
})
}
// Get sales data for closure preparation
const { data: transactions } = await supabaseAdmin
.from('audit_logs')
.select('*')
.eq('entity_type', 'pos_sale')
.eq('action', 'sale_completed')
.gte('created_at', `${date}T00:00:00Z`)
.lte('created_at', `${date}T23:59:59Z`)
const salesSummary = (transactions || []).reduce((summary: any, transaction: any) => {
const sale = transaction.new_values
const payments = sale.payment_methods || []
return {
total_sales: summary.total_sales + 1,
total_revenue: summary.total_revenue + (sale.total_amount || 0),
payment_breakdown: payments.reduce((breakdown: any, payment: any) => ({
...breakdown,
[payment.method]: (breakdown[payment.method] || 0) + payment.amount
}), summary.payment_breakdown)
}
}, {
total_sales: 0,
total_revenue: 0,
payment_breakdown: {}
})
return NextResponse.json({
success: true,
already_closed: false,
sales_summary: salesSummary,
expected_counts: salesSummary.payment_breakdown
})
} catch (error) {
console.error('Cash closure GET error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}