Skip to content

Commit

Permalink
Implement /v1/products* enpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
AchoArnold committed Jan 21, 2023
1 parent 40456ee commit 6e8bbe7
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 2 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ import "github.com/NdoleStudio/lemonsqueezy-go"
- **Customers**
- `GET /v1/customers/:id`: Retrieve a customer
- `GET /v1/customers`: List all customers
- **Products**
- `GET /v1/products/:id`: Retrieve a product
- `GET /v1/products`: List all products
- **Subscriptions**
- `PATCH /v1/subscriptions/:id`: Update a subscription
- `GET /v1/subscriptions/:id`: Retrieve a subscription
Expand Down
2 changes: 2 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Client struct {
Users *UsersService
Stores *StoresService
Customers *CustomersService
Products *ProductsService
}

// New creates and returns a new Client from a slice of Option.
Expand All @@ -49,6 +50,7 @@ func New(options ...Option) *Client {
client.Users = (*UsersService)(&client.common)
client.Stores = (*StoresService)(&client.common)
client.Customers = (*CustomersService)(&client.common)
client.Products = (*ProductsService)(&client.common)

return client
}
Expand Down
2 changes: 1 addition & 1 deletion customers_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"net/http"
)

// CustomersService is the API client for the `/v1/stores` endpoint
// CustomersService is the API client for the `/v1/customers` endpoint
type CustomersService service

