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
94 lines
3.3 KiB
Go
94 lines
3.3 KiB
Go
package costs
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/zap-proto/zip"
|
|
|
|
"github.com/hanzoai/commerce/middleware"
|
|
"github.com/hanzoai/commerce/util/permission"
|
|
)
|
|
|
|
// Route registers the vendor-cost (COGS) endpoints. These are a PLATFORM
|
|
// god-view (what WE pay our vendors across the whole business), so on top of the
|
|
// route-level token gate every handler ALSO enforces requireCostsAdmin — see its
|
|
// doc for why the middleware alone is not enough.
|
|
func Route(r *zip.Group, args ...zip.Handler) {
|
|
adminRequired := middleware.TokenRequired(permission.Admin)
|
|
|
|
api := r.Group("costs")
|
|
api.Use(adminRequired)
|
|
|
|
api.Raw(http.MethodGet, "", GetCosts)
|
|
api.Raw(http.MethodGet, "/margin", GetMargin)
|
|
}
|
|
|
|
// requireCostsAdmin is the in-handler gate for the vendor-cost god-view. It fails
|
|
// closed (403) unless the caller is a PLATFORM admin, and is enforced INSIDE each
|
|
// handler because the route-level middleware.TokenRequired(permission.Admin) is a
|
|
// NO-OP on the IAM path: it short-circuits (c.Next) for ANY IAM-authenticated
|
|
// request WITHOUT checking the Admin bit, and IAMTokenRequired stamps
|
|
// iam_authenticated=true whenever an X-Org-Id header is present — which the gateway
|
|
// sets on EVERY authenticated call. A handler must never trust that gate on its own.
|
|
//
|
|
// These figures are cross-tenant PLATFORM spend/margin, so this gates on the
|
|
// STRICTER GlobalAdmin predicate (or the trusted internal service token), NEVER the
|
|
// org-level Admin bit an org owner carries. That gate is shared with the SaaS
|
|
// metrics god-view (api/metrics) as the single middleware.RequirePlatformAdmin
|
|
// predicate — one and only one definition of "may read cross-org platform data",
|
|
// so costs and metrics can never drift apart. See middleware.MayReadPlatform for
|
|
// the exact principals admitted and why org-level admin is refused.
|
|
func requireCostsAdmin(c *zip.Ctx) bool {
|
|
return middleware.RequirePlatformAdmin(c)
|
|
}
|
|
|
|
// GetCosts returns the per-vendor COGS breakdown for a period.
|
|
//
|
|
// GET /v1/costs?period=YYYY-MM
|
|
//
|
|
// Response: {period, vendors:[{vendor,service,amountCents,period,source,note}], totalCents, currency}.
|
|
func GetCosts(c *zip.Ctx) error {
|
|
if !requireCostsAdmin(c) {
|
|
return nil
|
|
}
|
|
org := middleware.GetOrganization(c)
|
|
ctx := org.Namespaced(c.Context())
|
|
p := period(c.Query("period"))
|
|
|
|
report, _ := buildReport(ctx, org.TestMode(), p)
|
|
return c.JSON(200, report)
|
|
}
|
|
|
|
// GetMargin returns revenue (the usage ledger) minus COGS (this package) plus the
|
|
// gross-margin percentage — the MARGIN view for admin.hanzo.ai.
|
|
//
|
|
// GET /v1/costs/margin?period=YYYY-MM
|
|
//
|
|
// Revenue is the sum of api-usage charges in the period (what customers paid);
|
|
// COGS is the vendor total. Kept DRY with GetCosts: buildReport returns both the
|
|
// vendor lines and the ledger aggregate, so revenue and cost come from one walk.
|
|
func GetMargin(c *zip.Ctx) error {
|
|
if !requireCostsAdmin(c) {
|
|
return nil
|
|
}
|
|
org := middleware.GetOrganization(c)
|
|
ctx := org.Namespaced(c.Context())
|
|
p := period(c.Query("period"))
|
|
|
|
report, ledger := buildReport(ctx, org.TestMode(), p)
|
|
|
|
revenue := ledger.RevenueCents
|
|
cogs := report.TotalCents
|
|
margin := revenue - cogs
|
|
|
|
return c.JSON(200, MarginReport{
|
|
Period: p,
|
|
RevenueCents: revenue,
|
|
COGSCents: cogs,
|
|
MarginCents: margin,
|
|
GrossMarginPct: grossMarginPct(revenue, margin),
|
|
Vendors: report.Vendors,
|
|
Currency: "usd",
|
|
})
|
|
}
|