497 lines
29 KiB
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);
|
|
}
|
|
}
|