Subversion Repositories SmartDukaan

Rev

Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed

package com.spice.profitmandi.web.offercircular;

import static org.junit.Assert.assertEquals;

import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;

import org.junit.Test;

/**
 * Guards the product-name parsing, which decides which SKU an offer's money lands on.
 *
 * Every case here corresponds to a real mis-parse found against the Aug'26 circular.
 * They all share one root cause worth remembering: **characters and digits belonging to
 * the model name being consumed as memory or stripped as punctuation.** If a change
 * here breaks a test, the fix is almost certainly to add a word-boundary guard rather
 * than to relax the expectation.
 */
public class ProductNamesTest {

    private static Set<String> memory(String... tokens) {
        return new LinkedHashSet<>(Arrays.asList(tokens));
    }

    private static void check(String input, Set<String> expectedMemory, String expectedKey) {
        assertEquals("memory for " + input, expectedMemory, ProductNames.variantTokens(input));
        assertEquals("modelKey for " + input, expectedKey, ProductNames.modelKey(input));
    }

    /**
     * The rule: memory stated -> variant-specific; memory absent -> all variants.
     * So detecting memory correctly is what separates "this exact SKU" from
     * "every size of this model", and the OEMs use three different notations.
     */
    @Test
    public void detectsAllThreeMemoryNotations() {
        // Motorola: no unit at all. Missed before, which put a Rs.1,000 8+256 offer
        // and a Rs.2,000 12+256 offer on the SAME SKU.
        check("Edge 60 Pro (8+256)", memory("8X", "256X"), "EDGE60PRO");
        check("Edge 60) Pro (12+256", memory("12X", "256X"), "EDGE60PRO");
        // vivo: single-letter unit
        check("V60(12+256G)", memory("12X", "256X"), "V60");
        check("X300 Pro(16+512G)", memory("16X", "512X"), "X300PRO");
        // Oppo: explicit unit, and unit on both sides
        check("RENO16C 12+256GB", memory("12X", "256X"), "RENO16C");
        check("A5X(4GB+128GB)", memory("4X", "128X"), "A5X");
    }

    /** A bare 4G/5G is a network marker, never a capacity - but 4 GB of RAM is real. */
    @Test
    public void doesNotConfuseNetworkMarkerWithCapacity() {
        check("Y05(4+64G)", memory("4X", "64X"), "Y05");
        check("Reno 16C 5G (12GB 256GB)", memory("12X", "256X"), "RENO16C");
        check("Redmi Note 15 Pro 5G", memory(), "REDMINOTE15PRO");
    }

    /** Terabyte capacities must stay distinct from gigabyte ones. */
    @Test
    public void handlesTerabytes() {
        check("S26 Ultra (16/1TB)", memory("16X", "1Y"), "S26ULTRA");
        check("iPhone 17 Pro (1 TB)", memory("1Y"), "IPHONE17PRO");
        check("iPhone 17 Pro (256 GB)", memory("256X"), "IPHONE17PRO");
    }

    /**
     * A digit glued to letters is part of the name. Without word-boundary guards the
     * "5X" inside A5X read as 5 GB and collapsed the key to "A", making Oppo A5X and
     * A6X indistinguishable.
     */
    @Test
    public void doesNotEatDigitsOutOfModelNames() {
        assertEquals("A5X", ProductNames.modelKey("A5X(4GB+128GB)"));
        assertEquals("A6X", ProductNames.modelKey("A6X(6GB+128GB)"));
        assertEquals("XFOLD5", ProductNames.modelKey("X Fold5(16+512G)"));
    }

    /**
     * '+' in a model name is significant. Stripping it as punctuation made
     * "Realme 16 Pro" and "Realme 16 Pro+" the same key and cross-matched 6 SKUs
     * across 12 offers - 1,188 false collisions from one character.
     */
    @Test
    public void keepsModelNamePlusSignificant() {
        assertEquals("16PRO", ProductNames.modelKey("Realme 16 Pro"));
        assertEquals("16PROPLUS", ProductNames.modelKey("Realme 16 Pro+"));
        assertEquals("16PRO", ProductNames.modelKey("Realme 16 Pro 5G (8GB 256GB)"));
        assertEquals("16PROPLUS", ProductNames.modelKey("Realme 16 Pro+ 5G (8GB 256GB)"));
        assertEquals("REDMINOTE15PRO", ProductNames.modelKey("Redmi Note 15 Pro 5G"));
        assertEquals("REDMINOTE15PROPLUS", ProductNames.modelKey("Redmi Note 15 Pro+ 5G"));
    }

    /**
     * The circular writes "S25+" while the catalog writes "S25 Plus" - the same phone,
     * so they MUST converge. And all three must stay distinct from plain S25.
     * "S25+ 5G" once keyed as "S25X" and then "SG" while these guards were incomplete.
     */
    @Test
    public void s25PlusAndS25PlusSpelledOutAreTheSamePhone() {
        assertEquals("S25PLUS", ProductNames.modelKey("S25+ (12/256GB)"));
        assertEquals("S25PLUS", ProductNames.modelKey("S25 Plus (12GB 256GB)"));
        assertEquals("S25PLUS", ProductNames.modelKey("S25+ 5G (12GB 256GB)"));

        assertEquals("S25", ProductNames.modelKey("S25 (12/128 GB)"));
        assertEquals("S25ULTRA", ProductNames.modelKey("S25 Ultra (12/512GB)"));
        assertEquals("S25FE", ProductNames.modelKey("S25 FE (128GB)"));
        assertEquals("S25EDGE", ProductNames.modelKey("S25 Edge (12/256GB)"));
    }

