| Rev |
Age |
Author |
Path |
Log message |
Diff |
| 37729 |
20 d 11 h |
amit |
/trunk/ |
lead: one workable lead per mobile, with a 6-month supersede and an L2+ override
There was no choke point for lead creation. Ten sites did `new Lead()` across four
modules -- three in web's LeadController, two in V2FofoLeadController, three in fofo's
LeadController, one in TrialServiceImpl and one in the cron LeadSyncRunner -- and only
ONE of them (fofo /createLead) checked for an existing lead at all. Result on live data:
5,137 mobiles carrying duplicate leads over 12,156 rows, worst case 29 on one number,
and two agents unknowingly working the same shop.
THE RULE, in new LeadCreationService, which all ten now route through:
no active lead on the number -> create
active, last activity >= 6 months -> retire the old one, create the new one, SILENTLY
active, last activity < 6 months -> BLOCK; only an L2+ user may override
Active = status in (pending, followUp) AND the assignee is still an active auth_user.
Last activity = GREATEST(lead.updated/created, MAX(lead_activity.created)).
The stale branch is deliberately quiet. A shop enquiring again after six months is a
handover, not a clash, and mailing on it would train the desk to ignore the alert -- so
only a genuine collision notifies. Live split: 195 stale against 1,241 fresh, and roughly
three blocks a month.
WHY "ACTIVE" ALSO MEANS A LIVE OWNER
331 open leads are assigned to 11 DEACTIVATED accounts (157 to sm@smartdukaan.com alone,
whose newest lead is from 2022). Counting them as active would block fresh enquiries
behind an account nobody can log in to and therefore nobody can close. Requiring a live
owner defuses all 331 without retiring a single row. Retirement here is only ever
REACTIVE -- triggered by a new entry on the same number. Nothing runs on a schedule.
ASSUMPTION worth flagging: a superseded lead becomes status=notInterested (stage DROPPED)
with closure_timestamp and reason 'Superseded after 6 months inactivity', rather than a
new `expired` status. "Closed" is an explicit allow-list in the UI --
Arrays.asList(notInterested, finalized) at V2FofoLeadController:150 and fofo
LeadController:313 -- and there are ~107 references to specific LeadStatus values, so a
new enum value would make these leads vanish from BOTH the open and closed screens.
Stage DROPPED keeps the nuance (the shop never said no) and still maps to notInterested
via LeadStage.toLegacyStatus().
OVERRIDE is L2+ in ANY team, not Call Center only: Sales L1 owns 1,063 of the 1,776 open
leads, so a Call-Center-only gate would funnel every team's collisions through three
people. The MAIL still goes to Call Center L2+, resolved from cs.position at send time
rather than hardcoded. An override is a TAKEOVER -- it closes the existing lead -- because
a second live lead is the exact thing the rule exists to prevent.
UNATTENDED CALLERS (cron sync, CSV upload, trial registration, AI intake) have nobody to
offer an override to, so they use createUnattended: skip the colliding row and mail the
desk rather than throwing. CSV reports imported/duplicateSkipped/duplicateMobiles back to
the operator instead of failing the whole file over one number.
ALSO FIXES selectByMobileNumber, which called getSingleResult and therefore threw
NonUniqueResultException on any mobile with more than one lead -- GlitchTip #99 and #1359,
both still firing. It now prefers the open lead, then the most recently touched.
NOT INCLUDED, deliberately: no DB unique constraint. 10 mobiles already carry more than
one open lead and would have to be resolved by hand first, which conflicts with the
no-auto-retirement rule. The service enforces the invariant going forward.
NEEDS A DBA STEP: user.lead.mobile is unindexed on 37,580 rows, so this check is a full
scan on every create. Index DDL is in the accompanying note; it has NOT been applied. |
|
| 37728 |
20 d 11 h |
amit |
/trunk/ |
logging: stop three non-defects reporting to the error board as ERROR
log4j2.xml ships ERROR and above to GlitchTip, so the log level IS the filter.
Three sources of ordinary, expected behaviour were logging at ERROR and between
them accounted for 51,244 events -- 43% of the whole board -- none of them a bug.
GlobalExceptionHandler already logs ProfitMandiBusinessException at WARN and is
unchanged; every leak below bypassed it.
1. Session state (27,302 events, 23% of the board). CookiesProcessor and the two
interceptors logged a missing or expired cookie at ERROR. A logged-out or
anonymous visitor is the normal case -- the code already handles it by
redirecting to /login or returning 403 -- and each of these sits in a class
whose surrounding lines already log at DEBUG. These never reach the handler,
which is why its WARN-level treatment never applied. Now DEBUG.
2. Client disconnect (19,349 events). There was no handler for it, so a client
hanging up mid-response fell through to @ExceptionHandler(Exception.class) and
was recorded as an unhandled server bug. Adds an IOException handler to both
GlobalExceptionHandlers that logs a disconnect at DEBUG and everything else at
ERROR with its 500 intact.
⚠ ClientAbortException CANNOT be imported here: catalina is provided by the
container and is not on either module's compileClasspath (verified against
the configuration, not assumed). The match is therefore on the simple class
name plus the "broken pipe"/"connection reset" messages, walked down the cause
chain. A genuine IOException matches none of those and keeps its ERROR.
The handler returns null for a disconnect rather than a body: the connection
that would carry it is already closed, and writing to it is what raised the
exception. Null is the supported way to say "handled, no body" --
HttpEntityMethodProcessor marks the request handled before reading the value.
3. Business exceptions re-logged at ERROR (11 sites). Each catches a
ProfitMandiBusinessException, logs it, and CONTINUES with a fallback -- a
missing wallet history defaults to an empty list, a user not found by primary
email is retried against the secondary. That is an expected outcome being
reported as a failure, and logging it at ERROR pre-empted the handler that
would have logged it at WARN. Now WARN.
Selenium/WebDriver (58,776 events) is deliberately untouched: that is a genuinely
broken ChromeDriver, and muting it would hide Oppo/Realme IMEI activation failing.
It looks like noise only because one broken thing repeated 52,000 times. |
|
| 37725 |
23 d 4 h |
amit |
/trunk/profitmandi-fofo/src/main/java/com/spice/profitmandi/web/controller/ |
Fixed mail sender everywhere |
|
| 37723 |
23 d 6 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. |
|
| 37720 |
23 d 8 h |
ranu |
/trunk/profitmandi-fofo/src/main/java/com/spice/profitmandi/web/controller/monitors/ |
today po rbm view showing only for l7 and above |
|
| 37719 |
23 d 8 h |
amit |
/trunk/profitmandi-fofo/src/main/ |
feat(po): show only allocated warehouse as 'To Warehouse' on open PO list, drop generic buyer label |
|
| 37718 |
23 d 8 h |
amit |
/trunk/profitmandi-fofo/src/main/ |
feat(po): show allocated warehouse name and id under buyer on open PO list |
|
| 37717 |
23 d 9 h |
ranu |
/trunk/profitmandi-fofo/src/main/java/com/spice/profitmandi/web/controller/monitors/ |
today po rbm view showing only for l7 and above |
|
| 37716 |
23 d 10 h |
ranu |
/trunk/profitmandi-fofo/src/main/java/com/spice/profitmandi/web/controller/monitors/ |
today po rbm view showing only for l7 and above |
|
| 37715 |
23 d 19 h |
amit |
/trunk/ |
SD credit: stop read paths writing utilized_limit - the SD Credit admin page and V2 getLoans mutated managed SDCreditRequirement entities to show recomputed utilization, so Hibernate dirty-checking flushed an UPDATE per partner at commit; fofo now feeds the view from display maps and getLoans detaches before the display write, leaving output identical |
|
| 37711 |
23 d 19 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. |
|
| 37710 |
23 d 19 h |
amit |
/trunk/profitmandi-fofo/src/main/java/com/spice/profitmandi/web/controller/warehouse/ |
Closing a movement PO now cancels its orders, and refuses if any has already shipped
A movement is a purchase order and a transaction raised as one, but closing only ever ended the
PO. Its orders stayed live, still able to dispatch stock against an order nobody was going to
receive - the same split that let PO/07-26/52029 ship and invoice goods its PO could never take.
r37709 closed the PO when the last order was cancelled; this is the other direction.
Before cancelling anything it checks every order on the transaction. If one has moved past being
submitted for processing its stock is billed or already gone, and cancelling would write off a
movement that physically happened - so the close is refused, naming the order and its status,
rather than quietly reversing a real dispatch. Rare, but not impossible.
External vendor POs are untouched: they carry no transaction, so the check returns immediately. |
|
| 37704 |
23 d 19 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. |
|
| 37697 |
24 d 1 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. |
|
| 37692 |
24 d 5 h |
vikas |
/trunk/ |
LMS + Airtel Calling |
|
| 37685 |
24 d 6 h |
amit |
/trunk/profitmandi-fofo/ |
Remove selenium from the fofo portal entirely
No browser starts in this WAR any more, and the selenium-java and
webdrivermanager dependencies are gone with it. Verified: no source reference to
selenium/WebDriver/ChromeDriver anywhere in the module, and zero selenium
artifacts on the runtime classpath.
Three pieces:
1. The insights schedule and its pull move to profitmandi-cron (r37684) and dao
(r37683). This service keeps only the READ paths -- redis, then in-memory,
then cs.agent_daily_insight -- plus refreshInsights(), which now asks the dao
sync service to run once and re-reads what it wrote. The portal therefore
holds no credentials at all: KNOWLARITY_USERNAME/PASSWORD are deleted from
this source, along with INSIGHTS_PAGE_URL and the 200-character CSS selector
the scrape depended on.
2. KnowlarityScraperService deleted. It was dead, not merely idle: zero
references anywhere, both @Scheduled annotations commented out, and its
@PostConstruct selenium block commented out with the note 'DISABLED - Live
status now comes from WebSocket via profitmandi-cron'. SVN backs that up --
r36057/r36058 (25-Mar) moved agent status to the websocket and r36072/r36075
(26-Mar) created KnowlarityBreakLogService in dao, but nobody removed the
corpse. It kept selenium in this WAR for six months after nothing used it.
The data agrees: 'On Break - <reason>' rows in cs.rbm_break_log stop on
25-27 March and plain 'Break' takes over, which is exactly that handover.
⚠ Consequence worth knowing: break-REASON granularity (lunch/meeting/sick)
was lost at that migration and is not coming back from the websocket feed.
3. setTokens() and POST /indent/set_knowlarity_tokens removed -- the method had
already been reduced to a log line, and the pull now authenticates itself per
run. Also retired the orphan knowlarity.scraper.enabled property (nothing read
it; it was still 'true' in prod), and corrected a stale section header and a
doc comment that promised 'current tokens' which no longer exist.
Not touched, deliberately: POST update_agent_status / bulk_update_agent_status /
update_status_by_name still exist and still write cs.rbm_break_log through
AgentLiveStatusService. They are orphaned -- the deleted scraper was their only
feeder and no view or script in the deployed WAR calls them -- but they are
public HTTP surface, so proving there is no INTERNAL caller is not the same as
proving no external one. Left for a separate decision. |
|
| 37680 |
24 d 7 h |
amit |
/trunk/profitmandi-fofo/src/main/ |
feat(warehouse): resolve the logged-in user on PO creation and GRN mismatch resolution
Backdated POs are auto-approved in their creator's name, so the user has to travel with the request.
- createPurchaseOrder resolves the logged-in user via cookiesProcessor + authRepository and sets createdBy
- resolvedMismatchRequest passes the resolver through, for the correction PO it can raise
- PO create page no longer says backdated POs need HOD approval
Pairs with r37679. |
|
| 37671 |
24 d 12 h |
ranu |
/trunk/ |
loi revival process modify |
|
| 37663 |
25 d 5 h |
vikas |
/trunk/profitmandi-fofo/src/main/java/com/spice/profitmandi/web/controller/ |
Added exception to method sign |
|
| 37660 |
25 d 6 h |
ranu |
/trunk/ |
ticket download option given and some enhancement on notification panel |
|