mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
Multi-tenant commerce and billing: orders, subscriptions, invoices, plans, usage metering and a credit ledger. One Go binary. MIT OR Apache-2.0. Claude-Session: https://claude.ai/code/session_01SAtaDFm8VDiWrhYHNNtxMw
142 lines
5.6 KiB
Go
142 lines
5.6 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/billing/engine"
|
|
"github.com/hanzoai/commerce/datastore"
|
|
"github.com/hanzoai/commerce/util/test/ae"
|
|
)
|
|
|
|
// withSystemCtx wires a request so systemDB(c) resolves to the ae-backed SQLite
|
|
// datastore in the "system" namespace.
|
|
func withSystemCtx(c *zip.Ctx) {
|
|
ctx := context.Background()
|
|
c.SetContext(ctx)
|
|
}
|
|
|
|
// TestOSSPayout_HTTP_EndToEnd drives the real HTTP handlers through a gin
|
|
// router against a live SQLite datastore:
|
|
//
|
|
// POST /sbom (arcd ingest) ->
|
|
// engine.AccrueOSSPayout (the usage hook, called synchronously here) ->
|
|
// GET /oss-payout/summary shows per-package owed + resolved maintainer.
|
|
//
|
|
// This is the sandbox proof of the HTTP surface (routing + JSON + storage +
|
|
// rollup), complementing the engine-level integration test.
|
|
func TestOSSPayout_HTTP_EndToEnd(t *testing.T) {
|
|
tc := ae.NewContext()
|
|
defer tc.Close()
|
|
|
|
// ── 1. Ingest an SBOM via the HTTP handler ───────────────────────────
|
|
ingestBody, _ := json.Marshal(sbomIngestRequest{
|
|
ImageRef: "ghcr.io/hanzoai/commerce:1.785.15",
|
|
ImageDigest: "sha256:httpproof",
|
|
Service: "commerce",
|
|
Tool: "syft",
|
|
Components: []sbomComponent{
|
|
{PURL: "pkg:golang/github.com/gin-gonic/[email protected]", Name: "gin", Ecosystem: "golang", Version: "v1.12.0", Scope: "direct"},
|
|
{PURL: "pkg:golang/github.com/google/[email protected]", Name: "uuid", Ecosystem: "golang", Version: "v1.6.0", Scope: "direct"},
|
|
{PURL: "pkg:npm/[email protected]", Name: "react", Ecosystem: "npm", Version: "18.3.1", Scope: "transitive"},
|
|
},
|
|
})
|
|
|
|
ingestReq := httptest.NewRequest(http.MethodPost, "/v1/billing/sbom", bytes.NewReader(ingestBody))
|
|
ingestReq.Header.Set("Content-Type", "application/json")
|
|
w := driveSeeded(withSystemCtx, "/v1/billing/sbom", ingestReq, IngestSBOM)
|
|
|
|
if w.StatusCode != 201 {
|
|
t.Fatalf("ingest status = %d, body=%s", w.StatusCode, func() string { b, _ := io.ReadAll(w.Body); return string(b) }())
|
|
}
|
|
var ingestResp map[string]any
|
|
_ = json.Unmarshal(func() []byte { b, _ := io.ReadAll(w.Body); return b }(), &ingestResp)
|
|
if ingestResp["componentCount"].(float64) != 3 {
|
|
t.Fatalf("ingest componentCount = %v, want 3", ingestResp["componentCount"])
|
|
}
|
|
|
|
// ── 2. Accrue against a $100 org charge (the usage hook, sync) ────────
|
|
orgDB := datastore.New(context.Background())
|
|
orgDB.SetNamespace("acme")
|
|
engine.AccrueOSSPayout(orgDB, "acme", "acme/alice", 10_000, "usd", "txn_http_proof", false)
|
|
|
|
// ── 3. Read the payout summary via the HTTP handler ──────────────────
|
|
w2 := driveSeeded(withSystemCtx, "/v1/billing/oss-payout/summary",
|
|
httptest.NewRequest(http.MethodGet, "/v1/billing/oss-payout/summary", nil), GetOSSPayoutSummary)
|
|
|
|
if w2.StatusCode != 200 {
|
|
t.Fatalf("summary status = %d, body=%s", w2.StatusCode, func() string { b, _ := io.ReadAll(w2.Body); return string(b) }())
|
|
}
|
|
|
|
var summary struct {
|
|
TotalAccruedCents int64 `json:"totalAccruedCents"`
|
|
HeldCents int64 `json:"heldCents"`
|
|
PackageCount int `json:"packageCount"`
|
|
Maintainers []struct {
|
|
PURL string `json:"purl"`
|
|
AccruedCents int64 `json:"accruedCents"`
|
|
FundingTarget string `json:"fundingTarget"`
|
|
FundingKind string `json:"fundingKind"`
|
|
Status string `json:"status"`
|
|
} `json:"maintainers"`
|
|
}
|
|
if err := json.Unmarshal(func() []byte { b, _ := io.ReadAll(w2.Body); return b }(), &summary); err != nil {
|
|
t.Fatalf("decode summary: %v; body=%s", err, func() string { b, _ := io.ReadAll(w2.Body); return string(b) }())
|
|
}
|
|
|
|
// 25% of $100 = $25.00 = 2500 cents. Weights: 2 direct (1.0) + 1 transitive
|
|
// (0.25) = 2.25 total. direct = 2500*(1/2.25)=1111 (largest-remainder rounds
|
|
// to keep the pool exact), transitive react = 2500*(0.25/2.25)=277.
|
|
if summary.TotalAccruedCents != 2500 {
|
|
t.Fatalf("total accrued = %d, want 2500 (25%% cap)", summary.TotalAccruedCents)
|
|
}
|
|
if summary.PackageCount != 3 {
|
|
t.Fatalf("package count = %d, want 3", summary.PackageCount)
|
|
}
|
|
|
|
byPURL := map[string]int64{}
|
|
target := map[string]string{}
|
|
status := map[string]string{}
|
|
for _, m := range summary.Maintainers {
|
|
byPURL[m.PURL] = m.AccruedCents
|
|
target[m.PURL] = m.FundingTarget
|
|
status[m.PURL] = m.Status
|
|
}
|
|
|
|
// gin resolves to github_sponsors:gin-gonic, status pending.
|
|
if target["pkg:golang/github.com/gin-gonic/[email protected]"] != "github_sponsors:gin-gonic" {
|
|
t.Errorf("gin funding target = %q", target["pkg:golang/github.com/gin-gonic/[email protected]"])
|
|
}
|
|
if status["pkg:golang/github.com/gin-gonic/[email protected]"] != "pending" {
|
|
t.Errorf("gin status = %q, want pending", status["pkg:golang/github.com/gin-gonic/[email protected]"])
|
|
}
|
|
// react (npm, no VCS coordinate) is held.
|
|
if status["pkg:npm/[email protected]"] != "held" {
|
|
t.Errorf("react status = %q, want held", status["pkg:npm/[email protected]"])
|
|
}
|
|
if summary.HeldCents != byPURL["pkg:npm/[email protected]"] {
|
|
t.Errorf("heldCents = %d, want react's %d", summary.HeldCents, byPURL["pkg:npm/[email protected]"])
|
|
}
|
|
|
|
// Conservation across the HTTP rollup: directs + held == total.
|
|
var sum int64
|
|
for _, v := range byPURL {
|
|
sum += v
|
|
}
|
|
if sum != 2500 {
|
|
t.Fatalf("summary rollup sum = %d, want 2500", sum)
|
|
}
|
|
|
|
t.Logf("PROOF: $100 spend -> %d cents (25%%) across %d packages; gin=%dc->%s react=%dc->held",
|
|
summary.TotalAccruedCents, summary.PackageCount,
|
|
byPURL["pkg:golang/github.com/gin-gonic/[email protected]"], target["pkg:golang/github.com/gin-gonic/[email protected]"],
|
|
byPURL["pkg:npm/[email protected]"])
|
|
}
|