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
|
||||
//
|
||||
// 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() {
|
||||
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)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -3,28 +3,41 @@ package models
|
||||
import "time"
|
||||
|
||||
type Tenantinfo struct {
|
||||
Tenantid int `json:"tenantid" gorm:"Primary_Key"`
|
||||
Locationid int `json:"locationid"`
|
||||
Tenantname string `json:"tenantname"`
|
||||
Locationname string `json:"locationname"`
|
||||
Tenanttype string `json:"tenanttype"`
|
||||
Registrationno string `json:"registrationno"`
|
||||
Tenanttoken string `json:"tenanttoken"`
|
||||
Companyname string `json:"companyname"`
|
||||
Primaryemail string `json:"primaryemail"`
|
||||
Primarycontact string `json:"primarycontact"`
|
||||
Locationcatact string `json:"locationcontact"`
|
||||
Categoryid int `json:"categoryid"`
|
||||
Subcategoryid int `json:"subcategoryid"`
|
||||
Address string `json:"address"`
|
||||
Suburb string `json:"suburb"`
|
||||
City string `json:"city"`
|
||||
State string `json:"state"`
|
||||
Postcode string `json:"postcode"`
|
||||
Latitude string `json:"latitude"`
|
||||
Longitude string `json:"longitude"`
|
||||
Tenantimage string `json:"tenantimage"`
|
||||
Tenantinfo string `json:"tenantinfo"`
|
||||
Tenantid int `json:"tenantid" gorm:"Primary_Key"`
|
||||
Locationid int `json:"locationid"`
|
||||
Tenantname string `json:"tenantname"`
|
||||
Locationname string `json:"locationname"`
|
||||
Tenanttype string `json:"tenanttype"`
|
||||
Registrationno string `json:"registrationno"`
|
||||
Tenanttoken string `json:"tenanttoken"`
|
||||
Companyname string `json:"companyname"`
|
||||
Primaryemail string `json:"primaryemail"`
|
||||
Primarycontact string `json:"primarycontact"`
|
||||
Locationcatact string `json:"locationcontact"`
|
||||
Categoryid int `json:"categoryid"`
|
||||
Subcategoryid int `json:"subcategoryid"`
|
||||
Address string `json:"address"`
|
||||
Suburb string `json:"suburb"`
|
||||
City string `json:"city"`
|
||||
State string `json:"state"`
|
||||
Postcode string `json:"postcode"`
|
||||
Latitude string `json:"latitude"`
|
||||
Longitude string `json:"longitude"`
|
||||
Tenantimage string `json:"tenantimage"`
|
||||
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"`
|
||||
Paymode1 int `json:"paymode1"`
|
||||
Paymode2 int `json:"paymode2"`
|
||||
|
||||
@@ -777,6 +777,24 @@ func (r *tenantRepository) GetUserByNo(cno string) models.UserInfo {
|
||||
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) {
|
||||
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,
|
||||
f.locationname, f.contactno as locationcontact
|
||||
FROM tenants a
|
||||
INNER JOIN app_category b ON a.categoryid = b.categoryid
|
||||
INNER JOIN app_location c ON a.applocationid = c.applocationid
|
||||
LEFT JOIN app_category b ON a.categoryid = b.categoryid
|
||||
LEFT JOIN app_location c ON a.applocationid = c.applocationid
|
||||
LEFT JOIN partnerinfo d ON a.partnerid = d.partnerid
|
||||
LEFT JOIN app_types e ON d.allocationid = e.apptypeid
|
||||
LEFT JOIN tenantlocations f ON a.tenantid = f.tenantid
|
||||
|
||||
Reference in New Issue
Block a user