mirror of
https://github.com/marcogll/AnchorOS.git
synced 2026-03-15 19:24:32 +00:00
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
256 lines
8.7 KiB
PL/PgSQL
256 lines
8.7 KiB
PL/PgSQL
-- ============================================
|
|
-- 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;
|