mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
Commerce never initiates a refund. POST /v1/billing/refunds, POST /v1/billing/refund, POST /v1/order/:orderid/refund and POST /v1/payment/:paymentid/refund are gone with their handlers (engine.CreateRefund, checkout Refund, square.Refund) and the helpers only they used; GET /v1/billing/refunds stays read-only. A claim is settled by a replacement order, never a refund. A refund or dispute of a plan's payment reaches its invoice only from the processor's webhook, under the processor's own refund id, through one locked writer that reads the invoice afresh (engine.RecordRefund, engine.RecordDispute). Every state a dispute reports reaches the invoice; INQUIRY_CLOSED holds the payment no longer, WON stays final and LOST stays disputed. No backfill of the payment-to-invoice index is run: the only plan payments made before v1.50.139 are the team's own card purchases (z24, zk) and one Square record. Co-authored-by: Hanzo Dev <[email protected]>
86 lines
2.9 KiB
Go
86 lines
2.9 KiB
Go
package billing
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/zap-proto/zip"
|
|
|
|
"github.com/hanzoai/commerce/mintauth"
|
|
"github.com/hanzoai/commerce/models/organization"
|
|
)
|
|
|
|
// invokeMoneyHandler drives a single billing handler directly with a real
|
|
// org + datastore context and an optional header set — the harness for the H1
|
|
// money-correctness tests (Deposit behavior, independent of the C1 gate, which is
|
|
// proven separately). The org and ctx are shared with the test's setup
|
|
// so the handler's org.Namespaced(c.Context()) resolves the SAME namespace + defaultDB the
|
|
// setup wrote to.
|
|
func invokeMoneyHandler(org *organization.Organization, ctx context.Context, h zip.Handler, body string, headers map[string]string) *http.Response {
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
// These money handlers (Deposit) run in production ONLY behind
|
|
// middleware.PlatformOnly, which calls AuthorizeMint. Invoking them directly
|
|
// here skips that middleware, so authorize the datastore context to faithfully
|
|
// reproduce the post-gate state; the GATE itself (org-admin → 403 / ledger-sink
|
|
// refusal) is proven separately in mint_surface_test.go and the C1 tests.
|
|
app.Raw(http.MethodPost, "/v1/billing/x", func(c *zip.Ctx) error {
|
|
c.Locals("organization", org)
|
|
c.SetContext(mintauth.WithAuthorized(ctx))
|
|
return c.Next()
|
|
}, h)
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/billing/x", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
for k, v := range headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return resp
|
|
}
|
|
|
|
// driveSeeded mounts handler on a throwaway zip app behind seed (which sets the
|
|
// locals a live middleware chain would set), drives req through REAL routing so
|
|
// path params, query, and body parse exactly as production, and returns the
|
|
// recorder. routePattern carries the :param placeholders; req's URL is the
|
|
// concrete path. The shared harness for the direct-handler-drive tests that a
|
|
// gin CreateTestContext + handler(c) used to express.
|
|
func driveSeeded(seed func(*zip.Ctx), routePattern string, req *http.Request, handler zip.Handler) *http.Response {
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
app.Raw(zip.MethodAll, routePattern, func(c *zip.Ctx) error {
|
|
if seed != nil {
|
|
seed(c)
|
|
}
|
|
return c.Next()
|
|
}, handler)
|
|
resp, err := app.Test(req)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func txID(t *testing.T, r *http.Response) string {
|
|
t.Helper()
|
|
raw, _ := io.ReadAll(r.Body)
|
|
var out map[string]any
|
|
if err := json.Unmarshal(raw, &out); err != nil {
|
|
t.Fatalf("bad response json: %v (%s)", err, string(raw))
|
|
}
|
|
id, _ := out["transactionId"].(string)
|
|
return id
|
|
}
|
|
|
|
func moneyOrg(name string) *organization.Organization {
|
|
o := &organization.Organization{}
|
|
o.Name = name
|
|
o.Live = true
|
|
return o
|
|
}
|