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]>
142 lines
6.6 KiB
Go
142 lines
6.6 KiB
Go
package organization
|
|
|
|
// TestMode reports whether this org transacts in TEST mode. It is the SINGLE
|
|
// authority for BOTH the payment environment (Square sandbox vs production) AND
|
|
// the ledger (trans.Test, balance bucket, pay.Live). Keeping the charge
|
|
// environment and the ledger on ONE authority is what prevents a sandbox charge
|
|
// from crediting the live (spendable) balance and a production charge from
|
|
// booking test (unbilled) revenue.
|
|
//
|
|
// The authority is the ORG RECORD, and nothing else. It used to be the
|
|
// deployment's SQUARE_ENVIRONMENT, which consulted o.Live only when that
|
|
// variable was UNSET — so on any templated deploy every tenant was forced into
|
|
// one mode and each org's own flag was dead. That is what made a second
|
|
// deployment necessary to serve a sandbox merchant (commerce-api.testnet.hanzo.ai
|
|
// exists for exactly this reason), and why a replica was never freely
|
|
// interchangeable: its behaviour depended on which env block started it.
|
|
// Resolving per org is what lets ONE stateless replica serve a sandbox merchant
|
|
// and a live merchant in the same process, on the same request path.
|
|
//
|
|
// It is also what "configured per org, not in env files" means here. The
|
|
// credentials were already per org — o.Square.Sandbox / o.Square.Production,
|
|
// o.Stripe.Test / o.Stripe.Live, KMS-backed — and StripeToken() already chose
|
|
// between them from o.Live alone. Only the mode stayed deployment-wide, so the
|
|
// two could disagree. Now both read the same per-org fact.
|
|
//
|
|
// Still fail-CLOSED, but per tenant instead of per deployment: an org is in
|
|
// production only when its own record says Live, so a new, unset or
|
|
// half-configured org transacts in sandbox and a missing flag can never silently
|
|
// charge real cards. What improves over the env gate is isolation — one org's
|
|
// misconfiguration can no longer drag every other tenant on the pod into the
|
|
// wrong environment, in either direction.
|
|
//
|
|
// Above the record sits the deployment's Ceiling: where it is Test, every org
|
|
// transacts in test, a record marked Live included.
|
|
func (o Organization) TestMode() bool {
|
|
return !o.Live || Ceiling != Live
|
|
}
|
|
|
|
// SquareEnvironment returns "sandbox" or "production" — the value the Square SDK
|
|
// uses to select its API base URL — derived from TestMode (one authority).
|
|
func (o Organization) SquareEnvironment() string {
|
|
return o.kind().Word("square")
|
|
}
|
|
|
|
// A Kind is what an environment transacts with. There are two of them, and
|
|
// every payment processor has the same two under a different name — which is
|
|
// the whole reason to state them once here. Stripe says live and test, Square
|
|
// says production and sandbox, Braintree says production and sandbox, Adyen
|
|
// says live and test. Spelling a processor's word at each of the places that
|
|
// talks to it is how a sandbox key comes to be sent to a live endpoint.
|
|
type Kind string
|
|
|
|
const (
|
|
Live Kind = "live"
|
|
Test Kind = "test"
|
|
)
|
|
|
|
// Born is the kind a NEW org's record is created in, and only that: it is read
|
|
// once, as the record is first written, and never again. Every read after that
|
|
// is the record's own Live, so moving an existing org between kinds stays the
|
|
// one explicit act it was.
|
|
//
|
|
// The deployment declares it, because only the deployment knows whether real
|
|
// money exists where it runs. Undeclared, it is Test: a commerce that says
|
|
// nothing about its world creates sandbox tenants, the fail-closed direction
|
|
// TestMode describes. A host whose network exists to take real money declares
|
|
// Live (EmbedConfig.Born), and its customers are born able to pay.
|
|
var Born = Test
|
|
|
|
// Ceiling is the most an org may transact in, deployment-wide. Where it is Test,
|
|
// TestMode is true for every org whatever its record says, so a record marked
|
|
// Live — restored, copied, or set by a caller this deployment never meant to
|
|
// trust — still charges sandbox credentials and writes the test books. Live is
|
|
// no cap, and is what a deployment that declares nothing gets: the record
|
|
// decides, as TestMode describes.
|
|
//
|
|
// The deployment declares it (EmbedConfig.Ceiling), because only the host knows
|
|
// that no real money exists where it runs. Any value but Live caps at Test.
|
|
var Ceiling = Live
|
|
|
|
// An org may hold as many environments as it likes, and each one CHOOSES its
|
|
// kind. That is the point of choosing rather than deriving it from the name: a
|
|
// team can run an experimental build of its platform against real money while a
|
|
// stable one keeps serving customers, and can keep several test environments
|
|
// beside both. Deriving the kind from the name would have allowed exactly one
|
|
// live environment, which is the arrangement this replaces.
|
|
|
|
// word is what each processor calls the two kinds, live first. This table is
|
|
// the only place in commerce that knows a processor's spelling; everything else
|
|
// asks for the Kind and lets the processor's own edge translate it.
|
|
var word = map[string][2]string{
|
|
"stripe": {"live", "test"},
|
|
"square": {"production", "sandbox"},
|
|
"authorizenet": {"production", "sandbox"},
|
|
"paypal": {"live", "sandbox"},
|
|
"adyen": {"live", "test"},
|
|
"braintree": {"production", "sandbox"},
|
|
"lemonsqueezy": {"live", "test"},
|
|
"mercury": {"production", "sandbox"},
|
|
}
|
|
|
|
// Word is how one processor spells this kind. A processor nobody has taught us
|
|
// about gets our own word, which is wrong in a way that is visible at its edge
|
|
// rather than wrong in a way that charges the wrong merchant.
|
|
func (k Kind) Word(processor string) string {
|
|
w, ok := word[processor]
|
|
if !ok {
|
|
return string(k)
|
|
}
|
|
if k == Live {
|
|
return w[0]
|
|
}
|
|
return w[1]
|
|
}
|
|
|
|
// kind reports what this org transacts with as it stands. Unexported because
|
|
// the ORM's entity interface already claims the name Kind for the storage kind,
|
|
// and two different answers to "what kind is this" is how the wrong one gets
|
|
// read.
|
|
func (o Organization) kind() Kind {
|
|
if o.TestMode() {
|
|
return Test
|
|
}
|
|
return Live
|
|
}
|
|
|
|
// In returns this org as it transacts in an environment of one kind.
|
|
//
|
|
// The environment decides live-vs-test, and this is where it decides it — once,
|
|
// on the value, rather than at each of the sixty places that ask. Live already
|
|
// selects the credential pair, the ledger's books and pay.Live, so setting it
|
|
// here carries the environment to all of them without any of them learning a
|
|
// new word. The receiver is a value: the org this returns is a copy scoped to
|
|
// one request, and the stored record is never touched.
|
|
//
|
|
// Anything that is not exactly Live is Test, so an environment that arrives
|
|
// with no kind, or with a kind nobody recognises, charges test credentials.
|
|
func (o Organization) In(k Kind) Organization {
|
|
o.Live = k == Live
|
|
return o
|
|
}
|