MarketingService.java

package com.ecommerce.marketing.application.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.ecommerce.marketing.application.exception.MarketingError;
import com.ecommerce.marketing.application.exception.MarketingException;
import com.ecommerce.marketing.application.model.MarketingModels.AppliedBenefit;
import com.ecommerce.marketing.application.model.MarketingModels.BenefitView;
import com.ecommerce.marketing.application.model.MarketingModels.CreateRuleCommand;
import com.ecommerce.marketing.application.model.MarketingModels.DeliveryRegion;
import com.ecommerce.marketing.application.model.MarketingModels.DiscountAllocation;
import com.ecommerce.marketing.application.model.MarketingModels.GrantBenefitCommand;
import com.ecommerce.marketing.application.model.MarketingModels.LockPricingCommand;
import com.ecommerce.marketing.application.model.MarketingModels.PricingLine;
import com.ecommerce.marketing.application.model.MarketingModels.PricingLockView;
import com.ecommerce.marketing.application.model.MarketingModels.PricingPreviewView;
import com.ecommerce.marketing.application.model.MarketingModels.PreviewPricingCommand;
import com.ecommerce.marketing.application.model.MarketingModels.RegionRestriction;
import com.ecommerce.marketing.application.model.MarketingModels.RuleView;
import com.ecommerce.marketing.domain.BenefitStatus;
import com.ecommerce.marketing.domain.BenefitType;
import com.ecommerce.marketing.domain.PricingLockStatus;
import com.ecommerce.marketing.domain.RegionLevel;
import com.ecommerce.marketing.domain.RuleStatus;
import com.ecommerce.marketing.infrastructure.persistence.entity.MarketingRuleEntity;
import com.ecommerce.marketing.infrastructure.persistence.entity.PricingLockAllocationEntity;
import com.ecommerce.marketing.infrastructure.persistence.entity.PricingLockBenefitEntity;
import com.ecommerce.marketing.infrastructure.persistence.entity.PricingLockEntity;
import com.ecommerce.marketing.infrastructure.persistence.entity.RuleRegionEntity;
import com.ecommerce.marketing.infrastructure.persistence.entity.UserBenefitEntity;
import com.ecommerce.marketing.infrastructure.persistence.mapper.MarketingRuleMapper;
import com.ecommerce.marketing.infrastructure.persistence.mapper.PricingLockAllocationMapper;
import com.ecommerce.marketing.infrastructure.persistence.mapper.PricingLockBenefitMapper;
import com.ecommerce.marketing.infrastructure.persistence.mapper.PricingLockMapper;
import com.ecommerce.marketing.infrastructure.persistence.mapper.RuleRegionMapper;
import com.ecommerce.marketing.infrastructure.persistence.mapper.UserBenefitMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

@Service
public class MarketingService {

    private static final BigDecimal ZERO = new BigDecimal("0.00");

    private final MarketingRuleMapper ruleMapper;
    private final RuleRegionMapper regionMapper;
    private final UserBenefitMapper benefitMapper;
    private final PricingLockMapper lockMapper;
    private final PricingLockBenefitMapper lockBenefitMapper;
    private final PricingLockAllocationMapper allocationMapper;
    private final ObjectMapper objectMapper;

    public MarketingService(
            MarketingRuleMapper ruleMapper,
            RuleRegionMapper regionMapper,
            UserBenefitMapper benefitMapper,
            PricingLockMapper lockMapper,
            PricingLockBenefitMapper lockBenefitMapper,
            PricingLockAllocationMapper allocationMapper,
            ObjectMapper objectMapper) {
        this.ruleMapper = ruleMapper;
        this.regionMapper = regionMapper;
        this.benefitMapper = benefitMapper;
        this.lockMapper = lockMapper;
        this.lockBenefitMapper = lockBenefitMapper;
        this.allocationMapper = allocationMapper;
        this.objectMapper = objectMapper;
    }

