Subversion Repositories SmartDukaan

Rev

Show changed files | Details | Compare with Previous | Blame | RSS feed

Filtering Options

Rev Age Author Path Log message Diff
37723 3 d 0 h amit /trunk/profitmandi-fofo/src/ Bulk-uploaded movement rows are validated on screen; popup shows arriving stock per PO

Bulk upload only fills the PO screen - the PO is created from the screen. It used to price
with resolvePrices(qty), so one row over what could move rejected the whole file. It now prices
from describeAvailability (same cost layer order creation uses) and never refuses on quantity:
over-cap rows turn red on render, createPO blocks while any row is red and lists them, and
the server checks again on create. An item with no stock and nothing arriving still fails
the upload (nothing to price).

Popup reads in stock + arriving (per PO, <- supplier) - promised (per PO) = can move; the
'Of these, still to arrive' line is gone. Tests for the Oppo A6 UP->Noida case. jsVersion 433.
Needs dao r37722.
 
37711 3 d 13 h amit /trunk/ Bulk-uploaded PO rows show the same availability breakdown as hand-picked ones

A row added by hand showed what the warehouse holds, what older orders have promised and how many
units the order could take; the same row arriving from a bulk upload showed none of it. The file
was the one place the numbers behind a quantity were hidden, which is the case where a mistake is
least visible and hardest to unpick afterwards.

describeAvailability now also answers for a list of items. Both reads it needs already took a
list, so a whole file costs the same two queries a single item does rather than two per row. An
item the warehouse holds nothing of is left out of the result instead of failing the upload - its
quantity is still checked when the order is priced, and the row simply shows no breakdown.

The single-item and batch paths build their answer from one shared method, so the two cannot drift
apart, and the screen reuses the renderer it already had.
 
37707 3 d 13 h amit /trunk/profitmandi-fofo/src/main/ GRN price mismatch: say which PO the invoice will be received against, and what to do if it closes first

Resolving a price mismatch discards the PO line and raises a replacement dated to the invoice. That
replacement is what the invoice is received against, so it is the one that has to stay open long
enough for the stock to arrive - and it closes on its own after four days for a movement, six for a
vendor order. Until now nothing said so: the GRN simply stopped matching and the correction sat
holding stock that had already arrived.

The confirmation now states what is about to happen and what to do if the stock lands late, and the
request screen explains what Po Id is. Reopening is offered only where it exists - a movement can be
reopened from Purchase Orders, a vendor order cannot and needs a fresh PO.
 
37705 3 d 13 h amit /trunk/profitmandi-fofo/src/main/ PO list: show the transaction id so a movement can be found from the invoice being received

An internal movement is raised as a transaction, so the invoice on the GRN desk leads straight back to its PO
through that id. The list is a DataTable, so having the column makes it searchable - which is how someone finds
the PO to reopen when a receipt arrives after auto-close, instead of hunting through date ranges.

Placed after Status so existing column positions are unchanged.
 
37704 3 d 13 h amit /trunk/ Reopen a movement PO whose stock arrived late, and stop stranding GRN price corrections

Internal movements auto-close after four days, which fits 99.6% of them - 5,491 of 5,515 receipts
land inside the window. The remainder leave the PO closed with the stock still in transit and
nowhere to receive it: 998 internal POs closed during 2026 still holding 21,996 unreceived units.

A closed movement PO can now be reopened from the purchase order list. Reopening stamps
reopenedAt, and auto-close measures from WarehousePurchaseOrder.getOpenSince() - reopenedAt when
set, the PO date otherwise - so a reopened PO gets the same fresh window a new one gets instead of
being closed straight back on the next sweep. Only movements between our own warehouses: an
external vendor PO that has closed is settled with that vendor, not reopened unilaterally.

Separately, a GRN price correction now checks that the PO it just raised is one the invoice can
actually be received against. Matching reads POs that are open and approved for the same supplier
and warehouse dated on or before the invoice; it never looks at the PO being corrected, so what
matters is that the new PO is receivable. 54 were not - backdated into INIT by the old approval
gate, hence outside the match - and each stranded silently: original line discarded, GRN completed
without it, the correction left holding a reservation for stock that had already arrived.

