FASE 5 - Clientes y Fidelización: - Client Management (CRM) con búsqueda fonética - Galería de fotos restringida por tier (VIP/Black/Gold) - Sistema de Lealtad con puntos y expiración (6 meses) - Membresías (Gold, Black, VIP) con beneficios configurables - Notas técnicas con timestamp APIs Implementadas: - GET/POST /api/aperture/clients - CRUD completo de clientes - GET /api/aperture/clients/[id] - Detalles con historial de reservas - POST /api/aperture/clients/[id]/notes - Notas técnicas - GET/POST /api/aperture/clients/[id]/photos - Galería de fotos - GET /api/aperture/loyalty - Resumen de lealtad - GET/POST /api/aperture/loyalty/[customerId] - Historial y puntos FASE 6 - Pagos y Protección: - Stripe Webhooks (payment_intent.succeeded, payment_failed, charge.refunded) - No-Show Logic con detección automática (ventana 12h) - Check-in de clientes para prevenir no-shows - Override Admin para waivar penalizaciones - Finanzas y Reportes (expenses, daily closing, staff performance) APIs Implementadas: - POST /api/webhooks/stripe - Handler de webhooks Stripe - GET /api/cron/detect-no-shows - Detectar no-shows (cron job) - POST /api/aperture/bookings/no-show - Aplicar penalización - POST /api/aperture/bookings/check-in - Registrar check-in - GET /api/aperture/finance - Resumen financiero - POST/GET /api/aperture/finance/daily-closing - Reportes diarios - GET/POST /api/aperture/finance/expenses - Gestión de gastos - GET /api/aperture/finance/staff-performance - Performance de staff Documentación: - docs/APERATURE_SPECS.md - Especificaciones técnicas completas - docs/APERTURE_SQUARE_UI.md - Ejemplos de Radix UI con Square UI - docs/API.md - Actualizado con nuevas rutas Migraciones SQL: - 20260118050000_clients_loyalty_system.sql - Clientes, fotos, lealtad, membresías - 20260118060000_stripe_webhooks_noshow_logic.sql - Webhooks, no-shows, check-ins - 20260118070000_financial_reporting_expenses.sql - Gastos, reportes financieros
18 KiB
Aperture Technical Specifications
Documento maestro de especificaciones técnicas de Aperture (HQ Dashboard) Última actualización: Enero 2026
1. Arquitectura General
1.1 Stack Tecnológico
Frontend:
- Next.js 14 (App Router)
- React 18
- TypeScript 5.x
- Tailwind CSS + Radix UI
- Lucide React (icons)
- date-fns (manejo de fechas)
Backend:
- Next.js API Routes
- Supabase PostgreSQL
- Supabase Auth (roles: admin, manager, staff, customer, kiosk, artist)
- Stripe (pagos)
Infraestructura:
- Vercel (hosting)
- Supabase (database, auth, storage)
- Vercel Cron Jobs (tareas programadas)
2. Esquema de Base de Datos
2.1 Tablas Core
-- Locations (sucursales)
CREATE TABLE locations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
address TEXT NOT NULL,
phone TEXT,
timezone TEXT NOT NULL DEFAULT 'America/Mexico_City',
business_hours JSONB NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Staff (empleados)
CREATE TABLE staff (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
phone TEXT,
role TEXT NOT NULL CHECK (role IN ('admin', 'manager', 'staff', 'artist')),
location_id UUID REFERENCES locations(id),
hourly_rate DECIMAL(10,2) DEFAULT 0,
commission_rate DECIMAL(5,2) DEFAULT 0, -- Porcentaje de comisión
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Resources (recursos físicos)
CREATE TABLE resources (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL, -- Código estandarizado: mkup-1, lshs-1, pedi-1, mani-1
type TEXT NOT NULL CHECK (type IN ('mkup', 'lshs', 'pedi', 'mani')),
location_id UUID REFERENCES locations(id),
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Services (servicios)
CREATE TABLE services (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT,
base_price DECIMAL(10,2) NOT NULL,
duration_minutes INTEGER NOT NULL,
requires_dual_artist BOOLEAN DEFAULT false,
premium_fee DECIMAL(10,2) DEFAULT 0,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Customers (clientes)
CREATE TABLE customers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT UNIQUE,
phone TEXT,
first_name TEXT NOT NULL,
last_name TEXT,
tier TEXT NOT NULL DEFAULT 'free' CHECK (tier IN ('free', 'gold', 'black', 'VIP')),
weekly_invitations_used INTEGER DEFAULT 0,
referral_code TEXT UNIQUE,
referred_by UUID REFERENCES customers(id),
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Bookings (reservas)
CREATE TABLE bookings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
short_id TEXT UNIQUE NOT NULL,
customer_id UUID REFERENCES customers(id),
service_id UUID REFERENCES services(id),
location_id UUID REFERENCES locations(id),
staff_ids UUID[] NOT NULL, -- Array de staff IDs (1 o 2 para dual artist)
resource_id UUID REFERENCES resources(id),
start_time_utc TIMESTAMPTZ NOT NULL,
end_time_utc TIMESTAMPTZ NOT NULL,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'completed', 'cancelled', 'no_show')),
deposit_amount DECIMAL(10,2) DEFAULT 0,
deposit_paid BOOLEAN DEFAULT false,
total_price DECIMAL(10,2),
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Payments (pagos)
CREATE TABLE payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
booking_id UUID REFERENCES bookings(id),
amount DECIMAL(10,2) NOT NULL,
payment_method TEXT NOT NULL CHECK (payment_method IN ('cash', 'card', 'transfer', 'gift_card', 'membership', 'stripe')),
stripe_payment_intent_id TEXT,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'completed', 'refunded', 'failed')),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Payroll (nómina)
CREATE TABLE payroll (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
staff_id UUID REFERENCES staff(id),
period_start DATE NOT NULL,
period_end DATE NOT NULL,
base_salary DECIMAL(10,2) DEFAULT 0,
commission_total DECIMAL(10,2) DEFAULT 0,
tips_total DECIMAL(10,2) DEFAULT 0,
total_payment DECIMAL(10,2) NOT NULL,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'paid', 'cancelled')),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Audit Logs (auditoría)
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
entity_type TEXT NOT NULL,
entity_id UUID,
action TEXT NOT NULL,
old_values JSONB,
new_values JSONB,
performed_by UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
3. APIs Principales
3.1 Dashboard Stats
Endpoint: GET /api/aperture/stats
Response:
{
success: true,
stats: {
totalBookings: number, // Reservas del mes actual
totalRevenue: number, // Revenue del mes (servicios completados)
completedToday: number, // Citas completadas hoy
upcomingToday: number // Citas pendientes hoy
}
}
Business Rules:
- Month calculations: first day to last day of current month (UTC)
- Today calculations: 00:00 to 23:59:59.999 local timezone converted to UTC
- Revenue only includes
status = 'completed'bookings
3.2 Dashboard Data
Endpoint: GET /api/aperture/dashboard
Response:
{
success: true,
data: {
customers: {
total: number,
newToday: number,
newMonth: number
},
topPerformers: Array<{
id: string,
name: string,
bookingsCompleted: number,
revenueGenerated: number
}>,
activityFeed: Array<{
id: string,
type: 'booking' | 'payment' | 'staff' | 'system',
description: string,
timestamp: string,
metadata?: any
}>
}
}
3.3 Calendar API
Endpoint: GET /api/aperture/calendar
Query Params:
date: YYYY-MM-DD (default: today)location_id: UUID (optional, filter by location)staff_ids: UUID[] (optional, filter by staff)
Response:
{
success: true,
data: {
date: string,
slots: Array<{
time: string, // HH:mm format
bookings: Array<{
id: string,
short_id: string,
customer_name: string,
service_name: string,
staff_ids: string[],
staff_names: string[],
resource_id: string,
status: string,
duration: number,
requires_dual_artist: boolean,
start_time: string,
end_time: string,
notes?: string
}>
}>
},
staff: Array<{
id: string,
name: string,
role: string,
bookings_count: number
}>
}
3.4 Reschedule Booking
Endpoint: POST /api/aperture/bookings/[id]/reschedule
Request:
{
new_start_time_utc: string, // ISO 8601 timestamp
new_resource_id?: string // Optional new resource
}
Response:
{
success: boolean,
message?: string,
conflict?: {
type: 'staff' | 'resource',
message: string,
details: any
}
}
Validation:
- Check staff availability for new time
- Check resource availability for new time
- Verify no conflicts with existing bookings
- Update booking if no conflicts
3.5 Staff Management
CRUD Endpoints:
GET /api/aperture/staff- List all staffGET /api/aperture/staff/[id]- Get single staffPOST /api/aperture/staff- Create staffPUT /api/aperture/staff/[id]- Update staffDELETE /api/aperture/staff/[id]- Delete staff
Staff Object:
{
id: string,
first_name: string,
last_name: string,
email: string,
phone?: string,
role: 'admin' | 'manager' | 'staff' | 'artist',
location_id?: string,
hourly_rate: number,
commission_rate: number,
is_active: boolean,
business_hours?: {
monday: { start: string, end: string, is_off: boolean },
tuesday: { start: string, end: string, is_off: boolean },
// ... other days
}
}
3.6 Payroll Calculation
Endpoint: GET /api/aperture/payroll
Query Params:
period_start: YYYY-MM-DDperiod_end: YYYY-MM-DDstaff_id: UUID (optional)
Response:
{
success: true,
data: {
staff_payroll: Array<{
staff_id: string,
staff_name: string,
base_salary: number, // hourly_rate * hours_worked
commission_total: number, // revenue * commission_rate
tips_total: number, // Sum of tips
total_payment: number, // Sum of above
bookings_count: number,
hours_worked: number
}>,
summary: {
total_payroll: number,
total_bookings: number,
period: {
start: string,
end: string
}
}
}
}
Calculation Logic:
base_salary = hourly_rate * sum(booking duration / 60)
commission_total = total_revenue * (commission_rate / 100)
tips_total = sum(tips from completed bookings)
total_payment = base_salary + commission_total + tips_total
3.7 POS (Point of Sale)
Endpoint: POST /api/aperture/pos
Request:
{
items: Array<{
type: 'service' | 'product',
id: string,
name: string,
price: number,
quantity: number
}>,
payments: Array<{
method: 'cash' | 'card' | 'transfer' | 'gift_card' | 'membership',
amount: number,
stripe_payment_intent_id?: string
}>,
customer_id?: string,
booking_id?: string,
notes?: string
}
Response:
{
success: boolean,
transaction_id: string,
total_amount: number,
change?: number, // For cash payments
receipt_url?: string
}
3.8 Close Day
Endpoint: POST /api/aperture/pos/close-day
Request:
{
date: string, // YYYY-MM-DD
location_id?: string
}
Response:
{
success: true,
summary: {
date: string,
location_id?: string,
total_sales: number,
payment_breakdown: {
cash: number,
card: number,
transfer: number,
gift_card: number,
membership: number,
stripe: number
},
transaction_count: number,
refunds: number,
discrepancies: Array<{
type: string,
expected: number,
actual: number,
difference: number
}>
},
pdf_url: string
}
4. Horas Trabajadas (Automático desde Bookings)
4.1 Cálculo Automático
Las horas trabajadas por staff se calculan automáticamente desde bookings completados:
async function getStaffWorkHours(staffId: string, periodStart: Date, periodEnd: Date) {
const { data: bookings } = await supabase
.from('bookings')
.select('start_time_utc, end_time_utc')
.contains('staff_ids', [staffId])
.eq('status', 'completed')
.gte('start_time_utc', periodStart.toISOString())
.lte('start_time_utc', periodEnd.toISOString());
const totalMinutes = bookings.reduce((sum, booking) => {
const start = new Date(booking.start_time_utc);
const end = new Date(booking.end_time_utc);
return sum + (end.getTime() - start.getTime()) / 60000;
}, 0);
return totalMinutes / 60; // Return hours
}
4.2 Integración con Nómina
El cálculo de nómina utiliza estas horas automáticamente:
base_salary = staff.hourly_rate * work_hours
commission = total_revenue * (staff.commission_rate / 100)
5. POS System Specifications
5.1 Características Principales
Carrito de Compra:
- Soporte para múltiples productos/servicios
- Cantidad por item
- Descuentos aplicables
- Subtotal, taxes (si aplica), total
Métodos de Pago:
- Efectivo (con cálculo de cambio)
- Tarjeta (Stripe)
- Transferencia bancaria
- Gift Cards
- Membresías (créditos del cliente)
- Pagos mixtos (combinar múltiples métodos)
Múltiples Cajeros:
- Each staff can open a POS session
- Track cashier per transaction
- Close day per cashier or per location
5.2 Flujo de Cierre de Caja
- Solicitar fecha y location_id
- Calcular total ventas del día
- Breakdown por método de pago
- Verificar conciliación (esperado vs real)
- Generar PDF reporte
- Marcar day como "closed" (opcional flag)
6. Webhooks Stripe
6.1 Endpoints
Endpoint: POST /api/webhooks/stripe
Headers:
Stripe-Signature: Signature verification
Events:
payment_intent.succeeded: Payment completedpayment_intent.payment_failed: Payment failedcharge.refunded: Refund processed
6.2 payment_intent.succeeded
Actions:
- Extract metadata (booking details)
- Verify booking exists
- Update
paymentstable with completed status - Update booking
deposit_paid = true - Create audit log entry
- Send confirmation email/WhatsApp (si configurado)
6.3 payment_intent.payment_failed
Actions:
- Update
paymentstable with failed status - Send notification to customer
- Log failure in audit logs
- Optionally cancel booking or mark as pending
6.4 charge.refunded
Actions:
- Update
paymentstable with refunded status - Send refund confirmation to customer
- Log refund in audit logs
- Update booking status if applicable
7. No-Show Logic
7.1 Ventana de Cancelación
Regla: 12 horas antes de la cita (UTC)
7.2 Detección de No-Show
async function detectNoShows() {
const now = new Date();
const windowStart = new Date(now.getTime() - 12 * 60 * 60 * 1000); // 12h ago
const { data: noShows } = await supabase
.from('bookings')
.select('*')
.eq('status', 'confirmed')
.lte('start_time_utc', windowStart.toISOString());
for (const booking of noShows) {
// Check if customer showed up
const { data: checkIn } = await supabase
.from('check_ins')
.select('*')
.eq('booking_id', booking.id)
.single();
if (!checkIn) {
// Mark as no-show
await markAsNoShow(booking.id);
}
}
}
7.3 Penalización Automática
Actions:
- Mark booking status as
no_show - Retain deposit (do not refund)
- Send notification to customer
- Log action in audit_logs
- Track no-show count per customer (for future restrictions)
7.4 Override Admin
Admin puede marcar un no-show como "exonerated" (perdonado):
- Status remains
no_showbut with flagpenalty_waived = true - Refund deposit if appropriate
- Log admin override in audit logs
8. Seguridad y Permisos
8.1 RLS Policies
Admin:
- Full access to all tables
- Can override no-show penalties
- Can view all financial data
Manager:
- Access to location data only
- Can manage staff and bookings
- View financial reports for location
Staff/Artist:
- View own bookings and schedule
- Cannot view customer PII (email, phone)
- Cannot modify financial data
Kiosk:
- View only availability data
- Can create bookings with validated data
- No access to PII
8.2 API Authentication
Admin/Manager/Staff:
- Require valid Supabase session
- Check user role
- Filter by location for managers
Public:
- Use anon key
- Only public endpoints (availability, services, locations)
Cron Jobs:
- Require CRON_SECRET header
- Service role key required
9. Performance Considerations
9.1 Database Indexes
-- Critical indexes
CREATE INDEX idx_bookings_customer ON bookings(customer_id);
CREATE INDEX idx_bookings_staff ON bookings USING GIN(staff_ids);
CREATE INDEX idx_bookings_status_time ON bookings(status, start_time_utc);
CREATE INDEX idx_payments_booking ON payments(booking_id);
CREATE INDEX idx_payments_status ON payments(status);
CREATE INDEX idx_audit_logs_entity ON audit_logs(entity_type, entity_id);
9.2 N+1 Prevention
Use explicit joins for related data:
// BAD - N+1 queries
const bookings = await supabase.from('bookings').select('*');
for (const booking of bookings) {
const customer = await supabase.from('customers').select('*').eq('id', booking.customer_id);
}
// GOOD - Single query
const bookings = await supabase
.from('bookings')
.select(`
*,
customer:customers(*),
service:services(*),
location:locations(*)
`);
10. Testing Strategy
10.1 Unit Tests
- Generador de Short ID (collision detection)
- Cálculo de depósitos (200 vs 50% rule)
- Cálculo de nómina (salario base + comisiones + propinas)
- Disponibilidad de staff (horarios + calendar events)
10.2 Integration Tests
- API endpoints (GET, POST, PUT, DELETE)
- Stripe webhooks
- Cron jobs (reset invitations)
- No-show detection
10.3 E2E Tests
- Booking flow completo (customer → kiosk → staff)
- POS flow (items → payment → receipt)
- Dashboard navigation y visualización
- Calendar drag & drop
11. Deployment
11.1 Environment Variables
# Supabase
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
# Stripe
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
# Cron
CRON_SECRET=
# Email/WhatsApp (future)
RESEND_API_KEY=
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
11.2 Cron Jobs
# vercel.json
{
"crons": [
{
"path": "/api/cron/reset-invitations",
"schedule": "0 0 * * 1" # Monday 00:00 UTC
},
{
"path": "/api/cron/detect-no-shows",
"schedule": "0 */2 * * *" # Every 2 hours
}
]
}
12. Futuras Mejoras
12.1 Short Term (Q1 2026)
- Implementar The Vault (storage de fotos privadas)
- Implementar notificaciones WhatsApp
- Implementar recibos digitales con PDF
- Landing page Believers pública
12.2 Medium Term (Q2 2026)
- Google Calendar Sync bidireccional
- Sistema de lealtad con puntos
- Campañas de marketing masivas
- Precios dinámicos inteligentes
12.3 Long Term (Q3-Q4 2026)
- Sistema de passes digitales
- Móvil app para clientes
- Analytics avanzados con ML
- Integración con POS hardware