    @Transactional
    public RuleView createRule(CreateRuleCommand command) {
        BigDecimal threshold = money(command.thresholdAmount(), true);
        BigDecimal discount = money(command.discountAmount(), false);
        if (command.validFrom() == null || command.validUntil() == null
                || !command.validUntil().isAfter(command.validFrom())
                || command.stackOrder() < 0
                || command.regions().stream().map(region -> region.level() + ":" + region.regionCode())
                .distinct().count() != command.regions().size()) {
            throw new MarketingException(MarketingError.INVALID_RULE);
        }
        command.regions().forEach(region -> {
            if (region.level() == null || !isRegionCode(region.regionCode())) {
                throw new MarketingException(MarketingError.INVALID_RULE);
            }
        });
        Instant now = lockMapper.currentTime();
        MarketingRuleEntity rule = new MarketingRuleEntity();
        rule.setId(IdWorker.getId());
        rule.setRuleCode(command.ruleCode().trim());
        rule.setName(command.name().trim());
        rule.setBenefitType(command.benefitType().name());
        rule.setThresholdAmount(threshold);
        rule.setDiscountAmount(discount);
        rule.setStackOrder(command.stackOrder());
        rule.setStatus(RuleStatus.ACTIVE.name());
        rule.setValidFrom(command.validFrom());
        rule.setValidUntil(command.validUntil());
        rule.setVersion(0);
        rule.setCreatedAt(now);
        rule.setUpdatedAt(now);
        ruleMapper.insert(rule);
        for (RegionRestriction restriction : command.regions()) {
            RuleRegionEntity region = new RuleRegionEntity();
            region.setId(IdWorker.getId());
            region.setRuleId(rule.getId());
            region.setRegionLevel(restriction.level().name());
            region.setRegionCode(restriction.regionCode());
            region.setCreatedAt(now);
            regionMapper.insert(region);
        }
        return ruleView(rule);
    }

    @Transactional
    public BenefitView grantBenefit(GrantBenefitCommand command) {
        MarketingRuleEntity rule = ruleMapper.selectByRuleCode(command.ruleCode());
        if (rule == null) {
            throw new MarketingException(MarketingError.RESOURCE_NOT_FOUND);
        }
        Instant now = lockMapper.currentTime();
        long id = IdWorker.getId();
        UserBenefitEntity candidate = new UserBenefitEntity();
        candidate.setId(id);
        candidate.setBenefitNo("BEN" + id);
        candidate.setGrantKey(command.grantKey().trim());
        candidate.setRuleId(rule.getId());
        candidate.setUserId(command.userId());
        candidate.setStatus(BenefitStatus.AVAILABLE.name());
        candidate.setVersion(0);
        candidate.setCreatedAt(now);
        candidate.setUpdatedAt(now);
        benefitMapper.insertOrLockExisting(candidate);
        UserBenefitEntity benefit = benefitMapper.selectByGrantKeyForUpdate(command.userId(), command.grantKey().trim());
        if (benefit == null) {
            throw new MarketingException(MarketingError.CONCURRENT_MODIFICATION);
        }
        if (!benefit.getRuleId().equals(rule.getId())) {
            throw new MarketingException(MarketingError.IDEMPOTENCY_CONFLICT);
        }
        return benefitView(benefit, rule);
    }

    public List<BenefitView> listBenefits(Long userId) {
        return benefitMapper.selectList(new LambdaQueryWrapper<UserBenefitEntity>()
                        .eq(UserBenefitEntity::getUserId, userId)
                        .orderByDesc(UserBenefitEntity::getCreatedAt))
                .stream().map(benefit -> benefitView(benefit, requireRule(benefit.getRuleId()))).toList();
    }

    @Transactional(readOnly = true)
    public PricingPreviewView previewPricing(PreviewPricingCommand command) {
        ValidatedPricing request = validatePricing(command);
        Instant now = lockMapper.currentTime();
        PricingCalculation calculation = calculatePricing(
                request, loadEligibleBenefits(request, now, false));
        return new PricingPreviewView(
                request.originalAmount(),
                calculation.total(BenefitType.COUPON),
                calculation.total(BenefitType.RED_PACKET),
                calculation.total(BenefitType.SUBSIDY),
                calculation.discountAmount(),
                calculation.payableAmount(),
                calculation.appliedBenefits(),
                now);
    }

