Subversion Repositories SmartDukaan

Rev

Rev 37579 | Blame | Compare with Previous | Last modification | View Log | RSS feed

package com.spice.profitmandi.web.offercircular;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;

import java.time.LocalDate;

import org.junit.Test;

import com.spice.profitmandi.web.offercircular.WebOfferSyncService.Line;
import com.spice.profitmandi.web.offercircular.WebOfferSyncService.ModelOffer;

/**
 * Guards the consolidated badge - the one offer line a partner reads on a product card.
 *
 * Before consolidation a model carried one badge per circular row that named it; Edge 60
 * Pro 12+256 had five, and the partner had to reconcile them. These cases are taken from
 * the Sep'26 circular, because the failures that matter here are all real ones: a bank
 * that does not offer a scheme, a tenure that belongs to a different bank's offer, an
 * amount that stops earlier than the badge does.
 *
 * Constructed with no collaborators - the rendering must not need Spring or a database.
 */
public class ModelOfferTest {

    private final WebOfferSyncService service = new WebOfferSyncService(null, null);

    private static final LocalDate SEP_1 = LocalDate.of(2026, 9, 1);
    private static final LocalDate SEP_16 = LocalDate.of(2026, 9, 16);
    private static final LocalDate SEP_20 = LocalDate.of(2026, 9, 20);
    private static final LocalDate SEP_26 = LocalDate.of(2026, 9, 26);

    private static Line cashback(String mode, String amount, String banks,
                                 String tenures, LocalDate end) {
        Line line = new Line();
        line.txnMode = mode;
        line.calcType = "FLAT";
        line.flatAmount = new java.math.BigDecimal(amount);
        line.timing = "INSTANT";
        line.banks = banks;
        line.tenures = tenures;
        line.start = SEP_1;
        line.end = end;
        return line;
    }

    private static Line scheme(String tenures, String banks) {
        Line line = new Line();
        line.timing = "INSTANT";
        line.banks = banks;
        line.tenures = tenures;
        line.start = SEP_1;
        line.end = SEP_20;
        return line;
    }

    private static ModelOffer model(String brand, Line... lines) {
        ModelOffer offer = new ModelOffer();
        offer.brand = brand;
        for (Line line : lines) {
            offer.add(line);
        }
        return offer;
    }

    // ------------------------------------------------------------------ Edge 60 Pro

    /**
     * Motorola Edge 60 Pro (12GB 256GB), Sep'26 page 9 - five badges before, one now.
     *
     * The banks differ on every line, so none can be hoisted: the EMI cashback is
     * IDFC/Kotak, no-cost EMI is ICICI on 3 and 6 months but SBI on 3 only, and low-cost
     * is SBI. Stating one bank list for the badge would offer every bank every term.
     */
    @Test
    public void consolidatesFiveBadgesIntoOneWithBanksPerLine() {
        ModelOffer offer = model("Motorola",
                cashback("CC_EMI", "2500", "IDFC First Bank, Kotak Mahindra Bank", null, SEP_20),
                cashback("CC_FULL_SWIPE", "2000", "IDFC First Bank, State Bank of India", null, SEP_20),
                scheme("NCE 3, 6 months", "ICICI Bank"),
                scheme("NCE 3 month", "State Bank of India"),
                scheme("LCE 6, 9, 12, 18, 24 months", "State Bank of India"));

        String title = service.modelTitle(offer);
        assertEquals("SEPTEMBER 2026 Motorola Cashback Offer-2,500 On Credit Card EMI"
                + " & 2,000 On Credit Card Full Swipe", title);

        String detail = service.modelDetail(offer);
        assertTrue(detail, detail.contains("Rs.2,500 on Credit Card EMI with IDFC First Bank"));
        assertTrue(detail, detail.contains("NCE 3, 6 months with ICICI Bank"));
        assertTrue(detail, detail.contains("NCE 3 month with State Bank of India"));
        // no single trailing bank list, because no two lines share one
        assertTrue(detail, !detail.contains("Banks: "));
    }

    // ------------------------------------------------------------------ bank hoisting

    /** When every line shares one bank set it is stated once, not on each line. */
    @Test
    public void hoistsTheBankListWhenEveryLineSharesIt() {
        ModelOffer offer = model("Samsung",
                cashback("CC_FULL_SWIPE", "3000", "HDFC Bank, ICICI Bank", null, SEP_20),
                cashback("CC_EMI", "3000", "HDFC Bank, ICICI Bank", "LCE 3, 6 months", SEP_20));

        String detail = service.modelDetail(offer);
        assertTrue(detail, detail.endsWith("Banks: HDFC Bank, ICICI Bank"));
        assertTrue(detail, !detail.contains("with HDFC Bank"));
    }

    // ------------------------------------------------------------------ "Upto"

    /**
     * "Upto" only where a mode carries more than one value - which is what happens when
     * banks pay differently for the same thing. A single value is stated plainly, or the
     * badge would understate a firm offer.
     */
    @Test
    public void saysUptoOnlyWhenAModeHasSeveralValues() {
        assertTrue(service.modelTitle(model("Apple",
                cashback("CC_EMI", "6000", "Axis Bank", null, SEP_16),
                cashback("CC_EMI", "4000", "Axis Bank", null, SEP_26)))
                .contains("Upto 6,000 On Credit Card EMI"));

        String single = service.modelTitle(model("Apple",
                cashback("CC_EMI", "6000", "Axis Bank", null, SEP_26)));
        assertTrue(single, single.contains("6,000 On Credit Card EMI"));
        assertTrue(single, !single.contains("Upto"));
    }

    // ------------------------------------------------------------------ windows

