Correct v2 (#55)

* refactor library and upgrade version
* remove useless environment variable
* remove unsupported versions from go.mod
* fixes for error handling & data types
* different improvements for errors
* fixes for types and tests
* better coverage, fix error with the unmarshalers
This commit is contained in:
Pavel 2021-10-27 15:49:06 +03:00 committed by GitHub
parent 269764175e
commit 97b1abf470
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 2173 additions and 1193 deletions

View File

@ -9,7 +9,6 @@ on:
pull_request: pull_request:
env: env:
DEVELOPER_NODE: 1
RETAILCRM_URL: https://test.retailcrm.pro RETAILCRM_URL: https://test.retailcrm.pro
RETAILCRM_KEY: key RETAILCRM_KEY: key
@ -41,7 +40,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
go-version: ['1.11', '1.12', '1.13', '1.14', '1.15', '1.16', '1.17'] go-version: ['1.13', '1.14', '1.15', '1.16', '1.17']
include: include:
- go-version: '1.17' - go-version: '1.17'
coverage: 1 coverage: 1

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
# Folders # Folders
_obj _obj
vendor
# Architecture specific extensions/prefixes # Architecture specific extensions/prefixes
*.[568vq] *.[568vq]

View File

@ -146,9 +146,9 @@ linters-settings:
gocyclo: gocyclo:
min-complexity: 25 min-complexity: 25
goimports: goimports:
local-prefixes: github.com/retailcrm/api-client-go local-prefixes: github.com/retailcrm/api-client-go/v2
lll: lll:
line-length: 120 line-length: 160
maligned: maligned:
suggest-new: true suggest-new: true
misspell: misspell:
@ -180,6 +180,9 @@ issues:
- gocognit - gocognit
- gocyclo - gocyclo
- godot - godot
- gocritic
- gosec
- staticcheck
exclude-use-default: true exclude-use-default: true
exclude-case-sensitive: false exclude-case-sensitive: false
max-issues-per-linter: 0 max-issues-per-linter: 0

109
README.md
View File

@ -10,51 +10,51 @@
This is golang RetailCRM API client. This is golang RetailCRM API client.
## Install ## Installation
```bash ```bash
go get -x github.com/retailcrm/api-client-go go get -u github.com/retailcrm/api-client-go/v2
``` ```
## Usage ## Usage
```golang Example:
```go
package main package main
import ( import (
"fmt" "log"
"net/http"
"github.com/retailcrm/api-client-go/v5" "github.com/retailcrm/api-client-go/v2"
) )
func main() { func main() {
var client = v5.New("https://demo.retailcrm.pro", "09jIJ09j0JKhgyfvyuUIKhiugF") var client = retailcrm.New("https://demo.retailcrm.pro", "09jIJ09j0JKhgyfvyuUIKhiugF")
data, status, err := client.Orders(v5.OrdersRequest{ data, status, err := client.Orders(retailcrm.OrdersRequest{
Filter: v5.OrdersFilter{}, Filter: retailcrm.OrdersFilter{},
Limit: 20, Limit: 20,
Page: 1, Page: 1,
},) })
if err != nil { if err != nil {
fmt.Printf("%v", err.Error()) if apiErr, ok := retailcrm.AsAPIError(err); ok {
} log.Fatalf("http status: %d, %s", status, apiErr.String())
}
if status >= http.StatusBadRequest { log.Fatalf("http status: %d, error: %s", status, err)
fmt.Printf("%v", err.ApiError())
} }
for _, value := range data.Orders { for _, value := range data.Orders {
fmt.Printf("%v\n", value.Email) log.Printf("%v\n", value.Email)
} }
fmt.Println(data.Orders[1].FirstName) log.Println(data.Orders[1].FirstName)
idata, status, err := c.InventoriesUpload( inventories, status, err := client.InventoriesUpload([]retailcrm.InventoryUpload{
[]InventoryUpload{
{ {
XMLID: "pTKIKAeghYzX21HTdzFCe1", XMLID: "pTKIKAeghYzX21HTdzFCe1",
Stores: []InventoryUploadStore{ Stores: []retailcrm.InventoryUploadStore{
{ {
Code: "test-store-v5", Code: "test-store-v5",
Available: 10, Available: 10,
@ -74,7 +74,7 @@ func main() {
}, },
{ {
XMLID: "JQIvcrCtiSpOV3AAfMiQB3", XMLID: "JQIvcrCtiSpOV3AAfMiQB3",
Stores: []InventoryUploadStore{ Stores: []retailcrm.InventoryUploadStore{
{ {
Code: "test-store-v5", Code: "test-store-v5",
Available: 45, Available: 45,
@ -95,13 +95,70 @@ func main() {
}, },
) )
if err != nil { if err != nil {
fmt.Printf("%v", err.Error()) if apiErr, ok := retailcrm.AsAPIError(err); ok {
log.Fatalf("http status: %d, %s", status, apiErr.String())
}
log.Fatalf("http status: %d, error: %s", status, err)
} }
if status >= http.StatusBadRequest { log.Println(inventories.ProcessedOffersCount)
fmt.Printf("%v", err.ApiError())
}
fmt.Println(idata.processedOffersCount)
} }
``` ```
You can use different error types and `retailcrm.AsAPIError` to process client errors. Example:
```go
package retailcrm
import (
"errors"
"log"
"os"
"strings"
"github.com/retailcrm/api-client-go/v2"
)
func main() {
var client = retailcrm.New("https://demo.retailcrm.pro", "09jIJ09j0JKhgyfvyuUIKhiugF")
resp, status, err := client.APICredentials()
if err != nil {
apiErr, ok := retailcrm.AsAPIError(err)
if !ok {
log.Fatalf("http status: %d, error: %s", status, err)
}
switch {
case errors.Is(apiErr, retailcrm.ErrMissingCredentials):
log.Fatalln("No API key provided.")
case errors.Is(apiErr, retailcrm.ErrInvalidCredentials):
log.Fatalln("Invalid API key.")
case errors.Is(apiErr, retailcrm.ErrAccessDenied):
log.Fatalln("Access denied. Please check that the provided key has access to the credentials info.")
case errors.Is(apiErr, retailcrm.ErrAccountDoesNotExist):
log.Fatalln("There is no RetailCRM at the provided URL.")
case errors.Is(apiErr, retailcrm.ErrMissingParameter):
// retailcrm.APIError in this case will always contain "Name" key in the errors list with the parameter name.
log.Fatalln("This parameter should be present:", apiErr.Errors()["Name"])
case errors.Is(apiErr, retailcrm.ErrValidation):
log.Println("Validation errors from the API:")
for name, value := range apiErr.Errors() {
log.Printf(" - %s: %s\n", name, value)
}
os.Exit(1)
case errors.Is(apiErr, retailcrm.ErrGeneric):
log.Fatalf("failure from the API. %s", apiErr.String())
}
}
log.Println("Available scopes:", strings.Join(resp.Scopes, ", "))
}
```
## Upgrading
Please check the [UPGRADING.md](UPGRADING.md) to learn how to upgrade to the new version.

70
UPGRADING.md Normal file
View File

@ -0,0 +1,70 @@
# Upgrading to the v2
### Install the new version
```bash
go get -u github.com/retailcrm/api-client-go/v2
```
### Update all imports
Before:
```go
package main
import v5 "github.com/retailcrm/api-client-go/v5"
```
After:
```go
package main
import "github.com/retailcrm/api-client-go/v2"
```
You can use package alias `v5` to skip the second step.
### Replace package name for all imported symbols
Before:
```go
package main
import v5 "github.com/retailcrm/api-client-go/v5"
func main() {
client := v5.New("https://test.retailcrm.pro", "key")
data, status, err := client.Orders(v5.OrdersRequest{
Filter: v5.OrdersFilter{
City: "Moscow",
},
Page: 1,
})
...
}
```
After:
```go
package main
import "github.com/retailcrm/api-client-go/v2"
func main() {
client := retailcrm.New("https://test.retailcrm.pro", "key")
data, status, err := client.Orders(retailcrm.OrdersRequest{
Filter: retailcrm.OrdersFilter{
City: "Moscow",
},
Page: 1,
})
...
}
```
### Upgrade client usages
This major release contains some breaking changes regarding field names and fully redesigned error handling. Use the second example from
the readme to learn how to process errors correctly.

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
package v5 package retailcrm
import ( import (
"encoding/json" "encoding/json"
@ -20,14 +20,12 @@ import (
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
if os.Getenv("DEVELOPER_NODE") == "1" { err := godotenv.Load(".env")
err := godotenv.Load("../.env") if err != nil {
if err != nil { log.Fatal("Error loading .env file")
log.Fatal("Error loading .env file")
}
os.Exit(m.Run())
} }
os.Exit(m.Run())
} }
func init() { func init() {
@ -494,7 +492,7 @@ func TestClient_CustomersUpload_Fail(t *testing.T) {
MatchType("url"). MatchType("url").
BodyString(p.Encode()). BodyString(p.Encode()).
Reply(460). Reply(460).
BodyString(`{"success": false, "errorMsg": "Customers are loaded with errors"}`) BodyString(`{"success": false, "errorMsg": "Customers are loaded with ErrorsList"}`)
data, _, err := c.CustomersUpload(customers) data, _, err := c.CustomersUpload(customers)
if err == nil { if err == nil {
@ -631,7 +629,7 @@ func TestClient_CustomersFixExternalIds_Fail(t *testing.T) {
MatchType("url"). MatchType("url").
BodyString(p.Encode()). BodyString(p.Encode()).
Reply(400). Reply(400).
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"id": "ID must be an integer"}}`) BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"id": "ID must be an integer"}}`)
data, _, err := c.CustomersFixExternalIds(customers) data, _, err := c.CustomersFixExternalIds(customers)
if err == nil { if err == nil {
@ -690,7 +688,7 @@ func TestClient_CustomersHistory_Fail(t *testing.T) {
Get("/customers/history"). Get("/customers/history").
MatchParam("filter[startDate]", "2020-13-12"). MatchParam("filter[startDate]", "2020-13-12").
Reply(400). Reply(400).
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"children[startDate]": "Значение недопустимо."}}`) BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"children[startDate]": "Значение недопустимо."}}`)
data, _, err := c.CustomersHistory(f) data, _, err := c.CustomersHistory(f)
if err == nil { if err == nil {
@ -885,7 +883,7 @@ func TestClient_CorporateCustomersFixExternalIds_Fail(t *testing.T) {
MatchType("url"). MatchType("url").
BodyString(p.Encode()). BodyString(p.Encode()).
Reply(400). Reply(400).
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"id": "ID must be an integer"}}`) BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"id": "ID must be an integer"}}`)
data, _, err := c.CorporateCustomersFixExternalIds(customers) data, _, err := c.CorporateCustomersFixExternalIds(customers)
if err == nil { if err == nil {
@ -944,7 +942,7 @@ func TestClient_CorporateCustomersHistory_Fail(t *testing.T) {
Get("/customers-corporate/history"). Get("/customers-corporate/history").
MatchParam("filter[startDate]", "2020-13-12"). MatchParam("filter[startDate]", "2020-13-12").
Reply(400). Reply(400).
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"children[startDate]": "Значение недопустимо."}}`) BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"children[startDate]": "Значение недопустимо."}}`)
data, _, err := c.CorporateCustomersHistory(f) data, _, err := c.CorporateCustomersHistory(f)
if err == nil { if err == nil {
@ -1130,7 +1128,7 @@ func TestClient_CorporateCustomersUpload_Fail(t *testing.T) {
MatchType("url"). MatchType("url").
BodyString(p.Encode()). BodyString(p.Encode()).
Reply(460). Reply(460).
BodyString(`{"success": false, "errorMsg": "Customers are loaded with errors"}`) BodyString(`{"success": false, "errorMsg": "Customers are loaded with ErrorsList"}`)
data, _, err := c.CorporateCustomersUpload(customers) data, _, err := c.CorporateCustomersUpload(customers)
if err == nil { if err == nil {
@ -1703,7 +1701,7 @@ func TestClient_NotesNotes_Fail(t *testing.T) {
Get("/customers/notes"). Get("/customers/notes").
MatchParam("filter[createdAtFrom]", "2020-13-12"). MatchParam("filter[createdAtFrom]", "2020-13-12").
Reply(400). Reply(400).
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"children[createdAtFrom]": "This value is not valid."}}`) BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"children[createdAtFrom]": "This value is not valid."}}`)
data, _, err := c.CustomerNotes(NotesRequest{ data, _, err := c.CustomerNotes(NotesRequest{
Filter: NotesFilter{CreatedAtFrom: "2020-13-12"}, Filter: NotesFilter{CreatedAtFrom: "2020-13-12"},
@ -1803,7 +1801,7 @@ func TestClient_NotesCreateDelete_Fail(t *testing.T) {
MatchType("url"). MatchType("url").
BodyString(p.Encode()). BodyString(p.Encode()).
Reply(400). Reply(400).
BodyString(`{"success": false, "errorMsg": "Errors in the entity format", "errors": {"customer": "Set one of the following fields: id, externalId"}}`) BodyString(`{"success": false, "errorMsg": "Errors in the entity format", "ErrorsList": {"customer": "Set one of the following fields: id, externalId"}}`)
data, _, err := c.CustomerNoteCreate(note) data, _, err := c.CustomerNoteCreate(note)
if err == nil { if err == nil {
@ -1875,7 +1873,7 @@ func TestClient_OrdersOrders_Fail(t *testing.T) {
Get("/orders"). Get("/orders").
MatchParam("filter[attachments]", "7"). MatchParam("filter[attachments]", "7").
Reply(400). Reply(400).
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"children[attachments]": "SThis value is not valid."}}`) BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"children[attachments]": "SThis value is not valid."}}`)
data, _, err := c.Orders(OrdersRequest{Filter: OrdersFilter{Attachments: 7}}) data, _, err := c.Orders(OrdersRequest{Filter: OrdersFilter{Attachments: 7}})
if err == nil { if err == nil {
@ -2166,7 +2164,7 @@ func TestClient_OrdersUpload_Fail(t *testing.T) {
MatchType("url"). MatchType("url").
BodyString(p.Encode()). BodyString(p.Encode()).
Reply(460). Reply(460).
BodyString(`{"success": false, "errorMsg": "Orders are loaded with errors"}`) BodyString(`{"success": false, "errorMsg": "Orders are loaded with ErrorsList"}`)
data, _, err := c.OrdersUpload(orders) data, _, err := c.OrdersUpload(orders)
if err == nil { if err == nil {
@ -2395,7 +2393,7 @@ func TestClient_OrdersHistory_Fail(t *testing.T) {
Get("/orders/history"). Get("/orders/history").
MatchParam("filter[startDate]", "2020-13-12"). MatchParam("filter[startDate]", "2020-13-12").
Reply(400). Reply(400).
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"children[startDate]": "Значение недопустимо."}}`) BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"children[startDate]": "Значение недопустимо."}}`)
data, _, err := c.OrdersHistory(OrdersHistoryRequest{Filter: OrdersHistoryFilter{StartDate: "2020-13-12"}}) data, _, err := c.OrdersHistory(OrdersHistoryRequest{Filter: OrdersHistoryFilter{StartDate: "2020-13-12"}})
if err == nil { if err == nil {
@ -2521,7 +2519,7 @@ func TestClient_PaymentCreateEditDelete_Fail(t *testing.T) {
MatchType("url"). MatchType("url").
BodyString(p.Encode()). BodyString(p.Encode()).
Reply(400). Reply(400).
BodyString(`{"success": false, "errorMsg": "Errors in the entity format", "errors": {"order": "Set one of the following fields: id, externalId, number"}}`) BodyString(`{"success": false, "errorMsg": "Errors in the entity format", "ErrorsList": {"order": "Set one of the following fields: id, externalId, number"}}`)
data, _, err := c.OrderPaymentCreate(f) data, _, err := c.OrderPaymentCreate(f)
if err == nil { if err == nil {
@ -2743,7 +2741,7 @@ func TestClient_TaskChange_Fail(t *testing.T) {
MatchType("url"). MatchType("url").
BodyString(p.Encode()). BodyString(p.Encode()).
Reply(400). Reply(400).
BodyString(`{"success": false, "errorMsg": "Task is not loaded", "errors": {"performerId": "This value should not be blank."}}`) BodyString(`{"success": false, "errorMsg": "Task is not loaded", "ErrorsList": {"performerId": "This value should not be blank."}}`)
data, _, err := c.TaskEdit(f) data, _, err := c.TaskEdit(f)
if err == nil { if err == nil {
@ -2792,7 +2790,7 @@ func TestClient_UsersUsers_Fail(t *testing.T) {
MatchParam("filter[active]", "3"). MatchParam("filter[active]", "3").
MatchParam("page", "1"). MatchParam("page", "1").
Reply(400). Reply(400).
BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "errors": {"active": "he value you selected is not a valid choice."}}`) BodyString(`{"success": false, "errorMsg": "Errors in the input parameters", "ErrorsList": {"active": "he value you selected is not a valid choice."}}`)
data, _, err := c.Users(UsersRequest{Filter: UsersFilter{Active: 3}, Page: 1}) data, _, err := c.Users(UsersRequest{Filter: UsersFilter{Active: 3}, Page: 1})
if err == nil { if err == nil {
@ -3550,8 +3548,8 @@ func TestClient_Courier(t *testing.T) {
cur := Courier{ cur := Courier{
Active: true, Active: true,
Email: fmt.Sprintf("%s@example.com", RandomString(5)), Email: fmt.Sprintf("%s@example.com", RandomString(5)),
FirstName: fmt.Sprintf("%s", RandomString(5)), FirstName: RandomString(5),
LastName: fmt.Sprintf("%s", RandomString(5)), LastName: RandomString(5),
} }
defer gock.Off() defer gock.Off()
@ -3583,7 +3581,7 @@ func TestClient_Courier(t *testing.T) {
} }
cur.ID = data.ID cur.ID = data.ID
cur.Patronymic = fmt.Sprintf("%s", RandomString(5)) cur.Patronymic = RandomString(5)
jr, _ = json.Marshal(&cur) jr, _ = json.Marshal(&cur)
@ -3629,7 +3627,7 @@ func TestClient_Courier_Fail(t *testing.T) {
MatchType("url"). MatchType("url").
BodyString(p.Encode()). BodyString(p.Encode()).
Reply(400). Reply(400).
BodyString(`{"success": false, "errorMsg": "Errors in the entity format", "errors": {"firstName": "Specify the first name"}}`) BodyString(`{"success": false, "errorMsg": "Errors in the entity format", "ErrorsList": {"firstName": "Specify the first name"}}`)
data, st, err := c.CourierCreate(Courier{}) data, st, err := c.CourierCreate(Courier{})
if err == nil { if err == nil {
@ -3644,7 +3642,7 @@ func TestClient_Courier_Fail(t *testing.T) {
t.Error(successFail) t.Error(successFail)
} }
cur := Courier{Patronymic: fmt.Sprintf("%s", RandomString(5))} cur := Courier{Patronymic: RandomString(5)}
jr, _ = json.Marshal(&cur) jr, _ = json.Marshal(&cur)
p = url.Values{ p = url.Values{
@ -5793,7 +5791,7 @@ func TestClient_CostsUpload_Fail(t *testing.T) {
MatchType("url"). MatchType("url").
BodyString(p.Encode()). BodyString(p.Encode()).
Reply(460). Reply(460).
BodyString(`{"success": false, "errorMsg": "Costs are loaded with errors"}`) BodyString(`{"success": false, "errorMsg": "Costs are loaded with ErrorsList"}`)
data, _, err := c.CostsUpload(costsUpload) data, _, err := c.CostsUpload(costsUpload)
if err == nil { if err == nil {

192
error.go Normal file
View File

@ -0,0 +1,192 @@
package retailcrm
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
var missingParameterMatcher = regexp.MustCompile(`^Parameter \'([\w\]\[\_\-]+)\' is missing$`)
var (
// ErrMissingCredentials will be returned if no API key was provided to the API.
ErrMissingCredentials = NewAPIError(`apiKey is missing`)
// ErrInvalidCredentials will be returned if provided API key is invalid.
ErrInvalidCredentials = NewAPIError(`wrong "apiKey" value`)
// ErrAccessDenied will be returned in case of "Access denied" error.
ErrAccessDenied = NewAPIError("access denied")
// ErrAccountDoesNotExist will be returned if target system does not exist.
ErrAccountDoesNotExist = NewAPIError("account does not exist")
// ErrValidation will be returned in case of validation errors.
ErrValidation = NewAPIError("validation error")
// ErrMissingParameter will be returned if parameter is missing.
// Underlying error messages list will contain parameter name in the "Name" key.
ErrMissingParameter = NewAPIError("missing parameter")
// ErrGeneric will be returned if error cannot be classified as one of the errors above.
ErrGeneric = NewAPIError("API error")
)
// APIErrorsList struct.
type APIErrorsList map[string]string
// APIError returns when an API error was occurred.
type APIError interface {
error
fmt.Stringer
withWrapped(error) APIError
withErrors(APIErrorsList) APIError
Unwrap() error
Errors() APIErrorsList
}
type apiError struct {
ErrorMsg string `json:"errorMsg,omitempty"`
ErrorsList APIErrorsList `json:"errors,omitempty"`
wrapped error
}
// CreateAPIError from the provided response data. Different error types will be returned depending on the response,
// all of them can be matched using errors.Is. APi errors will always implement APIError interface.
func CreateAPIError(dataResponse []byte) error {
a := &apiError{}
if len(dataResponse) > 0 && dataResponse[0] == '<' {
return ErrAccountDoesNotExist
}
if err := json.Unmarshal(dataResponse, &a); err != nil {
return err
}
var found APIError
switch a.ErrorMsg {
case `"apiKey" is missing.`:
found = ErrMissingCredentials
case `Wrong "apiKey" value.`:
found = ErrInvalidCredentials
case "Access denied.":
found = ErrAccessDenied
case "Account does not exist.":
found = ErrAccountDoesNotExist
case "Errors in the entity format":
fallthrough
case "Validation error":
found = ErrValidation
default:
if param, ok := asMissingParameterErr(a.ErrorMsg); ok {
return a.withWrapped(ErrMissingParameter).withErrors(APIErrorsList{"Name": param})
}
found = ErrGeneric
}
result := NewAPIError(a.ErrorMsg).withWrapped(found)
if len(a.ErrorsList) > 0 {
return result.withErrors(a.ErrorsList)
}
return result
}
// CreateGenericAPIError for the situations when API response cannot be processed, but response was actually received.
func CreateGenericAPIError(message string) APIError {
return NewAPIError(message).withWrapped(ErrGeneric)
}
// NewAPIError returns API error with the provided message.
func NewAPIError(message string) APIError {
return &apiError{ErrorMsg: message}
}
// AsAPIError returns APIError and true if provided error is an APIError or contains wrapped APIError.
// Returns (nil, false) otherwise.
func AsAPIError(err error) (APIError, bool) {
apiErr := unwrapAPIError(err)
return apiErr, apiErr != nil
}
func unwrapAPIError(err error) APIError {
if err == nil {
return nil
}
if apiErr, ok := err.(APIError); ok { // nolint:errorlint
return apiErr
}
wrapper, ok := err.(interface { // nolint:errorlint
Unwrap() error
})
if ok {
return unwrapAPIError(wrapper.Unwrap())
}
return nil
}
// asMissingParameterErr returns true if "Parameter 'name' is missing" error message is provided.
func asMissingParameterErr(message string) (string, bool) {
matches := missingParameterMatcher.FindAllStringSubmatch(message, -1)
if len(matches) == 1 && len(matches[0]) == 2 {
return matches[0][1], true
}
return "", false
}
// Error returns errorMsg field from the response.
func (e *apiError) Error() string {
return e.ErrorMsg
}
// Unwrap returns wrapped error. It is usually one of the predefined types like ErrGeneric or ErrValidation.
// It can be used directly, but it's main purpose is to make errors matchable via errors.Is call.
func (e *apiError) Unwrap() error {
return e.wrapped
}
// Errors returns errors field from the response.
func (e *apiError) Errors() APIErrorsList {
return e.ErrorsList
}
// String returns string representation of an APIError.
func (e *apiError) String() string {
var sb strings.Builder
sb.Grow(256) // nolint:gomnd
sb.WriteString(fmt.Sprintf(`errorMsg: "%s"`, e.Error()))
if len(e.Errors()) > 0 {
i := 0
useIndex := true
errorList := make([]string, len(e.Errors()))
for index, errText := range e.Errors() {
if i == 0 && index == "0" {
useIndex = false
}
if useIndex {
errorList[i] = fmt.Sprintf(`%s: "%s"`, index, errText)
} else {
errorList[i] = errText
}
i++
}
sb.WriteString(", errors: [" + strings.Join(errorList, ", ") + "]")
}
return sb.String()
}
// withWrapped is a wrapped setter.
func (e *apiError) withWrapped(err error) APIError {
e.wrapped = err
return e
}
// withErrors is an ErrorsList setter.
func (e *apiError) withErrors(m APIErrorsList) APIError {
e.ErrorsList = m
return e
}

171
error_test.go Normal file
View File

@ -0,0 +1,171 @@
package retailcrm
import (
"testing"
"github.com/stretchr/testify/suite"
)
type ErrorTest struct {
suite.Suite
}
func TestError(t *testing.T) {
suite.Run(t, new(ErrorTest))
}
func (t *ErrorTest) TestFailure_ApiErrorsSlice() {
b := []byte(`{"success": false,
"errorMsg": "Failed to activate module",
"errors": [
"Your account has insufficient funds to activate integration module",
"Test error"
]}`)
expected := APIErrorsList{
"0": "Your account has insufficient funds to activate integration module",
"1": "Test error",
}
e := CreateAPIError(b)
apiErr, ok := AsAPIError(e)
t.Require().ErrorIs(e, ErrGeneric)
t.Require().NotNil(apiErr)
t.Require().True(ok)
t.Assert().Equal(expected, apiErr.Errors())
}
func (t *ErrorTest) TestFailure_ApiErrorsMap() {
b := []byte(`{"success": false,
"errorMsg": "Failed to activate module",
"errors": {"id": "ID must be an integer", "test": "Test error"}}`,
)
expected := APIErrorsList{
"id": "ID must be an integer",
"test": "Test error",
}
e := CreateAPIError(b)
apiErr, ok := AsAPIError(e)
t.Require().ErrorIs(e, ErrGeneric)
t.Require().NotNil(apiErr)
t.Require().True(ok)
t.Assert().Equal(expected, apiErr.Errors())
}
func (t *ErrorTest) TestFailure_APIKeyMissing() {
b := []byte(`{"success": false,
"errorMsg": "\"apiKey\" is missing."}`,
)
e := CreateAPIError(b)
apiErr, ok := AsAPIError(e)
t.Require().NotNil(apiErr)
t.Require().True(ok)
t.Require().ErrorIs(e, ErrMissingCredentials)
}
func (t *ErrorTest) TestFailure_APIKeyWrong() {
b := []byte(`{"success": false,
"errorMsg": "Wrong \"apiKey\" value."}`,
)
e := CreateAPIError(b)
apiErr, ok := AsAPIError(e)
t.Require().NotNil(apiErr)
t.Require().True(ok)
t.Require().ErrorIs(e, ErrInvalidCredentials)
}
func (t *ErrorTest) TestFailure_AccessDenied() {
b := []byte(`{"success": false,
"errorMsg": "Access denied."}`,
)
e := CreateAPIError(b)
apiErr, ok := AsAPIError(e)
t.Require().NotNil(apiErr)
t.Require().True(ok)
t.Require().ErrorIs(e, ErrAccessDenied)
}
func (t *ErrorTest) TestFailure_AccountDoesNotExist() {
b := []byte(`{"success": false,
"errorMsg": "Account does not exist."}`,
)
e := CreateAPIError(b)
apiErr, ok := AsAPIError(e)
t.Require().NotNil(apiErr)
t.Require().True(ok)
t.Require().ErrorIs(e, ErrAccountDoesNotExist)
}
func (t *ErrorTest) TestFailure_Validation() {
b := []byte(`{"success": false,
"errorMsg": "Errors in the entity format",
"errors": {"name": "name must be provided"}}`,
)
e := CreateAPIError(b)
apiErr, ok := AsAPIError(e)
t.Require().NotNil(apiErr)
t.Require().True(ok)
t.Require().ErrorIs(e, ErrValidation)
t.Assert().Equal("name must be provided", apiErr.Errors()["name"])
}
func (t *ErrorTest) TestFailure_Validation2() {
b := []byte(`{"success": false,
"errorMsg": "Validation error",
"errors": {"name": "name must be provided"}}`,
)
e := CreateAPIError(b)
apiErr, ok := AsAPIError(e)
t.Require().NotNil(apiErr)
t.Require().True(ok)
t.Require().ErrorIs(e, ErrValidation)
t.Assert().Equal("name must be provided", apiErr.Errors()["name"])
t.Assert().Equal("errorMsg: \"Validation error\", errors: [name: \"name must be provided\"]", apiErr.String())
}
func (t *ErrorTest) TestFailure_MissingParameter() {
b := []byte(`{"success": false,
"errorMsg": "Parameter 'item' is missing"}`,
)
e := CreateAPIError(b)
apiErr, ok := AsAPIError(e)
t.Require().NotNil(apiErr)
t.Require().True(ok)
t.Require().ErrorIs(e, ErrMissingParameter)
t.Assert().Equal("item", apiErr.Errors()["Name"])
}
func (t *ErrorTest) Test_CreateGenericAPIError() {
e := CreateGenericAPIError("generic error message")
apiErr, ok := AsAPIError(e)
t.Require().NotNil(apiErr)
t.Require().True(ok)
t.Assert().ErrorIs(apiErr, ErrGeneric)
t.Assert().Equal("generic error message", e.Error())
}
func (t *ErrorTest) TestFailure_HTML() {
e := CreateAPIError([]byte{'<'})
apiErr, ok := AsAPIError(e)
t.Require().NotNil(apiErr)
t.Require().True(ok)
t.Assert().ErrorIs(apiErr, ErrAccountDoesNotExist)
}

View File

@ -1,4 +1,4 @@
package v5 package retailcrm
// CustomersFilter type. // CustomersFilter type.
type CustomersFilter struct { type CustomersFilter struct {

6
go.mod
View File

@ -1,12 +1,10 @@
module github.com/retailcrm/api-client-go module github.com/retailcrm/api-client-go/v2
go 1.13 go 1.13
require ( require (
github.com/google/go-querystring v1.1.0 github.com/google/go-querystring v1.1.0
github.com/joho/godotenv v1.3.0 github.com/joho/godotenv v1.3.0
golang.org/x/net v0.0.0-20191021144547-ec77196f6094 // indirect github.com/stretchr/testify v1.7.0
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
gopkg.in/h2non/gentleman.v1 v1.0.4 // indirect
gopkg.in/h2non/gock.v1 v1.1.2 gopkg.in/h2non/gock.v1 v1.1.2
) )

18
go.sum
View File

@ -1,3 +1,5 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
@ -8,14 +10,16 @@ github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/h2non/gentleman.v1 v1.0.4/go.mod h1:JYuHVdFzS4MKOXe0o+chKJ4hCe6tqKKw9XH9YP6WFrg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/h2non/gock.v1 v1.0.16 h1:F11k+OafeuFENsjei5t2vMTSTs9L62AdyTe4E1cgdG8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/h2non/gock.v1 v1.0.16/go.mod h1:XVuDAssexPLwgxCLMvDTWNU5eqklsydR6I5phZ9oPB8=
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

25
log.go Normal file
View File

@ -0,0 +1,25 @@
package retailcrm
// BasicLogger provides basic functionality for logging.
type BasicLogger interface {
Printf(string, ...interface{})
}
// DebugLogger can be used to easily wrap any logger with Debugf method into the BasicLogger instance.
type DebugLogger interface {
Debugf(string, ...interface{})
}
type debugLoggerAdapter struct {
logger DebugLogger
}
// DebugLoggerAdapter returns BasicLogger that calls underlying DebugLogger.Debugf.
func DebugLoggerAdapter(logger DebugLogger) BasicLogger {
return &debugLoggerAdapter{logger: logger}
}
// Printf data in the log using DebugLogger.Debugf.
func (l *debugLoggerAdapter) Printf(format string, v ...interface{}) {
l.logger.Debugf(format, v...)
}

24
log_test.go Normal file
View File

@ -0,0 +1,24 @@
package retailcrm
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
type wrappedLogger struct {
lastMessage string
}
func (w *wrappedLogger) Debugf(msg string, v ...interface{}) {
w.lastMessage = fmt.Sprintf(msg, v...)
}
func TestDebugLoggerAdapter_Printf(t *testing.T) {
wrapped := &wrappedLogger{}
logger := DebugLoggerAdapter(wrapped)
logger.Printf("Test message #%d", 1)
assert.Equal(t, "Test message #1", wrapped.lastMessage)
}

102
marshaling.go Normal file
View File

@ -0,0 +1,102 @@
package retailcrm
import (
"encoding/json"
"fmt"
"strconv"
)
func (t Tag) MarshalJSON() ([]byte, error) {
return json.Marshal(t.Name)
}
func (a *APIErrorsList) UnmarshalJSON(data []byte) error {
var i interface{}
var m APIErrorsList
if err := json.Unmarshal(data, &i); err != nil {
return err
}
switch e := i.(type) {
case map[string]interface{}:
m = make(APIErrorsList, len(e))
for idx, val := range e {
m[idx] = fmt.Sprint(val)
}
case []interface{}:
m = make(APIErrorsList, len(e))
for idx, val := range e {
m[strconv.Itoa(idx)] = fmt.Sprint(val)
}
}
*a = m
return nil
}
func (l *CustomFieldsList) UnmarshalJSON(data []byte) error {
var i interface{}
var m CustomFieldsList
if err := json.Unmarshal(data, &i); err != nil {
return err
}
switch e := i.(type) {
case map[string]interface{}:
m = make(CustomFieldsList, len(e))
for idx, val := range e {
m[idx] = fmt.Sprint(val)
}
case []interface{}:
m = make(CustomFieldsList, len(e))
for idx, val := range e {
m[strconv.Itoa(idx)] = fmt.Sprint(val)
}
}
*l = m
return nil
}
func (p *OrderPayments) UnmarshalJSON(data []byte) error {
var i interface{}
var m OrderPayments
if err := json.Unmarshal(data, &i); err != nil {
return err
}
switch e := i.(type) {
case map[string]interface{}:
m = make(OrderPayments, len(e))
for idx, val := range e {
var res OrderPayment
err := unmarshalMap(val.(map[string]interface{}), &res)
if err != nil {
return err
}
m[idx] = res
}
case []interface{}:
m = make(OrderPayments, len(e))
for idx, val := range e {
var res OrderPayment
err := unmarshalMap(val.(map[string]interface{}), &res)
if err != nil {
return err
}
m[strconv.Itoa(idx)] = res
}
}
*p = m
return nil
}
func unmarshalMap(m map[string]interface{}, v interface{}) (err error) {
var data []byte
data, err = json.Marshal(m)
if err != nil {
return err
}
return json.Unmarshal(data, v)
}

69
marshaling_test.go Normal file
View File

@ -0,0 +1,69 @@
package retailcrm
import (
"encoding/json"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTag_MarshalJSON(t *testing.T) {
tags := []Tag{
{"first", "#3e89b6", false},
{"second", "#ffa654", false},
}
names := []byte(`["first","second"]`)
str, err := json.Marshal(tags)
if err != nil {
t.Errorf("%v", err.Error())
}
if !reflect.DeepEqual(str, names) {
t.Errorf("Marshaled: %#v\nExpected: %#v\n", str, names)
}
}
func TestAPIErrorsList_UnmarshalJSON(t *testing.T) {
var list APIErrorsList
require.NoError(t, json.Unmarshal([]byte(`["first", "second"]`), &list))
assert.Len(t, list, 2)
assert.Equal(t, list["0"], "first")
assert.Equal(t, list["1"], "second")
require.NoError(t, json.Unmarshal([]byte(`{"a": "first", "b": "second"}`), &list))
assert.Len(t, list, 2)
assert.Equal(t, list["a"], "first")
assert.Equal(t, list["b"], "second")
}
func TestCustomFieldsList_UnmarshalJSON(t *testing.T) {
var list CustomFieldsList
require.NoError(t, json.Unmarshal([]byte(`["first", "second"]`), &list))
assert.Len(t, list, 2)
assert.Equal(t, list["0"], "first")
assert.Equal(t, list["1"], "second")
require.NoError(t, json.Unmarshal([]byte(`{"a": "first", "b": "second"}`), &list))
assert.Len(t, list, 2)
assert.Equal(t, list["a"], "first")
assert.Equal(t, list["b"], "second")
}
func TestOrderPayments_UnmarshalJSON(t *testing.T) {
var list OrderPayments
require.NoError(t, json.Unmarshal([]byte(`[{"id": 1}, {"id": 2}]`), &list))
assert.Len(t, list, 2)
assert.Equal(t, list["0"], OrderPayment{ID: 1})
assert.Equal(t, list["1"], OrderPayment{ID: 2})
require.NoError(t, json.Unmarshal([]byte(`{"a": {"id": 1}, "b": {"id": 2}}`), &list))
assert.Len(t, list, 2)
assert.Equal(t, list["a"], OrderPayment{ID: 1})
assert.Equal(t, list["b"], OrderPayment{ID: 2})
}

View File

@ -1,4 +1,4 @@
package v5 package retailcrm
// CustomerRequest type. // CustomerRequest type.
type CustomerRequest struct { type CustomerRequest struct {
@ -177,8 +177,8 @@ type ProductsPropertiesRequest struct {
type DeliveryTrackingRequest struct { type DeliveryTrackingRequest struct {
DeliveryID string `json:"deliveryId,omitempty"` DeliveryID string `json:"deliveryId,omitempty"`
TrackNumber string `json:"trackNumber,omitempty"` TrackNumber string `json:"trackNumber,omitempty"`
History []DeliveryHistoryRecord `json:"history,omitempty,brackets"` History []DeliveryHistoryRecord `json:"history,omitempty"`
ExtraData map[string]string `json:"extraData,omitempty,brackets"` ExtraData map[string]string `json:"extraData,omitempty"`
} }
// DeliveryShipmentsRequest type. // DeliveryShipmentsRequest type.

View File

@ -1,4 +1,4 @@
package v5 package retailcrm
// SuccessfulResponse type. // SuccessfulResponse type.
type SuccessfulResponse struct { type SuccessfulResponse struct {
@ -20,7 +20,7 @@ type OrderCreateResponse struct {
// OperationResponse type. // OperationResponse type.
type OperationResponse struct { type OperationResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
Errors map[string]string `json:"errors,omitempty"` Errors map[string]string `json:"ErrorsList,omitempty"`
} }
// VersionResponse return available API versions. // VersionResponse return available API versions.
@ -31,7 +31,7 @@ type VersionResponse struct {
// CredentialResponse return available API methods. // CredentialResponse return available API methods.
type CredentialResponse struct { type CredentialResponse struct {
Success bool `json:"success,omitempty"` Success bool `json:"success,omitempty"`
// deprecated // deprecated
Credentials []string `json:"credentials,omitempty"` Credentials []string `json:"credentials,omitempty"`
Scopes []string `json:"scopes,omitempty"` Scopes []string `json:"scopes,omitempty"`
@ -409,7 +409,7 @@ type ResponseInfo struct {
// MgInfo type. // MgInfo type.
type MgInfo struct { type MgInfo struct {
EndpointUrl string `json:"endpointUrl"` EndpointURL string `json:"endpointUrl"`
Token string `json:"token"` Token string `json:"token"`
} }

View File

@ -1,4 +1,4 @@
package v5 package retailcrm
import ( import (
"encoding/json" "encoding/json"
@ -19,6 +19,7 @@ type Client struct {
Key string Key string
Debug bool Debug bool
httpClient *http.Client httpClient *http.Client
logger BasicLogger
} }
// Pagination type. // Pagination type.
@ -121,41 +122,41 @@ Customer related types
// Customer type. // Customer type.
type Customer struct { type Customer struct {
ID int `json:"id,omitempty"` ID int `json:"id,omitempty"`
ExternalID string `json:"externalId,omitempty"` ExternalID string `json:"externalId,omitempty"`
FirstName string `json:"firstName,omitempty"` FirstName string `json:"firstName,omitempty"`
LastName string `json:"lastName,omitempty"` LastName string `json:"lastName,omitempty"`
Patronymic string `json:"patronymic,omitempty"` Patronymic string `json:"patronymic,omitempty"`
Sex string `json:"sex,omitempty"` Sex string `json:"sex,omitempty"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
Phones []Phone `json:"phones,omitempty"` Phones []Phone `json:"phones,omitempty"`
Address *Address `json:"address,omitempty"` Address *Address `json:"address,omitempty"`
CreatedAt string `json:"createdAt,omitempty"` CreatedAt string `json:"createdAt,omitempty"`
Birthday string `json:"birthday,omitempty"` Birthday string `json:"birthday,omitempty"`
ManagerID int `json:"managerId,omitempty"` ManagerID int `json:"managerId,omitempty"`
Vip bool `json:"vip,omitempty"` Vip bool `json:"vip,omitempty"`
Bad bool `json:"bad,omitempty"` Bad bool `json:"bad,omitempty"`
Site string `json:"site,omitempty"` Site string `json:"site,omitempty"`
Source *Source `json:"source,omitempty"` Source *Source `json:"source,omitempty"`
Contragent *Contragent `json:"contragent,omitempty"` Contragent *Contragent `json:"contragent,omitempty"`
PersonalDiscount float32 `json:"personalDiscount,omitempty"` PersonalDiscount float32 `json:"personalDiscount,omitempty"`
CumulativeDiscount float32 `json:"cumulativeDiscount,omitempty"` CumulativeDiscount float32 `json:"cumulativeDiscount,omitempty"`
DiscountCardNumber string `json:"discountCardNumber,omitempty"` DiscountCardNumber string `json:"discountCardNumber,omitempty"`
EmailMarketingUnsubscribedAt string `json:"emailMarketingUnsubscribedAt,omitempty"` EmailMarketingUnsubscribedAt string `json:"emailMarketingUnsubscribedAt,omitempty"`
AvgMarginSumm float32 `json:"avgMarginSumm,omitempty"` AvgMarginSumm float32 `json:"avgMarginSumm,omitempty"`
MarginSumm float32 `json:"marginSumm,omitempty"` MarginSumm float32 `json:"marginSumm,omitempty"`
TotalSumm float32 `json:"totalSumm,omitempty"` TotalSumm float32 `json:"totalSumm,omitempty"`
AverageSumm float32 `json:"averageSumm,omitempty"` AverageSumm float32 `json:"averageSumm,omitempty"`
OrdersCount int `json:"ordersCount,omitempty"` OrdersCount int `json:"ordersCount,omitempty"`
CostSumm float32 `json:"costSumm,omitempty"` CostSumm float32 `json:"costSumm,omitempty"`
MaturationTime int `json:"maturationTime,omitempty"` MaturationTime int `json:"maturationTime,omitempty"`
FirstClientID string `json:"firstClientId,omitempty"` FirstClientID string `json:"firstClientId,omitempty"`
LastClientID string `json:"lastClientId,omitempty"` LastClientID string `json:"lastClientId,omitempty"`
BrowserID string `json:"browserId,omitempty"` BrowserID string `json:"browserId,omitempty"`
MgCustomerID string `json:"mgCustomerId,omitempty"` MgCustomerID string `json:"mgCustomerId,omitempty"`
PhotoURL string `json:"photoUrl,omitempty"` PhotoURL string `json:"photoUrl,omitempty"`
CustomFields map[string]string `json:"customFields,omitempty"` CustomFields CustomFieldsList `json:"customFields,omitempty"`
Tags []Tag `json:"tags,omitempty"` Tags []Tag `json:"tags,omitempty"`
} }
// CorporateCustomer type. // CorporateCustomer type.
@ -166,7 +167,7 @@ type CorporateCustomer struct {
CreatedAt string `json:"createdAt,omitempty"` CreatedAt string `json:"createdAt,omitempty"`
Vip bool `json:"vip,omitempty"` Vip bool `json:"vip,omitempty"`
Bad bool `json:"bad,omitempty"` Bad bool `json:"bad,omitempty"`
CustomFields map[string]string `json:"customFields,omitempty"` CustomFields CustomFieldsList `json:"customFields,omitempty"`
PersonalDiscount float32 `json:"personalDiscount,omitempty"` PersonalDiscount float32 `json:"personalDiscount,omitempty"`
DiscountCardNumber string `json:"discountCardNumber,omitempty"` DiscountCardNumber string `json:"discountCardNumber,omitempty"`
ManagerID int `json:"managerId,omitempty"` ManagerID int `json:"managerId,omitempty"`
@ -217,17 +218,17 @@ type CorporateCustomerContactCustomer struct {
} }
type Company struct { type Company struct {
ID int `json:"id,omitempty"` ID int `json:"id,omitempty"`
IsMain bool `json:"isMain,omitempty"` IsMain bool `json:"isMain,omitempty"`
ExternalID string `json:"externalId,omitempty"` ExternalID string `json:"externalId,omitempty"`
Active bool `json:"active,omitempty"` Active bool `json:"active,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Brand string `json:"brand,omitempty"` Brand string `json:"brand,omitempty"`
Site string `json:"site,omitempty"` Site string `json:"site,omitempty"`
CreatedAt string `json:"createdAt,omitempty"` CreatedAt string `json:"createdAt,omitempty"`
Contragent *Contragent `json:"contragent,omitempty"` Contragent *Contragent `json:"contragent,omitempty"`
Address *IdentifiersPair `json:"address,omitempty"` Address *IdentifiersPair `json:"address,omitempty"`
CustomFields map[string]string `json:"customFields,omitempty"` CustomFields CustomFieldsList `json:"customFields,omitempty"`
} }
// CorporateCustomerNote type. // CorporateCustomerNote type.
@ -272,57 +273,60 @@ type CorporateCustomerHistoryRecord struct {
Order related types Order related types
*/ */
type OrderPayments map[string]OrderPayment
type CustomFieldsList map[string]string
// Order type. // Order type.
type Order struct { type Order struct {
ID int `json:"id,omitempty"` ID int `json:"id,omitempty"`
ExternalID string `json:"externalId,omitempty"` ExternalID string `json:"externalId,omitempty"`
Number string `json:"number,omitempty"` Number string `json:"number,omitempty"`
FirstName string `json:"firstName,omitempty"` FirstName string `json:"firstName,omitempty"`
LastName string `json:"lastName,omitempty"` LastName string `json:"lastName,omitempty"`
Patronymic string `json:"patronymic,omitempty"` Patronymic string `json:"patronymic,omitempty"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
Phone string `json:"phone,omitempty"` Phone string `json:"phone,omitempty"`
AdditionalPhone string `json:"additionalPhone,omitempty"` AdditionalPhone string `json:"additionalPhone,omitempty"`
CreatedAt string `json:"createdAt,omitempty"` CreatedAt string `json:"createdAt,omitempty"`
StatusUpdatedAt string `json:"statusUpdatedAt,omitempty"` StatusUpdatedAt string `json:"statusUpdatedAt,omitempty"`
ManagerID int `json:"managerId,omitempty"` ManagerID int `json:"managerId,omitempty"`
Mark int `json:"mark,omitempty"` Mark int `json:"mark,omitempty"`
Call bool `json:"call,omitempty"` Call bool `json:"call,omitempty"`
Expired bool `json:"expired,omitempty"` Expired bool `json:"expired,omitempty"`
FromAPI bool `json:"fromApi,omitempty"` FromAPI bool `json:"fromApi,omitempty"`
MarkDatetime string `json:"markDatetime,omitempty"` MarkDatetime string `json:"markDatetime,omitempty"`
CustomerComment string `json:"customerComment,omitempty"` CustomerComment string `json:"customerComment,omitempty"`
ManagerComment string `json:"managerComment,omitempty"` ManagerComment string `json:"managerComment,omitempty"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
StatusComment string `json:"statusComment,omitempty"` StatusComment string `json:"statusComment,omitempty"`
FullPaidAt string `json:"fullPaidAt,omitempty"` FullPaidAt string `json:"fullPaidAt,omitempty"`
Site string `json:"site,omitempty"` Site string `json:"site,omitempty"`
OrderType string `json:"orderType,omitempty"` OrderType string `json:"orderType,omitempty"`
OrderMethod string `json:"orderMethod,omitempty"` OrderMethod string `json:"orderMethod,omitempty"`
CountryIso string `json:"countryIso,omitempty"` CountryIso string `json:"countryIso,omitempty"`
Summ float32 `json:"summ,omitempty"` Summ float32 `json:"summ,omitempty"`
TotalSumm float32 `json:"totalSumm,omitempty"` TotalSumm float32 `json:"totalSumm,omitempty"`
PrepaySum float32 `json:"prepaySum,omitempty"` PrepaySum float32 `json:"prepaySum,omitempty"`
PurchaseSumm float32 `json:"purchaseSumm,omitempty"` PurchaseSumm float32 `json:"purchaseSumm,omitempty"`
DiscountManualAmount float32 `json:"discountManualAmount,omitempty"` DiscountManualAmount float32 `json:"discountManualAmount,omitempty"`
DiscountManualPercent float32 `json:"discountManualPercent,omitempty"` DiscountManualPercent float32 `json:"discountManualPercent,omitempty"`
Weight float32 `json:"weight,omitempty"` Weight float32 `json:"weight,omitempty"`
Length int `json:"length,omitempty"` Length int `json:"length,omitempty"`
Width int `json:"width,omitempty"` Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"` Height int `json:"height,omitempty"`
ShipmentStore string `json:"shipmentStore,omitempty"` ShipmentStore string `json:"shipmentStore,omitempty"`
ShipmentDate string `json:"shipmentDate,omitempty"` ShipmentDate string `json:"shipmentDate,omitempty"`
ClientID string `json:"clientId,omitempty"` ClientID string `json:"clientId,omitempty"`
Shipped bool `json:"shipped,omitempty"` Shipped bool `json:"shipped,omitempty"`
UploadedToExternalStoreSystem bool `json:"uploadedToExternalStoreSystem,omitempty"` UploadedToExternalStoreSystem bool `json:"uploadedToExternalStoreSystem,omitempty"`
Source *Source `json:"source,omitempty"` Source *Source `json:"source,omitempty"`
Contragent *Contragent `json:"contragent,omitempty"` Contragent *Contragent `json:"contragent,omitempty"`
Customer *Customer `json:"customer,omitempty"` Customer *Customer `json:"customer,omitempty"`
Delivery *OrderDelivery `json:"delivery,omitempty"` Delivery *OrderDelivery `json:"delivery,omitempty"`
Marketplace *OrderMarketplace `json:"marketplace,omitempty"` Marketplace *OrderMarketplace `json:"marketplace,omitempty"`
Items []OrderItem `json:"items,omitempty"` Items []OrderItem `json:"items,omitempty"`
CustomFields map[string]string `json:"customFields,omitempty"` CustomFields CustomFieldsList `json:"customFields,omitempty"`
Payments map[string]OrderPayment `json:"payments,omitempty"` Payments OrderPayments `json:"payments,omitempty"`
} }
// OrdersStatus type. // OrdersStatus type.
@ -378,8 +382,14 @@ type OrderDeliveryData struct {
// UnmarshalJSON method. // UnmarshalJSON method.
func (v *OrderDeliveryData) UnmarshalJSON(b []byte) error { func (v *OrderDeliveryData) UnmarshalJSON(b []byte) error {
var additionalData map[string]interface{} var additionalData map[string]interface{}
json.Unmarshal(b, &additionalData) err := json.Unmarshal(b, &additionalData)
json.Unmarshal(b, &v.OrderDeliveryDataBasic) if err != nil {
return err
}
err = json.Unmarshal(b, &v.OrderDeliveryDataBasic)
if err != nil {
return err
}
object := reflect.TypeOf(v.OrderDeliveryDataBasic) object := reflect.TypeOf(v.OrderDeliveryDataBasic)
for i := 0; i < object.NumField(); i++ { for i := 0; i < object.NumField(); i++ {
@ -401,7 +411,10 @@ func (v *OrderDeliveryData) UnmarshalJSON(b []byte) error {
func (v OrderDeliveryData) MarshalJSON() ([]byte, error) { func (v OrderDeliveryData) MarshalJSON() ([]byte, error) {
result := map[string]interface{}{} result := map[string]interface{}{}
data, _ := json.Marshal(v.OrderDeliveryDataBasic) data, _ := json.Marshal(v.OrderDeliveryDataBasic)
json.Unmarshal(data, &result) err := json.Unmarshal(data, &result)
if err != nil {
return nil, err
}
for key, value := range v.AdditionalFields { for key, value := range v.AdditionalFields {
result[key] = value result[key] = value
@ -587,7 +600,7 @@ type User struct {
Phone string `json:"phone,omitempty"` Phone string `json:"phone,omitempty"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
Groups []UserGroup `json:"groups,omitempty"` Groups []UserGroup `json:"groups,omitempty"`
MgUserId uint64 `json:"mgUserId,omitempty"` MGUserID uint64 `json:"mgUserId,omitempty"`
} }
// UserGroup type. // UserGroup type.
@ -1054,7 +1067,7 @@ type Action struct {
// MgTransport type. // MgTransport type.
type MgTransport struct { type MgTransport struct {
WebhookUrl string `json:"webhookUrl,omitempty"` WebhookURL string `json:"webhookUrl,omitempty"`
} }
// MgBot type. // MgBot type.
@ -1072,7 +1085,7 @@ type CostRecord struct {
DateTo string `json:"dateTo,omitempty"` DateTo string `json:"dateTo,omitempty"`
Summ float32 `json:"summ,omitempty"` Summ float32 `json:"summ,omitempty"`
CostItem string `json:"costItem,omitempty"` CostItem string `json:"costItem,omitempty"`
UserId int `json:"userId,omitempty"` UserID int `json:"userId,omitempty"`
Order *Order `json:"order,omitempty"` Order *Order `json:"order,omitempty"`
Sites []string `json:"sites,omitempty"` Sites []string `json:"sites,omitempty"`
} }
@ -1089,7 +1102,7 @@ type Cost struct {
CreatedAt string `json:"createdAt,omitempty"` CreatedAt string `json:"createdAt,omitempty"`
CreatedBy string `json:"createdBy,omitempty"` CreatedBy string `json:"createdBy,omitempty"`
Order *Order `json:"order,omitempty"` Order *Order `json:"order,omitempty"`
UserId int `json:"userId,omitempty"` UserID int `json:"userId,omitempty"`
Sites []string `json:"sites,omitempty"` Sites []string `json:"sites,omitempty"`
} }

View File

@ -1,4 +1,4 @@
package v5 package retailcrm
import ( import (
"encoding/json" "encoding/json"

View File

@ -1,32 +0,0 @@
package v5
import "encoding/json"
// APIErrorsList struct.
type APIErrorsList map[string]string
// APIError struct.
type APIError struct {
SuccessfulResponse
ErrorMsg string `json:"errorMsg,omitempty"`
Errors APIErrorsList `json:"errors,omitempty"`
}
func (e *APIError) Error() string {
return e.ErrorMsg
}
func NewAPIError(dataResponse []byte) error {
a := &APIError{}
if len(dataResponse) > 0 && dataResponse[0] == '<' {
a.ErrorMsg = "Account does not exist."
return a
}
if err := json.Unmarshal(dataResponse, a); err != nil {
return err
}
return a
}

View File

@ -1,54 +0,0 @@
package v5
import (
"reflect"
"testing"
"golang.org/x/xerrors"
)
func TestFailure_ApiErrorsSlice(t *testing.T) {
b := []byte(`{"success": false,
"errorMsg": "Failed to activate module",
"errors": [
"Your account has insufficient funds to activate integration module",
"Test error"
]}`)
expected := APIErrorsList{
"0": "Your account has insufficient funds to activate integration module",
"1": "Test error",
}
var expEr *APIError
e := NewAPIError(b)
if xerrors.As(e, &expEr) {
if eq := reflect.DeepEqual(expEr.Errors, expected); eq != true {
t.Errorf("%+v", eq)
}
} else {
t.Errorf("Error must be type of APIError: %v", e)
}
}
func TestFailure_ApiErrorsMap(t *testing.T) {
b := []byte(`{"success": false,
"errorMsg": "Failed to activate module",
"errors": {"id": "ID must be an integer", "test": "Test error"}}`,
)
expected := APIErrorsList{
"id": "ID must be an integer",
"test": "Test error",
}
var expEr *APIError
e := NewAPIError(b)
if xerrors.As(e, &expEr) {
if eq := reflect.DeepEqual(expEr.Errors, expected); eq != true {
t.Errorf("%+v", eq)
}
} else {
t.Errorf("Error must be type of APIError: %v", e)
}
}

View File

@ -1,35 +0,0 @@
package v5
import (
"encoding/json"
"fmt"
"strconv"
)
func (t Tag) MarshalJSON() ([]byte, error) {
return json.Marshal(t.Name)
}
func (a *APIErrorsList) UnmarshalJSON(data []byte) error {
var i interface{}
var m map[string]string
if err := json.Unmarshal(data, &i); err != nil {
return err
}
switch e := i.(type) {
case map[string]interface{}:
m = make(map[string]string, len(e))
for idx, val := range e {
m[idx] = fmt.Sprint(val)
}
case []interface{}:
m = make(map[string]string, len(e))
for idx, val := range e {
m[strconv.Itoa(idx)] = fmt.Sprint(val)
}
}
*a = m
return nil
}

View File

@ -1,24 +0,0 @@
package v5
import (
"encoding/json"
"reflect"
"testing"
)
func TestTag_MarshalJSON(t *testing.T) {
tags := []Tag{
{"first", "#3e89b6", false},
{"second", "#ffa654", false},
}
names := []byte(`["first","second"]`)
str, err := json.Marshal(tags)
if err != nil {
t.Errorf("%v", err.Error())
}
if !reflect.DeepEqual(str, names) {
t.Errorf("Marshaled: %#v\nExpected: %#v\n", str, names)
}
}