Subversion Repositories SmartDukaan

Rev

Rev 37337 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
37337 amit 1
package com.spice.profitmandi.web.offercircular;
2
 
3
import static org.junit.Assert.assertEquals;
4
 
5
import java.util.Arrays;
6
import java.util.LinkedHashSet;
7
import java.util.Set;
8
 
9
import org.junit.Test;
10
 
11
/**
12
 * Guards the product-name parsing, which decides which SKU an offer's money lands on.
13
 *
14
 * Every case here corresponds to a real mis-parse found against the Aug'26 circular.
15
 * They all share one root cause worth remembering: **characters and digits belonging to
16
 * the model name being consumed as memory or stripped as punctuation.** If a change
17
 * here breaks a test, the fix is almost certainly to add a word-boundary guard rather
18
 * than to relax the expectation.
19
 */
20
public class ProductNamesTest {
21
 
22
    private static Set<String> memory(String... tokens) {
23
        return new LinkedHashSet<>(Arrays.asList(tokens));
24
    }
25
 
26
    private static void check(String input, Set<String> expectedMemory, String expectedKey) {
27
        assertEquals("memory for " + input, expectedMemory, ProductNames.variantTokens(input));
28
        assertEquals("modelKey for " + input, expectedKey, ProductNames.modelKey(input));
29
    }
30
 
31
    /**
32
     * The rule: memory stated -> variant-specific; memory absent -> all variants.
33
     * So detecting memory correctly is what separates "this exact SKU" from
34
     * "every size of this model", and the OEMs use three different notations.
35
     */
36
    @Test
37
    public void detectsAllThreeMemoryNotations() {
38
        // Motorola: no unit at all. Missed before, which put a Rs.1,000 8+256 offer
39
        // and a Rs.2,000 12+256 offer on the SAME SKU.
40
        check("Edge 60 Pro (8+256)", memory("8X", "256X"), "EDGE60PRO");
41
        check("Edge 60) Pro (12+256", memory("12X", "256X"), "EDGE60PRO");
42
        // vivo: single-letter unit
43
        check("V60(12+256G)", memory("12X", "256X"), "V60");
44
        check("X300 Pro(16+512G)", memory("16X", "512X"), "X300PRO");
45
        // Oppo: explicit unit, and unit on both sides
46
        check("RENO16C 12+256GB", memory("12X", "256X"), "RENO16C");
47
        check("A5X(4GB+128GB)", memory("4X", "128X"), "A5X");
48
    }
49
 
50
    /** A bare 4G/5G is a network marker, never a capacity - but 4 GB of RAM is real. */
51
    @Test
52
    public void doesNotConfuseNetworkMarkerWithCapacity() {
53
        check("Y05(4+64G)", memory("4X", "64X"), "Y05");
54
        check("Reno 16C 5G (12GB 256GB)", memory("12X", "256X"), "RENO16C");
55
        check("Redmi Note 15 Pro 5G", memory(), "REDMINOTE15PRO");
56
    }
57
 
58
    /** Terabyte capacities must stay distinct from gigabyte ones. */
59
    @Test
60
    public void handlesTerabytes() {
61
        check("S26 Ultra (16/1TB)", memory("16X", "1Y"), "S26ULTRA");
62
        check("iPhone 17 Pro (1 TB)", memory("1Y"), "IPHONE17PRO");
63
        check("iPhone 17 Pro (256 GB)", memory("256X"), "IPHONE17PRO");
64
    }
65
 
66
    /**
67
     * A digit glued to letters is part of the name. Without word-boundary guards the
68
     * "5X" inside A5X read as 5 GB and collapsed the key to "A", making Oppo A5X and
69
     * A6X indistinguishable.
70
     */
71
    @Test
72
    public void doesNotEatDigitsOutOfModelNames() {
73
        assertEquals("A5X", ProductNames.modelKey("A5X(4GB+128GB)"));
74
        assertEquals("A6X", ProductNames.modelKey("A6X(6GB+128GB)"));
75
        assertEquals("XFOLD5", ProductNames.modelKey("X Fold5(16+512G)"));
76
    }
77
 
78
    /**
79
     * '+' in a model name is significant. Stripping it as punctuation made
80
     * "Realme 16 Pro" and "Realme 16 Pro+" the same key and cross-matched 6 SKUs
81
     * across 12 offers - 1,188 false collisions from one character.
82
     */
83
    @Test
84
    public void keepsModelNamePlusSignificant() {
85
        assertEquals("16PRO", ProductNames.modelKey("Realme 16 Pro"));
86
        assertEquals("16PROPLUS", ProductNames.modelKey("Realme 16 Pro+"));
87
        assertEquals("16PRO", ProductNames.modelKey("Realme 16 Pro 5G (8GB 256GB)"));
88
        assertEquals("16PROPLUS", ProductNames.modelKey("Realme 16 Pro+ 5G (8GB 256GB)"));
89
        assertEquals("REDMINOTE15PRO", ProductNames.modelKey("Redmi Note 15 Pro 5G"));
90
        assertEquals("REDMINOTE15PROPLUS", ProductNames.modelKey("Redmi Note 15 Pro+ 5G"));
91
    }
92
 
93
    /**
94
     * The circular writes "S25+" while the catalog writes "S25 Plus" - the same phone,
95
     * so they MUST converge. And all three must stay distinct from plain S25.
96
     * "S25+ 5G" once keyed as "S25X" and then "SG" while these guards were incomplete.
97
     */
98
    @Test
99
    public void s25PlusAndS25PlusSpelledOutAreTheSamePhone() {
100
        assertEquals("S25PLUS", ProductNames.modelKey("S25+ (12/256GB)"));
101
        assertEquals("S25PLUS", ProductNames.modelKey("S25 Plus (12GB 256GB)"));
102
        assertEquals("S25PLUS", ProductNames.modelKey("S25+ 5G (12GB 256GB)"));
103
 
104
        assertEquals("S25", ProductNames.modelKey("S25 (12/128 GB)"));
105
        assertEquals("S25ULTRA", ProductNames.modelKey("S25 Ultra (12/512GB)"));
106
        assertEquals("S25FE", ProductNames.modelKey("S25 FE (128GB)"));
107
        assertEquals("S25EDGE", ProductNames.modelKey("S25 Edge (12/256GB)"));
108
    }
109
 
110
    /**
111
     * Spaces are insignificant so the circular's "RENO16C" matches the catalog's
112
     * "Reno 16C 5G"; digits are significant so a one-token difference never matches.
113
     */
114
    @Test
115
    public void spacesInsignificantDigitsSignificant() {
116
        assertEquals(ProductNames.modelKey("RENO16C 12+256GB"),
117
                ProductNames.modelKey("Reno 16C 5G (12GB 256GB)"));
118
        assertEquals("IPHONE17E", ProductNames.modelKey("iPhone 17e"));
119
        assertEquals("IPHONE7", ProductNames.modelKey("iPhone 7"));
120
    }
121
 
122
    /** WIFI/LTE are NOT noise: for tablets they are the only variant discriminator. */
123
    @Test
124
    public void keepsConnectivityDistinctForTablets() {
125
        assertEquals("TABS11ULTRAWIFI",
126
                ProductNames.modelKey("Tab S11 Ultra Wifi (12/256GB)"));
127
        assertEquals("TABS11ULTRALTE",
128
                ProductNames.modelKey("Tab S11 Ultra LTE (12/256GB)"));
129
    }
130
 
131
    /**
132
     * Splitting must never cut through a variant spec - doing so invented a phantom
133
     * product literally called "256GB" - and trailing variant-only fragments inherit
134
     * the preceding model name.
135
     */
136
    @Test
137
    public void splitsMultiProductCellsWithoutCuttingVariants() {
138
        assertEquals(Arrays.asList("Tab S11 Ultra Wifi (12/256GB)",
139
                        "Tab S11 Ultra Wifi (12/512GB)"),
140
                ProductNames.split(
141
                        "Tab S11 Ultra Wifi (12/256GB)/Tab S11 Ultra Wifi (12/512GB)"));
142
 
143
        assertEquals(Arrays.asList("RENO16C 12+256GB", "RENO16C 8+256GB", "RENO16C 8+128GB"),
144
                ProductNames.split("RENO16C 12+256GB, 8+256GB, 8+128GB"));
145
 
146
        // two products run together with no delimiter at all
147
        assertEquals(Arrays.asList("A37 5G (8GB/128GB)", "A37 5G (12GB/256GB)"),
148
                ProductNames.split("A37 5G (8GB/128GB)A37 5G (12GB/256GB)"));
149
    }
150
 
151
    /**
37531 amit 152
     * A comma-separated part that starts with a LETTER is a product, never a memory
153
     * spec, however many digits and G/T characters follow it.
154
     *
155
     * VARIANT_ONLY's unit group used to be reachable at position 0, so the "G" of a
156
     * Motorola G-series name read as the unit and "06"/"37" fell through the trailing
157
     * class. "G06, G37 , G37 Power" then collapsed to two entities - "G06" and
158
     * "G06 G37" - and G37, a real catalog SKU, lost its cashback on five offers.
159
     * Realme "GT 7" matched identically and escaped only by never following a comma.
160
     */
161
    @Test
162
    public void keepsLetterLedModelNamesOutOfVariantInheritance() {
163
        assertEquals(Arrays.asList("G06", "G37", "G37 Power"),
164
                ProductNames.split("G06, G37 , G37 Power"));
165
 
166
        assertEquals(Arrays.asList("GT 7T", "GT 7"),
167
                ProductNames.split("GT 7T, GT 7"));
168
 
169
        assertEquals(Arrays.asList("G06", "G37", "G37 Power", "G77 Power", "G Max",
170
                        "Edge 70 Fusion (12/256)", "Edge 60 Pro (8+256)"),
171
                ProductNames.split("G06, G37, G37 Power , G77 Power, G Max ,"
172
                        + " Edge 70 Fusion (12/256) , Edge 60 Pro (8+256)"));
173
 
174
        // and the inheritance it guards must still fire for genuine memory specs
175
        assertEquals(Arrays.asList("RENO 15 PRO 256GB", "RENO 15 PRO 512GB"),
176
                ProductNames.split("RENO 15 PRO 256GB, 512GB"));
177
        assertEquals(Arrays.asList("F33 6+128GB", "F33 8+128GB", "F33 8+256GB"),
178
                ProductNames.split("F33 6+128GB, 8+128GB, 8+256GB"));
179
    }
180
 
181
    /**
37337 amit 182
     * Two variant groups written back-to-back are two variants of one model. Merged,
183
     * their memory tokens ran together and the offer bound to one arbitrary SKU while
184
     * the other variant silently got nothing - money on the wrong SKU, same failure
185
     * class as the missing-unit bug.
186
     */
187
    @Test
188
    public void splitsAdjacentVariantGroupsIntoSeparateProducts() {
189
        assertEquals(Arrays.asList("Edge 70 Pro (8+256)", "Edge 70 Pro 12+256"),
190
                ProductNames.split("Edge 70 Pro (8+256)(12+256)"));
191
        assertEquals(Arrays.asList("G37 Power (4+64)", "G37 Power 4+128"),
192
                ProductNames.split("G37 Power (4+64)(4+128)"));
193
        assertEquals(Arrays.asList("A6 (6+128GB)", "A6 4+128GB"),
194
                ProductNames.split("A6 (6+128GB)(4+128GB)"));
195
 
196
        // a '/' between the groups is already a split point and must stay one
197
        assertEquals(Arrays.asList("X300 FE(12+512G)", "X300 FE 12+256G"),
198
                ProductNames.split("X300 FE(12+512G)/(12+256G)"));
199
    }
200
 
201
    /**
202
     * A bundled accessory suffix must SURVIVE splitting, so the bundle stays a distinct
203
     * product from the bare phone.
204
     *
205
     * Stripping it was implemented and reverted: it resolved 30 CATALOG_GAP rows, but on
206
     * Aug'26 vivo caps X300 Pro(16+512G) at Rs.10,000 on its own row and Rs.11,000 on the
207
     * "+Extender" row - the difference being the Extender. Merging them lets the bundle's
208
     * cap be claimed on a phone sold without the accessory, which is money on the wrong
209
     * SKU. Whether a bundle offer transfers to the bare SKU is a commercial question the
210
     * PDF does not answer, so it is a manual coverage decision, not a parsing rule.
211
     */
212
    @Test
213
    public void keepsBundledAccessoryDistinctFromTheBarePhone() {
214
        assertEquals(Arrays.asList("RENO16C 12+256GB +Bubble"),
215
                ProductNames.split("RENO16C 12+256GB +Bubble"));
216
        assertEquals(Arrays.asList("X300 Pro(16+512G)+Extender"),
217
                ProductNames.split("X300 Pro(16+512G)+Extender"));
218
 
219
        // and the bare phone in the same cell stays separate from the bundled one
220
        assertEquals(Arrays.asList("X300 Pro(16+512G)", "X300 Pro(16+512G)+Extender"),
221
                ProductNames.split("X300 Pro(16+512G), X300 Pro(16+512G)+Extender"));
222
 
223
        // standalone accessories are whole product names, never truncated to a phone;
224
        // they are removed as IGNORE aliases instead
225
        assertEquals(Arrays.asList("X300 FE Telephoto Extender"),
226
                ProductNames.split("X300 FE Telephoto Extender"));
227
        assertEquals(Arrays.asList("Moto Buds 2"), ProductNames.split("Moto Buds 2"));
228
    }
229
}