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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user