Subversion Repositories SmartDukaan

Rev

Rev 36999 | Blame | Compare with Previous | Last modification | View Log | RSS feed

package com.spice.profitmandi.service.pricing;

import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
import com.spice.profitmandi.common.model.GrnPendingDataModel;
import com.spice.profitmandi.common.util.FormattingUtils;
import com.spice.profitmandi.dao.entity.fofo.InventoryItem;
import com.spice.profitmandi.dao.entity.transaction.PriceDrop;
import com.spice.profitmandi.dao.entity.transaction.PriceDropIMEI;
import com.spice.profitmandi.dao.enumuration.transaction.PriceDropImeiStatus;
import com.spice.profitmandi.dao.repository.fofo.InventoryItemRepository;
import com.spice.profitmandi.dao.repository.transaction.OrderRepository;
import com.spice.profitmandi.dao.repository.transaction.PriceDropIMEIRepository;
import com.spice.profitmandi.dao.repository.transaction.PriceDropRepository;
import com.spice.profitmandi.service.inventory.InventoryService;
import com.spice.profitmandi.service.scheme.SchemeService;
import com.spice.profitmandi.service.wallet.WalletService;
import in.shop2020.model.v1.order.WalletReferenceType;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.text.MessageFormat;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Shared unit of work for price-hike deduction. {@link #deductPartnerUnits} (no transaction of its own —
 * runs in the caller's) is used by both the GRN hook (inside the GRN transaction) and the on-demand
 * endpoint. The endpoint path additionally uses the REQUIRES_NEW methods here so each retailer is isolated.
 */
@Component
public class PriceHikeWorker {

    private static final Logger LOGGER = LogManager.getLogger(PriceHikeWorker.class);

    @Autowired
    private PriceDropRepository priceDropRepository;
    @Autowired
    private PriceDropIMEIRepository priceDropIMEIRepository;
    @Autowired
    private InventoryItemRepository inventoryItemRepository;
    @Autowired
    private OrderRepository orderRepository;
    @Autowired
    private WalletService walletService;
    @Autowired
    private InventoryService inventoryService;
    @Autowired
    private SchemeService schemeService;

    /**
     * Idempotent, for one retailer's in-hand items against one hike. Runs in the caller's transaction.
     * Creates APPROVED rows, raises the item's price-drop accumulator so scheme margin follows the new
     * (hiked) price, debits the wallet, and — when {@code recomputeSchemes} is true — reverses and
     * reprocesses schemes as the price-drop flow does. Serials already recorded for this hike are skipped.
     *
     * @param recomputeSchemes false on the GRN path (the item has no SchemeInOut yet and the GRN flow's
     *                         own processSchemeIn runs right after, on the raised basis); true on the
     *                         endpoint path (the item is already GRN'd and has schemes to reprocess).
     */
    public void deductPartnerUnits(PriceDrop hike, int retailerId, List<InventoryItem> items, boolean recomputeSchemes)
            throws ProfitMandiBusinessException {
        if (items == null || items.isEmpty()) {
            return;
        }
        Set<String> existing = priceDropIMEIRepository.selectByPriceDropId(hike.getId()).stream()
                .filter(x -> x.getPartnerId() == retailerId)
                .map(PriceDropIMEI::getImei).collect(Collectors.toSet());
        List<InventoryItem> fresh = items.stream()
                .filter(ii -> ii.getSerialNumber() != null && !existing.contains(ii.getSerialNumber()))
                .collect(Collectors.toList());
        if (fresh.isEmpty()) {
            return;
        }
        LocalDateTime now = LocalDateTime.now();
        for (InventoryItem ii : fresh) {
            PriceDropIMEI row = new PriceDropIMEI();
            row.setPriceDropId(hike.getId());
            row.setPartnerId(retailerId);
            row.setImei(ii.getSerialNumber());
            row.setStatus(PriceDropImeiStatus.APPROVED);
            row.setInventoryItemId(ii.getId());
            row.setCreditTimestamp(now);
            row.setUpdateTimestamp(now);
            priceDropIMEIRepository.persist(row);
        }
        // Raise the item's cost basis (amount is negative), so scheme/offer margins track the new price.
        inventoryService.updatePriceDrop(fresh, hike.getAmount());
        if (recomputeSchemes) {
            String reversalReason = MessageFormat.format(
                    "Scheme reversal for Price Hike of Rs.{0} on {1}. Total {2} item(s)",
                    hike.getAmount(), FormattingUtils.formatDate(hike.getAffectedOn()), fresh.size());
            schemeService.reverseSchemes(fresh, hike.getId(), reversalReason);
            schemeService.processSchemeIn(fresh);
        }
        // amount is negative for a hike, so amount * count reduces the wallet balance (a debit).
        String reason = MessageFormat.format("Price Hike recovery of Rs.{0} per unit. Total {1} item(s)",
                FormattingUtils.formatDecimal(Math.abs(hike.getDropAmount())), fresh.size());
        walletService.addAmountToWallet(retailerId, hike.getId(), WalletReferenceType.PRICE_DROP, reason,
                hike.getAmount() * fresh.size(), hike.getAffectedOn());
        LOGGER.info("Price hike {} — debited retailer {} for {} unit(s), recomputeSchemes={}",
                hike.getId(), retailerId, fresh.size(), recomputeSchemes);
    }

    /** Endpoint path: eligible (gap-billed) serials for a hike, grouped by retailer. Own transaction. */
    @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
    public Map<Integer, List<String>> loadEligibleUnits(int priceDropId) throws ProfitMandiBusinessException {
        PriceDrop pd = priceDropRepository.selectById(priceDropId);
        if (pd == null || pd.getAmount() >= 0) {
            return Collections.emptyMap();
        }
        List<GrnPendingDataModel> billed = orderRepository.selectAllOrdersByBilledDateCatalogId(
                pd.getCatalogItemId(), pd.getAffectedOn(), pd.getCreatedOn());
        return billed.stream()
                .filter(x -> x.getSerialNumber() != null && x.getRetailerId() > 0)
                .collect(Collectors.groupingBy(GrnPendingDataModel::getRetailerId,
                        Collectors.mapping(GrnPendingDataModel::getSerialNumber, Collectors.toList())));
    }

    /**
     * Endpoint path: one retailer, its own transaction. Processes only serials already GRN'd (present in
     * inventory) — full scheme reprocess applies to them; not-yet-GRN'd serials are left for the GRN hook.
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void deductForRetailer(int priceDropId, int retailerId, List<String> serials)
            throws ProfitMandiBusinessException {
        PriceDrop hike = priceDropRepository.selectById(priceDropId);
        if (hike == null || hike.getAmount() >= 0) {
            return;
        }
        List<InventoryItem> items = inventoryItemRepository
                .selectByFofoIdSerialNumbers(retailerId, new HashSet<>(serials), false);
        deductPartnerUnits(hike, retailerId, items, true);
    }
}