    /**
     * Spaces are insignificant so the circular's "RENO16C" matches the catalog's
     * "Reno 16C 5G"; digits are significant so a one-token difference never matches.
     */
    @Test
    public void spacesInsignificantDigitsSignificant() {
        assertEquals(ProductNames.modelKey("RENO16C 12+256GB"),
                ProductNames.modelKey("Reno 16C 5G (12GB 256GB)"));
        assertEquals("IPHONE17E", ProductNames.modelKey("iPhone 17e"));
        assertEquals("IPHONE7", ProductNames.modelKey("iPhone 7"));
    }

    /** WIFI/LTE are NOT noise: for tablets they are the only variant discriminator. */
    @Test
    public void keepsConnectivityDistinctForTablets() {
        assertEquals("TABS11ULTRAWIFI",
                ProductNames.modelKey("Tab S11 Ultra Wifi (12/256GB)"));
        assertEquals("TABS11ULTRALTE",
                ProductNames.modelKey("Tab S11 Ultra LTE (12/256GB)"));
    }

    /**
     * Splitting must never cut through a variant spec - doing so invented a phantom
     * product literally called "256GB" - and trailing variant-only fragments inherit
     * the preceding model name.
     */
    @Test
    public void splitsMultiProductCellsWithoutCuttingVariants() {
        assertEquals(Arrays.asList("Tab S11 Ultra Wifi (12/256GB)",
                        "Tab S11 Ultra Wifi (12/512GB)"),
                ProductNames.split(
                        "Tab S11 Ultra Wifi (12/256GB)/Tab S11 Ultra Wifi (12/512GB)"));

        assertEquals(Arrays.asList("RENO16C 12+256GB", "RENO16C 8+256GB", "RENO16C 8+128GB"),
                ProductNames.split("RENO16C 12+256GB, 8+256GB, 8+128GB"));

        // two products run together with no delimiter at all
        assertEquals(Arrays.asList("A37 5G (8GB/128GB)", "A37 5G (12GB/256GB)"),
                ProductNames.split("A37 5G (8GB/128GB)A37 5G (12GB/256GB)"));
    }

    /**
     * Two variant groups written back-to-back are two variants of one model. Merged,
     * their memory tokens ran together and the offer bound to one arbitrary SKU while
     * the other variant silently got nothing - money on the wrong SKU, same failure
     * class as the missing-unit bug.
     */
    @Test
    public void splitsAdjacentVariantGroupsIntoSeparateProducts() {
        assertEquals(Arrays.asList("Edge 70 Pro (8+256)", "Edge 70 Pro 12+256"),
                ProductNames.split("Edge 70 Pro (8+256)(12+256)"));
        assertEquals(Arrays.asList("G37 Power (4+64)", "G37 Power 4+128"),
                ProductNames.split("G37 Power (4+64)(4+128)"));
        assertEquals(Arrays.asList("A6 (6+128GB)", "A6 4+128GB"),
                ProductNames.split("A6 (6+128GB)(4+128GB)"));

        // a '/' between the groups is already a split point and must stay one
        assertEquals(Arrays.asList("X300 FE(12+512G)", "X300 FE 12+256G"),
                ProductNames.split("X300 FE(12+512G)/(12+256G)"));
    }

    /**
     * A bundled accessory suffix must SURVIVE splitting, so the bundle stays a distinct
     * product from the bare phone.
     *
     * Stripping it was implemented and reverted: it resolved 30 CATALOG_GAP rows, but on
     * Aug'26 vivo caps X300 Pro(16+512G) at Rs.10,000 on its own row and Rs.11,000 on the
     * "+Extender" row - the difference being the Extender. Merging them lets the bundle's
     * cap be claimed on a phone sold without the accessory, which is money on the wrong
     * SKU. Whether a bundle offer transfers to the bare SKU is a commercial question the
     * PDF does not answer, so it is a manual coverage decision, not a parsing rule.
     */
    @Test
    public void keepsBundledAccessoryDistinctFromTheBarePhone() {
        assertEquals(Arrays.asList("RENO16C 12+256GB +Bubble"),
                ProductNames.split("RENO16C 12+256GB +Bubble"));
        assertEquals(Arrays.asList("X300 Pro(16+512G)+Extender"),
                ProductNames.split("X300 Pro(16+512G)+Extender"));

        // and the bare phone in the same cell stays separate from the bundled one
        assertEquals(Arrays.asList("X300 Pro(16+512G)", "X300 Pro(16+512G)+Extender"),
                ProductNames.split("X300 Pro(16+512G), X300 Pro(16+512G)+Extender"));

        // standalone accessories are whole product names, never truncated to a phone;
        // they are removed as IGNORE aliases instead
        assertEquals(Arrays.asList("X300 FE Telephoto Extender"),
                ProductNames.split("X300 FE Telephoto Extender"));
        assertEquals(Arrays.asList("Moto Buds 2"), ProductNames.split("Moto Buds 2"));
    }
}