Subversion Repositories SmartDukaan

Rev

Go to most recent revision | Show changed files | Directory listing | RSS feed

Filtering Options

Rev Age Author Path Log message Diff
37572 30 d 13 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 30 d 13 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.
 
37570 30 d 13 h ranu /trunk/profitmandi-fofo/src/main/webapp/WEB-INF/views/ftl/ drr view color change  
37569 30 d 13 h amit /trunk/profitmandi-cron/src/main/java/com/smartdukaan/cron/scheduled/ fcm: stop holding a row lock on the whole batch across every FCM call

sendNotification lived in ScheduledTasks, which carries a class-level @Transactional,
so one pass was one transaction: it loaded EVERY pending row uncapped, then per row
made an FCM call over the network and stamped sentTimestamp on the entity. A dirty
row stays write-locked until commit, so a run held a lock on every notification in
the batch for the SUM of all the remote waits. Volume is real -- 11,581 sent on
2026-09-09, 12,036 on 2026-09-05 -- and HttpClientFactory sets a 10s socket timeout,
so one bad run could hold thousands of locks for minutes.

That is the shape behind a 33 SECOND average InnoDB row-lock wait server-wide
(Innodb_row_lock_time 63,291s over 15.4 days uptime, max 51,941ms against a 50s
innodb_lock_wait_timeout -- which is where LockAcquisitionException comes from).

Moved to PushNotificationSendService using the same seam the invoicing pass already
uses in InvoiceService.updateIrnsToInvoices: one short read transaction to pick up
the batch and build the payloads, NO transaction across the network call, and one
REQUIRES_NEW transaction per row to record the outcome. No lock is held while waiting
on FCM.

It is a separate bean, and reaches its own transactional methods through an injected
self-reference, because REQUIRES_NEW is applied by a Spring proxy and a plain
self-invocation would bypass it -- same idiom, for the same reason, as InvoiceService.

Also caps the batch at 500/run (30,000/hour against a 12,000 peak day) using the
already-present but never-wired selectPendingNotifications(limit); the previous query
was uncapped, which is what let a campaign burst become one multi-minute transaction.
Send outcomes are unchanged: 200 stamps now, anything else stamps the 1970 sentinel.
The HttpClient is now closed, which it previously was not.
 
37568 30 d 13 h amit /trunk/profitmandi-fofo/src/main/java/com/spice/profitmandi/web/offercircular/ offer circular: a failed web-offer sync must not be silent

Swallowing the exception was right - a circular that parsed correctly must stay
PUBLISHED, and the sync is re-runnable while the parse is the expensive part.
Swallowing it SILENTLY was not.

A TINYINT(1) cast threw in the sync on every ingest from 2026-09-08 to r37553 and
nothing looked wrong: processed_at advanced, the circular published, the email said
success. The only symptom was web_offer.synced_at standing still, which nobody was
watching. It went unnoticed for two days.

Now:
- Summary carries webOfferSync, and states it in EVERY case - the counts on success,
'FAILED - <cause>' on failure, 'NOT RUN' if it never executed. A missing line would
be indistinguishable from a sync that never ran, so there is no missing line.
- That lands in offer_document.ingest_summary, so the review screen shows it.
- The notification email carries it too, and a failure renders as a red
'Web offers NOT updated' block saying partners are still seeing the previous
offers and that the parse itself is fine.
- The sync now runs BEFORE published(), so its outcome is part of what gets recorded
rather than an afterthought. It is still REQUIRES_NEW with its exception caught, so
it still cannot fail the ingest.
 
37567 30 d 13 h amit /trunk/profitmandi-cron/src/main/java/com/smartdukaan/cron/ Schedule the catalogue delist daily at 05:30

Runs 30 minutes before the existing 06:00 scheduledPushDataToSolr in the same
class, so the same morning's reindex publishes it. A bulk UPDATE fires no
TagListingChangeListener event, so without that ordering the portal would lag
until 18:00.

Gated on catalog.autoDelist.enabled with an inline default of true - env profiles
have no fallback between each other, so a key missing from one profile's
properties would otherwise stop the context booting.

Also exposes --delistDeadListings for a manual run, honouring the same flag.
 
37566 30 d 13 h amit /trunk/profitmandi-dao/src/main/ Delist logic v3: require a month of no movement; add CatalogDelistService

Zero stock alone was too weak a test. It fired on SKUs partners were still
transacting, where the recent movement was often the very sale that emptied the
stock. v3 additionally requires no movement for a month on either side:
- SmartDukaan via warehouse.scanNew (3.4M rows, live). Note warehouse.scan is a
dead Saholic relic - 2,476 rows, last write 2012.
- any partner via fofo.scan_record, any scan type.
Phrased as NOT EXISTS so they short-circuit on the first recent row and use the
(inventoryItemId, scannedAt) / (inventory_item_id, create_timestamp) indexes.
An item that has never moved still qualifies, which is correct.

