mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
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
190 lines
5.1 KiB
Go
190 lines
5.1 KiB
Go
package library
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/zap-proto/zip"
|
|
|
|
"sync"
|
|
|
|
"github.com/hanzoai/commerce/datastore"
|
|
"github.com/hanzoai/commerce/middleware"
|
|
"github.com/hanzoai/commerce/models/shippingrates"
|
|
"github.com/hanzoai/commerce/models/store"
|
|
"github.com/hanzoai/commerce/models/taxrates"
|
|
"github.com/hanzoai/commerce/models/types/country"
|
|
"github.com/hanzoai/commerce/models/types/currency"
|
|
"github.com/hanzoai/commerce/util/json"
|
|
"github.com/hanzoai/commerce/util/json/http"
|
|
)
|
|
|
|
// Countries Loading
|
|
type SubDivision struct {
|
|
Name string `json:"name"`
|
|
IsoCode string `json:"code"`
|
|
}
|
|
|
|
type Country struct {
|
|
Name string `json:"name"`
|
|
IsoCode string `json:"code"`
|
|
SubDivisions []SubDivision `json:"subdivisions"`
|
|
}
|
|
|
|
// countries is the shop's own view of the world — each country with its
|
|
// subdivisions — built on FIRST USE.
|
|
//
|
|
// It was an init(), and it walked every country's subdivisions to build a second
|
|
// copy of a dataset that had itself just been unmarshalled from 5.1 MB of YAML.
|
|
// Between them the two cost every process linking this package 121 ms before it
|
|
// could serve anything, for a list that two handlers answer with and nothing else
|
|
// reads. The shop asks for it when a shop is being loaded.
|
|
var countries = sync.OnceValue(func() []Country {
|
|
out := make([]Country, 0)
|
|
for _, c := range country.All() {
|
|
sdvs := make([]SubDivision, 0)
|
|
for _, sd := range c.SubDivisions() {
|
|
sdvs = append(sdvs, SubDivision{sd.Name, sd.Code})
|
|
}
|
|
out = append(out, Country{c.Name.Common, c.Codes.Alpha2, sdvs})
|
|
}
|
|
return out
|
|
})
|
|
|
|
// CountryLastUpdated is when this process's country data came into being, which is
|
|
// process start: the corpus is compiled in, so it cannot change while we run. The
|
|
// init() that set it also BUILT the list, and a client's freshness question does not
|
|
// need the list — only its age — so the stamp is read here and the list is not.
|
|
var CountryLastUpdated = time.Now()
|
|
|
|
// ShopJS request and response
|
|
type LoadShopJSReq struct {
|
|
HasCountries bool `json:"hasCountries"`
|
|
HasTaxRates bool `json:"hasTaxRates"`
|
|
HasShippingRates bool `json:"hasShippingRates"`
|
|
|
|
LastChecked time.Time `json:"lastChecked"`
|
|
StoreId string `json:"storeId"`
|
|
}
|
|
|
|
type LoadShopJSRes struct {
|
|
Countries []Country `json:"countries,omitempty"`
|
|
TaxRates *taxrates.TaxRates `json:"taxRates,omitempty"`
|
|
ShippingRates *shippingrates.ShippingRates `json:"shippingRates,omitempty"`
|
|
Currency currency.Type `json:"currency,omitempty"`
|
|
StoreId string `json:"storeId,omitempty"`
|
|
|
|
Live bool `json:"live"`
|
|
}
|
|
|
|
func LoadShopJS(c *zip.Ctx) error {
|
|
org := middleware.GetOrganization(c)
|
|
db := datastore.New(org.Namespaced(c.Context()))
|
|
|
|
// Decode response body to get ShopJS Params
|
|
req := &LoadShopJSReq{}
|
|
|
|
if err := json.DecodeBytes(c.Body(), req); err != nil {
|
|
return http.Fail(c, 400, "Failed decode request body", err)
|
|
}
|
|
|
|
// Default store if StoreId is left blank
|
|
if req.StoreId == "" {
|
|
req.StoreId = org.DefaultStore
|
|
}
|
|
|
|
stor := store.New(db)
|
|
if err := stor.GetById(req.StoreId); err != nil {
|
|
return http.Fail(c, 404, "Store `"+req.StoreId+"` not found", err)
|
|
}
|
|
|
|
// Build response
|
|
res := LoadShopJSRes{StoreId: req.StoreId}
|
|
|
|
// Determine Test Mode
|
|
res.Live = org.Live
|
|
|
|
if !req.HasCountries ||
|
|
req.LastChecked.Before(CountryLastUpdated) {
|
|
res.Countries = countries()
|
|
}
|
|
|
|
if res.Currency == "" {
|
|
res.Currency = stor.Currency
|
|
}
|
|
|
|
if res.Currency == "" {
|
|
res.Currency = org.Currency
|
|
}
|
|
|
|
if res.Currency == "" {
|
|
res.Currency = currency.USD
|
|
}
|
|
|
|
if req.HasTaxRates {
|
|
tr := taxrates.New(db)
|
|
if ok, err := tr.Query().Filter("StoreId=", req.StoreId).Get(); ok {
|
|
if req.LastChecked.Before(tr.UpdatedAt) {
|
|
res.TaxRates = tr
|
|
}
|
|
} else if err != nil {
|
|
return http.Fail(c, 500, err.Error(), err)
|
|
}
|
|
} else {
|
|
tr := taxrates.New(db)
|
|
if ok, err := tr.Query().Filter("StoreId=", req.StoreId).Get(); ok {
|
|
res.TaxRates = tr
|
|
} else if err != nil {
|
|
return http.Fail(c, 500, err.Error(), err)
|
|
}
|
|
}
|
|
|
|
if req.HasShippingRates {
|
|
sr := shippingrates.New(db)
|
|
if ok, err := sr.Query().Filter("StoreId=", req.StoreId).Get(); ok {
|
|
if req.LastChecked.Before(sr.UpdatedAt) {
|
|
res.ShippingRates = sr
|
|
}
|
|
} else if err != nil {
|
|
return http.Fail(c, 500, err.Error(), err)
|
|
}
|
|
} else {
|
|
sr := shippingrates.New(db)
|
|
if ok, err := sr.Query().Filter("StoreId=", req.StoreId).Get(); ok {
|
|
res.ShippingRates = sr
|
|
} else if err != nil {
|
|
return http.Fail(c, 500, err.Error(), err)
|
|
}
|
|
}
|
|
|
|
return http.Render(c, 200, res)
|
|
}
|
|
|
|
type LoadDaishoReq struct {
|
|
HasCountries bool `json:"hasCountries"`
|
|
|
|
LastChecked time.Time `json:"lastChecked"`
|
|
}
|
|
|
|
type LoadDaishoRes struct {
|
|
Countries []Country `json:"countries,omitempty"`
|
|
}
|
|
|
|
func LoadDaisho(c *zip.Ctx) error {
|
|
// Decode response body to get ShopJS Params
|
|
req := &LoadDaishoReq{}
|
|
|
|
if err := json.DecodeBytes(c.Body(), req); err != nil {
|
|
return http.Fail(c, 400, "Failed decode request body", err)
|
|
}
|
|
|
|
// Build response
|
|
res := LoadDaishoRes{}
|
|
|
|
if !req.HasCountries ||
|
|
req.LastChecked.Before(CountryLastUpdated) {
|
|
res.Countries = countries()
|
|
}
|
|
|
|
return http.Render(c, 200, res)
|
|
}
|