Skip to content

Commit

Permalink
Add checkout endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
AchoArnold committed Apr 1, 2023
1 parent 816aa18 commit fec294d
Show file tree
Hide file tree
Showing 12 changed files with 602 additions and 4 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ import "github.com/NdoleStudio/lemonsqueezy-go"
- **Discount Redemptions**
- `GET /v1/discount-redemptions/:id`: Retrieve a discount redemption
- `GET /v1/discount-redemptions`: List all discount redemptions
- **Checkouts**
- `POST /v1/checkouts`: Create a checkout
- `GET /v1/checkouts/:id`: Retrieve a checkout
- `GET /v1/checkouts`: List all checkouts
- **Webhooks**
- `Verify`: Verify that webhook requests are coming from Lemon Squeezy

Expand Down
100 changes: 100 additions & 0 deletions checkout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package client

import "time"

// CheckoutAttributes contains information about a percentage or amount discount that can be applied to an order at checkout via a code.
type CheckoutAttributes struct {
StoreID int `json:"store_id"`
VariantID int `json:"variant_id"`
CustomPrice interface{} `json:"custom_price"`
ProductOptions CheckoutProductOptions `json:"product_options"`
CheckoutOptions CheckoutOptions `json:"checkout_options"`
CheckoutData CheckoutData `json:"checkout_data"`
ExpiresAt *time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
TestMode bool `json:"test_mode"`
URL string `json:"url"`
}

// CheckoutProductOptions are options for a checkout product
type CheckoutProductOptions struct {
Name string `json:"name"`
Description string `json:"description"`
Media []any `json:"media"`
RedirectURL string `json:"redirect_url"`
ReceiptButtonText string `json:"receipt_button_text"`
ReceiptLinkURL string `json:"receipt_link_url"`
ReceiptThankYouNote string `json:"receipt_thank_you_note"`
EnabledVariants []int `json:"enabled_variants"`
}

// CheckoutOptions are options for a checkout
type CheckoutOptions struct {
Embed bool `json:"embed"`
Media bool `json:"media"`
Logo bool `json:"logo"`
Desc bool `json:"desc"`
Discount bool `json:"discount"`
Dark bool `json:"dark"`
SubscriptionPreview bool `json:"subscription_preview"`
ButtonColor string `json:"button_color"`
}

// BillingAddress contains the checkout billing address
type BillingAddress struct {
Country string `json:"country"`
Zip string `json:"zip"`
}

// CheckoutData contains information about a checkout
type CheckoutData struct {
Email string `json:"email"`
Name string `json:"name"`
BillingAddress []BillingAddress `json:"billing_address"`
TaxNumber string `json:"tax_number"`
DiscountCode string `json:"discount_code"`
Custom map[string]any `json:"custom"`
}

// CheckoutPreview contains information about a percentage or amount discount that can be applied to an order at checkout via a code.
type CheckoutPreview struct {
Currency string `json:"currency"`
CurrencyRate int `json:"currency_rate"`
Subtotal int `json:"subtotal"`
DiscountTotal int `json:"discount_total"`
Tax int `json:"tax"`
Total int `json:"total"`
SubtotalUsd int `json:"subtotal_usd"`
DiscountTotalUsd int `json:"discount_total_usd"`
TaxUsd int `json:"tax_usd"`
TotalUsd int `json:"total_usd"`
SubtotalFormatted string `json:"subtotal_formatted"`
DiscountTotalFormatted string `json:"discount_total_formatted"`
TaxFormatted string `json:"tax_formatted"`
TotalFormatted string `json:"total_formatted"`
}

// ApiResponseRelationshipsCheckout relationships of a checkout
type ApiResponseRelationshipsCheckout struct {
Store ApiResponseLinks `json:"store"`
Variant ApiResponseLinks `json:"variant"`
}

// CheckoutApiResponse is the api response for one checkout
type CheckoutApiResponse = ApiResponse[CheckoutAttributes, ApiResponseRelationshipsDiscount]

// CheckoutsApiResponse is the api response for a list of checkout.
type CheckoutsApiResponse = ApiResponseList[CheckoutAttributes, ApiResponseRelationshipsDiscount]

// CheckoutCreateParams are parameters for creating a checkout
type CheckoutCreateParams struct {
CustomPrice int `json:"custom_price"`
EnabledVariants []int `json:"enabled_variants"`
ButtonColor string `json:"button_color"`
DiscountCode *string `json:"discount_code"`
CustomData map[string]string `json:"custom_data"`
ExpiresAt time.Time `json:"expires_at"`
StoreID string `json:"store_id"`
VariantID string `json:"variant_id"`
}
101 changes: 101 additions & 0 deletions checkouts_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package client

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

