mirror of
https://github.com/marcogll/AnchorOS.git
synced 2026-03-15 18:24:31 +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:
255
supabase/migrations/20260118050000_clients_loyalty_system.sql
Normal file
255
supabase/migrations/20260118050000_clients_loyalty_system.sql
Normal file
@@ -0,0 +1,255 @@
|
||||
-- ============================================
|
||||
-- FASE 5 - CLIENTS AND LOYALTY SYSTEM
|
||||
-- Date: 20260118
|
||||
-- Description: Add customer notes, photo gallery, loyalty points, and membership plans
|
||||
-- ============================================
|
||||
|
||||
-- Add customer notes and technical information
|
||||
ALTER TABLE customers ADD COLUMN IF NOT EXISTS technical_notes TEXT;
|
||||
ALTER TABLE customers ADD COLUMN IF NOT EXISTS preferences JSONB DEFAULT '{}'::jsonb;
|
||||
ALTER TABLE customers ADD COLUMN IF NOT EXISTS loyalty_points INTEGER DEFAULT 0;
|
||||
ALTER TABLE customers ADD COLUMN IF NOT EXISTS loyalty_points_expiry_date DATE;
|
||||
ALTER TABLE customers ADD COLUMN IF NOT EXISTS no_show_count INTEGER DEFAULT 0;
|
||||
ALTER TABLE customers ADD COLUMN IF NOT EXISTS last_no_show_date DATE;
|
||||
|
||||
-- Create customer photos table (for VIP/Black/Gold only)
|
||||
CREATE TABLE IF NOT EXISTS customer_photos (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
||||
storage_path TEXT NOT NULL,
|
||||
description TEXT,
|
||||
taken_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
created_by UUID REFERENCES auth.users(id),
|
||||
is_active BOOLEAN DEFAULT true
|
||||
);
|
||||
|
||||
-- Create index for photos lookup
|
||||
CREATE INDEX IF NOT EXISTS idx_customer_photos_customer ON customer_photos(customer_id);
|
||||
|
||||
-- Create loyalty transactions table
|
||||
CREATE TABLE IF NOT EXISTS loyalty_transactions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
||||
points INTEGER NOT NULL,
|
||||
transaction_type TEXT NOT NULL CHECK (transaction_type IN ('earned', 'redeemed', 'expired', 'admin_adjustment')),
|
||||
description TEXT,
|
||||
reference_type TEXT,
|
||||
reference_id UUID,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
created_by UUID REFERENCES auth.users(id)
|
||||
);
|
||||
|
||||
-- Create index for loyalty lookup
|
||||
CREATE INDEX IF NOT EXISTS idx_loyalty_transactions_customer ON loyalty_transactions(customer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_loyalty_transactions_created ON loyalty_transactions(created_at DESC);
|
||||
|
||||
-- Create membership plans table
|
||||
CREATE TABLE IF NOT EXISTS membership_plans (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
tier TEXT NOT NULL CHECK (tier IN ('gold', 'black', 'VIP')),
|
||||
monthly_credits INTEGER DEFAULT 0,
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
benefits JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create customer subscriptions table
|
||||
CREATE TABLE IF NOT EXISTS customer_subscriptions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
|
||||
membership_plan_id UUID NOT NULL REFERENCES membership_plans(id),
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE,
|
||||
auto_renew BOOLEAN DEFAULT false,
|
||||
credits_remaining INTEGER DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'expired', 'cancelled', 'paused')),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
UNIQUE(customer_id, status)
|
||||
);
|
||||
|
||||
-- Create index for subscriptions
|
||||
CREATE INDEX IF NOT EXISTS idx_customer_subscriptions_customer ON customer_subscriptions(customer_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_customer_subscriptions_status ON customer_subscriptions(status);
|
||||
|
||||
-- Insert default membership plans
|
||||
INSERT INTO membership_plans (name, tier, monthly_credits, price, benefits) VALUES
|
||||
('Gold Membership', 'gold', 5, 499.00, '{
|
||||
"weekly_invitations": 5,
|
||||
"priority_booking": false,
|
||||
"exclusive_services": [],
|
||||
"discount_percentage": 5,
|
||||
"photo_gallery": true
|
||||
}'::jsonb),
|
||||
('Black Membership', 'black', 10, 999.00, '{
|
||||
"weekly_invitations": 10,
|
||||
"priority_booking": true,
|
||||
"exclusive_services": ["spa_day", "premium_manicure"],
|
||||
"discount_percentage": 10,
|
||||
"photo_gallery": true,
|
||||
"priority_support": true
|
||||
}'::jsonb),
|
||||
('VIP Membership', 'VIP', 15, 1999.00, '{
|
||||
"weekly_invitations": 15,
|
||||
"priority_booking": true,
|
||||
"exclusive_services": ["spa_day", "premium_manicure", "exclusive_hair_treatment"],
|
||||
"discount_percentage": 20,
|
||||
"photo_gallery": true,
|
||||
"priority_support": true,
|
||||
"personal_stylist": true,
|
||||
"private_events": true
|
||||
}'::jsonb)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- RLS Policies for customer photos
|
||||
ALTER TABLE customer_photos ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "Photos can be viewed by admins, managers, and customer owner"
|
||||
ON customer_photos FOR SELECT
|
||||
USING (
|
||||
(SELECT EXISTS (
|
||||
SELECT 1 FROM auth.users
|
||||
WHERE auth.users.id = auth.uid()
|
||||
AND auth.users.raw_user_meta_data->>'role' IN ('admin', 'manager')
|
||||
)) OR customer_id = (SELECT id FROM customers WHERE user_id = auth.uid())
|
||||
);
|
||||
|
||||
CREATE POLICY "Photos can be created by admins, managers, and assigned staff"
|
||||
ON customer_photos FOR INSERT
|
||||
WITH CHECK (
|
||||
(SELECT EXISTS (
|
||||
SELECT 1 FROM auth.users
|
||||
WHERE auth.users.id = auth.uid()
|
||||
AND auth.users.raw_user_meta_data->>'role' IN ('admin', 'manager', 'staff', 'artist')
|
||||
))
|
||||
);
|
||||
|
||||
CREATE POLICY "Photos can be deleted by admins and managers only"
|
||||
ON customer_photos FOR DELETE
|
||||
USING (
|
||||
(SELECT EXISTS (
|
||||
SELECT 1 FROM auth.users
|
||||
WHERE auth.users.id = auth.uid()
|
||||
AND auth.users.raw_user_meta_data->>'role' IN ('admin', 'manager')
|
||||
))
|
||||
);
|
||||
|
||||
-- RLS Policies for loyalty transactions
|
||||
ALTER TABLE loyalty_transactions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "Loyalty transactions visible to admins, managers, and customer owner"
|
||||
ON loyalty_transactions FOR SELECT
|
||||
USING (
|
||||
(SELECT EXISTS (
|
||||
SELECT 1 FROM auth.users
|
||||
WHERE auth.users.id = auth.uid()
|
||||
AND auth.users.raw_user_meta_data->>'role' IN ('admin', 'manager')
|
||||
)) OR customer_id = (SELECT id FROM customers WHERE user_id = auth.uid())
|
||||
);
|
||||
|
||||
-- Function to add loyalty points
|
||||
CREATE OR REPLACE FUNCTION add_loyalty_points(
|
||||
p_customer_id UUID,
|
||||
p_points INTEGER,
|
||||
p_transaction_type TEXT DEFAULT 'earned',
|
||||
p_description TEXT,
|
||||
p_reference_type TEXT DEFAULT NULL,
|
||||
p_reference_id UUID DEFAULT NULL
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_transaction_id UUID;
|
||||
v_points_expiry_date DATE;
|
||||
BEGIN
|
||||
-- Validate customer exists
|
||||
IF NOT EXISTS (SELECT 1 FROM customers WHERE id = p_customer_id) THEN
|
||||
RAISE EXCEPTION 'Customer not found';
|
||||
END IF;
|
||||
|
||||
-- Calculate expiry date (6 months from now for earned points)
|
||||
IF p_transaction_type = 'earned' THEN
|
||||
v_points_expiry_date := (CURRENT_DATE + INTERVAL '6 months');
|
||||
END IF;
|
||||
|
||||
-- Create transaction
|
||||
INSERT INTO loyalty_transactions (
|
||||
customer_id,
|
||||
points,
|
||||
transaction_type,
|
||||
description,
|
||||
reference_type,
|
||||
reference_id,
|
||||
created_by
|
||||
) VALUES (
|
||||
p_customer_id,
|
||||
p_points,
|
||||
p_transaction_type,
|
||||
p_description,
|
||||
p_reference_type,
|
||||
p_reference_id,
|
||||
auth.uid()
|
||||
) RETURNING id INTO v_transaction_id;
|
||||
|
||||
-- Update customer points balance
|
||||
UPDATE customers
|
||||
SET
|
||||
loyalty_points = loyalty_points + p_points,
|
||||
loyalty_points_expiry_date = v_points_expiry_date
|
||||
WHERE id = p_customer_id;
|
||||
|
||||
-- Log to audit
|
||||
INSERT INTO audit_logs (
|
||||
entity_type,
|
||||
entity_id,
|
||||
action,
|
||||
new_values,
|
||||
performed_by
|
||||
) VALUES (
|
||||
'customer',
|
||||
p_customer_id,
|
||||
'loyalty_points_updated',
|
||||
jsonb_build_object(
|
||||
'points_change', p_points,
|
||||
'new_balance', (SELECT loyalty_points FROM customers WHERE id = p_customer_id)
|
||||
),
|
||||
auth.uid()
|
||||
);
|
||||
|
||||
RETURN v_transaction_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Function to check if customer can access photo gallery
|
||||
CREATE OR REPLACE FUNCTION can_access_photo_gallery(p_customer_id UUID)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN EXISTS (
|
||||
SELECT 1 FROM customers
|
||||
WHERE id = p_customer_id
|
||||
AND tier IN ('gold', 'black', 'VIP')
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Function to get customer loyalty summary
|
||||
CREATE OR REPLACE FUNCTION get_customer_loyalty_summary(p_customer_id UUID)
|
||||
RETURNS JSONB AS $$
|
||||
DECLARE
|
||||
v_summary JSONB;
|
||||
BEGIN
|
||||
SELECT jsonb_build_object(
|
||||
'points', COALESCE(loyalty_points, 0),
|
||||
'expiry_date', loyalty_points_expiry_date,
|
||||
'no_show_count', COALESCE(no_show_count, 0),
|
||||
'last_no_show', last_no_show_date,
|
||||
'transactions_earned', COALESCE((SELECT COUNT(*) FROM loyalty_transactions WHERE customer_id = p_customer_id AND transaction_type = 'earned'), 0),
|
||||
'transactions_redeemed', COALESCE((SELECT COUNT(*) FROM loyalty_transactions WHERE customer_id = p_customer_id AND transaction_type = 'redeemed'), 0)
|
||||
) INTO v_summary
|
||||
FROM customers
|
||||
WHERE id = p_customer_id;
|
||||
|
||||
RETURN v_summary;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
Reference in New Issue
Block a user