isOpen() names the open set - INIT, READY, PARTIALLY_FULFILLED - that the movement and commitment
queries already read.

Migration sql/add_po_reopened_at_20260918.sql adds reopenedAt, nullable and additive. It must run
before this ships: the entity maps the column.
 
37701 3 d 17 h amit /trunk/profitmandi-fofo/src/ PO create: expected stock counts toward the quantity, popover wording follows

Stock still to arrive is now part of what the order can take, so the breakdown reports it as a
qualifier on that number rather than as something set aside - 'Of these, still to arrive' instead
of 'On the way in (not yet received)', which read as though it could not be ordered.

Tests follow the two rule changes: expected receipts can be ordered against, a quantity beyond
what is held and expected together is still refused and says so, and availability counts expected
receipts toward what can move. 21 tests pass.
 
37699 3 d 19 h amit /trunk/profitmandi-fofo/src/main/ Fix: movement availability popover rendered empty except the closing note

The breakdown was built as a table. Bootstrap 3.4.1 sanitizes popover content against a
whitelist that includes neither table nor its rows and cells, and it removes a non-whitelisted
element together with everything inside it - so the whole table was dropped and only the note
survived, leaving a popover that referred to orders that were not shown.

Rebuilt from divs and spans, which are whitelisted. Sanitizing stays on rather than being
switched off for this popover: the content carries PO numbers and warehouse names read out of
the database.

Also stops the zero-quantity note pointing at orders that are not there - a warehouse whose
only stock is still inbound has nothing promised to point at. jsVersion bumped.
 
37697 3 d 19 h amit /trunk/profitmandi-fofo/src/ PO create: show what can move, and which orders hold the rest, while the quantity is typed

Picking an item on a movement order already called getPricing, which read the sending
warehouse's stock and returned only a price. It now returns the availability too, so the row
can show it: an info marker beside the quantity box opens the breakdown - in stock, promised
with each holding order named by PO number and date, anything still arriving, and the quantity
this order can take.

The quantity box flags the moment what is typed passes that cap, so it is corrected before
submitting rather than after being refused. The marker turns amber when stock is partly
promised and red when none can move.

Outside vendors are unaffected: their pricing response carries no availability and their rows
are left exactly as they were. jsVersion bumped so the screen picks up the new script.

Tests cover the four cases that matter: what can move with orders named, zero movable reported
rather than refused, this order's cap kept separate from what the warehouse holds when stock
came in at two costs, and a warehouse holding none of the item still refusing.
 
37670 4 d 21 h amit /trunk/profitmandi-fofo/src/main/ Check GSTIN shape on the onboarding screen before submit (jsVersion 425)

Both GST boxes are validated client-side and must match, so ops see the problem on the screen
instead of discovering it when NIC refuses the first invoice. Server-side guard in dao r37669
is what actually enforces it.
 
37643 6 d 0 h amit /trunk/profitmandi-fofo/src/main/ Hot Deal brand: admin edits attributes + optional OEM catalog mapping; brand/date/cap controls removed; jsVersion 424  
37632 6 d 3 h aman /trunk/ fix(loi): don't leave an LOI half signed when the signed PDF fails to save

Confirm Sign stores the partner OTP (/validateLoiOtp) before the browser builds and uploads the
signed PDF (/saveLoiDoc). When that second step failed - html2pdf not loaded from cdnjs, upload
rejected, tab closed, server error - nothing was shown, no LOI mail went out, no onboarding was
created, and because loiOtpPresent hid Generate LOI the filler could never re-sign (LOI 734).