    @Transactional
    public PricingLockView lockPricing(LockPricingCommand command) {
        ValidatedPricing request = validatePricing(command);
        String requestHash = requestHash(request);
        Instant now = lockMapper.currentTime();
        long id = IdWorker.getId();
        PricingLockEntity candidate = new PricingLockEntity();
        candidate.setId(id);
        candidate.setLockNo("MKT" + id);
        candidate.setOrderNo(request.orderNo());
        candidate.setUserId(request.userId());
        candidate.setRequestHash(requestHash);
        candidate.setOriginalAmount(request.originalAmount());
        candidate.setDiscountAmount(ZERO);
        candidate.setPayableAmount(request.originalAmount());
        candidate.setStatus(PricingLockStatus.LOCKING.name());
        candidate.setVersion(0);
        candidate.setCreatedAt(now);
        candidate.setUpdatedAt(now);
        lockMapper.insertOrLockExisting(candidate);

        PricingLockEntity lock = lockMapper.selectByOrderNoForUpdate(request.orderNo());
        if (lock == null) {
            throw new MarketingException(MarketingError.CONCURRENT_MODIFICATION);
        }
        if (!MessageDigest.isEqual(lock.getRequestHash().getBytes(StandardCharsets.UTF_8),
                requestHash.getBytes(StandardCharsets.UTF_8))) {
            throw new MarketingException(MarketingError.IDEMPOTENCY_CONFLICT);
        }
        if (!PricingLockStatus.LOCKING.name().equals(lock.getStatus())) {
            return lockView(lock);
        }

        PricingCalculation calculation = calculatePricing(
                request, loadEligibleBenefits(request, now, true));
        for (CalculatedBenefit applied : calculation.benefits()) {
            insertAppliedBenefit(lock, applied, now);
            lockUserBenefit(applied.eligible().benefit(), request.orderNo(), now);
        }

        lock.setDiscountAmount(calculation.discountAmount());
        lock.setPayableAmount(calculation.payableAmount());
        lock.setStatus(PricingLockStatus.LOCKED.name());
        lock.setLockedAt(now);
        lock.setUpdatedAt(now);
        requireUpdated(lockMapper.updateById(lock));
        return lockView(lock);
    }

    @Transactional
    public PricingLockView release(String orderNo) {
        return release(requireLock(orderNo), orderNo);
    }

    @Transactional
    public void releaseIfPresent(String orderNo) {
        PricingLockEntity lock = lockMapper.selectByOrderNoForUpdate(orderNo);
        if (lock != null) {
            release(lock, orderNo);
        }
    }

    private PricingLockView release(PricingLockEntity lock, String orderNo) {
        if (PricingLockStatus.RELEASED.name().equals(lock.getStatus())) {
            return lockView(lock);
        }
        if (PricingLockStatus.REDEEMED.name().equals(lock.getStatus())
                || PricingLockStatus.LOCKING.name().equals(lock.getStatus())) {
            throw new MarketingException(MarketingError.INVALID_STATE);
        }
        Instant now = lockMapper.currentTime();
        for (PricingLockBenefitEntity applied : lockBenefits(lock.getId())) {
            UserBenefitEntity benefit = benefitMapper.selectByBenefitNoForUpdate(applied.getBenefitNo());
            if (benefit == null || !BenefitStatus.LOCKED.name().equals(benefit.getStatus())
                    || !orderNo.equals(benefit.getLockedOrderNo())) {
                throw new MarketingException(MarketingError.INVALID_STATE);
            }
            benefit.setStatus(BenefitStatus.AVAILABLE.name());
            benefit.setLockedOrderNo(null);
            benefit.setLockedAt(null);
            benefit.setUpdatedAt(now);
            requireUpdated(benefitMapper.updateById(benefit));
        }
        lock.setStatus(PricingLockStatus.RELEASED.name());
        lock.setReleasedAt(now);
        lock.setUpdatedAt(now);
        requireUpdated(lockMapper.updateById(lock));
        return lockView(lock);
    }

    @Transactional
    public PricingLockView redeem(String orderNo) {
        return redeem(requireLock(orderNo), orderNo);
    }

    @Transactional
    public void redeemIfPresent(String orderNo) {
        PricingLockEntity lock = lockMapper.selectByOrderNoForUpdate(orderNo);
        if (lock != null) {
            redeem(lock, orderNo);
        }
    }

