payments: call CoinPay REST API directly (drop coinpay CLI dependency)

CreatePremiumCharge/VerifyPremium now POST /payments/create and GET
/payments/:id against the CoinPay API (Bearer COINPAY_API_KEY, business_id =
AGENTBBS_COINPAY_MERCHANT_ID), so the droplet needs no coinpay CLI installed.
The created payment id is stored (store: User.PremiumPayID + premium_pay_id col
+ SetPremiumPayment); ensurePremium verifies that id on a later connect and
grants premium on confirmed/forwarded status. Removed the CLI command-template
env knobs; added AGENTBBS_COINPAY_API_URL. Tests: httptest-backed payments_test
+ store SetPremiumPayment test. Build/vet/gofmt/test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-06-14 08:27:33 +00:00
parent 5c0feda8d4
commit 7fd10c3436
6 changed files with 289 additions and 167 deletions

View file

@ -18,20 +18,21 @@ type User struct {
PubKeyFP string
Email string
EmailVerified bool
Premium bool // paid the one-time lifetime membership
Premium bool // paid the one-time lifetime membership
PremiumPayID string // CoinPay payment id of the pending/settled premium charge
CreatedAt time.Time
}
// userCols is the column list (in struct order) for every user SELECT, kept in
// sync with scanUser.
const userCols = `id, name, kind, pubkey_fp, email, email_verified, premium, created_at`
const userCols = `id, name, kind, pubkey_fp, email, email_verified, premium, premium_pay_id, created_at`
// scanUser reads one user row selected with userCols.
func scanUser(sc interface{ Scan(...any) error }) (User, error) {
var u User
var verified, premium int
var created string
if err := sc.Scan(&u.ID, &u.Name, &u.Kind, &u.PubKeyFP, &u.Email, &verified, &premium, &created); err != nil {
if err := sc.Scan(&u.ID, &u.Name, &u.Kind, &u.PubKeyFP, &u.Email, &verified, &premium, &u.PremiumPayID, &created); err != nil {
return User{}, err
}
u.EmailVerified = verified != 0
@ -74,6 +75,9 @@ type Store interface {
// and clears the code. Returns ok=false on a wrong/empty code.
ConfirmEmailCode(userID int64, code string) (User, bool, error)
// SetPremiumPayment records the CoinPay payment id of a pending premium
// charge so a later visit can verify whether it settled.
SetPremiumPayment(userID int64, payID string) error
// GrantPremium marks the account as a lifetime premium member (the $10
// one-time membership), recording the CoinPay payment reference. Idempotent.
GrantPremium(userID int64, paymentRef string) error
@ -154,6 +158,7 @@ func migrate(db *sql.DB) error {
{"verify_token", "verify_token TEXT NOT NULL DEFAULT ''"},
{"premium", "premium INTEGER NOT NULL DEFAULT 0"},
{"premium_ref", "premium_ref TEXT NOT NULL DEFAULT ''"},
{"premium_pay_id", "premium_pay_id TEXT NOT NULL DEFAULT ''"},
})
}
@ -312,6 +317,11 @@ func (s *sqliteStore) ConfirmEmailCode(userID int64, code string) (User, bool, e
return u, true, nil
}
func (s *sqliteStore) SetPremiumPayment(userID int64, payID string) error {
_, err := s.db.Exec(`UPDATE users SET premium_pay_id = ? WHERE id = ?`, payID, userID)
return err
}
func (s *sqliteStore) GrantPremium(userID int64, paymentRef string) error {
_, err := s.db.Exec(`UPDATE users SET premium = 1, premium_ref = ? WHERE id = ?`, paymentRef, userID)
return err