Commit Graph

17 Commits

Author SHA1 Message Date
Suriya
8709556704 Add read endpoints for counter sales
The ingest only ever wrote. A bill that reached pos_orders was safe and
completely unreachable — no screen in the product could show it, and the
only way to see a day's counter takings was to query the database by
hand.

Three endpoints: a paged bill list, one bill with its lines, and a
summary split the three ways somebody actually asks for — by tender for
reconciling a drawer, by day for a chart, by till for an outlet running
several counters.

locationid is required on all of them and is the authorisation boundary,
so a caller cannot page through another shop's takings by omitting a
parameter. Fetching a bill under the wrong outlet returns 404 even when
the reference is a real one.

Dates match businessdate rather than arrival, because a till that was
offline overnight uploads yesterday's bills this morning and they belong
to yesterday. The list is ordered by billedat for the same reason —
sorting by arrival would interleave a recovered backlog through today.

Unlike the ingest handlers these answer in the usual envelope: they are
read by the web app, not by a terminal, and nothing about them is bound
to the till's contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 19:17:03 +05:30
Suriya
e3459a0f1c Ingest counter sales from the POS terminals, over MQTT and HTTP
A till holds every bill in its own SQLite database and keeps it for
seven days after we acknowledge it, marking one synced only when its id
comes back in an ack. Everything here follows from that.

Silence is not acceptance, so a failing ingest publishes nothing at all
and the terminal simply sends again. A duplicate is a success, because
at-least-once delivery means a lost ack legitimately re-delivers bills
we already hold, and calling those failures would strand a day of
takings on the till. Deduplication is a unique index on the terminal's
UUID plus an advisory lock held for the transaction.

Bills land in pos_orders / pos_order_items rather than orders: a counter
bill carries a cashier, a terminal, a rounding adjustment, promos,
loyalty movement and a payment split that orders has nowhere to put, and
forcing one into the other loses whatever does not fit. Stock is *not*
split — a counter sale writes the same productstocks rows an app order
does, through helpers extracted from createOrderTx so the rule that
prevents overselling has one implementation rather than two.
GetRevenueSummary and GetSalesSummary were extended to union the new
table in; any new report has to remember the same.

Terminal health goes to Redis under a 90-second TTL, sharing the
instance the express backend uses. A heartbeat is a fact with an expiry
date: a till that loses power stops refreshing and ages off the board by
itself, where a Postgres row would need ~288k writes a day and a reaper.

Proven end to end against the live estate before commit: a bill over
HTTP and one over the real Mosquitto broker, the same bill three times
producing one row and one stock movement, and a heartbeat arriving on
the health endpoint. All probe data was removed afterwards.

Four things that only surfaced against real data. An unset jsonb column
failed the very first bill. Product SKUs are unusable as barcodes — 6,245
products share 93 SKUs and "1" covers 5,794 of them — against the till's
unique index, so barcodes fall back to the product id. A taxpercent of
-1 exists and would have put negative GST in a filed slab. And a product
with id 0 exists, which can never be billed and is now skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:48:00 +05:30
583cd89063 new api for offline sales 2026-07-30 17:25:09 +05:30
c94ddd34c7 Stop stock receipts clobbering products.productstatus
products.productstatus is a per-product lifecycle field holding
"Active"/"Inactive". CreateProductStock overwrote it with "available" on every
stock receipt — an availability value written into a lifecycle column — which
destroyed the real lifecycle state of the rows it touched. 136 products now
read "available" and 12 "outofstock" with no way to recover what they were.

A single column on products cannot express availability anyway: the same
product can be stocked at one outlet and empty at another. That fact belongs
to productlocations.status, which SyncProductLocationStatus already derives
from the ledger, so the receipt path now updates only that and leaves
productstatus alone. UpdateProductStatus remains available as an explicit
admin operation; it is simply no longer called as a side effect of stock
movement.

GetProductCount counted available/outofstock off the same corrupted column and
returned near-nonsense as a result: across 6245 products it matched
'available' on 136 and 'outofstock' on 12, leaving 6097 — the real answer —
uncounted under "Active". It now derives both from the ledger, counting a
product available when it holds positive stock at any of the tenant's outlets,
so total = available + outofstock (6245 = 22 + 6223).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 13:08:35 +05:30
3b60a90009 Derive stock and availability from the ledger, not stored fields
Stock shown in the console did not match the productstocks ledger, and two
product endpoints were failing outright. Every cause was on the read side or
in how the availability flag was maintained; the ledger writes themselves
(CreateOrder's "out" entry, cancellation's "in" entry) were already correct.

Read fixes, repositories/productRepository.go:

- GetProductStocks returned SQLSTATE 42803 on every call: bare a.tenantid /
  a.stocktype / a.status under GROUP BY a.productid. The per-ledger-row
  columns are now aggregated and the grouping covers the identity columns.

- FetchFilteredProducts filtered on an alias `e` that no query defines, so
  every /getallproducts call carrying a locationid failed with SQLSTATE 42P01
  instead of returning products.

- FetchFilteredProducts joined productlocations on productid alone and joined
  a (productid, locationid)-grouped stock subquery on productid alone, so a
  product carried by three outlets came back three times, each row showing
  another outlet's quantity and status. Both are now tenant-scoped subqueries
  collapsed to one row per product and scoped to the outlet when one is given.

