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 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> 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 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 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> 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 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> 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 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 body) { double orderAmount = body != null ? ((Number) body.getOrDefault("orderAmount", 0.0)).doubleValue() : 0.0; // Static validation logic Map 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 info = (Map) 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 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> 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 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 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 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 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> 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 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> 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 body) { Long customerId = ((Number) body.getOrDefault("customerId", 0L)).longValue(); String tier = (String) body.getOrDefault("tier", "GOLD"); @SuppressWarnings("unchecked") List purchaseHistory = (List) body.getOrDefault("purchaseHistory", List.of("Red Roses", "Peonies")); @SuppressWarnings("unchecked") List occasions = (List) 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 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); } }