diff --git a/client.go b/client.go index 39ba3db..a574052 100644 --- a/client.go +++ b/client.go @@ -38,6 +38,7 @@ type Client struct { Checkouts *CheckoutsService LicenseKeys *LicenseKeysService LicenseKeyInstances *LicenseKeyInstancesService + Licenses *LicensesService } // New creates and returns a new Client from a slice of Option. @@ -73,6 +74,7 @@ func New(options ...Option) *Client { client.Checkouts = (*CheckoutsService)(&client.common) client.LicenseKeys = (*LicenseKeysService)(&client.common) client.LicenseKeyInstances = (*LicenseKeyInstancesService)(&client.common) + client.Licenses = (*LicensesService)(&client.common) return client } diff --git a/internal/stubs/licenses.go b/internal/stubs/licenses.go new file mode 100644 index 0000000..6a248fe --- /dev/null +++ b/internal/stubs/licenses.go @@ -0,0 +1,104 @@ +package stubs + +// LicenseActivateResponse is a dummy response to the POST /v1/licenses/activate endpoint +func LicenseActivateResponse() []byte { + return []byte(` + { + "activated": true, + "error": null, + "license_key": { + "id": 1, + "status": "active", + "key": "38b1460a-5104-4067-a91d-77b872934d51", + "activation_limit": 1, + "activation_usage": 5, + "created_at": "2021-01-24T14:15:07.000000Z", + "expires_at": null + }, + "instance": { + "id": "47596ad9-a811-4ebf-ac8a-03fc7b6d2a17", + "name": "Test", + "created_at": "2021-04-06T14:15:07.000000Z" + }, + "meta": { + "store_id": 1, + "order_id": 2, + "order_item_id": 3, + "product_id": 4, + "product_name": "Example Product", + "variant_id": 5, + "variant_name": "Default", + "customer_id": 6, + "customer_name": "Luke Skywalker", + "customer_email": "luke@skywalker.com" + } + } +`) +} + +// LicenseValidateResponse is a dummy response to the POST /v1/licenses/validate endpoint +func LicenseValidateResponse() []byte { + return []byte(` + { + "valid": true, + "error": null, + "license_key": { + "id": 1, + "status": "active", + "key": "38b1460a-5104-4067-a91d-77b872934d51", + "activation_limit": 1, + "activation_usage": 5, + "created_at": "2021-01-24T14:15:07.000000Z", + "expires_at": "2022-01-24T14:15:07.000000Z" + }, + "instance": { + "id": "f90ec370-fd83-46a5-8bbd-44a241e78665", + "name": "Test", + "created_at": "2021-02-24T14:15:07.000000Z" + }, + "meta": { + "store_id": 1, + "order_id": 2, + "order_item_id": 3, + "product_id": 4, + "product_name": "Example Product", + "variant_id": 5, + "variant_name": "Default", + "customer_id": 6, + "customer_name": "Luke Skywalker", + "customer_email": "luke@skywalker.com" + } + } +`) +} + +// LicenseDeactivateResponse is a dummy response to the POST /v1/licenses/deactivate endpoint +func LicenseDeactivateResponse() []byte { + return []byte(` + { + "deactivated": true, + "error": null, + "license_key": { + "id": 1, + "status": "inactive", + "key": "38b1460a-5104-4067-a91d-77b872934d51", + "activation_limit": 5, + "activation_usage": 0, + "created_at": "2021-01-24T14:15:07.000000Z", + "expires_at": null + }, + "meta": { + "store_id": 1, + "order_id": 2, + "order_item_id": 3, + "product_id": 4, + "product_name": "Example Product", + "variant_id": 5, + "variant_name": "Default", + "customer_id": 6, + "customer_name": "Luke Skywalker", + "customer_email": "luke@skywalker.com" + } + } +`) +} diff --git a/licenses.go b/licenses.go new file mode 100644 index 0000000..8830737 --- /dev/null +++ b/licenses.go @@ -0,0 +1,54 @@ +package lemonsqueezy + +import "time" + +type LicenseAttributes struct { + Error string `json:"error"` + LicenseKey LicenseKey `json:"license_key"` + Instance LicenseInstance `json:"instance"` + Meta LicenseMeta `json:"meta"` +} + +type LicenseKey struct { + ID int `json:"id"` + Status string `json:"status"` + Key string `json:"key"` + ActivationLimit int `json:"activation_limit"` + ActivationUsage int `json:"activation_usage"` + CreatedAt time.Time `json:"created_at"` + ExpiresAt *time.Time `json:"expires_at"` + TestMode bool `json:"test_mode"` +} + +type LicenseInstance struct { + ID string `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` +} + +type LicenseMeta struct { + StoreID int `json:"store_id"` + OrderID int `json:"order_id"` + OrderItemID int `json:"order_item_id"` + VariantID int `json:"variant_id"` + VariantName string `json:"variant_name"` + ProductID int `json:"product_id"` + ProductName string `json:"product_name"` + CustomerID int `json:"customer_id"` + CustomerName string `json:"customer_name"` + CustomerEmail string `json:"customer_email"` +} + +type LicenseActivateApiResponse struct { + Activated bool `json:"activated"` + LicenseAttributes +} + +type LicenseValidateApiResponse struct { + Valid bool `json:"valid"` + LicenseAttributes +} +type LicenseDeactivateApiResponse struct { + Deactivated bool `json:"deactivated"` + LicenseAttributes +} diff --git a/licenses_service.go b/licenses_service.go new file mode 100644 index 0000000..3f110c2 --- /dev/null +++ b/licenses_service.go @@ -0,0 +1,76 @@ +package lemonsqueezy + +import ( + "context" + "encoding/json" + "net/http" +) + +// LicensesService is the API client for the `/v1/licenses` endpoint +type LicensesService service + +// Activate a license key. +// +// https://docs.lemonsqueezy.com/help/licensing/license-api +func (service *LicensesService) Activate(ctx context.Context, licenseKey, instanceName string) (*LicenseActivateApiResponse, *Response, error) { + payload := map[string]any{ + "license_key": licenseKey, + "instance_name": instanceName, + } + + response, err := service.client.do(ctx, http.MethodPost, "/v1/licenses/activate", payload) + if err != nil { + return nil, response, err + } + + activation := new(LicenseActivateApiResponse) + if err = json.Unmarshal(*response.Body, activation); err != nil { + return nil, response, err + } + + return activation, response, nil +} + +// Validate a license key. +// +// https://docs.lemonsqueezy.com/help/licensing/license-api +func (service *LicensesService) Validate(ctx context.Context, licenseKey, instanceID string) (*LicenseValidateApiResponse, *Response, error) { + payload := map[string]any{ + "license_key": licenseKey, + "instance_id": instanceID, + } + + response, err := service.client.do(ctx, http.MethodPost, "/v1/licenses/validate", payload) + if err != nil { + return nil, response, err + } + + validation := new(LicenseValidateApiResponse) + if err = json.Unmarshal(*response.Body, validation); err != nil { + return nil, response, err + } + + return validation, response, nil +} + +// Deactivate a license key. +// +// https://docs.lemonsqueezy.com/help/licensing/license-api +func (service *LicensesService) Deactivate(ctx context.Context, licenseKey, instanceID string) (*LicenseDeactivateApiResponse, *Response, error) { + payload := map[string]any{ + "license_key": licenseKey, + "instance_id": instanceID, + } + + response, err := service.client.do(ctx, http.MethodPost, "/v1/licenses/deactivate", payload) + if err != nil { + return nil, response, err + } + + deactivation := new(LicenseDeactivateApiResponse) + if err = json.Unmarshal(*response.Body, deactivation); err != nil { + return nil, response, err + } + + return deactivation, response, nil +} diff --git a/licenses_service_test.go b/licenses_service_test.go new file mode 100644 index 0000000..4ab8517 --- /dev/null +++ b/licenses_service_test.go @@ -0,0 +1,137 @@ +package lemonsqueezy + +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 TestLicensesService_Activate(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + server := helpers.MakeTestServer(http.StatusOK, stubs.LicenseActivateResponse()) + client := New(WithBaseURL(server.URL)) + + // Act + licenseActivation, response, err := client.Licenses.Activate(context.Background(), "1234567890", "test") + + // Assert + assert.Nil(t, err) + + assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + assert.Equal(t, stubs.LicenseActivateResponse(), *response.Body) + assert.Equal(t, true, licenseActivation.Activated) + + // Teardown + server.Close() +} + +func TestLicensesService_ActivateWithError(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + server := helpers.MakeTestServer(http.StatusInternalServerError, nil) + client := New(WithBaseURL(server.URL)) + + // Act + _, response, err := client.Licenses.Activate(context.Background(), "1234567890", "test") + + // Assert + assert.NotNil(t, err) + + assert.Equal(t, http.StatusInternalServerError, response.HTTPResponse.StatusCode) + + // Teardown + server.Close() +} + +func TestLicensesService_Validate(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + server := helpers.MakeTestServer(http.StatusOK, stubs.LicenseValidateResponse()) + client := New(WithBaseURL(server.URL)) + + // Act + licenseValidation, response, err := client.Licenses.Validate(context.Background(), "1234567890", "") + + // Assert + assert.Nil(t, err) + + assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + assert.Equal(t, stubs.LicenseValidateResponse(), *response.Body) + assert.Equal(t, true, licenseValidation.Valid) + + // Teardown + server.Close() +} + +func TestLicensesService_ValidateWithError(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + server := helpers.MakeTestServer(http.StatusInternalServerError, nil) + client := New(WithBaseURL(server.URL)) + + // Act + _, response, err := client.Licenses.Validate(context.Background(), "1234567890", "") + + // Assert + assert.NotNil(t, err) + + assert.Equal(t, http.StatusInternalServerError, response.HTTPResponse.StatusCode) + + // Teardown + server.Close() +} + +func TestLicensesService_Deactivate(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + server := helpers.MakeTestServer(http.StatusOK, stubs.LicenseDeactivateResponse()) + client := New(WithBaseURL(server.URL)) + + // Act + licenseDeactivation, response, err := client.Licenses.Deactivate(context.Background(), "1234567890", "abc123") + + // Assert + assert.Nil(t, err) + + assert.Equal(t, http.StatusOK, response.HTTPResponse.StatusCode) + assert.Equal(t, stubs.LicenseDeactivateResponse(), *response.Body) + assert.Equal(t, true, licenseDeactivation.Deactivated) + + // Teardown + server.Close() +} + +func TestLicensesService_DeactivateWithError(t *testing.T) { + // Setup + t.Parallel() + + // Arrange + server := helpers.MakeTestServer(http.StatusInternalServerError, nil) + client := New(WithBaseURL(server.URL)) + + // Act + _, response, err := client.Licenses.Deactivate(context.Background(), "1234567890", "abc123") + + // Assert + assert.NotNil(t, err) + + assert.Equal(t, http.StatusInternalServerError, response.HTTPResponse.StatusCode) + + // Teardown + server.Close() +}