package com.talentcard.server.service; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.talentcard.server.business.BusinessIdGenerator; import com.talentcard.server.business.order.OrderFundBoundaryService; import com.talentcard.server.request.BenefitVerificationRequest; import com.talentcard.server.business.merchant.auth.MerchantPrincipal; import com.talentcard.server.common.BusinessException; import com.talentcard.server.common.ErrorCode; import com.talentcard.server.entity.BenefitCinemaCoupon; import com.talentcard.server.entity.BenefitCinemaCouponClaim; import com.talentcard.server.entity.BenefitFreeStayVerificationCode; import com.talentcard.server.entity.BenefitFreeStayVoucher; import com.talentcard.server.entity.BenefitMerchant; import com.talentcard.server.entity.BenefitMerchantProduct; import com.talentcard.server.entity.BenefitOrder; import com.talentcard.server.entity.BenefitOrderDynamicVerificationCode; import com.talentcard.server.entity.BenefitVerifyRecord; import com.talentcard.server.entity.TicketPackageClaim; import com.talentcard.server.entity.TicketPackageCode; import com.talentcard.server.mapper.BenefitCinemaCouponClaimMapper; import com.talentcard.server.mapper.BenefitCinemaCouponMapper; import com.talentcard.server.mapper.BenefitFreeStayVerificationCodeMapper; import com.talentcard.server.mapper.BenefitFreeStayVoucherMapper; import com.talentcard.server.mapper.BenefitMerchantMapper; import com.talentcard.server.mapper.BenefitMerchantProductMapper; import com.talentcard.server.mapper.BenefitOrderDynamicVerificationCodeMapper; import com.talentcard.server.mapper.BenefitOrderMapper; import com.talentcard.server.mapper.BenefitVerifyRecordMapper; import com.talentcard.server.mapper.TicketPackageClaimMapper; import com.talentcard.server.mapper.TicketPackageCodeMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class BenefitVerificationService { private final BenefitQuotaService quotaService; private final BenefitVerifyRecordMapper verifyRecordMapper; private final BenefitCinemaCouponClaimMapper claimMapper; private final BenefitCinemaCouponMapper couponMapper; private final BenefitOrderDynamicVerificationCodeMapper orderCodeMapper; private final BenefitOrderMapper orderMapper; private final BenefitFreeStayVerificationCodeMapper freeStayCodeMapper; private final BenefitFreeStayVoucherMapper voucherMapper; private final BenefitMerchantMapper merchantMapper; private final BenefitMerchantProductMapper productMapper; private final TicketPackageClaimMapper ticketPackageClaimMapper; private final TicketPackageCodeMapper ticketPackageCodeMapper; private final TicketPackagePortalService ticketPackagePortalService; private final OrderFundBoundaryService orderFundBoundary; public BenefitVerificationService( BenefitQuotaService quotaService, BenefitVerifyRecordMapper verifyRecordMapper, BenefitCinemaCouponClaimMapper claimMapper, BenefitCinemaCouponMapper couponMapper, BenefitOrderDynamicVerificationCodeMapper orderCodeMapper, BenefitOrderMapper orderMapper, BenefitFreeStayVerificationCodeMapper freeStayCodeMapper, BenefitFreeStayVoucherMapper voucherMapper, BenefitMerchantMapper merchantMapper, BenefitMerchantProductMapper productMapper, TicketPackageClaimMapper ticketPackageClaimMapper, TicketPackageCodeMapper ticketPackageCodeMapper, TicketPackagePortalService ticketPackagePortalService, OrderFundBoundaryService orderFundBoundary ) { this.quotaService = quotaService; this.verifyRecordMapper = verifyRecordMapper; this.claimMapper = claimMapper; this.couponMapper = couponMapper; this.orderCodeMapper = orderCodeMapper; this.orderMapper = orderMapper; this.freeStayCodeMapper = freeStayCodeMapper; this.voucherMapper = voucherMapper; this.merchantMapper = merchantMapper; this.productMapper = productMapper; this.ticketPackageClaimMapper = ticketPackageClaimMapper; this.ticketPackageCodeMapper = ticketPackageCodeMapper; this.ticketPackagePortalService = ticketPackagePortalService; this.orderFundBoundary = orderFundBoundary; } @Transactional public Map verify(MerchantPrincipal principal, BenefitVerificationRequest request, String idempotencyKey) { String key = normalizeIdempotencyKey(idempotencyKey); BenefitVerifyRecord existing = verifiedByIdempotencyKey(principal, key); if (existing != null) { return result(existing, null, null, null, 0); } String benefitType = quotaService.normalizeBenefitType(request.benefitType()); String verifyCode = request.verifyCode() == null ? "" : request.verifyCode().trim(); if (verifyCode.isBlank()) { throw new BusinessException(ErrorCode.BAD_REQUEST, "核销码不能为空"); } if (BenefitQuotaService.MOVIE_TICKET.equals(benefitType)) { return verifyMovieTicket(principal, verifyCode, request.remark(), key); } return verifyHotel(principal, verifyCode, request.remark(), key); } private Map verifyMovieTicket(MerchantPrincipal principal, String verifyCode, String remark, String idempotencyKey) { BenefitCinemaCouponClaim probe = movieClaimByCode(verifyCode, false); if (probe == null) { return verifyTicketPackageClaim(principal, verifyCode, remark, idempotencyKey); } if (!verifyCode.startsWith(BenefitVerificationCodeService.MOVIE_TOKEN_PREFIX)) { throw new BusinessException(ErrorCode.CONFLICT, "电影票核销码必须由二期生成核销码接口生成"); } if (!principal.merchantId().equals(probe.getMerchantId())) { throw new BusinessException(ErrorCode.CONFLICT, "电影票不属于当前商户"); } quotaService.current(BenefitQuotaService.MOVIE_TICKET, probe.getCardId(), probe.getTalentId(), probe.getMerchantId(), null); BenefitCinemaCouponClaim claim = movieClaimByCode(verifyCode, true); validateMovieClaim(claim); int updated = claimMapper.update(null, new UpdateWrapper() .set("code_status", "USED") .set("status", "USED") .setSql("used_at = now()") .setSql("updated_at = now()") .eq("id", claim.getId()) .eq("deleted", 0) .eq("exchange_code", verifyCode) .apply("upper(coalesce(code_status, 'ACTIVE')) = 'ACTIVE'")); if (updated != 1) { throw new BusinessException(ErrorCode.CONFLICT, "电影票核销码已使用或已过期"); } BenefitVerifyRecord record = insertRecord(principal, null, claim.getTalentId(), claim.getCardId(), null, BenefitQuotaService.MOVIE_TICKET, 0L, 0L, sourceRemark("cinemaCouponClaimId=" + claim.getId(), remark), idempotencyKey); BenefitQuotaService.QuotaSnapshot quotaAfter = quotaService.current(BenefitQuotaService.MOVIE_TICKET, claim.getCardId(), claim.getTalentId(), claim.getMerchantId(), null); return result(record, "CINEMA_COUPON_CLAIM", claim.getId(), quotaAfter, 1); } private Map verifyTicketPackageClaim(MerchantPrincipal principal, String verifyCode, String remark, String idempotencyKey) { TicketPackageClaim probe = ticketPackageClaimMapper.selectByExchangeCodeForUpdate(verifyCode); if (probe == null) { throw new BusinessException(ErrorCode.NOT_FOUND, "电影票核销码不存在"); } if (!principal.merchantId().equals(probe.getMerchantId())) { throw new BusinessException(ErrorCode.CONFLICT, "电影票不属于当前商户"); } validateTicketPackageClaim(probe); int claimUpdated = ticketPackageClaimMapper.update(null, new UpdateWrapper() .set("status", "USED") .setSql("used_at = now()") .setSql("updated_at = now()") .eq("id", probe.getId()) .eq("deleted", 0) .eq("exchange_code", verifyCode) .eq("status", "ISSUED")); int codeUpdated = ticketPackageCodeMapper.update(null, new UpdateWrapper() .set("status", "USED") .setSql("used_at = now()") .setSql("updated_at = now()") .eq("id", probe.getCodeId()) .eq("deleted", 0) .eq("status", "ISSUED")); if (claimUpdated != 1 || codeUpdated != 1) { throw new BusinessException(ErrorCode.CONFLICT, "电影票核销码已使用或已过期"); } BenefitVerifyRecord record = insertRecord(principal, null, probe.getTalentId(), probe.getCardId(), null, BenefitQuotaService.MOVIE_TICKET, 0L, 0L, sourceRemark("ticketPackageClaimId=" + probe.getId(), remark), idempotencyKey); Map quotaAfter = ticketPackagePortalService.packageQuota( probe.getTalentId(), probe.getCardId(), probe.getMerchantId(), probe.getPackageId()); return ticketPackageResult(record, probe.getId(), quotaAfter); } private Map verifyHotel(MerchantPrincipal principal, String verifyCode, String remark, String idempotencyKey) { HotelOrderCodeContext orderCode = hotelOrderCode(verifyCode, principal, false); if (orderCode != null) { return verifyHotelOrderCode(principal, verifyCode, orderCode, remark, idempotencyKey); } FreeStayCodeContext freeStayCode = freeStayCode(verifyCode, false); if (freeStayCode != null) { return verifyFreeStayCode(principal, verifyCode, freeStayCode, remark, idempotencyKey); } throw new BusinessException(ErrorCode.NOT_FOUND, "酒店核销码不存在"); } private Map verifyHotelOrderCode(MerchantPrincipal principal, String verifyCode, HotelOrderCodeContext probe, String remark, String idempotencyKey) { if (!belongsToPrincipal(probe.order(), principal)) { throw new BusinessException(ErrorCode.CONFLICT, "酒店订单不属于当前门店"); } BenefitQuotaService.QuotaSnapshot quotaBefore = quotaService.requireAvailable(BenefitQuotaService.HOTEL, probe.order().getCardId(), probe.order().getTalentId(), principal.merchantId(), probe.order().getProductId(), 1); HotelOrderCodeContext context = hotelOrderCode(verifyCode, principal, true); if (context == null || !belongsToPrincipal(context.order(), principal)) { throw new BusinessException(ErrorCode.CONFLICT, "酒店订单核销码不属于当前门店"); } BenefitOrderDynamicVerificationCode code = context.code(); BenefitOrder order = context.order(); if (intValue(code.getStatus()) != 0 || expiredDateTime(code.getExpiresAt())) { throw new BusinessException(ErrorCode.CONFLICT, "酒店订单当前不可核销"); } // 与商户端核销、退款共用同一把订单行锁:未支付、已退款或已进入提现记录的订单都不能核销。 orderFundBoundary.lock(order.getId()).requireVerifiable(); int codeUpdated = orderCodeMapper.update(null, new UpdateWrapper() .set("status", 1) .eq("id", code.getId()) .eq("status", 0) .gt("expires_at", LocalDateTime.now())); int orderUpdated = orderMapper.update(null, new UpdateWrapper() .set("verify_status", 1) .eq("id", order.getId()) .eq("order_status", 1) .eq("verify_status", 0) .eq("deleted", 0)); if (codeUpdated != 1 || orderUpdated != 1) { throw new BusinessException(ErrorCode.CONFLICT, "酒店订单已核销或核销码已过期"); } BenefitVerifyRecord record = insertRecord(principal, order.getId(), order.getTalentId(), order.getCardId(), order.getBenefitId(), "HOTEL_ORDER", order.getPayAmount(), order.getSubsidyAmount(), sourceRemark("orderNo=" + order.getOrderNo(), remark), idempotencyKey); return result(record, "BENEFIT_ORDER", order.getId(), afterDeduct(quotaBefore, 1), 1); } private Map verifyFreeStayCode(MerchantPrincipal principal, String verifyCode, FreeStayCodeContext probe, String remark, String idempotencyKey) { if (!principal.merchantId().equals(probe.voucher().getMerchantId())) { throw new BusinessException(ErrorCode.CONFLICT, "酒店核销码不属于当前商户"); } HotelToken token = parseHotelToken(verifyCode); Long sourceCardId = token.sourceCardId() == null ? probe.voucher().getCardId() : token.sourceCardId(); Long productId = token.productId(); BenefitQuotaService.QuotaSnapshot quotaBefore = quotaService.requireAvailable(BenefitQuotaService.HOTEL, sourceCardId, null, principal.merchantId(), productId, 1); FreeStayCodeContext context = freeStayCode(verifyCode, true); if (context == null || !principal.merchantId().equals(context.voucher().getMerchantId())) { throw new BusinessException(ErrorCode.CONFLICT, "酒店核销码不属于当前商户"); } BenefitFreeStayVerificationCode code = context.code(); BenefitFreeStayVoucher voucher = context.voucher(); if (intValue(code.getStatus()) != 0 || expiredDateTime(code.getExpiresAt())) { throw new BusinessException(ErrorCode.CONFLICT, "酒店核销码已过期"); } if (intValue(voucher.getStatus()) != 1 || expiredDate(voucher.getValidEnd()) || intValue(voucher.getUsedCount()) >= intValue(voucher.getTotalCount())) { throw new BusinessException(ErrorCode.CONFLICT, "酒店权益券次数已用完"); } if (productId != null) { decrementHotelProductStock(principal.merchantId(), productId); } int codeUpdated = freeStayCodeMapper.update(null, new UpdateWrapper() .set("status", 1) .eq("id", code.getId()) .eq("status", 0) .gt("expires_at", LocalDateTime.now())); int voucherUpdated = voucherMapper.update(null, new UpdateWrapper() .setSql("used_count = used_count + 1") .eq("id", voucher.getId()) .eq("status", 1) .ge("valid_end", LocalDate.now()) .apply("used_count < total_count")); if (codeUpdated != 1 || voucherUpdated != 1) { throw new BusinessException(ErrorCode.CONFLICT, "酒店核销码已使用或权益次数已用完"); } BenefitVerifyRecord record = insertRecord(principal, null, quotaBefore.talentId(), quotaBefore.cardId(), null, "HOTEL", 0L, 0L, sourceRemark("freeStayVoucherId=" + voucher.getId() + productRemark(productId), remark), idempotencyKey); return result(record, "FREE_STAY_VERIFICATION_CODE", code.getId(), afterDeduct(quotaBefore, 1), 1); } private void validateMovieClaim(BenefitCinemaCouponClaim claim) { if (claim == null) { throw new BusinessException(ErrorCode.NOT_FOUND, "电影票核销码不存在"); } BenefitCinemaCoupon coupon = couponMapper.selectById(claim.getCouponId()); if (!"ACTIVE".equals(upper(claim.getCodeStatus()))) { throw new BusinessException(ErrorCode.CONFLICT, "电影票核销码已过期"); } if (coupon == null || expiredDate(claim.getValidEnd()) || intValue(coupon.getStatus()) != 1) { throw new BusinessException(ErrorCode.CONFLICT, "电影票权益券当前不可用"); } } private void validateTicketPackageClaim(TicketPackageClaim claim) { if (claim == null) { throw new BusinessException(ErrorCode.NOT_FOUND, "电影票核销码不存在"); } if (!"ISSUED".equals(upper(claim.getStatus()))) { throw new BusinessException(ErrorCode.CONFLICT, "电影票核销码已使用或已失效"); } if (expiredDate(claim.getValidEnd())) { throw new BusinessException(ErrorCode.CONFLICT, "电影票核销码已过期"); } } private BenefitCinemaCouponClaim movieClaimByCode(String code, boolean lock) { return claimMapper.selectOne(new QueryWrapper() .eq("deleted", 0) .eq("exchange_code", code) .last("limit 1" + (lock ? " for update" : ""))); } private HotelOrderCodeContext hotelOrderCode(String token, MerchantPrincipal principal, boolean lock) { BenefitOrderDynamicVerificationCode code = orderCodeMapper.selectOne(new QueryWrapper() .eq("code_token", token) .last("limit 1" + (lock ? " for update" : ""))); if (code == null) { return null; } BenefitOrder order = orderMapper.selectOne(new QueryWrapper() .eq("id", code.getOrderId()) .eq("deleted", 0) .last("limit 1" + (lock ? " for update" : ""))); if (order == null || !principal.merchantId().equals(order.getMerchantId())) { return null; } BenefitMerchant merchant = merchantMapper.selectById(order.getMerchantId()); if (merchant == null || intValue(merchant.getDeleted()) != 0 || !"hotel".equalsIgnoreCase(String.valueOf(merchant.getMerchantType()))) { return null; } return new HotelOrderCodeContext(code, order); } private FreeStayCodeContext freeStayCode(String token, boolean lock) { BenefitFreeStayVerificationCode code = freeStayCodeMapper.selectOne(new QueryWrapper() .eq("code_token", token) .last("limit 1" + (lock ? " for update" : ""))); if (code == null) { return null; } BenefitFreeStayVoucher voucher = voucherMapper.selectOne(new QueryWrapper() .eq("id", code.getVoucherId()) .last("limit 1" + (lock ? " for update" : ""))); if (voucher == null) { return null; } return new FreeStayCodeContext(code, voucher); } private void decrementHotelProductStock(Long merchantId, Long productId) { int updated = productMapper.update(null, new UpdateWrapper() .setSql("stock = case when stock = -1 then stock else stock - 1 end") .setSql("sales_count = sales_count + 1") .eq("id", productId) .eq("merchant_id", merchantId) .eq("deleted", 0) .eq("status", 1) .and(wrapper -> wrapper.isNull("valid_start_time").or().le("valid_start_time", LocalDateTime.now())) .and(wrapper -> wrapper.isNull("valid_end_time").or().gt("valid_end_time", LocalDateTime.now())) .apply("(stock = -1 or stock > 0)")); if (updated != 1) { throw new BusinessException(ErrorCode.CONFLICT, "酒店房型或套餐库存不足"); } } private BenefitVerifyRecord insertRecord(MerchantPrincipal principal, Long orderId, Long talentId, Long cardId, Long benefitId, String verifyType, Long verifyAmount, Long subsidyAmount, String remark, String idempotencyKey) { BenefitVerifyRecord record = new BenefitVerifyRecord(); record.setVerifyNo(BusinessIdGenerator.next("V")); record.setOrderId(orderId); record.setTalentId(talentId); record.setCardId(cardId); record.setBenefitId(benefitId); record.setMerchantId(principal.merchantId()); record.setStoreId(principal.storeId()); record.setStaffId(principal.operatorId()); record.setVerifyType(verifyType); record.setVerifyAmount(zeroIfNull(verifyAmount)); record.setSubsidyAmount(zeroIfNull(subsidyAmount)); record.setRemark(trimToLength(remark, 512)); record.setStatus(1); record.setIdempotencyKey(idempotencyKey); verifyRecordMapper.insert(record); return record(record.getId()); } private BenefitVerifyRecord verifiedByIdempotencyKey(MerchantPrincipal principal, String key) { return verifyRecordMapper.selectOne(new QueryWrapper() .eq("idempotency_key", key) .eq("merchant_id", principal.merchantId()) .eq("status", 1) .last("limit 1")); } private BenefitVerifyRecord record(Long recordId) { BenefitVerifyRecord record = verifyRecordMapper.selectById(recordId); if (record == null) { throw new BusinessException(ErrorCode.INTERNAL_ERROR, "核销记录保存失败"); } return record; } private Map result(BenefitVerifyRecord record, String sourceType, Object sourceId, BenefitQuotaService.QuotaSnapshot quota, int quantity) { Map result = new LinkedHashMap<>(); result.put("verified", true); result.put("verifyRecordId", record.getId()); result.put("verifyNo", record.getVerifyNo()); result.put("benefitType", record.getVerifyType()); result.put("sourceType", sourceType); result.put("sourceId", sourceId); result.put("quantity", quantity); result.put("verifiedAt", record.getVerifyTime()); result.put("sharedQuotaMode", "AUTO_DEDUCT"); if (quota != null) { result.put("sourceCardId", quota.cardId()); result.put("sourceTalentId", quota.talentId()); result.put("familyCard", quota.familyCard()); result.put("primaryCardId", quota.primaryCardId()); result.put("primaryTalentId", quota.primaryTalentId()); result.put("primaryCardNo", quota.primaryCardNo()); result.put("cardLevel", quota.cardLevel()); result.put("quotaTotal", quota.total()); result.put("quotaUsed", quota.used()); result.put("quotaRemaining", quota.remaining()); result.put("quotaResetCycle", quota.resetCycle()); result.put("quotaWindowStart", quota.windowStart()); } return result; } private Map ticketPackageResult(BenefitVerifyRecord record, Object sourceId, Map quota) { Map result = new LinkedHashMap<>(); result.put("verified", true); result.put("verifyRecordId", record.getId()); result.put("verifyNo", record.getVerifyNo()); result.put("benefitType", record.getVerifyType()); result.put("sourceType", "TICKET_PACKAGE_CLAIM"); result.put("sourceId", sourceId); result.put("quantity", 1); result.put("verifiedAt", record.getVerifyTime()); result.put("sharedQuotaMode", "AUTO_DEDUCT"); result.put("sourceCardId", quota.get("sourceCardId")); result.put("sourceTalentId", quota.get("sourceTalentId")); result.put("familyCard", quota.get("familyCard")); result.put("primaryCardId", quota.get("primaryCardId")); result.put("primaryTalentId", quota.get("primaryTalentId")); result.put("primaryCardNo", quota.get("primaryCardNo")); result.put("cardLevel", quota.get("cardLevel")); result.put("quotaTotal", quota.get("quotaTotal")); result.put("quotaUsed", quota.get("quotaUsed")); result.put("quotaRemaining", quota.get("quotaRemaining")); result.put("quotaResetCycle", quota.get("quotaResetCycle")); result.put("quotaWindowStart", quota.get("quotaWindowStart")); result.put("packageId", quota.get("packageId")); result.put("packageName", quota.get("packageName")); return result; } private BenefitQuotaService.QuotaSnapshot afterDeduct(BenefitQuotaService.QuotaSnapshot quota, int quantity) { return new BenefitQuotaService.QuotaSnapshot( quota.benefitType(), quota.talentId(), quota.cardId(), quota.cardNo(), quota.cardType(), quota.primaryTalentId(), quota.primaryCardId(), quota.primaryCardNo(), quota.cardLevel(), quota.total(), quota.used() + quantity, Math.max(quota.remaining() - quantity, 0), quota.resetCycle(), quota.detail(), quota.windowStart(), quota.familyCard(), quota.sharedCardIds() ); } private boolean belongsToPrincipal(BenefitOrder order, MerchantPrincipal principal) { Long storeId = order.getStoreId(); return principal.merchantId().equals(order.getMerchantId()) && (storeId == null || principal.storeId().equals(storeId)); } private HotelToken parseHotelToken(String token) { if (token == null || !token.startsWith(BenefitVerificationCodeService.HOTEL_TOKEN_PREFIX + "C")) { return new HotelToken(null, null); } try { int cIndex = token.indexOf('C', BenefitVerificationCodeService.HOTEL_TOKEN_PREFIX.length()); int pIndex = token.indexOf('P', cIndex + 1); int splitIndex = token.indexOf('_', pIndex + 1); if (cIndex < 0 || pIndex < 0 || splitIndex < 0) { return new HotelToken(null, null); } Long cardId = parsePositiveLong(token.substring(cIndex + 1, pIndex)); Long productId = parsePositiveLong(token.substring(pIndex + 1, splitIndex)); return new HotelToken(cardId, productId); } catch (RuntimeException ignored) { return new HotelToken(null, null); } } private Long parsePositiveLong(String value) { long parsed = Long.parseLong(value); return parsed > 0 ? parsed : null; } private boolean startsWithMoviePrefix(String code) { return code != null && code.startsWith(BenefitVerificationCodeService.MOVIE_TOKEN_PREFIX); } private boolean expiredDate(LocalDate value) { return value != null && value.isBefore(LocalDate.now()); } private boolean expiredDateTime(LocalDateTime value) { return value != null && !value.isAfter(LocalDateTime.now()); } private String normalizeIdempotencyKey(String idempotencyKey) { if (idempotencyKey == null || idempotencyKey.isBlank()) { throw new BusinessException(ErrorCode.BAD_REQUEST, "幂等键不能为空"); } String key = idempotencyKey.trim(); if (key.length() > 128) { throw new BusinessException(ErrorCode.BAD_REQUEST, "幂等键长度不能超过限制"); } return key; } private String sourceRemark(String source, String remark) { String cleaned = remark == null ? "" : remark.trim(); return cleaned.isBlank() ? source : source + "; " + cleaned; } private String productRemark(Long productId) { return productId == null ? "" : "; productId=" + productId; } private String trimToLength(String value, int maxLength) { if (value == null) { return null; } String trimmed = value.trim(); return trimmed.length() <= maxLength ? trimmed : trimmed.substring(0, maxLength); } private String upper(String value) { return value == null ? "" : value.trim().toUpperCase(Locale.ROOT); } private int intValue(Object value) { return value instanceof Number number ? number.intValue() : 0; } private long zeroIfNull(Long value) { return value == null ? 0L : value; } private record HotelToken(Long sourceCardId, Long productId) { } private record HotelOrderCodeContext(BenefitOrderDynamicVerificationCode code, BenefitOrder order) { } private record FreeStayCodeContext(BenefitFreeStayVerificationCode code, BenefitFreeStayVoucher voucher) { } }