    private PricingLockView redeem(PricingLockEntity lock, String orderNo) {
        if (PricingLockStatus.REDEEMED.name().equals(lock.getStatus())) {
            return lockView(lock);
        }
        if (!PricingLockStatus.LOCKED.name().equals(lock.getStatus())) {
            throw new MarketingException(MarketingError.INVALID_STATE);
        }
        Instant now = lockMapper.currentTime();
        for (PricingLockBenefitEntity applied : lockBenefits(lock.getId())) {
            UserBenefitEntity benefit = benefitMapper.selectByBenefitNoForUpdate(applied.getBenefitNo());
            if (benefit == null || !BenefitStatus.LOCKED.name().equals(benefit.getStatus())
                    || !orderNo.equals(benefit.getLockedOrderNo())) {
                throw new MarketingException(MarketingError.INVALID_STATE);
            }
            benefit.setStatus(BenefitStatus.REDEEMED.name());
            benefit.setLockedOrderNo(null);
            benefit.setLockedAt(null);
            benefit.setRedeemedOrderNo(orderNo);
            benefit.setRedeemedAt(now);
            benefit.setUpdatedAt(now);
            requireUpdated(benefitMapper.updateById(benefit));
        }
        lock.setStatus(PricingLockStatus.REDEEMED.name());
        lock.setRedeemedAt(now);
        lock.setUpdatedAt(now);
        requireUpdated(lockMapper.updateById(lock));
        return lockView(lock);
    }

    public PricingLockView getLock(String orderNo) {
        PricingLockEntity lock = lockMapper.selectOne(new LambdaQueryWrapper<PricingLockEntity>()
                .eq(PricingLockEntity::getOrderNo, orderNo));
        if (lock == null) {
            throw new MarketingException(MarketingError.RESOURCE_NOT_FOUND);
        }
        return lockView(lock);
    }

    private ValidatedPricing validatePricing(LockPricingCommand command) {
        if (command == null || command.orderNo() == null || command.orderNo().isBlank()) {
            throw new MarketingException(MarketingError.INVALID_PRICING_REQUEST);
        }
        return validatePricing(command.orderNo().trim(), command.userId(), command.originalAmount(),
                command.deliveryRegion(), command.lines(), command.benefitNos());
    }

    private ValidatedPricing validatePricing(PreviewPricingCommand command) {
        if (command == null) {
            throw new MarketingException(MarketingError.INVALID_PRICING_REQUEST);
        }
        return validatePricing(null, command.userId(), command.originalAmount(),
                command.deliveryRegion(), command.lines(), command.benefitNos());
    }

    private ValidatedPricing validatePricing(
            String orderNo,
            Long userId,
            BigDecimal originalAmount,
            DeliveryRegion deliveryRegion,
            List<PricingLine> requestedLines,
            List<String> requestedBenefitNos) {
        BigDecimal original = money(originalAmount, false);
        List<String> benefitNos = requestedBenefitNos == null ? List.of() : requestedBenefitNos;
        if (userId == null || userId <= 0 || requestedLines == null || requestedLines.isEmpty()
                || requestedLines.stream().anyMatch(Objects::isNull)
                || requestedLines.size() > 100 || benefitNos.size() > BenefitType.values().length
                || benefitNos.stream().anyMatch(benefitNo ->
                benefitNo == null || benefitNo.isBlank() || benefitNo.length() > 64)
                || new HashSet<>(benefitNos).size() != benefitNos.size()) {
            throw new MarketingException(MarketingError.INVALID_PRICING_REQUEST);
        }
        List<PricingLine> lines = requestedLines.stream().map(line ->
                new PricingLine(line.lineNo(), line.skuId(), money(line.lineAmount(), false))).toList();
        if (lines.stream().anyMatch(line -> line.lineNo() <= 0 || line.skuId() == null || line.skuId() <= 0)
                || lines.stream().map(PricingLine::lineNo).distinct().count() != lines.size()
                || lines.stream().map(PricingLine::lineAmount).reduce(ZERO, BigDecimal::add)
                .compareTo(original) != 0) {
            throw new MarketingException(MarketingError.INVALID_PRICING_REQUEST);
        }
        DeliveryRegion region = deliveryRegion;
        if (region == null || !optionalRegionCode(region.provinceCode())
                || !optionalRegionCode(region.cityCode()) || !optionalRegionCode(region.districtCode())) {
            throw new MarketingException(MarketingError.INVALID_PRICING_REQUEST);
        }
        return new ValidatedPricing(orderNo, userId, original, region,
                lines, benefitNos.stream().map(String::trim).sorted().toList());
    }