// Get returns the customer with the given ID.
Expand Down
2 changes: 1 addition & 1 deletion customers_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func TestCustomersService_ListWithError(t *testing.T) {
client := New(WithBaseURL(server.URL))

// Act
_, response, err := client.Stores.List(context.Background())
_, response, err := client.Customers.List(context.Background())

// Assert
assert.NotNil(t, err)
Expand Down
159 changes: 159 additions & 0 deletions internal/stubs/product.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package stubs

// ProductGetResponse is a dummy response to the GET /v1/products/:id endpoint
func ProductGetResponse() []byte {
return []byte(`
{
"jsonapi": {
"version": "1.0"
},
"links": {
"self": "https://api.lemonsqueezy.com/v1/products/1"
},
"data": {
"type": "products",
"id": "1",
"attributes": {
"store_id": 1,
"name": "Example Product",
"slug": "example-product",
"description": "<p>Lorem ipsum...</p>",
"status": "published",
"status_formatted": "Published",
"thumb_url": "https://app.lemonsqueezy.com/storage/media/1/1c40f227-aed5-4321-9ffc-62f37a06c9a0.jpg",
"large_thumb_url": "https://app.lemonsqueezy.com/storage/media/1/1c40f227-aed5-4321-9ffc-62f37a06c9a0.jpg",
"price": 999,
"pay_what_you_want": false,
"from_price": null,
"to_price": null,
"buy_now_url": "https://my-store.lemonsqueezy.com/checkout/buy/0a841cf5-4cc2-4bbb-ae5d-9529d97deec6",
"price_formatted": "$9.99",
"created_at": "2021-05-27T12:54:47.000000Z",
"updated_at": "2021-07-14T11:25:24.000000Z"
},
"relationships": {
"store": {
"links": {
"related": "https://api.lemonsqueezy.com/v1/products/1/store",
"self": "https://api.lemonsqueezy.com/v1/products/1/relationships/store"
}
},
"variants": {
"links": {
"related": "https://api.lemonsqueezy.com/v1/products/1/variants",
"self": "https://api.lemonsqueezy.com/v1/products/1/relationships/variants"
}
}
},
"links": {
"self": "https://api.lemonsqueezy.com/v1/products/1"
}
}
}
`)
}

// ProductsListResponse is a dummy response to GET /v1/products
func ProductsListResponse() []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/products?page%5Bnumber%5D=1&page%5Bsize%5D=10&sort=name",
"last": "https://api.lemonsqueezy.com/v1/products?page%5Bnumber%5D=1&page%5Bsize%5D=10&sort=name"
},
"data": [
{
"type": "products",
"id": "1",
"attributes": {
"store_id": 1,
"name": "Example Product",
"slug": "example-product",
"description": "<p>Lorem ipsum...</p>",
"status": "published",
"status_formatted": "Published",
"thumb_url": "https://app.lemonsqueezy.com/storage/media/1/1c40f227-aed5-4321-9ffc-62f37a06c9a0.jpg",
"large_thumb_url": "https://app.lemonsqueezy.com/storage/media/1/1c40f227-aed5-4321-9ffc-62f37a06c9a0.jpg",
"price": 999,
"pay_what_you_want": false,
"from_price": null,
"to_price": null,
"buy_now_url": "https://my-store.lemonsqueezy.com/checkout/buy/0a841cf5-4cc2-4bbb-ae5d-9529d97deec6",
"price_formatted": "$9.99",
"created_at": "2021-05-27T12:54:47.000000Z",
"updated_at": "2021-07-14T11:25:24.000000Z"
},
"relationships": {
"store": {
"links": {
"related": "https://api.lemonsqueezy.com/v1/products/1/store",
"self": "https://api.lemonsqueezy.com/v1/products/1/relationships/store"
}
},
"variants": {
"links": {
"related": "https://api.lemonsqueezy.com/v1/products/1/variants",
"self": "https://api.lemonsqueezy.com/v1/products/1/relationships/variants"
}
}
},
"links": {
"self": "https://api.lemonsqueezy.com/v1/products/1"
}
},
{
"type": "products",
"id": "2",
"attributes": {
"store_id": 2,
"name": "Example Product",
"slug": "example-product",
"description": "<p>Lorem ipsum...</p>",
"status": "published",
"status_formatted": "Published",
"thumb_url": "https://app.lemonsqueezy.com/storage/media/1/1c40f227-aed5-4321-9ffc-62f37a06c9a0.jpg",
"large_thumb_url": "https://app.lemonsqueezy.com/storage/media/1/1c40f227-aed5-4321-9ffc-62f37a06c9a0.jpg",
"price": 999,
"pay_what_you_want": false,
"from_price": null,
"to_price": null,
"buy_now_url": "https://my-store.lemonsqueezy.com/checkout/buy/0a841cf5-4cc2-4bbb-ae5d-9529d97deec6",
"price_formatted": "$9.99",
"created_at": "2021-05-27T12:54:47.000000Z",
"updated_at": "2021-07-14T11:25:24.000000Z"
},
"relationships": {
"store": {
"links": {
"related": "https://api.lemonsqueezy.com/v1/products/1/store",
"self": "https://api.lemonsqueezy.com/v1/products/1/relationships/store"
}
},
"variants": {
"links": {
"related": "https://api.lemonsqueezy.com/v1/products/1/variants",
"self": "https://api.lemonsqueezy.com/v1/products/1/relationships/variants"
}
}
},
"links": {
"self": "https://api.lemonsqueezy.com/v1/products/1"
}
}
]
}
`)
}
35 changes: 35 additions & 0 deletions product.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package client

import "time"

// ProductAttributes are the attributes of a lemonsqueezy product
type ProductAttributes struct {
StoreID int `json:"store_id"`
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
Status string `json:"status"`
StatusFormatted string `json:"status_formatted"`
ThumbURL string `json:"thumb_url"`
LargeThumbURL string `json:"large_thumb_url"`
Price int `json:"price"`
PayWhatYouWant bool `json:"pay_what_you_want"`
FromPrice *int `json:"from_price"`
ToPrice *int `json:"to_price"`
BuyNowURL string `json:"buy_now_url"`
PriceFormatted string `json:"price_formatted"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// ApiResponseRelationshipsProduct relationships of a product
type ApiResponseRelationshipsProduct struct {
Store ApiResponseLinks `json:"store"`
Variants ApiResponseLinks `json:"variants"`
}

