Files
commerce/api/billing/subscription_items.go
zeekay 1943f1c093 Hanzo Commerce
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
2026-09-23 21:52:16 -07:00

186 lines
5.2 KiB
Go

package billing
import (
"github.com/zap-proto/zip"
"github.com/hanzoai/commerce/datastore"
"github.com/hanzoai/commerce/log"
"github.com/hanzoai/commerce/middleware"
"github.com/hanzoai/commerce/models/subscriptionitem"
"github.com/hanzoai/commerce/util/json/http"
)
type createSubscriptionItemRequest struct {
SubscriptionId string `json:"subscriptionId"`
PriceId string `json:"priceId,omitempty"`
PlanId string `json:"planId,omitempty"`
MeterId string `json:"meterId,omitempty"`
Quantity int64 `json:"quantity,omitempty"`
BillingMode string `json:"billingMode,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// CreateSubscriptionItem adds an item to a subscription.
//
// POST /v1/billing/subscription-items
func CreateSubscriptionItem(c *zip.Ctx) error {
org := middleware.GetOrganization(c)
db := datastore.New(org.Namespaced(c.Context()))
var req createSubscriptionItemRequest
if err := c.Bind(&req); err != nil {
return http.Fail(c, 400, "invalid request body", err)
}
if req.SubscriptionId == "" {
return http.Fail(c, 400, "subscriptionId is required", nil)
}
si := subscriptionitem.New(db)
si.SubscriptionId = req.SubscriptionId
si.PriceId = req.PriceId
si.PlanId = req.PlanId
si.MeterId = req.MeterId
si.Quantity = req.Quantity
if req.BillingMode != "" {
si.BillingMode = req.BillingMode
}
if req.Metadata != nil {
si.Metadata = req.Metadata
}
if err := si.Create(); err != nil {
log.Error("Failed to create subscription item: %v", err, c)
return http.Fail(c, 500, "failed to create subscription item", err)
}
return c.JSON(201, subscriptionItemResponse(si))
}
// GetSubscriptionItem retrieves a subscription item by ID.
//
// GET /v1/billing/subscription-items/:id
func GetSubscriptionItem(c *zip.Ctx) error {
org := middleware.GetOrganization(c)
db := datastore.New(org.Namespaced(c.Context()))
si := subscriptionitem.New(db)
if err := si.GetById(c.Param("id")); err != nil {
return http.Fail(c, 404, "subscription item not found", err)
}
return c.JSON(200, subscriptionItemResponse(si))
}
// ListSubscriptionItems lists items for a subscription.
//
// GET /v1/billing/subscription-items?subscriptionId=...
func ListSubscriptionItems(c *zip.Ctx) error {
org := middleware.GetOrganization(c)
db := datastore.New(org.Namespaced(c.Context()))
rootKey := db.NewKey("synckey", "", 1, nil)
items := make([]*subscriptionitem.SubscriptionItem, 0)
q := subscriptionitem.Query(db).Ancestor(rootKey)
if subId := c.Query("subscriptionId"); subId != "" {
q = q.Filter("SubscriptionId=", subId)
}
iter := q.Order("-Created").Run()
for {
si := subscriptionitem.New(db)
if _, err := iter.Next(si); err != nil {
break
}
items = append(items, si)
}
results := make([]map[string]interface{}, len(items))
for i, si := range items {
results[i] = subscriptionItemResponse(si)
}
return c.JSON(200, results)
}
type updateSubscriptionItemRequest struct {
Quantity int64 `json:"quantity,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// UpdateSubscriptionItem updates a subscription item (e.g. seat count).
//
// PATCH /v1/billing/subscription-items/:id
func UpdateSubscriptionItem(c *zip.Ctx) error {
org := middleware.GetOrganization(c)
db := datastore.New(org.Namespaced(c.Context()))
si := subscriptionitem.New(db)
if err := si.GetById(c.Param("id")); err != nil {
return http.Fail(c, 404, "subscription item not found", err)
}
var req updateSubscriptionItemRequest
if err := c.Bind(&req); err != nil {
return http.Fail(c, 400, "invalid request body", err)
}
if req.Quantity > 0 {
si.Quantity = req.Quantity
}
if req.Metadata != nil {
si.Metadata = req.Metadata
}
if err := si.Update(); err != nil {
log.Error("Failed to update subscription item: %v", err, c)
return http.Fail(c, 500, "failed to update subscription item", err)
}
return c.JSON(200, subscriptionItemResponse(si))
}
// DeleteSubscriptionItem removes an item from a subscription.
//
// DELETE /v1/billing/subscription-items/:id
func DeleteSubscriptionItem(c *zip.Ctx) error {
org := middleware.GetOrganization(c)
db := datastore.New(org.Namespaced(c.Context()))
si := subscriptionitem.New(db)
if err := si.GetById(c.Param("id")); err != nil {
return http.Fail(c, 404, "subscription item not found", err)
}
if err := si.Delete(); err != nil {
log.Error("Failed to delete subscription item: %v", err, c)
return http.Fail(c, 500, "failed to delete subscription item", err)
}
return c.JSON(200, map[string]any{"deleted": true, "id": si.Id()})
}
func subscriptionItemResponse(si *subscriptionitem.SubscriptionItem) map[string]interface{} {
resp := map[string]interface{}{
"id": si.Id(),
"subscriptionId": si.SubscriptionId,
"billingMode": si.BillingMode,
"quantity": si.Quantity,
"created": si.Created,
}
if si.PriceId != "" {
resp["priceId"] = si.PriceId
}
if si.PlanId != "" {
resp["planId"] = si.PlanId
}
if si.MeterId != "" {
resp["meterId"] = si.MeterId
}
if si.Metadata != nil {
resp["metadata"] = si.Metadata
}
return resp
}