Also in v3:
- the internal pseudo-brands (Dummy, FOC, FOC HANDSET, Live Demo) are no longer
excluded and are swept like anything else.
- vendor-PO guard narrowed to status = 1. PARTIALLY_FULFILLED is UNREACHABLE,
not merely rare: none of the 8 PO-status writes in the codebase sets it, no
native SQL writes the column, and status = 2 has 0 rows across 51,593+ POs
back to 2011. Partial receipt is modelled per line (unfulfilledQuantity), so a
part-received PO sits in READY and is already caught. Documented so it is not
'restored' later as a supposed gap.

CatalogDelistService/Impl mirrors the migration statement for statement, for the
daily scheduled run. Bulk SQL rather than per-row updateActiveById +
publishStatusChange, which would commit to Solr once per catalog (~1,750 times
on the 2026-09-10 backlog); the job is instead scheduled ahead of the existing
06:00 full reindex. The dark-model step is deliberately house-wide rather than
run-scoped so it converges instead of leaving pre-existing dark models on a
stale RUNNING forever.

rollback_delist_recent_movement_20260910.sql restores the 24 listings / 21
models that the v2 run delisted before NEW 3 existed; their partners had
transacted them within the month. Id list frozen, not re-derived.
 
37565 30 d 22 h amit /trunk/profitmandi-cron/src/main/java/com/smartdukaan/cron/ IMEI activation: 20s tick lanes for oppo/realme/vivo, idle once the day clears

Replaces the two once-a-day passes with chunk-per-tick lanes. Each tick takes one
chunk and returns; when a brand's pool comes back empty its turn is skipped, and
when everything is clear the ticks do nothing and stay silent until midnight.

- browser lane (StandAlone): one chunk of 25 for one brand every 20s, oppo and
realme by turns. Still exactly one ChromeDriver alive at a time.
- vivo lane: its own tick, chunk of 250. It must not share the browser lane --
0.24s an imei against oppo's 10.2s means it would need ~70 hours behind them
for work it does alone in 39 minutes.

The snapshot is gone; the pool query is the cursor. That is what makes a restart
cost one chunk instead of the day: the 12:03 restart on 09-Sep forfeited ~4,000
lookups and the whole afternoon, and last_finish had read -1 for three days.

The snapshot existed to stop the re-ask loop (realme, 29-Aug: 4,524 requests
against 1,004 distinct imeis). That is now closed at the source instead -- oppo,
realme and motorola stamp every imei they asked about, not just the ones that
produced a map entry, so a failed lookup rests until tomorrow rather than coming
back on the next tick. Motorola is fixed pre-emptively; nothing schedules it yet.

Also:
- secondary and tertiary are merged by turns rather than concatenated. Safe while
a pass walked to the end; without that guarantee oppo's 163 tertiary serials sat
behind 3,819 secondary ones and would only be reached on a day that cleared.
- vivo abandons a tick rather than the chunk when the captcha solver returns no
code -- one probe per 20s while it is down instead of 250, and no rows rested
over a transient outage.
- the funnel gauges move from a pass to a day. due is measured on the first tick
after midnight, the rest accumulate, and last_finish_epoch becomes a real
completion clock. Truncation is detected at the midnight rollover, which is the
case that never reaches an end-of-run at all.

This does not create capacity. At the 21s/imei measured on 09-Sep the pool still
needs ~35 hours and will not clear; it now rolls over visibly instead of silently.
The lever for that is DAYS=1.
 
37564 30 d 22 h amit /trunk/profitmandi-fofo/src/main/ Creation screens request only live listings from the item typeahead

/item takes an activeOnly flag (default false, preserving current behaviour)
and passes it to getAllPartnerItemStringDescription. getItemAheadOptions takes
an optional 4th argument, so existing 3-argument callers are unaffected.

