Subversion Repositories SmartDukaan

Rev

Show changed files | Directory listing | RSS feed

Filtering Options

Rev Age Author Path Log message Diff
37743 16 h 47 m vikas /trunk/ LMS checklist for call  
37740 21 h 2 m amit /trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/service/transaction/ SD credit: getAvailableAmount no longer writes utilized_limit - it is a read path, but the managed entity was dirty-checked so every availability query (gateway callback, both sanction screens, bulk order creation) flushed an UPDATE; availability is now computed locally as limit - liveUtilization, same value  
37739 21 h 6 m amit /trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/service/integrations/gstpro/ GstProService: drop imports only the removed Perfios block used

HttpHostConnectException and JSONObject had no other use after r37736.
 
37738 21 h 40 m vikas /trunk/ LMS checklist for call  
37737 21 h 42 m vikas /trunk/ LMS checklist for call  
37736 21 h 57 m amit /trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/service/integrations/gstpro/ Remove dead Perfios GST-return OTP block from GstProService

Commented-out since it was added; the Perfios UAT URL and auth key were the
only trace of that integration. Part of the dead-integrations cleanup
(r37730-37735).
 
37732 22 h 9 m amit /trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/dao/repository/fofo/ purchase return: look up the open return against a document

selectOpenByDocumentReference returns the return still outstanding against a
document reference - neither refunded nor rejected - or null when none is.

Backs a duplicate guard on the invoice-return submit path: the same invoice was
being submitted twice, the closest pair eight seconds apart, and finance was
rejecting the extras by hand. A settled return deliberately does not count, since
rejecting a return is precisely what frees the invoice to be raised again.

Repository lands first so the method exists before the caller uses it.
 
37731 22 h 9 m amit /trunk/profitmandi-dao/src/main/ Remove dead third-party integrations: dao

Nothing removed here had a live caller. No tables are touched.

- Toffee Insurance client + models, and the tofee.* keys in shared-*.properties
- Bharti Assist (BAG) service and its certificate/brand models. BagPlanModel
and PlanVariant stay in bharti/model: OneAssist and ICICI Lombard use them.
- Private, uncalled Toffee/BAG request builders in InsuranceServiceImpl
- Wiseapp insurance client and ZestResponseModel
- SmartPing client. CallDetailModel and PushCallLogModel move to
kommuno/model because the Knowlarity webhooks still parse them.
- SpiceMoney SSO (2-partner pilot), Thriwe stub + DTOs, DTDC/Shipsy (demo
URLs only), Blue Dart SOAP stubs, legacy Pine Labs v1 client
- FundFina pre-approval entity/repository, HyperTrack key entity/repository,
affiliate Click entity/repository
- Speqtra SMS constant; aramex.tracking.url keys
- PAYU PAY entry in payment-options.json

Gateway.MANDII and Gateway.FUNDFINA stay: historical fofo.payment rows.
 
37729 22 h 10 m 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.
 
37727 22 h 28 m amit /trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/dao/entity/transaction/ Fixed mail sender everywhere  
37722 3 d 17 h amit /trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/ Movement PO availability: list stock arriving into the sending warehouse, per PO

The popup showed in stock, promised and can-move, but not stock on its way in once it was
fully promised - so UP to Noida read 'in stock 0, promised 4, can move 0' with the 4 units
arriving on PO 54031 nowhere, and looked wrong. It now reads as a sum:
in stock + arriving - promised = can move.

- selectOpenInboundPurchases: open POs into the warehouse, one row per PO, same filter as the
pending-stock layer so the rows add up to the arriving total
- InternalMovementAvailabilityModel: arrivingTotal (before holds) + arriving breakup;
inbound (after holds) unchanged, still used by the refusal message
- describeAvailability (single + batch) carries both; batch doc: quantities are no longer
checked at bulk upload, the screen flags them and order creation enforces them
 
37721 3 d 18 h amit /trunk/profitmandi-dao/src/ fix(orders): price each order by the cart line that asked for it, not the first line of its model

Orders were priced from one cart line per catalog, so colours of one model at different
prices all took an arbitrary one. Internal movements (cost layers) failed with WLT_1000
when the dearer line won and were silently under-billed when the cheaper one did.
fulfillQty now prices from the item's own line, newColorQty from the model's any-colour
line; same-price output is unchanged. Refuse the transaction if orders do not total the cart.
 
37715 4 d 5 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  
37713 4 d 5 h amit /trunk/profitmandi-dao/src/main/ fix(warehouse): record the order's own vendor when a receipt line carries no origin

- r37694 took the origin only from lineitem.origin_vendor_id, which exists on orders raised from that revision onward, so goods arriving on older open orders landed with a cost but no vendor - 282 rows on the first day
- A purchase from an outside vendor now falls back to that supplier; internal movements are unchanged and an unknowable origin still stays NULL
- sql/backfill_receipt_origin_20260918.sql repairs the 282 already received (run on prod 2026-09-18; bak warehouse._bak_receipt_origin_20260918); in-stock unknown 895 -> 648
 
37711 4 d 5 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.
 
