| Rev |
Age |
Author |
Path |
Log message |
Diff |
| 37612 |
16 m |
amit |
/trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/service/warehouse/ |
Value an internal PO's cart the same way the transaction validates it
Creating an internal PO failed with "Cart Value-Payment value mismatch" whenever a line
carried paise and the quantity was more than a handful. Two places were computing the same
cart total from the same stored float through two different decimal conversions, so they
disagreed on the value of prices binary cannot hold exactly.
createOrderInternally read WarehouseLineItem.unitPrice through String.valueOf, which
resolves a float via Float.toString and yields 10499.99, while createTransactionForWarehouse
re-values the persisted cart lines with BigDecimal.valueOf, which widens that same float to
double and yields 10499.990234375. The per-unit gap is around two ten-thousandths of a rupee;
quantity multiplies it, and it crosses the 0.001 tolerance the validator allows at a quantity
of five. Round prices are exact in binary and passed, which is why this looked intermittent
rather than total - across randomised carts the old arithmetic disagreed 98% of the time.
The total is no longer computed in its own loop. It is derived from the cart items that are
about to be written, using the conversion and the line-inclusion rule the validator applies,
so both sides are the same function over the same rows and cannot drift apart. That also
brings across two rules the PO side never had: quantities of zero or less, which
addItemsToCart drops and which therefore never reach the validated total, and the one paisa
carry bag, which the validator treats as a marker line rather than a billed one. Either of
those reaching an internal PO would have failed it outright, and a carry bag is only a
rounding error away from the paisa-per-unit pricing used for FOC stock.
The wallet top-up still truncates the total to whole rupees, and now lands within a rupee of
where it did before, well inside the twenty rupee buffer it already carried. Nothing outside
internal PO creation is touched: the validator, bulk orders and the refurb split keep their
existing numbers, and the wallet debit is driven by the order totals, not by this value. |
|
| 37611 |
1 h 4 m |
amit |
/trunk/profitmandi-fofo/src/test/java/com/spice/profitmandi/service/warehouse/ |
Pin the unrecorded-stock behaviour
Covers the shape behind the 2026-09-12 bulk-upload failures: an item with some stock
traceable to a vendor and some with no recorded origin must move in one order, at the
traceable stock's price. Two genuinely different vendors must still refuse.
Also pins that no refusal message quotes a zero price from either side - including when the
oldest stock is itself the unrecorded pile, which is how 'at 0.00 each' reached users.
Requires profitmandi-dao r37610. |
|
| 37610 |
1 h 4 m |
amit |
/trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/service/warehouse/ |
Carry stock with no recorded origin along instead of refusing the movement
A movement was refused whenever an item had some stock traceable to a vendor and some whose
origin was never recorded, telling the user to raise a second purchase order for the rest.
Unrecorded stock has no vendor of its own, so that second order carried no more information
than the first - and the prices were identical anyway: boAt 34336 at 85 both ways, Samsung
36343/36344 at 1099, Riversong 36589 at 524, 36590 at 657. Bulk uploads of 200 rows were
dying on the first such item, repeatedly.
Only a second ORIGINAL VENDOR stops an order now, which is the case the refusal was for: a
line item holds one price per item, so units genuinely bought from someone else cannot ride
along. Unrecorded units move with the oldest stock at its price. They are not attributed to
that vendor as a fact - nothing persists origin today, but if that is ever added they must
not be stamped from this.
The refusal also reported unrecorded stock as moving at 0.00. Such a bucket carries no price
until it is resolved from the catalog when units are allocated, so reading one off it early
reported perfectly good stock as worthless. Both sides of the message now resolve the price
the same way the allocation does.
Three tests added: a mixed traceable/unrecorded item moves in one order, two genuinely
different vendors still refuse, and no message quotes a zero price from either side. |
|
| 37609 |
8 h 24 m |
amit |
/trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/ |
Offer the stock a warehouse actually holds when ordering from an internal supplier
The purchase order item picker listed whatever the supplier had vendor catalog pricing rows
for. For an outside vendor that is right - the order is for stock nobody holds yet, so their
catalogue is the only sensible list. For one of our own warehouses it is not: a movement can
only send stock that is standing there, and the pricing rows were never a statement about
stock. Vendor 275 was offering 6,845 items while Noida held 79; Delhi 6,637 against 330.
It also made the picker depend on data the movement no longer needs. Pricing for internal
suppliers is derived from the stock itself since r37603, so those rows are inert - but
clearing them emptied the picker completely, because selectVendorItems joins
VendorCatalogPricing to Item and an internal supplier then matched nothing.
Internal suppliers now list distinct items with currentQuantity > 0 in their mapped
warehouse. External suppliers are untouched and still list from vendor catalog pricing. |
|
| 37608 |
8 h 57 m |
amit |
/trunk/profitmandi-dao/src/main/resources/sql/ |
Select a default schema before the multi-table deletes
MySQL's 'DELETE alias FROM' form needs a default database even when every table is fully
qualified, so the deletes aborted with 'No database selected' while the backups above them
had already been written. Added USE inventory so the script runs end to end regardless of
how the client is invoked.
Run on hadb1 2026-09-12: 12,372 + 17,241 + 33,252 internal-supplier rows removed, external
pricing untouched at 40,670 / 45,049 / 68,749. |
|
| 37607 |
9 h 9 m |
amit |
/trunk/profitmandi-dao/src/main/resources/sql/ |
SQL to remove vendor pricing held against internal suppliers
Nothing reads these since r37603-05: an internal movement is priced from the stock being
moved, resolved to the original external vendor, and receiving no longer validates price for
those movements. A stale row would silently win over the derived price if any lookup were
reintroduced, so they are worse than inert.
Backs every row up to a _bak_ table and deletes against those frozen backups rather than
re-reading supplier.internal, so what is removed is exactly what is preserved. Run only
after the fofo WAR carrying the warehouse-grn-request-items.vm guard (r37606) is live. |
|
| 37606 |
9 h 9 m |
amit |
/trunk/profitmandi-fofo/src/main/webapp/WEB-INF/views/ftl/ |
Render a dash, not a raw reference, when there is no system price
warehouse-grn-request-items.vm called getTransferPrice() straight off a map lookup with no
null guard. An internal movement has no vendor circular to quote a system price from, so
that lookup misses and Velocity prints the literal reference text into the System Price
column - it fails quietly rather than erroring, so the screen just looks broken.
Renders '-' instead, which is what the column actually means for a movement priced from the
stock itself. Prerequisite for removing internal-supplier pricing rows. |
|
| 37605 |
9 h 53 m |
amit |
/trunk/profitmandi-web/src/main/java/com/spice/profitmandi/web/v2/controller/fofo/ |
Use the derived movement price in the v2 vendor controller too
Same change as the fofo controller: /getPricing returns the movement price for internal
suppliers, and addVendorPricingIfMissing is gone. This copy is component-scanned, so
leaving it behind would have kept writing the pricing rows the fofo side stopped writing.
Requires profitmandi-dao r37603. |
|
| 37604 |
9 h 53 m |
amit |
/trunk/profitmandi-fofo/src/ |
Show the derived movement price when building an internal purchase order
Both places a price reaches a purchase order now use the movement price for internal
suppliers: the single lookup as an item is added on screen, and the per-item resolution
behind a bulk upload, which does the whole file in one pass instead of loading a circular
the sending warehouse does not have.
Removes addVendorPricingIfMissing, which fired only when someone typed a numeric item id
into the search box, and only for Samsung or non-handsets, writing permanent approved
pricing rows onto the internal supplier from a hardcoded source vendor. Bulk upload never
called it at all, which is why the same item could be added on screen but rejected in a
file.
Tests cover what is decided once origins are known - drawing the oldest stock first,
refusing a quantity that spans two original vendors while naming how many can move and
asking for a separate order, refusing an unmapped supplier, and falling back to the catalog
without ever recording an inferred vendor as though it were known.
Requires profitmandi-dao r37603. |
|
| 37603 |
9 h 54 m |
amit |
/trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/ |
Price internal warehouse movements from the stock's original vendor
Stock moving between our own warehouses was priced from the sending internal supplier's
own vendor catalog, which only ever holds prices copied from elsewhere - so every hop let
the price drift further from what the group actually paid, and a movement was blocked
outright whenever that supplier happened to have no row for the item.
It is now derived from the stock being moved: the original EXTERNAL vendor is established
per unit and that vendor's current catalog price is used. Origin is looked for in
descending order of certainty - the serial traced back to the external purchase that first
brought the unit in, then the unit's own inbound PO when that was external, both of which
are facts that survive any number of internal hops. Non-serialised stock that has already
moved internally has no recoverable origin at all, since fungible units carry no identity
and the movement records nothing linking the receiving row to its source, so those fall
back to the most recent externally approved price for the catalog and are never recorded
as though their vendor were known.
A purchase order carries one price per item, so an item can only move from one original
vendor at a time. Where a requested quantity would run past the oldest vendor's stock into
another's, this refuses and reports how many can move now and at what price, leaving the
second order to whoever is moving the stock, rather than averaging the two or silently
splitting the order. Each order therefore stays attributable to one vendor.
Receiving no longer validates price for these movements: both sides of the comparison come
from the same derivation, so a mismatch cannot mean anything. That also removes a null
dereference in GrnRequestServiceImpl, which assumed every supplier has a circular.
Removes addVendorPricingIfMissing, which wrote permanent approved pricing rows onto internal
suppliers sourced from hardcoded vendor 334 for Samsung or an arbitrary findFirst() vendor
otherwise, stamped with hardcoded auth ids. Those rows are now not just unnecessary but
harmful: they would pin a stale price that wins over the derived one.
Verified against live data: covers every unit in every warehouse with none unresolved,
agrees exactly with the origin join the FOCO/ImeiSupplierPricing report already runs in
production, and prices 12,305 of 13,589 units within 2% of what was actually paid.
Resolution takes 0.7ms for one item and 1.1ms for ten. |
|
| 37602 |
12 h 3 m |
amit |
/trunk/profitmandi-cron/src/main/java/com/smartdukaan/cron/ |
Remove no-op refreshSnapshotAgeing task; restore originalInventoryItemId backfill call
refreshSnapshotAgeing ran every 30 minutes aggregating warehouse.inventoryItem.rootInvoiceDate,
a column nothing ever wrote, so its UPDATE ... JOIN matched zero rows on every run and
currentinventorysnapshot.oldest_invoice_date was never populated on any of 11,686 rows.
Removed along with the sessionFactory field it was the only consumer of.
Application.java had migrations.migrateWarehouseOriginalInventoryItemId(batchSize) commented
out while still logging 'Starting migration...' and 'Migration completed.', so
--migrateOriginalInventoryItemId reported success while doing nothing. Restored the call;
it remains opt-in via the CLI flag and cannot fire on its own.
Requires profitmandi-dao r37601 (entity fields removed). |
|
| 37601 |
12 h 3 m |
amit |
/trunk/profitmandi-dao/src/main/ |
Remove dead stock-ageing fields rootInvoiceDate and oldest/newest_invoice_date
rootInvoiceDate on WarehouseInventoryItem had no writer anywhere in the codebase and
was read only by ScheduledTasks.refreshSnapshotAgeing(), which therefore matched zero
rows on every 30-minute run. oldest/newest_invoice_date on SaholicInventorySnapshot
were written only by that task and read by nothing. Verified on prod: null on all
1,528,669 inventoryItem rows and all 11,686 snapshot rows respectively.
Stock ageing that is actually in use resolves the original external supplier at query
time by serialNumber + supplier.internal = false, and does not read these columns.
originalInventoryItemId is deliberately left alone - it is populated on 157,020 rows.
Includes sql/drop_dead_ageing_columns_20260912.sql to drop the columns. Run it only
after web, fofo and cron are all on this build. |
|
| 37600 |
1 d 6 h |
amit |
/trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/ |
Replace entity loads with scalar aggregates in the investment sweep
Order.lineItem is a ManyToOne with FetchType.EAGER, so getInTransitOrders() and
selectPendingGrnOrders() pulled ~6,865 Order entities plus a LineItem for each
into the session every two minutes - all dirty-checked at flush - when the sweep
only reads getRetailerId() and getTotalAmount(). That, plus ~1,690 FofoStore and
~984 PartnerInvestment rows, put roughly 17,000 entities in one session per pass
and is most of the ~10s each sweep was taking.
Adds two scalar named queries returning (retailerId, sum(totalAmount)) grouped by
partner. A projection never hydrates the entity, so the eager association never
fires - about 13,700 entities become ~1,000 rows. The predicates copy
selectOrders(ids, pendingOrderStatus) and selectPendingGrnOrders(ids) exactly,
including the SD_START_DATE floor, and the comment on the named queries says to
change them together.
Deliberately NOT done by making lineItem lazy: that mapping is shared by every
Order consumer in the codebase and flipping it globally is a far wider change than
this needs.
Entity-returning variants are untouched for their existing callers. Drops the now
unused sumOrderAmounts helper and the TransactionService dependency.
Not a fix for a problem - the sweep was comfortably inside its 120s window at ~8%
duty. It removes waste that would matter if the cadence were ever shortened. |
|
| 37599 |
1 d 6 h |
amit |
/trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/ |
Reuse the managed snapshot row in the sweep instead of building a second instance
Every sweep after the first failed with 'A different object with the same
identifier value was already associated with the session :
PartnerInvestment#107198' and wrote nothing.
selectByFofoIds returns managed entities. The loop then constructed a fresh
PartnerInvestment for the same id and called saveOrUpdate on it, which Hibernate
rejects. Sweep one survived only because the table was empty, so no row was
managed - the failure could not appear until a second run.
Now mutates the row already in the session when there is one, falling back to a
new instance only for partners with no snapshot. The previous base and gross stock
are captured before mutation, since comparing the row against itself afterwards
would report nothing as ever changed.
persist() is kept: a no-op for the managed rows (dirty checking flushes them), and
still the INSERT for partners appearing between sweeps.
Drops baseDiffersFrom(), now unused - the comparison is against the captured prior
value, still rounded to paise by the setter on both sides. |
|
| 37598 |
1 d 6 h |
amit |
/trunk/profitmandi-dao/src/main/java/com/spice/profitmandi/service/ |
Add missing @Transactional to the investment sweep
The sweep failed on every run with 'Could not obtain transaction-synchronized
Session for current thread' and wrote nothing. BatchScheduledTasks deliberately
carries no class-level @Transactional - each partner gets its own via REQUIRES_NEW
in the helper beans - so the sweep ran outside a transaction and
sessionFactory.getCurrentSession() threw at the first repository call.
PartnerLimitHelper already does this correctly with @Transactional(readOnly=true)
on its read phase; the sweep simply did not mirror it. Compilation cannot catch
this - only a run can.
sweep() and refreshAgedStockDaily() both read and write, so they take a read-write
transaction. Annotations sit on the service rather than the caller because
BatchScheduledTasks is intentionally non-transactional, and the service is a
separate bean so the proxy applies. |
|
| 37597 |
1 d 6 h |
amit |
/trunk/profitmandi-dao/src/main/resources/sql/ |
Register /agedAppleImeis for the partner role
RoleInterceptor authorises every non-admin request through
RoleManager.isAuthorizedURI(), which regex-matches the URI against dtr.api rows
reachable via dtr.role_api and throws GE_1004 when nothing matches. The new
aged-Apple IMEI endpoint (r37593) had no row, so partners would have been denied -
the Java change alone does not expose an endpoint.
Adds the api row and links it to FOFO, mirroring /activatedImeis (id 215) and
/activated-imeis-grn-pending (id 285). FOFO_ADMIN needs no row because isAdmin()
short-circuits before the lookup.
Applied to hadb1: api id 330, linked to role FOFO. Both inserts are guarded by
NOT EXISTS so the script is safe to re-run per environment. |
|
| 37596 |
1 d 7 h |
amit |
/trunk/profitmandi-fofo/src/main/java/com/spice/profitmandi/web/controller/ |
Mirror the gateway sanction when a hard limit is released
/resetHardLimit set hard_limit=false and reverted credit_limit to suggested_limit
but never touched dtr.credit_account, so the SIDBI/SDDIRECT gateway kept the old
pinned sanction until some later full limit pass rewrote it. Releasing a hard
limit can move the number a long way - RJAMR501 Inder Mobile is pinned at 800,000
against a suggested 909,114, so the gateway would under-report him by 109,114.
This was previously masked rather than absent: the 20-minute pass rewrote ~220
partners every run because float noise made almost every comparison look like a
change, which repaired the mirror by accident. Rounding that comparison to paise
(r37589) removed the accidental repair and moving the full pass to daily (r37595)
stretched the window to most of a day, so the gap is now worth closing properly.
Mirrors sanctioned/available in the same request, matching what
/creditRequirement already does, and stamps update_timestamp so the change is
visible as a real limit movement. |
|
| 37595 |
1 d 7 h |
amit |
/trunk/profitmandi-cron/src/main/java/com/smartdukaan/cron/scheduled/ |
Move the full partner-limit pass from every 20 minutes to daily
The 2-minute sweep already recalculates the limit for every partner whose base
moved, which covers investment and utilisation - the inputs that actually change.
The full 980-partner rescan only exists for inputs base cannot see: credit risk,
the SIDBI floor and hard_limit.
Those move on a far slower clock. RISK_DECREASE_DAYS_THRESHOLD is 90 days, and
transaction.fofo_sidbi_sanction has had no new row since Oct 2024 and no recorded
settlement. Running the rescan 72 times a day to catch them was 71 wasted passes.
Folded into the existing daily job, after the aged-stock refresh so the limit pass
sees haircuts for partners whose Apple or demo stock crossed its threshold
overnight. Leaves two scheduled jobs instead of three.
Limit latency is unaffected - that is the 2-minute sweep's job and it is unchanged
(~3 partners per window show a changed base, peak 18). The --updatePartnerLimitWithBatch
CLI entrypoint is untouched for manual runs. |
|
| 37594 |
1 d 7 h |
amit |
/trunk/profitmandi-fofo/src/main/webapp/WEB-INF/views/ftl/ |
Format activated-stock amounts as currency on the dashboard banners
Both banners rendered the raw float - '112682.0' instead of a formatted amount -
because the value sat in a plain <strong> with no currency class, so
common.js formatCurrency() never picked it up. The sibling
dashboard-grn-pending-activated-imeis.vm already had the class, which is why one
banner formatted and the one above it did not.
No jsVersion bump needed: template-only change, no JS or CSS touched. |
|
| 37593 |
1 d 7 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 |
|