- pendingFormList: loiOtpPresent only when OTP AND loiDoc are both saved; new loiSignIncomplete
flag shows a red "signed LOI not saved" note on the Pending LOI list (web + V2 app).
- loi-form.js Confirm Sign: every failure after the OTP says the LOI was NOT saved and how to
retry; checks html2pdf is loaded; success alert only after /saveLoiDoc confirms; no double submit.
- /saveLoiDoc (fofo + V2): refuses a document without a verified OTP; a repeat call for an
already saved LOI is a no-op (no second mail / completion).
- /validateLoiOtp (fofo + V2): clear message when no OTP was sent in 24h; reports the real
rejection reason (e.g. OTP already used) instead of always "wrong".
- sendSignedLoiPdfToPartner: filler without a manager no longer throws and rolls back the save.
- OtpProcessor.generateOtp: never hands back an already verified OTP under the 2-minute throttle.
- AppConfig version 423 for loi-form.js.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RhJD2sc6pf3Zd7f4hyGxhH
 
37630 6 d 4 h amit /trunk/profitmandi-fofo/src/main/java/com/spice/profitmandi/web/ feat(mail): wire inactive-recipient filter and clean hardcoded addresses

Wire MailRecipientFilter into googleMailSender and gmailRelaySender. Remove inactive/unknown addresses from recipient and access lists, bulk uploader gate to akhil.kumar.
 
37620 7 d 1 h amit /trunk/profitmandi-fofo/src/main/ Offer creation suggests only active models unless asked for all

/entity accepted activeOnly but always searched with false, so the offer
screen's model picker listed every catalog of the brand, delisted ones included.
It now honours the flag. The default becomes false so the pages that never pass
it keep their current results; the notification product search, which already
asked for active-only, now gets it.

Each item-criteria block gets an Include inactive checkbox, off by default.
Ticking Exclude ticks it too: a brand-level offer still pays on delisted models
partners hold stock of, and they cannot be excluded if they cannot be picked.
Inactive models are labelled, and reloading the list keeps picks still present.

The reload flag is now per block; the old global one let a brand change in one
block be consumed by opening another, leaving the first with a stale list.
Brand names are URL-encoded so a brand like Ai+ is not sent as 'Ai '.

Requires profitmandi-common r37619 for the inactive label.
 
37593 9 d 22 h amit /trunk/profitmandi-fofo/src/main/ Show partners their aged Apple stock and why it lowers their credit limit

Apple handsets held past 30 days are deducted when the credit limit is
calculated, but nothing told the partner - they saw a limit drop with no
explanation. Adds a dashboard banner alongside the existing 'activated but not
billed' one, shown only when the partner actually has such stock (70 of 980
today), with the unit count, the value, and View Imeis for the detail.

- dashboard-aged-apple-stock.vm, parsed in dashboard1.vm under
#if($investments.aged_apple_qty > 0)
- count and value both read from the maintained partner_investment snapshot, so
the banner never runs its own query and always matches the amount the limit is
actually reduced by
- amount wrapped in <span class="currency"> so common.js formatCurrency() renders
it instead of a raw float
- /agedAppleImeis + aged-apple-imeis.vm list the units, excluding activated ones -
those are already deducted as activated stock and shown in their own banner, so
no handset appears in both
- click handler added to the existing activated-imeis.js rather than inline in the
fragment: an inline script in an AJAX-injected fragment re-registers
$(document).on on every load and stacks duplicate requests
- jsVersion 420 -> 421
 
37564 11 d 15 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).
 
37559 12 d 0 h ranu /trunk/profitmandi-fofo/src/main/ ticket download option given and some enhancement on notification panel  
37543 13 d 6 h amit /trunk/profitmandi-fofo/src/main/ receive debit note: resolve the DOA section from the declared condition, not from a change

The condition dropdown renders with the partner's declared value already selected, so
a declared BAD - the ordinary DOA case - fired no change event and the DOA certificate
radios stayed hidden. The operator could not supply a certificate, and receiveDebitNoteItems
then refused the receipt for a missing one, making those debit notes unreceivable.

applyDeclaredConditions seeds each dropdown from the row's data-declared and settles the
dependent sections on load; the show/hide logic moves out of the change handler into
syncReceiveConditionState, which reads the current state of the form rather than whatever
was last touched. The change handler now calls the same function.

The server-side 'selected' stays as a fallback: without it a declared BAD would present as
GOOD if the script failed to load, and the server reads that difference as the warehouse
disagreeing with the partner, which rejects the whole debit note.