    private List<EligibleBenefit> loadEligibleBenefits(
            ValidatedPricing request,
            Instant now,
            boolean lockRows) {
        List<EligibleBenefit> result = new ArrayList<>();
        Set<BenefitType> usedTypes = new HashSet<>();
        for (String benefitNo : request.benefitNos()) {
            UserBenefitEntity benefit = lockRows
                    ? benefitMapper.selectByBenefitNoForUpdate(benefitNo)
                    : benefitMapper.selectOne(new LambdaQueryWrapper<UserBenefitEntity>()
                    .eq(UserBenefitEntity::getBenefitNo, benefitNo));
            if (benefit == null || !benefit.getUserId().equals(request.userId())
                    || !(BenefitStatus.AVAILABLE.name().equals(benefit.getStatus())
                    || (lockRows && BenefitStatus.LOCKED.name().equals(benefit.getStatus())
                    && request.orderNo().equals(benefit.getLockedOrderNo())))) {
                throw new MarketingException(MarketingError.BENEFIT_NOT_ELIGIBLE);
            }
            MarketingRuleEntity rule = requireRule(benefit.getRuleId());
            BenefitType type = BenefitType.valueOf(rule.getBenefitType());
            if (!usedTypes.add(type)) {
                throw new MarketingException(MarketingError.DUPLICATE_BENEFIT_TYPE);
            }
            if (!RuleStatus.ACTIVE.name().equals(rule.getStatus())
                    || now.isBefore(rule.getValidFrom()) || !now.isBefore(rule.getValidUntil())
                    || request.originalAmount().compareTo(rule.getThresholdAmount()) < 0
                    || !regionMatches(rule.getId(), request.deliveryRegion())) {
                throw new MarketingException(MarketingError.BENEFIT_NOT_ELIGIBLE);
            }
            result.add(new EligibleBenefit(benefit, rule));
        }
        return result.stream().sorted(Comparator
                .comparingInt((EligibleBenefit item) -> item.rule().getStackOrder())
                .thenComparing(item -> item.benefit().getBenefitNo())).toList();
    }

    private PricingCalculation calculatePricing(
            ValidatedPricing request,
            List<EligibleBenefit> eligible) {
        Map<Integer, Long> remainingCents = new LinkedHashMap<>();
        Map<Integer, PricingLine> linesByNumber = new HashMap<>();
        request.lines().stream().sorted(Comparator.comparingInt(PricingLine::lineNo))
                .forEach(line -> {
                    remainingCents.put(line.lineNo(), cents(line.lineAmount()));
                    linesByNumber.put(line.lineNo(), line);
                });

        EnumMap<BenefitType, BigDecimal> totals = new EnumMap<>(BenefitType.class);
        List<CalculatedBenefit> applied = new ArrayList<>();
        BigDecimal totalDiscount = ZERO;
        for (EligibleBenefit selected : eligible) {
            long remaining = remainingCents.values().stream().mapToLong(Long::longValue).sum();
            if (remaining <= 0) {
                throw new MarketingException(MarketingError.BENEFIT_NOT_ELIGIBLE);
            }
            long appliedCents = Math.min(cents(selected.rule().getDiscountAmount()), remaining);
            Map<Integer, Long> shares = allocate(appliedCents, remainingCents);
            BigDecimal appliedAmount = amount(appliedCents);
            List<DiscountAllocation> allocations = shares.entrySet().stream()
                    .filter(entry -> entry.getValue() > 0)
                    .map(entry -> {
                        PricingLine line = linesByNumber.get(entry.getKey());
                        return new DiscountAllocation(
                                entry.getKey(),
                                line.skuId(),
                                selected.benefit().getBenefitNo(),
                                selected.rule().getRuleCode(),
                                BenefitType.valueOf(selected.rule().getBenefitType()),
                                amount(entry.getValue()));
                    })
                    .toList();
            AppliedBenefit view = new AppliedBenefit(
                    selected.benefit().getBenefitNo(),
                    selected.rule().getRuleCode(),
                    BenefitType.valueOf(selected.rule().getBenefitType()),
                    appliedAmount,
                    allocations);
            applied.add(new CalculatedBenefit(selected, view));
            totals.merge(view.benefitType(), appliedAmount, BigDecimal::add);
            totalDiscount = totalDiscount.add(appliedAmount);
            shares.forEach((lineNo, share) ->
                    remainingCents.compute(lineNo, (ignored, value) -> value - share));
        }
        return new PricingCalculation(
                totalDiscount,
                request.originalAmount().subtract(totalDiscount),
                totals,
                applied);
    }

