Subversion Repositories SmartDukaan

Rev

Show changed files | Directory listing | RSS feed

Filtering Options

Rev Age Author Path Log message Diff
37580 27 d 13 h amit /trunk/ web offer: state an EMI scheme the circular gave without months

Rs.2,500 on Credit Card EMI (CIB) with IDFC First Bank, Kotak Mahindra Bank

52 Sep'26 offers have a bare 'CIB' tenure cell - the circular names the EMI type and
gives no months. TenureParser correctly yields no tenure row for a cell with no digits,
so the type vanished and the badge said only 'on Credit Card EMI'. Every one of those
52 carries a cashback, so it was 52 offers' worth of SKUs silent about the EMI they
apply to.

The type is stated because the circular stated it. The MONTHS are not invented,
because it did not give them - the same line the CIB expansion was left on.

Read from offer_raw_row.col_emi_tenure rather than stored: 'scheme, no month' is not a
tenure, and offer_tenure.tenure_months is NOT NULL, so persisting it would mean a fake
0 row standing for a fact that is not a tenure.

Only ever attached to an EMI mode, and real tenures always win over the bare code.

ModelOfferTest 8 -> 11.
 
37579 27 d 14 h amit /trunk/profitmandi-fofo/src/ web offer: one badge per model, consolidating everything the circular grants it

Edge 60 Pro 12+256 carried five badges - a Full Swipe cashback, an EMI cashback and
three EMI schemes - and the partner's card listed all five. It now carries one:

SEPTEMBER 2026 Motorola Cashback Offer-2,500 On Credit Card EMI
& 2,000 On Credit Card Full Swipe

Models whose consolidated terms are IDENTICAL share a badge, so 314 models collapse to
~107 rows while every model still appears in exactly one.

Rules, from the taxonomy: any model has cashback on Full Swipe and/or EMI, instant or
deferred, plus NCE/LCE/CIB with tenures, and any of it may differ per bank.
- 'Upto' only where a mode carries several values - which is what per-bank pricing
looks like. One value is stated plainly.
- Highest value first.
- Banks ride with each line only when they DIFFER; one shared set is hoisted to a
single trailing 'Banks:' line. A third of models need the per-line form.
- An amount whose window is narrower than the badge's states its own end date. Apple
1025780 pays Rs.6,000 to the 16th then Rs.4,000 to the 26th; without this the badge
would advertise Rs.6,000 for ten days it is not available.

⚠️ Banks and tenures belong to the OFFER a line came from, never to the model. On Edge
60 Pro the no-cost EMI is ICICI on 3 and 6 months but SBI on 3 only, and the EMI
cashback is IDFC/Kotak with no tenure at all. Attaching the model's tenures to its
cashback would promise no-cost EMI on a bank that never offered it. This also corrects
an earlier reading of mine that called the SBI 3-month line a duplicate of ICICI's -
it is a different bank, not noise.

New ModelOfferTest, 8 cases, every one taken from a real Sep'26 row.
 
37577 27 d 14 h amit /trunk/profitmandi-fofo/src/ web offer: an EMI cashback names its tenures WITH their scheme

Rs.2,500 on Credit Card EMI (NCE 3, 6; LCE 9, 12 months)

A bare '3, 6, 9, 12 months' says which tenures earn the cashback but not which of
them are no-cost - and at the counter the two facts are only useful together.

The trailing scheme sentence is dropped when an EMI benefit already carried it, since
it would state the same thing twice. It stays when the cashback is on Full Swipe only:
EMI is still available, it just is not what the cashback is on.

Tenures whose scheme the circular never gave keep their months but carry no label -
the months are real, the scheme would be invented.

Tests 16 -> 18.
 
37574 27 d 15 h amit /trunk/profitmandi-fofo/src/ web offer: print the EMI scheme code, not a guessed expansion

NCE / LCE / CIB are industry standard and now appear verbatim:

Instant cashback. Rs.3,000 on Credit Card EMI (3, 6 months);
Rs.3,000 on Credit Card Full Swipe. LCE on 3, 6 months. Banks: ...

The expansions were not ours to invent. emi_scheme.display_name carries 'Cashback
Instant Benefit' for CIB, and the evidence contradicts it: the Sep'26 cell
'3 NC 6,9,12 CIB' puts CIB in ONE ladder with NCE, so it describes who bears the
interest on a tenure rather than how a cashback is delivered - and 53 of 55 CIB rows
carry a separate cashback amount anyway, which that expansion would make redundant.
Pine Labs' own docs define ICB as Instant Cashback on UPI; the circular writes CIB 60
times and ICB never.