37709 4 d 6 h amit /trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/dao/repository/transaction/ Cancelling a movement's last order now closes its purchase order

An internal movement is raised as a purchase order and a transaction together, but only the
transaction was ever cancelled. The PO stayed open and read as live work until the auto-close
sweep aged it out days later - and that sweep writes CLOSED, which is what a PO that actually
received its stock gets. Eight movements raised between 15 and 17 September sat this way, 59
units and Rs 10.68 lakh, every one cancelled the same day it was raised because the model or
colour was discontinued.

refundOrder now pre-closes the movement PO once every order on its transaction is refunded -
every one, because a transaction can carry several and the movement is only over when the last
goes. PRECLOSED, not CLOSED: nothing was received, and the difference is what keeps it
distinguishable from a real completion in every report that reads the status.

Refunds reaching here are already validated as never billed, so no dispatched stock is involved
and nothing is closed out from under stock in transit. Both lookups it needs already existed.
 
37706 4 d 6 h amit /trunk/ cart: take the cart row before any line, to break a lock-ordering deadlock

InnoDB was rolling back cart edits with "Deadlock found when trying to get lock"
(GlitchTip #53, 27 deadlocks) and losing others to OptimisticLockException
"actual row count: 0; expected: 1" (11 issues, 42 events). Both are the same cause.

Two request paths took the same two rows in OPPOSITE orders. Adding a line INSERTs
into user.line, and line_cart_id_fk shared-locks the parent cart FIRST, line second.
Validation/hydration mutated the line rows FIRST and wrote cart.total_price second.
Run those concurrently on one cart and it is a cycle. Caught in the act on prod:

T1: INSERT user.line (cart_id=175180781) -> holds S on cart, waits S on line
T2: UPDATE user.cart SET total_price=269340.0, version=175 WHERE version=174
-> holds X on line, waits X on cart
*** WE ROLL BACK TRANSACTION (1)

Fix is to give every mutating path one order: cart, then lines. CartRepository gains
selectByIdForUpdate, and it is called as the first statement of each path that writes
a cart line. Re-taking it inside one request is a no-op.

A lock-ordering fix is all-or-nothing -- one path in the wrong order is enough to
re-form the cycle -- so this covers ALL TEN cart-line writers, not just the two that
happened to show up in the stack traces: createCartItem, clearCart, getCartValidation,
addItemsToCart (x2), addShoppingBag, validateForOpen (x2) and V2BillingController's
bind/unbindInsurance. The audit is worth re-running before adding another writer.

Note validateForOpen and getCartValidation WRITE despite their names (they correct
cart_line quantity/price and roll up cart.total_price), which is why a "validate" call
was ever holding write locks. The lock is placed accordingly; making those genuinely
read-only is a separate, larger change.

This also serialises concurrent edits to the SAME cart, which is what the @Version
column on Cart was already trying and failing to express. Different carts are
different rows, so there is no cost across partners.
 
37704 4 d 6 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.
 
37703 4 d 6 h amit /trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/ GRN price mismatch: ask for the original PO to be reopened instead of stranding a correction PO

Resolving a price mismatch raised a correction PO dated to the supplier invoice, and stored its id
on the GRN request item to receive against. But by then the stock has already arrived and the PO it
corrects is closed - every one of the 54 raised this way had its mapped PO in CLOSED. The
correction therefore had nowhere to land: the GRN completed against the original, nothing was ever
received against the correction, and it sat holding a reservation for stock that was not coming.
Backdated by nature, it also parked in INIT under the old approval gate, so it could not have been
received even if anyone tried.

Resolving a mismatch against a closed PO now says so and names it, asking for that purchase order
to be reopened first, rather than silently creating a second PO that cannot be used.

WarehousePurchaseOrder.isOpen() names the open set - INIT, READY, PARTIALLY_FULFILLED - that the
movement and commitment queries already read, so it stops being restated per caller.
 
37702 4 d 6 h amit /trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/ Internal movement: an unapproved PO no longer dispatches stock

A PO raised on one of our own warehouses also raises the order that moves the stock, and that
order pays and processes immediately - so raising it ships goods. The approval gate never
governed that half. It set the PO to INIT and mailed an approval link, then created and processed
the order anyway, several lines later and without consulting the status it had just set. The gate
held back receiving - PO list, GRN matching, auto-close all skip INIT - while dispatch went ahead
unconditionally.

That is how PO/07-26/52029 was delivered and invoiced while frozen out of GRN. Its goods had to be
received against a second PO raised for the purpose, leaving the first holding a phantom
unfulfilled quantity for two months.

Order creation now waits on approval. WarehousePurchaseOrder.isApproved() names the rule once -
INIT is the only state before approval, every other state is something the order has already been
approved to do - matching how the PO list, GRN matching and the auto-close sweep already read it,
so callers stop restating it.

Backdated POs are approved at creation since r37682, so this holds nothing up today; it is what
keeps the two halves from separating again if any future rule leaves a PO unapproved. Nothing
raises the order later, so such a PO simply does not move stock and has to be raised again once
approved - the safe failure, and it is logged.
 

Show All