Files
zooqueenandHanzo Dev 9705d8bc03 A deployment caps every org at test with organization.Ceiling
EmbedConfig.Ceiling is declared by the host as Born is: Test where the
network holds no real money, "" or Live for no cap, and any other value
caps at Test. Under a Test ceiling TestMode is true for every org, a
record marked Live included, so it charges sandbox credentials, writes
the test books and publishes the sandbox application.

TestMode is the one reading of the mode: the ledger, balance, transaction,
library, account and Authorize.Net paths, StripeToken and PayPal read it
instead of org.Live, so the ceiling holds on each of them.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 07:27:25 -07:00

136 lines
3.9 KiB
Go

package transaction
import (
"github.com/zap-proto/zip"
"github.com/hanzoai/commerce/datastore"
"github.com/hanzoai/commerce/log"
"github.com/hanzoai/commerce/middleware"
"github.com/hanzoai/commerce/models/transaction"
"github.com/hanzoai/commerce/models/transaction/util"
"github.com/hanzoai/commerce/models/types/currency"
"github.com/hanzoai/commerce/util/json"
"github.com/hanzoai/commerce/util/json/http"
. "github.com/hanzoai/commerce/types"
)
type CreateHoldReq struct {
SourceId string `json:"sourceId"`
SourceKind string `json:"sourceKind"`
Currency currency.Type `json:"currency"`
Amount currency.Cents `json:"amount"`
Notes string `json:"notes"`
Tags string `json:"tags"`
Event string `json:"event"`
Metadata Map `json:"metadata"`
}
func CreateHold(c *zip.Ctx) error {
// Money move (places a hold against a balance): admin-only, enforced inside
// the handler (route middleware no-ops on the IAM path — Red HIGH-4).
if !middleware.RequireAdmin(c) {
return nil
}
org := middleware.GetOrganization(c)
db := datastore.NewNamespaced(org.Namespaced(c.Context()))
req := &CreateHoldReq{}
// Decode response body to create new request
if err := json.DecodeBytes(c.Body(), req); err != nil {
return http.Fail(c, 400, "Failed decode request body", err)
}
trans := transaction.New(db)
trans.SourceId = req.SourceId
trans.SourceKind = req.SourceKind
trans.Currency = req.Currency
trans.Amount = req.Amount
trans.Notes = req.Notes
trans.Tags = req.Tags
trans.Event = req.Event
trans.Metadata = req.Metadata
trans.Type = transaction.Hold
if trans.Amount == currency.Cents(0) {
log.Error(ErrorPointlessTransaction.Error(), c)
return http.Fail(c, 500, ErrorPointlessTransaction.Error(), ErrorPointlessTransaction)
}
if trans.Currency == "" {
log.Error(ErrorCurrencyRequired.Error(), c)
return http.Fail(c, 500, ErrorCurrencyRequired.Error(), ErrorCurrencyRequired)
}
if org.TestMode() {
log.Info("Hold created in test mode.", c)
trans.Test = true
}
// The balance is read, decided against, and then written to. Nothing below
// makes that one step — datastore.RunInTransaction is a stub that opens no
// transaction — so two concurrent holds for 100 against a balance of 100 both
// read Balance-Holds=100, both pass, and both create. See lockFunds.
defer lockFunds(trans.SourceKind, trans.SourceId, string(trans.Currency), trans.Test)()
err := db.RunInTransaction(func(db *datastore.Datastore) error {
datas, err := util.GetTransactionsByCurrency(db.Context, trans.SourceId, trans.SourceKind, trans.Currency, org.TestMode())
if err != nil {
return err
}
data := datas.Data[trans.Currency]
if data == nil {
log.Error("Source has no funds'%v'", c)
return ErrorInsufficientFunds
}
if data.Balance-data.Holds < trans.Amount {
log.Error("Source has insufficient funds '%v' - '%v' < '%v'", data.Balance, data.Holds, trans.Amount, c)
return ErrorInsufficientFunds
}
return trans.Create()
}, nil)
if err != nil {
return http.Fail(c, 500, err.Error(), err)
} else {
c.SetHeader("Location", c.Path()+"/"+trans.Id())
return http.Render(c, 201, trans)
}
}
func RemoveHold(c *zip.Ctx) error {
// Money move (releases a hold): admin-only, enforced inside the handler
// (route middleware no-ops on the IAM path — Red HIGH-4).
if !middleware.RequireAdmin(c) {
return nil
}
id := c.Param("id")
org := middleware.GetOrganization(c)
db := datastore.NewNamespaced(org.Namespaced(c.Context()))
trans := transaction.New(db)
err := db.RunInTransaction(func(db *datastore.Datastore) error {
if err := trans.GetById(id); err != nil {
return err
}
trans.Type = transaction.HoldRemoved
if err := trans.Update(); err != nil {
return err
}
return nil
}, nil)
if err != nil {
return http.Fail(c, 500, err.Error(), err)
}
return http.Render(c, 201, trans)
}