Files
zooqueenandHanzo Dev 2944b955a4 billing: only an org's admins change its auto-reload and cards, and auto-reload is bounded
Auto-reload charges the org's card with nobody present, so the rule is its
admins' to set — and a SuperAdmin's or a service's. The amount is a top-up's
own range ($5 to $5,000) and the balance it reloads below is $0 to $5,000,
refused with the range in the sentence. The sweep asks the processor to
charge one org at most three times a UTC day, a decline included, and names
the limit in the row it skips.

The day's reload is reserved under compare-and-swap before the card is
charged, against the rule as it stands, so a rule switched off mid-sweep is
not charged or re-armed, and overlapping sweeps share the limit. A charge
refused before the processor was asked, or a replay of one already counted,
gives its reload back. An admin's save swaps the rule's own fields and keeps
the day's count.

Saving, changing, choosing the default of and removing the org's cards, and
charging its card on file, are its admins' too. A member whose wallet is
their own person account still manages that wallet's cards.

A loaded rule is bound to its key, so an edit is the rule on file. Before,
an update wrote a second, parentless row the next read never found. The first
read now removes such a row, and honours it when it switched the rule off;
it never arms a rule or raises what it charges.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-16 02:55:31 -07:00

54 lines
1.9 KiB
Go

package autorecharge
import (
"github.com/hanzoai/commerce/datastore"
"github.com/hanzoai/commerce/models/mixin"
"github.com/hanzoai/commerce/util/val"
"github.com/hanzoai/orm"
)
func init() { orm.Register[AutoRecharge]("auto-recharge") }
// AutoRecharge is a per-org automatic top-up rule. When the org's available
// balance (balance minus holds) drops below ThresholdCents, the recharge cron
// charges the org's default payment method AmountCents and credits the balance.
// Mirrors the Claude/OpenAI "auto-reload" setting. One record per org (keyed by
// UserId = the org slug, which is the billing key used by topup/balance).
type AutoRecharge struct {
mixin.Model[AutoRecharge]
UserId string `json:"userId"` // org slug (billing key)
Enabled bool `json:"enabled"`
ThresholdCents int64 `json:"thresholdCents"`
AmountCents int64 `json:"amountCents"`
Currency string `json:"currency"`
// LastRechargedAt stores the ISO timestamp of the last successful auto
// top-up — used for observability and as a light guard against a runaway
// recharge loop within a single cron window.
LastRechargedAt string `json:"lastRechargedAt,omitempty"`
// Day is the UTC date ("2006-01-02") Reloads counts, and Reloads how many
// reloads charged the card on that day. The sweep stops at its daily limit.
Day string `json:"day,omitempty"`
Reloads int `json:"reloads,omitempty"`
// Txn is the transaction of the last reload counted, so a replayed charge
// (the same reload answered twice) is counted once.
Txn string `json:"txn,omitempty"`
}
func (a *AutoRecharge) Validator() *val.Validator {
return val.New()
}
func New(db *datastore.Datastore) *AutoRecharge {
a := new(AutoRecharge)
a.Init(db)
a.Parent = db.NewKey("synckey", "", 1, nil)
return a
}
func Query(db *datastore.Datastore) datastore.Query {
return db.Query("auto-recharge")
}