From 54f5fbca16a2cea515f61f719fdd168941a9cc82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Tue, 9 Sep 2025 11:51:31 -0300 Subject: [PATCH 01/19] Add support for Billing endpoints --- billing.go | 179 ++++++++++++++++++++++ billing/api.go | 35 +++++ billing/client.go | 162 ++++++++++++++++++++ billing/client_test.go | 328 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 704 insertions(+) create mode 100644 billing.go create mode 100644 billing/api.go create mode 100644 billing/client.go create mode 100644 billing/client_test.go diff --git a/billing.go b/billing.go new file mode 100644 index 00000000..8b4b7589 --- /dev/null +++ b/billing.go @@ -0,0 +1,179 @@ +package clerk + +// Feature represents a feature associated with a plan. +type Feature struct { + APIResource + + Object string `json:"object"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Slug string `json:"slug"` + AvatarURL string `json:"avatar_url"` +} + +// CommerceMoney represents money amounts with formatting. +type CommerceMoney struct { + APIResource + + Amount int64 `json:"amount"` + AmountFormatted string `json:"amount_formatted"` + Currency string `json:"currency"` + CurrencySymbol string `json:"currency_symbol"` +} + +// CommerceProduct represents a product. +type CommerceProduct struct { + APIResource + + Object string `json:"object"` + ID string `json:"id"` + Slug string `json:"slug"` + Currency string `json:"currency"` + Name string `json:"name"` + IsDefault bool `json:"is_default"` + Plans []*Plan `json:"plans"` +} + +// Plan represents a billing plan. +type Plan struct { + APIResource + + Object string `json:"object"` + ID string `json:"id"` + Name string `json:"name"` + Fee *CommerceMoney `json:"fee"` + AnnualMonthlyFee *CommerceMoney `json:"annual_monthly_fee"` + AnnualFee *CommerceMoney `json:"annual_fee"` + Amount *int64 `json:"amount,omitempty"` + AmountFormatted *string `json:"amount_formatted,omitempty"` + AnnualMonthlyAmount *int64 `json:"annual_monthly_amount,omitempty"` + AnnualMonthlyAmountFormatted *string `json:"annual_monthly_amount_formatted,omitempty"` + AnnualAmount *int64 `json:"annual_amount,omitempty"` + AnnualAmountFormatted *string `json:"annual_amount_formatted,omitempty"` + CurrencySymbol string `json:"currency_symbol"` + Currency string `json:"currency"` + Description string `json:"description"` + ProductID string `json:"product_id"` + Product *CommerceProduct `json:"product,omitempty"` + IsDefault bool `json:"is_default"` + IsRecurring bool `json:"is_recurring"` + PubliclyVisible bool `json:"publicly_visible"` + HasBaseFee bool `json:"has_base_fee"` + PayerType []string `json:"payer_type"` + ForPayerType string `json:"for_payer_type"` + Slug string `json:"slug"` + AvatarURL string `json:"avatar_url"` + Features []*Feature `json:"features"` + FreeTrialEnabled bool `json:"free_trial_enabled"` + FreeTrialDays *int `json:"free_trial_days"` +} + +// PlanList contains a list of plans. +type PlanList struct { + APIResource + + Data []*Plan `json:"data"` + TotalCount int64 `json:"total_count"` +} + +// CommercePaymentSource represents a payment source. +type CommercePaymentSource struct { + APIResource + + Object string `json:"object"` + ID string `json:"id"` + PayerID string `json:"payer_id"` + PaymentMethod string `json:"payment_method"` + IsDefault *bool `json:"is_default,omitempty"` + Gateway string `json:"gateway"` + GatewayExternalID string `json:"gateway_external_id"` + GatewayExternalAccountID *string `json:"gateway_external_account_id"` + Last4 string `json:"last4"` + Status string `json:"status"` + WalletType string `json:"wallet_type"` + CardType string `json:"card_type"` + ExpiryYear int `json:"expiry_year,omitempty"` + ExpiryMonth int `json:"expiry_month,omitempty"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + IsRemovable *bool `json:"is_removable,omitempty"` +} + +// CommerceSubscriptionItemNextPayment represents next payment info. +type CommerceSubscriptionItemNextPayment struct { + APIResource + + Amount *CommerceMoney `json:"amount"` + Date *int64 `json:"date"` +} + +// Payer represents a billing payer (user or organization). +type Payer struct { + APIResource + + Object string `json:"object"` + ID string `json:"id"` + InstanceID string `json:"instance_id"` + + // User payer only + UserID string `json:"user_id,omitempty"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + + // Org payer only + OrganizationID string `json:"organization_id,omitempty"` + OrganizationName string `json:"organization_name,omitempty"` + + // Used for both org and user payers + ImageURL string `json:"image_url"` + + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +// SubscriptionItem represents a billing subscription item. +type SubscriptionItem struct { + APIResource + + Object string `json:"object"` + ID string `json:"id"` + InstanceID string `json:"instance_id"` + Status string `json:"status"` + PlanID string `json:"plan_id"` + Plan *Plan `json:"plan"` + PlanPeriod string `json:"plan_period"` + PaymentSourceID string `json:"payment_source_id"` + PaymentSource *CommercePaymentSource `json:"payment_source"` + LifetimePaid *CommerceMoney `json:"lifetime_paid"` + Amount *CommerceMoney `json:"amount"` + NextInvoice *CommerceSubscriptionItemNextPayment `json:"next_invoice"` + NextPayment *CommerceSubscriptionItemNextPayment `json:"next_payment"` + PayerID string `json:"payer_id"` + Payer *Payer `json:"payer"` + IsFreeTrial bool `json:"is_free_trial"` + PeriodStart *int64 `json:"period_start"` + PeriodEnd *int64 `json:"period_end"` + ProrationDate string `json:"proration_date"` + CanceledAt *int64 `json:"canceled_at"` + PastDueAt *int64 `json:"past_due_at"` + EndedAt *int64 `json:"ended_at"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +// SubscriptionItemList contains a list of subscription items. +type SubscriptionItemList struct { + APIResource + + Data []*SubscriptionItem `json:"data"` + TotalCount int64 `json:"total_count"` +} + +// ExtendFreeTrialResponse represents the response from extending free trial. +type ExtendFreeTrialResponse struct { + APIResource + + *SubscriptionItem +} diff --git a/billing/api.go b/billing/api.go new file mode 100644 index 00000000..b2dffa3c --- /dev/null +++ b/billing/api.go @@ -0,0 +1,35 @@ +// Code generated by "gen"; DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. +package billing + +import ( + "context" + + "github.com/clerk/clerk-sdk-go/v2" +) + +// ListPlans returns a list of billing plans. +func ListPlans(ctx context.Context, params *ListPlansParams) (*clerk.PlanList, error) { + return getClient().ListPlans(ctx, params) +} + +// ListSubscriptionItems returns a list of subscription items. +func ListSubscriptionItems(ctx context.Context, params *ListSubscriptionItemsParams) (*clerk.SubscriptionItemList, error) { + return getClient().ListSubscriptionItems(ctx, params) +} + +// CancelSubscriptionItem cancels a subscription item. +func CancelSubscriptionItem(ctx context.Context, subscriptionItemID string, params *CancelSubscriptionItemParams) (*clerk.SubscriptionItem, error) { + return getClient().CancelSubscriptionItem(ctx, subscriptionItemID, params) +} + +// ExtendFreeTrial extends the free trial period of a subscription item. +func ExtendFreeTrial(ctx context.Context, subscriptionItemID string, params *ExtendFreeTrialParams) (*clerk.SubscriptionItem, error) { + return getClient().ExtendFreeTrial(ctx, subscriptionItemID, params) +} + +func getClient() *Client { + return &Client{ + Backend: clerk.GetBackend(), + } +} diff --git a/billing/client.go b/billing/client.go new file mode 100644 index 00000000..469b0563 --- /dev/null +++ b/billing/client.go @@ -0,0 +1,162 @@ +// Package billing provides the Billing API. +package billing + +import ( + "context" + "net/http" + "net/url" + + "github.com/clerk/clerk-sdk-go/v2" +) + +//go:generate go run ../cmd/gen/main.go + +const path = "/billing" + +// Client is used to invoke the Billing API. +type Client struct { + Backend clerk.Backend +} + +func NewClient(config *clerk.ClientConfig) *Client { + return &Client{ + Backend: clerk.NewBackend(&config.BackendConfig), + } +} + +type ListPlansParams struct { + clerk.APIParams + clerk.ListParams + PayerType *string `json:"payer_type,omitempty"` +} + +func (params *ListPlansParams) ToQuery() url.Values { + q := params.ListParams.ToQuery() + if params.PayerType != nil { + q.Set("payer_type", *params.PayerType) + } + return q +} + +// ListPlans returns a list of billing plans. +func (c *Client) ListPlans(ctx context.Context, params *ListPlansParams) (*clerk.PlanList, error) { + plansPath, err := clerk.JoinPath(path, "/plans") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodGet, plansPath) + req.SetParams(params) + planList := &clerk.PlanList{} + err = c.Backend.Call(ctx, req, planList) + return planList, err +} + +type ListSubscriptionItemsParams struct { + clerk.APIParams + clerk.ListParams + Status *string `json:"status,omitempty"` + PayerType *string `json:"payer_type,omitempty"` + PlanID *string `json:"plan_id,omitempty"` + IncludeFree *bool `json:"include_free,omitempty"` + Query *string `json:"query,omitempty"` + UserID *string `json:"user_id,omitempty"` + OrganizationID *string `json:"organization_id,omitempty"` +} + +func (params *ListSubscriptionItemsParams) ToQuery() url.Values { + q := params.ListParams.ToQuery() + if params.Status != nil { + q.Set("status", *params.Status) + } + if params.PayerType != nil { + q.Set("payer_type", *params.PayerType) + } + if params.PlanID != nil { + q.Set("plan_id", *params.PlanID) + } + if params.IncludeFree != nil { + if *params.IncludeFree { + q.Set("include_free", "true") + } else { + q.Set("include_free", "false") + } + } + if params.Query != nil { + q.Set("query", *params.Query) + } + if params.UserID != nil { + q.Set("user_id", *params.UserID) + } + if params.OrganizationID != nil { + q.Set("organization_id", *params.OrganizationID) + } + return q +} + +// ListSubscriptionItems returns a list of subscription items. +func (c *Client) ListSubscriptionItems(ctx context.Context, params *ListSubscriptionItemsParams) (*clerk.SubscriptionItemList, error) { + subscriptionItemsPath, err := clerk.JoinPath(path, "/subscription_items") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodGet, subscriptionItemsPath) + req.SetParams(params) + subscriptionItemList := &clerk.SubscriptionItemList{} + err = c.Backend.Call(ctx, req, subscriptionItemList) + return subscriptionItemList, err +} + +type CancelSubscriptionItemParams struct { + clerk.APIParams + EndNow *bool `json:"end_now,omitempty"` +} + +func (params *CancelSubscriptionItemParams) ToQuery() url.Values { + q := url.Values{} + if params.EndNow != nil { + if *params.EndNow { + q.Set("end_now", "true") + } else { + q.Set("end_now", "false") + } + } + return q +} + +// CancelSubscriptionItem cancels a subscription item. +func (c *Client) CancelSubscriptionItem(ctx context.Context, subscriptionItemID string, params *CancelSubscriptionItemParams) (*clerk.SubscriptionItem, error) { + cancelPath, err := clerk.JoinPath(path, "/subscription_items", subscriptionItemID) + if err != nil { + return nil, err + } + + if params != nil { + query := params.ToQuery() + if len(query) > 0 { + cancelPath += "?" + query.Encode() + } + } + + req := clerk.NewAPIRequest(http.MethodDelete, cancelPath) + subscriptionItem := &clerk.SubscriptionItem{} + err = c.Backend.Call(ctx, req, subscriptionItem) + return subscriptionItem, err +} + +type ExtendFreeTrialParams struct { + clerk.APIParams + ExtendTo string `json:"extend_to"` +} + +// ExtendFreeTrial extends the free trial period of a subscription item. +func (c *Client) ExtendFreeTrial(ctx context.Context, subscriptionItemID string, params *ExtendFreeTrialParams) (*clerk.SubscriptionItem, error) { + extendPath, err := clerk.JoinPath(path, "/subscription_items", subscriptionItemID, "/extend_free_trial") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodPost, extendPath) + req.SetParams(params) + subscriptionItem := &clerk.SubscriptionItem{} + err = c.Backend.Call(ctx, req, subscriptionItem) + return subscriptionItem, err +} diff --git a/billing/client_test.go b/billing/client_test.go new file mode 100644 index 00000000..3eed543b --- /dev/null +++ b/billing/client_test.go @@ -0,0 +1,328 @@ +package billing + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "testing" + + "github.com/clerk/clerk-sdk-go/v2" + "github.com/clerk/clerk-sdk-go/v2/clerktest" + "github.com/stretchr/testify/require" +) + +func TestBillingClientListPlans(t *testing.T) { + t.Parallel() + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(`{"data": [{"object":"plan","id":"plan_123","name":"Basic Plan","payer_type":["user"],"features":[{"object":"feature","id":"feature_456","name":"Feature 1","key":"feature_1"}]}],"total_count": 1}`), + Method: http.MethodGet, + Path: "/v1/billing/plans", + Query: &url.Values{ + "limit": []string{"10"}, + "offset": []string{"0"}, + "payer_type": []string{"user"}, + }, + }, + } + client := NewClient(config) + params := &ListPlansParams{ + PayerType: clerk.String("user"), + } + params.Limit = clerk.Int64(10) + params.Offset = clerk.Int64(0) + planList, err := client.ListPlans(context.Background(), params) + require.NoError(t, err) + require.Equal(t, int64(1), planList.TotalCount) + require.Equal(t, 1, len(planList.Data)) + require.Equal(t, "plan_123", planList.Data[0].ID) + require.Equal(t, "Basic Plan", planList.Data[0].Name) + require.Equal(t, []string{"user"}, planList.Data[0].PayerType) + require.Equal(t, 1, len(planList.Data[0].Features)) + require.Equal(t, "feature_456", planList.Data[0].Features[0].ID) + require.Equal(t, "Feature 1", planList.Data[0].Features[0].Name) +} + +func TestBillingClientListPlans_Error(t *testing.T) { + t.Parallel() + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Status: http.StatusBadRequest, + Out: json.RawMessage(`{ + "errors":[{ + "code":"list-plans-error-code" + }], + "clerk_trace_id":"list-plans-trace-id" +}`), + }, + } + client := NewClient(config) + _, err := client.ListPlans(context.Background(), &ListPlansParams{}) + require.Error(t, err) + apiErr, ok := err.(*clerk.APIErrorResponse) + require.True(t, ok) + require.Equal(t, "list-plans-trace-id", apiErr.TraceID) + require.Equal(t, 1, len(apiErr.Errors)) + require.Equal(t, "list-plans-error-code", apiErr.Errors[0].Code) +} + +func TestBillingClientListSubscriptionItems(t *testing.T) { + t.Parallel() + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(`{"data": [{"object":"subscription_item","id":"sub_item_123","payer_id":"payer_456","plan_id":"plan_789","status":"active","period_start":1640995200,"period_end":1643673600,"payer":{"object":"payer","id":"payer_456","first_name":"John","last_name":"Doe","created_at":1640995200,"updated_at":1640995200},"plan":{"object":"plan","id":"plan_789","name":"Pro Plan","payer_type":["user"],"features":[]},"created_at":1640995200,"updated_at":1640995200}],"total_count": 1}`), + Method: http.MethodGet, + Path: "/v1/billing/subscription_items", + Query: &url.Values{ + "limit": []string{"10"}, + "offset": []string{"0"}, + "user_id": []string{"user_456"}, + "status": []string{"active"}, + }, + }, + } + client := NewClient(config) + params := &ListSubscriptionItemsParams{ + UserID: clerk.String("user_456"), + Status: clerk.String("active"), + } + params.Limit = clerk.Int64(10) + params.Offset = clerk.Int64(0) + subscriptionItemList, err := client.ListSubscriptionItems(context.Background(), params) + require.NoError(t, err) + require.Equal(t, int64(1), subscriptionItemList.TotalCount) + require.Equal(t, 1, len(subscriptionItemList.Data)) + require.Equal(t, "sub_item_123", subscriptionItemList.Data[0].ID) + require.Equal(t, "payer_456", subscriptionItemList.Data[0].PayerID) + require.Equal(t, "plan_789", subscriptionItemList.Data[0].PlanID) + require.Equal(t, "active", subscriptionItemList.Data[0].Status) + require.Equal(t, int64(1640995200), *subscriptionItemList.Data[0].PeriodStart) + require.Equal(t, int64(1643673600), *subscriptionItemList.Data[0].PeriodEnd) + require.NotNil(t, subscriptionItemList.Data[0].Payer) + require.Equal(t, "payer_456", subscriptionItemList.Data[0].Payer.ID) + require.Equal(t, "John", subscriptionItemList.Data[0].Payer.FirstName) + require.NotNil(t, subscriptionItemList.Data[0].Plan) + require.Equal(t, "plan_789", subscriptionItemList.Data[0].Plan.ID) + require.Equal(t, "Pro Plan", subscriptionItemList.Data[0].Plan.Name) +} + +func TestBillingClientListSubscriptionItems_Error(t *testing.T) { + t.Parallel() + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Status: http.StatusBadRequest, + Out: json.RawMessage(`{ + "errors":[{ + "code":"list-subscription-items-error-code" + }], + "clerk_trace_id":"list-subscription-items-trace-id" +}`), + }, + } + client := NewClient(config) + _, err := client.ListSubscriptionItems(context.Background(), &ListSubscriptionItemsParams{}) + require.Error(t, err) + apiErr, ok := err.(*clerk.APIErrorResponse) + require.True(t, ok) + require.Equal(t, "list-subscription-items-trace-id", apiErr.TraceID) + require.Equal(t, 1, len(apiErr.Errors)) + require.Equal(t, "list-subscription-items-error-code", apiErr.Errors[0].Code) +} + +func TestBillingClientCancelSubscriptionItem(t *testing.T) { + t.Parallel() + subscriptionItemID := "sub_item_123" + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"object":"subscription_item","id":"%s","payer_id":"payer_456","plan_id":"plan_789","status":"canceled","period_start":1640995200,"period_end":1643673600,"created_at":1640995200,"updated_at":1640995260}`, subscriptionItemID)), + Method: http.MethodDelete, + Path: "/v1/billing/subscription_items/" + subscriptionItemID, + Query: &url.Values{ + "end_now": []string{"true"}, + }, + }, + } + client := NewClient(config) + subscriptionItem, err := client.CancelSubscriptionItem(context.Background(), subscriptionItemID, &CancelSubscriptionItemParams{ + EndNow: clerk.Bool(true), + }) + require.NoError(t, err) + require.Equal(t, subscriptionItemID, subscriptionItem.ID) + require.Equal(t, "canceled", subscriptionItem.Status) + require.Equal(t, int64(1640995260), subscriptionItem.UpdatedAt) +} + +func TestBillingClientCancelSubscriptionItem_Error(t *testing.T) { + t.Parallel() + subscriptionItemID := "sub_item_123" + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Status: http.StatusNotFound, + Out: json.RawMessage(`{ + "errors":[{ + "code":"subscription-item-not-found" + }], + "clerk_trace_id":"cancel-subscription-item-trace-id" +}`), + }, + } + client := NewClient(config) + _, err := client.CancelSubscriptionItem(context.Background(), subscriptionItemID, &CancelSubscriptionItemParams{}) + require.Error(t, err) + apiErr, ok := err.(*clerk.APIErrorResponse) + require.True(t, ok) + require.Equal(t, "cancel-subscription-item-trace-id", apiErr.TraceID) + require.Equal(t, 1, len(apiErr.Errors)) + require.Equal(t, "subscription-item-not-found", apiErr.Errors[0].Code) +} + +func TestBillingClientExtendFreeTrial(t *testing.T) { + t.Parallel() + subscriptionItemID := "sub_item_123" + extendTo := "2024-12-31T23:59:59Z" + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + In: json.RawMessage(fmt.Sprintf(`{"extend_to":"%s"}`, extendTo)), + Out: json.RawMessage(fmt.Sprintf(`{"object":"subscription_item","id":"%s","payer_id":"payer_456","plan_id":"plan_789","status":"trialing","period_start":1640995200,"period_end":1735689599,"created_at":1640995200,"updated_at":1640995260}`, subscriptionItemID)), + Method: http.MethodPost, + Path: "/v1/billing/subscription_items/" + subscriptionItemID + "/extend_free_trial", + }, + } + client := NewClient(config) + subscriptionItem, err := client.ExtendFreeTrial(context.Background(), subscriptionItemID, &ExtendFreeTrialParams{ + ExtendTo: extendTo, + }) + require.NoError(t, err) + require.Equal(t, subscriptionItemID, subscriptionItem.ID) + require.Equal(t, "trialing", subscriptionItem.Status) + require.Equal(t, int64(1735689599), *subscriptionItem.PeriodEnd) + require.Equal(t, int64(1640995260), subscriptionItem.UpdatedAt) +} + +func TestBillingClientExtendFreeTrial_Error(t *testing.T) { + t.Parallel() + subscriptionItemID := "sub_item_123" + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Status: http.StatusBadRequest, + Out: json.RawMessage(`{ + "errors":[{ + "code":"subscription-item-not-in-free-trial" + }], + "clerk_trace_id":"extend-free-trial-trace-id" +}`), + }, + } + client := NewClient(config) + _, err := client.ExtendFreeTrial(context.Background(), subscriptionItemID, &ExtendFreeTrialParams{ + ExtendTo: "2024-12-31T23:59:59Z", + }) + require.Error(t, err) + apiErr, ok := err.(*clerk.APIErrorResponse) + require.True(t, ok) + require.Equal(t, "extend-free-trial-trace-id", apiErr.TraceID) + require.Equal(t, 1, len(apiErr.Errors)) + require.Equal(t, "subscription-item-not-in-free-trial", apiErr.Errors[0].Code) +} + +func TestBillingClientCancelSubscriptionItemWithoutEndNow(t *testing.T) { + t.Parallel() + subscriptionItemID := "sub_item_123" + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"object":"subscription_item","id":"%s","payer_id":"payer_456","plan_id":"plan_789","status":"canceled","period_start":1640995200,"period_end":1643673600,"created_at":1640995200,"updated_at":1640995260}`, subscriptionItemID)), + Method: http.MethodDelete, + Path: "/v1/billing/subscription_items/" + subscriptionItemID, + Query: &url.Values{ + "end_now": []string{"false"}, + }, + }, + } + client := NewClient(config) + subscriptionItem, err := client.CancelSubscriptionItem(context.Background(), subscriptionItemID, &CancelSubscriptionItemParams{ + EndNow: clerk.Bool(false), + }) + require.NoError(t, err) + require.Equal(t, subscriptionItemID, subscriptionItem.ID) + require.Equal(t, "canceled", subscriptionItem.Status) +} + +func TestBillingClientListPlansWithoutFilters(t *testing.T) { + t.Parallel() + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(`{"data": [{"object":"plan","id":"plan_123","name":"Basic Plan","payer_type":["user"],"features":[]},{"object":"plan","id":"plan_456","name":"Pro Plan","payer_type":["organization"],"features":[]}],"total_count": 2}`), + Method: http.MethodGet, + Path: "/v1/billing/plans", + }, + } + client := NewClient(config) + planList, err := client.ListPlans(context.Background(), &ListPlansParams{}) + require.NoError(t, err) + require.Equal(t, int64(2), planList.TotalCount) + require.Equal(t, 2, len(planList.Data)) + require.Equal(t, "plan_123", planList.Data[0].ID) + require.Equal(t, "Basic Plan", planList.Data[0].Name) + require.Equal(t, []string{"user"}, planList.Data[0].PayerType) + require.Equal(t, "plan_456", planList.Data[1].ID) + require.Equal(t, "Pro Plan", planList.Data[1].Name) + require.Equal(t, []string{"organization"}, planList.Data[1].PayerType) +} + +func TestBillingClientListSubscriptionItemsWithMultipleFilters(t *testing.T) { + t.Parallel() + config := &clerk.ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(`{"data": [{"object":"subscription_item","id":"sub_item_123","payer_id":"payer_456","plan_id":"plan_789","status":"active","period_start":1640995200,"period_end":1643673600,"created_at":1640995200,"updated_at":1640995200}],"total_count": 1}`), + Method: http.MethodGet, + Path: "/v1/billing/subscription_items", + Query: &url.Values{ + "user_id": []string{"user_456"}, + "plan_id": []string{"plan_789"}, + "status": []string{"active"}, + "payer_type": []string{"user"}, + "include_free": []string{"false"}, + }, + }, + } + client := NewClient(config) + params := &ListSubscriptionItemsParams{ + UserID: clerk.String("user_456"), + PlanID: clerk.String("plan_789"), + Status: clerk.String("active"), + PayerType: clerk.String("user"), + IncludeFree: clerk.Bool(false), + } + subscriptionItemList, err := client.ListSubscriptionItems(context.Background(), params) + require.NoError(t, err) + require.Equal(t, int64(1), subscriptionItemList.TotalCount) + require.Equal(t, 1, len(subscriptionItemList.Data)) + require.Equal(t, "sub_item_123", subscriptionItemList.Data[0].ID) + require.Equal(t, "payer_456", subscriptionItemList.Data[0].PayerID) + require.Equal(t, "plan_789", subscriptionItemList.Data[0].PlanID) + require.Equal(t, "active", subscriptionItemList.Data[0].Status) +} From 3fa7665e14c2c49382f1aabe5ba8ddf5dc5ee098 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Tue, 9 Sep 2025 17:48:53 -0300 Subject: [PATCH 02/19] refactor: Rename commerce -> billing --- billing.go | 124 ++++++++++++++++++++++++++--------------------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/billing.go b/billing.go index 8b4b7589..b805d72f 100644 --- a/billing.go +++ b/billing.go @@ -12,8 +12,8 @@ type Feature struct { AvatarURL string `json:"avatar_url"` } -// CommerceMoney represents money amounts with formatting. -type CommerceMoney struct { +// BillingMoney represents money amounts with formatting. +type BillingMoney struct { APIResource Amount int64 `json:"amount"` @@ -22,8 +22,8 @@ type CommerceMoney struct { CurrencySymbol string `json:"currency_symbol"` } -// CommerceProduct represents a product. -type CommerceProduct struct { +// BillingProduct represents a product. +type BillingProduct struct { APIResource Object string `json:"object"` @@ -39,34 +39,34 @@ type CommerceProduct struct { type Plan struct { APIResource - Object string `json:"object"` - ID string `json:"id"` - Name string `json:"name"` - Fee *CommerceMoney `json:"fee"` - AnnualMonthlyFee *CommerceMoney `json:"annual_monthly_fee"` - AnnualFee *CommerceMoney `json:"annual_fee"` - Amount *int64 `json:"amount,omitempty"` - AmountFormatted *string `json:"amount_formatted,omitempty"` - AnnualMonthlyAmount *int64 `json:"annual_monthly_amount,omitempty"` - AnnualMonthlyAmountFormatted *string `json:"annual_monthly_amount_formatted,omitempty"` - AnnualAmount *int64 `json:"annual_amount,omitempty"` - AnnualAmountFormatted *string `json:"annual_amount_formatted,omitempty"` - CurrencySymbol string `json:"currency_symbol"` - Currency string `json:"currency"` - Description string `json:"description"` - ProductID string `json:"product_id"` - Product *CommerceProduct `json:"product,omitempty"` - IsDefault bool `json:"is_default"` - IsRecurring bool `json:"is_recurring"` - PubliclyVisible bool `json:"publicly_visible"` - HasBaseFee bool `json:"has_base_fee"` - PayerType []string `json:"payer_type"` - ForPayerType string `json:"for_payer_type"` - Slug string `json:"slug"` - AvatarURL string `json:"avatar_url"` - Features []*Feature `json:"features"` - FreeTrialEnabled bool `json:"free_trial_enabled"` - FreeTrialDays *int `json:"free_trial_days"` + Object string `json:"object"` + ID string `json:"id"` + Name string `json:"name"` + Fee *BillingMoney `json:"fee"` + AnnualMonthlyFee *BillingMoney `json:"annual_monthly_fee"` + AnnualFee *BillingMoney `json:"annual_fee"` + Amount *int64 `json:"amount,omitempty"` + AmountFormatted *string `json:"amount_formatted,omitempty"` + AnnualMonthlyAmount *int64 `json:"annual_monthly_amount,omitempty"` + AnnualMonthlyAmountFormatted *string `json:"annual_monthly_amount_formatted,omitempty"` + AnnualAmount *int64 `json:"annual_amount,omitempty"` + AnnualAmountFormatted *string `json:"annual_amount_formatted,omitempty"` + CurrencySymbol string `json:"currency_symbol"` + Currency string `json:"currency"` + Description string `json:"description"` + ProductID string `json:"product_id"` + Product *BillingProduct `json:"product,omitempty"` + IsDefault bool `json:"is_default"` + IsRecurring bool `json:"is_recurring"` + PubliclyVisible bool `json:"publicly_visible"` + HasBaseFee bool `json:"has_base_fee"` + PayerType []string `json:"payer_type"` + ForPayerType string `json:"for_payer_type"` + Slug string `json:"slug"` + AvatarURL string `json:"avatar_url"` + Features []*Feature `json:"features"` + FreeTrialEnabled bool `json:"free_trial_enabled"` + FreeTrialDays *int `json:"free_trial_days"` } // PlanList contains a list of plans. @@ -77,8 +77,8 @@ type PlanList struct { TotalCount int64 `json:"total_count"` } -// CommercePaymentSource represents a payment source. -type CommercePaymentSource struct { +// BillingPaymentSource represents a payment source. +type BillingPaymentSource struct { APIResource Object string `json:"object"` @@ -100,12 +100,12 @@ type CommercePaymentSource struct { IsRemovable *bool `json:"is_removable,omitempty"` } -// CommerceSubscriptionItemNextPayment represents next payment info. -type CommerceSubscriptionItemNextPayment struct { +// BillingSubscriptionItemNextPayment represents next payment info. +type BillingSubscriptionItemNextPayment struct { APIResource - Amount *CommerceMoney `json:"amount"` - Date *int64 `json:"date"` + Amount *BillingMoney `json:"amount"` + Date *int64 `json:"date"` } // Payer represents a billing payer (user or organization). @@ -137,30 +137,30 @@ type Payer struct { type SubscriptionItem struct { APIResource - Object string `json:"object"` - ID string `json:"id"` - InstanceID string `json:"instance_id"` - Status string `json:"status"` - PlanID string `json:"plan_id"` - Plan *Plan `json:"plan"` - PlanPeriod string `json:"plan_period"` - PaymentSourceID string `json:"payment_source_id"` - PaymentSource *CommercePaymentSource `json:"payment_source"` - LifetimePaid *CommerceMoney `json:"lifetime_paid"` - Amount *CommerceMoney `json:"amount"` - NextInvoice *CommerceSubscriptionItemNextPayment `json:"next_invoice"` - NextPayment *CommerceSubscriptionItemNextPayment `json:"next_payment"` - PayerID string `json:"payer_id"` - Payer *Payer `json:"payer"` - IsFreeTrial bool `json:"is_free_trial"` - PeriodStart *int64 `json:"period_start"` - PeriodEnd *int64 `json:"period_end"` - ProrationDate string `json:"proration_date"` - CanceledAt *int64 `json:"canceled_at"` - PastDueAt *int64 `json:"past_due_at"` - EndedAt *int64 `json:"ended_at"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + Object string `json:"object"` + ID string `json:"id"` + InstanceID string `json:"instance_id"` + Status string `json:"status"` + PlanID string `json:"plan_id"` + Plan *Plan `json:"plan"` + PlanPeriod string `json:"plan_period"` + PaymentSourceID string `json:"payment_source_id"` + PaymentSource *BillingPaymentSource `json:"payment_source"` + LifetimePaid *BillingMoney `json:"lifetime_paid"` + Amount *BillingMoney `json:"amount"` + NextInvoice *BillingSubscriptionItemNextPayment `json:"next_invoice"` + NextPayment *BillingSubscriptionItemNextPayment `json:"next_payment"` + PayerID string `json:"payer_id"` + Payer *Payer `json:"payer"` + IsFreeTrial bool `json:"is_free_trial"` + PeriodStart *int64 `json:"period_start"` + PeriodEnd *int64 `json:"period_end"` + ProrationDate string `json:"proration_date"` + CanceledAt *int64 `json:"canceled_at"` + PastDueAt *int64 `json:"past_due_at"` + EndedAt *int64 `json:"ended_at"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` } // SubscriptionItemList contains a list of subscription items. From 6821d99a2051e1812d4988cf51aff32f3995d8cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Sun, 14 Sep 2025 09:23:21 -0300 Subject: [PATCH 03/19] fix: make PlanID nullable to support planless subs --- billing.go | 2 +- billing/client_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/billing.go b/billing.go index b805d72f..597487c7 100644 --- a/billing.go +++ b/billing.go @@ -141,7 +141,7 @@ type SubscriptionItem struct { ID string `json:"id"` InstanceID string `json:"instance_id"` Status string `json:"status"` - PlanID string `json:"plan_id"` + PlanID *string `json:"plan_id"` Plan *Plan `json:"plan"` PlanPeriod string `json:"plan_period"` PaymentSourceID string `json:"payment_source_id"` diff --git a/billing/client_test.go b/billing/client_test.go index 3eed543b..bc17200f 100644 --- a/billing/client_test.go +++ b/billing/client_test.go @@ -102,7 +102,7 @@ func TestBillingClientListSubscriptionItems(t *testing.T) { require.Equal(t, 1, len(subscriptionItemList.Data)) require.Equal(t, "sub_item_123", subscriptionItemList.Data[0].ID) require.Equal(t, "payer_456", subscriptionItemList.Data[0].PayerID) - require.Equal(t, "plan_789", subscriptionItemList.Data[0].PlanID) + require.Equal(t, "plan_789", *subscriptionItemList.Data[0].PlanID) require.Equal(t, "active", subscriptionItemList.Data[0].Status) require.Equal(t, int64(1640995200), *subscriptionItemList.Data[0].PeriodStart) require.Equal(t, int64(1643673600), *subscriptionItemList.Data[0].PeriodEnd) @@ -323,6 +323,6 @@ func TestBillingClientListSubscriptionItemsWithMultipleFilters(t *testing.T) { require.Equal(t, 1, len(subscriptionItemList.Data)) require.Equal(t, "sub_item_123", subscriptionItemList.Data[0].ID) require.Equal(t, "payer_456", subscriptionItemList.Data[0].PayerID) - require.Equal(t, "plan_789", subscriptionItemList.Data[0].PlanID) + require.Equal(t, "plan_789", *subscriptionItemList.Data[0].PlanID) require.Equal(t, "active", subscriptionItemList.Data[0].Status) } From 4dc28c4fc9bfff28a4b936930e47f3754df710ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Tue, 23 Sep 2025 17:45:37 -0300 Subject: [PATCH 04/19] refactor: remove unused struct --- billing.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/billing.go b/billing.go index 597487c7..c6e19f35 100644 --- a/billing.go +++ b/billing.go @@ -170,10 +170,3 @@ type SubscriptionItemList struct { Data []*SubscriptionItem `json:"data"` TotalCount int64 `json:"total_count"` } - -// ExtendFreeTrialResponse represents the response from extending free trial. -type ExtendFreeTrialResponse struct { - APIResource - - *SubscriptionItem -} From e87db2368646aa8e089d8f87bf3399db950fb32a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Tue, 23 Sep 2025 18:16:04 -0300 Subject: [PATCH 05/19] fix: we will be using PaymentMethod from now on --- billing.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/billing.go b/billing.go index c6e19f35..a885f40d 100644 --- a/billing.go +++ b/billing.go @@ -77,8 +77,8 @@ type PlanList struct { TotalCount int64 `json:"total_count"` } -// BillingPaymentSource represents a payment source. -type BillingPaymentSource struct { +// BillingPaymentMethod represents a payment method. +type BillingPaymentMethod struct { APIResource Object string `json:"object"` @@ -144,8 +144,8 @@ type SubscriptionItem struct { PlanID *string `json:"plan_id"` Plan *Plan `json:"plan"` PlanPeriod string `json:"plan_period"` - PaymentSourceID string `json:"payment_source_id"` - PaymentSource *BillingPaymentSource `json:"payment_source"` + PaymentMethodID string `json:"payment_method_id"` + PaymentMethod *BillingPaymentMethod `json:"payment_method"` LifetimePaid *BillingMoney `json:"lifetime_paid"` Amount *BillingMoney `json:"amount"` NextInvoice *BillingSubscriptionItemNextPayment `json:"next_invoice"` From eedc3906882397adb79c91ced2467ff0490cf5af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Tue, 23 Sep 2025 19:33:00 -0300 Subject: [PATCH 06/19] refactor: remove deprecated fields --- billing.go | 50 ++++++++++++++++++++++---------------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/billing.go b/billing.go index a885f40d..e5300ff3 100644 --- a/billing.go +++ b/billing.go @@ -39,34 +39,28 @@ type BillingProduct struct { type Plan struct { APIResource - Object string `json:"object"` - ID string `json:"id"` - Name string `json:"name"` - Fee *BillingMoney `json:"fee"` - AnnualMonthlyFee *BillingMoney `json:"annual_monthly_fee"` - AnnualFee *BillingMoney `json:"annual_fee"` - Amount *int64 `json:"amount,omitempty"` - AmountFormatted *string `json:"amount_formatted,omitempty"` - AnnualMonthlyAmount *int64 `json:"annual_monthly_amount,omitempty"` - AnnualMonthlyAmountFormatted *string `json:"annual_monthly_amount_formatted,omitempty"` - AnnualAmount *int64 `json:"annual_amount,omitempty"` - AnnualAmountFormatted *string `json:"annual_amount_formatted,omitempty"` - CurrencySymbol string `json:"currency_symbol"` - Currency string `json:"currency"` - Description string `json:"description"` - ProductID string `json:"product_id"` - Product *BillingProduct `json:"product,omitempty"` - IsDefault bool `json:"is_default"` - IsRecurring bool `json:"is_recurring"` - PubliclyVisible bool `json:"publicly_visible"` - HasBaseFee bool `json:"has_base_fee"` - PayerType []string `json:"payer_type"` - ForPayerType string `json:"for_payer_type"` - Slug string `json:"slug"` - AvatarURL string `json:"avatar_url"` - Features []*Feature `json:"features"` - FreeTrialEnabled bool `json:"free_trial_enabled"` - FreeTrialDays *int `json:"free_trial_days"` + Object string `json:"object"` + ID string `json:"id"` + Name string `json:"name"` + Fee *BillingMoney `json:"fee"` + AnnualMonthlyFee *BillingMoney `json:"annual_monthly_fee"` + AnnualFee *BillingMoney `json:"annual_fee"` + CurrencySymbol string `json:"currency_symbol"` + Currency string `json:"currency"` + Description string `json:"description"` + ProductID string `json:"product_id"` + Product *BillingProduct `json:"product,omitempty"` + IsDefault bool `json:"is_default"` + IsRecurring bool `json:"is_recurring"` + PubliclyVisible bool `json:"publicly_visible"` + HasBaseFee bool `json:"has_base_fee"` + PayerType []string `json:"payer_type"` + ForPayerType string `json:"for_payer_type"` + Slug string `json:"slug"` + AvatarURL string `json:"avatar_url"` + Features []*Feature `json:"features"` + FreeTrialEnabled bool `json:"free_trial_enabled"` + FreeTrialDays *int `json:"free_trial_days"` } // PlanList contains a list of plans. From 68c58024a581a454d3fe0d0123b0603fc04e3ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Tue, 23 Sep 2025 19:37:31 -0300 Subject: [PATCH 07/19] refactor: remove deprecated invoice field --- billing.go | 1 - 1 file changed, 1 deletion(-) diff --git a/billing.go b/billing.go index e5300ff3..a4de1df4 100644 --- a/billing.go +++ b/billing.go @@ -142,7 +142,6 @@ type SubscriptionItem struct { PaymentMethod *BillingPaymentMethod `json:"payment_method"` LifetimePaid *BillingMoney `json:"lifetime_paid"` Amount *BillingMoney `json:"amount"` - NextInvoice *BillingSubscriptionItemNextPayment `json:"next_invoice"` NextPayment *BillingSubscriptionItemNextPayment `json:"next_payment"` PayerID string `json:"payer_id"` Payer *Payer `json:"payer"` From 83d5893122c4e57d2d30e445a215851fc6604572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Tue, 23 Sep 2025 20:07:31 -0300 Subject: [PATCH 08/19] refactor: omitempty is useless when unmarshalling --- billing.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/billing.go b/billing.go index a4de1df4..fe7fac3c 100644 --- a/billing.go +++ b/billing.go @@ -49,7 +49,7 @@ type Plan struct { Currency string `json:"currency"` Description string `json:"description"` ProductID string `json:"product_id"` - Product *BillingProduct `json:"product,omitempty"` + Product *BillingProduct `json:"product"` IsDefault bool `json:"is_default"` IsRecurring bool `json:"is_recurring"` PubliclyVisible bool `json:"publicly_visible"` @@ -79,7 +79,7 @@ type BillingPaymentMethod struct { ID string `json:"id"` PayerID string `json:"payer_id"` PaymentMethod string `json:"payment_method"` - IsDefault *bool `json:"is_default,omitempty"` + IsDefault *bool `json:"is_default"` Gateway string `json:"gateway"` GatewayExternalID string `json:"gateway_external_id"` GatewayExternalAccountID *string `json:"gateway_external_account_id"` @@ -87,11 +87,11 @@ type BillingPaymentMethod struct { Status string `json:"status"` WalletType string `json:"wallet_type"` CardType string `json:"card_type"` - ExpiryYear int `json:"expiry_year,omitempty"` - ExpiryMonth int `json:"expiry_month,omitempty"` + ExpiryYear int `json:"expiry_year"` + ExpiryMonth int `json:"expiry_month"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` - IsRemovable *bool `json:"is_removable,omitempty"` + IsRemovable *bool `json:"is_removable"` } // BillingSubscriptionItemNextPayment represents next payment info. @@ -111,14 +111,14 @@ type Payer struct { InstanceID string `json:"instance_id"` // User payer only - UserID string `json:"user_id,omitempty"` + UserID string `json:"user_id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` Email string `json:"email"` // Org payer only - OrganizationID string `json:"organization_id,omitempty"` - OrganizationName string `json:"organization_name,omitempty"` + OrganizationID string `json:"organization_id"` + OrganizationName string `json:"organization_name"` // Used for both org and user payers ImageURL string `json:"image_url"` From 56788a8caf37e8b7898a2b0cd873bc2f44a937a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Tue, 23 Sep 2025 20:20:00 -0300 Subject: [PATCH 09/19] refactor: query strings don't need omitempty --- billing/client.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/billing/client.go b/billing/client.go index 469b0563..35b3074c 100644 --- a/billing/client.go +++ b/billing/client.go @@ -54,13 +54,13 @@ func (c *Client) ListPlans(ctx context.Context, params *ListPlansParams) (*clerk type ListSubscriptionItemsParams struct { clerk.APIParams clerk.ListParams - Status *string `json:"status,omitempty"` - PayerType *string `json:"payer_type,omitempty"` - PlanID *string `json:"plan_id,omitempty"` - IncludeFree *bool `json:"include_free,omitempty"` - Query *string `json:"query,omitempty"` - UserID *string `json:"user_id,omitempty"` - OrganizationID *string `json:"organization_id,omitempty"` + Status *string `json:"status"` + PayerType *string `json:"payer_type"` + PlanID *string `json:"plan_id"` + IncludeFree *bool `json:"include_free"` + Query *string `json:"query"` + UserID *string `json:"user_id"` + OrganizationID *string `json:"organization_id"` } func (params *ListSubscriptionItemsParams) ToQuery() url.Values { From 2e3178a9ed4a8342f22c8ec0dc71741a3f811127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Wed, 24 Sep 2025 19:46:58 -0300 Subject: [PATCH 10/19] fix: several fix related to JSON tags --- billing.go | 12 ++++++------ billing/client.go | 18 +++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/billing.go b/billing.go index fe7fac3c..565b3580 100644 --- a/billing.go +++ b/billing.go @@ -111,14 +111,14 @@ type Payer struct { InstanceID string `json:"instance_id"` // User payer only - UserID string `json:"user_id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email string `json:"email"` + UserID *string `json:"user_id"` + FirstName *string `json:"first_name"` + LastName *string `json:"last_name"` + Email *string `json:"email"` // Org payer only - OrganizationID string `json:"organization_id"` - OrganizationName string `json:"organization_name"` + OrganizationID *string `json:"organization_id"` + OrganizationName *string `json:"organization_name"` // Used for both org and user payers ImageURL string `json:"image_url"` diff --git a/billing/client.go b/billing/client.go index 35b3074c..31c98bfd 100644 --- a/billing/client.go +++ b/billing/client.go @@ -27,7 +27,7 @@ func NewClient(config *clerk.ClientConfig) *Client { type ListPlansParams struct { clerk.APIParams clerk.ListParams - PayerType *string `json:"payer_type,omitempty"` + PayerType *string } func (params *ListPlansParams) ToQuery() url.Values { @@ -54,13 +54,13 @@ func (c *Client) ListPlans(ctx context.Context, params *ListPlansParams) (*clerk type ListSubscriptionItemsParams struct { clerk.APIParams clerk.ListParams - Status *string `json:"status"` - PayerType *string `json:"payer_type"` - PlanID *string `json:"plan_id"` - IncludeFree *bool `json:"include_free"` - Query *string `json:"query"` - UserID *string `json:"user_id"` - OrganizationID *string `json:"organization_id"` + Status *string + PayerType *string + PlanID *string + IncludeFree *bool + Query *string + UserID *string + OrganizationID *string } func (params *ListSubscriptionItemsParams) ToQuery() url.Values { @@ -108,7 +108,7 @@ func (c *Client) ListSubscriptionItems(ctx context.Context, params *ListSubscrip type CancelSubscriptionItemParams struct { clerk.APIParams - EndNow *bool `json:"end_now,omitempty"` + EndNow *bool } func (params *CancelSubscriptionItemParams) ToQuery() url.Values { From f397d46591d198032090a172b26aa7d4519699d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Wed, 24 Sep 2025 19:51:17 -0300 Subject: [PATCH 11/19] test: fix after struct changes --- billing/client_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/billing/client_test.go b/billing/client_test.go index bc17200f..1a73c5e5 100644 --- a/billing/client_test.go +++ b/billing/client_test.go @@ -78,7 +78,7 @@ func TestBillingClientListSubscriptionItems(t *testing.T) { config.HTTPClient = &http.Client{ Transport: &clerktest.RoundTripper{ T: t, - Out: json.RawMessage(`{"data": [{"object":"subscription_item","id":"sub_item_123","payer_id":"payer_456","plan_id":"plan_789","status":"active","period_start":1640995200,"period_end":1643673600,"payer":{"object":"payer","id":"payer_456","first_name":"John","last_name":"Doe","created_at":1640995200,"updated_at":1640995200},"plan":{"object":"plan","id":"plan_789","name":"Pro Plan","payer_type":["user"],"features":[]},"created_at":1640995200,"updated_at":1640995200}],"total_count": 1}`), + Out: json.RawMessage(`{"data": [{"object":"subscription_item","id":"sub_item_123","payer_id":"payer_456","plan_id":"plan_789","status":"active","period_start":1640995200,"period_end":1643673600,"payer":{"object":"payer","id":"payer_456","user_id":"user_456","first_name":"John","last_name":"Doe","email":"john@example.com","created_at":1640995200,"updated_at":1640995200},"plan":{"object":"plan","id":"plan_789","name":"Pro Plan","payer_type":["user"],"features":[]},"created_at":1640995200,"updated_at":1640995200}],"total_count": 1}`), Method: http.MethodGet, Path: "/v1/billing/subscription_items", Query: &url.Values{ @@ -108,7 +108,7 @@ func TestBillingClientListSubscriptionItems(t *testing.T) { require.Equal(t, int64(1643673600), *subscriptionItemList.Data[0].PeriodEnd) require.NotNil(t, subscriptionItemList.Data[0].Payer) require.Equal(t, "payer_456", subscriptionItemList.Data[0].Payer.ID) - require.Equal(t, "John", subscriptionItemList.Data[0].Payer.FirstName) + require.Equal(t, "John", *subscriptionItemList.Data[0].Payer.FirstName) require.NotNil(t, subscriptionItemList.Data[0].Plan) require.Equal(t, "plan_789", subscriptionItemList.Data[0].Plan.ID) require.Equal(t, "Pro Plan", subscriptionItemList.Data[0].Plan.Name) @@ -297,7 +297,7 @@ func TestBillingClientListSubscriptionItemsWithMultipleFilters(t *testing.T) { config.HTTPClient = &http.Client{ Transport: &clerktest.RoundTripper{ T: t, - Out: json.RawMessage(`{"data": [{"object":"subscription_item","id":"sub_item_123","payer_id":"payer_456","plan_id":"plan_789","status":"active","period_start":1640995200,"period_end":1643673600,"created_at":1640995200,"updated_at":1640995200}],"total_count": 1}`), + Out: json.RawMessage(`{"data": [{"object":"subscription_item","id":"sub_item_123","payer_id":"payer_456","plan_id":"plan_789","status":"active","period_start":1640995200,"period_end":1643673600,"payer":{"object":"payer","id":"payer_456","user_id":"user_456","first_name":"John","last_name":"Doe","email":"john@example.com","created_at":1640995200,"updated_at":1640995200},"plan":{"object":"plan","id":"plan_789","name":"Pro Plan","payer_type":["user"],"features":[]},"created_at":1640995200,"updated_at":1640995200}],"total_count": 1}`), Method: http.MethodGet, Path: "/v1/billing/subscription_items", Query: &url.Values{ From 51eaa2420ee3d7e655a9f8b2a94434acf88baa5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Wed, 24 Sep 2025 22:36:31 -0300 Subject: [PATCH 12/19] fix: Remove PayerType array --- billing.go | 1 - 1 file changed, 1 deletion(-) diff --git a/billing.go b/billing.go index 565b3580..8e6532eb 100644 --- a/billing.go +++ b/billing.go @@ -54,7 +54,6 @@ type Plan struct { IsRecurring bool `json:"is_recurring"` PubliclyVisible bool `json:"publicly_visible"` HasBaseFee bool `json:"has_base_fee"` - PayerType []string `json:"payer_type"` ForPayerType string `json:"for_payer_type"` Slug string `json:"slug"` AvatarURL string `json:"avatar_url"` From c7899113ccc51d251b559c3c6a02118b9d137035 Mon Sep 17 00:00:00 2001 From: Mauricio Antunes Date: Wed, 24 Sep 2025 22:37:52 -0300 Subject: [PATCH 13/19] fix: we are not returning null features Co-authored-by: Paddy --- billing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/billing.go b/billing.go index 8e6532eb..4d450f8b 100644 --- a/billing.go +++ b/billing.go @@ -57,7 +57,7 @@ type Plan struct { ForPayerType string `json:"for_payer_type"` Slug string `json:"slug"` AvatarURL string `json:"avatar_url"` - Features []*Feature `json:"features"` + Features []Feature `json:"features"` FreeTrialEnabled bool `json:"free_trial_enabled"` FreeTrialDays *int `json:"free_trial_days"` } From ab8a6e77bf12740d0ac6d5703dc876cbeaeff880 Mon Sep 17 00:00:00 2001 From: Mauricio Antunes Date: Wed, 24 Sep 2025 22:38:19 -0300 Subject: [PATCH 14/19] fix: we are not returning null plans Co-authored-by: Paddy --- billing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/billing.go b/billing.go index 4d450f8b..bb9ec82f 100644 --- a/billing.go +++ b/billing.go @@ -32,7 +32,7 @@ type BillingProduct struct { Currency string `json:"currency"` Name string `json:"name"` IsDefault bool `json:"is_default"` - Plans []*Plan `json:"plans"` + Plans []Plan `json:"plans"` } // Plan represents a billing plan. From 7f7ebc58f97f4767b50025d71f5f1616ba7f93cc Mon Sep 17 00:00:00 2001 From: Mauricio Antunes Date: Wed, 24 Sep 2025 22:38:48 -0300 Subject: [PATCH 15/19] fix: we are not returning null subscription items Co-authored-by: Paddy --- billing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/billing.go b/billing.go index bb9ec82f..c28eb8c7 100644 --- a/billing.go +++ b/billing.go @@ -159,6 +159,6 @@ type SubscriptionItem struct { type SubscriptionItemList struct { APIResource - Data []*SubscriptionItem `json:"data"` + Data []SubscriptionItem `json:"data"` TotalCount int64 `json:"total_count"` } From 0a5bb0d92416464c81e0e7a84409f0a4c3ba3d9e Mon Sep 17 00:00:00 2001 From: Mauricio Antunes Date: Wed, 24 Sep 2025 22:39:25 -0300 Subject: [PATCH 16/19] fix: we are not returning null plans Co-authored-by: Paddy --- billing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/billing.go b/billing.go index c28eb8c7..d7e3165c 100644 --- a/billing.go +++ b/billing.go @@ -66,7 +66,7 @@ type Plan struct { type PlanList struct { APIResource - Data []*Plan `json:"data"` + Data []Plan `json:"data"` TotalCount int64 `json:"total_count"` } From 725d325f63ca6fee344768d47edbd999e5442883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Wed, 24 Sep 2025 22:55:54 -0300 Subject: [PATCH 17/19] fix: remove currency deprecated fields --- billing.go | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/billing.go b/billing.go index d7e3165c..2cbe3306 100644 --- a/billing.go +++ b/billing.go @@ -26,12 +26,12 @@ type BillingMoney struct { type BillingProduct struct { APIResource - Object string `json:"object"` - ID string `json:"id"` - Slug string `json:"slug"` - Currency string `json:"currency"` - Name string `json:"name"` - IsDefault bool `json:"is_default"` + Object string `json:"object"` + ID string `json:"id"` + Slug string `json:"slug"` + Currency string `json:"currency"` + Name string `json:"name"` + IsDefault bool `json:"is_default"` Plans []Plan `json:"plans"` } @@ -45,8 +45,6 @@ type Plan struct { Fee *BillingMoney `json:"fee"` AnnualMonthlyFee *BillingMoney `json:"annual_monthly_fee"` AnnualFee *BillingMoney `json:"annual_fee"` - CurrencySymbol string `json:"currency_symbol"` - Currency string `json:"currency"` Description string `json:"description"` ProductID string `json:"product_id"` Product *BillingProduct `json:"product"` @@ -57,7 +55,7 @@ type Plan struct { ForPayerType string `json:"for_payer_type"` Slug string `json:"slug"` AvatarURL string `json:"avatar_url"` - Features []Feature `json:"features"` + Features []Feature `json:"features"` FreeTrialEnabled bool `json:"free_trial_enabled"` FreeTrialDays *int `json:"free_trial_days"` } @@ -67,7 +65,7 @@ type PlanList struct { APIResource Data []Plan `json:"data"` - TotalCount int64 `json:"total_count"` + TotalCount int64 `json:"total_count"` } // BillingPaymentMethod represents a payment method. @@ -160,5 +158,5 @@ type SubscriptionItemList struct { APIResource Data []SubscriptionItem `json:"data"` - TotalCount int64 `json:"total_count"` + TotalCount int64 `json:"total_count"` } From e834012fc30495da7c2f3d8b3d4e43441531f393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Wed, 24 Sep 2025 23:27:01 -0300 Subject: [PATCH 18/19] test: fix --- billing/client_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/billing/client_test.go b/billing/client_test.go index 1a73c5e5..6f51d624 100644 --- a/billing/client_test.go +++ b/billing/client_test.go @@ -19,7 +19,7 @@ func TestBillingClientListPlans(t *testing.T) { config.HTTPClient = &http.Client{ Transport: &clerktest.RoundTripper{ T: t, - Out: json.RawMessage(`{"data": [{"object":"plan","id":"plan_123","name":"Basic Plan","payer_type":["user"],"features":[{"object":"feature","id":"feature_456","name":"Feature 1","key":"feature_1"}]}],"total_count": 1}`), + Out: json.RawMessage(`{"data": [{"object":"plan","id":"plan_123","name":"Basic Plan","for_payer_type":"user","features":[{"object":"feature","id":"feature_456","name":"Feature 1","key":"feature_1"}]}],"total_count": 1}`), Method: http.MethodGet, Path: "/v1/billing/plans", Query: &url.Values{ @@ -41,7 +41,7 @@ func TestBillingClientListPlans(t *testing.T) { require.Equal(t, 1, len(planList.Data)) require.Equal(t, "plan_123", planList.Data[0].ID) require.Equal(t, "Basic Plan", planList.Data[0].Name) - require.Equal(t, []string{"user"}, planList.Data[0].PayerType) + require.Equal(t, "user", planList.Data[0].ForPayerType) require.Equal(t, 1, len(planList.Data[0].Features)) require.Equal(t, "feature_456", planList.Data[0].Features[0].ID) require.Equal(t, "Feature 1", planList.Data[0].Features[0].Name) @@ -273,7 +273,7 @@ func TestBillingClientListPlansWithoutFilters(t *testing.T) { config.HTTPClient = &http.Client{ Transport: &clerktest.RoundTripper{ T: t, - Out: json.RawMessage(`{"data": [{"object":"plan","id":"plan_123","name":"Basic Plan","payer_type":["user"],"features":[]},{"object":"plan","id":"plan_456","name":"Pro Plan","payer_type":["organization"],"features":[]}],"total_count": 2}`), + Out: json.RawMessage(`{"data": [{"object":"plan","id":"plan_123","name":"Basic Plan","for_payer_type":"user","features":[]},{"object":"plan","id":"plan_456","name":"Pro Plan","for_payer_type":"organization","features":[]}],"total_count": 2}`), Method: http.MethodGet, Path: "/v1/billing/plans", }, @@ -285,10 +285,10 @@ func TestBillingClientListPlansWithoutFilters(t *testing.T) { require.Equal(t, 2, len(planList.Data)) require.Equal(t, "plan_123", planList.Data[0].ID) require.Equal(t, "Basic Plan", planList.Data[0].Name) - require.Equal(t, []string{"user"}, planList.Data[0].PayerType) + require.Equal(t, "user", planList.Data[0].ForPayerType) require.Equal(t, "plan_456", planList.Data[1].ID) require.Equal(t, "Pro Plan", planList.Data[1].Name) - require.Equal(t, []string{"organization"}, planList.Data[1].PayerType) + require.Equal(t, "organization", planList.Data[1].ForPayerType) } func TestBillingClientListSubscriptionItemsWithMultipleFilters(t *testing.T) { From 1d64f3fb843ad64972b2d6ce46443b8c0281550d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maur=C3=ADcio=20Antunes?= Date: Wed, 24 Sep 2025 23:45:24 -0300 Subject: [PATCH 19/19] fix: period_start cannot be null --- billing.go | 2 +- billing/client_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/billing.go b/billing.go index 2cbe3306..ffd21668 100644 --- a/billing.go +++ b/billing.go @@ -143,7 +143,7 @@ type SubscriptionItem struct { PayerID string `json:"payer_id"` Payer *Payer `json:"payer"` IsFreeTrial bool `json:"is_free_trial"` - PeriodStart *int64 `json:"period_start"` + PeriodStart int64 `json:"period_start"` PeriodEnd *int64 `json:"period_end"` ProrationDate string `json:"proration_date"` CanceledAt *int64 `json:"canceled_at"` diff --git a/billing/client_test.go b/billing/client_test.go index 6f51d624..41abccda 100644 --- a/billing/client_test.go +++ b/billing/client_test.go @@ -104,7 +104,7 @@ func TestBillingClientListSubscriptionItems(t *testing.T) { require.Equal(t, "payer_456", subscriptionItemList.Data[0].PayerID) require.Equal(t, "plan_789", *subscriptionItemList.Data[0].PlanID) require.Equal(t, "active", subscriptionItemList.Data[0].Status) - require.Equal(t, int64(1640995200), *subscriptionItemList.Data[0].PeriodStart) + require.Equal(t, int64(1640995200), subscriptionItemList.Data[0].PeriodStart) require.Equal(t, int64(1643673600), *subscriptionItemList.Data[0].PeriodEnd) require.NotNil(t, subscriptionItemList.Data[0].Payer) require.Equal(t, "payer_456", subscriptionItemList.Data[0].Payer.ID)