mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
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]>
136 lines
3.9 KiB
Go
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)
|
|
}
|