commit 30fb57f4f7938a05c17affffe06d8571a370a003 Author: scheibling Date: Tue Apr 8 19:24:11 2025 +0200 Created diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..890997e --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Test coverage output +coverage*.* + +# postgres data volume used by postgres server container for testing purpose +testdata/postgres + +# server binary +./server + +# PID file generated to support live reload +.pid + +.idea/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..97d173f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,37 @@ +WooCommerce for golang Change Log +================================= + +## 1.0.4 under development + +-Bug: #1 Fixed signature error if query params include array value + +## 1.0.3 + +- Bug: Fixed date type params validation +- Bug: Fixed Setting group and option properties +- Enh: Add 501 error handling + +## 1.0.2 + +- Bug: Fixed parse string to float64 failed if an empty string +- New: Added data and report service tests +- Bug: Fixed an issue report query parameters did not take effect +- Bug: Fixed report struct error +- Chg: Modify order money field type from string to float64 + +## 1.0.1 + +- Enh: Perfect doc +- Enh: Perfect product variation query params validation +- Bug: Fixed All() method isLastPage return error +- Chg: Simplify query params process +- Bug: Fixed Include, Exclude query params type error +- Bug: Fixed shipping zone location endpoint error +- Enh: Set per page is max to 100 +- Bug: Fixed is last page check condition +- Enh: Add total and totalPages return in All() method +- Chg: product and product variation price, weight attribute change to float64 + +## 1.0.0 + +- Initial release. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8c3b89b --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022, hiscaler +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..9ec9e6b --- /dev/null +++ b/config/config.go @@ -0,0 +1,14 @@ +package config + +import "time" + +type Config struct { + Debug bool `json:"debug"` // 是否为调试模式 + URL string `json:"url"` // 店铺地址 + Version string `json:"version"` // API 版本 + ConsumerKey string `json:"consumer_key"` // Consumer Key + ConsumerSecret string `json:"consumer_secret"` // Consumer Secret + AddAuthenticationToURL bool `json:"add_authentication_to_url"` // 是否将认证信息附加到 URL 中 + Timeout time.Duration `json:"timeout"` // 超时时间(秒) + VerifySSL bool `json:"verify_ssl"` // 是否验证 SSL +} diff --git a/config/config_test.json b/config/config_test.json new file mode 100644 index 0000000..44c08ef --- /dev/null +++ b/config/config_test.json @@ -0,0 +1,10 @@ +{ + "debug": true, + "url": "http://127.0.0.1/", + "version": "v3", + "consumer_key": "", + "consumer_secret": "", + "add_authentication_to_url": false, + "timeout": 10, + "verify_ssl": true +} \ No newline at end of file diff --git a/constant/datetime.format.go b/constant/datetime.format.go new file mode 100644 index 0000000..89ccb85 --- /dev/null +++ b/constant/datetime.format.go @@ -0,0 +1,8 @@ +package constant + +const ( + DateFormat = "2006-01-02" + TimeFormat = "15:04:05" + DatetimeFormat = "2006-01-02 15:04:05" + WooDatetimeFormat = "2006-01-02T15:04:05" +) diff --git a/coupon.go b/coupon.go new file mode 100644 index 0000000..7d321a0 --- /dev/null +++ b/coupon.go @@ -0,0 +1,208 @@ +package woogo + +import ( + "errors" + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +// https://woocommerce.github.io/woocommerce-rest-api-docs/?php#coupon-properties + +type couponService service + +type CouponsQueryParams struct { + queryParams + Search string `url:"search,omitempty"` + After string `url:"after,omitempty"` + Before string `url:"before,omitempty"` + Exclude []int `url:"exclude,omitempty"` + Include []int `url:"include,omitempty"` + Code string `url:"code,omitempty"` +} + +func (m CouponsQueryParams) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Before, validation.When(m.Before != "", validation.By(func(value interface{}) error { + dateStr, _ := value.(string) + return IsValidateTime(dateStr) + }))), + validation.Field(&m.After, validation.When(m.After != "", validation.By(func(value interface{}) error { + dateStr, _ := value.(string) + return IsValidateTime(dateStr) + }))), + validation.Field(&m.OrderBy, validation.When(m.OrderBy != "", validation.In("id", "include", "date", "title", "slug").Error("无效的排序字段"))), + ) +} + +// All List all coupons +func (s couponService) All(params CouponsQueryParams) (items []entity.Coupon, total, totalPages int, isLastPage bool, err error) { + if err = params.Validate(); err != nil { + return + } + + params.TidyVars() + params.After = ToISOTimeString(params.After, false, true) + params.Before = ToISOTimeString(params.Before, true, false) + resp, err := s.httpClient.R().SetQueryParamsFromValues(toValues(params)).Get("/coupons") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + total, totalPages, isLastPage = parseResponseTotal(params.Page, resp) + } + return +} + +// One Retrieve a coupon +func (s couponService) One(id int) (item entity.Coupon, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/coupons/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Create + +type CreateCouponRequest struct { + Code string `json:"code"` + DiscountType string `json:"discount_type"` + Amount float64 `json:"amount,string"` + IndividualUse bool `json:"individual_use"` + ExcludeSaleItems bool `json:"exclude_sale_items"` + MinimumAmount float64 `json:"minimum_amount,string"` +} + +func (m CreateCouponRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.DiscountType, validation.In("percent", "fixed_cart", "fixed_product").Error("无效的折扣类型")), + validation.Field(&m.Amount, + validation.Min(0.0).Error("金额不能小于 {{.threshold}}"), + validation.When(m.DiscountType == "percent", validation.Max(100.0).Error("折扣比例不能大于 {{.threshold}}")), + ), + validation.Field(&m.MinimumAmount, validation.Min(0.0).Error("最小金额不能小于 {{.threshold}}")), + ) +} + +// Create Create a coupon +func (s couponService) Create(req CreateCouponRequest) (item entity.Coupon, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/coupons") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +type UpdateCouponRequest struct { + Code string `json:"code,omitempty"` + DiscountType string `json:"discount_type,omitempty"` + Amount float64 `json:"amount,omitempty,string"` + IndividualUse bool `json:"individual_use,omitempty"` + ExcludeSaleItems bool `json:"exclude_sale_items,omitempty"` + MinimumAmount float64 `json:"minimum_amount,omitempty,string"` +} + +func (m UpdateCouponRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.DiscountType, validation.When(m.DiscountType != "", validation.In("percent", "fixed_cart", "fixed_product").Error("无效的折扣类型"))), + validation.Field(&m.Amount, + validation.Min(0.0).Error("金额不能小于 {{.threshold}}"), + validation.When(m.DiscountType == "percent", validation.Max(100.0).Error("折扣比例不能大于 {{.threshold}}")), + ), + validation.Field(&m.MinimumAmount, validation.Min(0.0).Error("最小金额不能小于 {{.threshold}}")), + ) +} + +// Update Update a coupon +func (s couponService) Update(id int, req UpdateCouponRequest) (item entity.Coupon, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Put(fmt.Sprintf("/coupons/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Delete a coupon + +func (s couponService) Delete(id int, force bool) (item entity.Coupon, err error) { + resp, err := s.httpClient.R(). + SetBody(map[string]bool{"force": force}). + Delete(fmt.Sprintf("/coupons/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Batch update coupons + +type BatchCouponsCreateItem = CreateCouponRequest +type BatchCouponsUpdateItem struct { + ID string `json:"id"` + BatchCouponsCreateItem +} + +type BatchCouponsRequest struct { + Create []BatchCouponsCreateItem `json:"create,omitempty"` + Update []BatchCouponsUpdateItem `json:"update,omitempty"` + Delete []int `json:"delete,omitempty"` +} + +func (m BatchCouponsRequest) Validate() error { + if len(m.Create) == 0 && len(m.Update) == 0 && len(m.Delete) == 0 { + return errors.New("无效的请求数据") + } + return nil +} + +type BatchCouponsResult struct { + Create []entity.Coupon `json:"create"` + Update []entity.Coupon `json:"update"` + Delete []entity.Coupon `json:"delete"` +} + +// Batch Batch create/update/delete coupons +func (s couponService) Batch(req BatchCouponsRequest) (res BatchCouponsResult, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/coupons/batch") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &res) + } + return +} diff --git a/coupon_test.go b/coupon_test.go new file mode 100644 index 0000000..a5aadcb --- /dev/null +++ b/coupon_test.go @@ -0,0 +1,149 @@ +package woogo + +import ( + "errors" + "strings" + "testing" + + "git.cloudyne.io/go/hiscaler-gox/jsonx" + "git.cloudyne.io/go/hiscaler-gox/randx" + "git.cloudyne.io/go/woogo/entity" + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" +) + +func TestCouponService_All(t *testing.T) { + params := CouponsQueryParams{Search: ""} + params.PerPage = 100 + params.Order = SortDesc + items, _, _, _, err := wooClient.Services.Coupon.All(params) + if err != nil { + t.Errorf("wooClient.Services.Coupon.All error: %s", err.Error()) + } else { + t.Logf("items = %s", jsonx.ToPrettyJson(items)) + } +} + +func TestCouponService_One(t *testing.T) { + couponId := 4 + item, err := wooClient.Services.Coupon.One(couponId) + if err != nil { + t.Errorf("wooClient.Services.Coupon.One error: %s", err.Error()) + } else { + assert.Equal(t, couponId, item.ID, "coupon id") + } +} + +func TestCouponService_Create(t *testing.T) { + code := strings.ToLower(randx.Letter(8, false)) + req := CreateCouponRequest{ + Code: code, + DiscountType: "percent", + Amount: 1, + IndividualUse: false, + ExcludeSaleItems: false, + MinimumAmount: 2, + } + item, err := wooClient.Services.Coupon.Create(req) + if err != nil { + t.Fatalf("wooClient.Services.Coupon.Create error: %s", err.Error()) + } else { + assert.Equal(t, code, item.Code, "code") + } +} + +func TestCouponService_CreateUpdateDelete(t *testing.T) { + code := gofakeit.LetterN(8) + req := CreateCouponRequest{ + Code: code, + DiscountType: "percent", + Amount: 1, + IndividualUse: false, + ExcludeSaleItems: false, + MinimumAmount: 2, + } + var oldItem, newItem entity.Coupon + var err error + oldItem, err = wooClient.Services.Coupon.Create(req) + if err != nil { + t.Fatalf("wooClient.Services.Coupon.Create error: %s", err.Error()) + } else { + assert.Equal(t, code, oldItem.Code, "code") + } + + newItem, err = wooClient.Services.Coupon.One(oldItem.ID) + if err != nil { + t.Errorf("wooClient.Services.Customer.One(%d) error: %s", oldItem.ID, err.Error()) + } else { + updateReq := UpdateCouponRequest{ + Amount: 11, + IndividualUse: true, + ExcludeSaleItems: true, + MinimumAmount: 22, + } + newItem, err = wooClient.Services.Coupon.Update(oldItem.ID, updateReq) + if err != nil { + t.Fatalf("wooClient.Services.Coupon.Update error: %s", err.Error()) + } else { + assert.Equal(t, oldItem.Code, newItem.Code, "code") + assert.Equal(t, 11.0, newItem.Amount, "Amount") + assert.Equal(t, true, newItem.IndividualUse, "IndividualUse") + assert.Equal(t, true, newItem.ExcludeSaleItems, "ExcludeSaleItems") + assert.Equal(t, 22.0, newItem.MinimumAmount, "MinimumAmount") + } + + // Only change amount + updateReq = UpdateCouponRequest{Amount: 11.23} + newItem, err = wooClient.Services.Coupon.Update(oldItem.ID, updateReq) + if err != nil { + t.Fatalf("wooClient.Services.Coupon.Update error: %s", err.Error()) + } else { + assert.Equal(t, 11.23, newItem.Amount, "Amount") + assert.Equal(t, true, newItem.IndividualUse, "IndividualUse") + assert.Equal(t, true, newItem.ExcludeSaleItems, "ExcludeSaleItems") + assert.Equal(t, 22.0, newItem.MinimumAmount, "MinimumAmount") + } + + _, err = wooClient.Services.Coupon.Delete(oldItem.ID, true) + if err != nil { + t.Fatalf("wooClient.Services.Coupon.Delete(%d) error: %s", oldItem.ID, err.Error()) + } else { + _, err = wooClient.Services.Coupon.One(oldItem.ID) + if !errors.Is(err, ErrNotFound) { + t.Fatalf("wooClient.Services.Coupon.Delete(%d) failed", oldItem.ID) + } + } + } +} + +func TestCouponService_Batch(t *testing.T) { + n := 3 + createRequests := make([]BatchCouponsCreateItem, n) + codes := make([]string, n) + for i := 0; i < n; i++ { + code := strings.ToLower(gofakeit.LetterN(8)) + req := BatchCouponsCreateItem{ + Code: code, + DiscountType: "percent", + Amount: float64(i), + IndividualUse: false, + ExcludeSaleItems: false, + MinimumAmount: float64(i), + } + createRequests[i] = req + codes[i] = req.Code + } + batchReq := BatchCouponsRequest{ + Create: createRequests, + } + result, err := wooClient.Services.Coupon.Batch(batchReq) + if err != nil { + t.Fatalf("wooClient.Services.Coupon.Batch() error: %s", err.Error()) + } + assert.Equal(t, n, len(result.Create), "Batch create return len") + returnCodes := make([]string, 0) + for _, d := range result.Create { + returnCodes = append(returnCodes, d.Code) + } + assert.Equal(t, codes, returnCodes, "check codes is equal") +} diff --git a/customer.go b/customer.go new file mode 100644 index 0000000..4854f5a --- /dev/null +++ b/customer.go @@ -0,0 +1,219 @@ +package woogo + +import ( + "errors" + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + jsoniter "github.com/json-iterator/go" +) + +// https://woocommerce.github.io/woocommerce-rest-api-docs/?php#customers + +type customerService service + +type CustomersQueryParams struct { + queryParams + Search string `url:"search,omitempty"` + Exclude []int `url:"exclude,omitempty"` + Include []int `url:"include,omitempty"` + Email string `url:"email,omitempty"` + Role string `url:"role,omitempty"` +} + +func (m CustomersQueryParams) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.OrderBy, validation.When(m.OrderBy != "", validation.In("id", "include", "name", "registered_date").Error("无效的排序字段"))), + validation.Field(&m.Email, validation.When(m.Email != "", is.EmailFormat.Error("无效的邮箱"))), + validation.Field(&m.Role, validation.When(m.Role != "", validation.In("all", "administrator", "editor", "author", "contributor", "subscriber", "shop_manager").Error("无效的角色"))), + ) +} + +// All List all customers +func (s customerService) All(params CustomersQueryParams) (items []entity.Customer, total, totalPages int, isLastPage bool, err error) { + if err = params.Validate(); err != nil { + return + } + + params.TidyVars() + resp, err := s.httpClient.R().SetQueryParamsFromValues(toValues(params)).Get("/customers") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + total, totalPages, isLastPage = parseResponseTotal(params.Page, resp) + } + return +} + +// One Retrieve a customer +func (s customerService) One(id int) (item entity.Customer, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/customers/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// CreateCustomerRequest Create customer request +type CreateCustomerRequest struct { + Email string `json:"email,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Billing *entity.Billing `json:"billing,omitempty"` + Shipping *entity.Shipping `json:"shipping,omitempty"` + MetaData []entity.Meta `json:"meta_data,omitempty"` +} + +func (m CreateCustomerRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Email, + validation.Required.Error("邮箱不能为空"), + is.EmailFormat.Error("无效的邮箱"), + ), + validation.Field(&m.FirstName, validation.Required.Error("姓不能为空")), + validation.Field(&m.LastName, validation.Required.Error("名不能为空")), + validation.Field(&m.Username, validation.Required.Error("登录帐号不能为空")), + validation.Field(&m.Password, validation.Required.Error("登录密码不能为空")), + validation.Field(&m.Billing), + ) +} + +// Create create a customer +func (s customerService) Create(req CreateCustomerRequest) (item entity.Customer, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/customers") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Update customer + +type UpdateCustomerRequest struct { + Email string `json:"email,omitempty"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Billing *entity.Billing `json:"billing,omitempty"` + Shipping *entity.Shipping `json:"shipping,omitempty"` + MetaData []entity.Meta `json:"meta_data,omitempty"` +} + +func (m UpdateCustomerRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Email, validation.When(m.Email != "", is.EmailFormat.Error("无效的邮箱"))), + validation.Field(&m.Billing), + ) +} + +// Update update a customer +func (s customerService) Update(id int, req UpdateCustomerRequest) (item entity.Customer, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Put(fmt.Sprintf("/customers/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Delete Delete a customer + +type customerDeleteParams struct { + Force bool `json:"force,omitempty"` + Reassign []int `json:"reassign,omitempty"` +} + +func (s customerService) Delete(id int, params customerDeleteParams) (item entity.Customer, err error) { + resp, err := s.httpClient.R().SetBody(params).Delete(fmt.Sprintf("/customers/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Batch update customers + +type BatchCreateCustomerRequest = CreateCustomerRequest + +type BatchUpdateCustomerRequest struct { + ID string `json:"id"` + BatchCreateCustomerRequest +} + +type BatchCustomerRequest struct { + Create []BatchCreateCustomerRequest `json:"create,omitempty"` + Update []BatchUpdateCustomerRequest `json:"update,omitempty"` + Delete []int `json:"delete,omitempty"` +} + +func (m BatchCustomerRequest) Validate() error { + if len(m.Create) == 0 && len(m.Update) == 0 && len(m.Delete) == 0 { + return errors.New("无效的请求数据") + } + return nil +} + +type BatchCustomerResult struct { + Create []entity.Customer `json:"create"` + Update []entity.Customer `json:"update"` + Delete []entity.Customer `json:"delete"` +} + +// Batch Batch create/update/delete customers +func (s customerService) Batch(req BatchCustomerRequest) (res BatchCustomerResult, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/customers/batch") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &res) + } + return +} + +// Downloads retrieve a customer downloads +func (s customerService) Downloads(customerId int) (items []entity.CustomerDownload, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/customers/%d/downloads", customerId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} diff --git a/customer_test.go b/customer_test.go new file mode 100644 index 0000000..6390eda --- /dev/null +++ b/customer_test.go @@ -0,0 +1,189 @@ +package woogo + +import ( + "errors" + "testing" + + "git.cloudyne.io/go/hiscaler-gox/jsonx" + "git.cloudyne.io/go/woogo/entity" + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" +) + +func getCustomerId(t *testing.T) { + t.Log("Execute getCustomerId test") + params := CustomersQueryParams{} + params.Page = 1 + params.PerPage = 1 + items, _, _, _, err := wooClient.Services.Customer.All(params) + if err != nil || len(items) == 0 { + t.FailNow() + } + if len(items) == 0 { + t.Fatalf("getCustomerId not found one customer") + } + mainId = items[0].ID +} + +func TestCustomerService_All(t *testing.T) { + params := CustomersQueryParams{} + _, _, _, _, err := wooClient.Services.Customer.All(params) + if err != nil { + t.Errorf("wooClient.Services.Customer.All error: %s", err.Error()) + } +} + +func TestCustomerService_One(t *testing.T) { + t.Run("getCustomerId", getCustomerId) + item, err := wooClient.Services.Customer.One(mainId) + if err != nil { + t.Fatalf("wooClient.Services.Customer.One error: %s", err.Error()) + } + assert.Equal(t, mainId, item.ID, "customer id") +} + +func TestCustomerService_Create(t *testing.T) { + gofakeit.Seed(0) + address := gofakeit.Address() + req := CreateCustomerRequest{ + Email: gofakeit.Email(), + FirstName: gofakeit.FirstName(), + LastName: gofakeit.LastName(), + Username: gofakeit.Username(), + Password: gofakeit.Password(true, true, true, false, false, 10), + MetaData: nil, + Billing: &entity.Billing{ + FirstName: gofakeit.FirstName(), + LastName: gofakeit.LastName(), + Company: gofakeit.Company(), + Address1: address.Address, + Address2: "", + City: address.City, + State: address.State, + Postcode: address.Zip, + Country: address.Country, + Email: gofakeit.Email(), + Phone: gofakeit.Phone(), + }, + } + item, err := wooClient.Services.Customer.Create(req) + if err != nil { + t.Errorf("wooClient.Services.Customer.Create error: %s", err.Error()) + } else { + t.Logf("item = %#v", item) + } +} + +func TestCustomerService_CreateUpdateDelete(t *testing.T) { + gofakeit.Seed(0) + // Create + var oldItem, newItem entity.Customer + var err error + address := gofakeit.Address() + req := CreateCustomerRequest{ + Email: gofakeit.Email(), + FirstName: gofakeit.FirstName(), + LastName: gofakeit.LastName(), + Username: gofakeit.Username(), + Password: gofakeit.Password(true, true, true, false, false, 10), + MetaData: nil, + Billing: &entity.Billing{ + FirstName: gofakeit.FirstName(), + LastName: gofakeit.LastName(), + Company: gofakeit.Company(), + Address1: address.Address, + Address2: "", + City: address.City, + State: address.State, + Postcode: address.Zip, + Country: address.Country, + Email: gofakeit.Email(), + Phone: gofakeit.Phone(), + }, + } + oldItem, err = wooClient.Services.Customer.Create(req) + if err != nil { + t.Fatalf("wooClient.Services.Customer.Create error: %s", err.Error()) + } + + // Update + afterData := struct { + email string + billingFirstName string + billingLastName string + }{ + email: gofakeit.Email(), + billingFirstName: gofakeit.FirstName(), + billingLastName: gofakeit.LastName(), + } + updateReq := UpdateCustomerRequest{ + Email: afterData.email, + Billing: &entity.Billing{ + FirstName: afterData.billingFirstName, + LastName: afterData.billingLastName, + }, + } + newItem, err = wooClient.Services.Customer.Update(oldItem.ID, updateReq) + if err != nil { + t.Fatalf("wooClient.Services.Customer.Update error: %s", err.Error()) + } else { + assert.Equal(t, afterData.email, newItem.Email, "email") + assert.Equal(t, afterData.billingFirstName, newItem.Billing.FirstName, "billing first name") + assert.Equal(t, afterData.billingLastName, newItem.Billing.LastName, "billing last name") + } + + // Delete + _, err = wooClient.Services.Customer.Delete(oldItem.ID, customerDeleteParams{Force: true}) + if err != nil { + t.Fatalf("wooClient.Services.Customer.Delete(%d) error: %s", oldItem.ID, err.Error()) + } + + // Query check is exists + _, err = wooClient.Services.Customer.One(oldItem.ID) + if !errors.Is(err, ErrNotFound) { + t.Fatalf("wooClient.Services.Customer.Delete(%d) failed", oldItem.ID) + } +} + +func TestCustomerService_Batch(t *testing.T) { + n := 3 + createRequests := make([]BatchCreateCustomerRequest, n) + emails := make([]string, n) + for i := 0; i < n; i++ { + req := BatchCreateCustomerRequest{ + Email: gofakeit.Email(), + FirstName: gofakeit.FirstName(), + LastName: gofakeit.LastName(), + Username: gofakeit.Username(), + Password: gofakeit.Password(true, true, true, false, false, 10), + Billing: nil, + Shipping: nil, + MetaData: nil, + } + createRequests[i] = req + emails[i] = req.Email + } + batchReq := BatchCustomerRequest{ + Create: createRequests, + } + result, err := wooClient.Services.Customer.Batch(batchReq) + if err != nil { + t.Fatalf("wooClient.Services.Customer.Batch() error: %s", err.Error()) + } + assert.Equal(t, n, len(result.Create), "Batch create return len") + returnEmails := make([]string, 0) + for _, d := range result.Create { + returnEmails = append(returnEmails, d.Email) + } + assert.Equal(t, emails, returnEmails, "check emails is equal") +} + +func TestCustomerService_Downloads(t *testing.T) { + // todo + items, err := wooClient.Services.Customer.Downloads(0) + if err != nil { + t.Fatalf("wooClient.Services.Customer.Downloads() error: %s", err.Error()) + } else { + t.Logf("items = %s", jsonx.ToPrettyJson(items)) + } +} diff --git a/data.go b/data.go new file mode 100644 index 0000000..7e76538 --- /dev/null +++ b/data.go @@ -0,0 +1,114 @@ +package woogo + +import ( + "fmt" + + "git.cloudyne.io/go/woogo/entity" + jsoniter "github.com/json-iterator/go" +) + +type dataService service + +// All list all data +func (s dataService) All() (items []entity.Data, err error) { + resp, err := s.httpClient.R().Get("/data") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// Continents list all continents +func (s dataService) Continents() (items []entity.Continent, err error) { + resp, err := s.httpClient.R().Get("/data/continents") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// Continent retrieve continent data +func (s dataService) Continent(code string) (item entity.Continent, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/data/continents/%s", code)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Countries list all countries +func (s dataService) Countries() (items []entity.Country, err error) { + resp, err := s.httpClient.R().Get("/data/countries") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// Country retrieve country data +func (s dataService) Country(code string) (item entity.Country, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/data/countries/%s", code)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Currencies list all currencies +func (s dataService) Currencies() (items []entity.Currency, err error) { + resp, err := s.httpClient.R().Get("/data/currencies") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// Currency retrieve currency data +func (s dataService) Currency(code string) (item entity.Currency, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/data/currencies/%s", code)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// CurrentCurrency retrieve current currency +func (s dataService) CurrentCurrency() (item entity.Currency, err error) { + resp, err := s.httpClient.R().Get("/data/currencies/current") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} diff --git a/data_test.go b/data_test.go new file mode 100644 index 0000000..175514c --- /dev/null +++ b/data_test.go @@ -0,0 +1,32 @@ +package woogo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDataService_All(t *testing.T) { + _, err := wooClient.Services.Data.All() + assert.Equal(t, nil, err) +} + +func TestDataService_Countries(t *testing.T) { + _, err := wooClient.Services.Data.Countries() + assert.Equal(t, nil, err) +} + +func TestDataService_Currencies(t *testing.T) { + _, err := wooClient.Services.Data.Currencies() + assert.Equal(t, nil, err) +} + +func TestDataService_Continents(t *testing.T) { + _, err := wooClient.Services.Data.Continents() + assert.Equal(t, nil, err) +} + +func TestDataService_Continent(t *testing.T) { + _, err := wooClient.Services.Data.Continent("AF") + assert.Equal(t, nil, err) +} diff --git a/entity/billing.go b/entity/billing.go new file mode 100644 index 0000000..d3efed8 --- /dev/null +++ b/entity/billing.go @@ -0,0 +1,29 @@ +package entity + +import ( + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" +) + +// Billing order billing properties +type Billing struct { + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Company string `json:"company,omitempty"` + Address1 string `json:"address_1,omitempty"` + Address2 string `json:"address_2,omitempty"` + City string `json:"city,omitempty"` + State string `json:"state,omitempty"` + Postcode string `json:"postcode,omitempty"` + Country string `json:"country,omitempty"` + Email string `json:"email,omitempty"` + Phone string `json:"phone,omitempty"` +} + +func (m Billing) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Email, validation.When(m.Email != "", is.EmailFormat.Error("无效的邮箱"))), + validation.Field(&m.FirstName, validation.Required.Error("姓不能为空")), + validation.Field(&m.LastName, validation.Required.Error("名不能为空")), + ) +} diff --git a/entity/coupon.go b/entity/coupon.go new file mode 100644 index 0000000..af707a8 --- /dev/null +++ b/entity/coupon.go @@ -0,0 +1,32 @@ +package entity + +// Coupon coupon properties +type Coupon struct { + ID int `json:"id"` + Code string `json:"code"` + Amount float64 `json:"amount"` + DateCreated string `json:"date_created"` + DateCreatedGMT string `json:"date_created_gmt"` + DateModified string `json:"date_modified"` + DateModifiedGMT string `json:"date_modified_gmt"` + DiscountType string `json:"discount_type"` + Description string `json:"description"` + DateExpires string `json:"date_expires"` + DateExpiresGMT string `json:"date_expires_gmt"` + UsageCount int `json:"usage_count"` + IndividualUse bool `json:"individual_use"` + ProductIDs []int `json:"product_ids"` + ExcludedProductIDs []int `json:"excluded_product_ids"` + UsageLimit int `json:"usage_limit"` + UsageLimitPerUser int `json:"usage_limit_per_user"` + LimitUsageToXItems int `json:"limit_usage_to_x_items"` + FreeShipping bool `json:"free_shipping"` + ProductCategories []int `json:"product_categories"` + ExcludedProductCategories []int `json:"excluded_product_categories"` + ExcludeSaleItems bool `json:"exclude_sale_items"` + MinimumAmount float64 `json:"minimum_amount"` + MaximumAmount float64 `json:"maximum_amount"` + EmailRestrictions []string `json:"email_restrictions"` + UsedBy []int `json:"used_by"` + MetaData []Meta `json:"meta_data"` +} diff --git a/entity/customer.go b/entity/customer.go new file mode 100644 index 0000000..252aae9 --- /dev/null +++ b/entity/customer.go @@ -0,0 +1,21 @@ +package entity + +// Customer customer properties +type Customer struct { + ID int `json:"id"` + DateCreated string `json:"date_created"` + DateCreatedGMT string `json:"date_created_gmt"` + DateModified string `json:"date_modified"` + DateModifiedGMT string `json:"date_modified_gmt"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Role string `json:"role"` + Username string `json:"username"` + Password string `json:"password"` + Billing Billing `json:"billing"` + Shipping Shipping `json:"shipping"` + IsPayingCustomer bool `json:"is_paying_customer"` + AvatarURL string `json:"avatar_url"` + MetaData []Meta `json:"meta_data"` +} diff --git a/entity/customer_download.go b/entity/customer_download.go new file mode 100644 index 0000000..df6a8d6 --- /dev/null +++ b/entity/customer_download.go @@ -0,0 +1,18 @@ +package entity + +// https://woocommerce.github.io/woocommerce-rest-api-docs/?php#retrieve-customer-downloads + +// CustomerDownload customer download properties +type CustomerDownload struct { + DownloadId string `json:"download_id"` + DownloadURL string `json:"download_url"` + ProductId string `json:"product_id"` + ProductName string `json:"product_name"` + DownloadName string `json:"download_name"` + OrderId int `json:"order_id"` + OrderKey string `json:"order_key"` + DownloadRemaining string `json:"download_remaining"` + AccessExpires string `json:"access_expires"` + AccessExpiresGMT string `json:"access_expires_gmt"` + File CustomerDownloadFile `json:"file"` +} diff --git a/entity/customer_download_file.go b/entity/customer_download_file.go new file mode 100644 index 0000000..9ce77af --- /dev/null +++ b/entity/customer_download_file.go @@ -0,0 +1,8 @@ +package entity + +// https://woocommerce.github.io/woocommerce-rest-api-docs/?php#retrieve-customer-downloads + +type CustomerDownloadFile struct { + Name string `json:"name"` + File string `json:"file"` +} diff --git a/entity/data.go b/entity/data.go new file mode 100644 index 0000000..2aaaff4 --- /dev/null +++ b/entity/data.go @@ -0,0 +1,48 @@ +package entity + +// Data data properties +type Data struct { + Slug string `json:"slug"` + Description string `json:"description"` +} + +// Continent continent properties +type Continent struct { + Code string `json:"code"` + Name string `json:"name"` + Countries []ContinentCountry `json:"countries"` // Only code, name, []state? +} + +// ContinentCountry continent country properties +type ContinentCountry struct { + Code string `json:"code"` + CurrencyCode string `json:"currency_code"` + CurrencyPos string `json:"currency_pos"` + DecimalSep string `json:"decimal_sep"` + DimensionUnit string `json:"dimension_unit"` + Name string `json:"name"` + NumDecimals int `json:"num_decimals"` + States []State `json:"states"` + ThousandSep string `json:"thousand_sep"` + WeightUnit string `json:"weight_unit"` +} + +// State state properties +type State struct { + Code string `json:"code"` + Name string `json:"name"` +} + +// Country country properties +type Country struct { + Code string `json:"code"` + Name string `json:"name"` + States []State `json:"states"` +} + +// Currency currency properties +type Currency struct { + Code string `json:"code"` + Name string `json:"name"` + Symbol string `json:"symbol"` +} diff --git a/entity/image.go b/entity/image.go new file mode 100644 index 0000000..677d126 --- /dev/null +++ b/entity/image.go @@ -0,0 +1,13 @@ +package entity + +// ProductImage product iamge properties +type ProductImage struct { + ID int `json:"id"` + DateCreated string `json:"date_created"` + DateCreatedGMT string `json:"date_created_gmt"` + DateModified string `json:"date_modified"` + DateModifiedGMT string `json:"date_modified_gmt"` + Src string `json:"src"` + Name string `json:"name"` + Alt string `json:"alt"` +} diff --git a/entity/meta.go b/entity/meta.go new file mode 100644 index 0000000..af44205 --- /dev/null +++ b/entity/meta.go @@ -0,0 +1,7 @@ +package entity + +type Meta struct { + ID int `json:"id"` + Key string `json:"key"` + Value string `json:"value"` +} diff --git a/entity/order.go b/entity/order.go new file mode 100644 index 0000000..833a14e --- /dev/null +++ b/entity/order.go @@ -0,0 +1,91 @@ +package entity + +type LineItem struct { + ID int `json:"id"` + Name string `json:"name"` + ProductId int `json:"product_id"` + VariationId int `json:"variation_id"` + Quantity int `json:"quantity"` + TaxClass string `json:"tax_class"` + SubTotal float64 `json:"subtotal"` + SubTotalTax float64 `json:"subtotal_tax"` + Total float64 `json:"total"` + TotalTax float64 `json:"total_tax"` + Taxes []Tax `json:"taxes"` + MetaData []Meta `json:"meta_data"` + SKU string `json:"sku"` + Price float64 `json:"price"` + ParentName string `json:"parent_name"` +} + +type FeeLine struct { + ID int `json:"id"` + Name string `json:"name"` + TaxClass string `json:"tax_class"` + TaxStatus string `json:"tax_status"` + Total float64 `json:"total"` + TotalTax float64 `json:"total_tax"` + Taxes []Tax `json:"taxes"` + MetaData []Meta `json:"meta_data"` +} + +type CouponLine struct { + ID int `json:"id"` + Code string `json:"code"` + Discount float64 `json:"discount"` + DiscountTax float64 `json:"discount_tax"` + MetaData []Meta `json:"meta_data"` +} + +type Refund struct { + ID int `json:"id"` + Reason string `json:"reason"` + Total float64 `json:"total"` +} + +// Order order properties +type Order struct { + ID int `json:"id"` + ParentId int `json:"parent_id"` + Number string `json:"number"` + OrderKey string `json:"order_key"` + CreatedVia string `json:"created_via"` + Version string `json:"version"` + Status string `json:"status"` + Currency string `json:"currency"` + CurrencySymbol string `json:"currency_symbol"` + DateCreated string `json:"date_created"` + DateCreatedGMT string `json:"date_created_gmt"` + DateModified string `json:"date_modified"` + DateModifiedGMT string `json:"date_modified_gmt"` + DiscountTotal float64 `json:"discount_total"` + DiscountTax float64 `json:"discount_tax"` + ShippingTotal float64 `json:"shipping_total"` + ShippingTax float64 `json:"shipping_tax"` + CartTax float64 `json:"cart_tax"` + Total float64 `json:"total"` + TotalTax float64 `json:"total_tax"` + PricesIncludeTax bool `json:"prices_include_tax"` + CustomerId int `json:"customer_id"` + CustomerIpAddress string `json:"customer_ip_address"` + CustomerUserAgent string `json:"customer_user_agent"` + CustomerNote string `json:"customer_note"` + Billing Billing `json:"billing"` + Shipping Shipping `json:"shipping"` + PaymentMethod string `json:"payment_method"` + PaymentMethodTitle string `json:"payment_method_title"` + TransactionId string `json:"transaction_id"` + DatePaid string `json:"date_paid"` + DatePaidGMT string `json:"date_paid_gmt"` + DateCompleted string `json:"date_completed"` + DateCompletedGMT string `json:"date_completed_gmt"` + CartHash string `json:"cart_hash"` + MetaData []Meta `json:"meta_data"` + LineItems []LineItem `json:"line_items"` + TaxLines []TaxLine `json:"tax_lines"` + ShippingLines []ShippingLine `json:"shipping_lines"` + FeeLines []FeeLine `json:"fee_lines"` + CouponLines []CouponLine `json:"coupon_lines"` + Refunds []Refund `json:"refunds"` + SetPaid bool `json:"set_paid"` +} diff --git a/entity/order_note.go b/entity/order_note.go new file mode 100644 index 0000000..f874708 --- /dev/null +++ b/entity/order_note.go @@ -0,0 +1,12 @@ +package entity + +// OrderNote order note properties +type OrderNote struct { + ID int `json:"id"` + Author string `json:"author"` + DateCreated string `json:"date_created"` + DateCreatedGMT string `json:"date_created_gmt"` + Note string `json:"note"` + CustomerNote bool `json:"customer_note"` + AddedByUser bool `json:"added_by_user"` +} diff --git a/entity/order_refund.go b/entity/order_refund.go new file mode 100644 index 0000000..cfeed08 --- /dev/null +++ b/entity/order_refund.go @@ -0,0 +1,34 @@ +package entity + +// OrderRefund order refund properties +type OrderRefund struct { + ID int `json:"id"` + DateCreated string `json:"date_created"` + DateCreatedGMT string `json:"date_created_gmt"` + Amount float64 `json:"amount"` + Reason string `json:"reason"` + RefundedBy int `json:"refunded_by"` + RefundedPayment bool `json:"refunded_payment"` + MetaData []Meta `json:"meta_data"` + LineItems []OrderRefundLineItem `json:"line_items"` + APIRefund bool `json:"api_refund"` +} + +// OrderRefundLineItem order refund line item properties +type OrderRefundLineItem struct { + ID int `json:"id"` + Name string `json:"name"` + ProductId int `json:"product_id"` + VariationId int `json:"variation_id"` + Quantity int `json:"quantity"` + TaxClass int `json:"tax_class"` + SubTotal float64 `json:"subtotal"` + SubTotalTax float64 `json:"subtotal_tax"` + Total float64 `json:"total"` + TotalTax float64 `json:"total_tax"` + Taxes []Tax `json:"taxes"` + MetaData []Meta `json:"meta_data"` + SKU string `json:"sku"` + Price float64 `json:"price"` + RefundTotal float64 `json:"refund_total"` +} diff --git a/entity/payment_gateway.go b/entity/payment_gateway.go new file mode 100644 index 0000000..cb8bc7a --- /dev/null +++ b/entity/payment_gateway.go @@ -0,0 +1,25 @@ +package entity + +// PaymentGateway payment gateway properties +type PaymentGateway struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Order int `json:"order"` + Enabled bool `json:"enabled"` + MethodTitle string `json:"method_title"` + MethodDescription string `json:"method_description"` + MethodSupports []string `json:"method_supports"` + Settings map[string]PaymentGatewaySetting `json:"settings"` +} + +type PaymentGatewaySetting struct { + ID string `json:"id"` + Label string `json:"label"` + Description string `json:"description"` + Type string `json:"type"` + Value string `json:"value"` + Default string `json:"default"` + Tip string `json:"tip"` + Placeholder string `json:"placeholder"` +} diff --git a/entity/porduct_default_attribute.go b/entity/porduct_default_attribute.go new file mode 100644 index 0000000..87a3e2b --- /dev/null +++ b/entity/porduct_default_attribute.go @@ -0,0 +1,8 @@ +package entity + +// ProductDefaultAttribute product default attribute properties +type ProductDefaultAttribute struct { + ID int `json:"id"` + Name string `json:"name"` + Option string `json:"option"` +} diff --git a/entity/product.go b/entity/product.go new file mode 100644 index 0000000..3ce6882 --- /dev/null +++ b/entity/product.go @@ -0,0 +1,86 @@ +package entity + +type ProductDimension struct { + Length float64 `json:"length"` + Width float64 `json:"width"` + Height float64 `json:"height"` +} + +// Product product properties +type Product struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Permalink string `json:"permalink"` + DateCreated string `json:"date_created"` + DateCreatedGMT string `json:"date_created_gmt"` + DateModified string `json:"date_modified"` + DateModifiedGMT string `json:"date_modified_gmt"` + Type string `json:"type"` + Status string `json:"status"` + Featured bool `json:"featured"` + CatalogVisibility string `json:"catalog_visibility"` + Description string `json:"description"` + ShortDescription string `json:"short_description"` + SKU string `json:"sku"` + Price float64 `json:"price"` + RegularPrice float64 `json:"regular_price"` + SalePrice float64 `json:"sale_price"` + DateOnSaleFrom string `json:"date_on_sale_from"` + DateOnSaleFromGMT string `json:"date_on_sale_from_gmt"` + DateOnSaleTo string `json:"date_on_sale_to"` + DateOnSaleToGMT string `json:"date_on_sale_to_gmt"` + PriceHtml string `json:"price_html"` + OnSale bool `json:"on_sale"` + Purchasable bool `json:"purchasable"` + TotalSales int `json:"total_sales"` + Virtual bool `json:"virtual"` + Downloadable bool `json:"downloadable"` + Downloads []ProductDownload `json:"downloads"` + DownloadLimit int `json:"download_limit"` + DownloadExpiry int `json:"download_expiry"` + ExternalUrl string `json:"external_url"` + ButtonText string `json:"button_text"` + TaxStatus string `json:"tax_status"` + TaxClass string `json:"tax_class"` + ManageStock bool `json:"manage_stock"` + StockQuantity int `json:"stock_quantity"` + StockStatus string `json:"stock_status"` + Backorders string `json:"backorders"` + BackordersAllowed bool `json:"backorders_allowed"` + Backordered bool `json:"backordered"` + SoldIndividually bool `json:"sold_individually"` + Weight float64 `json:"weight"` + Dimensions *ProductDimension `json:"dimensions"` + ShippingRequired bool `json:"shipping_required"` + ShippingTaxable bool `json:"shipping_taxable"` + ShippingClass string `json:"shipping_class"` + ShippingClassId int `json:"shipping_class_id"` + ReviewsAllowed bool `json:"reviews_allowed"` + AverageRating float64 `json:"average_rating"` + RatingCount int `json:"rating_count"` + RelatedIds []int `json:"related_ids"` + UpsellIds []int `json:"upsell_ids"` + CrossSellIds []int `json:"cross_sell_ids"` + ParentId int `json:"parent_id"` + PurchaseNote string `json:"purchase_note"` + Categories []ProductCategory `json:"categories"` + Tags []ProductTag `json:"tags"` + Images []ProductImage `json:"images"` + Attributes []ProductAttributeItem `json:"attributes"` + DefaultAttributes []ProductDefaultAttribute `json:"default_attributes"` + Variations []int `json:"variations"` + GroupedProducts []int `json:"grouped_products"` + MenuOrder int `json:"menu_order"` + MetaData []Meta `json:"meta_data"` +} + +// ProductAttributeItem product attribute properties +type ProductAttributeItem struct { + ID int `json:"id"` + Name string `json:"name"` + Position int `json:"position"` + Visible bool `json:"visible"` + Variation bool `json:"variation"` + Options []string `json:"options"` +} diff --git a/entity/product_attribute.go b/entity/product_attribute.go new file mode 100644 index 0000000..72d14cd --- /dev/null +++ b/entity/product_attribute.go @@ -0,0 +1,11 @@ +package entity + +// ProductAttribute product attribute properties +type ProductAttribute struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Type string `json:"type"` + OrderBy string `json:"order_by"` + HasArchives bool `json:"has_archives"` +} diff --git a/entity/product_attribute_term.go b/entity/product_attribute_term.go new file mode 100644 index 0000000..ac54402 --- /dev/null +++ b/entity/product_attribute_term.go @@ -0,0 +1,11 @@ +package entity + +// ProductAttributeTerm product attribute term properties +type ProductAttributeTerm struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` + MenuOrder int `json:"menu_order"` + Count int `json:"count"` +} diff --git a/entity/product_category.go b/entity/product_category.go new file mode 100644 index 0000000..811009b --- /dev/null +++ b/entity/product_category.go @@ -0,0 +1,14 @@ +package entity + +// ProductCategory product category properties +type ProductCategory struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Parent int `json:"parent"` + Description string `json:"description"` + Display string `json:"display"` + Image *ProductImage `json:"image,omitempty"` + MenuOrder int `json:"menu_order"` + Count int `json:"count"` +} diff --git a/entity/product_download.go b/entity/product_download.go new file mode 100644 index 0000000..93eb3c2 --- /dev/null +++ b/entity/product_download.go @@ -0,0 +1,8 @@ +package entity + +// ProductDownload product download properties +type ProductDownload struct { + ID string `json:"id"` + Name string `json:"name"` + File string `json:"file"` +} diff --git a/entity/product_review.go b/entity/product_review.go new file mode 100644 index 0000000..2360fe3 --- /dev/null +++ b/entity/product_review.go @@ -0,0 +1,15 @@ +package entity + +// ProductReview product review properties +type ProductReview struct { + ID int `json:"id"` + DateCreated string `json:"date_created"` + DateCreatedGMT string `json:"date_created_gmt"` + ProductId int `json:"product_id"` + Status string `json:"status"` + Reviewer string `json:"reviewer"` + ReviewerEmail string `json:"reviewer_email"` + Review string `json:"review"` + Rating int `json:"rating"` + Verified bool `json:"verified"` +} diff --git a/entity/product_shipping_class.go b/entity/product_shipping_class.go new file mode 100644 index 0000000..0394aa7 --- /dev/null +++ b/entity/product_shipping_class.go @@ -0,0 +1,10 @@ +package entity + +// ProductShippingClass product shipping class properties +type ProductShippingClass struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` + Count int `json:"count"` +} diff --git a/entity/product_tag.go b/entity/product_tag.go new file mode 100644 index 0000000..89b112a --- /dev/null +++ b/entity/product_tag.go @@ -0,0 +1,10 @@ +package entity + +// ProductTag product tag properties +type ProductTag struct { + ID int `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` + Count int `json:"count"` +} diff --git a/entity/product_variation.go b/entity/product_variation.go new file mode 100644 index 0000000..30943ab --- /dev/null +++ b/entity/product_variation.go @@ -0,0 +1,55 @@ +package entity + +import ( + "time" +) + +// ProductVariationAttribute product variation attribute properties +type ProductVariationAttribute struct { + ID int `json:"id"` + Name string `json:"name"` + Option string `json:"option"` +} + +// ProductVariation product variation properties +type ProductVariation struct { + ID int `json:"id"` + DateCreate time.Time `json:"date_create,omitempty"` + DateCreateGMT time.Time `json:"date_create_gmt,omitempty"` + DateModified time.Time `json:"date_modified,omitempty"` + DateModifiedGMT time.Time `json:"date_modified_gmt,omitempty"` + Description string `json:"description"` + Permalink string `json:"permalink"` + SKU string `json:"sku"` + Price float64 `json:"price"` + RegularPrice float64 `json:"regular_price"` + SalePrice float64 `json:"sale_price"` + DateOnSaleFrom time.Time `json:"date_on_sale_from"` + DateOnSaleFromGMT time.Time `json:"date_on_sale_from_gmt"` + DateOnSaleTo time.Time `json:"date_on_sale_to"` + DateOnSaleToGMT time.Time `json:"date_on_sale_to_gmt"` + OnSale bool `json:"on_sale"` + Status string `json:"status"` + Purchasable bool `json:"purchasable"` + Virtual bool `json:"virtual"` + Downloadable bool `json:"downloadable"` + Downloads []ProductDownload `json:"downloads"` + DownloadLimit int `json:"download_limit"` + DownloadExpiry int `json:"download_expiry"` + TaxStatus string `json:"tax_status"` + TaxClass string `json:"tax_class"` + ManageStock bool `json:"manage_stock"` + StockQuantity int `json:"stock_quantity"` + StockStatus string `json:"stock_status"` + Backorders string `json:"backorders"` + BackordersAllowed bool `json:"backorders_allowed"` + Backordered bool `json:"backordered"` + Weight float64 `json:"weight"` + // ProductDimension *request.VariationDimensionsRequest `json:"dimensions"` + ShippingClass string `json:"shipping_class"` + ShippingClassId int `json:"shipping_class_id"` + Image *ProductImage `json:"image"` + Attributes []ProductVariationAttribute `json:"attributes"` + MenuOrder int `json:"menu_order"` + MetaData []Meta `json:"meta_data"` +} diff --git a/entity/report.go b/entity/report.go new file mode 100644 index 0000000..294399a --- /dev/null +++ b/entity/report.go @@ -0,0 +1,57 @@ +package entity + +// Report report properties +type Report struct { + Slug string `json:"slug"` + Description string `json:"description"` +} + +type SaleReport struct { + TotalSales float64 `json:"total_sales"` + NetSales float64 `json:"net_sales"` + AverageSales string `json:"average_sales"` + TotalOrders int `json:"total_orders"` + TotalItems int `json:"total_items"` + TotalTax float64 `json:"total_tax"` + TotalShipping float64 `json:"total_shipping"` + TotalRefunds int `json:"total_refunds"` + TotalDiscount int `json:"total_discount"` + TotalGroupedBy string `json:"total_grouped_by"` + Totals map[string]struct { + Sales float64 `json:"sales"` + Orders int `json:"orders"` + Items int `json:"items"` + Tax float64 `json:"tax"` + Shipping float64 `json:"shipping"` + Discount float64 `json:"discount"` + Customers int `json:"customers"` + } `json:"totals"` +} + +// TopSellerReport top sellers report properties +type TopSellerReport struct { + Title string `json:"title"` + ProductId int `json:"product_id"` + Quantity int `json:"quantity"` +} + +type TotalReport struct { + Slug string `json:"slug"` + Name string `json:"name"` + Total float64 `json:"total"` +} + +// CouponTotal coupon total properties +type CouponTotal TotalReport + +// CustomerTotal customer total properties +type CustomerTotal TotalReport + +// OrderTotal order total properties +type OrderTotal TotalReport + +// ProductTotal product total properties +type ProductTotal TotalReport + +// ReviewTotal review total properties +type ReviewTotal TotalReport diff --git a/entity/setting_group.go b/entity/setting_group.go new file mode 100644 index 0000000..a47f352 --- /dev/null +++ b/entity/setting_group.go @@ -0,0 +1,10 @@ +package entity + +// SettingGroup setting group properties +type SettingGroup struct { + ID string `json:"id"` + Label string `json:"label"` + Description string `json:"description"` + ParentId string `json:"parent_id"` + SubGroups []string `json:"sub_groups"` +} diff --git a/entity/setting_option.go b/entity/setting_option.go new file mode 100644 index 0000000..024af74 --- /dev/null +++ b/entity/setting_option.go @@ -0,0 +1,15 @@ +package entity + +// SettingOption setting option properties +type SettingOption struct { + ID string `json:"id"` + Label string `json:"label"` + Description string `json:"description"` + Value string `json:"value"` + Default string `json:"default"` + Tip string `json:"tip"` + PlaceHolder string `json:"place_holder"` + Type string `json:"type"` + Options map[string]string `json:"options"` + GroupId string `json:"group_id"` +} diff --git a/entity/shipping.go b/entity/shipping.go new file mode 100644 index 0000000..41e2d88 --- /dev/null +++ b/entity/shipping.go @@ -0,0 +1,23 @@ +package entity + +type Shipping struct { + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + Company string `json:"company,omitempty"` + Address1 string `json:"address_1,omitempty"` + Address2 string `json:"address_2,omitempty"` + City string `json:"city,omitempty"` + State string `json:"state,omitempty"` + Postcode string `json:"postcode,omitempty"` + Country string `json:"country,omitempty"` +} + +type ShippingLine struct { + ID int `json:"id"` + MethodTitle string `json:"method_title"` + MethodId string `json:"method_id"` + Total float64 `json:"total"` + TotalTax float64 `json:"total_tax"` + Taxes []Tax `json:"taxes"` + MetaData []Meta `json:"meta_data"` +} diff --git a/entity/shipping_method.go b/entity/shipping_method.go new file mode 100644 index 0000000..6c5c3d6 --- /dev/null +++ b/entity/shipping_method.go @@ -0,0 +1,12 @@ +package entity + +type ShippingMethod struct { + InstanceId int `json:"instance_id"` + Title string `json:"title"` + Order int `json:"order"` + Enabled bool `json:"enabled"` + MethodId string `json:"method_id"` + MethodTitle string `json:"method_title"` + MethodDescription string `json:"method_description"` + Settings []ShippingZoneMethodSetting `json:"settings"` +} diff --git a/entity/shipping_zone.go b/entity/shipping_zone.go new file mode 100644 index 0000000..da43bc5 --- /dev/null +++ b/entity/shipping_zone.go @@ -0,0 +1,8 @@ +package entity + +// ShippingZone shipping zone properties +type ShippingZone struct { + ID string `json:"id"` + Name string `json:"name"` + Order int `json:"order"` +} diff --git a/entity/shipping_zone_location.go b/entity/shipping_zone_location.go new file mode 100644 index 0000000..923bfec --- /dev/null +++ b/entity/shipping_zone_location.go @@ -0,0 +1,7 @@ +package entity + +// ShippingZoneLocation shipping zone location +type ShippingZoneLocation struct { + Code string `json:"code"` + Type string `json:"type"` +} diff --git a/entity/shipping_zone_method.go b/entity/shipping_zone_method.go new file mode 100644 index 0000000..e5cc420 --- /dev/null +++ b/entity/shipping_zone_method.go @@ -0,0 +1,16 @@ +package entity + +// ShippingZoneMethod shipping zone method properties +type ShippingZoneMethod = ShippingMethod + +// ShippingZoneMethodSetting shipping zone method setting properties +type ShippingZoneMethodSetting struct { + ID int `json:"id"` + Label string `json:"label"` + Description string `json:"description"` + Type string `json:"type"` + Value string `json:"value"` + Default string `json:"default"` + Tip string `json:"tip"` + PlaceHolder string `json:"place_holder"` +} diff --git a/entity/system_status.go b/entity/system_status.go new file mode 100644 index 0000000..01511b2 --- /dev/null +++ b/entity/system_status.go @@ -0,0 +1,11 @@ +package entity + +type SystemStatus struct { + Environment SystemStatusEnvironment `json:"environment"` + Database SystemStatusDatabase `json:"database"` + ActivePlugins []string `json:"active_plugins"` + Theme SystemStatusTheme `json:"theme"` + Settings SystemStatusSetting `json:"settings"` + Security SystemStatusSecurity `json:"security"` + Pages []string `json:"pages"` +} diff --git a/entity/system_status_database.go b/entity/system_status_database.go new file mode 100644 index 0000000..b2d681a --- /dev/null +++ b/entity/system_status_database.go @@ -0,0 +1,9 @@ +package entity + +// SystemStatusDatabase System status database properties +type SystemStatusDatabase struct { + WCDatabaseVersion string `json:"wc_database_version"` + DatabasePrefix string `json:"database_prefix"` + MaxmindGEOIPDatabase string `json:"maxmind_geoip_database"` + DatabaseTables []string `json:"database_tables"` +} diff --git a/entity/system_status_environment.go b/entity/system_status_environment.go new file mode 100644 index 0000000..3d8da56 --- /dev/null +++ b/entity/system_status_environment.go @@ -0,0 +1,34 @@ +package entity + +// SystemStatusEnvironment System status environment properties +type SystemStatusEnvironment struct { + HomeURL string `json:"home_url"` + SiteURL string `json:"site_url"` + WCVersion string `json:"wc_version"` + LogDirectory string `json:"log_directory"` + LogDirectoryWritable bool `json:"log_directory_writable"` + WPVersion string `json:"wp_version"` + WPMultisite bool `json:"wp_multisite"` + WPMemoryLimit int `json:"wp_memory_limit"` + WPDebugMode bool `json:"wp_debug_mode"` + WPCron bool `json:"wp_cron"` + Language string `json:"language"` + ServerInfo string `json:"server_info"` + PHPVersion string `json:"php_version"` + PHPPostMaxSize int `json:"php_post_max_size"` + PHPMaxExecutionTime int `json:"php_max_execution_time"` + PHPMaxInputVars int `json:"php_max_input_vars"` + CURLVersion string `json:"curl_version"` + SuhosinInstalled bool `json:"suhosin_installed"` + MaxUploadSize int `json:"max_upload_size"` + MySQLVersion string `json:"my_sql_version"` + DefaultTimezone string `json:"default_timezone"` + FSockOpenOrCurlEnabled bool `json:"fsockopen_or_curl_enabled"` + SOAPClientEnabled bool `json:"soap_client_enabled"` + GzipEnabled bool `json:"gzip_enabled"` + MbStringEnabled bool `json:"mbstring_enabled"` + RemotePostSuccessful bool `json:"remote_post_successful"` + RemotePostResponse string `json:"remote_post_response"` + RemoteGetSuccessful bool `json:"remote_get_successful"` + RemoteGetResponse string `json:"remote_get_response"` +} diff --git a/entity/system_status_security.go b/entity/system_status_security.go new file mode 100644 index 0000000..1d36e32 --- /dev/null +++ b/entity/system_status_security.go @@ -0,0 +1,7 @@ +package entity + +// SystemStatusSecurity System status security properties +type SystemStatusSecurity struct { + SecureConnection bool `json:"secure_connection"` + HideErrors bool `json:"hide_errors"` +} diff --git a/entity/system_status_setting.go b/entity/system_status_setting.go new file mode 100644 index 0000000..7b3b1d5 --- /dev/null +++ b/entity/system_status_setting.go @@ -0,0 +1,15 @@ +package entity + +// SystemStatusSetting System status setting properties +type SystemStatusSetting struct { + APIEnabled bool `json:"api_enabled"` + ForceSSL bool `json:"force_ssl"` + Currency string `json:"currency"` + CurrencySymbol string `json:"currency_symbol"` + CurrencyPosition string `json:"currency_position"` + ThousandSeparator string `json:"thousand_separator"` + DecimalSeparator string `json:"decimal_separator"` + NumberOfDecimals int `json:"number_of_decimals"` + GeolocationEnabled bool `json:"geolocation_enabled"` + Taxonomies []string `json:"taxonomies"` +} diff --git a/entity/system_status_theme.go b/entity/system_status_theme.go new file mode 100644 index 0000000..84a9012 --- /dev/null +++ b/entity/system_status_theme.go @@ -0,0 +1,17 @@ +package entity + +// SystemStatusTheme System status theme properties +type SystemStatusTheme struct { + Name string `json:"name"` + Version string `json:"version"` + VersionLatest string `json:"version_latest"` + AuthorURL string `json:"author_url"` + IsChildTheme bool `json:"is_child_theme"` + HasWooCommerceSupport bool `json:"has_woo_commerce_support"` + HasWooCommerceFile bool `json:"has_woo_commerce_file"` + HasOutdatedTemplates bool `json:"has_outdated_templates"` + Overrides []string `json:"overrides"` + ParentName string `json:"parent_name"` + ParentVersion string `json:"parent_version"` + ParentAuthorURL string `json:"parent_author_url"` +} diff --git a/entity/system_status_tool.go b/entity/system_status_tool.go new file mode 100644 index 0000000..2afa328 --- /dev/null +++ b/entity/system_status_tool.go @@ -0,0 +1,12 @@ +package entity + +// SystemStatusTool system status tool properties +type SystemStatusTool struct { + ID string `json:"id"` + Name string `json:"name"` + Action string `json:"action"` + Description string `json:"description"` + Success bool `json:"success"` + Message string `json:"message"` + Confirm bool `json:"confirm"` +} diff --git a/entity/tax_class.go b/entity/tax_class.go new file mode 100644 index 0000000..470368f --- /dev/null +++ b/entity/tax_class.go @@ -0,0 +1,7 @@ +package entity + +// TaxClass tax class properties +type TaxClass struct { + Slug string `json:"slug"` + Name string `json:"name"` +} diff --git a/entity/tax_rate.go b/entity/tax_rate.go new file mode 100644 index 0000000..7cbf8e5 --- /dev/null +++ b/entity/tax_rate.go @@ -0,0 +1,19 @@ +package entity + +// TaxRate tax rate properties +type TaxRate struct { + ID int `json:"id"` + Country string `json:"country"` + State string `json:"state"` + Postcode string `json:"postcode"` + City string `json:"city"` + Postcodes []string `json:"postcodes"` + Cities []string `json:"cities"` + Rate string `json:"rate"` + Name string `json:"name"` + Priority int `json:"priority"` + Compound bool `json:"compound"` + Shipping bool `json:"shipping"` + Order int `json:"order"` + Class string `json:"class"` +} diff --git a/entity/taxes.go b/entity/taxes.go new file mode 100644 index 0000000..81ea3db --- /dev/null +++ b/entity/taxes.go @@ -0,0 +1,23 @@ +package entity + +type Tax struct { + ID int `json:"id"` + RateCode string `json:"rate_code"` + RateId string `json:"rate_id"` + Label string `json:"label"` + Compound bool `json:"compound"` + TaxTotal float64 `json:"tax_total"` + ShippingTaxTotal float64 `json:"shipping_tax_total"` + MetaData []Meta `json:"meta_data"` +} + +type TaxLine struct { + ID int `json:"id"` + RateCode string `json:"rate_code"` + RateId string `json:"rate_id"` + Label string `json:"label"` + Compound bool `json:"compound"` + TaxTotal float64 `json:"tax_total"` + ShippingTaxTotal float64 `json:"shipping_tax_total"` + MetaData []Meta `json:"meta_data"` +} diff --git a/entity/webhook.go b/entity/webhook.go new file mode 100644 index 0000000..625e163 --- /dev/null +++ b/entity/webhook.go @@ -0,0 +1,18 @@ +package entity + +// Webhook webhook properties +type Webhook struct { + ID int `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Topic string `json:"topic"` + Resource string `json:"resource"` + Event string `json:"event"` + Hooks []string `json:"hooks"` + DeliveryURL string `json:"delivery_url"` + Secret string `json:"secret"` + DateCreated string `json:"date_created"` + DateCreatedGMT string `json:"date_created_gmt"` + DateModified string `json:"date_modified"` + DateModifiedGMT string `json:"date_modified_gmt"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5810263 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module git.cloudyne.io/go/woogo + +go 1.23.0 + +require ( + github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de + github.com/brianvoe/gofakeit/v6 v6.16.0 + github.com/go-ozzo/ozzo-validation/v4 v4.3.0 + github.com/go-resty/resty/v2 v2.7.0 + github.com/google/go-querystring v1.1.0 + git.cloudyne.io/go/hiscaler-gox v1.1.1 + github.com/json-iterator/go v1.1.12 + github.com/stretchr/testify v1.7.0 +) diff --git a/order.go b/order.go new file mode 100644 index 0000000..ae1c27b --- /dev/null +++ b/order.go @@ -0,0 +1,208 @@ +package woogo + +import ( + "errors" + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type orderService service + +// OrdersQueryParams orders query params +type OrdersQueryParams struct { + queryParams + Search string `url:"search,omitempty"` + After string `url:"after,omitempty"` + Before string `url:"before,omitempty"` + Exclude []int `url:"exclude,omitempty"` + Include []int `url:"include,omitempty"` + Parent []int `url:"parent,omitempty"` + ParentExclude []int `url:"parent_exclude,omitempty"` + Status []string `url:"status,omitempty"` + Customer int `url:"customer,omitempty"` + Product int `url:"product,omitempty"` + DecimalPoint int `url:"dp,omitempty"` +} + +func (m OrdersQueryParams) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Before, validation.When(m.Before != "", validation.By(func(value interface{}) error { + dateStr, _ := value.(string) + return IsValidateTime(dateStr) + }))), + validation.Field(&m.After, validation.When(m.After != "", validation.By(func(value interface{}) error { + dateStr, _ := value.(string) + return IsValidateTime(dateStr) + }))), + validation.Field(&m.OrderBy, validation.When(m.OrderBy != "", validation.In("id", "date", "include", "title", "slug").Error("无效的排序字段"))), + validation.Field(&m.Status, validation.When(len(m.Status) > 0, validation.By(func(value interface{}) error { + statuses, ok := value.([]string) + if !ok { + return errors.New("无效的状态值") + } + validStatuses := []string{"any", "pending", "processing", "on-hold", "completed", "cancelled", "refunded", "failed ", "trash"} + for _, status := range statuses { + valid := false + for _, validStatus := range validStatuses { + if status == validStatus { + valid = true + break + } + } + if !valid { + return fmt.Errorf("无效的状态值:%s", status) + } + } + return nil + }))), + ) +} + +// All list all orders +// +// Usage: +// +// params := OrdersQueryParams{ +// After: "2022-06-10", +// } +// params.PerPage = 100 +// for { +// orders, total, totalPages, isLastPage, err := wooClient.Services.Order.All(params) +// if err != nil { +// break +// } +// fmt.Println(fmt.Sprintf("Page %d/%d", total, totalPages)) +// // read orders +// for _, order := range orders { +// _ = order +// } +// if err != nil || isLastPage { +// break +// } +// params.Page++ +// } +func (s orderService) All(params OrdersQueryParams) (items []entity.Order, total, totalPages int, isLastPage bool, err error) { + if err = params.Validate(); err != nil { + return + } + + params.TidyVars() + params.After = ToISOTimeString(params.After, false, true) + params.Before = ToISOTimeString(params.Before, true, false) + resp, err := s.httpClient.R().SetQueryParamsFromValues(toValues(params)).Get("/orders") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + total, totalPages, isLastPage = parseResponseTotal(params.Page, resp) + } else { + err = ErrorWrap(resp.StatusCode(), "") + } + return +} + +// One retrieve an order +func (s orderService) One(id int) (item entity.Order, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/orders/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } else { + err = ErrorWrap(resp.StatusCode(), "") + } + return +} + +// Create order + +type CreateOrderRequest struct { + Status string `json:"status,omitempty"` + Currency string `json:"currency,omitempty"` + CurrencySymbol string `json:"currency_symbol,omitempty"` + PricesIncludeTax bool `json:"prices_include_tax,omitempty"` + CustomerId int `json:"customer_id,omitempty"` + CustomerNote string `json:"customer_note,omitempty"` + Billing *entity.Billing `json:"billing,omitempty"` + Shipping *entity.Shipping `json:"shipping,omitempty"` + PaymentMethod string `json:"payment_method,omitempty"` + PaymentMethodTitle string `json:"payment_method_title,omitempty"` + TransactionId string `json:"transaction_id,omitempty"` + MetaData []entity.Meta `json:"meta_data,omitempty"` + LineItems []entity.LineItem `json:"line_items,omitempty"` + TaxLines []entity.TaxLine `json:"tax_lines,omitempty"` + ShippingLines []entity.ShippingLine `json:"shipping_lines,omitempty"` + FeeLines []entity.FeeLine `json:"fee_lines,omitempty"` + CouponLines []entity.CouponLine `json:"coupon_lines,omitempty"` + SetPaid bool `json:"set_paid,omitempty"` +} + +func (m CreateOrderRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Status, validation.When(m.Status != "", validation.In("pending", "processing", "on-hold", "completed", "cancelled", "refunded", "failed", "trash").Error("无效的状态"))), + ) +} + +func (s orderService) Create(req CreateOrderRequest) (item entity.Order, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/orders") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } else { + err = ErrorWrap(resp.StatusCode(), "") + } + return +} + +// Update order + +type UpdateOrderRequest = CreateOrderRequest + +func (s orderService) Update(id int, req UpdateOrderRequest) (item entity.Order, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Put(fmt.Sprintf("/orders/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } else { + err = ErrorWrap(resp.StatusCode(), "") + } + return +} + +// Delete delete an order +func (s orderService) Delete(id int, force bool) (item entity.Order, err error) { + resp, err := s.httpClient.R(). + SetBody(map[string]bool{"force": force}). + Delete(fmt.Sprintf("/orders/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } else { + err = ErrorWrap(resp.StatusCode(), "") + } + return +} diff --git a/order_note.go b/order_note.go new file mode 100644 index 0000000..4884260 --- /dev/null +++ b/order_note.go @@ -0,0 +1,97 @@ +package woogo + +import ( + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/google/go-querystring/query" + jsoniter "github.com/json-iterator/go" +) + +type orderNoteService service + +type OrderNotesQueryParams struct { + queryParams + Type string `url:"type,omitempty"` +} + +func (m OrderNotesQueryParams) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Type, validation.When(m.Type != "", validation.In("any", "customer", "internal").Error("无效的类型"))), + ) +} + +func (s orderNoteService) All(orderId int, params OrderNotesQueryParams) (items []entity.OrderNote, total, totalPages int, isLastPage bool, err error) { + if err = params.Validate(); err != nil { + return + } + + urlValues, _ := query.Values(params) + resp, err := s.httpClient.R().SetQueryParamsFromValues(urlValues).Get(fmt.Sprintf("/orders/%d/notes", orderId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + total, totalPages, isLastPage = parseResponseTotal(params.Page, resp) + } + return +} + +func (s orderNoteService) One(orderId, noteId int) (item entity.OrderNote, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/orders/%d/notes/%d", orderId, noteId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Create order note + +type CreateOrderNoteRequest struct { + Note string `json:"note"` +} + +func (m CreateOrderNoteRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Note, validation.Required.Error("内容不能为空")), + ) +} + +func (s orderNoteService) Create(orderId int, req CreateOrderNoteRequest) (item entity.OrderNote, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R(). + SetBody(req). + Post(fmt.Sprintf("/orders/%d/notes", orderId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +func (s orderNoteService) Delete(orderId, noteId int, force bool) (item entity.OrderNote, err error) { + resp, err := s.httpClient.R(). + SetBody(map[string]bool{"force": force}). + Delete(fmt.Sprintf("/orders/%d/notes/%d", orderId, noteId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} diff --git a/order_note_test.go b/order_note_test.go new file mode 100644 index 0000000..fbe8c55 --- /dev/null +++ b/order_note_test.go @@ -0,0 +1,74 @@ +package woogo + +import ( + "errors" + "testing" + + "git.cloudyne.io/go/hiscaler-gox/jsonx" + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" +) + +func TestOrderNoteService_All(t *testing.T) { + t.Run("getOrderId", getOrderId) + params := OrderNotesQueryParams{} + items, _, _, _, err := wooClient.Services.OrderNote.All(orderId, params) + if err != nil { + t.Errorf("wooClient.Services.OrderNote.All error: %s", err.Error()) + } else { + if len(items) > 0 { + noteId = items[0].ID + } + t.Logf("items = %s", jsonx.ToPrettyJson(items)) + } +} + +func TestOrderNoteService_Create(t *testing.T) { + note := gofakeit.Address().Address + req := CreateOrderNoteRequest{ + Note: note, + } + item, err := wooClient.Services.OrderNote.Create(orderId, req) + if err != nil { + t.Fatalf("wooClient.Services.OrderNote.Create error: %s", err.Error()) + } else { + assert.Equal(t, note, item.Note, "note") + noteId = item.ID + } +} + +func TestOrderNoteService_One(t *testing.T) { + t.Run("TestOrderNoteService_All", TestOrderNoteService_All) + item, err := wooClient.Services.OrderNote.One(orderId, noteId) + if err != nil { + t.Errorf("wooClient.Services.OrderNote.One(%d, %d) error: %s", orderId, noteId, err.Error()) + } else { + assert.Equal(t, noteId, item.ID, "note id") + } +} + +func TestOrderNoteService_CreateDelete(t *testing.T) { + t.Run("getOrderId", getOrderId) + note := gofakeit.Address().Address + req := CreateOrderNoteRequest{ + Note: note, + } + item, err := wooClient.Services.OrderNote.Create(orderId, req) + if err != nil { + t.Fatalf("wooClient.Services.OrderNote.Create error: %s", err.Error()) + } else { + assert.Equal(t, note, item.Note, "note") + noteId = item.ID + } + + // Delete + _, err = wooClient.Services.OrderNote.Delete(orderId, noteId, true) + if err != nil { + t.Fatalf("wooClient.Services.OrderNote.Delete(%d, %d, %v) error: %s", orderId, noteId, true, err.Error()) + } else { + _, err = wooClient.Services.OrderNote.One(orderId, noteId) + if !errors.Is(err, ErrNotFound) { + t.Fatalf("wooClient.Services.OrderNote.Delete(%d, %d, %v) failed", orderId, noteId, true) + } + } +} diff --git a/order_refund.go b/order_refund.go new file mode 100644 index 0000000..0a61331 --- /dev/null +++ b/order_refund.go @@ -0,0 +1,132 @@ +package woogo + +import ( + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type orderRefundService service + +// List all order refunds + +type OrderRefundsQueryParams struct { + queryParams + Search string `url:"search,omitempty"` + After string `url:"after,omitempty"` + Before string `url:"before,omitempty"` + Exclude []int `url:"exclude,omitempty"` + Include []int `url:"include,omitempty"` + Parent []int `url:"parent,omitempty"` + ParentExclude []int `url:"parent_exclude,omitempty"` + DecimalPoint int `url:"dp,omitempty"` +} + +func (m OrderRefundsQueryParams) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Before, validation.When(m.Before != "", validation.By(func(value interface{}) error { + dateStr, _ := value.(string) + return IsValidateTime(dateStr) + }))), + validation.Field(&m.After, validation.When(m.After != "", validation.By(func(value interface{}) error { + dateStr, _ := value.(string) + return IsValidateTime(dateStr) + }))), + validation.Field(&m.OrderBy, validation.When(m.OrderBy != "", validation.In("id", "date", "include", "title", "slug").Error("无效的排序值"))), + ) +} + +func (s orderRefundService) All(orderId int, params OrderRefundsQueryParams) (items []entity.OrderRefund, total, totalPages int, isLastPage bool, err error) { + if err = params.Validate(); err != nil { + return + } + + params.TidyVars() + params.After = ToISOTimeString(params.After, false, true) + params.Before = ToISOTimeString(params.Before, true, false) + resp, err := s.httpClient.R().SetQueryParamsFromValues(toValues(params)).Get(fmt.Sprintf("/orders/%d/refunds", orderId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + total, totalPages, isLastPage = parseResponseTotal(params.Page, resp) + } else { + err = ErrorWrap(resp.StatusCode(), "") + } + return +} + +// Create an order refund + +type CreateOrderRefundRequest struct { + Amount float64 `json:"amount,string"` + Reason string `json:"reason,omitempty"` + RefundedBy int `json:"refunded_by,omitempty"` + MetaData []entity.Meta `json:"meta_data,omitempty"` + LineItems []entity.OrderRefundLineItem `json:"line_items,omitempty"` +} + +func (m CreateOrderRefundRequest) Validate() error { + return nil +} + +func (s orderRefundService) Create(orderId int, req CreateOrderRefundRequest) (item entity.OrderRefund, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R(). + SetBody(req). + Post(fmt.Sprintf("/orders/%d/refunds", orderId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } else { + err = ErrorWrap(resp.StatusCode(), "") + } + return +} + +// One retrieve an order refund +func (s orderRefundService) One(orderId, refundId, decimalPoint int) (item entity.OrderRefund, err error) { + if decimalPoint <= 0 || decimalPoint >= 6 { + decimalPoint = 2 + } + resp, err := s.httpClient.R(). + SetBody(map[string]int{"dp": decimalPoint}). + Get(fmt.Sprintf("/orders/%d/refunds/%d", orderId, refundId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } else { + err = ErrorWrap(resp.StatusCode(), "") + } + return +} + +// Delete delete an order refund +func (s orderRefundService) Delete(orderId, refundId int, force bool) (item entity.OrderRefund, err error) { + resp, err := s.httpClient.R(). + SetBody(map[string]bool{"force": force}). + Delete(fmt.Sprintf("/orders/%d/refunds/%d", orderId, refundId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } else { + err = ErrorWrap(resp.StatusCode(), "") + } + return +} diff --git a/order_refund_test.go b/order_refund_test.go new file mode 100644 index 0000000..01137b0 --- /dev/null +++ b/order_refund_test.go @@ -0,0 +1,80 @@ +package woogo + +import ( + "errors" + "testing" + + "git.cloudyne.io/go/hiscaler-gox/jsonx" + "github.com/stretchr/testify/assert" +) + +func TestOrderRefundService_All(t *testing.T) { + t.Run("getOrderId", getOrderId) + params := OrderRefundsQueryParams{} + items, _, _, _, err := wooClient.Services.OrderRefund.All(orderId, params) + if err != nil { + t.Errorf("wooClient.Services.OrderRefund.All error: %s", err.Error()) + } else { + if len(items) > 0 { + childId = items[0].ID + } + t.Logf("items = %s", jsonx.ToPrettyJson(items)) + } +} + +func TestOrderRefundService_Create(t *testing.T) { + t.Run("getOrderId", getOrderId) + req := CreateOrderRefundRequest{ + Amount: 1, + Reason: "product is lost", + RefundedBy: 0, + MetaData: nil, + LineItems: nil, + } + item, err := wooClient.Services.OrderRefund.Create(mainId, req) + if err != nil { + t.Fatalf("wooClient.Services.OrderRefund.Create error: %s", err.Error()) + } else { + assert.Equal(t, 100.00, item.Amount, "refund amount") + childId = item.ID + } +} + +func TestOrderRefundService_One(t *testing.T) { + t.Run("TestOrderRefundService_All", TestOrderRefundService_All) + item, err := wooClient.Services.OrderRefund.One(mainId, childId, 2) + if err != nil { + t.Errorf("wooClient.Services.OrderRefund.One(%d, %d) error: %s", orderId, noteId, err.Error()) + } else { + assert.Equal(t, childId, item.ID, "note id") + } +} + +func TestOrderRefundService_CreateDelete(t *testing.T) { + t.Run("getOrderId", getOrderId) + req := CreateOrderRefundRequest{ + Amount: 100, + Reason: "product is lost", + RefundedBy: 0, + MetaData: nil, + LineItems: nil, + } + item, err := wooClient.Services.OrderRefund.Create(mainId, req) + if err != nil { + t.Fatalf("wooClient.Services.OrderRefund.Create error: %s", err.Error()) + } else { + assert.Equal(t, 100.00, item.Amount, "refund amount") + noteId = item.ID + } + + // Delete + _, err = wooClient.Services.OrderNote.Delete(mainId, childId, true) + if err != nil { + t.Fatalf("wooClient.Services.OrderRefund.Delete(%d, %d, %v) error: %s", mainId, childId, true, err.Error()) + } else { + _, err = wooClient.Services.OrderNote.One(mainId, childId) + if !errors.Is(err, ErrNotFound) { + t.Fatalf("wooClient.Services.OrderRefund.Delete(%d, %d, %v) failed", mainId, childId, true) + } + } +} diff --git a/order_test.go b/order_test.go new file mode 100644 index 0000000..50cfd33 --- /dev/null +++ b/order_test.go @@ -0,0 +1,98 @@ +package woogo + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Query orders +func ExampleAll() { + params := OrdersQueryParams{ + After: "2022-06-10", + } + params.PerPage = 100 + for { + orders, total, totalPages, isLastPage, err := wooClient.Services.Order.All(params) + if err != nil { + break + } + fmt.Println(fmt.Sprintf("Page %d/%d", total, totalPages)) + // read orders + for _, order := range orders { + _ = order + } + if err != nil || isLastPage { + break + } + params.Page++ + } +} + +func TestOrderService_All(t *testing.T) { + params := OrdersQueryParams{ + After: "2022-06-10", + } + params.PerPage = 100 + items, _, _, isLastPage, err := wooClient.Services.Order.All(params) + if err != nil { + t.Fatalf("wooClient.Services.Order.All error: %s", err.Error()) + } + if len(items) > 0 { + orderId = items[0].ID + } + assert.Equal(t, true, isLastPage, "check isLastPage") +} + +func TestOrderService_AllByArrayParams(t *testing.T) { + params := OrdersQueryParams{ + Status: []string{"completed"}, + Include: []int{914, 849}, + } + params.PerPage = 300 + _, _, _, isLastPage, err := wooClient.Services.Order.All(params) + if err != nil { + t.Fatalf("wooClient.Services.Order.All By Array Params error: %s", err.Error()) + } + assert.Equal(t, true, isLastPage, "check isLastPage") +} + +func TestOrderService_One(t *testing.T) { + item, err := wooClient.Services.Order.One(orderId) + if err != nil { + t.Errorf("wooClient.Services.Order.One(%d) error: %s", orderId, err.Error()) + } else { + assert.Equal(t, orderId, item.ID, "order id") + } +} + +func TestOrderService_Create(t *testing.T) { + req := CreateOrderRequest{} + item, err := wooClient.Services.Order.Create(req) + if err != nil { + t.Fatalf("wooClient.Services.Order.Create error: %s", err.Error()) + } + orderId = item.ID +} + +func TestOrderService_Update(t *testing.T) { + t.Run("getOrderId", getOrderId) + req := UpdateOrderRequest{ + PaymentMethod: "paypal", + PaymentMethodTitle: "Paypal", + } + item, err := wooClient.Services.Order.Update(orderId, req) + if err != nil { + t.Fatalf("wooClient.Services.Order.Update error: %s", err.Error()) + } else { + assert.Equal(t, orderId, item.ID, "order id") + } +} + +func TestOrderService_Delete(t *testing.T) { + _, err := wooClient.Services.Order.Delete(orderId, true) + if err != nil { + t.Fatalf("wooClient.Services.Order.Delete(%d, true) error: %s", orderId, err.Error()) + } +} diff --git a/payment_gateway.go b/payment_gateway.go new file mode 100644 index 0000000..8e28121 --- /dev/null +++ b/payment_gateway.go @@ -0,0 +1,69 @@ +package woogo + +import ( + "fmt" + + "git.cloudyne.io/go/woogo/entity" + jsoniter "github.com/json-iterator/go" +) + +type paymentGatewayService service + +func (s paymentGatewayService) All() (items []entity.PaymentGateway, err error) { + resp, err := s.httpClient.R().Get("/payment_gateways") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// One retrieve a payment gateway +func (s paymentGatewayService) One(id string) (item entity.PaymentGateway, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/payment_gateways/%s", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Update + +type UpdatePaymentGatewayRequest struct { + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Order int `json:"order,omitempty"` + Enabled bool `json:"enabled,omitempty"` + MethodTitle string `json:"method_title,omitempty"` + MethodDescription string `json:"method_description,omitempty"` + MethodSupports []string `json:"method_supports,omitempty"` + Settings map[string]entity.PaymentGatewaySetting `json:"settings,omitempty"` +} + +func (m UpdatePaymentGatewayRequest) Validate() error { + return nil +} + +func (s paymentGatewayService) Update(id string, req UpdatePaymentGatewayRequest) (item entity.PaymentGateway, err error) { + if err = req.Validate(); err != nil { + return + } + resp, err := s.httpClient.R(). + SetBody(req). + Put(fmt.Sprintf("/payment_gateways/%s", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} diff --git a/payment_gateway_test.go b/payment_gateway_test.go new file mode 100644 index 0000000..16d9e3f --- /dev/null +++ b/payment_gateway_test.go @@ -0,0 +1,57 @@ +package woogo + +import ( + "testing" + + "git.cloudyne.io/go/woogo/entity" + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" +) + +var paymentGatewayId string + +func TestPaymentGatewayService_All(t *testing.T) { + items, err := wooClient.Services.PaymentGateway.All() + if err != nil { + t.Fatalf("wooClient.Services.PaymentGateway.All error: %s", err.Error()) + } + if len(items) > 0 { + paymentGatewayId = items[0].ID + } +} + +func TestPaymentGatewayService_One(t *testing.T) { + t.Run("TestPaymentGatewayService_All", TestPaymentGatewayService_All) + item, err := wooClient.Services.PaymentGateway.One(paymentGatewayId) + if err != nil { + t.Errorf("wooClient.Services.Coupon.PaymentGateway error: %s", err.Error()) + } else { + assert.Equal(t, paymentGatewayId, item.ID, "payment gateway id") + } +} + +func TestPaymentGatewayService_Update(t *testing.T) { + t.Run("TestPaymentGatewayService_All", TestPaymentGatewayService_All) + + var oldItem, newItem entity.PaymentGateway + var err error + oldItem, err = wooClient.Services.PaymentGateway.One(paymentGatewayId) + if err != nil { + t.Fatalf("wooClient.Services.PaymentGateway.One error: %s", err.Error()) + } + + req := UpdatePaymentGatewayRequest{} + newItem, err = wooClient.Services.PaymentGateway.Update(paymentGatewayId, req) + if err != nil { + t.Fatalf("wooClient.Services.PaymentGateway.Update error: %s", err.Error()) + } + assert.Equal(t, oldItem, newItem, "all no change") + + // Change title + req.Title = gofakeit.RandomString([]string{"A", "B", "C", "D", "E", "F", "G"}) + newItem, err = wooClient.Services.PaymentGateway.Update(paymentGatewayId, req) + if err != nil { + t.Fatalf("wooClient.Services.PaymentGateway.Update error: %s", err.Error()) + } + assert.Equal(t, req.Title, newItem.Title, "title") +} diff --git a/product.go b/product.go new file mode 100644 index 0000000..507c06a --- /dev/null +++ b/product.go @@ -0,0 +1,204 @@ +package woogo + +import ( + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type productService service + +// Products + +type ProductsQueryParams struct { + queryParams + Search string `url:"search,omitempty"` + After string `url:"after,omitempty"` + Before string `url:"before,omitempty"` + Exclude []int `url:"exclude,omitempty"` + Include []int `url:"include,omitempty"` + Parent []int `url:"parent,omitempty"` + ParentExclude []int `url:"parent_exclude,omitempty"` + Slug string `url:"slug,omitempty"` + Status string `url:"status,omitempty"` + Type string `url:"type,omitempty"` + SKU string `url:"sku,omitempty"` + Featured bool `url:"featured,omitempty"` + Category string `url:"category,omitempty"` + Tag string `url:"tag,omitempty"` + ShippingClass string `url:"shipping_class,omitempty"` + Attribute string `url:"attribute,omitempty"` + AttributeTerm string `url:"attribute_term,omitempty"` + TaxClass string `url:"tax_class,omitempty"` + OnSale bool `url:"on_sale,omitempty"` + MinPrice float64 `url:"min_price,string,omitempty"` + MaxPrice float64 `url:"max_price,string,omitempty"` + StockStatus string `url:"stock_status,omitempty"` +} + +func (m ProductsQueryParams) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Before, validation.When(m.Before != "", validation.By(func(value interface{}) error { + dateStr, _ := value.(string) + return IsValidateTime(dateStr) + }))), + validation.Field(&m.After, validation.When(m.After != "", validation.By(func(value interface{}) error { + dateStr, _ := value.(string) + return IsValidateTime(dateStr) + }))), + validation.Field(&m.OrderBy, validation.When(m.OrderBy != "", validation.In("id", "include", "title", "slug", "price", "popularity", "rating").Error("无效的排序字段"))), + validation.Field(&m.Status, validation.When(m.Status != "", validation.In("any", "draft", "pending", "private", "publish").Error("无效的状态"))), + validation.Field(&m.Type, validation.When(m.Type != "", validation.In("simple", "grouped", "external", "variable").Error("无效的类型"))), + ) +} + +// All List all products +func (s productService) All(params ProductsQueryParams) (items []entity.Product, total, totalPages int, isLastPage bool, err error) { + if err = params.Validate(); err != nil { + return + } + + params.TidyVars() + params.After = ToISOTimeString(params.After, false, true) + params.Before = ToISOTimeString(params.Before, true, false) + resp, err := s.httpClient.R().SetQueryParamsFromValues(toValues(params)).Get("/products") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + total, totalPages, isLastPage = parseResponseTotal(params.Page, resp) + } + return +} + +// One Retrieve a product +func (s productService) One(id int) (item entity.Product, err error) { + var res entity.Product + resp, err := s.httpClient.R().Get(fmt.Sprintf("/products/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + if err = jsoniter.Unmarshal(resp.Body(), &res); err == nil { + item = res + } + } + return +} + +// Create + +type CreateProductRequest struct { + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` + Type string `json:"type,omitempty"` + Status string `json:"status,omitempty"` + Featured bool `json:"featured,omitempty"` + CatalogVisibility string `json:"catalog_visibility,omitempty"` + Description string `json:"description,omitempty"` + ShortDescription string `json:"short_description,omitempty"` + SKU string `json:"sku,omitempty"` + RegularPrice float64 `json:"regular_price,string,omitempty"` + SalePrice float64 `json:"sale_price,string,omitempty"` + DateOnSaleFrom string `json:"date_on_sale_from,omitempty"` + DateOnSaleFromGMT string `json:"date_on_sale_from_gmt,omitempty"` + DateOnSaleTo string `json:"date_on_sale_to,omitempty"` + DateOnSaleToGMT string `json:"date_on_sale_to_gmt,omitempty"` + Virtual bool `json:"virtual,omitempty"` + Downloadable bool `json:"downloadable,omitempty"` + Downloads []entity.ProductDownload `json:"downloads,omitempty"` + DownloadLimit int `json:"download_limit,omitempty"` + DownloadExpiry int `json:"download_expiry,omitempty"` + ExternalUrl string `json:"external_url,omitempty"` + ButtonText string `json:"button_text,omitempty"` + TaxStatus string `json:"tax_status,omitempty"` + TaxClass string `json:"tax_class,omitempty"` + ManageStock bool `json:"manage_stock,omitempty"` + StockQuantity int `json:"stock_quantity,omitempty"` + StockStatus string `json:"stock_status,omitempty"` + Backorders string `json:"backorders,omitempty"` + SoldIndividually bool `json:"sold_individually,omitempty"` + Weight string `json:"weight,omitempty"` + Dimensions *entity.ProductDimension `json:"dimensions,omitempty"` + ShippingClass string `json:"shipping_class,omitempty"` + ReviewsAllowed bool `json:"reviews_allowed,omitempty"` + UpsellIds []int `json:"upsell_ids,omitempty"` + CrossSellIds []int `json:"cross_sell_ids,omitempty"` + ParentId int `json:"parent_id,omitempty"` + PurchaseNote string `json:"purchase_note,omitempty"` + Categories []entity.ProductCategory `json:"categories,omitempty"` + Tags []entity.ProductTag `json:"tags,omitempty"` + Images []entity.ProductImage `json:"images,omitempty"` + Attributes []entity.ProductAttribute `json:"attributes,omitempty"` + DefaultAttributes []entity.ProductDefaultAttribute `json:"default_attributes,omitempty"` + GroupedProducts []int `json:"grouped_products,omitempty"` + MenuOrder int `json:"menu_order,omitempty"` + MetaData []entity.Meta `json:"meta_data,omitempty"` +} + +func (m CreateProductRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Name, validation.Required.Error("商品名称不能为空")), + validation.Field(&m.Type, validation.When(m.Type != "", validation.In("simple", "grouped", "external ", "variable").Error("无效的类型"))), + validation.Field(&m.Status, validation.When(m.Status != "", validation.In("draft", "pending", "private", "publish").Error("无效的状态"))), + validation.Field(&m.CatalogVisibility, validation.When(m.CatalogVisibility != "", validation.In("visible", "catalog", "search", "hidden").Error("无效的目录可见性"))), + validation.Field(&m.TaxStatus, validation.When(m.TaxStatus != "", validation.In("taxable", "shipping ", "none").Error("无效的税务状态"))), + validation.Field(&m.StockStatus, validation.When(m.StockStatus != "", validation.In("instock", "outofstock ", "onbackorder").Error("无效的库存状态"))), + validation.Field(&m.Backorders, validation.When(m.Backorders != "", validation.In("yes", "no ", "notify").Error("无效的缺货订单状态"))), + ) +} + +// Create create a product +func (s productService) Create(req CreateProductRequest) (item entity.Product, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/products") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Update + +type UpdateProductRequest = CreateProductRequest + +func (s productService) Update(id int, req UpdateProductRequest) (item entity.Product, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Put(fmt.Sprintf("/products/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Delete delete a product +func (s productService) Delete(id int, force bool) (item entity.Product, err error) { + resp, err := s.httpClient.R().SetBody(map[string]bool{"force": force}).Delete(fmt.Sprintf("/products/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} diff --git a/product_attribute.go b/product_attribute.go new file mode 100644 index 0000000..12dc833 --- /dev/null +++ b/product_attribute.go @@ -0,0 +1,164 @@ +package woogo + +import ( + "errors" + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type productAttributeService service + +type ProductAttributesQueryParams struct { + queryParams +} + +func (m ProductAttributesQueryParams) Validate() error { + return nil +} + +// All List all product attributes +func (s productAttributeService) All(params ProductAttributesQueryParams) (items []entity.ProductAttribute, total, totalPages int, isLastPage bool, err error) { + if err = params.Validate(); err != nil { + return + } + + params.TidyVars() + resp, err := s.httpClient.R().SetQueryParamsFromValues(toValues(params)).Get("/products/attributes") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + total, totalPages, isLastPage = parseResponseTotal(params.Page, resp) + } + return +} + +// One Retrieve a product attribute +func (s productAttributeService) One(id int) (item entity.ProductAttribute, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/products/attributes/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Create + +type CreateProductAttributeRequest struct { + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` + Type string `json:"type,omitempty"` + OrderBy string `json:"order_by,omitempty"` + HasArchives bool `json:"has_archives,omitempty"` +} + +func (m CreateProductAttributeRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.OrderBy, validation.When(m.OrderBy != "", validation.In("menu_order", "name", "name_num", "id").Error("无效的排序方式"))), + ) +} + +// Create Create a product attribute +func (s productAttributeService) Create(req CreateProductAttributeRequest) (item entity.ProductAttribute, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/products/attributes") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +type UpdateProductAttributeRequest = CreateProductAttributeRequest + +// Update Update a product attribute +func (s productAttributeService) Update(id int, req UpdateProductAttributeRequest) (item entity.ProductAttribute, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Put(fmt.Sprintf("/products/attributes/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Delete a product attribute + +func (s productAttributeService) Delete(id int, force bool) (item entity.ProductAttribute, err error) { + resp, err := s.httpClient.R(). + SetBody(map[string]bool{"force": force}). + Delete(fmt.Sprintf("/products/attributes/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Batch update product attributes + +type BatchProductAttributesCreateItem = CreateProductAttributeRequest +type BatchProductAttributesUpdateItem struct { + ID string `json:"id"` + BatchProductAttributesCreateItem +} + +type BatchProductAttributesRequest struct { + Create []BatchProductAttributesCreateItem `json:"create,omitempty"` + Update []BatchProductAttributesUpdateItem `json:"update,omitempty"` + Delete []int `json:"delete,omitempty"` +} + +func (m BatchProductAttributesRequest) Validate() error { + if len(m.Create) == 0 && len(m.Update) == 0 && len(m.Delete) == 0 { + return errors.New("无效的请求数据") + } + return nil +} + +type BatchProductAttributesResult struct { + Create []entity.ProductAttribute `json:"create"` + Update []entity.ProductAttribute `json:"update"` + Delete []entity.ProductAttribute `json:"delete"` +} + +// Batch Batch create/update/delete product attributes +func (s productAttributeService) Batch(req BatchProductAttributesRequest) (res BatchProductAttributesResult, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/products/attributes/batch") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &res) + } + return +} diff --git a/product_attribute_term.go b/product_attribute_term.go new file mode 100644 index 0000000..c1d1eb8 --- /dev/null +++ b/product_attribute_term.go @@ -0,0 +1,170 @@ +package woogo + +import ( + "errors" + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type productAttributeTermService service + +type ProductAttributeTermsQueryParaTerms struct { + queryParams + Search string `url:"search,omitempty"` + Exclude []int `url:"exclude,omitempty"` + Include []int `url:"include,omitempty"` + HideEmpty bool `url:"hide_empty,omitempty"` + Parent int `url:"parent,omitempty"` + Product int `url:"product,omitempty"` + Slug string `url:"slug,omitempty"` +} + +func (m ProductAttributeTermsQueryParaTerms) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.OrderBy, validation.When(m.OrderBy != "", validation.In("id", "include", "name", "slug", "term_group", "description", "count").Error("无效的排序类型"))), + ) +} + +// All List all product attribute terms +func (s productAttributeTermService) All(attributeId int, params ProductAttributeTermsQueryParaTerms) (items []entity.ProductAttributeTerm, total, totalPages int, isLastPage bool, err error) { + if err = params.Validate(); err != nil { + return + } + + params.TidyVars() + resp, err := s.httpClient.R().SetQueryParamsFromValues(toValues(params)).Get(fmt.Sprintf("/products/attributes/%d/terms", attributeId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + total, totalPages, isLastPage = parseResponseTotal(params.Page, resp) + } + return +} + +// One Retrieve a product attribute +func (s productAttributeTermService) One(attributeId, termId int) (item entity.ProductAttributeTerm, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/products/attributes/%d/terms/%d", attributeId, termId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Create + +type CreateProductAttributeTermRequest struct { + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` + Description string `json:"description,omitempty"` + MenuOrder int `json:"menu_order,omitempty"` +} + +func (m CreateProductAttributeTermRequest) Validate() error { + return nil +} + +// Create Create a product attribute term +func (s productAttributeTermService) Create(attributeId int, req CreateProductAttributeTermRequest) (item entity.ProductAttributeTerm, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post(fmt.Sprintf("/products/attributes/%d/terms", attributeId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +type UpdateProductAttributeTermRequest = CreateProductAttributeTermRequest + +// Update Update a product attribute term +func (s productAttributeTermService) Update(attributeId, termId int, req UpdateProductAttributeTermRequest) (item entity.ProductAttributeTerm, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Put(fmt.Sprintf("/products/attributes/%d/terms/%d", attributeId, termId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Delete a product attribute term + +func (s productAttributeTermService) Delete(attributeId, termId int, force bool) (item entity.ProductAttributeTerm, err error) { + resp, err := s.httpClient.R(). + SetBody(map[string]bool{"force": force}). + Delete(fmt.Sprintf("/products/attributes/%d/terms/%d", attributeId, termId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Batch update product attribute terms + +type BatchProductAttributeTermsCreateItem = CreateProductAttributeTermRequest +type BatchProductAttributeTermsUpdateItem struct { + ID string `json:"id"` + BatchProductAttributeTermsCreateItem +} + +type BatchProductAttributeTermsRequest struct { + Create []BatchProductAttributeTermsCreateItem `json:"create,omitempty"` + Update []BatchProductAttributeTermsUpdateItem `json:"update,omitempty"` + Delete []int `json:"delete,omitempty"` +} + +func (m BatchProductAttributeTermsRequest) Validate() error { + if len(m.Create) == 0 && len(m.Update) == 0 && len(m.Delete) == 0 { + return errors.New("无效的请求数据") + } + return nil +} + +type BatchProductAttributeTermsResult struct { + Create []entity.ProductAttributeTerm `json:"create"` + Update []entity.ProductAttributeTerm `json:"update"` + Delete []entity.ProductAttributeTerm `json:"delete"` +} + +// Batch Batch create/update/delete product attribute terms +func (s productAttributeTermService) Batch(attributeId int, req BatchProductAttributeTermsRequest) (res BatchProductAttributeTermsResult, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post(fmt.Sprintf("/products/attributes/%d/batch", attributeId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &res) + } + return +} diff --git a/product_category.go b/product_category.go new file mode 100644 index 0000000..16dde74 --- /dev/null +++ b/product_category.go @@ -0,0 +1,173 @@ +package woogo + +import ( + "errors" + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type productCategoryService service + +type ProductCategoriesQueryParams struct { + queryParams + Search string `url:"search,omitempty"` + Exclude []int `url:"exclude,omitempty"` + Include []int `url:"include,omitempty"` + HideEmpty bool `url:"hide_empty,omitempty"` + Parent int `url:"parent,omitempty"` + Product int `url:"product,omitempty"` + Slug string `url:"slug,omitempty"` +} + +func (m ProductCategoriesQueryParams) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.OrderBy, validation.When(m.OrderBy != "", validation.In("id", "include", "name", "slug", "term_group", "description", "count").Error("无效的排序字段"))), + ) +} + +func (s productCategoryService) All(params ProductCategoriesQueryParams) (items []entity.ProductCategory, total, totalPages int, isLastPage bool, err error) { + if err = params.Validate(); err != nil { + return + } + + params.TidyVars() + resp, err := s.httpClient.R().SetQueryParamsFromValues(toValues(params)).Get("/products/categories") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + total, totalPages, isLastPage = parseResponseTotal(params.Page, resp) + } + return +} + +func (s productCategoryService) One(id int) (item entity.ProductCategory, err error) { + var res entity.ProductCategory + resp, err := s.httpClient.R().Get(fmt.Sprintf("/products/categories/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + if err = jsoniter.Unmarshal(resp.Body(), &res); err == nil { + item = res + } + } + return +} + +// 新增商品标签 + +type UpsertProductCategoryRequest struct { + Name string `json:"name"` + Slug string `json:"slug,omitempty"` + Parent int `json:"parent,omitempty"` + Description string `json:"description,omitempty"` + Display string `json:"display,omitempty"` + Image *entity.ProductImage `json:"image,omitempty"` + MenuOrder int `json:"menu_order,omitempty"` +} + +type CreateProductCategoryRequest = UpsertProductCategoryRequest +type UpdateProductCategoryRequest = UpsertProductCategoryRequest + +func (m UpsertProductCategoryRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Name, + validation.Required.Error("分类名称不能为空"), + ), + ) +} + +func (s productCategoryService) Create(req CreateProductCategoryRequest) (item entity.ProductCategory, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/products/categories") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +func (s productCategoryService) Update(id int, req UpdateProductCategoryRequest) (item entity.ProductCategory, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Put(fmt.Sprintf("/products/categories/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +func (s productCategoryService) Delete(id int, force bool) (item entity.ProductCategory, err error) { + resp, err := s.httpClient.R(). + SetBody(map[string]bool{"force": force}). + Delete(fmt.Sprintf("/products/categories/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Batch category create,update and delete operation + +type BatchProductCategoriesCreateItem = UpsertProductCategoryRequest +type BatchProductCategoriesUpdateItem struct { + ID int `json:"id"` + UpsertProductTagRequest +} +type BatchProductCategoriesRequest struct { + Create []BatchProductCategoriesCreateItem `json:"create,omitempty"` + Update []BatchProductCategoriesUpdateItem `json:"update,omitempty"` + Delete []int `json:"delete,omitempty"` +} + +func (m BatchProductCategoriesRequest) Validate() error { + if len(m.Create) == 0 && len(m.Update) == 0 && len(m.Delete) == 0 { + return errors.New("无效的请求数据") + } + return nil +} + +type BatchProductCategoriesResult struct { + Create []entity.ProductTag `json:"create"` + Update []entity.ProductTag `json:"update"` + Delete []entity.ProductTag `json:"delete"` +} + +func (s productCategoryService) Batch(req BatchProductCategoriesRequest) (res BatchProductCategoriesResult, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/products/categories/batch") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &res) + } + return +} diff --git a/product_category_test.go b/product_category_test.go new file mode 100644 index 0000000..bc1d3b2 --- /dev/null +++ b/product_category_test.go @@ -0,0 +1,92 @@ +package woogo + +import ( + "errors" + "testing" + + "git.cloudyne.io/go/hiscaler-gox/jsonx" + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" +) + +func TestProductCategoryService_All(t *testing.T) { + params := ProductCategoriesQueryParams{} + items, _, _, _, err := wooClient.Services.ProductCategory.All(params) + if err != nil { + t.Errorf("wooClient.Services.ProductCategory.All: %s", err.Error()) + } else { + t.Logf("Items: %s", jsonx.ToPrettyJson(items)) + } +} + +func TestProductCategoryService_One(t *testing.T) { + item, err := wooClient.Services.ProductCategory.One(15) + if err != nil { + t.Errorf("wooClient.Services.ProductCategory.One: %s", err.Error()) + } else { + assert.Equal(t, 15, item.ID, "one") + } +} + +func TestProductCategoryService_CreateUpdateDelete(t *testing.T) { + name := gofakeit.BeerName() + req := CreateProductCategoryRequest{ + Name: name, + } + item, err := wooClient.Services.ProductCategory.Create(req) + if err != nil { + t.Fatalf("wooClient.Services.ProductCategory.Create: %s", err.Error()) + } + assert.Equal(t, name, item.Name, "product category name") + categoryId := item.ID + + // Update + newName := gofakeit.BeerName() + updateReq := UpdateProductCategoryRequest{ + Name: newName, + } + item, err = wooClient.Services.ProductCategory.Update(categoryId, updateReq) + if err != nil { + t.Fatalf("wooClient.Services.ProductCategory.Update: %s", err.Error()) + } + assert.Equal(t, newName, item.Name, "product category name") + + // Delete + _, err = wooClient.Services.ProductCategory.Delete(categoryId, true) + if err != nil { + t.Fatalf("wooClient.Services.ProductCategory.Delete: %s", err.Error()) + } + + // Check is exists + _, err = wooClient.Services.ProductCategory.One(categoryId) + if !errors.Is(err, ErrNotFound) { + t.Fatalf("%d is not deleted, error: %s", categoryId, err.Error()) + } +} + +func TestProductCategoryService_Batch(t *testing.T) { + n := 3 + createRequests := make([]BatchProductCategoriesCreateItem, n) + names := make([]string, n) + for i := 0; i < n; i++ { + req := BatchProductCategoriesCreateItem{ + Name: gofakeit.Word(), + Description: gofakeit.Address().Address, + } + createRequests[i] = req + names[i] = req.Name + } + batchReq := BatchProductCategoriesRequest{ + Create: createRequests, + } + result, err := wooClient.Services.ProductCategory.Batch(batchReq) + if err != nil { + t.Fatalf("wooClient.Services.ProductCategory.Batch() error: %s", err.Error()) + } + assert.Equal(t, n, len(result.Create), "Batch create return len") + returnNames := make([]string, 0) + for _, d := range result.Create { + returnNames = append(returnNames, d.Name) + } + assert.Equal(t, names, returnNames, "check names is equal") +} diff --git a/product_review.go b/product_review.go new file mode 100644 index 0000000..f974b68 --- /dev/null +++ b/product_review.go @@ -0,0 +1,192 @@ +package woogo + +import ( + "errors" + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type productReviewService service + +type ProductReviewsQueryParams struct { + queryParams + Search string `url:"search,omitempty"` + After string `url:"after,omitempty"` + Before string `url:"before,omitempty"` + Exclude []int `url:"exclude,omitempty"` + Include []int `url:"include,omitempty"` + Reviewer []int `url:"reviewer,omitempty"` + ReviewerExclude []int `url:"reviewer_exclude,omitempty"` + ReviewerEmail []string `url:"reviewer_email,omitempty"` + Product []int `url:"product,omitempty"` + Status string `url:"status,omitempty"` +} + +func (m ProductReviewsQueryParams) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Before, validation.When(m.Before != "", validation.By(func(value interface{}) error { + dateStr, _ := value.(string) + return IsValidateTime(dateStr) + }))), + validation.Field(&m.After, validation.When(m.After != "", validation.By(func(value interface{}) error { + dateStr, _ := value.(string) + return IsValidateTime(dateStr) + }))), + validation.Field(&m.OrderBy, validation.When(m.OrderBy != "", validation.In("id", "date", "date_gmt", "slug", "include", "product").Error("无效的排序方式"))), + ) +} + +// All List all product reviews +func (s productReviewService) All(params ProductReviewsQueryParams) (items []entity.ProductReview, total, totalPages int, isLastPage bool, err error) { + if err = params.Validate(); err != nil { + return + } + + params.TidyVars() + params.After = ToISOTimeString(params.After, false, true) + params.Before = ToISOTimeString(params.Before, true, false) + resp, err := s.httpClient.R().SetQueryParamsFromValues(toValues(params)).Get("/products/reviews") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + total, totalPages, isLastPage = parseResponseTotal(params.Page, resp) + } + return +} + +// One Retrieve a product review +func (s productReviewService) One(id int) (item entity.ProductReview, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/products/reviews/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Create + +type CreateProductReviewRequest struct { + ProductId int `json:"product_id,omitempty"` + Status string `json:"status,omitempty"` + Reviewer string `json:"reviewer,omitempty"` + ReviewerEmail string `json:"reviewer_email,omitempty"` + Review string `json:"review,omitempty"` + Rating int `json:"rating,omitempty"` + Verified bool `json:"verified,omitempty"` +} + +func (m CreateProductReviewRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Status, validation.When(m.Status != "", validation.In("approved", "hold", "spam", "unspam", "trash", "untrash").Error("无效的状态"))), + validation.Field(&m.Rating, + validation.Min(0).Error("评级最小为 {{threshold}}"), + validation.Min(5).Error("评级最大为 {{threshold}}"), + ), + ) +} + +// Create Create a product review +func (s productReviewService) Create(req CreateProductReviewRequest) (item entity.ProductReview, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/products/reviews") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +type UpdateProductReviewRequest = CreateProductReviewRequest + +// Update Update a product review +func (s productReviewService) Update(id int, req UpdateProductReviewRequest) (item entity.ProductReview, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Put(fmt.Sprintf("/products/reviews/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Delete a product review + +func (s productReviewService) Delete(id int, force bool) (item entity.ProductReview, err error) { + resp, err := s.httpClient.R(). + SetBody(map[string]bool{"force": force}). + Delete(fmt.Sprintf("/products/reviews/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Batch update product reviews + +type BatchProductReviewsCreateItem = CreateProductReviewRequest +type BatchProductReviewsUpdateItem struct { + ID string `json:"id"` + BatchProductReviewsCreateItem +} + +type BatchProductReviewsRequest struct { + Create []BatchProductReviewsCreateItem `json:"create,omitempty"` + Update []BatchProductReviewsUpdateItem `json:"update,omitempty"` + Delete []int `json:"delete,omitempty"` +} + +func (m BatchProductReviewsRequest) Validate() error { + if len(m.Create) == 0 && len(m.Update) == 0 && len(m.Delete) == 0 { + return errors.New("无效的请求数据") + } + return nil +} + +type BatchProductReviewsResult struct { + Create []entity.ProductReview `json:"create"` + Update []entity.ProductReview `json:"update"` + Delete []entity.ProductReview `json:"delete"` +} + +// Batch Batch create/update/delete product reviews +func (s productReviewService) Batch(req BatchProductReviewsRequest) (res BatchProductReviewsResult, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/products/reviews/batch") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &res) + } + return +} diff --git a/product_shipping_class.go b/product_shipping_class.go new file mode 100644 index 0000000..ecb685b --- /dev/null +++ b/product_shipping_class.go @@ -0,0 +1,170 @@ +package woogo + +import ( + "errors" + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type productShippingClassService service + +type ProductShippingClassesQueryParams struct { + queryParams + Search string `url:"search,omitempty"` + Exclude []int `url:"exclude,omitempty"` + Include []int `url:"include,omitempty"` + HideEmpty bool `url:"hide_empty,omitempty"` + Parent int `url:"parent,omitempty"` + Product int `url:"product,omitempty"` + Slug string `url:"slug,omitempty"` +} + +func (m ProductShippingClassesQueryParams) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.OrderBy, validation.When(m.OrderBy != "", validation.In("id", "include", "name", "slug", "term_group", "description", "count").Error("无效的排序类型"))), + ) +} + +// All List all product shipping class +func (s productShippingClassService) All(params ProductShippingClassesQueryParams) (items []entity.ProductShippingClass, total, totalPages int, isLastPage bool, err error) { + if err = params.Validate(); err != nil { + return + } + + params.TidyVars() + resp, err := s.httpClient.R().SetQueryParamsFromValues(toValues(params)).Get("/products/shipping_classes") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + total, totalPages, isLastPage = parseResponseTotal(params.Page, resp) + } + return +} + +// One Retrieve a product shipping class +func (s productShippingClassService) One(id int) (item entity.ProductShippingClass, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/products/shipping_classes/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Create + +type CreateProductShippingClassRequest struct { + Name string `json:"name,omitempty"` + Slug string `json:"slug,omitempty"` + Description string `json:"description,omitempty"` +} + +func (m CreateProductShippingClassRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Name, validation.Required.Error("名称不能为空")), + ) +} + +// Create Create a product shipping class +func (s productShippingClassService) Create(req CreateProductShippingClassRequest) (item entity.ProductShippingClass, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/products/shipping_classes") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +type UpdateProductShippingClassRequest = CreateProductShippingClassRequest + +// Update Update a product shipping class +func (s productShippingClassService) Update(id int, req UpdateProductShippingClassRequest) (item entity.ProductShippingClass, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Put(fmt.Sprintf("/products/shipping_classes/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Delete a product shipping class +func (s productShippingClassService) Delete(id int, force bool) (item entity.ProductShippingClass, err error) { + resp, err := s.httpClient.R(). + SetBody(map[string]bool{"force": force}). + Delete(fmt.Sprintf("/products/shipping_classes/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Batch update product shipping classes + +type BatchProductShippingClassesCreateItem = CreateProductShippingClassRequest +type BatchProductShippingClassesUpdateItem struct { + ID string `json:"id"` + BatchProductShippingClassesCreateItem +} + +type BatchProductShippingClassesRequest struct { + Create []BatchProductShippingClassesCreateItem `json:"create,omitempty"` + Update []BatchProductShippingClassesUpdateItem `json:"update,omitempty"` + Delete []int `json:"delete,omitempty"` +} + +func (m BatchProductShippingClassesRequest) Validate() error { + if len(m.Create) == 0 && len(m.Update) == 0 && len(m.Delete) == 0 { + return errors.New("无效的请求数据") + } + return nil +} + +type BatchProductShippingClassesResult struct { + Create []entity.ProductShippingClass `json:"create"` + Update []entity.ProductShippingClass `json:"update"` + Delete []entity.ProductShippingClass `json:"delete"` +} + +// Batch Batch create/update/delete product shipping classes +func (s productShippingClassService) Batch(req BatchProductShippingClassesRequest) (res BatchProductShippingClassesResult, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/products/shipping_classes/batch") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &res) + } + return +} diff --git a/product_tag.go b/product_tag.go new file mode 100644 index 0000000..0033ccb --- /dev/null +++ b/product_tag.go @@ -0,0 +1,169 @@ +package woogo + +import ( + "errors" + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type productTagService service + +type ProductTagsQueryParams struct { + queryParams + Search string `url:"search,omitempty"` + Exclude []int `url:"exclude,omitempty"` + Include []int `url:"include,omitempty"` + HideEmpty bool `url:"hide_empty,omitempty"` + Product int `url:"product,omitempty"` + Slug string `url:"slug,omitempty"` +} + +func (m ProductTagsQueryParams) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.OrderBy, validation.When(m.OrderBy != "", validation.In("id", "include", "name", "slug", "term_group", "description", "count").Error("无效的排序字段"))), + ) +} + +func (s productTagService) All(params ProductTagsQueryParams) (items []entity.ProductTag, total, totalPages int, isLastPage bool, err error) { + if err = params.Validate(); err != nil { + return + } + + params.TidyVars() + resp, err := s.httpClient.R().SetQueryParamsFromValues(toValues(params)).Get("/products/tags") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + total, totalPages, isLastPage = parseResponseTotal(params.Page, resp) + } + return +} + +func (s productTagService) One(id int) (item entity.ProductTag, err error) { + var res entity.ProductTag + resp, err := s.httpClient.R().Get(fmt.Sprintf("/products/tags/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + if err = jsoniter.Unmarshal(resp.Body(), &res); err == nil { + item = res + } + } + return +} + +// 新增商品标签 + +type UpsertProductTagRequest struct { + Name string `json:"name"` + Slug string `json:"slug,omitempty"` + Description string `json:"description,omitempty"` +} + +type CreateProductTagRequest = UpsertProductTagRequest +type UpdateProductTagRequest = UpsertProductTagRequest + +func (m UpsertProductTagRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Name, + validation.Required.Error("标签名称不能为空"), + ), + ) +} + +func (s productTagService) Create(req CreateProductTagRequest) (item entity.ProductTag, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/products/tags") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +func (s productTagService) Update(id int, req UpdateProductTagRequest) (item entity.ProductTag, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Put(fmt.Sprintf("/products/tags/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +func (s productTagService) Delete(id int, force bool) (item entity.ProductTag, err error) { + resp, err := s.httpClient.R(). + SetBody(map[string]bool{"force": force}). + Delete(fmt.Sprintf("/products/tags/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Batch tag create,update and delete operation + +type BatchProductTagsCreateItem = UpsertProductTagRequest + +type BatchProductTagsUpdateItem struct { + ID int `json:"id"` + UpsertProductTagRequest +} +type BatchProductTagsRequest struct { + Create []BatchProductTagsCreateItem `json:"create,omitempty"` + Update []BatchProductTagsUpdateItem `json:"update,omitempty"` + Delete []int `json:"delete,omitempty"` +} + +func (m BatchProductTagsRequest) Validate() error { + if len(m.Create) == 0 && len(m.Update) == 0 && len(m.Delete) == 0 { + return errors.New("无效的请求数据") + } + return nil +} + +type BatchProductTagsResult struct { + Create []entity.ProductTag `json:"create"` + Update []entity.ProductTag `json:"update"` + Delete []entity.ProductTag `json:"delete"` +} + +func (s productTagService) Batch(req BatchProductTagsRequest) (res BatchProductTagsResult, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/products/tags/batch") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &res) + } + return +} diff --git a/product_tag_test.go b/product_tag_test.go new file mode 100644 index 0000000..e50f47d --- /dev/null +++ b/product_tag_test.go @@ -0,0 +1,91 @@ +package woogo + +import ( + "errors" + "testing" + + "git.cloudyne.io/go/hiscaler-gox/jsonx" + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" +) + +func TestProductTagService_All(t *testing.T) { + params := ProductTagsQueryParams{} + items, _, _, _, err := wooClient.Services.ProductTag.All(params) + if err != nil { + t.Errorf("wooClient.Services.ProductTag.All: %s", err.Error()) + } else { + t.Logf("Items: %s", jsonx.ToPrettyJson(items)) + } +} + +func TestProductTagService_One(t *testing.T) { + item, err := wooClient.Services.ProductTag.One(51) + if err != nil { + t.Errorf("wooClient.Services.ProductTag.One: %s", err.Error()) + } else { + assert.Equal(t, 51, item.ID, "one") + } +} + +func TestProductTagService_CreateUpdateDelete(t *testing.T) { + name := gofakeit.BeerName() + req := CreateProductTagRequest{ + Name: name, + } + item, err := wooClient.Services.ProductTag.Create(req) + if err != nil { + t.Fatalf("wooClient.Services.ProductTag.Create: %s", err.Error()) + } + assert.Equal(t, name, item.Name, "product tag name") + tagId := item.ID + + // Update + newName := gofakeit.BeerName() + updateReq := UpdateProductTagRequest{ + Name: newName, + } + item, err = wooClient.Services.ProductTag.Update(tagId, updateReq) + if err != nil { + t.Fatalf("wooClient.Services.ProductTag.Update: %s", err.Error()) + } + assert.Equal(t, newName, item.Name, "product tag name") + + // Delete + _, err = wooClient.Services.ProductTag.Delete(tagId, true) + if err != nil { + t.Fatalf("wooClient.Services.ProductTag.Delete: %s", err.Error()) + } + + // Check is exists + _, err = wooClient.Services.ProductTag.One(tagId) + if !errors.Is(err, ErrNotFound) { + t.Fatalf("%d is not deleted, error: %s", tagId, err.Error()) + } +} + +func TestProductTagService_Batch(t *testing.T) { + n := 3 + createRequests := make([]BatchProductTagsCreateItem, n) + names := make([]string, n) + for i := 0; i < n; i++ { + req := BatchProductTagsCreateItem{ + Name: gofakeit.Word(), + } + createRequests[i] = req + names[i] = req.Name + } + batchReq := BatchProductTagsRequest{ + Create: createRequests, + } + result, err := wooClient.Services.ProductTag.Batch(batchReq) + if err != nil { + t.Fatalf("wooClient.Services.ProductTag.Batch() error: %s", err.Error()) + } + assert.Equal(t, n, len(result.Create), "Batch create return len") + returnNames := make([]string, 0) + for _, d := range result.Create { + returnNames = append(returnNames, d.Name) + } + assert.Equal(t, names, returnNames, "check names is equal") +} diff --git a/product_test.go b/product_test.go new file mode 100644 index 0000000..9662187 --- /dev/null +++ b/product_test.go @@ -0,0 +1,106 @@ +package woogo + +import ( + "errors" + "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/stretchr/testify/assert" +) + +func TestProductService_All(t *testing.T) { + params := ProductsQueryParams{} + items, _, _, _, err := wooClient.Services.Product.All(params) + if err != nil { + t.Errorf("wooClient.Services.Product.All: %s", err.Error()) + } else { + if len(items) > 0 { + mainId = items[0].ID + } + } +} + +func TestProductService_One(t *testing.T) { + t.Run("TestProductService_All", TestProductService_All) + product, err := wooClient.Services.Product.One(mainId) + if err != nil { + t.Errorf("wooClient.Services.Product.One: %s", err.Error()) + } else { + assert.Equal(t, mainId, product.ID, "product id") + } +} + +func TestProductService_CreateUpdateDelete(t *testing.T) { + name := gofakeit.Word() + req := CreateProductRequest{ + Name: name, + } + item, err := wooClient.Services.Product.Create(req) + if err != nil { + t.Fatalf("wooClient.Services.Product.Create error: %s", err.Error()) + } + productId := item.ID + assert.Equal(t, name, item.Name, "product name") + name = gofakeit.Word() + updateReq := UpdateProductRequest{ + Name: name, + } + _, err = wooClient.Services.Product.Update(productId, updateReq) + if err != nil { + t.Fatalf("wooClient.Services.Product.Update error: %s", err.Error()) + } + item, err = wooClient.Services.Product.One(productId) + if err != nil { + t.Fatalf("wooClient.Services.Product.One error: %s", err.Error()) + } + assert.Equal(t, name, item.Name, "product name") + + // Delete + _, err = wooClient.Services.Product.Delete(productId, true) + if err != nil { + t.Fatalf("wooClient.Services.Product.Delete error: %s", err.Error()) + } + _, err = wooClient.Services.Product.One(productId) + if !errors.Is(err, ErrNotFound) { + t.Fatalf("wooClient.Services.Product.Delete(%d) failed", productId) + } +} + +func TestProductService_All_Filter(t *testing.T) { + name := gofakeit.Word() + sku := "SKU WITH SPACES" + + req := CreateProductRequest{ + Name: name, + SKU: sku, + } + item, err := wooClient.Services.Product.Create(req) + if err != nil { + t.Fatalf("wooClient.Services.Product.Create error: %s", err.Error()) + } + productId := item.ID + assert.Equal(t, name, item.Name, "product name") + name = gofakeit.Word() + + params := ProductsQueryParams{ + SKU: sku, + } + items, _, _, _, err := wooClient.Services.Product.All(params) + if err != nil { + t.Errorf("wooClient.Services.Product.All: %s", err.Error()) + } else { + if len(items) == 0 { + t.Fatalf("wooClient.Services.Product.All error") + } + } + + // Delete + _, err = wooClient.Services.Product.Delete(productId, true) + if err != nil { + t.Fatalf("wooClient.Services.Product.Delete error: %s", err.Error()) + } + _, err = wooClient.Services.Product.One(productId) + if !errors.Is(err, ErrNotFound) { + t.Fatalf("wooClient.Services.Product.Delete(%d) failed", productId) + } +} diff --git a/product_variation.go b/product_variation.go new file mode 100644 index 0000000..c5e879d --- /dev/null +++ b/product_variation.go @@ -0,0 +1,227 @@ +package woogo + +import ( + "errors" + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type productVariationService service + +// Product variations + +type ProductVariationsQueryParams struct { + queryParams + Search string `url:"search,omitempty"` + After string `url:"after,omitempty"` + Before string `url:"before,omitempty"` + Exclude []int `url:"exclude,omitempty"` + Include []int `url:"include,omitempty"` + Parent []int `url:"parent,omitempty"` + ParentExclude []int `url:"parent_exclude,omitempty"` + Slug string `url:"slug,omitempty"` + Status string `url:"status,omitempty"` + SKU string `url:"sku,omitempty"` + TaxClass string `url:"tax_class,omitempty"` + OnSale string `url:"on_sale,omitempty"` + MinPrice float64 `url:"min_price,omitempty"` + MaxPrice float64 `url:"max_price,omitempty"` + StockStatus string `url:"stock_status,omitempty"` +} + +func (m ProductVariationsQueryParams) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Before, validation.When(m.Before != "", validation.By(func(value interface{}) error { + dateStr, _ := value.(string) + return IsValidateTime(dateStr) + }))), + validation.Field(&m.After, validation.When(m.After != "", validation.By(func(value interface{}) error { + dateStr, _ := value.(string) + return IsValidateTime(dateStr) + }))), + validation.Field(&m.OrderBy, validation.When(m.OrderBy != "", validation.In("id", "title", "include", "date", "slug").Error("无效的排序字段"))), + validation.Field(&m.Status, validation.When(m.Status != "", validation.In("any", "draft", "pending", "private", "publish").Error("Invalid status value"))), + validation.Field(&m.TaxClass, validation.When(m.TaxClass != "", validation.In("standard", "reduced-rate", "zero-rate").Error("Invalid tax class"))), + validation.Field(&m.StockStatus, validation.When(m.StockStatus != "", validation.In("instock", "outofstock", "onbackorder").Error("Invalid stock status"))), + validation.Field(&m.MinPrice, validation.Min(0.0)), + validation.Field(&m.MaxPrice, validation.Min(m.MinPrice)), + ) +} + +// All List all product variations +func (s productVariationService) All(productId int, params ProductVariationsQueryParams) (items []entity.ProductVariation, total, totalPages int, isLastPage bool, err error) { + if err = params.Validate(); err != nil { + return + } + + params.TidyVars() + params.After = ToISOTimeString(params.After, false, true) + params.Before = ToISOTimeString(params.Before, true, false) + resp, err := s.httpClient.R().SetQueryParamsFromValues(toValues(params)).Get(fmt.Sprintf("/products/%d/variations", productId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + total, totalPages, isLastPage = parseResponseTotal(params.Page, resp) + } else { + err = ErrorWrap(resp.StatusCode(), "") + } + return +} + +// One retrieve a product variation +func (s productVariationService) One(productId, variationId int) (item entity.ProductVariation, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/products/%d/variations/%d", productId, variationId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } else { + err = ErrorWrap(resp.StatusCode(), "") + } + return +} + +// Create + +type CreateProductVariationRequest struct { + Description string `json:"description,omitempty"` + SKU string `json:"sku,omitempty"` + RegularPrice float64 `json:"regular_price,string,omitempty"` + SalePrice float64 `json:"sale_price,string,omitempty"` + Status string `json:"status,omitempty"` + Virtual bool `json:"virtual,omitempty"` + Downloadable bool `json:"downloadable,omitempty"` + Downloads []entity.ProductDownload `json:"downloads,omitempty"` + DownloadLimit int `json:"download_limit,omitempty"` + DownloadExpiry int `json:"download_expiry,omitempty"` + TaxStatus string `json:"tax_status,omitempty"` + TaxClass string `json:"tax_class,omitempty"` + ManageStock bool `json:"manage_stock,omitempty"` + StockQuantity int `json:"stock_quantity,omitempty"` + StockStatus string `json:"stock_status,omitempty"` + Backorders string `json:"backorders,omitempty"` + Weight float64 `json:"weight,string,omitempty"` + Dimension *entity.ProductDimension `json:"dimensions,omitempty"` + ShippingClass string `json:"shipping_class,omitempty"` + Image *entity.ProductImage `json:"image,omitempty"` + Attributes []entity.ProductVariationAttribute `json:"attributes,omitempty"` + MenuOrder int `json:"menu_order,omitempty"` + MetaData []entity.Meta `json:"meta_data,omitempty"` +} + +func (m CreateProductVariationRequest) Validate() error { + return nil +} + +func (s productVariationService) Create(productId int, req CreateProductVariationRequest) (item entity.ProductVariation, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R(). + SetBody(req). + Post(fmt.Sprintf("/products/%d/variations", productId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } else { + err = ErrorWrap(resp.StatusCode(), "") + } + return +} + +// Update + +type UpdateProductVariationRequest = CreateProductVariationRequest + +func (s productVariationService) Update(productId int, req UpdateProductVariationRequest) (item entity.ProductVariation, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R(). + SetBody(req). + Put(fmt.Sprintf("/products/%d/variations", productId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } else { + err = ErrorWrap(resp.StatusCode(), "") + } + return +} + +// Delete + +func (s productVariationService) Delete(productId, variationId int, force bool) (item entity.ProductVariation, err error) { + resp, err := s.httpClient.R(). + SetBody(map[string]bool{"force": force}). + Delete(fmt.Sprintf("/products/%d/variations/%d", productId, variationId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } else { + err = ErrorWrap(resp.StatusCode(), "") + } + return +} + +// Batch Update + +type BatchProductVariationsCreateItem = CreateProductVariationRequest +type BatchProductVariationsUpdateItem struct { + ID int `json:"id"` + CreateProductVariationRequest +} + +type BatchProductVariationsRequest struct { + Create []BatchProductVariationsCreateItem `json:"create,omitempty"` + Update []BatchProductVariationsUpdateItem `json:"update,omitempty"` + Delete []int `json:"delete,omitempty"` +} + +func (m BatchProductVariationsRequest) Validate() error { + if len(m.Create) == 0 && len(m.Update) == 0 && len(m.Delete) == 0 { + return errors.New("无效的请求数据") + } + return nil +} + +type BatchProductVariationsResult struct { + Create []entity.ProductVariation `json:"create"` + Update []entity.ProductVariation `json:"update"` + Delete []entity.ProductVariation `json:"delete"` +} + +func (s productVariationService) Batch(req BatchProductVariationsRequest) (res BatchProductVariationsResult, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/products/variations/batch") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &res) + } + return +} diff --git a/product_variation_test.go b/product_variation_test.go new file mode 100644 index 0000000..8d9215c --- /dev/null +++ b/product_variation_test.go @@ -0,0 +1,17 @@ +package woogo + +import ( + "testing" + + "git.cloudyne.io/go/hiscaler-gox/jsonx" +) + +func TestProductVariationService_All(t *testing.T) { + params := ProductVariationsQueryParams{} + items, _, _, _, err := wooClient.Services.ProductVariation.All(1, params) + if err != nil { + t.Errorf("wooClient.Services.ProductVariation.All: %s", err.Error()) + } else { + t.Logf("items: %s", jsonx.ToPrettyJson(items)) + } +} diff --git a/query_params.go b/query_params.go new file mode 100644 index 0000000..1dd33d4 --- /dev/null +++ b/query_params.go @@ -0,0 +1,66 @@ +package woogo + +import ( + "net/url" + "strings" + + "github.com/google/go-querystring/query" +) + +const ( + SortAsc = "asc" + SortDesc = "desc" +) + +const ( + ViewContext = "view" + EditContext = "edit" +) + +type queryParams struct { + Page int `url:"page,omitempty"` + PerPage int `url:"per_page,omitempty"` + Offset int `url:"offset,omitempty"` + Order string `url:"order,omitempty"` + OrderBy string `url:"order_by,omitempty"` + Context string `url:"context,omitempty"` +} + +func (q *queryParams) TidyVars() *queryParams { + if q.Page <= 0 { + q.Page = 1 + } + if q.PerPage <= 0 { + q.PerPage = 10 + } else if q.PerPage > 100 { + q.PerPage = 100 + } + if q.Offset < 0 { + q.Offset = 0 + } + + if q.Order == "" { + q.Order = SortAsc + } else { + q.Order = strings.ToLower(q.Order) + if q.Order != SortDesc { + q.OrderBy = SortAsc + } + } + + if q.Context == "" { + q.Context = ViewContext + } else { + q.Context = strings.ToLower(q.Context) + if q.Context != EditContext { + q.Context = ViewContext + } + } + return q +} + +// change to url.values +func toValues(i interface{}) (values url.Values) { + values, _ = query.Values(i) + return +} diff --git a/report.go b/report.go new file mode 100644 index 0000000..ddbe368 --- /dev/null +++ b/report.go @@ -0,0 +1,181 @@ +package woogo + +import ( + "fmt" + + "git.cloudyne.io/go/woogo/entity" + "github.com/araddon/dateparse" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type reportService service + +type ReportsQueryParams struct { + Context string `url:"context,omitempty"` + Period string `url:"period,omitempty"` + DateMin string `url:"date_min,omitempty"` + DateMax string `url:"date_max,omitempty"` +} + +func (m ReportsQueryParams) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Period, validation.When(m.Period != "", validation.In("week", "month", "last_month", "year").Error("无效的报表周期"))), + validation.Field(&m.DateMin, + validation.Required.Error("报表开始时间不能为空"), + validation.By(func(value interface{}) error { + dateStr, _ := value.(string) + return IsValidateTime(dateStr) + }), + ), + validation.Field(&m.DateMax, + validation.Required.Error("报表结束时间不能为空"), + validation.By(func(value interface{}) (err error) { + dateStr, _ := value.(string) + err = IsValidateTime(dateStr) + if err != nil { + return + } + dateMin, err := dateparse.ParseAny(m.DateMin) + if err != nil { + return + } + dateMax, err := dateparse.ParseAny(m.DateMax) + if err != nil { + return + } + if dateMax.Before(dateMin) { + return fmt.Errorf("结束时间不能小于 %s", m.DateMin) + } + return nil + }), + ), + ) +} + +// All list all reports +func (s reportService) All() (items []entity.Report, err error) { + resp, err := s.httpClient.R().Get("/reports") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// Sales reports + +type SalesReportsQueryParams = ReportsQueryParams + +// SalesReports list all sales reports +func (s reportService) SalesReports(params SalesReportsQueryParams) (items []entity.SaleReport, err error) { + if err = params.Validate(); err != nil { + return + } + + params.DateMin = ToISOTimeString(params.DateMin, true, false) + params.DateMax = ToISOTimeString(params.DateMax, false, true) + resp, err := s.httpClient.R(). + SetQueryParamsFromValues(toValues(params)). + Get("/reports/sales") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// TopSellerReports list all sales reports + +type TopSellerReportsQueryParams = SalesReportsQueryParams + +func (s reportService) TopSellerReports(params SalesReportsQueryParams) (items []entity.TopSellerReport, err error) { + if err = params.Validate(); err != nil { + return + } + + params.DateMin = ToISOTimeString(params.DateMin, true, false) + params.DateMax = ToISOTimeString(params.DateMax, false, true) + resp, err := s.httpClient.R(). + SetQueryParamsFromValues(toValues(params)). + Get("/reports/top_sellers") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// CouponTotals retrieve coupons totals +func (s reportService) CouponTotals() (items []entity.CouponTotal, err error) { + resp, err := s.httpClient.R().Get("/reports/coupons/totals") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// CustomerTotals retrieve customer totals +func (s reportService) CustomerTotals() (items []entity.CustomerTotal, err error) { + resp, err := s.httpClient.R().Get("/reports/customers/totals") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// OrderTotals retrieve customer totals +func (s reportService) OrderTotals() (items []entity.OrderTotal, err error) { + resp, err := s.httpClient.R().Get("/reports/orders/totals") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// ProductTotals retrieve product totals +func (s reportService) ProductTotals() (items []entity.OrderTotal, err error) { + resp, err := s.httpClient.R().Get("/reports/products/totals") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// ReviewTotals retrieve review totals +func (s reportService) ReviewTotals() (items []entity.OrderTotal, err error) { + resp, err := s.httpClient.R().Get("/reports/reviews/totals") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} diff --git a/report_test.go b/report_test.go new file mode 100644 index 0000000..c56ce84 --- /dev/null +++ b/report_test.go @@ -0,0 +1,21 @@ +package woogo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReportService_All(t *testing.T) { + _, err := wooClient.Services.Report.All() + assert.Equal(t, nil, err) +} + +func TestReportService_SalesReports(t *testing.T) { + req := SalesReportsQueryParams{ + DateMin: "2022-01-01", + DateMax: "2022-01-01", + } + _, err := wooClient.Services.Report.SalesReports(req) + assert.Equal(t, nil, err) +} diff --git a/setting.go b/setting.go new file mode 100644 index 0000000..01906c8 --- /dev/null +++ b/setting.go @@ -0,0 +1,20 @@ +package woogo + +import ( + "git.cloudyne.io/go/woogo/entity" + jsoniter "github.com/json-iterator/go" +) + +type settingService service + +func (s settingService) Groups() (items []entity.SettingGroup, err error) { + resp, err := s.httpClient.R().Get("/settings") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} diff --git a/setting_option.go b/setting_option.go new file mode 100644 index 0000000..58012c9 --- /dev/null +++ b/setting_option.go @@ -0,0 +1,68 @@ +package woogo + +import ( + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +// https://woocommerce.github.io/woocommerce-rest-api-docs/?php#setting-options + +type settingOptionService service + +// All list all setting options +func (s settingOptionService) All(settingId string) (items []entity.SettingOption, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/settings/%s", settingId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// One retrieve a setting option +func (s settingOptionService) One(groupId, optionId string) (item entity.SettingOption, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/settings/%s/%s", groupId, optionId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Create + +type UpdateSettingOptionRequest struct { + Value string `json:"value"` +} + +func (m UpdateSettingOptionRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Value, validation.Required.Error("设置值不能为空")), + ) +} + +func (s settingOptionService) Update(groupId, optionId string, req UpdateSettingOptionRequest) (item entity.SettingOption, err error) { + if err = req.Validate(); err != nil { + return + } + resp, err := s.httpClient.R(). + SetBody(req). + Put(fmt.Sprintf("/settings/%s/%s", groupId, optionId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} diff --git a/shipping_method.go b/shipping_method.go new file mode 100644 index 0000000..ceacb63 --- /dev/null +++ b/shipping_method.go @@ -0,0 +1,36 @@ +package woogo + +import ( + "fmt" + + "git.cloudyne.io/go/woogo/entity" + jsoniter "github.com/json-iterator/go" +) + +type shippingMethodService service + +// All list all shipping methods +func (s shippingMethodService) All() (items []entity.ShippingMethod, err error) { + resp, err := s.httpClient.R().Get("/shipping_methods") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// One retrieve a shipping method +func (s shippingMethodService) One(id int) (item entity.ShippingMethod, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/shipping_methods/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} diff --git a/shipping_zone.go b/shipping_zone.go new file mode 100644 index 0000000..208ec35 --- /dev/null +++ b/shipping_zone.go @@ -0,0 +1,100 @@ +package woogo + +import ( + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type shippingZoneService service + +// All list all shipping zones +func (s shippingZoneService) All() (items []entity.ShippingZone, err error) { + resp, err := s.httpClient.R().Get("/shipping/zones") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// One retrieve a shipping zone +func (s shippingZoneService) One(id int) (item entity.ShippingZone, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/shipping/zones/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Create + +type CreateShippingZoneRequest struct { + Name string `json:"name"` + Order int `json:"order"` +} + +func (m CreateShippingZoneRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Name, validation.Required.Error("名称不能为空")), + validation.Field(&m.Order, validation.Min(0).Error("排序值不能小于 {{.threshold}}")), + ) +} + +func (s shippingZoneService) Create(req CreateShippingZoneRequest) (item entity.ShippingZone, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/shipping/zones") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Update + +type UpdateShippingZoneRequest = CreateShippingZoneRequest + +func (s shippingZoneService) Update(id int, req UpdateShippingZoneRequest) (item entity.ShippingZone, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Put(fmt.Sprintf("/shipping/zones/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Delete delete a shipping zone +func (s shippingZoneService) Delete(id int, force bool) (item entity.ShippingZone, err error) { + resp, err := s.httpClient.R().SetBody(map[string]bool{"force": force}).Delete(fmt.Sprintf("/shipping/zones/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} diff --git a/shipping_zone_location.go b/shipping_zone_location.go new file mode 100644 index 0000000..a7a5327 --- /dev/null +++ b/shipping_zone_location.go @@ -0,0 +1,66 @@ +package woogo + +import ( + "errors" + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type shippingZoneLocationService service + +// All list all shipping zone locations +func (s shippingZoneLocationService) All(shippingZoneId int) (items []entity.ShippingZoneLocation, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/shipping/zones/%d/locations", shippingZoneId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// Update + +type UpdateShippingZoneLocationsRequest []entity.ShippingZoneLocation + +func (m UpdateShippingZoneLocationsRequest) Validate() error { + return validation.Validate(m, validation.Required.Error("待更新数据不能为空"), + validation.By(func(value interface{}) error { + items, ok := value.([]entity.ShippingZoneLocation) + if !ok { + return errors.New("待更新数据错误") + } + + for _, item := range items { + err := validation.ValidateStruct(&item, + validation.Field(&item.Code, validation.Required.Error("代码不能为空")), + validation.Field(&item.Type, validation.In("postcode", "country", "state", "continent").Error("类型错误")), + ) + if err != nil { + return err + } + } + return nil + }), + ) +} + +func (s shippingZoneLocationService) Update(shippingZoneId int, req UpdateShippingZoneLocationsRequest) (items []entity.ShippingZoneLocation, err error) { + if err = req.Validate(); err != nil { + return + } + resp, err := s.httpClient.R().SetBody(req).Put(fmt.Sprintf("/shipping/zones/%d/locations", shippingZoneId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} diff --git a/shipping_zone_method.go b/shipping_zone_method.go new file mode 100644 index 0000000..22c5583 --- /dev/null +++ b/shipping_zone_method.go @@ -0,0 +1,116 @@ +package woogo + +import ( + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type shippingZoneMethodService service + +// All list all shipping zone methods +func (s shippingZoneMethodService) All(zoneId int) (items []entity.ShippingZoneMethod, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/shipping/zones/%d/methods", zoneId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// One retrieve a shipping zone method +func (s shippingZoneMethodService) One(zoneId, methodId int) (item entity.ShippingZoneMethod, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/shipping/zones/%d/methods/%d", zoneId, methodId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Include include a shipping method to a shipping zone + +type ShippingZoneMethodIncludeRequest struct { + MethodId string `json:"method_id"` +} + +func (m ShippingZoneMethodIncludeRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.MethodId, validation.Required.Error("配送方式不能为空")), + ) +} + +func (s shippingZoneMethodService) Include(zoneId int, req ShippingZoneMethodIncludeRequest) (item entity.ShippingZoneMethod, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R(). + SetBody(req). + Post(fmt.Sprintf("/shipping/zones/%d/methods", zoneId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Update + +type UpdateShippingZoneMethodSetting struct { + Value string `json:"value"` +} + +type UpdateShippingZoneMethodRequest struct { + Order int `json:"order"` + Enabled bool `json:"enabled"` + Settings UpdateShippingZoneMethodSetting `json:"settings"` +} + +func (m UpdateShippingZoneMethodRequest) Validate() error { + return nil +} + +func (s shippingZoneMethodService) Update(zoneId, methodId int, req UpdateShippingZoneMethodRequest) (item entity.ShippingZoneMethod, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R(). + SetBody(req). + Put(fmt.Sprintf("/shipping/zones/%d/methods/%d", zoneId, methodId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Delete delete a shipping zone +func (s shippingZoneMethodService) Delete(zoneId, methodId int, force bool) (item entity.ShippingZone, err error) { + resp, err := s.httpClient.R(). + SetBody(map[string]bool{"force": force}). + Delete(fmt.Sprintf("/shipping/zones/%d/%d", zoneId, methodId)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} diff --git a/system_status.go b/system_status.go new file mode 100644 index 0000000..83c6254 --- /dev/null +++ b/system_status.go @@ -0,0 +1,20 @@ +package woogo + +import ( + "git.cloudyne.io/go/woogo/entity" + jsoniter "github.com/json-iterator/go" +) + +type systemStatusService service + +func (s systemStatusService) All() (item entity.SystemStatus, err error) { + resp, err := s.httpClient.R().Get("/system_status") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} diff --git a/system_status_tool.go b/system_status_tool.go new file mode 100644 index 0000000..2a086d7 --- /dev/null +++ b/system_status_tool.go @@ -0,0 +1,46 @@ +package woogo + +import ( + "fmt" + + "git.cloudyne.io/go/woogo/entity" + jsoniter "github.com/json-iterator/go" +) + +type systemStatusToolService service + +func (s systemStatusToolService) All() (item entity.SystemStatusTool, err error) { + resp, err := s.httpClient.R().Get("/system_status/tools") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +func (s systemStatusToolService) One(id string) (item entity.SystemStatusTool, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/system_status/tools/%s", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +func (s systemStatusToolService) Run(id string) (item entity.SystemStatusTool, err error) { + resp, err := s.httpClient.R().Put(fmt.Sprintf("/system_status/tools/%s", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} diff --git a/tax_class.go b/tax_class.go new file mode 100644 index 0000000..c55cb93 --- /dev/null +++ b/tax_class.go @@ -0,0 +1,74 @@ +package woogo + +import ( + "errors" + "fmt" + "strings" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type taxClassService service + +// All List all tax classes +func (s taxClassService) All() (items []entity.TaxClass, err error) { + resp, err := s.httpClient.R().Get("/tax/classes") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + } + return +} + +// Create tax class request + +type CreateTaxClassRequest struct { + Name string `json:"name"` +} + +func (m CreateTaxClassRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Name, validation.Required.Error("名称不能为空")), + ) +} + +// Create Create a tax class +func (s taxClassService) Create(req CreateTaxClassRequest) (item entity.TaxClass, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/taxes/classes") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Delete Delete a tax classes +func (s taxClassService) Delete(slug string, force bool) (item entity.TaxClass, err error) { + slug = strings.TrimSpace(slug) + if slug == "" { + err = errors.New("slug 参数不能为空") + return + } + + resp, err := s.httpClient.R().SetBody(map[string]bool{"force": force}).Delete(fmt.Sprintf("/taxes/classes/%s", slug)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} diff --git a/tax_rate.go b/tax_rate.go new file mode 100644 index 0000000..473551a --- /dev/null +++ b/tax_rate.go @@ -0,0 +1,172 @@ +package woogo + +import ( + "errors" + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + jsoniter "github.com/json-iterator/go" +) + +type taxRateService service + +type TaxRatesQueryParams struct { + queryParams + Class string `url:"class,omitempty"` +} + +func (m TaxRatesQueryParams) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.OrderBy, validation.When(m.OrderBy != "", validation.In("id", "order", "priority").Error("无效的排序字段"))), + ) +} + +// All List all tax rate +func (s taxRateService) All(params TaxRatesQueryParams) (items []entity.TaxRate, total, totalPages int, isLastPage bool, err error) { + if err = params.Validate(); err != nil { + return + } + + params.TidyVars() + resp, err := s.httpClient.R().SetQueryParamsFromValues(toValues(params)).Get("/taxes") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + total, totalPages, isLastPage = parseResponseTotal(params.Page, resp) + } + return +} + +// One Retrieve a tax rate +func (s taxRateService) One(id int) (item entity.TaxRate, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/taxes/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Create + +type CreateTaxRateRequest struct { + Country string `json:"country,omitempty"` + State string `json:"state,omitempty"` + Postcode string `json:"postcode,omitempty"` + City string `json:"city,omitempty"` + Postcodes []string `json:"postcodes,omitempty"` + Cities []string `json:"cities,omitempty"` + Rate string `json:"rate,omitempty"` + Name string `json:"name,omitempty"` + Priority int `json:"priority,omitempty"` + Compound bool `json:"compound,omitempty"` + Shipping bool `json:"shipping,omitempty"` + Order int `json:"order,omitempty"` + Class string `json:"class,omitempty"` +} + +func (m CreateTaxRateRequest) Validate() error { + return nil +} + +// Create Create a product attribute +func (s taxRateService) Create(req CreateTaxRateRequest) (item entity.TaxRate, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/taxes") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +type UpdateTaxRateRequest = CreateTaxRateRequest + +// Update Update a tax rate +func (s taxRateService) Update(id int, req UpdateTaxRateRequest) (item entity.TaxRate, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Put(fmt.Sprintf("/taxes/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Delete a tax rate +func (s taxRateService) Delete(id int, force bool) (item entity.TaxRate, err error) { + resp, err := s.httpClient.R(). + SetBody(map[string]bool{"force": force}). + Delete(fmt.Sprintf("/taxes/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Batch update tax rates + +type BatchTaxRatesCreateItem = CreateTaxRateRequest +type BatchTaxRatesUpdateItem struct { + ID string `json:"id"` + BatchTaxRatesCreateItem +} + +type BatchTaxRatesRequest struct { + Create []BatchTaxRatesCreateItem `json:"create,omitempty"` + Update []BatchTaxRatesUpdateItem `json:"update,omitempty"` + Delete []int `json:"delete,omitempty"` +} + +func (m BatchTaxRatesRequest) Validate() error { + if len(m.Create) == 0 && len(m.Update) == 0 && len(m.Delete) == 0 { + return errors.New("无效的请求数据") + } + return nil +} + +type BatchTaxRatesResult struct { + Create []entity.TaxRate `json:"create"` + Update []entity.TaxRate `json:"update"` + Delete []entity.TaxRate `json:"delete"` +} + +// Batch Batch create/update/delete tax rates +func (s taxRateService) Batch(req BatchTaxRatesRequest) (res BatchTaxRatesResult, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/taxes/batch") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &res) + } + return +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..e96f9b5 --- /dev/null +++ b/utils.go @@ -0,0 +1,52 @@ +package woogo + +import ( + "fmt" + "strings" + "time" + + "git.cloudyne.io/go/woogo/constant" + "github.com/araddon/dateparse" +) + +// ToISOTimeString Convert to iso time string +// If date format is invalid, then return original value +// If dateStr include time part, and you set addMinTimeString/addMaxTimeString to true, +// but still return original dateStr value. +func ToISOTimeString(dateStr string, addMinTimeString, addMaxTimeString bool) (s string) { + dateStr = strings.TrimSpace(dateStr) + if dateStr == "" { + return + } + + s = dateStr + format, err := dateparse.ParseFormat(dateStr) + if err == nil && (format == constant.DateFormat || format == constant.DatetimeFormat || format == constant.WooDatetimeFormat) { + if strings.Index(dateStr, " ") == -1 { + if addMinTimeString { + dateStr += " 00:00:00" + } + if addMaxTimeString { + dateStr += " 23:59:59" + } + } + if t, err := dateparse.ParseAny(dateStr); err == nil { + return t.Format(time.RFC3339) + } + } + return s +} + +// IsValidateTime Is validate time +func IsValidateTime(dateStr string) error { + format, err := dateparse.ParseFormat(dateStr) + if err != nil { + return err + } + switch format { + case constant.DateFormat, constant.DatetimeFormat, constant.WooDatetimeFormat: + return nil + default: + return fmt.Errorf("%s 日期格式无效", dateStr) + } +} diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000..f6d70ea --- /dev/null +++ b/utils_test.go @@ -0,0 +1,25 @@ +package woogo + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToISOTimeString(t *testing.T) { + testCases := []struct { + tag string + date string + addMin bool + addMax bool + expected string + }{ + {"min", "2020-01-01", true, false, "2020-01-01T00:00:00Z"}, + {"has time", "2020-01-01 01:02:03", true, false, "2020-01-01T01:02:03Z"}, + {"bad format", "2020-01-0101:02:03", true, false, "2020-01-0101:02:03"}, + } + for _, testCase := range testCases { + s := ToISOTimeString(testCase.date, testCase.addMin, testCase.addMax) + assert.Equal(t, testCase.expected, s, testCase.tag) + } +} diff --git a/webhook.go b/webhook.go new file mode 100644 index 0000000..f32a883 --- /dev/null +++ b/webhook.go @@ -0,0 +1,183 @@ +package woogo + +import ( + "errors" + "fmt" + + "git.cloudyne.io/go/woogo/entity" + validation "github.com/go-ozzo/ozzo-validation/v4" + "github.com/go-ozzo/ozzo-validation/v4/is" + jsoniter "github.com/json-iterator/go" +) + +type webhookService service + +type WebhooksQueryParams struct { + queryParams + Search string `url:"search"` + After string `url:"after"` + Before string `url:"before"` + Exclude []int `url:"exclude"` + Include []int `url:"include"` + Status string `url:"status"` +} + +func (m WebhooksQueryParams) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.Before, validation.When(m.Before != "", validation.By(func(value interface{}) error { + dateStr, _ := value.(string) + return IsValidateTime(dateStr) + }))), + validation.Field(&m.After, validation.When(m.After != "", validation.By(func(value interface{}) error { + dateStr, _ := value.(string) + return IsValidateTime(dateStr) + }))), + ) +} + +// All List all webhooks +func (s webhookService) All(params WebhooksQueryParams) (items []entity.Webhook, total, totalPages int, isLastPage bool, err error) { + if err = params.Validate(); err != nil { + return + } + + params.TidyVars() + params.After = ToISOTimeString(params.After, false, true) + params.Before = ToISOTimeString(params.Before, true, false) + resp, err := s.httpClient.R().SetQueryParamsFromValues(toValues(params)).Get("/products/webhooks") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &items) + total, totalPages, isLastPage = parseResponseTotal(params.Page, resp) + } + return +} + +// One Retrieve a webhook +func (s webhookService) One(id int) (item entity.Webhook, err error) { + resp, err := s.httpClient.R().Get(fmt.Sprintf("/products/webhooks/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Create + +type CreateWebhookRequest struct { + Name string `json:"name,omitempty"` + Status string `json:"status,omitempty"` + Topic string `json:"topic,omitempty"` + DeliveryURL string `json:"delivery_url,omitempty"` + Secret string `json:"secret,omitempty"` +} + +func (m CreateWebhookRequest) Validate() error { + return validation.ValidateStruct(&m, + validation.Field(&m.DeliveryURL, validation.When(m.DeliveryURL != "", is.URL.Error("投递 URL 格式错误"))), + validation.Field(&m.Status, validation.When(m.Status != "", validation.In("active", "paused", "disabled").Error("无效的状态值"))), + ) +} + +// Create Create a product attribute +func (s webhookService) Create(req CreateWebhookRequest) (item entity.Webhook, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/products/webhooks") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +type UpdateWebhookRequest = CreateWebhookRequest + +// Update Update a webhook +func (s webhookService) Update(id int, req UpdateWebhookRequest) (item entity.Webhook, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Put(fmt.Sprintf("/products/webhooks/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Delete a webhook + +func (s webhookService) Delete(id int, force bool) (item entity.Webhook, err error) { + resp, err := s.httpClient.R(). + SetBody(map[string]bool{"force": force}). + Delete(fmt.Sprintf("/products/webhooks/%d", id)) + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &item) + } + return +} + +// Batch update webhooks + +type BatchWebhooksCreateItem = CreateWebhookRequest +type BatchWebhooksUpdateItem struct { + ID string `json:"id"` + BatchWebhooksCreateItem +} + +type BatchWebhooksRequest struct { + Create []BatchWebhooksCreateItem `json:"create,omitempty"` + Update []BatchWebhooksUpdateItem `json:"update,omitempty"` + Delete []int `json:"delete,omitempty"` +} + +func (m BatchWebhooksRequest) Validate() error { + if len(m.Create) == 0 && len(m.Update) == 0 && len(m.Delete) == 0 { + return errors.New("无效的请求数据") + } + return nil +} + +type BatchWebhooksResult struct { + Create []entity.Webhook `json:"create"` + Update []entity.Webhook `json:"update"` + Delete []entity.Webhook `json:"delete"` +} + +// Batch Batch create/update/delete webhooks +func (s webhookService) Batch(req BatchWebhooksRequest) (res BatchWebhooksResult, err error) { + if err = req.Validate(); err != nil { + return + } + + resp, err := s.httpClient.R().SetBody(req).Post("/products/webhooks/batch") + if err != nil { + return + } + + if resp.IsSuccess() { + err = jsoniter.Unmarshal(resp.Body(), &res) + } + return +} diff --git a/woo.go b/woo.go new file mode 100644 index 0000000..e7e21f6 --- /dev/null +++ b/woo.go @@ -0,0 +1,335 @@ +// package woogo is a Woo Commerce lib. +// +// Quick start: +// +// b, err := os.ReadFile("./config/config_test.json") +// if err != nil { +// panic(fmt.Sprintf("Read config error: %s", err.Error())) +// } +// var c config.Config +// err = jsoniter.Unmarshal(b, &c) +// if err != nil { +// panic(fmt.Sprintf("Parse config file error: %s", err.Error())) +// } +// +// wooClient = NewClient(c) +// // Query an order +// order, err := wooClient.Services.Order.One(1) +// if err != nil { +// fmt.Println(err) +// } else { +// fmt.Println(fmt.Sprintf("%#v", order)) +// } +package woogo + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha1" + "crypto/sha256" + "crypto/tls" + "encoding/base64" + "errors" + "fmt" + "log" + "net" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "time" + "unsafe" + + "git.cloudyne.io/go/hiscaler-gox/inx" + "git.cloudyne.io/go/hiscaler-gox/stringx" + "git.cloudyne.io/go/woogo/config" + "github.com/go-resty/resty/v2" + jsoniter "github.com/json-iterator/go" + "github.com/json-iterator/go/extra" +) + +const ( + Version = "1.0.3" + UserAgent = "WooCommerce API Client-Golang/" + Version + HashAlgorithm = "HMAC-SHA256" +) + +// https://woocommerce.github.io/woocommerce-rest-api-docs/?php#request-response-format +const ( + BadRequestError = 400 // 错误的请求 + UnauthorizedError = 401 // 身份验证或权限错误 + NotFoundError = 404 // 访问资源不存在 + InternalServerError = 500 // 服务器内部错误 + MethodNotImplementedError = 501 // 方法未实现 +) + +var ErrNotFound = errors.New("WooCommerce: not found") + +func init() { + extra.RegisterFuzzyDecoders() +} + +type WooCommerce struct { + Debug bool // Is debug mode + Logger *log.Logger // Log + Services services // WooCommerce API services +} + +type service struct { + debug bool // Is debug mode + logger *log.Logger // Log + httpClient *resty.Client // HTTP client +} + +type services struct { + Coupon couponService + Customer customerService + Order orderService + OrderNote orderNoteService + OrderRefund orderRefundService + Product productService + ProductVariation productVariationService + ProductAttribute productAttributeService + ProductAttributeTerm productAttributeTermService + ProductCategory productCategoryService + ProductShippingClass productShippingClassService + ProductTag productTagService + ProductReview productReviewService + Report reportService + TaxRate taxRateService + TaxClass taxClassService + Webhook webhookService + Setting settingService + SettingOption settingOptionService + PaymentGateway paymentGatewayService + ShippingZone shippingZoneService + ShippingZoneLocation shippingZoneLocationService + ShippingZoneMethod shippingZoneMethodService + ShippingMethod shippingMethodService + SystemStatus systemStatusService + SystemStatusTool systemStatusToolService + Data dataService +} + +// OAuth signature +func oauthSignature(config config.Config, method, endpoint string, params url.Values) string { + sb := strings.Builder{} + sb.WriteString(config.ConsumerSecret) + if config.Version != "v1" && config.Version != "v2" { + sb.WriteByte('&') + } + consumerSecret := sb.String() + + sb.Reset() + sb.WriteString(method) + sb.WriteByte('&') + sb.WriteString(url.QueryEscape(endpoint)) + sb.WriteByte('&') + sb.WriteString(url.QueryEscape(params.Encode())) + mac := hmac.New(sha256.New, stringx.ToBytes(consumerSecret)) + mac.Write(stringx.ToBytes(sb.String())) + signatureBytes := mac.Sum(nil) + return base64.StdEncoding.EncodeToString(signatureBytes) +} + +// NewClient Creates a new WooCommerce client +// +// You must give a config with NewClient method params. +// After you can operate data use this client. +func NewClient(config config.Config) *WooCommerce { + logger := log.New(os.Stdout, "[ WooCommerce ] ", log.LstdFlags|log.Llongfile) + wooClient := &WooCommerce{ + Debug: config.Debug, + Logger: logger, + } + // Add default value + if config.Version == "" { + config.Version = "v3" + } else { + config.Version = strings.ToLower(config.Version) + if !inx.StringIn(config.Version, "v1", "v2", "v3") { + config.Version = "v3" + } + } + if config.Timeout < 2 { + config.Timeout = 2 + } + + httpClient := resty.New(). + SetDebug(config.Debug). + SetBaseURL(strings.TrimRight(config.URL, "/") + "/wp-json/wc/" + config.Version). + SetHeaders(map[string]string{ + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": UserAgent, + }). + SetAllowGetMethodPayload(true). + SetTimeout(config.Timeout * time.Second). + SetTransport(&http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.VerifySSL}, + DialContext: (&net.Dialer{ + Timeout: config.Timeout * time.Second, + }).DialContext, + }). + OnBeforeRequest(func(client *resty.Client, request *resty.Request) error { + params := url.Values{} + for k, vs := range request.QueryParam { + var v string + switch len(vs) { + case 0: + continue + case 1: + v = vs[0] + default: + // if is array params, must convert to string, example: status=1&status=2 replace to status=1,2 + v = strings.Join(vs, ",") + } + params.Set(k, v) + } + if strings.HasPrefix(config.URL, "https") { + // basicAuth + if config.AddAuthenticationToURL { + params.Add("consumer_key", config.ConsumerKey) + params.Add("consumer_secret", config.ConsumerSecret) + } else { + // Set to header + client.SetAuthScheme("Basic"). + SetAuthToken(fmt.Sprintf("%s %s", config.ConsumerKey, config.ConsumerSecret)) + } + } else { + // oAuth + params.Add("oauth_consumer_key", config.ConsumerKey) + params.Add("oauth_timestamp", strconv.Itoa(int(time.Now().Unix()))) + nonce := make([]byte, 16) + rand.Read(nonce) + sha1Nonce := fmt.Sprintf("%x", sha1.Sum(nonce)) + params.Add("oauth_nonce", sha1Nonce) + params.Add("oauth_signature_method", HashAlgorithm) + params.Add("oauth_signature", oauthSignature(config, request.Method, client.BaseURL+request.URL, params)) + } + request.QueryParam = params + return nil + }). + OnAfterResponse(func(client *resty.Client, response *resty.Response) (err error) { + if response.IsError() { + r := struct { + Code string `json:"code"` + Message string `json:"message"` + }{} + if err = jsoniter.Unmarshal(response.Body(), &r); err == nil { + err = ErrorWrap(response.StatusCode(), r.Message) + } + } + if err != nil { + logger.Printf("OnAfterResponse error: %s", err.Error()) + } + return + }) + if config.Debug { + httpClient.EnableTrace() + } + + jsoniter.RegisterTypeDecoderFunc("float64", func(ptr unsafe.Pointer, iter *jsoniter.Iterator) { + switch iter.WhatIsNext() { + case jsoniter.StringValue: + var t float64 + v := strings.TrimSpace(iter.ReadString()) + if v != "" { + var err error + if t, err = strconv.ParseFloat(v, 64); err != nil { + iter.Error = err + return + } + } + *((*float64)(ptr)) = t + default: + *((*float64)(ptr)) = iter.ReadFloat64() + } + }) + httpClient.JSONMarshal = jsoniter.Marshal + httpClient.JSONUnmarshal = jsoniter.Unmarshal + xService := service{ + debug: config.Debug, + logger: logger, + httpClient: httpClient, + } + wooClient.Services = services{ + Coupon: (couponService)(xService), + Customer: (customerService)(xService), + Order: (orderService)(xService), + OrderNote: (orderNoteService)(xService), + OrderRefund: (orderRefundService)(xService), + Product: (productService)(xService), + ProductVariation: (productVariationService)(xService), + ProductAttribute: (productAttributeService)(xService), + ProductAttributeTerm: (productAttributeTermService)(xService), + ProductCategory: (productCategoryService)(xService), + ProductShippingClass: (productShippingClassService)(xService), + ProductTag: (productTagService)(xService), + ProductReview: (productReviewService)(xService), + Report: (reportService)(xService), + TaxRate: (taxRateService)(xService), + TaxClass: (taxClassService)(xService), + Webhook: (webhookService)(xService), + Setting: (settingService)(xService), + SettingOption: (settingOptionService)(xService), + PaymentGateway: (paymentGatewayService)(xService), + ShippingZone: (shippingZoneService)(xService), + ShippingZoneLocation: (shippingZoneLocationService)(xService), + ShippingZoneMethod: (shippingZoneMethodService)(xService), + ShippingMethod: (shippingMethodService)(xService), + SystemStatus: (systemStatusService)(xService), + SystemStatusTool: (systemStatusToolService)(xService), + Data: (dataService)(xService), + } + return wooClient +} + +// Parse response header, get total and total pages, and check it is last page +func parseResponseTotal(currentPage int, resp *resty.Response) (total, totalPages int, isLastPage bool) { + if currentPage == 0 { + currentPage = 1 + } + value := resp.Header().Get("X-Wp-Total") + if value != "" { + total, _ = strconv.Atoi(value) + } + + value = resp.Header().Get("X-Wp-Totalpages") + if value != "" { + totalPages, _ = strconv.Atoi(value) + } + isLastPage = currentPage >= totalPages + return +} + +// ErrorWrap wrap an error, if status code is 200, return nil, otherwise return an error +func ErrorWrap(code int, message string) error { + if code == http.StatusOK { + return nil + } + + if code == NotFoundError { + return ErrNotFound + } + + message = strings.TrimSpace(message) + if message == "" { + switch code { + case BadRequestError: + message = "Bad request" + case UnauthorizedError: + message = "Unauthorized operation, please confirm whether you have permission" + case NotFoundError: + message = "Resource not found" + case InternalServerError: + message = "Server internal error" + case MethodNotImplementedError: + message = "method not implemented" + default: + message = "Unknown error" + } + } + return fmt.Errorf("%d: %s", code, message) +} diff --git a/woo_test.go b/woo_test.go new file mode 100644 index 0000000..7de2bd2 --- /dev/null +++ b/woo_test.go @@ -0,0 +1,90 @@ +package woogo + +import ( + "fmt" + "os" + "testing" + + "git.cloudyne.io/go/woogo/config" + jsoniter "github.com/json-iterator/go" +) + +var wooClient *WooCommerce + +var orderId, noteId int +var mainId, childId int + +// Operate data use WooCommerce for golang +func Example() { + b, err := os.ReadFile("./config/config_test.json") + if err != nil { + panic(fmt.Sprintf("Read config error: %s", err.Error())) + } + var c config.Config + err = jsoniter.Unmarshal(b, &c) + if err != nil { + panic(fmt.Sprintf("Parse config file error: %s", err.Error())) + } + + wooClient = NewClient(c) + // Query an order + order, err := wooClient.Services.Order.One(1) + if err != nil { + fmt.Println(err) + } else { + fmt.Println(fmt.Sprintf("%#v", order)) + } + + // Query orders + params := OrdersQueryParams{ + After: "2022-06-10", + } + params.PerPage = 100 + for { + orders, total, totalPages, isLastPage, err := wooClient.Services.Order.All(params) + if err != nil { + break + } + fmt.Println(fmt.Sprintf("Page %d/%d", total, totalPages)) + // read orders + for _, order := range orders { + _ = order + } + if err != nil || isLastPage { + break + } + params.Page++ + } +} + +func ExampleErrorWrap() { + err := ErrorWrap(200, "Ok") + if err != nil { + return + } +} + +func getOrderId(t *testing.T) { + t.Log("Execute getOrderId test") + items, _, _, _, err := wooClient.Services.Order.All(OrdersQueryParams{}) + if err != nil || len(items) == 0 { + t.FailNow() + } + orderId = items[0].ID + mainId = items[0].ID +} + +func TestMain(m *testing.M) { + b, err := os.ReadFile("./config/config_test.json") + if err != nil { + panic(fmt.Sprintf("Read config error: %s", err.Error())) + } + var c config.Config + err = jsoniter.Unmarshal(b, &c) + if err != nil { + panic(fmt.Sprintf("Parse config file error: %s", err.Error())) + } + + wooClient = NewClient(c) + m.Run() +}