Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
37330 amit 1
package com.smartdukaan.cron.offercircular;
2
 
3
import java.util.ArrayList;
4
import java.util.List;
5
import java.util.Map;
6
import java.util.regex.Matcher;
7
import java.util.regex.Pattern;
8
 
9
/**
10
 * Turns the "# ..." notes printed under each page's table into structured rules.
11
 *
12
 * These are real eligibility rules, not decoration, and they are PAGE-scoped: pages 2
13
 * and 3 of the Aug'26 circular read "HDFC, AU & SBI" where every other page reads
14
 * "HDFC & SBI", so AU's 3-month exclusion applies only to the Oppo and Xiaomi pages.
15
 * Recording them document-wide would over-apply AU; ignoring them loses the rule
16
 * entirely.
17
 *
18
 * Bank names are resolved with {@link BankTextParser} so footnotes get exactly the
19
 * same aliasing as the bank columns.
20
 */
21
public final class FootnoteParser {
22
 
23
    public static final String TYPE_TENURE_BANK_EXCLUSION = "TENURE_BANK_EXCLUSION";
24
    public static final String TYPE_BANK_RESTRICTION = "BANK_RESTRICTION";
25
    public static final String TYPE_BRAND_RESTRICTION = "BRAND_RESTRICTION";
26
    public static final String TYPE_NOTE = "NOTE";
27
 
28
    /** "Additional cashback not applicable on 3 Month tenure of HDFC, AU & SBI" */
29
    private static final Pattern TENURE_EXCLUSION = Pattern.compile(
30
            "additional\\s+cashback\\s+not\\s+applicable.*?(\\d+)\\s*month\\s*tenure\\s*(?:of)?\\s*(.*)$",
31
            Pattern.CASE_INSENSITIVE);
32
 
33
    /** "Instant cashback applicable on HDFC, ICICI, OneCard, ..." */
34
    private static final Pattern BANK_ALLOWLIST = Pattern.compile(
35
            "instant\\s+cashback\\s+applicable\\s+on\\s+(.*)$", Pattern.CASE_INSENSITIVE);
36
 
37
    /** "SBI Instant Cashback applicable on selective Brands Only." */
38
    private static final Pattern BRAND_RESTRICTION = Pattern.compile(
39
            "^(.*?)\\s+instant\\s+cashback\\s+applicable\\s+on\\s+selective\\s+brands",
40
            Pattern.CASE_INSENSITIVE);
41
 
42
    /** One row destined for offers.offer_condition. */
43
    public static final class Condition {
44
        private final int pageNo;
45
        private final String conditionType;
46
        private final Integer bankId;
47
        private final Integer tenureMonths;
48
        private final String noteText;
49
        private final String rawText;
50
 
51
        Condition(int pageNo, String conditionType, Integer bankId, Integer tenureMonths,
52
                  String noteText, String rawText) {
53
            this.pageNo = pageNo;
54
            this.conditionType = conditionType;
55
            this.bankId = bankId;
56
            this.tenureMonths = tenureMonths;
57
            this.noteText = noteText;
58
            this.rawText = rawText;
59
        }
60
        public int getPageNo() { return pageNo; }
61
        public String getConditionType() { return conditionType; }
62
        public Integer getBankId() { return bankId; }
63
        public Integer getTenureMonths() { return tenureMonths; }
64
        public String getNoteText() { return noteText; }
65
        public String getRawText() { return rawText; }
66
        @Override public String toString() {
67
            return conditionType + " p" + pageNo + " bank=" + bankId + " months=" + tenureMonths;
68
        }
69
    }
70
 
71
    public static final class Result {
72
        private final List<Condition> conditions = new ArrayList<>();
73
        private final List<String> warnings = new ArrayList<>();
74
        public List<Condition> getConditions() { return conditions; }
75
        public List<String> getWarnings() { return warnings; }
76
    }
77
 
78
    private FootnoteParser() { }
79
 
80
    public static Result parse(List<CircularExtractor.Footnote> footnotes,
81
                               Map<String, Integer> bankAliases) {
82
        Result result = new Result();
83
        for (CircularExtractor.Footnote footnote : footnotes) {
84
            int page = footnote.getPageNo();
85
            String text = footnote.getText().trim();
86
 
87
            Matcher tenure = TENURE_EXCLUSION.matcher(text);
88
            if (tenure.find()) {
89
                int months = Integer.parseInt(tenure.group(1));
90
                List<Integer> banks = banks(tenure.group(2), bankAliases, result, text);
91
                for (Integer bankId : banks) {
92
                    result.conditions.add(new Condition(page, TYPE_TENURE_BANK_EXCLUSION,
93
                            bankId, months, text, text));
94
                }
95
                if (banks.isEmpty()) {
96
                    result.conditions.add(new Condition(page, TYPE_TENURE_BANK_EXCLUSION,
97
                            null, months, text, text));
98
                }
99
                continue;
100
            }
101
 
102
            Matcher brand = BRAND_RESTRICTION.matcher(text);
103
            if (brand.find()) {
104
                List<Integer> banks = banks(brand.group(1), bankAliases, result, text);
105
                result.conditions.add(new Condition(page, TYPE_BRAND_RESTRICTION,
106
                        banks.isEmpty() ? null : banks.get(0), null, text, text));
107
                continue;
108
            }
109
 
110
            Matcher allow = BANK_ALLOWLIST.matcher(text);
111
            if (allow.find()) {
112
                List<Integer> banks = banks(allow.group(1), bankAliases, result, text);
113
                for (Integer bankId : banks) {
114
                    result.conditions.add(new Condition(page, TYPE_BANK_RESTRICTION,
115
                            bankId, null, text, text));
116
                }
117
                if (banks.isEmpty()) {
118
                    result.conditions.add(new Condition(page, TYPE_BANK_RESTRICTION,
119
                            null, null, text, text));
120
                }
121
                continue;
122
            }
123
 
124
            // An unrecognised footnote is kept verbatim rather than discarded - the
125
            // OEMs add new ones, and losing a rule silently is the worst outcome.
126
            result.warnings.add("unclassified footnote (page " + page + "): " + text);
127
            result.conditions.add(new Condition(page, TYPE_NOTE, null, null, text, text));
128
        }
129
        return result;
130
    }
131
 
132
    private static List<Integer> banks(String fragment, Map<String, Integer> bankAliases,
133
                                       Result result, String wholeNote) {
134
        BankTextParser.Result parsed = BankTextParser.parse(fragment, bankAliases);
135
        for (String unresolved : parsed.getUnresolved()) {
136
            result.warnings.add("footnote names an unmapped bank '" + unresolved
137
                    + "' in: " + wholeNote);
138
        }
139
        return parsed.getBankIds();
140
    }
141
}