jsVersion 417 -> 418.
 
37526 18 d 0 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).
 
37523 18 d 17 h ranu /trunk/ loi process added, revival and code changes process  
37513 19 d 23 h ranu /trunk/ notification live  
37493 20 d 5 h amit /trunk/profitmandi-fofo/src/main/ Fix: scheme item inline date edit fired N duplicate PUTs per click

scheme-details.vm is injected with .html(), which re-executes its inline
<script>, and a successful save re-injects it - so each load stacked another
$(document) handler for .edit/.cancel/.save-item-dates. One Save click then
fired N identical PUT /scheme/item/window, and PostInterceptor rejected all
but the first with 'Duplicate request.' (27 per click in prod today).
The stacked copies also carried a stale $scheme closure, so a save could
repaint the container with a previously viewed scheme.

Move the handlers to scheme.js (loaded once via include-scripts.vm) and read
the scheme id/window from data- attributes on the fragment root, keeping them
stateless. Fragment is now markup only.

Reuse cleanup while in here:
- configureMultiselect() replaces 3 near-identical multiselect configs
- loadBrandsByCategory/loadCatalogDescriptionByBrands take an optional
afterRender so the add-item modal reuses them instead of duplicating both
- toggleItemDateEdit()/initSingleDatePicker() collapse mirrored blocks
- SCHEME_DETAILS_CONTAINER single-sources the container id
- drop the template's duplicate toIsoDateTime (scheme.js already had it)
- updateSchemeItemWindow() now prefixes context like every other call

Bump jsVersion 409 -> 410.
 
37480 21 d 0 h amit /trunk/ mail: correct a wrong claim in r37479 -- the Google app password IS valid

r37479 stated googleMailSender's app password was rejected. That was wrong. The
test behind it resolved smtp.gmail.com over IPv6; repeating it over IPv4 with
the same credential gives AUTH OK on both 465 and 587.

The real fault is not the credential and not the bean config, both of which are
correct. SMTP from this host works over IPv4 only:

smtp.gmail.com IPv4 -> AUTH OK IPv6 -> 535 5.7.8 Username and Password not accepted
smtp-relay IPv4 -> 250 MAIL FROM IPv6 -> 550 5.7.1 Invalid credentials for relay

The JVM prefers IPv4, which is the only reason mail leaves this box at all.
Anything that prefers IPv6 fails on both paths.
 
37479 21 d 0 h amit /trunk/ mail: make the relay the default sender, not the Google identity

Correcting r37474-37476. Those made googleMailSender @Primary on the assumption
its credentials worked. Tested against the live servers from the prod host:

googleMailSender 535 5.7.8 Username and Password not accepted (BadCredentials)
-- the app password in the source is no longer valid
relay over IPv4 250 OK for MAIL FROM:<noreply@smartdukaan.com>
relay over IPv6 550 5.7.1 Invalid credentials for relay

So promoting google would have replaced one broken default with another. The
relay is what actually delivers today and it becomes 'mailSender'. It does not
authenticate -- Workspace authorises it by allowlisted source IP -- so sending
as noreply@ is legitimate there and AuthenticatedIdentityMailSender correctly
leaves it alone.

googleMailSender stays available by qualifier. Point the default back at it once
a valid app password is issued for sdtech@smartdukaan.com.

Also noted in the javadoc: the relay allowlist covers the IPv4 address only, so
anything that prefers IPv6 will be refused.
 
37476 21 d 0 h amit /trunk/profitmandi-fofo/src/main/java/com/spice/profitmandi/web/config/ mail: delete the dead SendGrid bean from fofo, make the authenticated identity primary

googleMailSender is now @Primary and answers to 'mailSender'. Two senders remain:
the authenticated Workspace identity and the IP-authorised relay.
 
37458 23 d 4 h aman /trunk/profitmandi-fofo/src/main/ LOI payment screenshot: report upload failures instead of reporting the field as missing

