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();}}