guardia-mall/backend/src/main/java/com/zioinfo/mall/controller/CustomerLoyaltyController.java

497 lines
29 KiB
Java

package com.zioinfo.mall.controller;
import com.zioinfo.mall.ai.OllamaClient;
import com.zioinfo.mall.common.ApiResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* 고객 로열티 API — /api/mall/loyalty
* 멤버십·포인트·등급·쿠폰·추천·구독·위시리스트 관리
* 25개 엔드포인트
*/
@RestController
@RequestMapping("/api/mall/loyalty/v2")
@RequiredArgsConstructor
public class CustomerLoyaltyController {
private final OllamaClient ollama;
// ── 1. 멤버십 현황 ─────────────────────────────────────────
@GetMapping("/members/{customerId}")
public ApiResponse<?> getMembership(@PathVariable Long customerId) {
return ApiResponse.ok(Map.of(
"customerId", customerId,
"memberSince", "2024-03-15",
"tier", "GOLD",
"tierProgress", Map.of("current", 2847, "nextTier", "PLATINUM", "pointsNeeded", 153, "pct", "95%"),
"lifetimeSpend", 847.50,
"totalOrders", 11,
"activeSubscriptions", 1,
"referrals", 3,
"wishlistItems", 4,
"status", "ACTIVE"
));
}
// ── 2. 멤버십 가입 ─────────────────────────────────────────
@PostMapping("/members/{customerId}/enroll")
public ApiResponse<?> enrollMembership(@PathVariable Long customerId,
@RequestBody(required = false) Map<String, Object> body) {
String referralCode = body != null ? (String) body.getOrDefault("referralCode", "") : "";
int bonusPoints = referralCode.isEmpty() ? 0 : 200;
return ApiResponse.ok(Map.of(
"customerId", customerId,
"tier", "BRONZE",
"startingPoints", 100 + bonusPoints,
"referralBonus", bonusPoints,
"referralCode", referralCode,
"enrolledAt", LocalDateTime.now().toString(),
"welcomeOffer", "10% off your next order!",
"message", "Welcome to Montvale Florist Rewards! You've earned " + (100 + bonusPoints) + " welcome points."
));
}
// ── 3. 포인트 잔액 ─────────────────────────────────────────
@GetMapping("/points/{customerId}")
public ApiResponse<?> getPoints(@PathVariable Long customerId) {
return ApiResponse.ok(Map.of(
"customerId", customerId,
"currentPoints", 2847,
"pendingPoints", 150,
"lifetimeEarned", 4230,
"lifetimeRedeemed", 1233,
"expiringPoints", Map.of("amount", 200, "expiresBy", LocalDate.now().plusDays(30).toString()),
"pointValue", "$0.01 per point",
"redeemableValue", "$28.47"
));
}
// ── 4. 포인트 이력 ─────────────────────────────────────────
@GetMapping("/points/{customerId}/history")
public ApiResponse<?> getPointsHistory(@PathVariable Long customerId,
@RequestParam(defaultValue = "20") int limit) {
List<Map<String, Object>> history = List.of(
Map.of("date", "2026-06-15", "type", "EARN", "points", +150, "description", "Order ORD-1234 — Red Roses", "balance", 2847),
Map.of("date", "2026-06-10", "type", "EARN", "points", +80, "description", "Order ORD-1198 — Birthday Bouquet", "balance", 2697),
Map.of("date", "2026-06-05", "type", "REDEEM", "points", -500, "description", "Discount applied to ORD-1178", "balance", 2617),
Map.of("date", "2026-05-28", "type", "EARN", "points", +200, "description", "Referral bonus — Friend enrolled", "balance", 3117),
Map.of("date", "2026-05-20", "type", "EARN", "points", +110, "description", "Order ORD-1145 — Subscription delivery", "balance", 2917),
Map.of("date", "2026-05-10", "type", "BONUS", "points", +100, "description", "Birthday month bonus", "balance", 2807),
Map.of("date", "2026-04-30", "type", "EARN", "points", +95, "description", "Order ORD-1102 — Mother's Day Bouquet", "balance", 2707)
);
return ApiResponse.ok(Map.of("customerId", customerId, "history", history.subList(0, Math.min(limit, history.size())), "totalTransactions", 47));
}
// ── 5. 포인트 적립 ─────────────────────────────────────────
@PostMapping("/points/{customerId}/earn")
public ApiResponse<?> earnPoints(@PathVariable Long customerId, @RequestBody Map<String, Object> body) {
String orderId = (String) body.getOrDefault("orderId", "ORD-0000");
double orderAmount = ((Number) body.getOrDefault("orderAmount", 0.0)).doubleValue();
String source = (String) body.getOrDefault("source", "PURCHASE");
// 1 point per $1, bonus for tier
int basePoints = (int) orderAmount;
int tierBonus = (int) (basePoints * 0.25); // 25% bonus for GOLD
int totalPoints = basePoints + tierBonus;
return ApiResponse.ok(Map.of(
"customerId", customerId, "orderId", orderId,
"basePoints", basePoints, "tierBonus", tierBonus, "totalEarned", totalPoints,
"source", source, "newBalance", 2847 + totalPoints,
"message", "You earned " + totalPoints + " points (including 25% Gold tier bonus)!"
));
}
// ── 6. 포인트 사용 ─────────────────────────────────────────
@PostMapping("/points/{customerId}/redeem")
public ApiResponse<?> redeemPoints(@PathVariable Long customerId, @RequestBody Map<String, Object> body) {
int pointsToRedeem = ((Number) body.getOrDefault("points", 500)).intValue();
String orderId = (String) body.getOrDefault("orderId", "");
int currentBalance = 2847;
if (pointsToRedeem > currentBalance) {
return ApiResponse.fail("Insufficient points. Current balance: " + currentBalance);
}
if (pointsToRedeem < 100) {
return ApiResponse.fail("Minimum redemption is 100 points");
}
double discountValue = pointsToRedeem * 0.01;
return ApiResponse.ok(Map.of(
"customerId", customerId, "orderId", orderId,
"pointsRedeemed", pointsToRedeem,
"discountApplied", String.format("$%.2f", discountValue),
"remainingBalance", currentBalance - pointsToRedeem,
"message", "Redeemed " + pointsToRedeem + " points for $" + String.format("%.2f", discountValue) + " discount!"
));
}
// ── 7. 등급 목록 ───────────────────────────────────────────
@GetMapping("/tiers")
public ApiResponse<?> getTiers() {
List<Map<String, Object>> tiers = List.of(
Map.of("tier", "BRONZE", "minPoints", 0, "maxPoints", 999, "color", "#CD7F32",
"benefits", List.of("1 point per $1", "Birthday coupon 10%", "Free delivery on $75+"),
"memberCount", 1247),
Map.of("tier", "SILVER", "minPoints", 1000, "maxPoints", 1999, "color", "#C0C0C0",
"benefits", List.of("1.15 points per $1", "Birthday coupon 15%", "Free delivery on $50+", "Priority customer service"),
"memberCount", 483),
Map.of("tier", "GOLD", "minPoints", 2000, "maxPoints", 4999, "color", "#FFD700",
"benefits", List.of("1.25 points per $1", "Birthday coupon 20%", "Free delivery always", "Early access to new arrivals", "Monthly florist tips"),
"memberCount", 287),
Map.of("tier", "PLATINUM", "minPoints", 5000, "maxPoints", null, "color", "#E5E4E2",
"benefits", List.of("1.50 points per $1", "Birthday coupon 25%", "Free delivery + priority", "Personal florist consultant", "Exclusive monthly gift", "VIP event invites"),
"memberCount", 54)
);
return ApiResponse.ok(Map.of("tiers", tiers, "totalMembers", 2071));
}
// ── 8. 등급 혜택 조회 ──────────────────────────────────────
@GetMapping("/tiers/{customerId}/benefits")
public ApiResponse<?> getTierBenefits(@PathVariable Long customerId) {
return ApiResponse.ok(Map.of(
"customerId", customerId,
"currentTier", "GOLD",
"activeBenefits", List.of(
Map.of("benefit", "1.25x Points Multiplier", "status", "ACTIVE", "appliedTo", "All purchases"),
Map.of("benefit", "Free Delivery", "status", "ACTIVE", "appliedTo", "All orders"),
Map.of("benefit", "Birthday Coupon 20%", "status", "PENDING", "appliedTo", "Birthday month", "expiresOn", "2026-07-31"),
Map.of("benefit", "Early Access — New Arrivals", "status", "ACTIVE", "appliedTo", "Every Tuesday 8AM"),
Map.of("benefit", "Monthly Florist Tips Newsletter", "status", "ACTIVE", "appliedTo", "Email")
),
"nextTierBenefits", List.of(
"1.50x Points Multiplier",
"Personal Florist Consultant",
"Exclusive Monthly Gift ($15 value)"
)
));
}
// ── 9. 등급 업그레이드 ─────────────────────────────────────
@PostMapping("/tiers/{customerId}/upgrade")
@PreAuthorize("hasRole('ADMIN')")
public ApiResponse<?> upgradeTier(@PathVariable Long customerId, @RequestBody Map<String, Object> body) {
String newTier = (String) body.getOrDefault("tier", "GOLD");
String reason = (String) body.getOrDefault("reason", "Admin override");
return ApiResponse.ok(Map.of(
"customerId", customerId,
"previousTier", "SILVER",
"newTier", newTier,
"reason", reason,
"effectiveAt", LocalDateTime.now().toString(),
"notification", "Customer will receive an email about their tier upgrade with welcome benefits"
));
}
// ── 10. 사용 가능 쿠폰 ─────────────────────────────────────
@GetMapping("/coupons/available")
public ApiResponse<?> getAvailableCoupons(@RequestParam(required = false) Long customerId) {
List<Map<String, Object>> coupons = List.of(
Map.of("code", "BIRTHDAY20", "type", "PERCENT", "discount", "20%", "minOrder", 50.00,
"expiresOn", LocalDate.now().plusDays(15).toString(), "usesLeft", 1, "category", "Birthday"),
Map.of("code", "FLASH10", "type", "PERCENT", "discount", "10%", "minOrder", 30.00,
"expiresOn", LocalDate.now().plusDays(2).toString(), "usesLeft", 1, "category", "Flash Sale"),
Map.of("code", "REFER200", "type", "POINTS", "discount", "200 pts", "minOrder", 0.00,
"expiresOn", LocalDate.now().plusDays(60).toString(), "usesLeft", 1, "category", "Referral"),
Map.of("code", "FREESHIP", "type", "FREE_DELIVERY", "discount", "Free Delivery", "minOrder", 40.00,
"expiresOn", LocalDate.now().plusDays(30).toString(), "usesLeft", 1, "category", "Delivery")
);
return ApiResponse.ok(Map.of("customerId", customerId, "coupons", coupons, "totalAvailable", coupons.size()));
}
// ── 11. 쿠폰 발급 (관리자) ─────────────────────────────────
@PostMapping("/coupons/issue")
@PreAuthorize("hasRole('ADMIN')")
public ApiResponse<?> issueCoupon(@RequestBody Map<String, Object> body) {
String code = (String) body.getOrDefault("code", "MANUAL" + System.currentTimeMillis() % 10000);
String type = (String) body.getOrDefault("type", "PERCENT");
Object discount = body.getOrDefault("discount", "10%");
double minOrder = ((Number) body.getOrDefault("minOrder", 0.0)).doubleValue();
int validDays = ((Number) body.getOrDefault("validDays", 30)).intValue();
Object targetCustomer = body.getOrDefault("customerId", "ALL");
return ApiResponse.ok(Map.of(
"code", code.toUpperCase(), "type", type, "discount", discount,
"minOrder", minOrder, "validUntil", LocalDate.now().plusDays(validDays).toString(),
"targetCustomer", targetCustomer, "issuedAt", LocalDateTime.now().toString(),
"message", "Coupon issued successfully"
));
}
// ── 12. 쿠폰 유효성 검증 ───────────────────────────────────
@PostMapping("/coupons/{code}/validate")
public ApiResponse<?> validateCoupon(@PathVariable String code,
@RequestBody(required = false) Map<String, Object> body) {
double orderAmount = body != null ? ((Number) body.getOrDefault("orderAmount", 0.0)).doubleValue() : 0.0;
// Static validation logic
Map<String, Object> knownCoupons = new HashMap<>();
knownCoupons.put("BIRTHDAY20", Map.of("valid", true, "type", "PERCENT", "discount", 20, "minOrder", 50.0));
knownCoupons.put("FLASH10", Map.of("valid", true, "type", "PERCENT", "discount", 10, "minOrder", 30.0));
knownCoupons.put("FREESHIP", Map.of("valid", true, "type", "FREE_DELIVERY", "discount", 0, "minOrder", 40.0));
knownCoupons.put("EXPIRED", Map.of("valid", false, "reason", "Coupon expired"));
Object couponInfo = knownCoupons.get(code.toUpperCase());
if (couponInfo == null) {
return ApiResponse.ok(Map.of("code", code, "valid", false, "reason", "Coupon not found"));
}
@SuppressWarnings("unchecked")
Map<String, Object> info = (Map<String, Object>) couponInfo;
boolean valid = (boolean) info.get("valid");
if (!valid) return ApiResponse.ok(Map.of("code", code, "valid", false, "reason", info.get("reason")));
double minOrder = ((Number) info.get("minOrder")).doubleValue();
if (orderAmount > 0 && orderAmount < minOrder) {
return ApiResponse.ok(Map.of("code", code, "valid", false, "reason", "Minimum order $" + minOrder + " required. Current: $" + orderAmount));
}
return ApiResponse.ok(Map.of("code", code, "valid", true, "couponDetails", info));
}
// ── 13. 쿠폰 적용 ─────────────────────────────────────────
@PostMapping("/coupons/{code}/apply")
public ApiResponse<?> applyCoupon(@PathVariable String code, @RequestBody Map<String, Object> body) {
String orderId = (String) body.getOrDefault("orderId", "");
double orderAmount = ((Number) body.getOrDefault("orderAmount", 0.0)).doubleValue();
// Simple discount calculation
double discount = 0;
String discountDescription = "";
if (code.toUpperCase().equals("BIRTHDAY20") && orderAmount >= 50) {
discount = orderAmount * 0.20;
discountDescription = "20% birthday discount";
} else if (code.toUpperCase().equals("FLASH10") && orderAmount >= 30) {
discount = orderAmount * 0.10;
discountDescription = "10% flash sale discount";
} else if (code.toUpperCase().equals("FREESHIP")) {
discount = 8.99;
discountDescription = "Free delivery";
}
if (discount == 0) return ApiResponse.fail("Coupon cannot be applied to this order");
return ApiResponse.ok(Map.of(
"orderId", orderId, "code", code.toUpperCase(),
"originalAmount", orderAmount, "discountApplied", Math.round(discount * 100.0) / 100.0,
"discountDescription", discountDescription,
"finalAmount", Math.round((orderAmount - discount) * 100.0) / 100.0,
"appliedAt", LocalDateTime.now().toString()
));
}
// ── 14. 추천인 현황 ────────────────────────────────────────
@GetMapping("/referrals/{customerId}")
public ApiResponse<?> getReferrals(@PathVariable Long customerId) {
List<Map<String, Object>> referred = List.of(
Map.of("referredAt", "2026-05-15", "status", "CONVERTED", "earnedPoints", 200, "friendInitials", "J.K."),
Map.of("referredAt", "2026-04-20", "status", "CONVERTED", "earnedPoints", 200, "friendInitials", "M.L."),
Map.of("referredAt", "2026-03-10", "status", "PENDING", "earnedPoints", 0, "friendInitials", "S.P.")
);
return ApiResponse.ok(Map.of(
"customerId", customerId,
"totalReferrals", 3,
"convertedReferrals", 2,
"pendingReferrals", 1,
"totalEarned", 400,
"referrals", referred,
"nextRewardAt", "5 referrals — Bonus $25 credit"
));
}
// ── 15. 추천 코드 생성 ─────────────────────────────────────
@PostMapping("/referrals")
public ApiResponse<?> generateReferralCode(@RequestBody Map<String, Object> body) {
Long customerId = ((Number) body.getOrDefault("customerId", 0L)).longValue();
String code = "FLOWER" + customerId + String.valueOf(System.currentTimeMillis() % 1000).toUpperCase();
return ApiResponse.ok(Map.of(
"customerId", customerId,
"referralCode", code,
"shareUrl", "https://shop.montvaleflowers.com/refer?code=" + code,
"reward", Map.of(
"referrer", "200 points when friend orders",
"friend", "$10 off first order"
),
"createdAt", LocalDateTime.now().toString(),
"expiresAt", LocalDate.now().plusDays(90).toString()
));
}
// ── 16. 구독 상세 ─────────────────────────────────────────
@GetMapping("/subscriptions/{customerId}")
public ApiResponse<?> getSubscription(@PathVariable Long customerId) {
Map<String, Object> sub = new LinkedHashMap<>();
sub.put("customerId", customerId);
sub.put("subscriptionId", "SUB-" + customerId + "-001");
sub.put("plan", "BIWEEKLY");
sub.put("status", "ACTIVE");
sub.put("product", "Seasonal Mixed Bouquet");
sub.put("pricePerDelivery", 60.00);
sub.put("nextDelivery", LocalDate.now().plusDays(8).toString());
sub.put("deliveryAddress", "123 Main St, Montvale, NJ 07645");
sub.put("deliveryDay", "Saturday");
sub.put("startDate", "2026-01-15");
sub.put("totalDeliveries", 11);
sub.put("savedAmount", 66.00);
sub.put("specialInstructions", "Please leave at the door if no answer");
return ApiResponse.ok(sub);
}
// ── 17. 구독 일시정지 ──────────────────────────────────────
@PutMapping("/subscriptions/{customerId}/pause")
public ApiResponse<?> pauseSubscription(@PathVariable Long customerId,
@RequestBody(required = false) Map<String, Object> body) {
int pauseWeeks = body != null ? ((Number) body.getOrDefault("weeks", 2)).intValue() : 2;
String reason = body != null ? (String) body.getOrDefault("reason", "Going on vacation") : "Customer request";
LocalDate resumeDate = LocalDate.now().plusWeeks(pauseWeeks);
return ApiResponse.ok(Map.of(
"customerId", customerId, "status", "PAUSED",
"pauseReason", reason, "pauseWeeks", pauseWeeks,
"resumeDate", resumeDate.toString(),
"skippedDeliveries", pauseWeeks / 2,
"message", "Subscription paused until " + resumeDate + ". We'll send a reminder before resumption."
));
}
// ── 18. 구독 재개 ─────────────────────────────────────────
@PutMapping("/subscriptions/{customerId}/resume")
public ApiResponse<?> resumeSubscription(@PathVariable Long customerId) {
LocalDate nextDelivery = LocalDate.now().plusDays(7);
return ApiResponse.ok(Map.of(
"customerId", customerId, "status", "ACTIVE",
"resumedAt", LocalDateTime.now().toString(),
"nextDelivery", nextDelivery.toString(),
"message", "Subscription resumed! Your next delivery is scheduled for " + nextDelivery
));
}
// ── 19. 구독 취소 ─────────────────────────────────────────
@PutMapping("/subscriptions/{customerId}/cancel")
public ApiResponse<?> cancelSubscription(@PathVariable Long customerId,
@RequestBody(required = false) Map<String, Object> body) {
String reason = body != null ? (String) body.getOrDefault("reason", "Not specified") : "Customer request";
String feedback = body != null ? (String) body.getOrDefault("feedback", "") : "";
return ApiResponse.ok(Map.of(
"customerId", customerId, "status", "CANCELLED",
"cancellationReason", reason, "feedback", feedback,
"cancelledAt", LocalDateTime.now().toString(),
"finalDelivery", LocalDate.now().plusDays(3).toString(),
"earnedPoints", 2847,
"offeredRetention", Map.of(
"offer", "Come back anytime — your 2,847 points are saved",
"winbackCoupon", "COMEBACK15",
"couponValue", "15% off your next order"
),
"message", "Subscription cancelled. We hope to see you again soon."
));
}
// ── 20. 위시리스트 ────────────────────────────────────────
@GetMapping("/wishlist/{customerId}")
public ApiResponse<?> getWishlist(@PathVariable Long customerId) {
List<Map<String, Object>> items = List.of(
Map.of("productId", 1L, "name", "Grand Luxury Rose Tower", "price", 350.00,
"addedOn", "2026-06-10", "inStock", true, "priceDropped", false),
Map.of("productId", 2L, "name", "Pink Peony Paradise", "price", 110.00,
"addedOn", "2026-05-28", "inStock", true, "priceDropped", true, "previousPrice", 130.00),
Map.of("productId", 3L, "name", "Orchid Elegance Display", "price", 280.00,
"addedOn", "2026-05-15", "inStock", false, "priceDropped", false),
Map.of("productId", 4L, "name", "Bohemian Wildflowers XL", "price", 95.00,
"addedOn", "2026-04-30", "inStock", true, "priceDropped", false)
);
return ApiResponse.ok(Map.of(
"customerId", customerId, "items", items, "count", items.size(),
"priceDropAlerts", 1, "backInStockAlerts", 0
));
}
// ── 21. 위시리스트 추가 ────────────────────────────────────
@PostMapping("/wishlist/{customerId}")
public ApiResponse<?> addToWishlist(@PathVariable Long customerId, @RequestBody Map<String, Object> body) {
Long productId = ((Number) body.getOrDefault("productId", 0L)).longValue();
boolean priceAlert = (boolean) body.getOrDefault("priceAlert", true);
boolean stockAlert = (boolean) body.getOrDefault("stockAlert", true);
return ApiResponse.ok(Map.of(
"customerId", customerId, "productId", productId,
"addedAt", LocalDateTime.now().toString(),
"priceAlertEnabled", priceAlert, "stockAlertEnabled", stockAlert,
"wishlistCount", 5,
"message", "Added to wishlist. We'll notify you of price drops and stock changes."
));
}
// ── 22. 위시리스트 제거 ────────────────────────────────────
@DeleteMapping("/wishlist/{customerId}/{productId}")
public ApiResponse<?> removeFromWishlist(@PathVariable Long customerId, @PathVariable Long productId) {
return ApiResponse.ok(Map.of(
"customerId", customerId, "productId", productId,
"removedAt", LocalDateTime.now().toString(),
"remainingWishlistCount", 3,
"message", "Item removed from wishlist"
));
}
// ── 23. 재주문 추천 (자주 구매) ──────────────────────────
@GetMapping("/reorder/{customerId}")
public ApiResponse<?> getReorderSuggestions(@PathVariable Long customerId) {
List<Map<String, Object>> suggestions = List.of(
Map.of("productId", 1L, "name", "Classic Red Dozen Roses",
"lastOrdered", "2026-06-01", "timesOrdered", 4, "avgInterval", "18 days",
"daysOverdue", 7, "suggestionStrength", "STRONG",
"price", 89.00, "discountForReorder", "5% loyalty discount"),
Map.of("productId", 5L, "name", "Seasonal Mixed Bouquet",
"lastOrdered", "2026-05-25", "timesOrdered", 3, "avgInterval", "21 days",
"daysOverdue", 0, "suggestionStrength", "MEDIUM",
"price", 70.00, "discountForReorder", null),
Map.of("productId", 3L, "name", "Pink Peony Delight",
"lastOrdered", "2026-05-10", "timesOrdered", 2, "avgInterval", "30 days",
"daysOverdue", 8, "suggestionStrength", "MEDIUM",
"price", 110.00, "discountForReorder", null)
);
return ApiResponse.ok(Map.of(
"customerId", customerId,
"reorderSuggestions", suggestions,
"tip", "Your usual flowers are ready to reorder — 1-click to repeat your favorites!"
));
}
// ── 24. AI 고객 개인화 (Ollama) ───────────────────────────
@PostMapping("/ai-personalize")
public ApiResponse<?> aiPersonalize(@RequestBody Map<String, Object> body) {
Long customerId = ((Number) body.getOrDefault("customerId", 0L)).longValue();
String tier = (String) body.getOrDefault("tier", "GOLD");
@SuppressWarnings("unchecked")
List<String> purchaseHistory = (List<String>) body.getOrDefault("purchaseHistory", List.of("Red Roses", "Peonies"));
@SuppressWarnings("unchecked")
List<String> occasions = (List<String>) body.getOrDefault("occasions", List.of("anniversary", "birthday"));
String prompt = "You are a loyalty AI for a flower shop. Customer tier: " + tier
+ ". Past purchases: " + purchaseHistory + ". Occasions: " + occasions
+ ". Provide: 3 personalized next-purchase recommendations with reasons, 1 exclusive loyalty offer, 1 engagement tip. Keep it warm and personal.";
String aiResponse = ollama.generate(prompt);
if (aiResponse.isEmpty()) {
aiResponse = "As a valued GOLD member, we recommend: 1) Premium Rose Bundle ($99) — perfect for upcoming occasions, 2) Seasonal Subscription — save 15% monthly, 3) Exclusive Orchid Collection — new arrivals just for GOLD members. Special offer: Double points this weekend!";
}
return ApiResponse.ok(Map.of(
"customerId", customerId, "tier", tier,
"aiPersonalization", aiResponse,
"generatedAt", LocalDateTime.now().toString()
));
}
// ── 25. 멤버십 전체 통계 ───────────────────────────────────
@GetMapping("/stats")
@PreAuthorize("hasRole('ADMIN')")
public ApiResponse<?> getMembershipStats() {
Map<String, Object> stats = new LinkedHashMap<>();
stats.put("totalMembers", 2071);
stats.put("activeMembers", 1843);
stats.put("tierBreakdown", Map.of("BRONZE", 1247, "SILVER", 483, "GOLD", 287, "PLATINUM", 54));
stats.put("totalPointsIssued", 8_420_000);
stats.put("totalPointsRedeemed", 3_180_000);
stats.put("pointsLiability", "$52,400");
stats.put("avgPointsPerMember", 2847);
stats.put("topEarnerThisMonth", Map.of("customerId", 1042L, "pointsEarned", 1250));
stats.put("subscriptionMetrics", Map.of("activeSubscriptions", 287, "churnThisMonth", 9, "newSubscriptions", 41));
stats.put("couponMetrics", Map.of("couponsIssued", 1240, "couponsRedeemed", 987, "redemptionRate", "79.6%", "avgDiscountValue", "$12.40"));
stats.put("referralMetrics", Map.of("totalReferrals", 312, "convertedReferrals", 247, "conversionRate", "79.2%"));
stats.put("reportDate", LocalDate.now().toString());
return ApiResponse.ok(stats);
}
}