    private boolean regionMatches(Long ruleId, DeliveryRegion deliveryRegion) {
        List<RuleRegionEntity> regions = regions(ruleId);
        if (regions.isEmpty()) {
            return true;
        }
        return regions.stream().anyMatch(region -> switch (RegionLevel.valueOf(region.getRegionLevel())) {
            case PROVINCE -> region.getRegionCode().equals(deliveryRegion.provinceCode());
            case CITY -> region.getRegionCode().equals(deliveryRegion.cityCode());
            case DISTRICT -> region.getRegionCode().equals(deliveryRegion.districtCode());
        });
    }

    private Map<Integer, Long> allocate(long discountCents, Map<Integer, Long> capacities) {
        long totalCapacity = capacities.values().stream().mapToLong(Long::longValue).sum();
        if (discountCents <= 0 || discountCents > totalCapacity) {
            throw new MarketingException(MarketingError.INVALID_PRICING_REQUEST);
        }
        BigInteger total = BigInteger.valueOf(totalCapacity);
        List<Share> shares = new ArrayList<>();
        long assigned = 0;
        for (Map.Entry<Integer, Long> entry : capacities.entrySet()) {
            BigInteger numerator = BigInteger.valueOf(discountCents).multiply(BigInteger.valueOf(entry.getValue()));
            BigInteger[] quotientAndRemainder = numerator.divideAndRemainder(total);
            long base = quotientAndRemainder[0].longValueExact();
            shares.add(new Share(entry.getKey(), entry.getValue(), base, quotientAndRemainder[1]));
            assigned += base;
        }
        long remainder = discountCents - assigned;
        shares.sort(Comparator.comparing(Share::remainder).reversed().thenComparingInt(Share::lineNo));
        for (Share share : shares) {
            if (remainder == 0) {
                break;
            }
            if (share.allocated() < share.capacity()) {
                share.increment();
                remainder--;
            }
        }
        if (remainder != 0) {
            throw new MarketingException(MarketingError.INVALID_PRICING_REQUEST);
        }
        shares.sort(Comparator.comparingInt(Share::lineNo));
        Map<Integer, Long> result = new LinkedHashMap<>();
        shares.forEach(share -> result.put(share.lineNo(), share.allocated()));
        return result;
    }

    private void insertAppliedBenefit(
            PricingLockEntity lock,
            CalculatedBenefit applied,
            Instant now) {
        EligibleBenefit selected = applied.eligible();
        AppliedBenefit view = applied.view();
        PricingLockBenefitEntity snapshot = new PricingLockBenefitEntity();
        snapshot.setId(IdWorker.getId());
        snapshot.setLockId(lock.getId());
        snapshot.setUserBenefitId(selected.benefit().getId());
        snapshot.setBenefitNo(view.benefitNo());
        snapshot.setRuleCode(view.ruleCode());
        snapshot.setBenefitType(view.benefitType().name());
        snapshot.setDiscountAmount(view.discountAmount());
        snapshot.setCreatedAt(now);
        lockBenefitMapper.insert(snapshot);
        for (DiscountAllocation source : view.allocations()) {
            PricingLockAllocationEntity allocation = new PricingLockAllocationEntity();
            allocation.setId(IdWorker.getId());
            allocation.setLockId(lock.getId());
            allocation.setBenefitNo(source.benefitNo());
            allocation.setRuleCode(source.ruleCode());
            allocation.setBenefitType(source.benefitType().name());
            allocation.setLineNo(source.lineNo());
            allocation.setSkuId(source.skuId());
            allocation.setDiscountAmount(source.discountAmount());
            allocation.setCreatedAt(now);
            allocationMapper.insert(allocation);
        }
    }