Sent true from the two creation entry points:
- warehouse purchase (PO) invoice line picker
- the new-price-drop modal (#typeaheaditem)

Left false where the screen works on records that already exist and may since
have been delisted: the price-drop pause/manage picker (#typeaheaditem1),
catalog-item, combo and prebooking.

jsVersion 419 -> 420 to cache-bust common.js / warehouse-purchase.js (419 was
already taken by another change).
 
37563 30 d 23 h amit /trunk/profitmandi-dao/src/main/ Filter delisted SKUs out of creation pickers; add rolling 24m delist migration

Creation screens could offer items whose catalog.tag_listing.active = 0:
- Item.selectAllModels (scheme/offer model dropdown) joined tag_listing with
no active predicate. Adding tl.active = true also gives the model-level
rollup, since callers dedupe to catalogItemId: a model whose colours are
all inactive now yields no rows, one keeping a listed colour stays.
- The shared partner item picker had no active filter at all. Added a second
basis via getAllPartnerItemStringDescription(anyColor, activeOnly);
activeOnly participates in the fofoItems cache key so the two bases cannot
serve each other's cached results. Default stays false for screens that
inspect or edit existing records.
- New TagListing.selectActiveItemIds named query backs the filter.

Also adds the rolling 24-month delist migration: zero stock both sides, no
outstanding vendor PO (status IN (1,2), external supplier, unfulfilled > 0 --
INIT is excluded as it is a drawer of stale drafts), and re-categorises
fully-dark mobile models to OTHER. Idempotent, audited, rollback documented.
 
37562 31 d 1 h amit /trunk/profitmandi-dao/src/main/resources/sql/ Add SQL to delist pre-2025 Samsung listings with no stock

Marks 738 catalog.tag_listing rows (tag_id 4) inactive with eol_date
2026-09-08. These are Samsung listings published before 2025-01-01 holding
no stock in any SmartDukaan warehouse and none at any active partner store.

Id list is frozen from the prod evaluation rather than re-derived, so the
same listings are delisted in every environment; stock tables in a copy
drift from prod. Script is idempotent and carries guards, verification and
a rollback block.

Applied to prod hadb1 and the local dev copy.
 
37561 31 d 7 h ranu /trunk/profitmandi-fofo/src/main/webapp/WEB-INF/views/ftl/ ticket download option given and some enhancement on notification panel  
37560 31 d 7 h ranu /trunk/profitmandi-fofo/src/main/ ticket download option given and some enhancement on notification panel  
37559 31 d 8 h ranu /trunk/profitmandi-fofo/src/main/ ticket download option given and some enhancement on notification panel  
37558 31 d 8 h ranu /trunk/ system generated leak bgc l2 can assign to l1 without any geo location  
37557 31 d 10 h ranu /trunk/profitmandi-fofo/src/main/java/com/spice/profitmandi/web/controller/ system generated leak bgc l2 can assign to l1 without any geo location  
37556 31 d 11 h ranu /trunk/profitmandi-fofo/src/main/webapp/WEB-INF/views/ftl/ loi process added, revival and code changes process  
37555 31 d 11 h amit /trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/dao/repository/offers/ web offer sync: one value per SKU per payment mode

The circular says two things about the same phone. Motorola Sep'26 page 9 names
'G37 Power' generically at Rs.1,000 CC Full Swipe (row 12) and 'G37 Power (8+128)'
at Rs.2,000 (row 13), so G37 Power 8+128 carried both badges and a partner could
quote either.

Precedence, per (catalog_id, txn_mode):
1. a specifically named VARIANT beats a generic MODEL - the named line is the OEM
being precise about that SKU
2. at the same level, the higher value wins
3. tie-break on lowest offer id, so the outcome is deterministic

The generic line still covers every variant nobody named: G37 Power 4+64 keeps its
Rs.1,000 Full Swipe while 8+128 takes Rs.2,000. Verified on the live data - 1026311
now resolves to exactly CC_EMI 2500 and CC_FULL_SWIPE 2000, down from four competing
values.

An offer keeps a SKU if it wins on at least one of its own modes, since one badge
carries every mode of its offer. Scheme-only offers are never filtered - no value to
compare.

Deliberately NOT reusing v_offer_applicable, which encodes the same VARIANT-beats-
MODEL rule but has no document or status filter: once August is re-ingested its rows
enter that view and an EXPIRED August variant would silently suppress a live
September model. Scoping to one document avoids that; the view is left alone for
other consumers.

Measured on Sep'26: 557 product links -> 458, 191 badges -> 166.
 
37554 31 d 12 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.
 
37553 31 d 12 h amit /trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/dao/repository/offers/ web offer sync: all_banks is TINYINT(1), so never cast it to Number

ClassCastException: java.lang.Boolean cannot be cast to java.lang.Number, thrown from
selectPublishableOffers on every ingest since 2026-09-08. Confirmed against the live
driver: SELECT all_banks returns java.lang.Boolean.

MySQL Connector/J maps TINYINT(1) to Boolean while Hibernate may hand back a Number,
so the value must be read through isTrue() - the same shape as ScopeConfig.isTrue and
OfferScopeRepositoryImpl.isTrue, both of which exist for exactly this reason and both
of which I had in front of me when writing the cast.

The failure was silent by design: CircularIngestRunner swallows sync exceptions so a
web-offer problem can never fail a good ingest. The circular kept publishing, the
document's processed_at kept moving, and the only symptom was that web_offer.synced_at
stopped advancing - which is what should have been checked before reporting the sync
as working on 8 September.
 

Show All