From a4127264ef080d2ce1db2be06d789ada543f5666 Mon Sep 17 00:00:00 2001 From: Bastian de Byl Date: Tue, 6 Jun 2023 18:28:10 -0400 Subject: [PATCH] expanded webhook tax coverage, moved and renamed items to exclude Snipcart preamble --- snipcart/client.go | 0 snipcart/common.go | 42 ++++++++ snipcart/snipcart.go | 231 ++++++++++++++++--------------------------- snipcart/webhook.go | 85 +++++++++++++--- 4 files changed, 198 insertions(+), 160 deletions(-) create mode 100644 snipcart/client.go create mode 100644 snipcart/common.go diff --git a/snipcart/client.go b/snipcart/client.go new file mode 100644 index 0000000..e69de29 diff --git a/snipcart/common.go b/snipcart/common.go new file mode 100644 index 0000000..57806b2 --- /dev/null +++ b/snipcart/common.go @@ -0,0 +1,42 @@ +package snipcart + +const ( + defaultLimit = 50 + apiUri = "https://app.snipcart.com" + ordersPath = "/api/orders" + productsPath = "/api/products" + validationPath = "/api/requestvalidation/" +) + +var ( + orderUri = apiUri + ordersPath + productsUri = apiUri + productsPath + validationUri = apiUri + validationPath +) + +type Address struct { + FullName string `json:"fullName"` + FirstName string `json:"firstName"` + Name string `json:"name"` + Company string `json:"company"` + Address1 string `json:"address1"` + Address2 string `json:"address2"` + FullAddress string `json:"fullAddress"` + City string `json:"city"` + Country string `json:"country"` + PostalCode string `json:"postalCode"` + Province string `json:"province"` + Phone string `json:"phone"` + VatNumber string `json:"vatNumber,omitempty"` +} + +type Client struct { + Key string + AuthBase64 string + Limit int +} + +type CustomField struct { + Name string `json:"name"` + Value string `json:"value"` +} diff --git a/snipcart/snipcart.go b/snipcart/snipcart.go index 1996c0a..2c7cdec 100644 --- a/snipcart/snipcart.go +++ b/snipcart/snipcart.go @@ -5,85 +5,59 @@ import ( "encoding/json" "errors" "fmt" - "net/http" "time" helper "github.com/debyltech/go-helpers/json" "github.com/skip2/go-qrcode" ) -const ( - defaultLimit = 50 - apiUri = "https://app.snipcart.com" - ordersPath = "/api/orders" - productsPath = "/api/products" - validationPath = "/api/requestvalidation/" -) - -var ( - orderUri = apiUri + ordersPath - productsUri = apiUri + productsPath - validationUri = apiUri + validationPath -) - -type Client struct { - SnipcartKey string - AuthBase64 string - Limit int +type Item struct { + UUID string `json:"uniqueId"` + ID string `json:"id"` + Name string `json:"name"` + Quantity int `json:"quantity"` + TotalWeight float64 `json:"totalWeight,omitempty"` + TotalPrice float64 `json:"totalPrice,omitempty"` + CustomFields []CustomField `json:"customFields"` + Length float64 `json:"length,omitempty"` + Width float64 `json:"width,omitempty"` + Height float64 `json:"height,omitempty"` + Weight float64 `json:"weight,omitempty"` + Shippable bool `json:"shippable,omitempty"` } -type SnipcartCustomField struct { - Name string `json:"name"` - Value string `json:"value"` +type Order struct { + Token string `json:"token"` + Created time.Time `json:"creationDate"` + Modified time.Time `json:"modificationDate"` + Invoice string `json:"invoiceNumber"` + Subtotal float64 `json:"subtotal,omitempty"` + Currency string `json:"currency,omitempty"` + Total float64 `json:"grandTotal,omitempty"` + Status string `json:"status"` + TotalWeight float64 `json:"totalWeight"` + ShippingAddress Address `json:"shippingAddress,omitempty"` + Name string `json:"shippingAddressName,omitempty"` + Company string `json:"shippingAddressCompanyName,omitempty"` + Address1 string `json:"shippingAddressAddress1,omitempty"` + Address2 string `json:"shippingAddressAddress2,omitempty"` + City string `json:"shippingAddressCity,omitempty"` + Province string `json:"shippingAddressProvince,omitempty"` + Country string `json:"shippingAddressCountry,omitempty"` + PostalCode string `json:"shippingAddressPostalCode,omitempty"` + Phone string `json:"shippingAddressPhone,omitempty"` + Email string `json:"email,omitempty"` + TrackingNumber string `json:"trackingNumber"` + TrackingUrl string `json:"trackingUrl"` + ShippingCost float64 `json:"shippingFees"` + ShippingProvider string `json:"shippingProvider,omitempty"` + ShippingMethod string `json:"shippingMethod,omitempty"` + ShippingRateId string `json:"shippingRateUserDefinedId,omitempty"` + Items []Item `json:"items"` + Metadata any `json:"metadata"` } -type SnipcartItem struct { - UUID string `json:"uniqueId"` - ID string `json:"id"` - Name string `json:"name"` - Quantity int `json:"quantity"` - TotalWeight float64 `json:"totalWeight,omitempty"` - TotalPrice float64 `json:"totalPrice,omitempty"` - CustomFields []SnipcartCustomField `json:"customFields"` - Length float64 `json:"length,omitempty"` - Width float64 `json:"width,omitempty"` - Height float64 `json:"height,omitempty"` - Weight float64 `json:"weight,omitempty"` - Shippable bool `json:"shippable,omitempty"` -} - -type SnipcartOrder struct { - Token string `json:"token"` - Created time.Time `json:"creationDate"` - Modified time.Time `json:"modificationDate"` - Invoice string `json:"invoiceNumber"` - Subtotal float64 `json:"subtotal,omitempty"` - Currency string `json:"currency,omitempty"` - Total float64 `json:"grandTotal,omitempty"` - Status string `json:"status"` - TotalWeight float64 `json:"totalWeight"` - ShippingAddress SnipcartShippingAddress `json:"shippingAddress,omitempty"` - Name string `json:"shippingAddressName,omitempty"` - Company string `json:"shippingAddressCompanyName,omitempty"` - Address1 string `json:"shippingAddressAddress1,omitempty"` - Address2 string `json:"shippingAddressAddress2,omitempty"` - City string `json:"shippingAddressCity,omitempty"` - Province string `json:"shippingAddressProvince,omitempty"` - Country string `json:"shippingAddressCountry,omitempty"` - PostalCode string `json:"shippingAddressPostalCode,omitempty"` - Phone string `json:"shippingAddressPhone,omitempty"` - Email string `json:"email,omitempty"` - TrackingNumber string `json:"trackingNumber"` - TrackingUrl string `json:"trackingUrl"` - ShippingCost float64 `json:"shippingFees"` - ShippingProvider string `json:"shippingProvider,omitempty"` - ShippingMethod string `json:"shippingMethod,omitempty"` - ShippingRateId string `json:"shippingRateUserDefinedId,omitempty"` - Items []SnipcartItem `json:"items"` - Metadata any `json:"metadata"` -} - -type SnipcartOrderUpdate struct { +type OrderUpdate struct { Status OrderStatus `json:"status"` PaymentStatus string `json:"paymentStatus,omitempty"` TrackingNumber string `json:"trackingNumber,omitempty"` @@ -92,25 +66,18 @@ type SnipcartOrderUpdate struct { Metadata any `json:"metadata,omitempty"` } -type SnipcartOrders struct { +type Orders struct { TotalItems int - Items []SnipcartOrder + Items []Order } -type SnipcartTax struct { - Name string `json:"name"` - Amount float64 `json:"amount"` - NumberForInvoice string `json:"numberForInvoice"` - Rate float64 `json:"rate"` -} - -type SnipcartNotification struct { +type Notification struct { Type NotificationType `json:"type"` DeliveryMethod string `json:"deliveryMethod"` Message string `json:"message,omitempty"` } -type SnipcartNotificationResponse struct { +type NotificationResponse struct { Id string `json:"id"` Created time.Time `json:"creationDate"` Type NotificationType `json:"type"` @@ -121,13 +88,13 @@ type SnipcartNotificationResponse struct { SentOn time.Time `json:"sentOn"` } -type SnipcartProductVariant struct { +type ProductVariant struct { Stock int `json:"stock"` Variation []any `json:"variation"` AllowBackorder bool `json:"allowOutOfStockPurchases"` } -type SnipcartProductCustomField struct { +type ProductCustomField struct { Name string `json:"name"` Placeholder string `json:"placeholder"` DisplayValue string `json:"displayValue"` @@ -139,40 +106,40 @@ type SnipcartProductCustomField struct { OptionsArray []string `json:"optionsArray"` } -type SnipcartProduct struct { - Token string `json:"id"` - Id string `json:"userDefinedId"` - Name string `json:"name"` - Stock int `json:"stock"` - TotalStock int `json:"totalStock"` - AllowBackorder bool `json:"allowOutOfStockPurchases"` - CustomFields []SnipcartProductCustomField `json:"customFields"` - Variants []SnipcartProductVariant `json:"variants"` +type Product struct { + Token string `json:"id"` + Id string `json:"userDefinedId"` + Name string `json:"name"` + Stock int `json:"stock"` + TotalStock int `json:"totalStock"` + AllowBackorder bool `json:"allowOutOfStockPurchases"` + CustomFields []ProductCustomField `json:"customFields"` + Variants []ProductVariant `json:"variants"` } -type SnipcartProductsResponse struct { - Keywords string `json:"keywords"` - UserDefinedId string `json:"userDefinedId"` - Archived bool `json:"archived"` - From time.Time `json:"from"` - To time.Time `json:"to"` - OrderBy string `json:"orderBy"` - Paginated bool `json:"hasMoreResults"` - TotalItems int `json:"totalItems"` - Offset int `json:"offset"` - Limit int `json:"limit"` - Sort []any `json:"sort"` - Items []SnipcartProduct `json:"items"` +type ProductsResponse struct { + Keywords string `json:"keywords"` + UserDefinedId string `json:"userDefinedId"` + Archived bool `json:"archived"` + From time.Time `json:"from"` + To time.Time `json:"to"` + OrderBy string `json:"orderBy"` + Paginated bool `json:"hasMoreResults"` + TotalItems int `json:"totalItems"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []any `json:"sort"` + Items []Product `json:"items"` } func NewClient(snipcartApiKey string) *Client { return &Client{ - SnipcartKey: snipcartApiKey, - AuthBase64: base64.StdEncoding.EncodeToString([]byte(snipcartApiKey + ":")), + Key: snipcartApiKey, + AuthBase64: base64.StdEncoding.EncodeToString([]byte(snipcartApiKey + ":")), } } -func (s *Client) GetOrder(token string) (*SnipcartOrder, error) { +func (s *Client) GetOrder(token string) (*Order, error) { response, err := helper.Get(orderUri+"/"+token, "Basic", s.AuthBase64, nil) if err != nil { return nil, err @@ -183,7 +150,7 @@ func (s *Client) GetOrder(token string) (*SnipcartOrder, error) { defer response.Body.Close() - var order SnipcartOrder + var order Order err = json.NewDecoder(response.Body).Decode(&order) if err != nil { return nil, err @@ -192,7 +159,7 @@ func (s *Client) GetOrder(token string) (*SnipcartOrder, error) { return &order, nil } -func (s *Client) GetOrders(queries map[string]string) (*SnipcartOrders, error) { +func (s *Client) GetOrders(queries map[string]string) (*Orders, error) { response, err := helper.Get(orderUri, "Basic", s.AuthBase64, queries) if err != nil { return nil, err @@ -203,7 +170,7 @@ func (s *Client) GetOrders(queries map[string]string) (*SnipcartOrders, error) { defer response.Body.Close() - var orders SnipcartOrders + var orders Orders err = json.NewDecoder(response.Body).Decode(&orders) if err != nil { return nil, err @@ -212,7 +179,7 @@ func (s *Client) GetOrders(queries map[string]string) (*SnipcartOrders, error) { return &orders, nil } -func (s *Client) GetOrdersByStatus(status OrderStatus) (*SnipcartOrders, error) { +func (s *Client) GetOrdersByStatus(status OrderStatus) (*Orders, error) { if status == "" { return nil, errors.New("status is not set") } @@ -220,7 +187,7 @@ func (s *Client) GetOrdersByStatus(status OrderStatus) (*SnipcartOrders, error) return s.GetOrders(map[string]string{"status": string(status)}) } -func (o *SnipcartOrder) TokenPNGBase64() (string, error) { +func (o *Order) TokenPNGBase64() (string, error) { img, err := qrcode.Encode("order:"+o.Token, qrcode.Medium, 128) if err != nil { return "", err @@ -229,7 +196,7 @@ func (o *SnipcartOrder) TokenPNGBase64() (string, error) { return base64.StdEncoding.EncodeToString(img), nil } -func (s *Client) UpdateOrder(token string, orderUpdate *SnipcartOrderUpdate) (*SnipcartOrder, error) { +func (s *Client) UpdateOrder(token string, orderUpdate *OrderUpdate) (*Order, error) { response, err := helper.Put(orderUri+"/"+token, "Basic", s.AuthBase64, orderUpdate) if err != nil { return nil, err @@ -240,7 +207,7 @@ func (s *Client) UpdateOrder(token string, orderUpdate *SnipcartOrderUpdate) (*S defer response.Body.Close() - var responseOrder SnipcartOrder + var responseOrder Order err = json.NewDecoder(response.Body).Decode(&responseOrder) if err != nil { return nil, err @@ -249,7 +216,7 @@ func (s *Client) UpdateOrder(token string, orderUpdate *SnipcartOrderUpdate) (*S return &responseOrder, nil } -func (s *Client) SendNotification(token string, notification *SnipcartNotification) (*SnipcartNotificationResponse, error) { +func (s *Client) SendNotification(token string, notification *Notification) (*NotificationResponse, error) { response, err := helper.Post(orderUri+"/"+token+"/notifications", "Basic", s.AuthBase64, notification) if err != nil { return nil, err @@ -257,7 +224,7 @@ func (s *Client) SendNotification(token string, notification *SnipcartNotificati defer response.Body.Close() - var responseNotification SnipcartNotificationResponse + var responseNotification NotificationResponse err = json.NewDecoder(response.Body).Decode(&responseNotification) if err != nil { return nil, err @@ -266,31 +233,7 @@ func (s *Client) SendNotification(token string, notification *SnipcartNotificati return &responseNotification, nil } -func (s *Client) ValidateWebhook(token string) error { - validateRequest, err := http.NewRequest("GET", validationUri+token, nil) - if err != nil { - return err - } - - client := &http.Client{} - - auth := base64.StdEncoding.EncodeToString([]byte(s.SnipcartKey + ":")) - validateRequest.Header.Set("Authorization", fmt.Sprintf("Basic %s", auth)) - validateRequest.Header.Set("Accept", "application/json") - - validateResponse, err := client.Do(validateRequest) - if err != nil { - return fmt.Errorf("error validating webhook: %s", err.Error()) - } - - if validateResponse.StatusCode < 200 || validateResponse.StatusCode >= 300 { - return fmt.Errorf("non-2XX status code for validating webhook: %d", validateResponse.StatusCode) - } - - return nil -} - -func (s *Client) GetProducts(queries map[string]string) (*SnipcartProductsResponse, error) { +func (s *Client) GetProducts(queries map[string]string) (*ProductsResponse, error) { response, err := helper.Get(productsUri, "Basic", s.AuthBase64, queries) if err != nil { return nil, err @@ -301,7 +244,7 @@ func (s *Client) GetProducts(queries map[string]string) (*SnipcartProductsRespon defer response.Body.Close() - var products SnipcartProductsResponse + var products ProductsResponse err = json.NewDecoder(response.Body).Decode(&products) if err != nil { return nil, err @@ -310,7 +253,7 @@ func (s *Client) GetProducts(queries map[string]string) (*SnipcartProductsRespon return &products, nil } -func (s *Client) GetProductById(id string) (*SnipcartProduct, error) { +func (s *Client) GetProductById(id string) (*Product, error) { response, err := helper.Get(productsUri, "Basic", s.AuthBase64, map[string]string{"userDefinedId": id}) if err != nil { return nil, err @@ -321,7 +264,7 @@ func (s *Client) GetProductById(id string) (*SnipcartProduct, error) { defer response.Body.Close() - var products SnipcartProductsResponse + var products ProductsResponse err = json.NewDecoder(response.Body).Decode(&products) if err != nil { return nil, err diff --git a/snipcart/webhook.go b/snipcart/webhook.go index dfa101c..fa3f6c8 100644 --- a/snipcart/webhook.go +++ b/snipcart/webhook.go @@ -1,21 +1,74 @@ package snipcart -type SnipcartShippingAddress struct { - FullName string `json:"fullName"` - FirstName string `json:"firstName"` - Name string `json:"name"` - Company string `json:"company"` - Address1 string `json:"address1"` - Address2 string `json:"address2"` - FullAddress string `json:"fullAddress"` - City string `json:"city"` - Country string `json:"country"` - PostalCode string `json:"postalCode"` - Province string `json:"province"` - Phone string `json:"phone"` - VatNumber string `json:"vatNumber,omitempty"` +import ( + "encoding/base64" + "fmt" + "net/http" + "time" +) + +type TaxShippingInfo struct { + Fees float64 `json:"fees"` + Method string `json:"method"` } -type SnipcartWebhookTaxResponse struct { - Taxes []SnipcartTax `json:"taxes"` +type TaxContent struct { + Created time.Time `json:"creationDate"` + Modified time.Time `json:"modificationDate"` + Token string `json:"token"` + Email string `json:"email"` + ShipToBillingAddress bool `json:"shipToBillingAddress"` + BillingAddress Address `json:"billingAddress"` + ShippingAddress Address `json:"shippingAddress"` + InvoiceNumber string `json:"invoiceNumber"` + ShippingInformation TaxShippingInfo `json:"shippingInformation"` + Items []Item `json:"items"` + Discounts []any `json:"discounts"` + CustomFields []CustomField `json:"customFields"` + Plans []any `json:"plans"` + Refunds []any `json:"refunds"` + Taxes []any `json:"taxes"` + Currency string `json:"currency"` + Total float64 `json:"total"` + DiscountsTotal float64 `json:"discountsTotal"` + ItemsTotal float64 `json:"itemsTotal"` + TaxesTotal float64 `json:"taxesTotal"` + PlansTotal float64 `json:"plansTotal"` + TaxProvider any `json:"taxProvider"` + Metadata any `json:"metadata"` +} + +type Tax struct { + Name string `json:"name"` + Amount float64 `json:"amount"` + NumberForInvoice string `json:"numberForInvoice"` + Rate float64 `json:"rate"` +} + +type TaxResponse struct { + Taxes []Tax `json:"taxes"` +} + +func (s *Client) ValidateWebhook(token string) error { + validateRequest, err := http.NewRequest("GET", validationUri+token, nil) + if err != nil { + return err + } + + client := &http.Client{} + + auth := base64.StdEncoding.EncodeToString([]byte(s.Key + ":")) + validateRequest.Header.Set("Authorization", fmt.Sprintf("Basic %s", auth)) + validateRequest.Header.Set("Accept", "application/json") + + validateResponse, err := client.Do(validateRequest) + if err != nil { + return fmt.Errorf("error validating webhook: %s", err.Error()) + } + + if validateResponse.StatusCode < 200 || validateResponse.StatusCode >= 300 { + return fmt.Errorf("non-2XX status code for validating webhook: %d", validateResponse.StatusCode) + } + + return nil }