Subversion Repositories SmartDukaan

Rev

Blame | Last modification | View Log | RSS feed

package com.smartdukaan.cron.offercircular;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Turns the "# ..." notes printed under each page's table into structured rules.
 *
 * These are real eligibility rules, not decoration, and they are PAGE-scoped: pages 2
 * and 3 of the Aug'26 circular read "HDFC, AU & SBI" where every other page reads
 * "HDFC & SBI", so AU's 3-month exclusion applies only to the Oppo and Xiaomi pages.
 * Recording them document-wide would over-apply AU; ignoring them loses the rule
 * entirely.
 *
 * Bank names are resolved with {@link BankTextParser} so footnotes get exactly the
 * same aliasing as the bank columns.
 */
public final class FootnoteParser {

    public static final String TYPE_TENURE_BANK_EXCLUSION = "TENURE_BANK_EXCLUSION";
    public static final String TYPE_BANK_RESTRICTION = "BANK_RESTRICTION";
    public static final String TYPE_BRAND_RESTRICTION = "BRAND_RESTRICTION";
    public static final String TYPE_NOTE = "NOTE";

    /** "Additional cashback not applicable on 3 Month tenure of HDFC, AU & SBI" */
    private static final Pattern TENURE_EXCLUSION = Pattern.compile(
            "additional\\s+cashback\\s+not\\s+applicable.*?(\\d+)\\s*month\\s*tenure\\s*(?:of)?\\s*(.*)$",
            Pattern.CASE_INSENSITIVE);

    /** "Instant cashback applicable on HDFC, ICICI, OneCard, ..." */
    private static final Pattern BANK_ALLOWLIST = Pattern.compile(
            "instant\\s+cashback\\s+applicable\\s+on\\s+(.*)$", Pattern.CASE_INSENSITIVE);

    /** "SBI Instant Cashback applicable on selective Brands Only." */
    private static final Pattern BRAND_RESTRICTION = Pattern.compile(
            "^(.*?)\\s+instant\\s+cashback\\s+applicable\\s+on\\s+selective\\s+brands",
            Pattern.CASE_INSENSITIVE);

    /** One row destined for offers.offer_condition. */
    public static final class Condition {
        private final int pageNo;
        private final String conditionType;
        private final Integer bankId;
        private final Integer tenureMonths;
        private final String noteText;
        private final String rawText;

        Condition(int pageNo, String conditionType, Integer bankId, Integer tenureMonths,
                  String noteText, String rawText) {
            this.pageNo = pageNo;
            this.conditionType = conditionType;
            this.bankId = bankId;
            this.tenureMonths = tenureMonths;
            this.noteText = noteText;
            this.rawText = rawText;
        }
        public int getPageNo() { return pageNo; }
        public String getConditionType() { return conditionType; }
        public Integer getBankId() { return bankId; }
        public Integer getTenureMonths() { return tenureMonths; }
        public String getNoteText() { return noteText; }
        public String getRawText() { return rawText; }
        @Override public String toString() {
            return conditionType + " p" + pageNo + " bank=" + bankId + " months=" + tenureMonths;
        }
    }

    public static final class Result {
        private final List<Condition> conditions = new ArrayList<>();
        private final List<String> warnings = new ArrayList<>();
        public List<Condition> getConditions() { return conditions; }
        public List<String> getWarnings() { return warnings; }
    }

    private FootnoteParser() { }

    public static Result parse(List<CircularExtractor.Footnote> footnotes,
                               Map<String, Integer> bankAliases) {
        Result result = new Result();
        for (CircularExtractor.Footnote footnote : footnotes) {
            int page = footnote.getPageNo();
            String text = footnote.getText().trim();

            Matcher tenure = TENURE_EXCLUSION.matcher(text);
            if (tenure.find()) {
                int months = Integer.parseInt(tenure.group(1));
                List<Integer> banks = banks(tenure.group(2), bankAliases, result, text);
                for (Integer bankId : banks) {
                    result.conditions.add(new Condition(page, TYPE_TENURE_BANK_EXCLUSION,
                            bankId, months, text, text));
                }
                if (banks.isEmpty()) {
                    result.conditions.add(new Condition(page, TYPE_TENURE_BANK_EXCLUSION,
                            null, months, text, text));
                }
                continue;
            }

            Matcher brand = BRAND_RESTRICTION.matcher(text);
            if (brand.find()) {
                List<Integer> banks = banks(brand.group(1), bankAliases, result, text);
                result.conditions.add(new Condition(page, TYPE_BRAND_RESTRICTION,
                        banks.isEmpty() ? null : banks.get(0), null, text, text));
                continue;
            }

            Matcher allow = BANK_ALLOWLIST.matcher(text);
            if (allow.find()) {
                List<Integer> banks = banks(allow.group(1), bankAliases, result, text);
                for (Integer bankId : banks) {
                    result.conditions.add(new Condition(page, TYPE_BANK_RESTRICTION,
                            bankId, null, text, text));
                }
                if (banks.isEmpty()) {
                    result.conditions.add(new Condition(page, TYPE_BANK_RESTRICTION,
                            null, null, text, text));
                }
                continue;
            }

            // An unrecognised footnote is kept verbatim rather than discarded - the
            // OEMs add new ones, and losing a rule silently is the worst outcome.
            result.warnings.add("unclassified footnote (page " + page + "): " + text);
            result.conditions.add(new Condition(page, TYPE_NOTE, null, null, text, text));
        }
        return result;
    }

    private static List<Integer> banks(String fragment, Map<String, Integer> bankAliases,
                                       Result result, String wholeNote) {
        BankTextParser.Result parsed = BankTextParser.parse(fragment, bankAliases);
        for (String unresolved : parsed.getUnresolved()) {
            result.warnings.add("footnote names an unmapped bank '" + unresolved
                    + "' in: " + wholeNote);
        }
        return parsed.getBankIds();
    }
}