Files
commerce/models/paymentorg/paymentorg.go
zooqueenandHanzo Dev ca7fd95c5c A dispute moves money only while Square withholds the funds, every dispute report is ordered by the dispute's version, and Square's dispute.state.updated is read as dispute.state.changed was
dispute.state.updated and dispute.state.changed both normalize to dispute.updated
and both are clawback events. A dispute report carries the dispute's version and
updated_at, and dispute.Report (embedded in the invoice and in the wallet payment's
record, models/paymentorg) keeps the latest: a report older by version, else by
updated_at, else by phase (inquiry, chargeback, decided), changes nothing, and WON
stays final.

An inquiry holds no funds: it takes nothing from a wallet, and on an invoice it is
recorded while the period stays paid. A chargeback (EVIDENCE_REQUIRED, PROCESSING),
or one lost or accepted, takes a wallet payment back once and unpays a period; a
chargeback won gives the wallet payment back and restores the period.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-01 17:42:55 -07:00

93 lines
2.9 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, or of the payment for a plan's period, is traced to its org
// here, never read off the request.
package paymentorg
import (
"context"
"strings"
"time"
"github.com/hanzoai/commerce/datastore"
"github.com/hanzoai/commerce/models/dispute"
"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. A payment that paid an invoice names it
// in Invoice and credited no wallet. Report is a dispute of a payment that credited
// a wallet, as the processor last reported it.
type PaymentOrg struct {
mixin.Model[PaymentOrg]
Org string `json:"org"`
Subject string `json:"subject"`
Invoice string `json:"invoice,omitempty"`
Test bool `json:"test"`
dispute.Report
}
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, and the invoice it
// paid ("" for a payment that credited subject's wallet). Idempotent: the payment id
// is the key.
func Put(payment, org, subject, invoice 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.Invoice, p.Test = org, subject, invoice, 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
}
}
// RecordDispute records a reported state of a dispute of payment on its record and
// reports whether it is the latest known (dispute.Report.SetDispute). A payment
// nothing recorded answers false.
func RecordDispute(payment, state string, version int64, at time.Time) (bool, error) {
p, found, err := Get(payment)
if err != nil || !found {
return false, err
}
if !p.SetDispute(state, version, at) {
return false, nil
}
return true, p.Put()
}