| 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 |
/**
|
|
|
152 |
* Two variant groups written back-to-back are two variants of one model. Merged,
|
|
|
153 |
* their memory tokens ran together and the offer bound to one arbitrary SKU while
|
|
|
154 |
* the other variant silently got nothing - money on the wrong SKU, same failure
|
|
|
155 |
* class as the missing-unit bug.
|
|
|
156 |
*/
|
|
|
157 |
@Test
|
|
|
158 |
public void splitsAdjacentVariantGroupsIntoSeparateProducts() {
|
|
|
159 |
assertEquals(Arrays.asList("Edge 70 Pro (8+256)", "Edge 70 Pro 12+256"),
|
|
|
160 |
ProductNames.split("Edge 70 Pro (8+256)(12+256)"));
|
|
|
161 |
assertEquals(Arrays.asList("G37 Power (4+64)", "G37 Power 4+128"),
|
|
|
162 |
ProductNames.split("G37 Power (4+64)(4+128)"));
|
|
|
163 |
assertEquals(Arrays.asList("A6 (6+128GB)", "A6 4+128GB"),
|
|
|
164 |
ProductNames.split("A6 (6+128GB)(4+128GB)"));
|
|
|
165 |
|
|
|
166 |
// a '/' between the groups is already a split point and must stay one
|
|
|
167 |
assertEquals(Arrays.asList("X300 FE(12+512G)", "X300 FE 12+256G"),
|
|
|
168 |
ProductNames.split("X300 FE(12+512G)/(12+256G)"));
|
|
|
169 |
}
|
|
|
170 |
|
|
|
171 |
/**
|
|
|
172 |
* A bundled accessory suffix must SURVIVE splitting, so the bundle stays a distinct
|
|
|
173 |
* product from the bare phone.
|
|
|
174 |
*
|
|
|
175 |
* Stripping it was implemented and reverted: it resolved 30 CATALOG_GAP rows, but on
|
|
|
176 |
* Aug'26 vivo caps X300 Pro(16+512G) at Rs.10,000 on its own row and Rs.11,000 on the
|
|
|
177 |
* "+Extender" row - the difference being the Extender. Merging them lets the bundle's
|
|
|
178 |
* cap be claimed on a phone sold without the accessory, which is money on the wrong
|
|
|
179 |
* SKU. Whether a bundle offer transfers to the bare SKU is a commercial question the
|
|
|
180 |
* PDF does not answer, so it is a manual coverage decision, not a parsing rule.
|
|
|
181 |
*/
|
|
|
182 |
@Test
|
|
|
183 |
public void keepsBundledAccessoryDistinctFromTheBarePhone() {
|
|
|
184 |
assertEquals(Arrays.asList("RENO16C 12+256GB +Bubble"),
|
|
|
185 |
ProductNames.split("RENO16C 12+256GB +Bubble"));
|
|
|
186 |
assertEquals(Arrays.asList("X300 Pro(16+512G)+Extender"),
|
|
|
187 |
ProductNames.split("X300 Pro(16+512G)+Extender"));
|
|
|
188 |
|
|
|
189 |
// and the bare phone in the same cell stays separate from the bundled one
|
|
|
190 |
assertEquals(Arrays.asList("X300 Pro(16+512G)", "X300 Pro(16+512G)+Extender"),
|
|
|
191 |
ProductNames.split("X300 Pro(16+512G), X300 Pro(16+512G)+Extender"));
|
|
|
192 |
|
|
|
193 |
// standalone accessories are whole product names, never truncated to a phone;
|
|
|
194 |
// they are removed as IGNORE aliases instead
|
|
|
195 |
assertEquals(Arrays.asList("X300 FE Telephoto Extender"),
|
|
|
196 |
ProductNames.split("X300 FE Telephoto Extender"));
|
|
|
197 |
assertEquals(Arrays.asList("Moto Buds 2"), ProductNames.split("Moto Buds 2"));
|
|
|
198 |
}
|
|
|
199 |
}
|