    /**
     * Apple 1025780, Sep'26: Rs.6,000 to the 16th, then Rs.4,000 to the 26th - the same
     * banks and the same mode, stepping down.
     *
     * The badge spans 1-26 Sep, so the earlier amount MUST carry its own end date or the
     * badge advertises Rs.6,000 for ten days it is not available. The later amount runs to
     * the badge's own end and says nothing, which keeps the common case clean.
     */
    @Test
    public void anAmountThatEndsEarlyStatesItsOwnValidity() {
        ModelOffer offer = model("Apple",
                cashback("CC_EMI", "6000", "Axis Bank", null, SEP_16),
                cashback("CC_EMI", "4000", "Axis Bank", null, SEP_26));

        assertEquals(SEP_1, offer.start);
        assertEquals(SEP_26, offer.end);

        String detail = service.modelDetail(offer);
        assertTrue(detail, detail.contains("Rs.6,000 on Credit Card EMI (till 16 Sep)"));
        assertTrue(detail, detail.contains("Rs.4,000 on Credit Card EMI"));
        assertTrue(detail, !detail.contains("Rs.4,000 on Credit Card EMI (till"));
    }

    // ------------------------------------------------------------------ ordering

    /** Highest value first, so the number a partner is looking for leads. */
    @Test
    public void statesHigherValuesFirst() {
        String detail = service.modelDetail(model("Motorola",
                cashback("CC_EMI", "1500", "SBI", null, SEP_20),
                cashback("CC_EMI", "2500", "SBI", null, SEP_20)));
        assertTrue(detail, detail.indexOf("Rs.2,500") < detail.indexOf("Rs.1,500"));
    }

    // ------------------------------------------------------------------ scheme-only

    /** A model with no cashback at all is titled by its scheme, never "Cashback Offer". */
    @Test
    public void aSchemeOnlyModelIsTitledByItsScheme() {
        String title = service.modelTitle(model("Motorola",
                scheme("NCE 3, 6 months", "State Bank of India")));
        assertEquals("SEPTEMBER 2026 Motorola NCE 3, 6 months", title);
        assertTrue(title, !title.contains("Cashback Offer"));
        assertTrue(title, !title.contains("NA"));
    }

    // ------------------------------------------------------------------ tenure origin

    /**
     * A tenure belongs to the offer that granted it, never to the model.
     *
     * Edge 60 Pro's EMI cashback is IDFC/Kotak with NO tenure of its own, while its
     * schemes are ICICI's and SBI's. Attaching the model's tenures to that cashback would
     * promise no-cost EMI on a bank that never offered it.
     */
    @Test
    public void doesNotAttachAnotherOffersTenuresToACashback() {
        String detail = service.modelDetail(model("Motorola",
                cashback("CC_EMI", "2500", "IDFC First Bank", null, SEP_20),
                scheme("NCE 3, 6 months", "ICICI Bank")));
        assertTrue(detail, detail.contains("Rs.2,500 on Credit Card EMI with IDFC First Bank"));
        assertTrue(detail, !detail.contains("Credit Card EMI (NCE"));
    }

    // ------------------------------------------------------------------ length

    /** detailedText is VARCHAR(1024); a model with many lines must not overflow it. */
    @Test
    public void neverExceedsTheColumnWidth() {
        ModelOffer offer = new ModelOffer();
        offer.brand = "Samsung";
        for (int i = 0; i < 40; i++) {
            offer.add(cashback("CC_EMI", String.valueOf(1000 + i),
                    "HDFC Bank, ICICI Bank, State Bank of India, Axis Bank " + i, null, SEP_20));
        }
        assertTrue(service.modelDetail(offer).length() <= 1024);
        assertTrue(service.modelTitle(offer).length() <= 256);
    }

    // ------------------------------------------------------ scheme without months

    /**
     * Edge 60 Pro's Rs.2,500, Sep'26 page 9 row 21: the tenure cell is a bare "CIB" -
     * the circular names the EMI type and gives no months.
     *
     * 52 offers are of this shape and every one carries a cashback, so dropping the cell
     * left the badge silent about the EMI it applies to. The type is stated because the
     * circular stated it; the months are NOT invented, because it did not give them.
     */
    @Test
    public void statesASchemeThatWasGivenWithoutMonths() {
        Line line = cashback("CC_EMI", "2500", "IDFC First Bank", null, SEP_20);
        line.schemeOnly = "CIB";
        String detail = service.modelDetail(model("Motorola", line));
        assertTrue(detail, detail.contains("Rs.2,500 on Credit Card EMI (CIB)"));
        // no fabricated tenure
        assertTrue(detail, !detail.matches(".*CIB \\d.*"));
    }

    /** A scheme code is never attached to a mode that has no EMI at all. */
    @Test
    public void doesNotPutASchemeOnAFullSwipeLine() {
        Line line = cashback("CC_FULL_SWIPE", "2000", "IDFC First Bank", null, SEP_20);
        line.schemeOnly = "CIB";
        String detail = service.modelDetail(model("Motorola", line));
        assertTrue(detail, detail.contains("Rs.2,000 on Credit Card Full Swipe"));
        assertTrue(detail, !detail.contains("(CIB)"));
    }

    /** Real months always win over a bare code. */
    @Test
    public void prefersRealTenuresOverTheBareCode() {
        Line line = cashback("CC_EMI", "3000", "HDFC Bank", "LCE 3, 6 months", SEP_20);
        line.schemeOnly = "CIB";
        String detail = service.modelDetail(model("Samsung", line));
        assertTrue(detail, detail.contains("(LCE 3, 6 months)"));
        assertTrue(detail, !detail.contains("(CIB)"));
    }
}