mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
Square keeps an event's created_at across every retry, and the receiver refused anything older than five minutes, so every retry of an event first sent while cloud was down (Recreate, ten rollouts a day) was refused until Square gave up. The verifier now lives in payment/providers/square and needs only the key and the URL; thirdparty/square is the API client and no longer registers itself or verifies anything, so every registry holds one Square type. Replay is stopped by the event id instead: each verified event is recorded once for the whole deployment, keyed on the verifying processor and the event id with an insert-if-absent swap, so a repeat is a duplicate under any path or org. The path names the one processor that verifies, and only one that holds its webhook secret may: an unknown name, or a verifier that needs no secret, is refused rather than tried. The books an event lands in are the verifying account's, else the deployment's brand — never a header or session, which on a replay are the replayer's. An effect that did not land (a store or the ledger did not answer) leaves the delivery open with a 503, so the provider redelivers; it used to be acknowledged and never retried. A settlement reads the payment index before the org's books, because an in-session charge's receipt is in the payer's org. POST /v1/commerce/webhooks/:provider is declared once, in setupRoutes, so standalone and embedded commerce serve the same address; the /v1/billing/webhooks spelling is gone. thirdparty/kms read /v1/kms/orgs/<org>/secrets/..., which cloud no longer serves, and was off in production anyway, so per-org payment credentials never loaded. It is deleted. secrets.Org reads orgs/<org>/<name> through the host's reader, the caller re-pointed at that org with zip.ActingAs so the KMS tenancy check admits it, and payment.Hydrate loads an org's credentials where the org receives the money: storefront checkout, storefront subscriptions and wire, and the brand's bank for the billing wire rail. Hanzo's own billing no longer reads the paying org's credentials; it charges the deployment's account, as it did. The standalone binary reads through hanzoai/kms/sdk/go.
143 lines
5.0 KiB
Go
143 lines
5.0 KiB
Go
// Package square is commerce's one Square payment processor: the type every
|
|
// registry holds for processor.Square. processor.Global() holds one seeded from
|
|
// the deployment environment, which is what verifies webhooks; a per-org registry
|
|
// (payment.ProcessorsForOrg) holds one configured with the charging org's
|
|
// credentials.
|
|
//
|
|
// thirdparty/square is the Square API client underneath. It moves money and
|
|
// manages customers and cards; it does not verify webhooks and does not register
|
|
// itself anywhere. The lifecycle — configuration, availability, verification —
|
|
// is here.
|
|
package square
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/commerce/models/types/currency"
|
|
"github.com/hanzoai/commerce/payment/processor"
|
|
thirdparty "github.com/hanzoai/commerce/thirdparty/square"
|
|
)
|
|
|
|
// Config holds Square credentials and environment settings.
|
|
type Config struct {
|
|
// ApplicationID is the Square Developer Dashboard application ID.
|
|
// Required for the Web Payments SDK (browser-side tokenization).
|
|
ApplicationID string
|
|
// AccessToken is the server-side bearer that authenticates calls to the
|
|
// Square API.
|
|
AccessToken string
|
|
// LocationID pins charges to a specific Square merchant location.
|
|
LocationID string
|
|
// WebhookSignatureKey is the signature key of the Square webhook
|
|
// subscription; ValidateWebhook keys its HMAC with it.
|
|
WebhookSignatureKey string
|
|
// WebhookURL is the EXACT notification URL of that subscription. Square
|
|
// signs HMAC over notificationURL+body, so this is part of the proof.
|
|
WebhookURL string
|
|
// Environment must be "sandbox" or "production".
|
|
Environment string
|
|
// Org is the org this Square account belongs to: a delivery it verifies is
|
|
// applied to that org's books. Empty is the deployment's own account.
|
|
Org string
|
|
}
|
|
|
|
// Provider implements processor.PaymentProcessor and
|
|
// processor.CustomerProcessor for Square, delegating every API call to one
|
|
// *thirdparty.SquareProcessor.
|
|
type Provider struct {
|
|
*processor.BaseProcessor
|
|
config Config
|
|
inner *thirdparty.SquareProcessor
|
|
}
|
|
|
|
func init() {
|
|
// The global registry's Square slot, seeded from the deployment. It is the
|
|
// sessionless verifier HandleProviderWebhook reaches with no tenant resolver
|
|
// in front, so it is configured here or not at all.
|
|
p := &Provider{
|
|
BaseProcessor: processor.NewBaseProcessor(processor.Square, thirdparty.SquareSupportedCurrencies()),
|
|
}
|
|
p.Configure(configFromEnv())
|
|
processor.Register(p)
|
|
}
|
|
|
|
// configFromEnv is the deployment's own Square account: SQUARE_ENVIRONMENT picks
|
|
// the sandbox or production credential set, and only that set
|
|
// (thirdparty.FromEnv), plus the webhook subscription's key and URL.
|
|
func configFromEnv() Config {
|
|
env := strings.ToLower(strings.TrimSpace(os.Getenv("SQUARE_ENVIRONMENT")))
|
|
sandbox := thirdparty.SandboxEnv()
|
|
creds := thirdparty.FromEnv(sandbox)
|
|
if sandbox {
|
|
env = "sandbox"
|
|
} else if env == "" {
|
|
env = "production"
|
|
}
|
|
return Config{
|
|
ApplicationID: creds.ApplicationID,
|
|
AccessToken: creds.AccessToken,
|
|
LocationID: creds.LocationID,
|
|
WebhookSignatureKey: strings.TrimSpace(os.Getenv("SQUARE_WEBHOOK_SIGNATURE_KEY")),
|
|
WebhookURL: strings.TrimSpace(os.Getenv("SQUARE_WEBHOOK_URL")),
|
|
Environment: env,
|
|
}
|
|
}
|
|
|
|
// NewProvider builds and configures a Provider in one shot.
|
|
func NewProvider(cfg Config) *Provider {
|
|
p := &Provider{
|
|
BaseProcessor: processor.NewBaseProcessor(processor.Square, thirdparty.SquareSupportedCurrencies()),
|
|
}
|
|
p.Configure(cfg)
|
|
return p
|
|
}
|
|
|
|
// Configure replaces the provider's configuration. The webhook half is kept
|
|
// whether or not charge credentials are present; the API client exists only
|
|
// with an access token and a location.
|
|
func (p *Provider) Configure(cfg Config) {
|
|
p.config = cfg
|
|
if cfg.AccessToken == "" || cfg.LocationID == "" {
|
|
p.inner = nil
|
|
p.SetConfigured(false)
|
|
return
|
|
}
|
|
p.inner = thirdparty.NewProcessor(thirdparty.Config{
|
|
AccessToken: cfg.AccessToken,
|
|
LocationID: cfg.LocationID,
|
|
Environment: cfg.Environment,
|
|
})
|
|
p.SetConfigured(true)
|
|
}
|
|
|
|
// Type returns the processor type.
|
|
func (p *Provider) Type() processor.ProcessorType {
|
|
return processor.Square
|
|
}
|
|
|
|
// IsAvailable reports whether the provider can charge: an API client exists.
|
|
func (p *Provider) IsAvailable(ctx context.Context) bool {
|
|
return p.inner != nil && p.config.AccessToken != "" && p.config.LocationID != ""
|
|
}
|
|
|
|
// SupportedCurrencies returns the currencies Square supports.
|
|
func (p *Provider) SupportedCurrencies() []currency.Type {
|
|
return thirdparty.SquareSupportedCurrencies()
|
|
}
|
|
|
|
// ApplicationID returns the browser-facing application ID.
|
|
func (p *Provider) ApplicationID() string { return p.config.ApplicationID }
|
|
|
|
// LocationID returns the configured merchant location.
|
|
func (p *Provider) LocationID() string { return p.config.LocationID }
|
|
|
|
// Environment returns "sandbox" or "production".
|
|
func (p *Provider) Environment() string { return p.config.Environment }
|
|
|
|
var (
|
|
_ processor.PaymentProcessor = (*Provider)(nil)
|
|
_ processor.CustomerProcessor = (*Provider)(nil)
|
|
)
|