A guessed expansion on a partner badge risks stating the opposite of the truth - for
CIB, advertising a benefit where the customer may in fact be bearing the interest.
The code cannot be wrong that way.

A scheme is still stated ONLY when the circular gave one; tenures with no scheme are
described as nothing at all.

Also fixes the singular: 'NCE on 3 month', not '3 months'.
 
37572 27 d 15 h amit /trunk/profitmandi-fofo/src/ web offer: state the whole offer in the title, and the EMI scheme in the detail

Title now states EVERY payment mode, highest first, in the house style of the
hand-written rows:

SEPTEMBER 2026 Samsung Cashback Offer-3000 On Credit Card Full Swipe
& 3000 On Credit Card EMI & 3000 On UPI

A partner scanning the card should see the whole offer, not just its best number.
Brand keeps its catalog casing (Samsung, not SAMSUNG). A scheme-only offer is still
titled by its scheme - calling it a cashback offer is what produced the live
'Cashback Offer- NA' badges.

Detail additions:
- the EMI scheme, when the circular gave one: '... Low Cost EMI on 3, 6 months.'
The circular states it in its own column ('3m,6m LCE' beside '3000 CC Full
Swipe/CC EMI/UPI') and a partner needs both halves.
- eligible tenures on an EMI benefit: 'Rs.3,000 on Credit Card EMI (3, 6 months)'.
Cashback 'on Credit Card EMI' is not actionable without knowing which tenures
qualify. Full Swipe and UPI carry no tenure, so they get none.

⚠️ A scheme is stated ONLY when the circular gave one. Tenures with no scheme are
described as nothing at all - naming them would read as a no-cost or low-cost claim
that was never made.

Tests 14 -> 16.
 
37571 27 d 15 h amit /trunk/profitmandi-fofo/src/ offer circular: the month unit was swallowing the EMI scheme marker

'M' is the month UNIT, not a separator, but the token loop flushed on it. '3m,6m LCE'
tokenises to 3, M, 6, M, LCE - each M banked its month as NONE, so the LCE arrived
with nothing pending and was silently lost.

74 Sep'26 offers carry a scheme in the tenure cell that never reached offer_tenure;
22 of them through this path ('3m,6m LCE', '6m,9m LCE', '3m & 6m LCE', '3m, 6m, 9m,
12m, 18m, 24m LCE'). The rest are bare 'CIB' cells with no months at all, which is a
separate case and left alone.

It also fixes '3M (NCE), 6M & Above LCE', where the M flushed BOTH groups before
either marker was seen - so an offer that is no-cost on 3 months and low-cost beyond
recorded neither.

This is money: no-cost EMI costs the customer nothing and low-cost does not, so a lost
marker is a term a partner could quote wrongly at the counter.

Months with no marker anywhere still bank as NONE via the end-of-cell flush - the same
answer the M branch used to give for a genuinely unmarked cell.

New TenureParserTest, 6 cases, covering the regression and the shapes that must not
change.
 
37554 28 d 14 h amit /trunk/profitmandi-fofo/src/ web offer: put the brand back in the title

SEPTEMBER 2026 MOTOROLA Rs.1,000 cashback on Credit Card Full Swipe
SEPTEMBER 2026 MOTOROLA No Cost EMI on 3, 6 months

Month and brand now both match the convention of the 3,850 hand-written rows, which
is how the admin listing is scanned. What stays gone is the redundant
'Cashback Offer-' label: the value already states what the offer is, and calling a
scheme-only row a cashback offer is what produced the live 'Cashback Offer- NA'
badges on 81 SKUs.

Brand comes from the division's catalog brand, falling back to its display name, so
a division with no catalog.brand row (Google) still reads sensibly rather than
emitting an empty gap.
 
37551 28 d 15 h amit /trunk/profitmandi-fofo/src/ web offer: keep the month in the title

SEPTEMBER 2026 Up to Rs.4,000 cashback
SEPTEMBER 2026 Rs.1,500 cashback on Credit Card EMI
SEPTEMBER 2026 No Cost EMI on 3, 6 months

Matches the convention of the 3,850 hand-written rows and is how the admin listing is
scanned. The brand stays out - the product card already shows it, so that was the
redundant half, and dropping it is what makes room for the value to lead.

The month comes from the OFFER's own start date, not the circular's period: offers
run past a month end - four Aug'26 Apple offers ran to 26 September - and labelling
one of those AUGUST on a badge read in September would be wrong.
 
37550 28 d 15 h amit /trunk/profitmandi-fofo/src/ web offer: partner-facing titles, and stop calling a scheme offer a cashback offer

web_offer.title is partner-facing - StoreController and DealsController map it into
the product's offer list - so it now leads with the value rather than a filing label:

Up to Rs.4,000 cashback
Rs.1,500 cashback on Credit Card EMI
10% cashback up to Rs.7,500 on Credit Card EMI
No Cost EMI on 3, 6 months

Month and brand are gone: the card already shows the product, and the offer carries
its own dates. The full breakdown sits in detailedText below it.

This fixes a live defect. 16 published offers have no cashback at all, and the old
template rendered them 'SEPTEMBER 2026 XIAOMI Cashback Offer- NA' with detail 'NA',
on 81 SKUs. They are EMI-scheme offers and now say so.

Scheme inference at ingest: Motorola states the scheme in the DESCRIPTION rather than
the tenure cell - '[3&6 M]' plus 'Only NCEMI', where every other OEM writes
'6(NCE),9,12(LCE)'. TenureParser is right to return NONE for such a cell; the scheme
is simply not in it. Recovered in CircularIngestService where both columns are in
hand, and deliberately narrow: only when EVERY tenure came back without a scheme AND
the description names exactly one. On Sep'26 that is 10 offers, all Motorola; the
other 23 scheme-less rows are cashback offers whose descriptions are amounts, and
inventing a scheme for those would misstate the terms.

This distinction is money: four of the Motorola rows are LOW cost EMI, and calling
them no-cost would promise a partner something the bank will not honour.

'No cashback.' is dropped everywhere - the absence of an amount says it, and beside a
Cashback Instant Benefit scheme it flatly contradicted the name.

Tests 8 -> 12.
 
37546 29 d 8 h amit /trunk/profitmandi-fofo/src/ web offer sync: compose detailed text from the parsed offer

detailedText is now built from benefit_timing + offer_benefit + offer_bank instead
of copying the circular's line:

Instant cashback. Rs.4,000 on Credit Card EMI; Rs.3,000 on Credit Card Full
Swipe. Banks: Axis Bank, ICICI Bank, State Bank of India (credit cards)

Every payment mode is always named, even when the circular's prose is terse
('10% CC EMI'), and the amounts come from offer_benefit - the authoritative
per-mode source. CC/DC cannot leak here at all, because the mode codes expand
natively rather than being patched by find-and-replace.

Three cases that would otherwise read badly:
- 21 offers carry NO benefit (circular cell 'NA', timing NONE). They are tenure-only
offers, so they read 'No-cost EMI. No cashback.' rather than showing a blank badge.
- The bank columns often list a bank under both card types while only one carries a
benefit. Card types shown are restricted to those actually paid on, or the text
would imply a debit offer that does not exist.
- benefit_timing UPI and NONE are not timings. Nothing is asserted rather than
inventing one.

smallText keeps the circular's short line - that column is only VARCHAR(256) and the
composed detail runs past it. One flowing paragraph, no line breaks, since the badge
may not render them.

Tests 8 -> 14.
 
37544 29 d 11 h amit /trunk/profitmandi-fofo/src/ web offer sync: expand CC and DC to Credit Card and Debit Card

The circular writes 'Rs.4000 Cashback on CC EMI'; a partner reading the badge should
see 'Credit Card EMI'. Applied to the title and the small/detailed text together, so
a badge and its detail never disagree.

Word-bounded and applied BEFORE truncation: an expansion is never cut in half, and
letters inside a word are untouched - the circular's cells carry strings like
'X300 Ext Kit' and 'ACCESSORIES', which an unbounded replace would corrupt.

On Sep'26 this affects 155 of 259 descriptions for CC and 48 for DC.

Two existing expectations updated to the new wording, and two tests added: one for
the expansion, one asserting DCX and ACCESSORIES survive intact.
 
37531 34 d 9 h amit /trunk/profitmandi-fofo/src/ offer circular: stop letter-led model names inheriting the previous variant

A part that is ONLY a memory spec inherits the preceding model name, because Oppo
writes 'RENO 15 PRO 256GB, 512GB' and the second part is not a product. The pattern
matched too loosely: the optional unit group matched the FIRST letter of a model
name and the rest fell through the trailing character class, so 'G06, G37, G37
Power' inherited its way to 'G06 G37' - two Motorola phones fused into one entity
that matches no SKU, costing G37 its cashback on five offers.

Requiring a leading digit makes it a memory spec rather than anything that merely
contains G/T and digits. Realme 'GT 7' matches the same way and survived only
because it never follows a comma.

Same root cause as the word-boundary guards on CAPACITY and MEMORY_PAIR: a digit
glued to letters belongs to the model name.

ProductNamesTest: 11 -> 12, covering 'G06, G37, G37 Power' and 'GT 7T, GT 7'.
 
37528 34 d 9 h amit /trunk/profitmandi-fofo/src/ offer circular: apply the supersede rule on ingest (fofo)

A published circular now expires still-running offers from earlier circulars, and
deactivates the web offers they produced, so the same cashback is never current
twice. The converse also holds: re-ingesting an older circular while a newer one is
live leaves it expired rather than resurrecting it.

Older offers remain readable as reference - only status changes, never end_date.

Counts appear in the ingest summary as supersededOlderOffers / expiredAsSuperseded
and, on the web side, supersededOlderMonths / notPublishedSuperseded.

ScopeConfigTest's stub gains the two new CircularIngestRepository methods - the same
brittleness the scripts/offer_circular README notes for JdbcIngestRepo.
 
37526 34 d 10 h amit /trunk/profitmandi-fofo/src/ offer circular: scope config screen, resolve screen, web offer sync (fofo)

Screens (each its own endpoint, under the OFFER CIRCULAR menu):
- /offerCircularScope - add/edit divisions, take a brand in or out of scope,
register label aliases. 'Remove' is in_scope=0 + a required reason, never a
DELETE: offer.division_id is an FK and the history would go with it.
- /offerCircularResolve - the product queue, split out of the review screen. The
editor was a <td colspan=6> pretending to be a form, which is why it never
aligned; it is now master-detail. Naming and Coverage are separate tabs because
an alias cannot answer a bundle at all - the coverage panel says so and offers
the two answers that ARE safe (ignore, or reclassify as naming).

Ingest:
- ScopeConfig resolves division aliases and carries the canonical label on
Decision. insertOffer and ProductAliases.find use it; offer_raw_row keeps the
verbatim label, being the source of truth for re-parsing.
- CircularIngestRunner publishes to dtr.web_offer after the document is marked
PUBLISHED, in its own transaction with exceptions swallowed - a circular that
parsed correctly must stay published even if the web sync fails.

Review screen:
- the ingest summary was a raw Map.toString() inside a nowrap span and ran off
the card; now parsed into chips with the drop reasons behind a disclosure.

jsVersion -> 417 (merged with r37525's 412; cssVersion 53 kept from that commit).
 
37337 50 d 14 h amit /trunk/profitmandi-fofo/ Move offer-circular ingest out of cron and into the portal

The parse now runs in profitmandi-fofo, on a background thread, triggered by the
upload that produced the document.

Why: splitting one feature across two artifacts with independent deploy cadences
cost a full day. fofo shipped, cron did not, and a valid upload sat in DRAFT with
nothing on the server able to parse it - the deployed cron jar contained none of the
ingest classes. One 9-page PDF a month never justified a batch tier, and the portal
already ships two PDF stacks, so the isolation argument for keeping PDFBox out was
weaker than it looked.

- 13 parser classes move verbatim from com.smartdukaan.cron.offercircular to
com.spice.profitmandi.web.offercircular. No logic changed.
- CircularIngestScheduler becomes CircularIngestRunner: the @Scheduled(every 5 min)
entry point and the offer.circular.ingest.enabled flag are gone, replaced by a
single-threaded daemon executor. All claim, ingest and notification logic is
unchanged.
- Upload hands the document id to the runner AFTER COMMIT, not inline. The DRAFT row
is written inside the request transaction; a worker starting immediately would race
that commit, find nothing to claim and silently do nothing - which is precisely the
stuck-on-DRAFT symptom this change removes.
- The guarded claim is KEPT even though there is now one trigger. It still stops a
double-submit, a second portal node, and a re-ingest racing an in-flight parse.
- Re-ingest parses immediately instead of queueing for a scheduler.
- The stall reaper runs when the review screen loads. There is no timer here any
more, and a document stranded by a redeploy mid-parse only matters when somebody
looks for it - which matters more now the parse lives in the web application.
- tabula moves to this module with its exclusions intact, as does the
dumpCircularClasspath helper the local ingest harness depends on.
- Screen no longer claims "the ingest job runs every 5 minutes", which was untrue the
moment cron stopped being the route; poll interval 15s -> 3s to match a parse that
takes seconds. jsVersion 404 -> 405.
- offer.circular.review.url added here, since the runner sends that email now.

Verified: full ingest of the Aug'26 circular through the relocated code is identical
to the reference - 239 offers, 663 products, 279 AUTO_EXACT, 361 benefits, 511
tenures, 911 bank links. ProductNamesTest 11/11 in its new home.