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>
This commit is contained in:
Suriya
2026-07-16 17:34:20 +05:30
parent 950064de6c
commit fab06bb33e
20 changed files with 1408 additions and 15 deletions

View File

@@ -3,6 +3,7 @@ package db
import (
"fmt"
"log"
"net/url"
"os"
"time"
@@ -12,6 +13,10 @@ import (
var (
DB *gorm.DB
// CatalogueDB is a separate connection to the pgvector/catalogue database.
// It is intentionally isolated from DB (nearledb) so catalogue integration
// never touches the main database.
CatalogueDB *gorm.DB
)
// --------------------
@@ -36,6 +41,44 @@ func Connect() {
setupDB(DB)
fmt.Println("✅ Database connected")
connectCatalogueDB()
connectImageStore()
}
// connectCatalogueDB opens the read-only connection to the catalogue
// (pgvector) database. If its env vars are not set, catalogue endpoints
// are simply unavailable — this must never block startup of the main app.
func connectCatalogueDB() {
host := getEnv("CATALOGUE_DB_HOST", "")
if host == "" {
fmt.Println("⚠️ Catalogue DB env vars not set, skipping catalogue DB connection")
return
}
// Built as a URL (not a keyword=value DSN) and percent-encoded via
// net/url, because this credential's password contains characters
// (quotes, #) that the keyword=value DSN format would misparse as
// quoting/comment syntax.
dsnURL := url.URL{
Scheme: "postgres",
User: url.UserPassword(mustEnv("CATALOGUE_DB_USER"), mustEnv("CATALOGUE_DB_PASSWORD")),
Host: fmt.Sprintf("%s:%s", host, getEnv("CATALOGUE_DB_PORT", "5432")),
Path: "/" + mustEnv("CATALOGUE_DB_NAME"),
}
q := dsnURL.Query()
q.Set("sslmode", "disable")
dsnURL.RawQuery = q.Encode()
catalogueDB, err := gorm.Open(postgres.Open(dsnURL.String()), &gorm.Config{})
if err != nil {
log.Println("❌ Could not connect to catalogue database:", err)
return
}
setupDB(catalogueDB)
CatalogueDB = catalogueDB
fmt.Println("✅ Catalogue database connected")
}
func setupDB(database *gorm.DB) {
@@ -53,16 +96,17 @@ func setupDB(database *gorm.DB) {
// --------------------
func CloseDB() {
if DB == nil {
return
if DB != nil {
if sqlDB, err := DB.DB(); err == nil {
sqlDB.Close()
}
}
sqlDB, err := DB.DB()
if err != nil {
log.Println("Error retrieving sql.DB:", err)
return
if CatalogueDB != nil {
if sqlDB, err := CatalogueDB.DB(); err == nil {
sqlDB.Close()
}
}
fmt.Println("Connection closed Successfully")
sqlDB.Close()
}
// --------------------