From f224f1cc4c41fb70914d40c83145fc5bbc19c30b Mon Sep 17 00:00:00 2001 From: saul Date: Wed, 15 Nov 2023 00:14:28 -0500 Subject: [PATCH] feat: added subscription item endpoints and object --- README.md | 6 +- client.go | 2 + internal/stubs/subscription_item.go | 179 ++++++++++++++++++++ internal/stubs/subscriptions.go | 15 +- subscription_items.go | 23 ++- subscription_items_service.go | 101 ++++++++++++ subscription_items_service_test.go | 242 ++++++++++++++++++++++++++++ subscriptions.go | 65 ++++---- subscriptions_service.go | 16 +- subscriptions_service_test.go | 42 ++--- 10 files changed, 628 insertions(+), 63 deletions(-) create mode 100644 internal/stubs/subscription_item.go create mode 100644 subscription_items_service.go create mode 100644 subscription_items_service_test.go diff --git a/README.md b/README.md index 81c7c16..1a2e43f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ [![GitHub license](https://img.shields.io/github/license/NdoleStudio/lemonsqueezy-go?color=brightgreen)](https://github.com/NdoleStudio/lemonsqueezy-go/blob/master/LICENSE) [![PkgGoDev](https://pkg.go.dev/badge/github.com/NdoleStudio/lemonsqueezy-go)](https://pkg.go.dev/github.com/NdoleStudio/lemonsqueezy-go) - This package provides a go API client for the lemonsqueezy API ## Installation @@ -58,6 +57,11 @@ import "github.com/NdoleStudio/lemonsqueezy-go" - **Subscription Invoices** - `GET /v1/subscription-invoices/:id`: Retrieve a subscription invoice - `GET /v1/subscription-invoices`: List all subscription invoices +- **Subscription Items** + - `GET /v1/subscription-items/:id`: Retrieve a subscription item + - `PATCH /v1/subscription-items/:id`: Update a subscription item + - `GET /v1/subscription-items`: List all subscription items + - `GET /v1/subscription-items/:id/current-usage`: Retrieve a subscription item's current usage - **Discounts** - `POST /v1/discounts`: Create a discount - `GET /v1/discounts/:id`: Retrieve a discount diff --git a/client.go b/client.go index 1c431f9..39ba3db 100644 --- a/client.go +++ b/client.go @@ -32,6 +32,7 @@ type Client struct { Orders *OrdersService OrderItems *OrderItemsService SubscriptionInvoices *SubscriptionInvoicesService + SubscriptionItems *SubscriptionItemsService DiscountRedemptions *DiscountRedemptionsService Discounts *DiscountsService Checkouts *CheckoutsService @@ -66,6 +67,7 @@ func New(options ...Option) *Client { client.Orders = (*OrdersService)(&client.common) client.OrderItems = (*OrderItemsService)(&client.common) client.SubscriptionInvoices = (*SubscriptionInvoicesService)(&client.common) + client.SubscriptionItems = (*SubscriptionItemsService)(&client.common) client.DiscountRedemptions = (*DiscountRedemptionsService)(&client.common) client.Discounts = (*DiscountsService)(&client.common) client.Checkouts = (*CheckoutsService)(&client.common) diff --git a/internal/stubs/subscription_item.go b/internal/stubs/subscription_item.go new file mode 100644 index 0000000..f3f0711 --- /dev/null +++ b/internal/stubs/subscription_item.go @@ -0,0 +1,179 @@ +package stubs + +// SubscriptionItemGetResponse returns a dummy response to GET /v1/subscription-items/:id endpoint +func SubscriptionItemGetResponse() []byte { + return []byte(` +{ + "jsonapi": { + "version": "1.0" + }, + "links": { + "self": "https://api.lemonsqueezy.com/v1/subscription-item/1" + }, + "data": { + "type": "subscription-items", + "id": "1", + "attributes": { + "subscription_id": 1, + "price_id": 1, + "quantity": 1, + "is_usage_based": false, + "created_at": "2023-07-18T12:16:24.000000Z", + "updated_at": "2023-07-18T12:16:24.000000Z" + }, + "relationships": { + "subscription": { + "links": { + "related": "https://api.lemonsqueezy.com/v1/subscription-items/1/subscription", + "self": "https://api.lemonsqueezy.com/v1/subscription-items/1/relationships/subscription" + } + }, + "price": { + "links": { + "related": "https://api.lemonsqueezy.com/v1/subscription-items/1/price", + "self": "https://api.lemonsqueezy.com/v1/subscription-items/1/relationships/price" + } + }, + "usage-records": { + "links": { + "related": "https://api.lemonsqueezy.com/v1/subscription-items/1/usage-records", + "self": "https://api.lemonsqueezy.com/v1/subscription-items/1/relationships/usage-records" + } + } + }, + "links": { + "self": "https://api.lemonsqueezy.com/v1/subscription-items/1" + } + } +} +`) +} + +// SubscriptionItemUpdateResponse is a dummy response to the PATCH /v1/subscription-items/:id endpoint +func SubscriptionItemUpdateResponse() []byte { + return []byte(` +{ + "jsonapi": { + "version": "1.0" + }, + "links": { + "self": "https://api.lemonsqueezy.com/v1/subscription-item/1" + }, + "data": { + "type": "subscription-items", + "id": "1", + "attributes": { + "subscription_id": 1, + "price_id": 1, + "quantity": 10, + "is_usage_based": false, + "created_at": "2023-07-18T12:16:24.000000Z", + "updated_at": "2023-07-18T12:23:18.000000Z" + }, + "relationships": { + "subscription": { + "links": { + "related": "https://api.lemonsqueezy.com/v1/subscription-items/1/subscription", + "self": "https://api.lemonsqueezy.com/v1/subscription-items/1/relationships/subscription" + } + }, + "price": { + "links": { + "related": "https://api.lemonsqueezy.com/v1/subscription-items/1/price", + "self": "https://api.lemonsqueezy.com/v1/subscription-items/1/relationships/price" + } + }, + "usage-records": { + "links": { + "related": "https://api.lemonsqueezy.com/v1/subscription-items/1/usage-records", + "self": "https://api.lemonsqueezy.com/v1/subscription-items/1/relationships/usage-records" + } + } + }, + "links": { + "self": "https://api.lemonsqueezy.com/v1/subscription-items/1" + } + } +} +`) +} + +// SubscriptionItemsListResponse returns a dummy response to GET /v1/subscription-items endpoint +func SubscriptionItemsListResponse() []byte { + return []byte(` +{ + "meta": { + "page": { + "currentPage": 1, + "from": 1, + "lastPage": 1, + "perPage": 10, + "to": 10, + "total": 10 + } + }, + "jsonapi": { + "version": "1.0" + }, + "links": { + "first": "https://api.lemonsqueezy.com/v1/subscription-items?page%5Bnumber%5D=1&page%5Bsize%5D=10&sort=-created_at", + "last": "https://api.lemonsqueezy.com/v1/subscription-items?page%5Bnumber%5D=1&page%5Bsize%5D=10&sort=-created_at" + }, + "data": [ + { + "type": "subscription-items", + "id": "1", + "attributes": { + "subscription_id": 1, + "price_id": 1, + "quantity": 1, + "is_usage_based": false, + "created_at": "2023-07-18T12:16:24.000000Z", + "updated_at": "2023-07-18T12:16:24.000000Z" + }, + "relationships": { + "subscription": { + "links": { + "related": "https://api.lemonsqueezy.com/v1/subscription-items/1/subscription", + "self": "https://api.lemonsqueezy.com/v1/subscription-items/1/relationships/subscription" + } + }, + "price": { + "links": { + "related": "https://api.lemonsqueezy.com/v1/subscription-items/1/price", + "self": "https://api.lemonsqueezy.com/v1/subscription-items/1/relationships/price" + } + }, + "usage-records": { + "links": { + "related": "https://api.lemonsqueezy.com/v1/subscription-items/1/usage-records", + "self": "https://api.lemonsqueezy.com/v1/subscription-items/1/relationships/usage-records" + } + } + }, + "links": { + "self": "https://api.lemonsqueezy.com/v1/subscription-items/1" + } + } + ] +} +`) +} + +// SubscriptionItemCurrentUsageResponse returns a dummy response to GET /v1/subscription-items/:id/current-usage endpoint +func SubscriptionItemCurrentUsageResponse() []byte { + return []byte(` +{ + "jsonapi": { + "version": "1.0" + }, + "meta": { + "period_start": "2023-08-10T13:08:16.000000Z", + "period_end": "2023-09-10T13:03:16.000000Z", + "quantity": 5, + "interval_unit": "month", + "interval_quantity": 1 + } +} +`) +} diff --git a/internal/stubs/subscriptions.go b/internal/stubs/subscriptions.go index b2e1154..44d6996 100644 --- a/internal/stubs/subscriptions.go +++ b/internal/stubs/subscriptions.go @@ -41,7 +41,8 @@ func SubscriptionGetResponse() []byte { "updated_at": "2021-08-11T13:47:28.000000Z" }, "urls": { - "update_payment_method": "https://app.lemonsqueezy.com/my-orders/2ba92a4e-a00a-45d2-a128-16856ffa8cdf/subscription/8/update-payment-method?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413" + "update_payment_method": "https://my-store.lemonsqueezy.com/subscription/1/payment-details?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413", + "customer_portal": "https://my-store.lemonsqueezy.com/billing?expires=1666869343&signature=82ae290ceac8edd4190c82825dd73a8743346d894a8ddbc4898b97eb96d105a5" }, "renews_at": "2022-11-12T00:00:00.000000Z", "ends_at": null, @@ -146,7 +147,8 @@ func SubscriptionUpdateResponse() []byte { "updated_at": "2021-08-11T13:47:28.000000Z" }, "urls": { - "update_payment_method": "https://app.lemonsqueezy.com/my-orders/2ba92a4e-a00a-45d2-a128-16856ffa8cdf/subscription/8/update-payment-method?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413" + "update_payment_method": "https://my-store.lemonsqueezy.com/subscription/1/payment-details?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413", + "customer_portal": "https://my-store.lemonsqueezy.com/billing?expires=1666869343&signature=82ae290ceac8edd4190c82825dd73a8743346d894a8ddbc4898b97eb96d105a5" }, "renews_at": "2022-11-12T00:00:00.000000Z", "ends_at": null, @@ -210,7 +212,8 @@ func SubscriptionsListResponse() []byte { "updated_at": "2021-08-11T13:47:28.000000Z" }, "urls":{ - "update_payment_method":"https://app.lemonsqueezy.com/my-orders/2ba92a4e-a00a-45d2-a128-16856ffa8cdf/subscription/8/update-payment-method?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413" + "update_payment_method": "https://my-store.lemonsqueezy.com/subscription/1/payment-details?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413", + "customer_portal": "https://my-store.lemonsqueezy.com/billing?expires=1666869343&signature=82ae290ceac8edd4190c82825dd73a8743346d894a8ddbc4898b97eb96d105a5" }, "renews_at":"2022-11-12T00:00:00.000000Z", "ends_at":null, @@ -301,7 +304,8 @@ func SubscriptionsListResponse() []byte { "updated_at": "2021-08-11T13:47:28.000000Z" }, "urls":{ - "update_payment_method":"https://app.lemonsqueezy.com/my-orders/2ba92a4e-a00a-45d2-a128-16856ffa8cdf/subscription/8/update-payment-method?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413" + "update_payment_method": "https://my-store.lemonsqueezy.com/subscription/2/payment-details?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413", + "customer_portal": "https://my-store.lemonsqueezy.com/billing?expires=1666869343&signature=82ae290ceac8edd4190c82825dd73a8743346d894a8ddbc4898b97eb96d105a5" }, "renews_at":"2022-11-12T00:00:00.000000Z", "ends_at":null, @@ -407,7 +411,8 @@ func SubscriptionCancelResponse() []byte { "updated_at": "2021-08-11T13:47:28.000000Z" }, "urls": { - "update_payment_method": "https://app.lemonsqueezy.com/my-orders/2ba92a4e-a00a-45d2-a128-16856ffa8cdf/subscription/8/update-payment-method?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413" + "update_payment_method": "https://my-store.lemonsqueezy.com/subscription/1/payment-details?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413", + "customer_portal": "https://my-store.lemonsqueezy.com/billing?expires=1666869343&signature=82ae290ceac8edd4190c82825dd73a8743346d894a8ddbc4898b97eb96d105a5" }, "renews_at": "2022-11-12T00:00:00.000000Z", "ends_at": "2022-11-12T00:00:00.000000Z", diff --git a/subscription_items.go b/subscription_items.go index 8bd183c..5bf37f1 100644 --- a/subscription_items.go +++ b/subscription_items.go @@ -5,7 +5,6 @@ import "time" // In Lemon Squeezy A subscription item is an object that links a price to a subscription and also contains quantity information. // https://docs.lemonsqueezy.com/api/subscription-items#the-subscription-item-object type SubscriptionItem struct { - ID int `json:"id"` SubscriptionID int `json:"subscription_id"` PriceID int `json:"price_id"` Quantity int `json:"quantity"` @@ -20,12 +19,26 @@ type SubscriptionItemUpdateParams struct { Attributes SubscriptionItemUpdateParamsAttributes `json:"attributes"` } +// SubscriptionItemListParams are parameters for filtering list responses +type SubscriptionItemListParams struct { + SubscriptionID string + PriceID string +} + // SubscriptionUpdateParamsAttributes are subscription update attributes type SubscriptionItemUpdateParamsAttributes struct { Quantity int `json:"quantity,omitempty"` } -// ApiResponseRelationshipsSubscription relationships of a subscription object +type ApiResponseMetaSubscriptionItemCurrentUsage struct { + PeriodStart time.Time `json:"period_start"` + PeriodEnd time.Time `json:"period_end"` + Quantity int `json:"quantity"` + IntervalUnit string `json:"interval_unit"` + IntervalQuantity int `json:"interval_quantity"` +} + +// ApiResponseRelationshipsSubscription relationships of a subscription item object type ApiResponseRelationshipsSubscriptionItem struct { Subscription ApiResponseLinks `json:"subscription"` Price ApiResponseLinks `json:"price"` @@ -37,3 +50,9 @@ type SubscriptionItemApiResponse = ApiResponse[SubscriptionItem, ApiResponseRela // SubscriptionItemsApiResponse represents a list of subscription items api responses type SubscriptionItemsApiResponse = ApiResponseList[SubscriptionItem, ApiResponseRelationshipsSubscriptionItem] + +// SubscriptionItemsCurrentUsageApiResponse represents the subscription item's current usage api response +type SubscriptionItemCurrentUsageApiResponse struct { + Jsonapi ApiResponseJSONAPI `json:"jsonapi"` + Meta ApiResponseMetaSubscriptionItemCurrentUsage `json:"meta"` +} diff --git a/subscription_items_service.go b/subscription_items_service.go new file mode 100644 index 0000000..7e0f3e4 --- /dev/null +++ b/subscription_items_service.go @@ -0,0 +1,101 @@ +package lemonsqueezy + +import ( + "context" + "encoding/json" + "net/http" + "net/url" +) + +// SubscriptionItemsService is the API client for the `/subscription-items` endpoint +type SubscriptionItemsService service + +// Update a subscription item +// +// https://docs.lemonsqueezy.com/api/subscription-items#update-a-subscription-item +func (service *SubscriptionItemsService) Update(ctx context.Context, params *SubscriptionItemUpdateParams) (*SubscriptionItemApiResponse, *Response, error) { + payload := map[string]any{ + "data": map[string]any{ + "type": "subscription-items", + "id": params.ID, + "attributes": params.Attributes, + }, + } + + response, err := service.client.do(ctx, http.MethodPatch, "/v1/subscriptions-items/"+params.ID, payload) + if err != nil { + return nil, response, err + } + + subscriptionItem := new(SubscriptionItemApiResponse) + if err = json.Unmarshal(*response.Body, subscriptionItem); err != nil { + return nil, response, err + } + + return subscriptionItem, response, nil +} + +// List returns a paginated list of subscription items you can add extra query params to your request +// +// https://docs.lemonsqueezy.com/api/subscriptions#list-all-subscriptions +func (service *SubscriptionItemsService) List(ctx context.Context, queryParams map[string]string) (*SubscriptionItemsApiResponse, *Response, error) { + basePath := "/v1/subscription-items" + parsedURL, err := url.Parse(basePath) + if err != nil { + return nil, nil, err + } + + if queryParams != nil { + query := parsedURL.Query() + for key, val := range queryParams { + query.Add(key, val) + } + parsedURL.RawQuery = query.Encode() + } + + response, err := service.client.do(ctx, http.MethodGet, parsedURL.String()) + if err != nil { + return nil, response, err + } + + subscriptionItems := new(SubscriptionItemsApiResponse) + if err = json.Unmarshal(*response.Body, subscriptionItems); err != nil { + return nil, response, err + } + + return subscriptionItems, response, nil +} + +// Get returns the subscription item with the given ID. +// +// https://docs.lemonsqueezy.com/api/subscription-items#retrieve-a-subscription-item +func (service *SubscriptionItemsService) Get(ctx context.Context, subscriptionItemID string) (*SubscriptionItemApiResponse, *Response, error) { + response, err := service.client.do(ctx, http.MethodGet, "/v1/subscription-items/"+subscriptionItemID) + if err != nil { + return nil, response, err + } + + subscriptionItem := new(SubscriptionItemApiResponse) + if err = json.Unmarshal(*response.Body, subscriptionItem); err != nil { + return nil, response, err + } + + return subscriptionItem, response, nil +} + +// Current usage returns a subscription item's current usage with the given ID. +// +// https://docs.lemonsqueezy.com/api/subscription-items#retrieve-a-subscription-item-s-current-usage +func (service *SubscriptionItemsService) CurrentUsage(ctx context.Context, subscriptionItemID string) (*SubscriptionItemCurrentUsageApiResponse, *Response, error) { + response, err := service.client.do(ctx, http.MethodGet, "/v1/subscription-items/"+subscriptionItemID+"/current-usage") + if err != nil { + return nil, response, err + } + + subscriptionItemCurrentUsage := new(SubscriptionItemCurrentUsageApiResponse) + if err = json.Unmarshal(*response.Body, subscriptionItemCurrentUsage); err != nil { + return nil, response, err + } + + return subscriptionItemCurrentUsage, response, nil +} diff --git a/subscription_items_service_test.go b/subscription_items_service_test.go new file mode 100644 index 0000000..28610d0 --- /dev/null +++ b/subscription_items_service_test.go @@ -0,0 +1,242 @@ +package lemonsqueezy + +import ( + "context" + "net/http" + "testing" + "time" + + "github.com/NdoleStudio/lemonsqueezy-go/internal/helpers" + "github.com/NdoleStudio/lemonsqueezy-go/internal/stubs" + "github.com/stretchr/testify/assert" +) + +func TestSubscriptionItemsService_Get(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + server := helpers.MakeTestServer(http.StatusOK, stubs.SubscriptionItemGetResponse()) + client := New(WithBaseURL(server.URL)) + + // Act + subscriptionItem, response, err := client.SubscriptionItems.Get(context.Background(), "1") + + // Assert + assert.Nil(t, err) + + assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + assert.Equal(t, stubs.SubscriptionItemGetResponse(), *response.Body) + + assert.Equal(t, &SubscriptionItemApiResponse{ + Jsonapi: ApiResponseJSONAPI{ + Version: "1.0", + }, + Links: ApiResponseSelfLink{ + Self: "https://api.lemonsqueezy.com/v1/subscription-item/1", + }, + Data: ApiResponseData[SubscriptionItem, ApiResponseRelationshipsSubscriptionItem]{ + Type: "subscription-items", + ID: "1", + Attributes: SubscriptionItem{ + SubscriptionID: 1, + PriceID: 1, + Quantity: 1, + IsUsageBased: false, + CreatedAt: time.Date(2023, time.July, 18, 12, 16, 24, 0, time.UTC), + UpdatedAt: time.Date(2023, time.July, 18, 12, 16, 24, 0, time.UTC), + }, + Relationships: ApiResponseRelationshipsSubscriptionItem{ + Subscription: ApiResponseLinks{ + Links: ApiResponseLink{ + Related: "https://api.lemonsqueezy.com/v1/subscription-items/1/subscription", + Self: "https://api.lemonsqueezy.com/v1/subscription-items/1/relationships/subscription", + }, + }, + Price: ApiResponseLinks{ + Links: ApiResponseLink{ + Related: "https://api.lemonsqueezy.com/v1/subscription-items/1/price", + Self: "https://api.lemonsqueezy.com/v1/subscription-items/1/relationships/price", + }, + }, + UsageRecords: ApiResponseLinks{ + Links: ApiResponseLink{ + Related: "https://api.lemonsqueezy.com/v1/subscription-items/1/usage-records", + Self: "https://api.lemonsqueezy.com/v1/subscription-items/1/relationships/usage-records", + }, + }, + }, + Links: ApiResponseSelfLink{ + Self: "https://api.lemonsqueezy.com/v1/subscription-items/1", + }, + }, + }, subscriptionItem) + + // Teardown + server.Close() +} + +func TestSubscriptionItemsService_GetWithError(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + server := helpers.MakeTestServer(http.StatusInternalServerError, nil) + client := New(WithBaseURL(server.URL)) + + // Act + _, response, err := client.SubscriptionItems.Get(context.Background(), "1") + + // Assert + assert.NotNil(t, err) + + assert.Equal(t, http.StatusInternalServerError, response.HTTPResponse.StatusCode) + + // Teardown + server.Close() +} + +func TestSubscriptionItemsService_Update(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + server := helpers.MakeTestServer(http.StatusOK, stubs.SubscriptionItemUpdateResponse()) + client := New(WithBaseURL(server.URL)) + + // Act + subscriptionItem, response, err := client.SubscriptionItems.Update(context.Background(), &SubscriptionItemUpdateParams{ + ID: "1", + Attributes: SubscriptionItemUpdateParamsAttributes{ + Quantity: 10, + }, + }) + + // Assert + assert.Nil(t, err) + + assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + assert.Equal(t, stubs.SubscriptionItemUpdateResponse(), *response.Body) + assert.Equal(t, "1", subscriptionItem.Data.ID) + assert.Equal(t, 10, subscriptionItem.Data.Attributes.Quantity) + + // Teardown + server.Close() +} + +func TestSubscriptionItemsService_UpdateWithError(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + server := helpers.MakeTestServer(http.StatusInternalServerError, nil) + client := New(WithBaseURL(server.URL)) + + // Act + _, response, err := client.Subscriptions.Update(context.Background(), &SubscriptionUpdateParams{}) + + // Assert + assert.NotNil(t, err) + + assert.Equal(t, http.StatusInternalServerError, response.HTTPResponse.StatusCode) + + // Teardown + server.Close() +} + +func TestSubscriptionItemsService_List(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + server := helpers.MakeTestServer(http.StatusOK, stubs.SubscriptionItemsListResponse()) + client := New(WithBaseURL(server.URL)) + + // Act + subscriptionItems, response, err := client.SubscriptionItems.List(context.Background(), nil) + + // Assert + assert.Nil(t, err) + + assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + assert.Equal(t, stubs.SubscriptionItemsListResponse(), *response.Body) + assert.Equal(t, 1, len(subscriptionItems.Data)) + assert.Equal(t, "1", subscriptionItems.Data[0].ID) + + // Teardown + server.Close() +} + +func TestSubscriptionItemsService_ListWithError(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + server := helpers.MakeTestServer(http.StatusInternalServerError, nil) + client := New(WithBaseURL(server.URL)) + + // Act + _, response, err := client.SubscriptionItems.List(context.Background(), nil) + + // Assert + assert.NotNil(t, err) + + assert.Equal(t, http.StatusInternalServerError, response.HTTPResponse.StatusCode) + + // Teardown + server.Close() +} + +func TestSubscriptionItemsService_CurrentUsage(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + server := helpers.MakeTestServer(http.StatusOK, stubs.SubscriptionItemCurrentUsageResponse()) + client := New(WithBaseURL(server.URL)) + + // Act + subscriptionItemCurrentUsage, response, err := client.SubscriptionItems.CurrentUsage(context.Background(), "1") + + // Assert + assert.Nil(t, err) + + assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + assert.Equal(t, stubs.SubscriptionItemCurrentUsageResponse(), *response.Body) + + assert.Equal(t, &SubscriptionItemCurrentUsageApiResponse{ + Jsonapi: ApiResponseJSONAPI{ + Version: "1.0", + }, + Meta: ApiResponseMetaSubscriptionItemCurrentUsage{ + PeriodStart: time.Date(2023, time.August, 10, 13, 8, 16, 0, time.UTC), + PeriodEnd: time.Date(2023, time.September, 10, 13, 3, 16, 0, time.UTC), + Quantity: 5, + IntervalUnit: "month", + IntervalQuantity: 1, + }, + }, subscriptionItemCurrentUsage) + + // Teardown + server.Close() +} + +func TestSubscriptionItemsService_CurrentUsageWithError(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + server := helpers.MakeTestServer(http.StatusInternalServerError, nil) + client := New(WithBaseURL(server.URL)) + + // Act + _, response, err := client.SubscriptionItems.CurrentUsage(context.Background(), "1") + + // Assert + assert.NotNil(t, err) + + assert.Equal(t, http.StatusInternalServerError, response.HTTPResponse.StatusCode) + + // Teardown + server.Close() +} diff --git a/subscriptions.go b/subscriptions.go index 06e49d1..3468ef7 100644 --- a/subscriptions.go +++ b/subscriptions.go @@ -5,36 +5,43 @@ import "time" // Subscription is created when a subscription product is purchased and will bill the customer on a recurring basis. // https://docs.lemonsqueezy.com/api/subscriptions#the-subscription-object type Subscription struct { - StoreID int `json:"store_id"` - CustomerID int `json:"customer_id"` - OrderID int `json:"order_id"` - OrderItemID int `json:"order_item_id"` - ProductID int `json:"product_id"` - VariantID int `json:"variant_id"` - ProductName string `json:"product_name"` - VariantName string `json:"variant_name"` - UserName string `json:"user_name"` - UserEmail string `json:"user_email"` - Status string `json:"status"` - StatusFormatted string `json:"status_formatted"` - CardBrand string `json:"card_brand"` - CardLastFour string `json:"card_last_four"` - Pause *SubscriptionPause `json:"pause"` - Cancelled bool `json:"cancelled"` - TrialEndsAt *time.Time `json:"trial_ends_at"` - BillingAnchor int `json:"billing_anchor"` - FirstSubscriptionItem *SubscriptionItem `json:"first_subscription_item"` - Urls SubscriptionURLs `json:"urls"` - RenewsAt time.Time `json:"renews_at"` - EndsAt *time.Time `json:"ends_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - TestMode bool `json:"test_mode"` + StoreID int `json:"store_id"` + CustomerID int `json:"customer_id"` + OrderID int `json:"order_id"` + OrderItemID int `json:"order_item_id"` + ProductID int `json:"product_id"` + VariantID int `json:"variant_id"` + ProductName string `json:"product_name"` + VariantName string `json:"variant_name"` + UserName string `json:"user_name"` + UserEmail string `json:"user_email"` + Status string `json:"status"` + StatusFormatted string `json:"status_formatted"` + CardBrand string `json:"card_brand"` + CardLastFour string `json:"card_last_four"` + Pause *SubscriptionPause `json:"pause"` + Cancelled bool `json:"cancelled"` + TrialEndsAt *time.Time `json:"trial_ends_at"` + BillingAnchor int `json:"billing_anchor"` + FirstSubscriptionItem *SubscriptionFirstSubscriptionItem `json:"first_subscription_item"` + Urls SubscriptionURLs `json:"urls"` + RenewsAt time.Time `json:"renews_at"` + EndsAt *time.Time `json:"ends_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + TestMode bool `json:"test_mode"` +} + +// SubscriptionSubscriptionItem is an object representing the first subscription item belonging to this subscription. +type SubscriptionFirstSubscriptionItem struct { + ID int `json:"id"` + SubscriptionItem } // SubscriptionURLs is object of customer-facing URLs for managing the subscription. type SubscriptionURLs struct { UpdatePaymentMethod string `json:"update_payment_method"` + CustomerPortal string `json:"customer_portal"` } // SubscriptionPause is object of customer-facing URLs for managing the subscription. @@ -72,8 +79,8 @@ type ApiResponseRelationshipsSubscription struct { SubscriptionInvoices ApiResponseLinks `json:"subscription-invoices"` } -// ApiResponseSubscription represents a subscription api response -type ApiResponseSubscription = ApiResponse[Subscription, ApiResponseRelationshipsSubscription] +// SubscriptionApiResponse represents a subscription api response +type SubscriptionApiResponse = ApiResponse[Subscription, ApiResponseRelationshipsSubscription] -// ApiResponseSubscriptionList represents a list of subscription api responses. -type ApiResponseSubscriptionList = ApiResponseList[Subscription, ApiResponseRelationshipsSubscription] +// SubscriptionsApiResponse represents a list of subscription api responses. +type SubscriptionsApiResponse = ApiResponseList[Subscription, ApiResponseRelationshipsSubscription] diff --git a/subscriptions_service.go b/subscriptions_service.go index 92451d5..c9405ec 100644 --- a/subscriptions_service.go +++ b/subscriptions_service.go @@ -12,7 +12,7 @@ type SubscriptionsService service // Update an existing subscription to specific parameter // // https://docs.lemonsqueezy.com/api/subscriptions#update-a-subscription -func (service *SubscriptionsService) Update(ctx context.Context, params *SubscriptionUpdateParams) (*ApiResponseSubscription, *Response, error) { +func (service *SubscriptionsService) Update(ctx context.Context, params *SubscriptionUpdateParams) (*SubscriptionApiResponse, *Response, error) { payload := map[string]any{ "data": map[string]any{ "id": params.ID, @@ -26,7 +26,7 @@ func (service *SubscriptionsService) Update(ctx context.Context, params *Subscri return nil, response, err } - subscription := new(ApiResponseSubscription) + subscription := new(SubscriptionApiResponse) if err = json.Unmarshal(*response.Body, subscription); err != nil { return nil, response, err } @@ -37,13 +37,13 @@ func (service *SubscriptionsService) Update(ctx context.Context, params *Subscri // List returns a paginated list of subscriptions ordered by created_at (descending) // // https://docs.lemonsqueezy.com/api/subscriptions#list-all-subscriptions -func (service *SubscriptionsService) List(ctx context.Context) (*ApiResponseSubscriptionList, *Response, error) { +func (service *SubscriptionsService) List(ctx context.Context) (*SubscriptionsApiResponse, *Response, error) { response, err := service.client.do(ctx, http.MethodGet, "/v1/subscriptions") if err != nil { return nil, response, err } - subscriptions := new(ApiResponseSubscriptionList) + subscriptions := new(SubscriptionsApiResponse) if err = json.Unmarshal(*response.Body, subscriptions); err != nil { return nil, response, err } @@ -54,13 +54,13 @@ func (service *SubscriptionsService) List(ctx context.Context) (*ApiResponseSubs // Get returns the subscription with the given ID. // // https://docs.lemonsqueezy.com/api/subscriptions#retrieve-a-subscription -func (service *SubscriptionsService) Get(ctx context.Context, subscriptionID string) (*ApiResponseSubscription, *Response, error) { +func (service *SubscriptionsService) Get(ctx context.Context, subscriptionID string) (*SubscriptionApiResponse, *Response, error) { response, err := service.client.do(ctx, http.MethodGet, "/v1/subscriptions/"+subscriptionID) if err != nil { return nil, response, err } - subscription := new(ApiResponseSubscription) + subscription := new(SubscriptionApiResponse) if err = json.Unmarshal(*response.Body, subscription); err != nil { return nil, response, err } @@ -71,13 +71,13 @@ func (service *SubscriptionsService) Get(ctx context.Context, subscriptionID str // Cancel an active subscription the given ID. // // https://docs.lemonsqueezy.com/api/subscriptions#retrieve-a-subscription -func (service *SubscriptionsService) Cancel(ctx context.Context, subscriptionID string) (*ApiResponseSubscription, *Response, error) { +func (service *SubscriptionsService) Cancel(ctx context.Context, subscriptionID string) (*SubscriptionApiResponse, *Response, error) { response, err := service.client.do(ctx, http.MethodDelete, "/v1/subscriptions/"+subscriptionID) if err != nil { return nil, response, err } - subscription := new(ApiResponseSubscription) + subscription := new(SubscriptionApiResponse) if err = json.Unmarshal(*response.Body, subscription); err != nil { return nil, response, err } diff --git a/subscriptions_service_test.go b/subscriptions_service_test.go index 79fb753..0b4997c 100644 --- a/subscriptions_service_test.go +++ b/subscriptions_service_test.go @@ -28,7 +28,7 @@ func TestSubscriptionsService_Get(t *testing.T) { assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) assert.Equal(t, stubs.SubscriptionGetResponse(), *response.Body) - assert.Equal(t, &ApiResponseSubscription{ + assert.Equal(t, &SubscriptionApiResponse{ Jsonapi: ApiResponseJSONAPI{ Version: "1.0", }, @@ -57,16 +57,19 @@ func TestSubscriptionsService_Get(t *testing.T) { Cancelled: false, TrialEndsAt: nil, BillingAnchor: 12, - FirstSubscriptionItem: &SubscriptionItem{ - ID: 1, - SubscriptionID: 1, - PriceID: 1, - Quantity: 5, - CreatedAt: time.Date(2021, time.August, 11, 13, 47, 28, 0, time.UTC), - UpdatedAt: time.Date(2021, time.August, 11, 13, 47, 28, 0, time.UTC), + FirstSubscriptionItem: &SubscriptionFirstSubscriptionItem{ + ID: 1, + SubscriptionItem: SubscriptionItem{ + SubscriptionID: 1, + PriceID: 1, + Quantity: 5, + CreatedAt: time.Date(2021, time.August, 11, 13, 47, 28, 0, time.UTC), + UpdatedAt: time.Date(2021, time.August, 11, 13, 47, 28, 0, time.UTC), + }, }, Urls: SubscriptionURLs{ - UpdatePaymentMethod: "https://app.lemonsqueezy.com/my-orders/2ba92a4e-a00a-45d2-a128-16856ffa8cdf/subscription/8/update-payment-method?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413", + UpdatePaymentMethod: "https://my-store.lemonsqueezy.com/subscription/1/payment-details?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413", + CustomerPortal: "https://my-store.lemonsqueezy.com/billing?expires=1666869343&signature=82ae290ceac8edd4190c82825dd73a8743346d894a8ddbc4898b97eb96d105a5", }, RenewsAt: time.Date(2022, time.November, 12, 0, 0, 0, 0, time.UTC), EndsAt: nil, @@ -171,7 +174,7 @@ func TestSubscriptionsService_Cancel(t *testing.T) { assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) assert.Equal(t, stubs.SubscriptionCancelResponse(), *response.Body) - assert.Equal(t, &ApiResponseSubscription{ + assert.Equal(t, &SubscriptionApiResponse{ Jsonapi: ApiResponseJSONAPI{ Version: "1.0", }, @@ -198,16 +201,19 @@ func TestSubscriptionsService_Cancel(t *testing.T) { Cancelled: true, TrialEndsAt: nil, BillingAnchor: 12, - FirstSubscriptionItem: &SubscriptionItem{ - ID: 1, - SubscriptionID: 1, - PriceID: 1, - Quantity: 5, - CreatedAt: time.Date(2021, time.August, 11, 13, 47, 28, 0, time.UTC), - UpdatedAt: time.Date(2021, time.August, 11, 13, 47, 28, 0, time.UTC), + FirstSubscriptionItem: &SubscriptionFirstSubscriptionItem{ + ID: 1, + SubscriptionItem: SubscriptionItem{ + SubscriptionID: 1, + PriceID: 1, + Quantity: 5, + CreatedAt: time.Date(2021, time.August, 11, 13, 47, 28, 0, time.UTC), + UpdatedAt: time.Date(2021, time.August, 11, 13, 47, 28, 0, time.UTC), + }, }, Urls: SubscriptionURLs{ - UpdatePaymentMethod: "https://app.lemonsqueezy.com/my-orders/2ba92a4e-a00a-45d2-a128-16856ffa8cdf/subscription/8/update-payment-method?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413", + UpdatePaymentMethod: "https://my-store.lemonsqueezy.com/subscription/1/payment-details?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413", + CustomerPortal: "https://my-store.lemonsqueezy.com/billing?expires=1666869343&signature=82ae290ceac8edd4190c82825dd73a8743346d894a8ddbc4898b97eb96d105a5", }, RenewsAt: time.Date(2022, time.November, 12, 0, 0, 0, 0, time.UTC), EndsAt: (func() *time.Time {