    private void lockUserBenefit(UserBenefitEntity benefit, String orderNo, Instant now) {
        benefit.setStatus(BenefitStatus.LOCKED.name());
        benefit.setLockedOrderNo(orderNo);
        benefit.setLockedAt(now);
        benefit.setUpdatedAt(now);
        requireUpdated(benefitMapper.updateById(benefit));
    }

    private PricingLockEntity requireLock(String orderNo) {
        PricingLockEntity lock = lockMapper.selectByOrderNoForUpdate(orderNo);
        if (lock == null) {
            throw new MarketingException(MarketingError.RESOURCE_NOT_FOUND);
        }
        return lock;
    }

    private MarketingRuleEntity requireRule(Long ruleId) {
        MarketingRuleEntity rule = ruleMapper.selectById(ruleId);
        if (rule == null) {
            throw new MarketingException(MarketingError.RESOURCE_NOT_FOUND);
        }
        return rule;
    }

    private RuleView ruleView(MarketingRuleEntity rule) {
        return new RuleView(rule.getRuleCode(), rule.getName(), BenefitType.valueOf(rule.getBenefitType()),
                rule.getThresholdAmount(), rule.getDiscountAmount(), rule.getStackOrder(), rule.getStatus(),
                rule.getValidFrom(), rule.getValidUntil(), regionViews(rule.getId()), rule.getVersion());
    }

    private BenefitView benefitView(UserBenefitEntity benefit, MarketingRuleEntity rule) {
        return new BenefitView(benefit.getBenefitNo(), benefit.getUserId(), rule.getRuleCode(),
                BenefitType.valueOf(rule.getBenefitType()), rule.getThresholdAmount(), rule.getDiscountAmount(),
                benefit.getStatus(), benefit.getLockedOrderNo(), benefit.getRedeemedOrderNo(),
                rule.getValidFrom(), rule.getValidUntil(), regionViews(rule.getId()));
    }

    private PricingLockView lockView(PricingLockEntity lock) {
        List<PricingLockAllocationEntity> allocations = allocationMapper.selectList(
                new LambdaQueryWrapper<PricingLockAllocationEntity>()
                        .eq(PricingLockAllocationEntity::getLockId, lock.getId())
                        .orderByAsc(PricingLockAllocationEntity::getLineNo));
        Map<String, List<DiscountAllocation>> byBenefit = new HashMap<>();
        for (PricingLockAllocationEntity allocation : allocations) {
            byBenefit.computeIfAbsent(allocation.getBenefitNo(), ignored -> new ArrayList<>()).add(
                    new DiscountAllocation(allocation.getLineNo(), allocation.getSkuId(),
                            allocation.getBenefitNo(), allocation.getRuleCode(),
                            BenefitType.valueOf(allocation.getBenefitType()), allocation.getDiscountAmount()));
        }
        EnumMap<BenefitType, BigDecimal> totals = new EnumMap<>(BenefitType.class);
        List<AppliedBenefit> applied = lockBenefits(lock.getId()).stream().map(snapshot -> {
            BenefitType type = BenefitType.valueOf(snapshot.getBenefitType());
            totals.merge(type, snapshot.getDiscountAmount(), BigDecimal::add);
            return new AppliedBenefit(snapshot.getBenefitNo(), snapshot.getRuleCode(), type,
                    snapshot.getDiscountAmount(), byBenefit.getOrDefault(snapshot.getBenefitNo(), List.of()));
        }).toList();
        return new PricingLockView(lock.getLockNo(), lock.getOrderNo(), lock.getUserId(),
                lock.getOriginalAmount(), totals.getOrDefault(BenefitType.COUPON, ZERO),
                totals.getOrDefault(BenefitType.RED_PACKET, ZERO),
                totals.getOrDefault(BenefitType.SUBSIDY, ZERO), lock.getDiscountAmount(),
                lock.getPayableAmount(), lock.getStatus(), applied, lock.getLockedAt(),
                lock.getReleasedAt(), lock.getRedeemedAt(), lock.getVersion());
    }