- GetProductStocks and FetchFilteredProducts compared stocktype = 'in'
  case-sensitively. Production holds 'in' and 'IN' both, so uppercase receipts
  were silently dropped from the balance: one outlet reported 0 for a product
  holding 50, another reported 0 for twelve products holding 200-840.

- GetStockStatement summed opening over stockdate <= CURRENT_DATE, making it
  arithmetically identical to closing. The Inventory ledger showed the same
  number in both columns on every row, which reads as stock never moving.

Availability flag:

productlocations.status was maintained by two different rules — the order path
derived it from the balance, the receiving path set 'available' on any "in"
entry regardless of the resulting balance. A partial restock that left the
balance at or below zero marked a product sellable, and a flag set by an old
order never cleared for stock that arrived by a route the API did not own.

Both paths now derive the flag from the live balance through one rule:
SyncProductLocationStatus (receiving side) and syncProductLocationStatus
(order side, inside the caller's transaction). ReactivateProductLocations is
replaced by the former; the service no longer filters refs by stocktype, since
the direction of the movement is no longer what decides the flag. A row that
has already drifted now repairs itself on its next ledger entry.

Verified against the live database: all four stock endpoints return matching
balances, /getallproducts no longer duplicates rows, and the flag sync was
exercised in both directions inside a rolled-back transaction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 12:14:59 +05:30
84dfa8e640 Add a synthetic "All" category to getappcategories
The mobile app needs an "All" tile to browse every product regardless of
category. Rather than inserting a real app_category row (which would break
once any category-scoped product filter treats it as an actual, empty
category), the service now prepends a synthetic entry with categoryid=0 —
reusing the "0 = no category filter" convention GetAllProducts already
implements in FetchFilteredProducts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 11:04:37 +05:30
7db81c0e68 Return the created location from CreateTenantLocation so its locationid is available immediately
The store/branch QR code the app scans is just {tenantid, locationid} JSON, but the
onboarding response previously discarded the DB-assigned locationid, so the frontend
had no way to render a store's QR right after onboarding without a separate lookup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 11:08:57 +05:30
Suriya
d3a7466f4c Sync stock status on restock and expose live stock on getproductbyvariant
Two gaps found while auditing the order/stock flow:

- Receiving stock via an approved stock request only updated
  products.productstatus (a global per-product flag). A location
  flagged outofstock by CreateOrder never got reactivated, since
  nothing touched productlocations.status on the way back in.
  CreateProductStock now reactivates the specific
  (tenant, location, product) row to "available" for every "in"
  entry, the counterpart to how it gets flagged out.

- getproductbyvariant returned no stock info at all, so the app could
  only find out a product was unavailable from the 409 at order time.
  It now accepts an optional locationid and, when passed, returns
  live productstock (same SUM(in)-SUM(out) formula the order check
  uses) and locationstatus per product. Omitting locationid keeps the
  old response shape.

Added MOBILE_ORDER_VERIFICATION.md as a handoff doc for the mobile
team covering the expected request/response shapes and how to verify
their integration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 18:01:39 +05:30
Suriya
a9c13292f0 Make subcategory optional and fix category picker for real tenant data
subcategoryid is no longer required to import a catalogue product —
it's a display/grouping hint elsewhere (the codebase already has an
"Uncategorized" fallback for subcategoryid=0), so requiring it was
pure friction with no correctness payoff.

Fixes the category picker at the root: categoryid 2, which tenant
1135's real products actually use, has no row in productcategories at
all (not a filter bug — the master data is genuinely missing it).
Rather than inventing category master data, adds
GET /products/gettenantcategories, which lists categories a tenant's
own products actually use (falling back to a synthesized label when
the master table has no name), so the import category picker always
offers something real instead of an incomplete global list.

Also relaxes the subcategory lookup's tenant filter to include
unowned/global rows (tenantid NULL or 0), not just exact tenant
matches — categoryid 2's real subcategories carry no tenant at all,
so the strict filter was hiding them even when a tenant wanted one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 18:18:51 +05:30
Suriya
fab06bb33e Bridge global catalogue DB to per-store product catalogue
Adds a separate CatalogueDB (pgvector) connection alongside the main
nearledb, plus a new catalogue module (repository/service/controller/
routes) to browse it by brand, category, and keyword, with brand
optional so the whole ~237-product catalogue can be browsed unfiltered.

Adds the actual bridge: importing a catalogue product snapshots it into
the tenant's own products table (keyed on brand+catalogueid, since a
catalogue row's bare id is only unique within its own brand table),
then links it via the existing productlocations upsert. Re-importing
tops up stock and refreshes price instead of duplicating. Also adds an
imported-refs endpoint so the frontend can badge already-imported items
without diffing full product lists, and wires the new AWS S3 image
store used to resolve catalogue product photos.

Bumps Go/Docker to 1.24 for the AWS SDK dependency this needs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 17:34:20 +05:30
950064de6c productRepository created 2026-07-15 15:25:50 +05:30
Suriya
7e9b3f98ba stores login fix 2026-07-09 18:03:44 +05:30
1437a08be5 corrections in stockrequest table 2026-07-09 15:31:36 +05:30
Suriya
135df9fd18 store users login 2026-07-07 12:09:34 +05:30
abe2cb997b stock request table created 2026-07-04 17:49:54 +05:30
Suriya
f220c24c17 new changes 2026-07-01 10:53:57 +05:30
Gokul
0d42ac84e1 initial commit 2026-05-25 11:52:26 +05:30