| | |
| | | result.put("cardLevel", string(source.get("cardLevel"))); |
| | | result.put("benefitText", benefitText(principal.merchantId(), string(source.get("cardLevel")))); |
| | | result.put("rules", rules(principal.merchantId(), string(source.get("cardLevel")))); |
| | | result.put("coupons", coupons(principal.merchantId(), number(source.get("talentId")), string(source.get("cardLevel")))); |
| | | result.put("expiresAt", source.get("expiresAt")); |
| | | return result; |
| | | } |
| | |
| | | Map<String, Object> source = source(principal, request.verificationId(), false); |
| | | ensurePending(source); |
| | | Map<String, Object> rule = rule(principal.merchantId(), string(source.get("cardLevel")), request.ruleId()); |
| | | return quote(rule, request.amount()); |
| | | Map<String, Object> coupon = usableCoupon(principal.merchantId(), number(source.get("talentId")), string(source.get("cardLevel")), request.couponId(), discountedAmount(rule, request.amount())); |
| | | return quote(rule, coupon, request.amount()); |
| | | } |
| | | |
| | | @Transactional |
| | |
| | | return createdOrder(existingOrderId); |
| | | } |
| | | ensurePending(source); |
| | | if (expired(source.get("expiresAt"))) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "人才码已失效,请重新扫码"); |
| | | } |
| | | Map<String, Object> rule = rule(principal.merchantId(), string(source.get("cardLevel")), request.ruleId()); |
| | | Map<String, Object> amounts = quote(rule, request.amount()); |
| | | Map<String, Object> rule = rule(principal.merchantId(), string(source.get("cardLevel")), request.ruleId()); |
| | | Map<String, Object> coupon = usableCoupon(principal.merchantId(), number(source.get("talentId")), string(source.get("cardLevel")), request.couponId(), discountedAmount(rule, request.amount())); |
| | | Map<String, Object> amounts = quote(rule, coupon, request.amount()); |
| | | Long dynamicCodeId = number(source.get("dynamicCodeId")); |
| | | int codeUpdated = jdbc.update(""" |
| | | update talent_dynamic_code set status = 1, merchant_id = ?, used_at = now() |
| | | where id = ? and status = 0 and expires_at > now() |
| | | where id = ? and status = 0 |
| | | """, principal.merchantId(), dynamicCodeId); |
| | | if (codeUpdated == 0) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "人才码已被使用或已失效"); |
| | |
| | | String orderNo = BusinessIdGenerator.next("MC"); |
| | | jdbc.update(""" |
| | | insert into benefit_order(order_no, talent_id, card_id, merchant_id, store_id, order_type, |
| | | order_amount, subsidy_amount, base_pay_amount, pay_amount, expires_at, price_version, |
| | | order_amount, subsidy_amount, base_pay_amount, pay_amount, coupon_id, coupon_amount, expires_at, price_version, |
| | | merchant_name_snapshot, merchant_address_snapshot, merchant_mobile_snapshot, |
| | | merchant_longitude_snapshot, merchant_latitude_snapshot, product_name_snapshot, |
| | | order_status, verify_status, merchant_verification_attempt_id, client_idempotency_key) |
| | | values (?, ?, ?, ?, ?, 'STORE_CONSUME', ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?) |
| | | values (?, ?, ?, ?, ?, 'STORE_CONSUME', ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?) |
| | | """, orderNo, number(source.get("talentId")), number(source.get("cardId")), principal.merchantId(), principal.storeId(), |
| | | amounts.get("originalAmount"), amounts.get("subsidyAmount"), amounts.get("payAmount"), amounts.get("payAmount"), |
| | | amounts.get("originalAmount"), amounts.get("subsidyAmount"), amounts.get("basePayAmount"), amounts.get("payAmount"), request.couponId(), amounts.get("couponAmount"), |
| | | LocalDateTime.now().plusMinutes(15), merchant.get("merchantName"), merchant.get("address"), merchant.get("contactMobile"), |
| | | merchant.get("longitude"), merchant.get("latitude"), "到店消费 - " + rule.get("ruleName"), |
| | | number(source.get("attemptId")), "manual-consumption-" + number(source.get("attemptId"))); |
| | |
| | | if (number(source.get("attemptStatus")) != ATTEMPT_PENDING) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "该人才码已处理,请重新扫码"); |
| | | } |
| | | if (expired(source.get("expiresAt"))) { |
| | | throw new BusinessException(ErrorCode.CONFLICT, "人才码已失效,请重新扫码"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private List<Map<String, Object>> rules(Long merchantId, String cardLevel) { |
| | | List<Map<String, Object>> configured = configuredRules(merchantId, cardLevel); |
| | |
| | | return found.get(0); |
| | | } |
| | | |
| | | private List<Map<String, Object>> coupons(Long merchantId, Long talentId, String cardLevel) { |
| | | return rows(""" |
| | | select c.id, c.coupon_name as name, c.coupon_type as couponType, c.coupon_amount as couponAmount, c.talent_levels as talentLevels, |
| | | c.condition_type as conditionType, c.condition_amount as conditionAmount, c.valid_end as validEnd |
| | | from merchant_coupon c |
| | | where c.merchant_id = ? and c.deleted = 0 and c.status = 1 and c.store_consume = 1 |
| | | and (c.valid_start is null or c.valid_start <= current_date) and (c.valid_end is null or c.valid_end >= current_date) |
| | | and not exists ( |
| | | select 1 from benefit_order o |
| | | where o.coupon_id = c.id and o.talent_id = ? and o.deleted = 0 and o.order_status = 0 |
| | | and (o.expires_at is null or o.expires_at > current_timestamp) |
| | | ) |
| | | order by c.coupon_amount desc, c.id desc |
| | | """, merchantId, talentId).stream().filter(c -> supportsCard(string(c.get("talentLevels")), cardLevel)).map(c -> { |
| | | Map<String,Object> result = new LinkedHashMap<>(c); result.put("couponAmount", decimal(c.get("couponAmount")).movePointRight(2).longValue()); return result; |
| | | }).toList(); |
| | | } |
| | | |
| | | private Map<String, Object> usableCoupon(Long merchantId, Long talentId, String cardLevel, Long couponId, BigDecimal amount) { |
| | | if (couponId == null) return null; |
| | | List<Map<String,Object>> found = rows(""" |
| | | select c.id, c.coupon_amount as couponAmount, c.condition_type as conditionType, c.condition_amount as conditionAmount, |
| | | c.talent_levels as talentLevels |
| | | from merchant_coupon c |
| | | where c.id = ? and c.merchant_id = ? and c.deleted = 0 and c.status = 1 and c.store_consume = 1 |
| | | and (c.valid_start is null or c.valid_start <= current_date) and (c.valid_end is null or c.valid_end >= current_date) |
| | | and not exists ( |
| | | select 1 from benefit_order o |
| | | where o.coupon_id = c.id and o.talent_id = ? and o.deleted = 0 and o.order_status = 0 |
| | | and (o.expires_at is null or o.expires_at > current_timestamp) |
| | | ) |
| | | """, couponId, merchantId, talentId); |
| | | if (found.isEmpty() || !supportsCard(string(found.get(0).get("talentLevels")), cardLevel)) throw new BusinessException(ErrorCode.CONFLICT, "优惠券当前不可用,请重新选择"); |
| | | Map<String,Object> coupon = found.get(0); |
| | | String condition = string(coupon.get("conditionType")); |
| | | if (!"NONE".equalsIgnoreCase(condition) && coupon.get("conditionAmount") != null && amount.compareTo(decimal(coupon.get("conditionAmount"))) < 0) throw new BusinessException(ErrorCode.CONFLICT, "消费金额未达到优惠券使用条件"); |
| | | return coupon; |
| | | } |
| | | |
| | | private boolean supportsCard(String levels, String cardLevel) { |
| | | if (levels == null || levels.isBlank()) return true; |
| | | return java.util.Arrays.stream(levels.split("[,、,]" )).anyMatch(v -> matchesCardLevel(v, cardLevel)); |
| | | } |
| | | |
| | | private long configRuleId(String level) { |
| | | return switch (level) { case "EXCELLENT" -> -1L; case "OUTSTANDING" -> -2L; case "ELITE" -> -3L; default -> -9L; }; |
| | | } |
| | |
| | | return "true".equals(text) || "1".equals(text) || "yes".equals(text); |
| | | } |
| | | |
| | | private Map<String, Object> quote(Map<String, Object> rule, BigDecimal inputAmount) { |
| | | private Map<String, Object> quote(Map<String, Object> rule, Map<String,Object> coupon, BigDecimal inputAmount) { |
| | | long originalAmount = inputAmount.movePointRight(2).setScale(0, RoundingMode.HALF_UP).longValueExact(); |
| | | BigDecimal rate = decimal(rule.get("discountRate")); |
| | | if (rate.signum() <= 0 || rate.compareTo(BigDecimal.ONE) > 0) throw new BusinessException(ErrorCode.CONFLICT, "商户优惠规则配置无效"); |
| | | long payAmount = BigDecimal.valueOf(originalAmount).multiply(rate).setScale(0, RoundingMode.HALF_UP).longValueExact(); |
| | | long payAmount = discountedAmountCents(rule, originalAmount); |
| | | long basePayAmount = payAmount; |
| | | long couponAmount = coupon == null ? 0 : Math.min(decimal(coupon.get("couponAmount")).movePointRight(2).setScale(0, RoundingMode.HALF_UP).longValue(), basePayAmount); |
| | | payAmount = basePayAmount - couponAmount; |
| | | long discountAmount = originalAmount - payAmount; |
| | | long subsidyAmount = number(rule.get("subsidyEnabled")) == 1 ? discountAmount : 0; |
| | | long subsidyAmount = originalAmount - payAmount; |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("originalAmount", originalAmount); |
| | | result.put("discountAmount", discountAmount); |
| | | result.put("payAmount", payAmount); |
| | | result.put("basePayAmount", basePayAmount); |
| | | result.put("couponAmount", couponAmount); |
| | | result.put("subsidyAmount", subsidyAmount); |
| | | result.put("ruleName", rule.get("ruleName")); |
| | | return result; |
| | | } |
| | | |
| | | private BigDecimal discountedAmount(Map<String, Object> rule, BigDecimal inputAmount) { |
| | | long originalAmount = inputAmount.movePointRight(2).setScale(0, RoundingMode.HALF_UP).longValueExact(); |
| | | return BigDecimal.valueOf(discountedAmountCents(rule, originalAmount), 2); |
| | | } |
| | | |
| | | private long discountedAmountCents(Map<String, Object> rule, long originalAmount) { |
| | | BigDecimal rate = decimal(rule.get("discountRate")); |
| | | if (rate.signum() <= 0 || rate.compareTo(BigDecimal.ONE) > 0) throw new BusinessException(ErrorCode.CONFLICT, "商户优惠规则配置无效"); |
| | | return BigDecimal.valueOf(originalAmount).multiply(rate).setScale(0, RoundingMode.HALF_UP).longValueExact(); |
| | | } |
| | | private Map<String, Object> merchant(MerchantPrincipal principal) { |
| | | List<Map<String, Object>> merchants = rows(""" |
| | |
| | | .map(benefit -> string(benefit.get("benefitText"))).findFirst().orElse("以当前商户到店消费规则为准"); |
| | | } |
| | | |
| | | String benefitTextForMerchant(Long merchantId, String cardLevel) { |
| | | return benefitText(merchantId, cardLevel); |
| | | } |
| | | |
| | | private boolean matchesCardLevel(String configured, String actual) { |
| | | return normalizedCardLevel(configured).equals(normalizedCardLevel(actual)); |
| | | } |