Skip to content

Commit

Permalink
Implemented all subscription endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
AchoArnold committed Jan 7, 2023
1 parent e5c0297 commit e0f12f8
Show file tree
Hide file tree
Showing 10 changed files with 438 additions and 67 deletions.
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
repos:
- repo: https://github.com/tekwizely/pre-commit-golang
rev: master
rev: v1.0.0-rc.1
hooks:
- id: go-fumpt
- id: go-mod-tidy
- id: go-lint
- id: go-imports
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
rev: v4.4.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import "github.com/NdoleStudio/lemonsqueezy-go"
## Implemented

- **Subscriptions**
- `PATCH /v1/subscriptions/:id`: Update a subscription
- `GET /v1/subscriptions/:id`: Retrieve a subscription
- `GET /v1/subscriptions`: List all subscriptions
- `DELETE /v1/subscriptions/{id}`: Cancel an active subscription
- **Webhooks**
- `Verify`: Verify that webhook requests are coming from Lemon Squeezy
Expand Down
8 changes: 4 additions & 4 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ type Client struct {
apiKey string
signingSecret string

Webhooks *webhooksService
Subscriptions *subscriptionsService
Webhooks *WebhooksService
Subscriptions *SubscriptionsService
}

// New creates and returns a new Client from a slice of Option.
Expand All @@ -42,8 +42,8 @@ func New(options ...Option) *Client {
}

client.common.client = client
client.Subscriptions = (*subscriptionsService)(&client.common)
client.Webhooks = (*webhooksService)(&client.common)
client.Subscriptions = (*SubscriptionsService)(&client.common)
client.Webhooks = (*WebhooksService)(&client.common)

return client
}
Expand Down
63 changes: 45 additions & 18 deletions contracts.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
package client

