mirror of
https://github.com/acaloiaro/go-tremendous
synced 2026-07-21 02:09:52 +00:00
initial commit
Initial commit
This commit is contained in:
commit
da7e4f4d34
13 changed files with 689 additions and 0 deletions
2
.envrc
Normal file
2
.envrc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
source .env
|
||||
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.env
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 Adriano Caloiaro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
13
README.md
Normal file
13
README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# go-tremendous
|
||||
|
||||
go-tremendous is a Go client for [Tremendous](https://tremendous.com)
|
||||
|
||||
This client supports a subset of the [tremendous api](https://developers.tremendous.com/docs/introduction). It does not support some endpoints and parameters, and is not affiliated with Tremendous.
|
||||
|
||||
If you would like to contribute additional functionality to this client, please feel free to open a pull request.
|
||||
|
||||
If you would like to report a bug or request additional features, please do not open an issue. This client is provided as-is.
|
||||
|
||||
## Usage
|
||||
|
||||
See [examples](examples/) for examples.
|
||||
92
campaigns.go
Normal file
92
campaigns.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package tremendous
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CampaignsService handles the "campaigns" resource.
|
||||
type CampaignsService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Campaign represents a single campaign record from Tremendous.
|
||||
type Campaign struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// listCampaignsResponse models the JSON response for GET /campaigns.
|
||||
type listCampaignsResponse struct {
|
||||
Campaigns []Campaign `json:"campaigns"`
|
||||
}
|
||||
|
||||
// List fetches a filtered list of campaigns from the API.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// client.Campaigns.List(&tremendous.ListCampaignsOptions{Name: "Summer", Status: "active"})
|
||||
func (cs *CampaignsService) List() ([]Campaign, error) {
|
||||
// Construct the base endpoint
|
||||
baseURL := fmt.Sprintf("%s/campaigns", cs.client.BaseURL)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, baseURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = cs.client.headers()
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var answer listCampaignsResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&answer); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode campaigns list: %w", err)
|
||||
}
|
||||
return answer.Campaigns, nil
|
||||
}
|
||||
|
||||
// Retrieve fetches a single campaign by its ID if needed.
|
||||
// (Optional: Implement similarly to the templates/products retrieve.)
|
||||
func (cs *CampaignsService) Retrieve(campaignID string) (*Campaign, error) {
|
||||
campaignID = strings.TrimSpace(campaignID)
|
||||
urlStr := fmt.Sprintf("%s/campaigns/%s", cs.client.BaseURL, campaignID)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, urlStr, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = cs.client.headers()
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var single struct {
|
||||
Campaign Campaign `json:"campaign"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&single); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode single campaign: %w", err)
|
||||
}
|
||||
return &single.Campaign, nil
|
||||
}
|
||||
52
client.go
Normal file
52
client.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package tremendous
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// TestflightURL is the testflight API endpoint.
|
||||
const TestflightURL = "https://testflight.tremendous.com/api/v2"
|
||||
|
||||
// ProductionURL is the production API endpoint
|
||||
const ProductionURL = "https://api.tremendous.com/api/v2"
|
||||
|
||||
// Client is the primary entry point for using the Tremendous API
|
||||
type Client struct {
|
||||
APIKey string
|
||||
BaseURL string
|
||||
Campaigns *CampaignsService
|
||||
Orders *OrdersService
|
||||
Products *ProductsService
|
||||
}
|
||||
|
||||
type ClientArgs struct {
|
||||
ApiKey string // the api key used to authenticate to the API
|
||||
Production bool // whether to use the production or testflight API
|
||||
}
|
||||
|
||||
// NewClient constructs a new Tremendous client using the given API key.
|
||||
func NewClient(args ClientArgs) *Client {
|
||||
baseURL := TestflightURL
|
||||
if args.Production {
|
||||
baseURL = ProductionURL
|
||||
}
|
||||
client := &Client{
|
||||
APIKey: args.ApiKey,
|
||||
BaseURL: baseURL,
|
||||
}
|
||||
client.Campaigns = &CampaignsService{client: client}
|
||||
client.Orders = &OrdersService{client: client}
|
||||
client.Products = &ProductsService{client: client}
|
||||
return client
|
||||
}
|
||||
|
||||
// headers returns the default headers for requests, including authorization.
|
||||
func (c *Client) headers() http.Header {
|
||||
h := http.Header{}
|
||||
h.Set("Authorization", fmt.Sprintf("Bearer %s", c.APIKey))
|
||||
h.Set("Content-Type", "application/json")
|
||||
h.Set("Accept", "application/json")
|
||||
h.Set("User-Agent", "go-tremendous/1.0.0")
|
||||
return h
|
||||
}
|
||||
27
examples/list_campaigns/main.go
Normal file
27
examples/list_campaigns/main.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/acaloiaro/go-tremendous"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Replace with your actual API key.
|
||||
args := tremendous.ClientArgs{
|
||||
ApiKey: cmp.Or(os.Getenv("TREMENDOUS_API_KEY"), "YOUR_API_KEY"),
|
||||
Production: false,
|
||||
}
|
||||
client := tremendous.NewClient(args)
|
||||
campaigns, err := client.Campaigns.List()
|
||||
if err != nil {
|
||||
fmt.Printf("unable to list campaigns: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, c := range campaigns {
|
||||
fmt.Println(c)
|
||||
}
|
||||
}
|
||||
28
examples/list_orders/main.go
Normal file
28
examples/list_orders/main.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/acaloiaro/go-tremendous"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Replace with your actual API key.
|
||||
args := tremendous.ClientArgs{
|
||||
ApiKey: cmp.Or(os.Getenv("TREMENDOUS_API_KEY"), "YOUR_API_KEY"),
|
||||
Production: false,
|
||||
}
|
||||
client := tremendous.NewClient(args)
|
||||
|
||||
orders, err := client.Orders.List()
|
||||
if err != nil {
|
||||
log.Fatalf("List error: %v", err)
|
||||
}
|
||||
|
||||
for i, order := range orders {
|
||||
fmt.Printf("Order %d: %v\n", i, order)
|
||||
}
|
||||
}
|
||||
30
examples/list_products/main.go
Normal file
30
examples/list_products/main.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/acaloiaro/go-tremendous"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Replace with your actual API key.
|
||||
args := tremendous.ClientArgs{
|
||||
ApiKey: cmp.Or(os.Getenv("TREMENDOUS_API_KEY"), "YOUR_API_KEY"),
|
||||
Production: false,
|
||||
}
|
||||
client := tremendous.NewClient(args)
|
||||
|
||||
products, err := client.Products.List(&tremendous.ListProductsOptions{
|
||||
Country: "US", // list only US products
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("List error: %v", err)
|
||||
}
|
||||
|
||||
for i, product := range products {
|
||||
fmt.Printf("Product %d: %v\n", i, product)
|
||||
}
|
||||
}
|
||||
58
examples/place_order/main.go
Normal file
58
examples/place_order/main.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/acaloiaro/go-tremendous"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Replace with your actual API key.
|
||||
args := tremendous.ClientArgs{
|
||||
ApiKey: cmp.Or(os.Getenv("TREMENDOUS_API_KEY"), "YOUR_API_KEY"),
|
||||
Production: false,
|
||||
}
|
||||
client := tremendous.NewClient(args)
|
||||
|
||||
campaigns, err := client.Campaigns.List()
|
||||
if err != nil {
|
||||
fmt.Println("unable to list campaigns", err)
|
||||
return
|
||||
}
|
||||
|
||||
newOrderArgs := tremendous.OrderArgs{
|
||||
Reward: tremendous.RewardArg{
|
||||
CampaignID: &campaigns[0].ID,
|
||||
Recipient: tremendous.OrderRecipient{
|
||||
Name: "Testy McTesterson",
|
||||
Email: "testy@example.com",
|
||||
},
|
||||
Value: tremendous.OrderDenomination{
|
||||
Denomination: 1.00,
|
||||
CurrencyCode: "USD",
|
||||
},
|
||||
Delivery: tremendous.OrderDelivery{
|
||||
Method: "LINK",
|
||||
},
|
||||
},
|
||||
Payment: tremendous.OrderPaymentArg{
|
||||
FundingSourceID: "BALANCE",
|
||||
},
|
||||
}
|
||||
// Example: Create an order (replace with valid fields).
|
||||
newOrder, err := client.Orders.Create(newOrderArgs)
|
||||
if err != nil {
|
||||
log.Fatalf("Create error: %v", err)
|
||||
}
|
||||
fmt.Println("Created order:", newOrder, "\n\n")
|
||||
|
||||
// Example: Retrieve that order by ID.
|
||||
retOrder, err := client.Orders.Retrieve(newOrder.ID)
|
||||
if err != nil {
|
||||
log.Fatalf("Retrieve error: %v", err)
|
||||
}
|
||||
fmt.Println("Retrieved order:", retOrder)
|
||||
}
|
||||
3
go.mod
Normal file
3
go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/acaloiaro/go-tremendous
|
||||
|
||||
go 1.23.0
|
||||
198
orders.go
Normal file
198
orders.go
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
package tremendous
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OrdersService handles the "orders" resource.
|
||||
type OrdersService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Order represents the JSON structure of an order in the Tremendous API.
|
||||
type Order struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
ExternalID string `json:"external_id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Payment Payment `json:"payment,omitempty"`
|
||||
Rewards []Reward `json:"rewards"`
|
||||
}
|
||||
|
||||
// Payment captures details of the payment object used by the order.
|
||||
type Payment struct {
|
||||
FundingSourceID string `json:"funding_source_id,omitempty"`
|
||||
Amount float64 `json:"amount,omitempty"`
|
||||
CurrencyCode string `json:"currency_code,omitempty"`
|
||||
Object string `json:"object,omitempty"`
|
||||
}
|
||||
|
||||
// Reward captures details of a reward that has been paid out
|
||||
type Reward struct {
|
||||
ID string `json:"id"`
|
||||
OrderID string `json:"order_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Value OrderDenomination `json:"value"`
|
||||
Delivery OrderDelivery `json:"delivery"`
|
||||
Recipient OrderRecipient `json:"recipient"`
|
||||
}
|
||||
|
||||
// OrderArgs is the request struct for creating an order. It wraps
|
||||
// the "order" object exactly as Tremendous expects in the JSON payload.
|
||||
type OrderArgs struct {
|
||||
CampaignID *string `json:"campaign_id"`
|
||||
ExternalID *string `json:"external_id"`
|
||||
Payment OrderPaymentArg `json:"payment"`
|
||||
Reward RewardArg `json:"reward"`
|
||||
}
|
||||
|
||||
// OrderPaymentArg describes an order's payment details
|
||||
type OrderPaymentArg struct {
|
||||
FundingSourceID string `json:"funding_source_id,omitempty"`
|
||||
}
|
||||
|
||||
// OrderDenomination denominates the reward's monetary value
|
||||
type OrderDenomination struct {
|
||||
Denomination float64 `json:"denomination"`
|
||||
CurrencyCode string `json:"currency_code"`
|
||||
}
|
||||
|
||||
// OrderDelivery designates how an order's reward is delivered
|
||||
type OrderDelivery struct {
|
||||
Method string `json:"method"` // email, link, or phone
|
||||
Status string `json:"status"`
|
||||
Link string `json:"link"`
|
||||
}
|
||||
|
||||
// RewardArg is subset of field for on order reward
|
||||
type RewardArg struct {
|
||||
CampaignID *string `json:"campaign_id"`
|
||||
Delivery OrderDelivery `json:"delivery"`
|
||||
Products []string `json:"products"`
|
||||
Recipient OrderRecipient `json:"recipient"`
|
||||
Value OrderDenomination `json:"value"`
|
||||
}
|
||||
|
||||
// OrderRecipient contains details about reward recipients
|
||||
type OrderRecipient struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
|
||||
// singleOrderResponse models the JSON for a single order return.
|
||||
type singleOrderResponse struct {
|
||||
Order Order `json:"order"`
|
||||
}
|
||||
|
||||
// listOrdersResponse models the JSON for a multi-order listing.
|
||||
type listOrdersResponse struct {
|
||||
Orders []Order `json:"orders"`
|
||||
}
|
||||
|
||||
// List fetches all orders. (Query params, pagination, etc., may be added later.)
|
||||
func (s *OrdersService) List() ([]Order, error) {
|
||||
url := fmt.Sprintf("%s/orders", s.client.BaseURL)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = s.client.headers()
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var ordersResp listOrdersResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&ordersResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode orders list: %w", err)
|
||||
}
|
||||
return ordersResp.Orders, nil
|
||||
}
|
||||
|
||||
// Retrieve fetches a single order by its ID.
|
||||
func (s *OrdersService) Retrieve(orderID string) (*Order, error) {
|
||||
orderID = strings.TrimSpace(orderID)
|
||||
url := fmt.Sprintf("%s/orders/%s", s.client.BaseURL, orderID)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = s.client.headers()
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var singleResp singleOrderResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&singleResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode single order: %w", err)
|
||||
}
|
||||
return &singleResp.Order, nil
|
||||
}
|
||||
|
||||
// Create sends a new order to the Tremendous API for creation.
|
||||
// It requires the Payment field to be non-nil in CreateOrderArgs.
|
||||
func (s *OrdersService) Create(args OrderArgs) (*Order, error) {
|
||||
url := fmt.Sprintf("%s/orders", s.client.BaseURL)
|
||||
|
||||
// Basic validation: ensure payment is present
|
||||
if args.Payment.FundingSourceID == "" {
|
||||
return nil, errors.New("the 'payment.funding_source_id' field is required but was empty")
|
||||
}
|
||||
|
||||
// Marshal the typed struct into JSON
|
||||
bodyBytes, err := json.Marshal(&args)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal CreateOrderArgs: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Request", string(bodyBytes))
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = s.client.headers()
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Expect 201 Created (some APIs return 200 instead).
|
||||
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var singleResp singleOrderResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&singleResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode create order response: %w", err)
|
||||
}
|
||||
return &singleResp.Order, nil
|
||||
}
|
||||
160
products.go
Normal file
160
products.go
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
package tremendous
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ProductsService handles the “products” resource.
|
||||
type ProductsService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Product is a fully parsed product object (fields may vary by API).
|
||||
type Product struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Brand string `json:"brand,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Price float64 `json:"price,omitempty"`
|
||||
Currency string `json:"currency,omitempty"`
|
||||
Object string `json:"object,omitempty"`
|
||||
}
|
||||
|
||||
// CreateProductArgs is the typed request for creating a new product.
|
||||
type CreateProductArgs struct {
|
||||
Product CreateProduct `json:"product"`
|
||||
}
|
||||
|
||||
// CreateProduct is the structure inside the top-level "product" key for creation.
|
||||
type CreateProduct struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Brand string `json:"brand,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Price float64 `json:"price,omitempty"`
|
||||
Currency string `json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
// singleProductResponse models the JSON for a single product spit back by the API.
|
||||
type singleProductResponse struct {
|
||||
Product Product `json:"product"`
|
||||
}
|
||||
|
||||
// listProductsResponse models the JSON structure for multiple products.
|
||||
type listProductsResponse struct {
|
||||
Products []Product `json:"products"`
|
||||
}
|
||||
|
||||
// ListProductsOptions specifies optional query filters, such as country.
|
||||
type ListProductsOptions struct {
|
||||
Country string // e.g. "US" or "CA"
|
||||
}
|
||||
|
||||
// List retrieves all products, optionally filtering by country (and other options).
|
||||
func (ps *ProductsService) List(opts *ListProductsOptions) ([]Product, error) {
|
||||
base := fmt.Sprintf("%s/products", ps.client.BaseURL)
|
||||
|
||||
// Build query parameters only if opts is provided
|
||||
if opts != nil {
|
||||
queryVals := url.Values{}
|
||||
if opts.Country != "" {
|
||||
queryVals.Set("country", opts.Country)
|
||||
}
|
||||
// Add additional query parameters as needed
|
||||
encoded := queryVals.Encode()
|
||||
if encoded != "" {
|
||||
base += "?" + encoded
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, base, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = ps.client.headers()
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var listResp listProductsResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&listResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode products list: %w", err)
|
||||
}
|
||||
return listResp.Products, nil
|
||||
}
|
||||
|
||||
// Create adds a new product to the Tremendous API.
|
||||
func (ps *ProductsService) Create(args CreateProductArgs) (*Product, error) {
|
||||
url := fmt.Sprintf("%s/products", ps.client.BaseURL)
|
||||
|
||||
jsonBytes, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal CreateProductArgs: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = ps.client.headers()
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Expect 201 Created or 200 is possible in some APIs
|
||||
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var singleResp singleProductResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&singleResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode create product response: %w", err)
|
||||
}
|
||||
return &singleResp.Product, nil
|
||||
}
|
||||
|
||||
// Retrieve fetches a single product by its ID.
|
||||
func (ps *ProductsService) Retrieve(productID string) (*Product, error) {
|
||||
productID = strings.TrimSpace(productID)
|
||||
url := fmt.Sprintf("%s/products/%s", ps.client.BaseURL, productID)
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header = ps.client.headers()
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var singleResp singleProductResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&singleResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode single product: %w", err)
|
||||
}
|
||||
return &singleResp.Product, nil
|
||||
}
|
||||
Loading…
Reference in a new issue