Files
commerce/api/store/listing.go
zeekay 1943f1c093 Hanzo Commerce
Multi-tenant commerce and billing: orders, subscriptions, invoices, plans,
usage metering and a credit ledger. One Go binary. MIT OR Apache-2.0.

Claude-Session: https://claude.ai/code/session_01SAtaDFm8VDiWrhYHNNtxMw
2026-09-23 21:52:16 -07:00

194 lines
5.4 KiB
Go

package store
import (
"errors"
"fmt"
"github.com/zap-proto/zip"
"github.com/hanzoai/commerce/datastore"
"github.com/hanzoai/commerce/middleware"
"github.com/hanzoai/commerce/models/store"
"github.com/hanzoai/commerce/util/json"
"github.com/hanzoai/commerce/util/json/http"
)
// orgNamespacedDB returns a datastore scoped to the authenticated org's namespace.
// All listing handlers MUST use this instead of raw datastore.New(c) to ensure
// tenant isolation.
func orgNamespacedDB(c *zip.Ctx) *datastore.Datastore {
org := middleware.GetOrganization(c)
// Red MED-1: datastore.New binds the shared systemDB regardless of the ctx
// namespace, so the listing sub-routes (create/update/patch/delete/get/list)
// read/write systemDB while the store itself is created per-org via
// NewNamespaced (rest.newEntity) — a mismatch that 404s every listing op on a
// real per-org store. NewNamespaced routes to the caller org's own SQLite,
// matching where the store lives.
return datastore.NewNamespaced(org.Namespaced(c.Context()))
}
// Return all listings
func listListing(c *zip.Ctx) error {
id := c.Param("storeid")
db := orgNamespacedDB(c)
stor := store.New(db)
if err := stor.GetById(id); err != nil {
return http.Fail(c, 404, fmt.Sprintf("Failed to retrieve store '%v': %v", id, err), err)
}
return http.Render(c, 200, stor.Listings)
}
// Get single store listing for given product/variant
func getListing(c *zip.Ctx) error {
db := orgNamespacedDB(c)
id := c.Param("storeid")
key := c.Param("key")
// Get store
stor := store.New(db)
if err := stor.GetById(id); err != nil {
return http.Fail(c, 404, fmt.Sprintf("Failed to retrieve store '%v': %v", id, err), err)
}
// Try and grab listing
listing, ok := stor.Listings[key]
// Maybe we have a slug or sku?
if !ok {
for _, listing = range stor.Listings {
if key == listing.Slug || key == listing.SKU {
ok = true
break
}
}
}
// Do not override on create, user should explicitly update instead
if !ok {
msg := fmt.Sprintf("No listing exists for '%v' in store '%v'", key, id)
return http.Fail(c, 404, msg, errors.New(msg))
}
return http.Render(c, 200, listing)
}
func createListing(c *zip.Ctx) error {
id := c.Param("storeid")
key := c.Param("key")
db := orgNamespacedDB(c)
stor := store.New(db)
if err := stor.GetById(id); err != nil {
msg := fmt.Sprintf("Failed to retrieve store '%v'", id)
return http.Fail(c, 404, msg, err)
}
// Try and grab listing
listing, ok := stor.Listings[key]
// Do not override on create, user should explicitly update instead
if ok {
msg := fmt.Sprintf("'%v' already exists in store '%v' listing", key, id)
return http.Fail(c, 400, msg, errors.New(msg))
}
// Decode response body for listing
if err := json.DecodeBytes(c.Body(), &listing); err != nil {
return http.Fail(c, 400, "Failed decode request body", err)
}
// Update include listing in listings
stor.AddListing(key, listing)
// Save store
if err := stor.Put(); err != nil {
return http.Fail(c, 500, "Failed to save store listings", err)
} else {
c.SetHeader("Location", c.Path())
return http.Render(c, 201, stor.Listings)
}
}
func updateListing(c *zip.Ctx) error {
id := c.Param("storeid")
key := c.Param("key")
org, ok := middleware.GetOrganizationOK(c)
if !ok || org == nil {
return http.Fail(c, 403, "no organization on the request", errors.New("no org"))
}
// The one core the plane op also asks, so a listing written through either
// surface is written the same way — decoded ONTO the existing listing, never
// replacing it.
listings, existed, err := SetListing(c.Context(), org, id, key, c.Body())
if err != nil {
return http.Fail(c, 500, "Failed to save store listings", err)
}
if existed {
return http.Render(c, 200, listings)
}
c.SetHeader("Location", c.Path())
return http.Render(c, 201, listings)
}
func patchListing(c *zip.Ctx) error {
id := c.Param("storeid")
key := c.Param("key")
db := orgNamespacedDB(c)
stor := store.New(db)
if err := stor.GetById(id); err != nil {
return http.Fail(c, 404, fmt.Sprintf("Failed to retrieve store '%v': %v", id, err), err)
}
listing, ok := stor.Listings[key]
// Can't patch an object that doesn't exist!
if !ok {
msg := fmt.Sprintf("No listing exists for '%v' in store '%v'", key, id)
return http.Fail(c, 404, msg, errors.New(msg))
}
// Decode response body to update listings
if err := json.DecodeBytes(c.Body(), &listing); err != nil {
return http.Fail(c, 400, "Failed decode request body", err)
}
// Try to save store
if err := stor.Put(); err != nil {
return http.Fail(c, 500, "Failed to save store listings", err)
} else {
return http.Render(c, 200, stor.Listings)
}
}
func deleteListing(c *zip.Ctx) error {
id := c.Param("storeid")
key := c.Param("key")
db := orgNamespacedDB(c)
stor := store.New(db)
if err := stor.GetById(id); err != nil {
return http.Fail(c, 404, fmt.Sprintf("Failed to retrieve store '%v': %v", id, err), err)
}
// Check if file even exists
_, ok := stor.Listings[key]
if !ok {
msg := fmt.Sprintf("No listing exists for '%v' in store '%v'", key, id)
return http.Fail(c, 404, msg, errors.New(msg))
}
// Delete key from map
delete(stor.Listings, key)
// Try to save store
if err := stor.Put(); err != nil {
return http.Fail(c, 500, "Failed to save store listings", err)
} else {
c.SetHeader("Content-Type", "application/json")
return c.Bytes(204, make([]byte, 0))
}
}