feat: loyalty/discount system with milestone campaigns, auto-redemption, and admin UI

- Database: loyalty_redemptions, discount_campaigns, booking_discounts tables
- Backend: auto-create pending redemption at 10 stamps, apply discounts at completion
- Backend: discount_eligible flag on booking creation (user + admin flows)
- Backend: campaign CRUD handlers (GET/POST/PUT/DELETE + stats)
- Backend: milestone campaigns (per-user, global, anniversary)
- Frontend: customer account page shows 'card full' status at 10 stamps
- Frontend: admin discounts page with campaign management UI
- Frontend: TypeScript types for all discount entities
- Tests: 9 integration tests covering loyalty, campaigns, milestones, edge cases
This commit is contained in:
2026-05-08 17:32:20 +01:00
parent bec4100e4d
commit 1f54d8565c
9 changed files with 2864 additions and 8 deletions
+59
View File
@@ -73,6 +73,7 @@ export interface BookingUser {
date_of_birth?: string;
account_role: string;
loyalty_stamps?: number;
pending_redemption?: boolean;
referral_code?: string;
referral_code_uses?: number;
created_at: string;
@@ -120,6 +121,7 @@ export interface Booking {
amount_paid: number;
amount_due: number;
duration_minutes: number;
discount_eligible?: boolean;
}
export interface BookingListResponse {
@@ -130,3 +132,60 @@ export interface BookingListResponse {
total_pages: number;
}
export interface LoyaltyRedemption {
id: string;
user_id: string;
stamps_redeemed: number;
status: 'pending' | 'applied' | 'expired';
applied_to_booking_id?: string;
redeemed_at: string;
applied_at?: string;
expires_at: string;
}
export type CampaignType = 'time_based' | 'milestone';
export type MilestoneType = 'per_user_booking_count' | 'global_booking_count' | 'anniversary';
export type MilestoneUnit = 'bookings' | 'months' | 'years';
export type DiscountCampaignScope = 'all_bookings' | 'first_booking_only' | 'new_customers_only';
export type DiscountCampaignStatus = 'draft' | 'active' | 'completed' | 'cancelled';
export interface DiscountCampaign {
id: string;
name: string;
description?: string;
campaign_type: CampaignType;
discount_percent: number;
scope?: DiscountCampaignScope;
start_date?: string;
end_date?: string;
milestone_type?: MilestoneType;
milestone_value?: number;
milestone_unit?: MilestoneUnit;
status: DiscountCampaignStatus;
max_redemptions?: number;
times_redeemed: number;
created_at: string;
updated_at: string;
created_by?: string;
}
export interface BookingDiscount {
id: string;
booking_id: string;
user_id: string;
discount_source: 'loyalty' | 'campaign';
source_id?: string;
campaign_type?: CampaignType;
milestone_type?: MilestoneType;
discount_percent: number;
original_total: number;
discount_amount: number;
applied_at: string;
}
export interface CampaignStats {
campaign: DiscountCampaign;
total_discount_amount: number;
booking_count: number;
}