// ProductApiResponse represents a product api response
type ProductApiResponse = ApiResponse[ProductAttributes, ApiResponseRelationshipsProduct]

// ProductsApiResponse represents a list of products api responses.
type ProductsApiResponse = ApiResponseList[ProductAttributes, ApiResponseRelationshipsProduct]
42 changes: 42 additions & 0 deletions products_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package client

import (
"context"
"encoding/json"
"net/http"
)

// ProductsService is the API client for the `/v1/products` endpoint
type ProductsService service

// Get returns the product with the given ID.
// https://docs.lemonsqueezy.com/api/products#retrieve-a-product
func (service *ProductsService) Get(ctx context.Context, productID string) (*ProductApiResponse, *Response, error) {
response, err := service.client.do(ctx, http.MethodGet, "/v1/products/"+productID)
if err != nil {
return nil, response, err
}

product := new(ProductApiResponse)
if err = json.Unmarshal(*response.Body, product); err != nil {
return nil, response, err
}

return product, response, nil
}

// List returns a paginated list of products.
// https://docs.lemonsqueezy.com/api/products#list-all-products
func (service *ProductsService) List(ctx context.Context) (*ProductsApiResponse, *Response, error) {
response, err := service.client.do(ctx, http.MethodGet, "/v1/products")
if err != nil {
return nil, response, err
}

products := new(ProductsApiResponse)
if err = json.Unmarshal(*response.Body, products); err != nil {
return nil, response, err
}

return products, response, nil
}
95 changes: 95 additions & 0 deletions products_service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package client

import (
"context"
"net/http"
"testing"

"github.com/NdoleStudio/lemonsqueezy-go/internal/helpers"
"github.com/NdoleStudio/lemonsqueezy-go/internal/stubs"
"github.com/stretchr/testify/assert"
)

func TestProductsService_Get(t *testing.T) {
// Setup
t.Parallel()

// Arrange
server := helpers.MakeTestServer(http.StatusOK, stubs.ProductGetResponse())
client := New(WithBaseURL(server.URL))

// Act
product, response, err := client.Products.Get(context.Background(), "1")

// Assert
assert.Nil(t, err)

assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode)
assert.Equal(t, stubs.ProductGetResponse(), *response.Body)
assert.Equal(t, "1", product.Data.ID)

// Teardown
server.Close()
}

func TestProductsService_GetWithError(t *testing.T) {
// Setup
t.Parallel()

// Arrange
server := helpers.MakeTestServer(http.StatusInternalServerError, nil)
client := New(WithBaseURL(server.URL))

// Act
_, response, err := client.Products.Get(context.Background(), "1")

// Assert
assert.NotNil(t, err)

assert.Equal(t, http.StatusInternalServerError, response.HTTPResponse.StatusCode)

// Teardown
server.Close()
}

func TestProductsService_List(t *testing.T) {
// Setup
t.Parallel()

// Arrange
server := helpers.MakeTestServer(http.StatusOK, stubs.ProductsListResponse())
client := New(WithBaseURL(server.URL))

// Act
products, response, err := client.Products.List(context.Background())

// Assert
assert.Nil(t, err)

assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode)
assert.Equal(t, stubs.ProductsListResponse(), *response.Body)
assert.Equal(t, 2, len(products.Data))

// Teardown
server.Close()
}

func TestProductsService_ListWithError(t *testing.T) {
// Setup
t.Parallel()

// Arrange
server := helpers.MakeTestServer(http.StatusInternalServerError, nil)
client := New(WithBaseURL(server.URL))

// Act
_, response, err := client.Products.List(context.Background())

// Assert
assert.NotNil(t, err)

assert.Equal(t, http.StatusInternalServerError, response.HTTPResponse.StatusCode)

// Teardown
server.Close()
}

0 comments on commit 6e8bbe7

Please sign in to comment.