// ApiResponse represents an API response
type ApiResponse[T any] struct {
Jsonapi ApiResponseJSONAPI `json:"jsonapi"`
Links ApiResponseLink `json:"links"`
Data ApiResponseData[T] `json:"data"`
type ApiResponse[T any, R any] struct {
Jsonapi ApiResponseJSONAPI `json:"jsonapi"`
Links ApiResponseLink `json:"links"`
Data ApiResponseData[T, R] `json:"data"`
}

// ApiResponseList represents an API response with a list of items
type ApiResponseList[T any, R any] struct {
Jsonapi ApiResponseJSONAPI `json:"jsonapi"`
Links ApiResponseListLink `json:"links"`
Meta ApiResponseListMeta `json:"meta"`
Data []ApiResponseData[T, R] `json:"data"`
}

// ApiResponseData contains the api response data
type ApiResponseData[T any] struct {
Type string `json:"type"`
ID string `json:"id"`
Attributes T `json:"attributes"`
Relationships ApiResponseRelationships `json:"relationships"`
Links ApiResponseLink `json:"links"`
type ApiResponseData[T any, R any] struct {
Type string `json:"type"`
ID string `json:"id"`
Attributes T `json:"attributes"`
Relationships R `json:"relationships"`
Links ApiResponseLink `json:"links"`
}

// Resource returns a lemonsqueezy resource. It's similar to ApiResponseData but without the links
type Resource[T any] struct {
Type string `json:"type"`
ID string `json:"id"`
Attributes T `json:"attributes"`
}

// ApiResponseJSONAPI API version
Expand All @@ -28,15 +43,27 @@ type ApiResponseLinks struct {

// ApiResponseLink defines a link
type ApiResponseLink struct {
Related string `json:"related"`
Related string `json:"related,omitempty"`
Self string `json:"self"`
}

// ApiResponseRelationships relationships of an object
type ApiResponseRelationships struct {
Store ApiResponseLinks `json:"store"`
Order ApiResponseLinks `json:"order"`
OrderItem ApiResponseLinks `json:"order-item"`
Product ApiResponseLinks `json:"product"`
Variant ApiResponseLinks `json:"variant"`
// ApiResponseListLink defines a link for list os resources
type ApiResponseListLink struct {
First string `json:"first"`
Last string `json:"last"`
}

// ApiResponseListMeta defines the meta data for a list api response
type ApiResponseListMeta struct {
Page ApiResponseListMetaPage `json:"page"`
}

// ApiResponseListMetaPage defines the pagination meta data for a list api response
type ApiResponseListMetaPage struct {
CurrentPage int `json:"currentPage"`
From int `json:"from"`
LastPage int `json:"lastPage"`
PerPage int `json:"perPage"`
To int `json:"to"`
Total int `json:"total"`
}
33 changes: 9 additions & 24 deletions internal/helpers/test_helper.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package helpers

import (
"bytes"
"context"
"io"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

// MakeTestServer creates an api server for testing
Expand All @@ -19,25 +20,9 @@ func MakeTestServer(responseCode int, body []byte) *httptest.Server {
}))
}

// MakeRequestCapturingTestServer creates an api server that captures the request object
func MakeRequestCapturingTestServer(responseCode int, response []byte, request *http.Request) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, req *http.Request) {
clonedRequest := req.Clone(context.Background())

// clone body
body, err := io.ReadAll(req.Body)
if err != nil {
panic(err)
}
req.Body = io.NopCloser(bytes.NewReader(body))
clonedRequest.Body = io.NopCloser(bytes.NewReader(body))

*request = *clonedRequest

responseWriter.WriteHeader(responseCode)
_, err = responseWriter.Write(response)
if err != nil {
panic(err)
}
}))
// AssertObjectEqualsJSON checks if the JSON representation of an object matches the expected value
func AssertObjectEqualsJSON(t *testing.T, expectedJSON []byte, actual any) {
actualJSON, err := json.Marshal(actual)
assert.Nil(t, err)
assert.JSONEq(t, string(expectedJSON), string(actualJSON))
}
190 changes: 190 additions & 0 deletions internal/stubs/subscriptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,196 @@ func SubscriptionGetResponse() []byte {
`)
}

// SubscriptionUpdateResponse is a dummy response to the PATCH /v1/subscriptions/:id endpoint
func SubscriptionUpdateResponse() []byte {
return []byte(`
{
"type": "subscriptions",
"id": "1",
"attributes": {
"store_id": 1,
"order_id": 1,
"order_item_id": 1,
"product_id": 9,
"variant_id": 11,
"product_name": "New Plan Product",
"variant_name": "New Plan Variant",
"user_name": "Darlene Daugherty",
"user_email": "[email protected]",
"status": "active",
"status_formatted": "Active",
"pause": null,
"cancelled": false,
"trial_ends_at": null,
"billing_anchor": 29,
"urls": {
"update_payment_method": "https://app.lemonsqueezy.com/my-orders/2ba92a4e-a00a-45d2-a128-16856ffa8cdf/subscription/8/update-payment-method?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413"
},
"renews_at": "2022-11-12T00:00:00.000000Z",
"ends_at": null,
"created_at": "2021-08-11T13:47:27.000000Z",
"updated_at": "2021-08-11T13:54:19.000000Z",
"test_mode": false
}
}
`)
}

// SubscriptionsListResponse returns a list of subscription responses ordered by created at
func SubscriptionsListResponse() []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/subscriptions?page%5Bnumber%5D=1&page%5Bsize%5D=10&sort=-createdAt",
"last":"https://api.lemonsqueezy.com/v1/subscriptions?page%5Bnumber%5D=1&page%5Bsize%5D=10&sort=-createdAt"
},
"data":[
{
"type":"subscriptions",
"id":"1",
"attributes":{
"store_id":1,
"order_id":1,
"order_item_id":1,
"product_id":1,
"variant_id":1,
"product_name":"Example Product",
"variant_name":"Example Variant",
"user_name":"Darlene Daugherty",
"user_email":"[email protected]",
"status":"active",
"status_formatted":"Active",
"pause":null,
"cancelled":false,
"trial_ends_at":null,
"billing_anchor":12,
"urls":{
"update_payment_method":"https://app.lemonsqueezy.com/my-orders/2ba92a4e-a00a-45d2-a128-16856ffa8cdf/subscription/8/update-payment-method?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413"
},
"renews_at":"2022-11-12T00:00:00.000000Z",
"ends_at":null,
"created_at":"2021-08-11T13:47:27.000000Z",
"updated_at":"2021-08-11T13:54:19.000000Z",
"test_mode":false
},
"relationships":{
"store":{
"links":{
"related":"https://api.lemonsqueezy.com/v1/subscriptions/1/store",
"self":"https://api.lemonsqueezy.com/v1/subscriptions/1/relationships/store"
}
},
"order":{
"links":{
"related":"https://api.lemonsqueezy.com/v1/subscriptions/1/order",
"self":"https://api.lemonsqueezy.com/v1/subscriptions/1/relationships/order"
}
},
"order-item":{
"links":{
"related":"https://api.lemonsqueezy.com/v1/subscriptions/1/order-item",
"self":"https://api.lemonsqueezy.com/v1/subscriptions/1/relationships/order-item"
}
},
"product":{
"links":{
"related":"https://api.lemonsqueezy.com/v1/subscriptions/1/product",
"self":"https://api.lemonsqueezy.com/v1/subscriptions/1/relationships/product"
}
},
"variant":{
"links":{
"related":"https://api.lemonsqueezy.com/v1/subscriptions/1/variant",
"self":"https://api.lemonsqueezy.com/v1/subscriptions/1/relationships/variant"
}
}
},
"links":{
"self":"https://api.lemonsqueezy.com/v1/subscriptions/1"
}
},
{
"type":"subscriptions",
"id":"2",
"attributes":{
"store_id":2,
"order_id":2,
"order_item_id":2,
"product_id":2,
"variant_id":2,
"product_name":"Example Product 2",
"variant_name":"Example Variant 2",
"user_name":"Darlene Daugherty 2",
"user_email":"[email protected]",
"status":"active",
"status_formatted":"Active",
"pause":null,
"cancelled":false,
"trial_ends_at":null,
"billing_anchor":13,
"urls":{
"update_payment_method":"https://app.lemonsqueezy.com/my-orders/2ba92a4e-a00a-45d2-a128-16856ffa8cdf/subscription/8/update-payment-method?expires=1666869343&signature=9985e3bf9007840aeb3951412be475abc17439c449c1af3e56e08e45e1345413"
},
"renews_at":"2022-11-12T00:00:00.000000Z",
"ends_at":null,
"created_at":"2021-08-11T13:47:27.000000Z",
"updated_at":"2021-08-11T13:54:19.000000Z",
"test_mode":false
},
"relationships":{
"store":{
"links":{
"related":"https://api.lemonsqueezy.com/v1/subscriptions/1/store",
"self":"https://api.lemonsqueezy.com/v1/subscriptions/1/relationships/store"
}
},
"order":{
"links":{
"related":"https://api.lemonsqueezy.com/v1/subscriptions/1/order",
"self":"https://api.lemonsqueezy.com/v1/subscriptions/1/relationships/order"
}
},
"order-item":{
"links":{
"related":"https://api.lemonsqueezy.com/v1/subscriptions/1/order-item",
"self":"https://api.lemonsqueezy.com/v1/subscriptions/1/relationships/order-item"
}
},
"product":{
"links":{
"related":"https://api.lemonsqueezy.com/v1/subscriptions/1/product",
"self":"https://api.lemonsqueezy.com/v1/subscriptions/1/relationships/product"
}
},
"variant":{
"links":{
"related":"https://api.lemonsqueezy.com/v1/subscriptions/1/variant",
"self":"https://api.lemonsqueezy.com/v1/subscriptions/1/relationships/variant"
}
}
},
"links":{
"self":"https://api.lemonsqueezy.com/v1/subscriptions/1"
}
}
]
}
`)
}

// SubscriptionCancelResponse returns a dummy response to DELETE /v1/subscriptions/:id endpoint
func SubscriptionCancelResponse() []byte {
return []byte(`
Expand Down
Loading

0 comments on commit e0f12f8

Please sign in to comment.