changes
This commit is contained in:
59
.env.example
Normal file
59
.env.example
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Fiesta configuration.
|
||||||
|
#
|
||||||
|
# Copy to `.env` and fill in. `.env` and `.env.*` are gitignored (the one
|
||||||
|
# exception is this file) — and they are gitignored for a reason: this
|
||||||
|
# repository's history already contains a committed `.env` from before
|
||||||
|
# 2026-08-03, so those database credentials are in the history and should be
|
||||||
|
# rotated. Do not add another.
|
||||||
|
#
|
||||||
|
# `godotenv.Load()` in main.go reads `.env` from the working directory, so
|
||||||
|
# `go run .` from this folder picks it up with no flags.
|
||||||
|
|
||||||
|
# ── Where it listens ────────────────────────────────────────────────────────
|
||||||
|
# 1122 is what production serves on. Change it locally to run a second copy
|
||||||
|
# beside something else; the console then points at the same number.
|
||||||
|
APP_PORT=1122
|
||||||
|
|
||||||
|
ENV=development
|
||||||
|
|
||||||
|
# ── The main database (nearledb) ────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# ⚠️ POINTING THIS AT PRODUCTION MAKES LOCAL TESTING WRITE TO PRODUCTION.
|
||||||
|
#
|
||||||
|
# There is no "local mode" that protects you: `go run .` against the live host
|
||||||
|
# creates real tenants, real logins and real stock movements, and main.go runs
|
||||||
|
# schema migrations on boot. If the point of running locally is to try a change
|
||||||
|
# before it is deployed, a local Postgres with a dump restored into it is the
|
||||||
|
# only version that actually does that.
|
||||||
|
# These match docker-compose.local.yml, so `docker compose -f
|
||||||
|
# docker-compose.local.yml up -d` and `go run .` work together with no edits.
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5433
|
||||||
|
DB_NAME=nearledb
|
||||||
|
DB_USER=nearle
|
||||||
|
DB_PASSWORD=localdev
|
||||||
|
|
||||||
|
# ── The catalogue database (pgvector) ───────────────────────────────────────
|
||||||
|
#
|
||||||
|
# A separate connection on purpose, so catalogue work never touches nearledb.
|
||||||
|
# Leave blank to start without it: catalogue endpoints then fail at query time
|
||||||
|
# rather than at boot, which is fine for testing anything else.
|
||||||
|
# 5434, not 5432: a developer machine usually has something on 5432 already,
|
||||||
|
# and a silent connection to the wrong database is worse than a refused one.
|
||||||
|
CATALOGUE_DB_HOST=localhost
|
||||||
|
CATALOGUE_DB_PORT=5434
|
||||||
|
CATALOGUE_DB_NAME=cataloguedb
|
||||||
|
CATALOGUE_DB_USER=nearle
|
||||||
|
CATALOGUE_DB_PASSWORD=localdev
|
||||||
|
|
||||||
|
# ── Redis — POS terminal presence, under a TTL ──────────────────────────────
|
||||||
|
#
|
||||||
|
# Optional. Losing the health board is an inconvenience; losing a sale is not,
|
||||||
|
# so the API runs without it.
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_USER=
|
||||||
|
REDIS_DB=0
|
||||||
|
|
||||||
|
# ── Auth ────────────────────────────────────────────────────────────────────
|
||||||
|
JWT_SECRET_KEY=
|
||||||
|
USER_CONTEXT_KEY=
|
||||||
114
docker-compose.local.yml
Normal file
114
docker-compose.local.yml
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# A local stack to develop Fiesta against, so a change can be tried before it is
|
||||||
|
# deployed.
|
||||||
|
#
|
||||||
|
# Named `.local` because it is NOT the deployment compose. Nothing here should
|
||||||
|
# ever run on a server: the passwords are literals, the ports are published to
|
||||||
|
# the host, and the whole point is that the data is disposable.
|
||||||
|
#
|
||||||
|
# docker compose -f docker-compose.local.yml up -d
|
||||||
|
#
|
||||||
|
# ── READ THIS FIRST ─────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# An EMPTY database is not enough to boot Fiesta. `main.go` runs migrations on
|
||||||
|
# startup, and most of them assume tables that nothing in this repository
|
||||||
|
# creates — `ALTER TABLE products`, `ALTER TABLE productlocations`. AutoMigrate
|
||||||
|
# covers only stockrequests, the POS order tables and staffshifts. Against a
|
||||||
|
# blank database the first ALTER fails and `log.Fatal` stops the process.
|
||||||
|
#
|
||||||
|
# So load the schema before the first run:
|
||||||
|
#
|
||||||
|
# pg_dump --schema-only --no-owner --no-privileges \
|
||||||
|
# -h <live-host> -p 5433 -U <user> -d nearledb \
|
||||||
|
# > init/nearledb/01-schema.sql
|
||||||
|
#
|
||||||
|
# Anything in ./init/nearledb is applied, in filename order, the first time the
|
||||||
|
# volume is created. To reload after changing it, drop the volume:
|
||||||
|
#
|
||||||
|
# docker compose -f docker-compose.local.yml down -v
|
||||||
|
#
|
||||||
|
# ── Why bother ──────────────────────────────────────────────────────────────
|
||||||
|
#
|
||||||
|
# Because pointing `DB_HOST` at production is not local testing — it is
|
||||||
|
# production with a local UI. `go run .` there creates real tenants and real
|
||||||
|
# logins, and runs those same schema migrations against live data.
|
||||||
|
|
||||||
|
services:
|
||||||
|
# The main database. Port 5433 on the host, matching production's DB_PORT, so
|
||||||
|
# the only line that changes between the two is DB_HOST.
|
||||||
|
nearledb:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: nearle-db-local
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: nearledb
|
||||||
|
POSTGRES_USER: nearle
|
||||||
|
POSTGRES_PASSWORD: localdev
|
||||||
|
# Asia/Kolkata to match the DSN Fiesta builds. Timestamps written here
|
||||||
|
# otherwise differ from production by five and a half hours, which is the
|
||||||
|
# sort of thing that looks like a bug in the code being tested.
|
||||||
|
TZ: Asia/Kolkata
|
||||||
|
PGTZ: Asia/Kolkata
|
||||||
|
ports:
|
||||||
|
- '5433:5432'
|
||||||
|
volumes:
|
||||||
|
- nearledb-data:/var/lib/postgresql/data
|
||||||
|
- ./init/nearledb:/docker-entrypoint-initdb.d:ro
|
||||||
|
healthcheck:
|
||||||
|
# `go run .` fails hard if the database is not up yet, so the compose
|
||||||
|
# waits for a real connection rather than for the container to exist.
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U nearle -d nearledb']
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
# The catalogue database, kept separate exactly as it is in production — the
|
||||||
|
# comment on `db.CatalogueDB` is explicit that catalogue work must never touch
|
||||||
|
# nearledb, and one container per database is the cheapest way to keep that
|
||||||
|
# true locally too.
|
||||||
|
#
|
||||||
|
# The pgvector image rather than plain postgres: the live catalogue is a
|
||||||
|
# pgvector database. Nothing in Fiesta's Go code uses a vector column today —
|
||||||
|
# it reads the per-brand tables — but a schema dump from the real one will
|
||||||
|
# carry `CREATE EXTENSION vector`, and that fails on a stock image.
|
||||||
|
#
|
||||||
|
# 5434 on the host, not 5432: a developer machine usually already has
|
||||||
|
# something on 5432, and a silent connection to the wrong database is worse
|
||||||
|
# than a refused one.
|
||||||
|
cataloguedb:
|
||||||
|
image: pgvector/pgvector:pg16
|
||||||
|
container_name: nearle-catalogue-local
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: cataloguedb
|
||||||
|
POSTGRES_USER: nearle
|
||||||
|
POSTGRES_PASSWORD: localdev
|
||||||
|
TZ: Asia/Kolkata
|
||||||
|
PGTZ: Asia/Kolkata
|
||||||
|
ports:
|
||||||
|
- '5434:5432'
|
||||||
|
volumes:
|
||||||
|
- cataloguedb-data:/var/lib/postgresql/data
|
||||||
|
- ./init/cataloguedb:/docker-entrypoint-initdb.d:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U nearle -d cataloguedb']
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
|
||||||
|
# POS terminal presence, under a TTL.
|
||||||
|
#
|
||||||
|
# Optional in the same way it is optional in production: `db.InitRedis()`
|
||||||
|
# failing costs the till health board and nothing else, because losing a sale
|
||||||
|
# matters and losing a dashboard does not. Included because it is one line.
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: nearle-redis-local
|
||||||
|
ports:
|
||||||
|
- '6379:6379'
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'redis-cli', 'ping']
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
nearledb-data:
|
||||||
|
cataloguedb-data:
|
||||||
10
init/.gitignore
vendored
Normal file
10
init/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Schema and data dumps. A dump taken from production carries real customers,
|
||||||
|
# real orders and cleartext passwords — none of it belongs in this repository,
|
||||||
|
# and a local seed is regenerated in one command anyway.
|
||||||
|
*.sql
|
||||||
|
|
||||||
|
# ...except the synthetic fixture. It invents a merchant rather than copying
|
||||||
|
# one, so there is nothing in it to leak and everybody gets the same shop to
|
||||||
|
# develop against.
|
||||||
|
!02-seed.sql
|
||||||
|
*.dump
|
||||||
49
init/README.md
Normal file
49
init/README.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Local database seed
|
||||||
|
|
||||||
|
Anything in `nearledb/` or `cataloguedb/` is applied by Postgres, in filename
|
||||||
|
order, **the first time the volume is created**. Editing a file later does
|
||||||
|
nothing on its own — drop the volume to re-apply:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose -f ../docker-compose.local.yml down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why this is not optional
|
||||||
|
|
||||||
|
Fiesta runs migrations on boot, and most of them assume tables that nothing in
|
||||||
|
this repository creates:
|
||||||
|
|
||||||
|
```
|
||||||
|
ALTER TABLE products ADD COLUMN IF NOT EXISTS productimages
|
||||||
|
ALTER TABLE products ADD COLUMN IF NOT EXISTS imageid
|
||||||
|
ALTER TABLE productlocations ADD COLUMN IF NOT EXISTS publishedat
|
||||||
|
```
|
||||||
|
|
||||||
|
`AutoMigrate` covers only `stockrequests`, the two POS order tables and
|
||||||
|
`staffshifts`. Against an empty database the first `ALTER` fails and
|
||||||
|
`log.Fatal` stops the process — so a schema is required before the first run.
|
||||||
|
|
||||||
|
## Getting the schema
|
||||||
|
|
||||||
|
Structure only, no data, no ownership:
|
||||||
|
|
||||||
|
```
|
||||||
|
pg_dump --schema-only --no-owner --no-privileges \
|
||||||
|
-h <live-host> -p 5433 -U <user> -d nearledb \
|
||||||
|
> nearledb/01-schema.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
**Take the schema, not the data.** A dump with rows in it puts real customers,
|
||||||
|
real orders and real (cleartext) passwords on a laptop, and this directory is
|
||||||
|
inside a git repository. `.gitignore` excludes `*.sql` here for that reason.
|
||||||
|
|
||||||
|
## Getting something to test against
|
||||||
|
|
||||||
|
An empty schema boots but has no tenants, so there is nothing to sign in as.
|
||||||
|
Two options:
|
||||||
|
|
||||||
|
- **Onboard a tenant through the console** once it is pointed at localhost.
|
||||||
|
That exercises the real path and is usually what you want.
|
||||||
|
- **Copy a few rows** you actually need — a tenant, its locations, its
|
||||||
|
app_users — with `pg_dump --data-only --table=...`. Check what you are
|
||||||
|
copying: `app_users.password` is stored in clear.
|
||||||
164
init/nearledb/02-seed.sql
Normal file
164
init/nearledb/02-seed.sql
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
-- A synthetic shop to develop against.
|
||||||
|
--
|
||||||
|
-- INVENTED DATA, and that is the point. Copying rows out of production to get a
|
||||||
|
-- working local console puts real customers, real orders and real (cleartext)
|
||||||
|
-- passwords on a laptop — so this file makes up a merchant instead, which means
|
||||||
|
-- it can be committed, shared, and reset without anybody thinking about what is
|
||||||
|
-- in it.
|
||||||
|
--
|
||||||
|
-- Everything uses ids from 9000 up, well clear of anything real, so a local
|
||||||
|
-- database that has also had production rows loaded into it will not collide.
|
||||||
|
--
|
||||||
|
-- What it gives you:
|
||||||
|
--
|
||||||
|
-- * a complete merchant (9001 Testmart) — profile filled in, so the setup
|
||||||
|
-- walkthrough shows it finished
|
||||||
|
-- * an INCOMPLETE merchant (9002 Halfmart) with `categoryid = 0` — the exact
|
||||||
|
-- shape that broke `gettenantinfo` and made the profile step impossible to
|
||||||
|
-- finish. Worth keeping as a permanent regression fixture.
|
||||||
|
-- * two branches, so the "All branches" tenant-wide read has something to
|
||||||
|
-- aggregate and cannot silently show one outlet
|
||||||
|
-- * one account per role, so every workspace can be signed into
|
||||||
|
-- * products in each of the three app-visibility states — on sale, no stock,
|
||||||
|
-- no price — because those three are what most console bugs turn out to be
|
||||||
|
--
|
||||||
|
-- Passwords are the literal string below. Fiesta compares passwords in clear,
|
||||||
|
-- which is a real problem in production and simply a fact here.
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- ── Masters the tenant joins hang off ───────────────────────────────────────
|
||||||
|
INSERT INTO app_location (applocationid, locationname, latitude, longitude, radius)
|
||||||
|
VALUES (9001, 'Testville', '11.0168', '76.9558', 25000)
|
||||||
|
ON CONFLICT (applocationid) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO app_category (categoryid, categoryname)
|
||||||
|
VALUES (9001, 'Grocery')
|
||||||
|
ON CONFLICT (categoryid) DO NOTHING;
|
||||||
|
|
||||||
|
-- ── Merchant one: complete ──────────────────────────────────────────────────
|
||||||
|
INSERT INTO tenants (
|
||||||
|
tenantid, tenantname, companyname, configid, categoryid, applocationid,
|
||||||
|
primaryemail, primarycontact, address, suburb, city, state, postcode,
|
||||||
|
latitude, longitude, tenantimage, tenantinfo, licenseno, registrationno,
|
||||||
|
minorder, approved, status
|
||||||
|
) VALUES (
|
||||||
|
9001, 'Testmart', 'Testmart Retail', 1, 9001, 9001,
|
||||||
|
'owner@testmart.invalid', '9000000001', '1 Test Street', 'Testville', 'Coimbatore',
|
||||||
|
'Tamil Nadu', '641001', '11.0168', '76.9558',
|
||||||
|
'https://placehold.co/200x200?text=Testmart', 'Daily needs and fresh produce.',
|
||||||
|
'12345678901234', 'REG-TESTMART-1',
|
||||||
|
99, 1, 'Active'
|
||||||
|
) ON CONFLICT (tenantid) DO NOTHING;
|
||||||
|
|
||||||
|
-- ── Merchant two: the regression fixture ────────────────────────────────────
|
||||||
|
--
|
||||||
|
-- `categoryid = 0` matches no `app_category` row. Both master joins in
|
||||||
|
-- GetTenantByID were INNER, so this tenant came back as an all-zero record —
|
||||||
|
-- the console showed an empty profile form over a real business, and the setup
|
||||||
|
-- walkthrough's first step could never complete. Four of two hundred live
|
||||||
|
-- tenants are in this state. Keep it: it is the cheapest possible guard against
|
||||||
|
-- that join going back.
|
||||||
|
INSERT INTO tenants (
|
||||||
|
tenantid, tenantname, configid, categoryid, applocationid,
|
||||||
|
primaryemail, primarycontact, address, city, state, postcode, approved, status
|
||||||
|
) VALUES (
|
||||||
|
9002, 'Halfmart', 1, 0, 9001,
|
||||||
|
'owner@halfmart.invalid', '9000000002', '2 Test Street', 'Coimbatore',
|
||||||
|
'Tamil Nadu', '641002', 1, 'Active'
|
||||||
|
) ON CONFLICT (tenantid) DO NOTHING;
|
||||||
|
|
||||||
|
-- ── Two branches, so tenant-wide reads have something to aggregate ──────────
|
||||||
|
INSERT INTO tenantlocations (
|
||||||
|
locationid, tenantid, locationname, email, contactno, address, suburb, city,
|
||||||
|
state, postcode, latitude, longitude, opentime, closetime, applocationid,
|
||||||
|
deliveryradius, deliverymins, status
|
||||||
|
) VALUES
|
||||||
|
(9101, 9001, 'Testmart Main', 'main@testmart.invalid', '9000000011',
|
||||||
|
'1 Test Street', 'Testville', 'Coimbatore', 'Tamil Nadu', '641001',
|
||||||
|
'11.0168', '76.9558', '08:00', '22:00', 9001, 5000, 30, 'Active'),
|
||||||
|
(9102, 9001, 'Testmart North', 'north@testmart.invalid', '9000000012',
|
||||||
|
'9 North Road', 'Northville', 'Coimbatore', 'Tamil Nadu', '641004',
|
||||||
|
'11.0500', '76.9600', '09:00', '21:00', 9001, 5000, 30, 'Active'),
|
||||||
|
(9103, 9002, 'Halfmart Main', 'main@halfmart.invalid', '9000000021',
|
||||||
|
'2 Test Street', 'Testville', 'Coimbatore', 'Tamil Nadu', '641002',
|
||||||
|
'11.0170', '76.9560', '08:00', '22:00', 9001, 5000, 30, 'Active')
|
||||||
|
ON CONFLICT (locationid) DO NOTHING;
|
||||||
|
|
||||||
|
-- ── One account per role ────────────────────────────────────────────────────
|
||||||
|
--
|
||||||
|
-- `authname` and `configid` are both set on every row. Login is
|
||||||
|
-- `WHERE authname = ? AND configid = ?` and never looks at the email column, so
|
||||||
|
-- an account missing either is created, listed, and refused at the sign-in
|
||||||
|
-- screen. That was a real bug; these rows are what it looks like done right.
|
||||||
|
--
|
||||||
|
-- roleid decides the workspace: 1 and 3 reach Store Admin, everything else is a
|
||||||
|
-- branch user. 7 and 8 are till accounts and are excluded from every
|
||||||
|
-- back-office query by the backend itself.
|
||||||
|
INSERT INTO app_users (
|
||||||
|
userid, authname, firstname, lastname, email, dialcode, contactno,
|
||||||
|
configid, roleid, password, tenantid, locationid, applocationid,
|
||||||
|
status, issuperadmin
|
||||||
|
) VALUES
|
||||||
|
(9201, 'admin@testmart.invalid', 'Tessa', 'Admin', 'admin@testmart.invalid',
|
||||||
|
'+91', '9000000101', 1, 3, 'localdev', 9001, 9101, 9001, 'Active', false),
|
||||||
|
(9202, 'main@testmart.invalid', 'Mani', 'Manager', 'main@testmart.invalid',
|
||||||
|
'+91', '9000000102', 1, 4, 'localdev', 9001, 9101, 9001, 'Active', false),
|
||||||
|
-- Hired, not yet placed. `locationid = 0` is the state the people screen
|
||||||
|
-- exists to resolve, and it was invisible until GetStaffs stopped INNER
|
||||||
|
-- JOINing tenantlocations.
|
||||||
|
(9203, 'newhire@testmart.invalid', 'Nila', 'Newhire', 'newhire@testmart.invalid',
|
||||||
|
'+91', '9000000103', 1, 4, 'localdev', 9001, 0, 9001, 'Active', false),
|
||||||
|
(9204, 'super@testmart.invalid', 'Sup', 'Ervisor', 'super@testmart.invalid',
|
||||||
|
'+91', '9000000104', 1, 7, 'localdev', 9001, 9101, 9001, 'Active', false),
|
||||||
|
(9205, 'cash@testmart.invalid', 'Cash', 'Ier', 'cash@testmart.invalid',
|
||||||
|
'+91', '9000000105', 1, 8, 'localdev', 9001, 9101, 9001, 'Active', false),
|
||||||
|
(9206, 'admin@halfmart.invalid', 'Hal', 'Admin', 'admin@halfmart.invalid',
|
||||||
|
'+91', '9000000201', 1, 3, 'localdev', 9002, 9103, 9001, 'Active', false)
|
||||||
|
ON CONFLICT (userid) DO NOTHING;
|
||||||
|
|
||||||
|
-- ── Products, in each of the three visibility states ────────────────────────
|
||||||
|
--
|
||||||
|
-- categoryid 2 is what the customer app browses. A product filed anywhere else
|
||||||
|
-- is invisible to shoppers however well priced and stocked, which is the single
|
||||||
|
-- most common cause of "it is not showing in the app".
|
||||||
|
INSERT INTO products (
|
||||||
|
productid, tenantid, categoryid, subcategoryid, productname, productbrand,
|
||||||
|
productsku, productunit, productcost, retailprice, taxpercent, approve,
|
||||||
|
productimage, productdesc
|
||||||
|
) VALUES
|
||||||
|
(9301, 9001, 2, 0, 'Test Rice 5kg', 'Testbrand', 'TM-RICE-5K', '5kg',
|
||||||
|
320, 395, 5, 1, 'https://placehold.co/120x120?text=Rice', 'Everyday long grain'),
|
||||||
|
(9302, 9001, 2, 0, 'Test Oil 1L', 'Testbrand', 'TM-OIL-1L', '1L',
|
||||||
|
150, 198, 5, 1, 'https://placehold.co/120x120?text=Oil', 'Cooking oil'),
|
||||||
|
(9303, 9001, 2, 0, 'Test Biscuits 100g', 'Testbrand', 'TM-BISC-100', '100g',
|
||||||
|
18, 25, 12, 1, 'https://placehold.co/120x120?text=Biscuits', 'Sweet biscuits'),
|
||||||
|
-- Filed under no category: priced, released and stocked below, and still
|
||||||
|
-- invisible to the app. The state that catches everybody.
|
||||||
|
(9304, 9001, 0, 0, 'Test Uncategorised', 'Testbrand', 'TM-UNCAT', '1pc',
|
||||||
|
10, 15, 0, 1, 'https://placehold.co/120x120?text=Uncat', 'No category on purpose')
|
||||||
|
ON CONFLICT (productid) DO NOTHING;
|
||||||
|
|
||||||
|
-- price + publishedat = priced and released. Both are needed: a product with
|
||||||
|
-- one and not the other looks identical on the shelf and cannot be sold.
|
||||||
|
INSERT INTO productlocations (
|
||||||
|
productlocationid, tenantid, locationid, productid, price, status, publishedat
|
||||||
|
) VALUES
|
||||||
|
(9401, 9001, 9101, 9301, 395, 'Active', NOW()), -- on sale
|
||||||
|
(9402, 9001, 9101, 9302, 198, 'Active', NOW()), -- released, no stock below
|
||||||
|
(9403, 9001, 9101, 9303, 0, 'Active', NULL), -- no price, not released
|
||||||
|
(9404, 9001, 9101, 9304, 15, 'Active', NOW()), -- everything but a category
|
||||||
|
(9405, 9001, 9102, 9301, 395, 'Active', NOW()) -- same product, second branch
|
||||||
|
ON CONFLICT (productlocationid) DO NOTHING;
|
||||||
|
|
||||||
|
-- Stock is SUM(in) - SUM(out) per outlet, never a stored figure.
|
||||||
|
INSERT INTO productstocks (
|
||||||
|
productstockid, tenantid, locationid, productid, stockdate, stocktype, quantity, status
|
||||||
|
) VALUES
|
||||||
|
(9501, 9001, 9101, 9301, NOW(), 'in', 40, 'Active'),
|
||||||
|
(9502, 9001, 9101, 9301, NOW(), 'out', 5, 'Active'), -- balance 35
|
||||||
|
(9503, 9001, 9101, 9304, NOW(), 'in', 10, 'Active'),
|
||||||
|
(9504, 9001, 9102, 9301, NOW(), 'in', 12, 'Active')
|
||||||
|
ON CONFLICT (productstockid) DO NOTHING;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
13
main.go
13
main.go
@@ -255,8 +255,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
|
//
|
||||||
|
// The port comes from APP_PORT, defaulting to the 1122 this has always
|
||||||
|
// served on. It was hardcoded, which left `config.Load()`'s Port field
|
||||||
|
// dead and the Dockerfile's `EXPOSE 1009` describing a port nothing
|
||||||
|
// listened on — and made running a second copy locally, on a free port,
|
||||||
|
// impossible without editing this line.
|
||||||
|
port := os.Getenv("APP_PORT")
|
||||||
|
if port == "" {
|
||||||
|
port = "1122"
|
||||||
|
}
|
||||||
go func() {
|
go func() {
|
||||||
if err := app.Listen(":1122"); err != nil {
|
log.Printf("🚀 listening on :%s", port)
|
||||||
|
if err := app.Listen(":" + port); err != nil {
|
||||||
log.Fatal("Server failed to start:", err)
|
log.Fatal("Server failed to start:", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -3,28 +3,41 @@ package models
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type Tenantinfo struct {
|
type Tenantinfo struct {
|
||||||
Tenantid int `json:"tenantid" gorm:"Primary_Key"`
|
Tenantid int `json:"tenantid" gorm:"Primary_Key"`
|
||||||
Locationid int `json:"locationid"`
|
Locationid int `json:"locationid"`
|
||||||
Tenantname string `json:"tenantname"`
|
Tenantname string `json:"tenantname"`
|
||||||
Locationname string `json:"locationname"`
|
Locationname string `json:"locationname"`
|
||||||
Tenanttype string `json:"tenanttype"`
|
Tenanttype string `json:"tenanttype"`
|
||||||
Registrationno string `json:"registrationno"`
|
Registrationno string `json:"registrationno"`
|
||||||
Tenanttoken string `json:"tenanttoken"`
|
Tenanttoken string `json:"tenanttoken"`
|
||||||
Companyname string `json:"companyname"`
|
Companyname string `json:"companyname"`
|
||||||
Primaryemail string `json:"primaryemail"`
|
Primaryemail string `json:"primaryemail"`
|
||||||
Primarycontact string `json:"primarycontact"`
|
Primarycontact string `json:"primarycontact"`
|
||||||
Locationcatact string `json:"locationcontact"`
|
Locationcatact string `json:"locationcontact"`
|
||||||
Categoryid int `json:"categoryid"`
|
Categoryid int `json:"categoryid"`
|
||||||
Subcategoryid int `json:"subcategoryid"`
|
Subcategoryid int `json:"subcategoryid"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
Suburb string `json:"suburb"`
|
Suburb string `json:"suburb"`
|
||||||
City string `json:"city"`
|
City string `json:"city"`
|
||||||
State string `json:"state"`
|
State string `json:"state"`
|
||||||
Postcode string `json:"postcode"`
|
Postcode string `json:"postcode"`
|
||||||
Latitude string `json:"latitude"`
|
Latitude string `json:"latitude"`
|
||||||
Longitude string `json:"longitude"`
|
Longitude string `json:"longitude"`
|
||||||
Tenantimage string `json:"tenantimage"`
|
Tenantimage string `json:"tenantimage"`
|
||||||
Tenantinfo string `json:"tenantinfo"`
|
Tenantinfo string `json:"tenantinfo"`
|
||||||
|
// The FSSAI or trade licence.
|
||||||
|
//
|
||||||
|
// The column has always existed and `GetCustomerTenants` already selects it
|
||||||
|
// for the customer app — it was missing from THIS struct alone, so
|
||||||
|
// `gettenantinfo` could never return it however well it was stored.
|
||||||
|
//
|
||||||
|
// That is not cosmetic. The shop-profile screen reads a merchant's own
|
||||||
|
// record back through this endpoint, and the setup walkthrough decides
|
||||||
|
// whether the profile step is finished by looking at exactly this field. A
|
||||||
|
// merchant could type their licence, save it, see it stored — and the step
|
||||||
|
// would stay open forever, because the read that judges it never carried
|
||||||
|
// the value.
|
||||||
|
Licenseno string `json:"licenseno"`
|
||||||
Paymenttype int `json:"paymenttype"`
|
Paymenttype int `json:"paymenttype"`
|
||||||
Paymode1 int `json:"paymode1"`
|
Paymode1 int `json:"paymode1"`
|
||||||
Paymode2 int `json:"paymode2"`
|
Paymode2 int `json:"paymode2"`
|
||||||
|
|||||||
@@ -777,6 +777,24 @@ func (r *tenantRepository) GetUserByNo(cno string) models.UserInfo {
|
|||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTenantByID reads one business.
|
||||||
|
//
|
||||||
|
// Both master joins are LEFT, and that is the whole fix. They were INNER —
|
||||||
|
// `app_category` on `a.categoryid` and `app_location` on `a.applocationid` — so
|
||||||
|
// a tenant whose category or city master row is missing did not come back
|
||||||
|
// "without a category name", it did not come back AT ALL. The endpoint answered
|
||||||
|
// 200 with an all-zero record: tenantid 0, every string empty.
|
||||||
|
//
|
||||||
|
// Four of two hundred tenants have `categoryid = 0`, which matches no
|
||||||
|
// `app_category` row, and a newly onboarded shop is the likeliest to be one of
|
||||||
|
// them. The damage was silent and total: the shop-profile screen seeded an
|
||||||
|
// empty form over a business that had an address, and the setup walkthrough
|
||||||
|
// read every field as blank forever — so its first step could never complete
|
||||||
|
// however many times somebody saved it.
|
||||||
|
//
|
||||||
|
// The same INNER-JOIN-as-a-filter mistake has been fixed twice before in this
|
||||||
|
// file's neighbours, in GetStaffs and GetUserById. A master row is context; its
|
||||||
|
// absence must never delete the record it decorates.
|
||||||
func (r *tenantRepository) GetTenantByID(tid int, locationid int, userid int) (models.Tenantinfo, error) {
|
func (r *tenantRepository) GetTenantByID(tid int, locationid int, userid int) (models.Tenantinfo, error) {
|
||||||
var data models.Tenantinfo
|
var data models.Tenantinfo
|
||||||
|
|
||||||
@@ -792,8 +810,8 @@ func (r *tenantRepository) GetTenantByID(tid int, locationid int, userid int) (m
|
|||||||
SELECT a.*,b.categoryname,c.locationname AS applocation, d.allocationid AS allocationmode,e.typename AS allocationtype,e.mapid AS allocationid,f.locationid,
|
SELECT a.*,b.categoryname,c.locationname AS applocation, d.allocationid AS allocationmode,e.typename AS allocationtype,e.mapid AS allocationid,f.locationid,
|
||||||
f.locationname, f.contactno as locationcontact
|
f.locationname, f.contactno as locationcontact
|
||||||
FROM tenants a
|
FROM tenants a
|
||||||
INNER JOIN app_category b ON a.categoryid = b.categoryid
|
LEFT JOIN app_category b ON a.categoryid = b.categoryid
|
||||||
INNER JOIN app_location c ON a.applocationid = c.applocationid
|
LEFT JOIN app_location c ON a.applocationid = c.applocationid
|
||||||
LEFT JOIN partnerinfo d ON a.partnerid = d.partnerid
|
LEFT JOIN partnerinfo d ON a.partnerid = d.partnerid
|
||||||
LEFT JOIN app_types e ON d.allocationid = e.apptypeid
|
LEFT JOIN app_types e ON d.allocationid = e.apptypeid
|
||||||
LEFT JOIN tenantlocations f ON a.tenantid = f.tenantid
|
LEFT JOIN tenantlocations f ON a.tenantid = f.tenantid
|
||||||
|
|||||||
Reference in New Issue
Block a user