// CheckoutsService is the API client for the `/v1/checkouts` endpoint
type CheckoutsService service

// Create a custom checkout.
//
// https://docs.lemonsqueezy.com/api/checkouts#create-a-checkout
func (service *CheckoutsService) Create(ctx context.Context, params *CheckoutCreateParams) (*CheckoutApiResponse, *Response, error) {
checkoutData := map[string]any{
"custom": params.CustomData,
}
if params.DiscountCode != nil {
checkoutData["discount_code"] = params.DiscountCode
}

payload := map[string]any{
"data": map[string]any{
"type": "checkouts",
"attributes": map[string]any{
"custom_price": params.CustomPrice,
"product_options": map[string]any{
"enabled_variants": params.EnabledVariants,
},
"checkout_options": map[string]any{
"button_color": params.ButtonColor,
},
"checkout_data": checkoutData,
"expires_at": params.ExpiresAt.Format(time.RFC3339),
"preview": true,
},
"relationships": map[string]any{
"store": map[string]any{
"data": map[string]any{
"id": params.StoreID,
"type": "stores",
},
},
"variant": map[string]any{
"data": map[string]any{
"id": params.VariantID,
"type": "variants",
},
},
},
},
}

response, err := service.client.do(ctx, http.MethodPost, "/v1/checkouts", payload)
if err != nil {
return nil, response, err
}

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

return checkout, response, nil
}

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

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

return checkout, response, nil
}

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

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

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

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 TestCheckoutService_Create(t *testing.T) {
// Setup
t.Parallel()

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

// Act
checkout, response, err := client.Checkouts.Create(context.Background(), &CheckoutCreateParams{
CustomPrice: 5000,
EnabledVariants: []int{1},
ButtonColor: "#2DD272",
CustomData: map[string]string{"user_id": "123"},
ExpiresAt: time.Now().UTC(),
StoreID: "1",
VariantID: "1",
})

// Assert
assert.Nil(t, err)

assert.Equal(t, http.StatusCreated, response.HTTPResponse.StatusCode)
assert.Equal(t, stubs.CheckoutGetResponse(), *response.Body)
assert.Equal(t, "5e8b546c-c561-4a2c-a586-40c18bb2a195", checkout.Data.ID)

// Teardown
server.Close()
}

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

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

// Act
_, response, err := client.Checkouts.Create(context.Background(), &CheckoutCreateParams{})

// Assert
assert.NotNil(t, err)

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

// Teardown
server.Close()
}

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

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

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

// Assert
assert.Nil(t, err)

assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode)
assert.Equal(t, stubs.CheckoutGetResponse(), *response.Body)
assert.Equal(t, "5e8b546c-c561-4a2c-a586-40c18bb2a195", checkout.Data.ID)

// Teardown
server.Close()
}

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

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

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

// Assert
assert.NotNil(t, err)

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

// Teardown
server.Close()
}

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

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

// Act
checkouts, response, err := client.Checkouts.List(context.Background())

// Assert
assert.Nil(t, err)

assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode)
assert.Equal(t, stubs.CheckoutListResponse(), *response.Body)
assert.Equal(t, 1, len(checkouts.Data))

// Teardown
server.Close()
}

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

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

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

// Assert
assert.NotNil(t, err)

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

// Teardown
server.Close()
}
2 changes: 2 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Client struct {
SubscriptionInvoices *SubscriptionInvoicesService
DiscountRedemptions *DiscountRedemptionsService
Discounts *DiscountsService
Checkouts *CheckoutsService
}

// New creates and returns a new Client from a slice of Option.
Expand Down Expand Up @@ -65,6 +66,7 @@ func New(options ...Option) *Client {
client.SubscriptionInvoices = (*SubscriptionInvoicesService)(&client.common)
client.DiscountRedemptions = (*DiscountRedemptionsService)(&client.common)
client.Discounts = (*DiscountsService)(&client.common)
client.Checkouts = (*CheckoutsService)(&client.common)

return client
}
Expand Down
2 changes: 1 addition & 1 deletion discount.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type DiscountAttributes struct {

// ApiResponseRelationshipsDiscount relationships of a discount
type ApiResponseRelationshipsDiscount struct {
Store ApiResponseLinks `json:"order"`
Store ApiResponseLinks `json:"store"`
DiscountRedemptions ApiResponseLinks `json:"discount-redemptions"`
Variants ApiResponseLinks `json:"variants"`
}
Expand Down
Loading

0 comments on commit fec294d

Please sign in to comment.