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:
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
type CustomerRelationship struct {
|
||||
TotalSpend float64 `json:"totalSpend"`
|
||||
TotalSaved float64 `json:"totalSaved"`
|
||||
TotalTips float64 `json:"totalTips"`
|
||||
TotalVisits int `json:"totalVisits"`
|
||||
CustomerFor string `json:"customerFor"`
|
||||
@@ -62,11 +63,25 @@ func GetCustomerRelationshipHandler(w http.ResponseWriter, r *http.Request) {
|
||||
WHERE b.user_id = $1
|
||||
AND p.status = 'completed'
|
||||
AND p.payment_type IN ('full', 'partial', 'balance', 'deposit')
|
||||
AND p.payment_method != 'discount'
|
||||
`, userID).Scan(&result.TotalSpend)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
log.Printf("Failed to get total spend for user %s: %v", userID, err)
|
||||
}
|
||||
|
||||
err = db.DB.QueryRow(r.Context(), `
|
||||
SELECT COALESCE(SUM(p.amount), 0)
|
||||
FROM payments p
|
||||
JOIN bookings b ON p.booking_id = b.id
|
||||
WHERE b.user_id = $1
|
||||
AND p.status = 'completed'
|
||||
AND p.payment_type IN ('full', 'partial', 'balance', 'deposit')
|
||||
AND p.payment_method = 'discount'
|
||||
`, userID).Scan(&result.TotalSaved)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
log.Printf("Failed to get total saved for user %s: %v", userID, err)
|
||||
}
|
||||
|
||||
err = db.DB.QueryRow(r.Context(), `
|
||||
SELECT COALESCE(SUM(p.amount), 0)
|
||||
FROM payments p
|
||||
|
||||
@@ -318,3 +318,56 @@ func createPayment(t *testing.T, pool *pgxpool.Pool, bookingID, paymentType stri
|
||||
t.Fatalf("failed to create payment: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomerRelationship_WithDiscounts(t *testing.T) {
|
||||
resetTestData(t)
|
||||
|
||||
userID, err := fixtures.CreateTestUser(db.DB)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create test user: %v", err)
|
||||
}
|
||||
|
||||
svcID, err := createService(db.DB, "Test Service", 100.00)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create service: %v", err)
|
||||
}
|
||||
|
||||
bookingID := createCompletedBooking(t, db.DB, userID, svcID, "2024-03-01 10:00:00+00", 100.00)
|
||||
|
||||
// User pays £80.00 cash/card and receives £20.00 discount
|
||||
createPayment(t, db.DB, bookingID, "balance", 80.00)
|
||||
createDiscountPayment(t, db.DB, bookingID, 20.00)
|
||||
|
||||
req := newAdminRequest("GET", "/api/admin/users/"+userID+"/relationship", userID)
|
||||
rr := httptest.NewRecorder()
|
||||
GetCustomerRelationshipHandler(rr, req)
|
||||
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d. body: %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
|
||||
var result CustomerRelationship
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &result); err != nil {
|
||||
t.Fatalf("failed to unmarshal response: %v", err)
|
||||
}
|
||||
|
||||
if result.TotalSpend != 80.00 {
|
||||
t.Errorf("expected total spend 80.00, got %.2f", result.TotalSpend)
|
||||
}
|
||||
|
||||
if result.TotalSaved != 20.00 {
|
||||
t.Errorf("expected total saved 20.00, got %.2f", result.TotalSaved)
|
||||
}
|
||||
}
|
||||
|
||||
func createDiscountPayment(t *testing.T, pool *pgxpool.Pool, bookingID string, amount float64) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
_, err := pool.Exec(ctx, `
|
||||
INSERT INTO payments (booking_id, payment_type, payment_method, amount, status)
|
||||
VALUES ($1, 'partial', 'discount', $2, 'completed')
|
||||
`, bookingID, amount)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create discount payment: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user