Subversion Repositories SmartDukaan

Rev

Show changed files | Directory listing | RSS feed

Filtering Options

Rev Age Author Path Log message Diff
37609 1 h 6 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 1 h 38 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 1 h 50 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 1 h 50 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 2 h 34 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 2 h 35 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 2 h 36 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 4 h 44 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 4 h 44 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 22 h 55 m 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 23 h 26 m 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 23 h 38 m 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 23 h 40 m 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 23 h 52 m 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 0 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 0 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 0 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
 
37592 1 d 0 h amit /trunk/profitmandi-dao/src/main/ Fix live-demo deduction lost in the investment snapshot; maintain aged-Apple qty

BUG FIX to r37588. InventoryService.getTotalAmountInStock() returns stock net of
live-demo units held past 90 days, and both original investment paths assigned
that netted value to inStockAmount - which is why
PartnerDailyInvestment.getTotalInvestment() carries no separate demo term. The
sweep stored the RAW snapshot sum and never applied the deduction, overstating
investment for 328 partners by Rs 5.19 crore and their limits by roughly Rs 1.3
crore. Maxwell validated clean earlier only because he holds no demo stock.

Fixed by storing what is measured and deriving what is computed: the column is
now in_stock_gross_amount and getNetInStockAmount() applies the haircut. The demo
holding moves on its own daily clock, so storing the netted figure would have
meant re-netting it in three places every time the haircut changed - the same
'two things must move together' trap behind the original cache bug. Deriving it
means a daily demo refresh flows through to stock and base with nothing to sync.

Also:
- aged_apple_qty stored on the snapshot and maintained on the same daily cycle as
aged_apple_stock_amount, so the partner banner reads a saved count instead of
running a count query per dashboard load, and count and value can never disagree
- selectPartnerStockCountMap mirrors selectPartnerStockValueMap, including the
activated exclusion
- selectAgedAppleStock lists the units behind the haircut for the partner view
- FofoUser exposes aged_apple_stock / aged_apple_qty
- stock-moved detection compares GROSS: a demo unit crossing 90 days changes net
stock but nothing physically moved, and that is the daily refresh's job

Schema: partner_investment_snapshot.sql updated (in_stock_gross_amount,
aged_apple_qty). Applied to prod - the table was empty, so it was dropped and
recreated from the script.
 
37591 1 d 2 h amit /trunk/profitmandi-web/src/main/java/com/spice/profitmandi/web/v2/controller/fofo/ Route V2 admin wallet adjustment through WalletServiceImpl

Byte-identical duplicate of the fofo /walletUpdate handler, with the same missing
row lock. Points at the shared walletService.applyManualAdjustment so both
modules share one code path.
 
37590 1 d 2 h amit /trunk/profitmandi-fofo/src/main/java/com/spice/profitmandi/web/controller/ Route admin wallet adjustment through WalletServiceImpl

/walletUpdate hand-rolled the balance update and history row, bypassing the
service entirely, and read the wallet with selectByRetailerId instead of
selectByRetailerIdForUpdate - so unlike every other wallet path it took no row
lock, and a concurrent adjustment and purchase could lose an update.

Now calls walletService.applyManualAdjustment, which reuses getManualReference
for the reference allocation and routes the movement through
addAmountToWallet/consumeAmountFromWallet(forced). Behaviour is preserved: admin
debits stay forced, so they can still push a wallet negative as before. Removes
~45 duplicated lines.
 

Show All