feat(loyalty-discount): implement loyalty and discount system
- Add discount campaign management and validation logic - Update booking handlers with discount application flow - Add customer relationship endpoints for loyalty tracking - Update frontend modals (booking, approval, payment, reschedule) - Add DiscountsManagement and loyalty reference documentation - Update dev scripts and database init for discount tables - Clean up completed plan files
This commit is contained in:
+123
-1
@@ -569,7 +569,7 @@ D=$(open_day_past "$(TZ=Europe/London date -d "today -23 days" +%Y-%m-%d)")
|
||||
if create_admin_booking "$GRACE_ID" "$(format_london_time "$D" "$SLOT_A")" "[\"$(get_svc 7)\"]" "" "Grace - Luxury Gel Manicure"; then count_past=$((count_past+1)); fi
|
||||
|
||||
# Primary test user — variety of past bookings
|
||||
for day_offset in 2 4 8 11 15; do
|
||||
for day_offset in 2 4 8 11 15 18 20 22 24 26 28; do
|
||||
D=$(open_day_past "$(TZ=Europe/London date -d "today -$day_offset days" +%Y-%m-%d)")
|
||||
SVC_IDX=$(( (day_offset % 4) ))
|
||||
if create_admin_booking "$USER_USER_ID" "$(format_london_time "$D" "$SLOT_C")" "[\"$(get_svc $SVC_IDX)\"]" "" "User - Past booking $D"; then count_past=$((count_past+1)); fi
|
||||
@@ -887,6 +887,127 @@ docker exec postgres psql -U myuser -d mydb -c \
|
||||
completed_count=$(docker exec postgres psql -U myuser -d mydb -tAc \
|
||||
"SELECT COUNT(*) FROM bookings WHERE status = 'completed'" 2>/dev/null)
|
||||
|
||||
# --- Seeding campaigns and stacked discounts for user@example.com ---
|
||||
echo -e "\n${C_BLUE}🎟️ Seeding Campaigns & Loyalty Stacked Discounts...${C_RESET}"
|
||||
docker exec -i postgres psql -U myuser -d mydb << 'CAMPAIGN_SQL' > /dev/null 2>&1
|
||||
DO $$
|
||||
DECLARE
|
||||
u_id CHAR(12);
|
||||
past_camp_id CHAR(12);
|
||||
active_camp_id CHAR(12);
|
||||
milestone_camp_id CHAR(12);
|
||||
b_rec RECORD;
|
||||
b_idx INT := 1;
|
||||
red_id CHAR(12);
|
||||
b_total NUMERIC(10,2);
|
||||
loyalty_disc NUMERIC(10,2);
|
||||
camp_disc NUMERIC(10,2);
|
||||
balance NUMERIC(10,2);
|
||||
BEGIN
|
||||
-- 1. Get user id
|
||||
SELECT id INTO u_id FROM users WHERE email = 'user@example.com';
|
||||
|
||||
-- 2. Insert 3 campaigns
|
||||
INSERT INTO discount_campaigns (name, description, campaign_type, discount_percent, scope, start_date, end_date, status, max_redemptions)
|
||||
VALUES ('Demo Sale', 'Get 10% off visits because teehee', 'time_based', 10.00, 'all_bookings', NOW() - INTERVAL '30 days', NOW() - INTERVAL '10 days', 'active', 500)
|
||||
RETURNING id INTO past_camp_id;
|
||||
|
||||
INSERT INTO discount_campaigns (name, description, campaign_type, discount_percent, scope, start_date, end_date, status, max_redemptions)
|
||||
VALUES ('Summer Sale', 'Enjoy 15% off all summer treatments', 'time_based', 15.00, 'all_bookings', NOW() - INTERVAL '1 day', NOW() + INTERVAL '30 days', 'active', 1000)
|
||||
RETURNING id INTO active_camp_id;
|
||||
|
||||
INSERT INTO discount_campaigns (name, description, campaign_type, discount_percent, scope, status, milestone_type, milestone_value)
|
||||
VALUES ('10th Visit Celebration', 'Receive 20% off on your 10th milestone visit', 'milestone', 20.00, 'all_bookings', 'active', 'per_user_booking_count', 10)
|
||||
RETURNING id INTO milestone_camp_id;
|
||||
|
||||
-- 3. Loop through completed bookings of user@example.com in chronological order
|
||||
FOR b_rec IN
|
||||
SELECT b.id, b.start_time,
|
||||
COALESCE(SUM(COALESCE(bs.override_price, s.price)), 0) as total
|
||||
FROM bookings b
|
||||
JOIN booking_services bs ON bs.booking_id = b.id
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE b.user_id = u_id AND b.status = 'completed'
|
||||
GROUP BY b.id, b.start_time
|
||||
ORDER BY b.start_time ASC
|
||||
LOOP
|
||||
b_total := b_rec.total;
|
||||
|
||||
-- Booking 6 (chronologically Day 18) reaches 10 stamps (stamps go 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10)
|
||||
IF b_idx = 6 THEN
|
||||
INSERT INTO loyalty_redemptions (user_id, stamps_redeemed, status, redeemed_at)
|
||||
VALUES (u_id, 10, 'pending', b_rec.start_time)
|
||||
RETURNING id INTO red_id;
|
||||
END IF;
|
||||
|
||||
-- Booking 7 (chronologically Day 15) consumes the redemption AND gets past campaign (since Day 15 is within past campaign dates)
|
||||
IF b_idx = 7 THEN
|
||||
-- Get the pending redemption
|
||||
SELECT id INTO red_id FROM loyalty_redemptions WHERE user_id = u_id AND status = 'pending' ORDER BY redeemed_at ASC LIMIT 1;
|
||||
|
||||
IF red_id IS NOT NULL THEN
|
||||
loyalty_disc := ROUND(b_total * 0.10, 2);
|
||||
camp_disc := ROUND(b_total * 0.10, 2);
|
||||
balance := ROUND(b_total - loyalty_disc - camp_disc, 2);
|
||||
|
||||
-- Create loyalty discount row
|
||||
INSERT INTO booking_discounts (booking_id, user_id, discount_source, source_id, discount_percent, original_total, discount_amount, applied_at)
|
||||
VALUES (b_rec.id, u_id, 'loyalty', red_id, 10.00, b_total, loyalty_disc, b_rec.start_time);
|
||||
|
||||
-- Create loyalty payment row
|
||||
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_at)
|
||||
VALUES (b_rec.id, 'partial', 'discount', loyalty_disc, 'completed', b_rec.start_time);
|
||||
|
||||
-- Update loyalty redemption
|
||||
UPDATE loyalty_redemptions
|
||||
SET status = 'applied', applied_to_booking_id = b_rec.id, applied_at = b_rec.start_time
|
||||
WHERE id = red_id;
|
||||
|
||||
-- Create campaign discount row
|
||||
INSERT INTO booking_discounts (booking_id, user_id, discount_source, source_id, campaign_type, discount_percent, original_total, discount_amount, applied_at)
|
||||
VALUES (b_rec.id, u_id, 'campaign', past_camp_id, 'time_based', 10.00, b_total, camp_disc, b_rec.start_time);
|
||||
|
||||
-- Create campaign payment row
|
||||
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_at)
|
||||
VALUES (b_rec.id, 'partial', 'discount', camp_disc, 'completed', b_rec.start_time);
|
||||
|
||||
-- Increment campaign times_redeemed
|
||||
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = past_camp_id;
|
||||
|
||||
-- Create standard payment balance row
|
||||
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_at)
|
||||
VALUES (b_rec.id, 'balance', 'in_person_card', balance, 'completed', b_rec.start_time);
|
||||
END IF;
|
||||
|
||||
-- Other bookings during past campaign (Bookings 1-5, and 8: Day 28, 26, 24, 22, 20, 11) get campaign discount only
|
||||
ELSIF b_rec.start_time >= (NOW() - INTERVAL '30 days') AND b_rec.start_time <= (NOW() - INTERVAL '10 days') THEN
|
||||
camp_disc := ROUND(b_total * 0.10, 2);
|
||||
balance := ROUND(b_total - camp_disc, 2);
|
||||
|
||||
-- Create campaign discount row
|
||||
INSERT INTO booking_discounts (booking_id, user_id, discount_source, source_id, campaign_type, discount_percent, original_total, discount_amount, applied_at)
|
||||
VALUES (b_rec.id, u_id, 'campaign', past_camp_id, 'time_based', 10.00, b_total, camp_disc, b_rec.start_time);
|
||||
|
||||
-- Create campaign payment row
|
||||
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_at)
|
||||
VALUES (b_rec.id, 'partial', 'discount', camp_disc, 'completed', b_rec.start_time);
|
||||
|
||||
-- Increment campaign times_redeemed
|
||||
UPDATE discount_campaigns SET times_redeemed = times_redeemed + 1 WHERE id = past_camp_id;
|
||||
|
||||
-- Create standard payment balance row
|
||||
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_at)
|
||||
VALUES (b_rec.id, 'balance', 'in_person_card', balance, 'completed', b_rec.start_time);
|
||||
END IF;
|
||||
|
||||
b_idx := b_idx + 1;
|
||||
END LOOP;
|
||||
|
||||
-- Update final stamps to 5
|
||||
UPDATE users SET loyalty_stamps = 5 WHERE id = u_id;
|
||||
END $$;
|
||||
CAMPAIGN_SQL
|
||||
|
||||
# Pure SQL payment seeding — randomized per booking, no bash loops
|
||||
docker exec -i postgres psql -U myuser -d mydb << 'PAYMENT_SQL' > /dev/null 2>&1
|
||||
DO $$
|
||||
@@ -907,6 +1028,7 @@ BEGIN
|
||||
JOIN booking_services bs ON bs.booking_id = b.id
|
||||
JOIN services s ON bs.service_id = s.id
|
||||
WHERE b.status = 'completed'
|
||||
AND NOT EXISTS (SELECT 1 FROM payments WHERE booking_id = b.id)
|
||||
GROUP BY b.id
|
||||
LOOP
|
||||
booking_total := rec.total;
|
||||
|
||||
Reference in New Issue
Block a user