    private List<PricingLockBenefitEntity> lockBenefits(Long lockId) {
        return lockBenefitMapper.selectList(new LambdaQueryWrapper<PricingLockBenefitEntity>()
                .eq(PricingLockBenefitEntity::getLockId, lockId)
                .orderByAsc(PricingLockBenefitEntity::getId));
    }

    private List<RuleRegionEntity> regions(Long ruleId) {
        return regionMapper.selectList(new LambdaQueryWrapper<RuleRegionEntity>()
                .eq(RuleRegionEntity::getRuleId, ruleId)
                .orderByAsc(RuleRegionEntity::getRegionLevel)
                .orderByAsc(RuleRegionEntity::getRegionCode));
    }

    private List<RegionRestriction> regionViews(Long ruleId) {
        return regions(ruleId).stream().map(region ->
                new RegionRestriction(RegionLevel.valueOf(region.getRegionLevel()), region.getRegionCode())).toList();
    }

    private String requestHash(ValidatedPricing request) {
        try {
            byte[] canonical = objectMapper.writeValueAsBytes(request);
            return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(canonical));
        } catch (JsonProcessingException | NoSuchAlgorithmException exception) {
            throw new IllegalStateException("Unable to hash pricing request", exception);
        }
    }

    private BigDecimal money(BigDecimal value, boolean allowZero) {
        if (value == null) {
            throw new MarketingException(MarketingError.INVALID_PRICING_REQUEST);
        }
        try {
            BigDecimal normalized = value.setScale(2, RoundingMode.UNNECESSARY);
            if (allowZero ? normalized.signum() < 0 : normalized.signum() <= 0) {
                throw new MarketingException(MarketingError.INVALID_PRICING_REQUEST);
            }
            return normalized;
        } catch (ArithmeticException exception) {
            throw new MarketingException(MarketingError.INVALID_PRICING_REQUEST, exception);
        }
    }

    private long cents(BigDecimal amount) {
        try {
            return amount.movePointRight(2).longValueExact();
        } catch (ArithmeticException exception) {
            throw new MarketingException(MarketingError.INVALID_PRICING_REQUEST, exception);
        }
    }

    private BigDecimal amount(long cents) {
        return BigDecimal.valueOf(cents, 2);
    }

    private boolean isRegionCode(String code) {
        return code != null && code.matches("\\d{6}");
    }

    private boolean optionalRegionCode(String code) {
        return code == null || isRegionCode(code);
    }

    private void requireUpdated(int rows) {
        if (rows != 1) {
            throw new MarketingException(MarketingError.CONCURRENT_MODIFICATION);
        }
    }

    private record EligibleBenefit(UserBenefitEntity benefit, MarketingRuleEntity rule) {
    }

    private record CalculatedBenefit(EligibleBenefit eligible, AppliedBenefit view) {
    }

    private record PricingCalculation(
            BigDecimal discountAmount,
            BigDecimal payableAmount,
            EnumMap<BenefitType, BigDecimal> totals,
            List<CalculatedBenefit> benefits
    ) {
        private BigDecimal total(BenefitType type) {
            return totals.getOrDefault(type, ZERO);
        }

        private List<AppliedBenefit> appliedBenefits() {
            return benefits.stream().map(CalculatedBenefit::view).toList();
        }
    }

    private record ValidatedPricing(
            String orderNo,
            Long userId,
            BigDecimal originalAmount,
            DeliveryRegion deliveryRegion,
            List<PricingLine> lines,
            List<String> benefitNos
    ) {
    }

    private static final class Share {
        private final int lineNo;
        private final long capacity;
        private long allocated;
        private final BigInteger remainder;

        private Share(int lineNo, long capacity, long allocated, BigInteger remainder) {
            this.lineNo = lineNo;
            this.capacity = capacity;
            this.allocated = allocated;
            this.remainder = remainder;
        }

        int lineNo() {
            return lineNo;
        }

        long capacity() {
            return capacity;
        }

        long allocated() {
            return allocated;
        }

        BigInteger remainder() {
            return remainder;
        }

        void increment() {
            allocated++;
        }
    }
}