mirror of
https://github.com/marcogll/AnchorOS.git
synced 2026-03-15 17:24:30 +00:00
feat: Implement FASE 5 (Clients & Loyalty) and FASE 6 (Payments & Financial)
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
This commit is contained in:
792
docs/APERATURE_SPECS.md
Normal file
792
docs/APERATURE_SPECS.md
Normal file
@@ -0,0 +1,792 @@
|
||||
# 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
|
||||
|
||||
```sql
|
||||
-- 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:**
|
||||
```typescript
|
||||
{
|
||||
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:**
|
||||
```typescript
|
||||
{
|
||||
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:**
|
||||
```typescript
|
||||
{
|
||||
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:**
|
||||
```typescript
|
||||
{
|
||||
new_start_time_utc: string, // ISO 8601 timestamp
|
||||
new_resource_id?: string // Optional new resource
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```typescript
|
||||
{
|
||||
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 staff
|
||||
- `GET /api/aperture/staff/[id]` - Get single staff
|
||||
- `POST /api/aperture/staff` - Create staff
|
||||
- `PUT /api/aperture/staff/[id]` - Update staff
|
||||
- `DELETE /api/aperture/staff/[id]` - Delete staff
|
||||
|
||||
**Staff Object:**
|
||||
```typescript
|
||||
{
|
||||
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-DD
|
||||
- `period_end`: YYYY-MM-DD
|
||||
- `staff_id`: UUID (optional)
|
||||
|
||||
**Response:**
|
||||
```typescript
|
||||
{
|
||||
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:**
|
||||
```typescript
|
||||
{
|
||||
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:**
|
||||
```typescript
|
||||
{
|
||||
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:**
|
||||
```typescript
|
||||
{
|
||||
date: string, // YYYY-MM-DD
|
||||
location_id?: string
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```typescript
|
||||
{
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
1. Solicitar fecha y location_id
|
||||
2. Calcular total ventas del día
|
||||
3. Breakdown por método de pago
|
||||
4. Verificar conciliación (esperado vs real)
|
||||
5. Generar PDF reporte
|
||||
6. 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 completed
|
||||
- `payment_intent.payment_failed`: Payment failed
|
||||
- `charge.refunded`: Refund processed
|
||||
|
||||
### 6.2 payment_intent.succeeded
|
||||
|
||||
**Actions:**
|
||||
1. Extract metadata (booking details)
|
||||
2. Verify booking exists
|
||||
3. Update `payments` table with completed status
|
||||
4. Update booking `deposit_paid = true`
|
||||
5. Create audit log entry
|
||||
6. Send confirmation email/WhatsApp (si configurado)
|
||||
|
||||
### 6.3 payment_intent.payment_failed
|
||||
|
||||
**Actions:**
|
||||
1. Update `payments` table with failed status
|
||||
2. Send notification to customer
|
||||
3. Log failure in audit logs
|
||||
4. Optionally cancel booking or mark as pending
|
||||
|
||||
### 6.4 charge.refunded
|
||||
|
||||
**Actions:**
|
||||
1. Update `payments` table with refunded status
|
||||
2. Send refund confirmation to customer
|
||||
3. Log refund in audit logs
|
||||
4. 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
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
1. Mark booking status as `no_show`
|
||||
2. Retain deposit (do not refund)
|
||||
3. Send notification to customer
|
||||
4. Log action in audit_logs
|
||||
5. 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_show` but with flag `penalty_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
|
||||
|
||||
```sql
|
||||
-- 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:
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```env
|
||||
# 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
|
||||
|
||||
```yaml
|
||||
# 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
|
||||
Reference in New Issue
Block a user