Subversion Repositories SmartDukaan

Rev

Blame | Last modification | View Log | RSS feed

package com.smartdukaan.cron.offercircular;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Parses the "Offer Description" cell into a value per payment mode.
 *
 * One circular row routinely carries two different values, and even two different
 * calculation types:
 *   "10% cashback up to 2500 on CC EMI & Rs 1500 on CC Full Swipe"
 *     -> CC_EMI        = PERCENT 10% capped at 2500
 *        CC_FULL_SWIPE = FLAT 1500
 * which is exactly why the amount belongs on (offer, mode) and never on the offer.
 */
public final class BenefitParser {

    public static final String CALC_FLAT = "FLAT";
    public static final String CALC_PERCENT = "PERCENT";

    /** Order matters: the compound forms must be tested before the simple ones. */
    private static final Object[][] MODE_PATTERNS = {
        {"CC\\s*/\\s*DC\\s*FULL\\s*SWIPE", new String[]{"CC_FULL_SWIPE", "DC_FULL_SWIPE"}},
        {"CC\\s*/\\s*DC\\s*EMI",           new String[]{"CC_EMI", "DC_EMI"}},
        {"DC\\s*EMI",                      new String[]{"DC_EMI"}},
        {"CC\\s*EMI",                      new String[]{"CC_EMI"}},
        {"CCEMI",                          new String[]{"CC_EMI"}},
        {"DC\\s*FULL\\s*SWIPE",            new String[]{"DC_FULL_SWIPE"}},
        {"CC\\s*FULL\\s*SWIPE",            new String[]{"CC_FULL_SWIPE"}},
        {"CC\\s*REG",                       new String[]{"CC_FULL_SWIPE"}},
        {"FULL\\s*SWIPE",                  new String[]{"CC_FULL_SWIPE"}},
        {"\\bFS\\b",                       new String[]{"CC_FULL_SWIPE"}},
        {"\\bUPI\\b",                      new String[]{"UPI"}},
        {"CARDLESS",                       new String[]{"CARDLESS_EMI"}},
    };

    private static final List<Pattern> MODE_REGEX = new ArrayList<>();
    static {
        for (Object[] row : MODE_PATTERNS) {
            MODE_REGEX.add(Pattern.compile((String) row[0], Pattern.CASE_INSENSITIVE));
        }
    }

    private static final Pattern PERCENT = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*%");
    private static final Pattern CAP =
            Pattern.compile("(?:up\\s*to|upto)\\s*(?:rs\\.?\\s*)?(\\d[\\d,]*)", Pattern.CASE_INSENSITIVE);
    private static final Pattern FLAT =
            Pattern.compile("(?:rs\\.?\\s*)?(\\d[\\d,]{2,})", Pattern.CASE_INSENSITIVE);
    private static final Pattern EMI_ONLY =
            Pattern.compile("only\\s*nc\\s*emi", Pattern.CASE_INSENSITIVE);

    public static final class Benefit {
        private final String calcType;
        private final BigDecimal flatAmount;
        private final BigDecimal percent;
        private final BigDecimal maxAmount;
        Benefit(String calcType, BigDecimal flatAmount, BigDecimal percent, BigDecimal maxAmount) {
            this.calcType = calcType;
            this.flatAmount = flatAmount;
            this.percent = percent;
            this.maxAmount = maxAmount;
        }
        public String getCalcType() { return calcType; }
        public BigDecimal getFlatAmount() { return flatAmount; }
        public BigDecimal getPercent() { return percent; }
        public BigDecimal getMaxAmount() { return maxAmount; }
        @Override public String toString() {
            return CALC_PERCENT.equals(calcType)
                    ? percent + "%" + (maxAmount == null ? "" : " cap " + maxAmount)
                    : String.valueOf(flatAmount);
        }
    }

    public static final class Result {
        /** payment mode -> value; insertion-ordered */
        private final Map<String, Benefit> benefits = new LinkedHashMap<>();
        private String warning;
        public Map<String, Benefit> getBenefits() { return benefits; }
        public String getWarning() { return warning; }
    }

    private BenefitParser() { }

    private static List<String> findModes(String text) {
        Set<String> ordered = new LinkedHashSet<>();
        for (int i = 0; i < MODE_REGEX.size(); i++) {
            if (MODE_REGEX.get(i).matcher(text).find()) {
                ordered.addAll(Arrays.asList((String[]) MODE_PATTERNS[i][1]));
            }
        }
        return new ArrayList<>(ordered);
    }

    private static BigDecimal num(String raw) {
        return new BigDecimal(raw.replace(",", ""));
    }

    private static Benefit findValue(String text) {
        Matcher pct = PERCENT.matcher(text);
        if (pct.find()) {
            Matcher cap = CAP.matcher(text);
            return new Benefit(CALC_PERCENT, null, new BigDecimal(pct.group(1)),
                    cap.find() ? num(cap.group(1)) : null);
        }
        Matcher flat = FLAT.matcher(text);
        if (flat.find()) {
            return new Benefit(CALC_FLAT, num(flat.group(1)), null, null);
        }
        return null;
    }

    public static Result parse(String cell) {
        Result result = new Result();
        if (cell == null) {
            result.warning = "blank";
            return result;
        }
        String text = cell.trim();
        if (text.isEmpty() || "-".equals(text) || "NA".equals(text) || "N/A".equals(text)) {
            result.warning = "blank";
            return result;
        }
        if (EMI_ONLY.matcher(text).find()) {
            result.warning = "emi-only-no-cashback";
            return result;
        }

        // '&' separates the per-mode clauses: "Rs.4000 on CC EMI & Rs.3000 CC Full Swipe"
        List<Benefit> values = new ArrayList<>();
        List<List<String>> modesPerSegment = new ArrayList<>();
        Benefit carried = null;
        for (String segment : text.split("\\s*&\\s*")) {
            if (segment.trim().isEmpty()) {
                continue;
            }
            Benefit value = findValue(segment);
            if (value == null) {
                value = carried;            // "…& UPI" inherits the preceding amount
            } else {
                carried = value;
            }
            if (value == null) {
                continue;
            }
            List<String> modes = findModes(segment);
            values.add(value);
            modesPerSegment.add(modes.isEmpty() ? null : modes);
        }
        if (values.isEmpty()) {
            result.warning = "no-value-found";
            return result;
        }

        // A clause naming no mode of its own applies to whatever modes the whole cell
        // mentions and no other clause has claimed; ANY when the cell names none.
        List<String> allModes = findModes(text);
        if (allModes.isEmpty()) {
            allModes = new ArrayList<>(Arrays.asList("ANY"));
        }
        Set<String> claimed = new LinkedHashSet<>();
        for (List<String> modes : modesPerSegment) {
            if (modes != null) {
                claimed.addAll(modes);
            }
        }
        for (int i = 0; i < values.size(); i++) {
            List<String> targets = modesPerSegment.get(i);
            if (targets == null) {
                targets = new ArrayList<>();
                for (String mode : allModes) {
                    if (!claimed.contains(mode)) {
                        targets.add(mode);
                    }
                }
                if (targets.isEmpty()) {
                    targets = Arrays.asList("ANY");
                }
            }
            for (String mode : targets) {
                // first clause wins for a given mode
                result.benefits.putIfAbsent(mode, values.get(i));
            }
        }
        return result;
    }
}