Submit validated the hidden #payment-sc-docId, but a file input keeps displaying the
chosen filename regardless of what happens next - so a failed /document-upload left the
field looking attached to the operator and missing to the validator ("The following
fields are required: Payment screenshot" on a form that was in fact filled in).

The failure was silent three ways: doAjaxUploadRequestHandler had no error handler; the
global ajaxError reporter calls bootbox, which loads from cdnjs and throws if that is
blocked; and the confirm('Confirm file upload ?') gate returned false with no trace once
Chrome's "prevent this page from creating additional dialogs" box was ticked.

- common.js: optional error callback on doAjaxUploadRequestHandler (additive, opts out of
the generic global dialog via suppressGlobalError); uploadErrorMessage() extracts the
server's real reason from the JSON body or the plain-text duplicate-request body;
uploadDocument() pre-checks size/type, verifies a document_id actually came back, and
reports a message naming the file - .HEIC and .webp get specific guidance;
showAlert() falls back to native alert when bootbox is absent.
- loi-form.js: drop the confirm() gate, 'input' -> 'change', and clear the file input
whenever the id does not land so the widget can never claim an unuploaded file. Status
line under the field (Uploading... / Attached: name / Not attached). Submit now
distinguishes not-picked, still-uploading and upload-failed. Same treatment for the
OnboardingDocumentTable and mk_brand-fee-collection-details uploads, and the brand-fee
dialog names the Attachment field instead of echoing the raw form key.
- loi-form.vm, pendingForm.vm: accept="..." on both file inputs plus the status span.
- FileUploadController: reject empty files, report actual size against the actual limit,
and resolveExtension() falls back to the filename when the browser sends a generic MIME
type (Android file managers send application/octet-stream for ordinary JPG/PNG/PDF).
- AppConfig: static resource version 395 -> 396 for the JS change.
 
37455 23 d 10 h amit /trunk/profitmandi-fofo/src/main/ Make the KYC state a master-backed dropdown instead of free text

fofo-kyc-row.vm and legal-kyc-row.vm captured state as a free-text input, so
nothing stopped a spelling the master does not hold. That state is resolved back
through inventory.statemaster by name later, and an unresolvable one cannot be
turned into a state code. Both now render a $stateNames select, and all five
render paths in PartnerOnBoardingPanelController supply the list.

panel-listing.js reads these fields positionally, by td index and
input[type='text']. A select does not match that selector, so the value would
have come back undefined and KYC would have saved a blank state silently -
both selectors now target the select. Row editing is unaffected: the enable
toggle uses :input, which covers select.

Bump the asset version for the panel-listing.js change.
 
37400 26 d 21 h amit /trunk/profitmandi-fofo/src/main/ Merge the three sale-invoice download endpoints into /invoice/download

generateInvoice, generateInvoices and downloadInvoices differed only in how they
resolved the order ids and who was allowed to ask; everything after that was the
same. That drift meant only the single-order download stapled the policy
certificates, so the same invoice pulled from sale history came out without
them. One handler now selects by orderId, partner date range (admin only) or the
caller's own sale-history search, over reusable resolvers plus a shared
render-and-respond step. The old URLs stay as deprecated shims.

The admin range download is now fault tolerant - one unbillable order used to
fail the whole batch.

Remove the commented-out thermal variant of generateInvoice and the unused
paymentOptionIdPaymentOptionMapUsingPaymentOptions. Bump the asset version for
the sale.js change.
 
37369 32 d 6 h amit /trunk/ Fixed mail sender everywhere  
37365 32 d 21 h amit /trunk/profitmandi-fofo/src/main/ Debit note receive: surface the partner-declared condition and warn before rejecting

The receive screen defaulted every row's condition to GOOD regardless of what the
partner declared, so a warehouse user could not see what they were disagreeing with -
and the backend treats any disagreement as a rejection of the whole debit note.

- Show 'Declared By Partner' per row and preselect the condition to match it, so
submitting unchanged is a genuine agreement rather than an accident of the default.
- A changed row now switches the submit button to 'Reject Return', shows what rejection
means (no refund; partner, Logistics, RBM and Sales notified), makes the remark
mandatory and confirms before posting.
- SMTP timeouts raised to 30s connect / 120s read-write; the 10s read timeout was
cutting off larger attachment sends.
- jsVersion 405 -> 406 for the return.js change.
 
37337 34 d 4 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.
 
37335 34 d 6 h amit /trunk/profitmandi-fofo/src/main/ Show offer circular ingest state on the review screen

Upload and parsing run in different processes - the portal stores the PDF as DRAFT
and the cron job claims it minutes later - so the screen looked identical before and
after a successful ingest. A reviewer had no way to tell "not parsed yet" from
"parsed and produced nothing", and a FAILED circular showed nothing at all.

- the selected circular's state is shown as queued / parsing / parsed / failed,
with the recorded reason on failure and the ingest summary on success
- while it is DRAFT or PROCESSING the screen polls every 15s and pulls in the rows
the moment the parse lands. Polling stops as soon as the state settles, so it
cannot spin forever, and a transient error keeps waiting rather than giving up
- jsVersion 403 -> 404, since offer-circular-review.js is already being served and
this is a modification rather than a new file

Pairs with the stall reaper: a circular abandoned mid-parse is now both recovered
and explained on screen instead of sitting silently in PROCESSING.
 
37327 35 d 0 h ranu /trunk/ super retailer club 5 live  
37270 42 d 7 h amit /trunk/profitmandi-fofo/src/main/ Hot deals admin: activated -> fresh (labels, params, row editors); manual Push to Solr for selected models (checkbox selection + /hotDeals/pushSolr calling FofoSolr.updateSingleCatalog directly, bypasses prod-gated event so Category team curation is searchable immediately); jsVersion 395  
37256 46 d 2 h amit /trunk/profitmandi-fofo/src/main/ feat: hot-deals admin — all-brands searchable/paginated table

- /hotDeals/all replaces per-brand /hotDeals table: server-side search (model/
brand), 20/page with prev/next, Brand column, newest window first
- /hotDeals/slots: active-slot usage per brand (n / 15) for the add form
- jsVersion 394
 
37246 46 d 7 h amit /trunk/profitmandi-fofo/src/main/ Hot deals admin: five mandatory attribute inputs (no defaults) on add + inline row edit, table columns, updateDeal params; jsVersion 393  
37222 48 d 2 h amit /trunk/profitmandi-fofo/src/main/ external api v2: Technology > External API Clients admin screen (read-only client/store/category mappings), jsVersion 392  
37212 48 d 5 h amit /trunk/profitmandi-fofo/src/main/ Hot Deals: Category-team page under Catalog menu - brand-wise hot deal models with inline date editing (manage-hot-deals)  
37194 52 d 0 h aman /trunk/ LOI OTP: report real send outcome instead of always 'OTP Sent Successfully!'

The Send OTP button showed success for any HTTP 200, including the daily-cap
failure where the controller returned 'Something went wrong..'. Ops could not
tell a working send from a broken one, so genuine failures were invisible.

- OTPResponse: add 'sent' flag. result=true did not mean anything was
dispatched - the 2-minute resend throttle returns the previous OTP without
contacting the gateway.
- OtpProcessor: set sent=true only on branches that actually dispatch.
- LoiFormController.sentLoiAcceptanceOtp: return {status, sent, message} with
the processor's real reason, wrap generateOtp in try/catch so a dispatch
exception is logged and reported instead of surfacing as a generic 500, and
log loiId/result/sent/reason.
- loi-form.js: check the payload; distinct green/amber/red states, re-enable
the button on failure instead of leaving it stuck on 'Sending...'.
- common.js: optional error callback on doAjaxRequestHandler /
doPostAjaxRequestHandler (additive; existing callers unchanged).
- AppConfig: static resource version 389 -> 390 for the JS change.
 
37187 53 d 1 h amit /trunk/profitmandi-fofo/src/main/ PJP agenda config: sidebar action_class handler in Sales PJP menu; jsVersion 389  
37171 53 d 5 h amit /trunk/profitmandi-fofo/src/main/ Wallet page: show sanctioned credit limit tile alongside utilization/total due; format amounts via currency class + formatCurrency on load; jsVersion 388