Files
zooqueenandHanzo Dev e1271578a2 A subscription says how its period was paid, a refund or dispute finds its payment's org from the payment, and a dispute won gives its money back
Subscriptions carry Settled: the current period's paid-invoice method (card only when
a card paid all of it; mixed beside prepaid money) or external:<processor> for a
recorded plan. Every settled payment is indexed by payment id to the org and wallet
its receipt lives in (models/paymentorg), and the refund/dispute webhook resolves from
that index, never the request's org. CreditLedger gains Restore for a dispute won.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-30 09:13:36 -07:00

70 lines
2.0 KiB
Go

// Package paymentorg records, for every settled payment, the org whose books its
// receipt lives in — by the processor's payment id, in the system namespace.
//
// A processor's callback names a payment and no org: Square sends no X-Org-Id, and a
// sessionless request resolves to the default org. So a refund or a dispute of a
// customer's top-up is traced to its org here, never read off the request.
package paymentorg
import (
"context"
"strings"
"github.com/hanzoai/commerce/datastore"
"github.com/hanzoai/commerce/models/mixin"
"github.com/hanzoai/commerce/util/nscontext"
"github.com/hanzoai/orm"
)
func init() {
orm.Register[PaymentOrg]("payment-org", orm.WithStringKey[PaymentOrg]())
}
// PaymentOrg is where one payment's receipt lives: the org, the wallet it credited,
// and whether the charge was a sandbox one.
type PaymentOrg struct {
mixin.Model[PaymentOrg]
Org string `json:"org"`
Subject string `json:"subject"`
Test bool `json:"test"`
}
func (p *PaymentOrg) Load(ps []datastore.Property) error { return datastore.LoadStruct(p, ps) }
func (p *PaymentOrg) Save() ([]datastore.Property, error) { return datastore.SaveStruct(p) }
func db() *datastore.Datastore {
return datastore.New(nscontext.WithNamespace(context.Background(), "system"))
}
// Put records that payment's receipt lives in org, for subject. Idempotent: the
// payment id is the key.
func Put(payment, org, subject string, test bool) error {
payment = strings.TrimSpace(payment)
if payment == "" || org == "" {
return nil
}
d := db()
p := new(PaymentOrg)
p.Init(d)
p.SetId(payment)
p.Org, p.Subject, p.Test = org, subject, test
return p.Put()
}
// Get answers where payment's receipt lives, and false when nothing recorded it.
func Get(payment string) (*PaymentOrg, bool, error) {
d := db()
p := new(PaymentOrg)
p.Init(d)
err := p.Get(d.NewKey(p.Kind(), strings.TrimSpace(payment), 0, nil))
switch {
case err == nil:
return p, true, nil
case err == datastore.ErrNoSuchEntity:
return nil, false, nil
default:
return nil, false, err
}
}