This commit is contained in:
scheibling
2025-04-08 19:24:11 +02:00
commit 30fb57f4f7
92 changed files with 6196 additions and 0 deletions

26
.gitignore vendored Normal file
View File

@@ -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/

37
CHANGELOG.md Normal file
View File

@@ -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.

29
LICENSE Normal file
View File

@@ -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.

14
config/config.go Normal file
View File

@@ -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
}

10
config/config_test.json Normal file
View File

@@ -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
}

View File

@@ -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"
)

208
coupon.go Normal file
View File

@@ -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
}

149
coupon_test.go Normal file
View File

@@ -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")
}

219
customer.go Normal file
View File

@@ -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
}

189
customer_test.go Normal file
View File

@@ -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))
}
}

114
data.go Normal file
View File

@@ -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
}

32
data_test.go Normal file
View File

@@ -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)
}

29
entity/billing.go Normal file
View File

@@ -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("名不能为空")),
)
}

32
entity/coupon.go Normal file
View File

@@ -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"`
}

21
entity/customer.go Normal file
View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

48
entity/data.go Normal file
View File

@@ -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"`
}

13
entity/image.go Normal file
View File

@@ -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"`
}

7
entity/meta.go Normal file
View File

@@ -0,0 +1,7 @@
package entity
type Meta struct {
ID int `json:"id"`
Key string `json:"key"`
Value string `json:"value"`
}

91
entity/order.go Normal file
View File

@@ -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"`
}

12
entity/order_note.go Normal file
View File

@@ -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"`
}

34
entity/order_refund.go Normal file
View File

@@ -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"`
}

25
entity/payment_gateway.go Normal file
View File

@@ -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"`
}

View File

@@ -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"`
}

86
entity/product.go Normal file
View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

15
entity/product_review.go Normal file
View File

@@ -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"`
}

View File

@@ -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"`
}

10
entity/product_tag.go Normal file
View File

@@ -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"`
}

View File

@@ -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"`
}

57
entity/report.go Normal file
View File

@@ -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

10
entity/setting_group.go Normal file
View File

@@ -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"`
}

15
entity/setting_option.go Normal file
View File

@@ -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"`
}

23
entity/shipping.go Normal file
View File

@@ -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"`
}

12
entity/shipping_method.go Normal file
View File

@@ -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"`
}

8
entity/shipping_zone.go Normal file
View File

@@ -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"`
}

View File

@@ -0,0 +1,7 @@
package entity
// ShippingZoneLocation shipping zone location
type ShippingZoneLocation struct {
Code string `json:"code"`
Type string `json:"type"`
}

View File

@@ -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"`
}

11
entity/system_status.go Normal file
View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -0,0 +1,7 @@
package entity
// SystemStatusSecurity System status security properties
type SystemStatusSecurity struct {
SecureConnection bool `json:"secure_connection"`
HideErrors bool `json:"hide_errors"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

7
entity/tax_class.go Normal file
View File

@@ -0,0 +1,7 @@
package entity
// TaxClass tax class properties
type TaxClass struct {
Slug string `json:"slug"`
Name string `json:"name"`
}

19
entity/tax_rate.go Normal file
View File

@@ -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"`
}

23
entity/taxes.go Normal file
View File

@@ -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"`
}

18
entity/webhook.go Normal file
View File

@@ -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"`
}

14
go.mod Normal file
View File

@@ -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
)

208
order.go Normal file
View File

@@ -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
}

97
order_note.go Normal file
View File

@@ -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
}

74
order_note_test.go Normal file
View File

@@ -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)
}
}
}

132
order_refund.go Normal file
View File

@@ -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
}

80
order_refund_test.go Normal file
View File

@@ -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)
}
}
}

98
order_test.go Normal file
View File

@@ -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())
}
}

69
payment_gateway.go Normal file
View File

@@ -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
}

57
payment_gateway_test.go Normal file
View File

@@ -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")
}

204
product.go Normal file
View File

@@ -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
}

164
product_attribute.go Normal file
View File

@@ -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
}

170
product_attribute_term.go Normal file
View File

@@ -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
}

173
product_category.go Normal file
View File

@@ -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
}

92
product_category_test.go Normal file
View File

@@ -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")
}

192
product_review.go Normal file
View File

@@ -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
}

170
product_shipping_class.go Normal file
View File

@@ -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
}

169
product_tag.go Normal file
View File

@@ -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
}

91
product_tag_test.go Normal file
View File

@@ -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")
}

106
product_test.go Normal file
View File

@@ -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)
}
}

227
product_variation.go Normal file
View File

@@ -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
}

17
product_variation_test.go Normal file
View File

@@ -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))
}
}

66
query_params.go Normal file
View File

@@ -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
}

181
report.go Normal file
View File

@@ -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
}

21
report_test.go Normal file
View File

@@ -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)
}

20
setting.go Normal file
View File

@@ -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
}

68
setting_option.go Normal file
View File

@@ -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
}

36
shipping_method.go Normal file
View File

@@ -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
}

100
shipping_zone.go Normal file
View File

@@ -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
}

66
shipping_zone_location.go Normal file
View File

@@ -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
}

116
shipping_zone_method.go Normal file
View File

@@ -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
}

20
system_status.go Normal file
View File

@@ -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
}

46
system_status_tool.go Normal file
View File

@@ -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
}

74
tax_class.go Normal file
View File

@@ -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
}

172
tax_rate.go Normal file
View File

@@ -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
}

52
utils.go Normal file
View File

@@ -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)
}
}

25
utils_test.go Normal file
View File

@@ -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)
}
}

183
webhook.go Normal file
View File

@@ -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
}

335
woo.go Normal file
View File

@@ -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)
}

90
woo_test.go Normal file
View File

@@ -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()
}