Compare commits

..

60 commits

Author SHA1 Message Date
c62a02aa1f
Merge pull request #100 from dendd1/master
fix for clear cart method
2025-04-04 15:14:19 +03:00
Суханов Данила
871459c8b7 fix for clear cart method 2025-04-04 15:12:04 +03:00
d0b0dd59d6
Merge pull request #99 from dendd1/master
support /customer-interaction/site/cart
2025-04-01 14:51:34 +03:00
Суханов Данила
cc83657f32 support /customer-interaction/site/cart 2025-04-01 14:49:13 +03:00
53e2ab5130
Merge pull request #98 from Neur0toxine/fix/costs_filter_field_types
Fix costs filter field types
2025-03-18 17:20:29 +03:00
0123057e86 fix lint issues 2025-03-18 17:18:06 +03:00
a8cae8b200 fix type in the tests 2025-03-18 17:11:38 +03:00
5fa64ff23e
Merge pull request #97 from Neur0toxine/rate-limit
rate limiter
2025-03-18 17:02:23 +03:00
e2113a640e fix linter 2025-03-18 17:00:42 +03:00
2f33b56cd3 rate limiter 2025-03-18 16:53:45 +03:00
Pavel Tsayukov
8892fe6895 Fix types of fields in CostsFilter 2025-02-18 14:55:13 +03:00
Pavel Tsayukov
908f16b173 Fix wrong name of query key for OrderExternalIds field 2025-02-18 14:55:12 +03:00
2859073353
Merge pull request #95 from Neur0toxine/fix-regionId-cityId
fix regionId & cityId unmarshaling error
2025-01-31 11:50:00 +03:00
a482cd1a2f update linter config 2025-01-31 11:47:59 +03:00
92a5741c84 fix ci 2025-01-31 11:41:22 +03:00
634ec386b1 fix regionId & cityId unmarshaling error 2025-01-31 11:38:47 +03:00
d101ddb097
Merge pull request #94 from RenCurs/transport-action
add mg transport action
2024-12-04 10:32:34 +03:00
Ruslan Efanov
3186470ed9 add mg transport action 2024-12-04 10:24:19 +03:00
904796f97a
Support for urlLike filter in GET /api/v5/store/products 2024-09-25 11:29:48 +03:00
Vlasov
2f0f55be42 Added support for urlLike filter in GET /api/v5/store/products 2024-09-24 18:38:29 +03:00
c79e6c0497
Fix loyalty account request fields 2024-09-11 15:01:53 +03:00
2587dd786a
add offers method 2024-09-11 14:56:04 +03:00
Ruslan Efanov
2333dbf493 add offers method 2024-09-11 14:54:33 +03:00
Aleksandr Kokockin
d195460141 rename expireDate field 2024-09-10 12:47:44 +03:00
Aleksandr Kokockin
e6fc8f1e0e rename page field 2024-09-10 12:47:24 +03:00
c9e5b1f79d
add mgTransport callbacks 2024-09-04 13:34:37 +03:00
3282ab045e
Merge branch 'master' into add-mgTransport-callbacks 2024-09-04 13:25:44 +03:00
4398f85214
add field refreshToken for mg integrations 2024-09-02 11:23:29 +03:00
Ruslan Efanov
77b49e04ab add field refreshToken for mg integrations 2024-09-02 11:13:24 +03:00
29062b8bf0 add title field to ChatVisitedPage 2024-03-20 15:01:10 +03:00
7c8e142cab country & city fields instead of ip field 2024-03-19 12:01:02 +03:00
e28631dcb2 fieldalignment fix 2024-03-19 12:01:02 +03:00
a4de6df146 fix field names 2024-03-19 12:01:02 +03:00
81a09e24d4 callbacks structs 2024-03-19 12:01:02 +03:00
b5e7c3ff33 add mgTransport callbacks 2024-03-19 12:01:02 +03:00
76135226fb
add services support 2023-12-28 12:55:00 +03:00
a72a57fbe1 add services support 2023-12-28 12:52:45 +03:00
c2a33378b8
add new API methods & new parameters 2023-09-06 15:44:19 +03:00
Danila
583362bfe3 Adding functionality from recent updates to the library 2023-09-06 07:40:46 +03:00
Danila
ab648cd06a Adding functionality from recent updates to the library 2023-09-05 18:09:36 +03:00
Danila
5c6d2ebead Adding functionality from recent updates to the library 2023-09-05 15:06:08 +03:00
407ecf5066
add mg settings to settings response 2023-08-11 12:23:51 +03:00
Kirill Sukhorukov
2875b8620a add mg settings to settings response 2023-08-10 12:25:39 +03:00
b445dfdfe5
add new order field DialogID and fix custom fields marshaling 2023-08-01 08:01:12 +03:00
1e9692ec15
Update marshaling.go 2023-07-31 16:52:08 +03:00
Kirill Sukhorukov
0ed90e8351 add new order field DialogID and fix custom fields marshaling 2023-07-27 14:06:03 +03:00
076ce77bdb
fix for template api methods 2023-05-29 15:08:31 +03:00
Ruslan Efanov
8377a8789d fix model fields 2023-05-26 17:19:44 +03:00
280f078632
add methods for mg templates 2023-05-24 11:20:05 +03:00
Ruslan Efanov
e6efd56497 fix test 2023-05-23 15:17:38 +03:00
Ruslan Efanov
9bd3d646fc correct templates methods 2023-05-23 13:09:05 +03:00
Ефанов Руслан
16e2bc304c add methods for mg templates 2023-05-17 17:51:33 +03:00
d08ed4e1b2
Add multi dictionary custom field support 2023-05-03 21:51:13 +03:00
Vlasov
afb7c1b881 Add multi dictionary custom field support 2023-05-03 18:04:53 +03:00
e513134df9
update linter and it's rules 2023-01-24 17:42:34 +03:00
5b7ed8697e
update types with new fields 2023-01-24 17:42:13 +03:00
107a4d150b update types with new fields 2023-01-24 11:13:20 +03:00
a51bab6df4
Add Settings field NonWorkingDays 2023-01-17 17:52:31 +03:00
Ruslan Efanov
49905ab9c6 update linter and it's rules 2022-12-30 11:19:20 +03:00
Vragov Roman
bafdf24755 Add Settings field NonWorkingDays 2022-12-26 16:18:03 +03:00
14 changed files with 2701 additions and 307 deletions

View file

@ -20,10 +20,10 @@ jobs:
steps: steps:
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Go 1.17 - name: Set up Go 1.23
uses: actions/setup-go@v2 uses: actions/setup-go@v3
with: with:
go-version: '1.17' go-version: '1.23'
- name: Get dependencies - name: Get dependencies
run: | run: |
go mod tidy go mod tidy
@ -31,7 +31,7 @@ jobs:
- name: Lint code with golangci-lint - name: Lint code with golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3
with: with:
version: v1.45.2 version: v1.62.2
only-new-issues: true only-new-issues: true
skip-pkg-cache: true skip-pkg-cache: true
args: --build-tags=testutils args: --build-tags=testutils
@ -40,13 +40,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
go-version: ['1.13', '1.14', '1.15', '1.16', '1.17'] go-version: ['1.19', '1.20', '1.21', '1.22', '1.23', 'stable']
include: include:
- go-version: '1.17' - go-version: '1.23'
coverage: 1 coverage: 1
steps: steps:
- name: Set up Go ${{ matrix.go-version }} - name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2 uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
@ -59,17 +59,21 @@ jobs:
env: env:
COVERAGE: ${{ matrix.coverage }} COVERAGE: ${{ matrix.coverage }}
if: env.COVERAGE != 1 if: env.COVERAGE != 1
run: go test -tags=testutils ./... run: |
go install gotest.tools/gotestsum@latest
gotestsum --format testdox ./... -tags=testutils -v -cpu 2 -timeout 60s -race
- name: Tests with coverage - name: Tests with coverage
env: env:
COVERAGE: ${{ matrix.coverage }} COVERAGE: ${{ matrix.coverage }}
if: env.COVERAGE == 1 if: env.COVERAGE == 1
run: | run: |
go test -tags=testutils ./... -race -coverprofile=coverage.txt -covermode=atomic "$d" go install gotest.tools/gotestsum@latest
gotestsum --format testdox ./... -tags=testutils -v -cpu 2 -timeout 60s -race -cover -coverprofile=coverage.txt -covermode=atomic "$d"
- name: Coverage - name: Coverage
env: env:
COVERAGE: ${{ matrix.coverage }} COVERAGE: ${{ matrix.coverage }}
if: env.COVERAGE == 1 if: env.COVERAGE == 1
run: | run: |
go install github.com/axw/gocov/gocov@latest
gocov convert ./coverage.txt | gocov report
bash <(curl -s https://codecov.io/bash) bash <(curl -s https://codecov.io/bash)
rm coverage.txt

View file

@ -1,32 +1,70 @@
run: run:
skip-dirs-use-default: true skip-dirs-use-default: true
allow-parallel-runners: true allow-parallel-runners: true
skip-files:
issues:
exclude-files:
- testutils.go - testutils.go
exclude-rules:
- path: _test\.go
linters:
- lll
- errcheck
- misspell
- ineffassign
- whitespace
- makezero
- errcheck
- funlen
- goconst
- gocognit
- gocyclo
- godot
- unused
- errchkjson
- varnamelen
exclude-use-default: true
exclude-case-sensitive: false
max-issues-per-linter: 0
max-same-issues: 0
fix: true
output: output:
format: colored-line-number formats:
- format: colored-line-number
sort-results: true sort-results: true
# Linters below do not support go1.18 yet because of generics.
# See https://github.com/golangci/golangci-lint/issues/2649
# - bodyclose
# - sqlclosecheck
linters: linters:
disable-all: true disable-all: true
enable: enable:
- deadcode - asciicheck
- asasalint
- varnamelen
- reassign
- nilnil
- nilerr
- nakedret
- goprintffuncname
- typecheck
- errchkjson
- errcheck - errcheck
- gosimple - gosimple
- govet - govet
- ineffassign - ineffassign
- staticcheck - staticcheck
- structcheck
- unused - unused
- unparam - unparam
- varcheck
- bodyclose
- dogsled - dogsled
- dupl - dupl
- errorlint - errorlint
- exhaustive - exhaustive
- exportloopref - exportloopref
- copyloopvar
- funlen - funlen
- gocognit - gocognit
- goconst - goconst
@ -35,16 +73,13 @@ linters:
- godot - godot
- goimports - goimports
- revive - revive
- gomnd
- gosec - gosec
- ifshort
- lll - lll
- makezero - makezero
- misspell - misspell
- nestif - nestif
- prealloc - prealloc
- predeclared - predeclared
- sqlclosecheck
- unconvert - unconvert
- whitespace - whitespace
@ -55,6 +90,7 @@ linters-settings:
enable: enable:
- assign - assign
- atomic - atomic
- atomicalign
- bools - bools
- buildtag - buildtag
- copylocks - copylocks
@ -69,7 +105,6 @@ linters-settings:
- unmarshal - unmarshal
- unreachable - unreachable
- unsafeptr - unsafeptr
- unused
settings: settings:
printf: printf:
funcs: funcs:
@ -137,11 +172,13 @@ linters-settings:
threshold: 200 threshold: 200
errorlint: errorlint:
errorf: true errorf: true
asserts: false
comparison: false
exhaustive: exhaustive:
check-generated: false check-generated: false
default-signifies-exhaustive: false default-signifies-exhaustive: false
funlen: funlen:
lines: 60 lines: 90
statements: 40 statements: 40
gocognit: gocognit:
min-complexity: 25 min-complexity: 25
@ -151,8 +188,6 @@ linters-settings:
local-prefixes: github.com/retailcrm/api-client-go/v2 local-prefixes: github.com/retailcrm/api-client-go/v2
lll: lll:
line-length: 160 line-length: 160
maligned:
suggest-new: true
misspell: misspell:
locale: US locale: US
nestif: nestif:
@ -160,41 +195,19 @@ linters-settings:
whitespace: whitespace:
multi-if: false multi-if: false
multi-func: false multi-func: false
varnamelen:
issues: max-distance: 10
exclude-rules: ignore-map-index-ok: true
- path: _test\.go ignore-type-assert-ok: true
linters: ignore-chan-recv-ok: true
- gomnd ignore-decls:
- lll - t *testing.T
- bodyclose - e error
- errcheck - i int
- sqlclosecheck
- misspell
- ineffassign
- whitespace
- makezero
- maligned
- ifshort
- errcheck
- funlen
- goconst
- gocognit
- gocyclo
- godot
- gocritic
- gosec
- staticcheck
- unused
exclude-use-default: true
exclude-case-sensitive: false
max-issues-per-linter: 0
max-same-issues: 0
fix: true
severity: severity:
default-severity: error default-severity: error
case-sensitive: false case-sensitive: false
service: service:
golangci-lint-version: 1.36.x golangci-lint-version: 1.62.x

743
client.go
View file

@ -17,10 +17,6 @@ import (
"github.com/google/go-querystring/query" "github.com/google/go-querystring/query"
) )
// HTTPStatusUnknown can return for the method `/api/v5/customers/upload`, `/api/v5/customers-corporate/upload`,
// `/api/v5/orders/upload`.
const HTTPStatusUnknown = 460
// New initialize client. // New initialize client.
func New(url string, key string) *Client { func New(url string, key string) *Client {
return &Client{ return &Client{
@ -36,6 +32,125 @@ func (c *Client) WithLogger(logger BasicLogger) *Client {
return c return c
} }
// EnableRateLimiter activates rate limiting with specified retry attempts.
func (c *Client) EnableRateLimiter(maxAttempts uint) *Client {
c.mutex.Lock()
defer c.mutex.Unlock()
c.limiter = &RateLimiter{
maxAttempts: maxAttempts,
lastRequest: time.Now().Add(-time.Second), // Initialize to allow immediate first request.
}
return c
}
// applyRateLimit applies rate limiting before sending a request.
func (c *Client) applyRateLimit(uri string) {
if c.limiter == nil {
return
}
c.limiter.mutex.Lock()
defer c.limiter.mutex.Unlock()
var delay time.Duration
if strings.HasPrefix(uri, "/telephony") {
delay = telephonyDelay
} else {
delay = regularDelay
}
elapsed := time.Since(c.limiter.lastRequest)
if elapsed < delay {
time.Sleep(delay - elapsed)
}
c.limiter.lastRequest = time.Now()
}
func (c *Client) executeWithRetryBytes(
uri string,
executeFunc func() (interface{}, int, error),
) ([]byte, int, error) {
res, status, err := c.executeWithRetry(uri, executeFunc)
if res == nil {
return nil, status, err
}
return res.([]byte), status, err
}
func (c *Client) executeWithRetryReadCloser(
uri string,
executeFunc func() (interface{}, int, error),
) (io.ReadCloser, int, error) {
res, status, err := c.executeWithRetry(uri, executeFunc)
if res == nil {
return nil, status, err
}
return res.(io.ReadCloser), status, err
}
// executeWithRetry executes a request with retry logic for rate limiting.
func (c *Client) executeWithRetry(
uri string,
executeFunc func() (interface{}, int, error),
) (interface{}, int, error) {
if c.limiter == nil {
return executeFunc()
}
var (
res interface{}
statusCode int
err error
lastAttempt bool
attempt uint = 1
maxAttempts = c.limiter.maxAttempts
totalAttempts = "∞"
infinite = maxAttempts == 0
)
var baseDelay time.Duration
if strings.HasPrefix(uri, "/telephony") {
baseDelay = telephonyDelay
} else {
baseDelay = regularDelay
}
if !infinite {
totalAttempts = strconv.FormatUint(uint64(maxAttempts), 10)
}
for infinite || attempt <= maxAttempts {
c.applyRateLimit(uri)
res, statusCode, err = executeFunc()
lastAttempt = !infinite && attempt == maxAttempts
// If rate limited on final attempt, set error to ErrRateLimited. Return results otherwise.
if statusCode == http.StatusServiceUnavailable && lastAttempt {
return res, statusCode, ErrRateLimited
}
// If not rate limited or on final attempt, return result.
if statusCode != http.StatusServiceUnavailable || lastAttempt {
return res, statusCode, err
}
// Calculate exponential backoff delay: baseDelay * 2^(attempt-1).
backoffDelay := baseDelay * (1 << (attempt - 1))
if c.Debug {
c.writeLog("API Error: rate limited (503), retrying in %v (attempt %d/%s)",
backoffDelay, attempt, totalAttempts)
}
time.Sleep(backoffDelay)
attempt++
}
return res, statusCode, err
}
// writeLog writes to the log. // writeLog writes to the log.
func (c *Client) writeLog(format string, v ...interface{}) { func (c *Client) writeLog(format string, v ...interface{}) {
if c.logger != nil { if c.logger != nil {
@ -48,7 +163,6 @@ func (c *Client) writeLog(format string, v ...interface{}) {
// GetRequest implements GET Request. // GetRequest implements GET Request.
func (c *Client) GetRequest(urlWithParameters string, versioned ...bool) ([]byte, int, error) { func (c *Client) GetRequest(urlWithParameters string, versioned ...bool) ([]byte, int, error) {
var res []byte
var prefix = "/api/v5" var prefix = "/api/v5"
if len(versioned) > 0 { if len(versioned) > 0 {
@ -57,6 +171,11 @@ func (c *Client) GetRequest(urlWithParameters string, versioned ...bool) ([]byte
} }
} }
uri := urlWithParameters
return c.executeWithRetryBytes(uri, func() (interface{}, int, error) {
var res []byte
req, err := http.NewRequest("GET", fmt.Sprintf("%s%s%s", c.URL, prefix, urlWithParameters), nil) req, err := http.NewRequest("GET", fmt.Sprintf("%s%s%s", c.URL, prefix, urlWithParameters), nil)
if err != nil { if err != nil {
return res, 0, err return res, 0, err
@ -73,7 +192,7 @@ func (c *Client) GetRequest(urlWithParameters string, versioned ...bool) ([]byte
return res, 0, err return res, 0, err
} }
if resp.StatusCode >= http.StatusInternalServerError { if resp.StatusCode >= http.StatusInternalServerError && resp.StatusCode != http.StatusServiceUnavailable {
return res, resp.StatusCode, CreateGenericAPIError( return res, resp.StatusCode, CreateGenericAPIError(
fmt.Sprintf("HTTP request error. Status code: %d.", resp.StatusCode)) fmt.Sprintf("HTTP request error. Status code: %d.", resp.StatusCode))
} }
@ -83,7 +202,9 @@ func (c *Client) GetRequest(urlWithParameters string, versioned ...bool) ([]byte
return res, 0, err return res, 0, err
} }
if resp.StatusCode >= http.StatusBadRequest && resp.StatusCode < http.StatusInternalServerError { if resp.StatusCode >= http.StatusBadRequest &&
resp.StatusCode < http.StatusInternalServerError &&
resp.StatusCode != http.StatusServiceUnavailable {
return res, resp.StatusCode, CreateAPIError(res) return res, resp.StatusCode, CreateAPIError(res)
} }
@ -92,6 +213,7 @@ func (c *Client) GetRequest(urlWithParameters string, versioned ...bool) ([]byte
} }
return res, resp.StatusCode, nil return res, resp.StatusCode, nil
})
} }
// PostRequest implements POST Request with generic body data. // PostRequest implements POST Request with generic body data.
@ -100,12 +222,7 @@ func (c *Client) PostRequest(
postData interface{}, postData interface{},
contType ...string, contType ...string,
) ([]byte, int, error) { ) ([]byte, int, error) {
var ( var contentType string
res []byte
contentType string
)
prefix := "/api/v5"
if len(contType) > 0 { if len(contType) > 0 {
contentType = contType[0] contentType = contType[0]
@ -113,6 +230,11 @@ func (c *Client) PostRequest(
contentType = "application/x-www-form-urlencoded" contentType = "application/x-www-form-urlencoded"
} }
prefix := "/api/v5"
return c.executeWithRetryBytes(uri, func() (interface{}, int, error) {
var res []byte
reader, err := getReaderForPostData(postData) reader, err := getReaderForPostData(postData)
if err != nil { if err != nil {
return res, 0, err return res, 0, err
@ -135,7 +257,7 @@ func (c *Client) PostRequest(
return res, 0, err return res, 0, err
} }
if resp.StatusCode >= http.StatusInternalServerError { if resp.StatusCode >= http.StatusInternalServerError && resp.StatusCode != http.StatusServiceUnavailable {
return res, resp.StatusCode, CreateGenericAPIError( return res, resp.StatusCode, CreateGenericAPIError(
fmt.Sprintf("HTTP request error. Status code: %d.", resp.StatusCode)) fmt.Sprintf("HTTP request error. Status code: %d.", resp.StatusCode))
} }
@ -145,7 +267,9 @@ func (c *Client) PostRequest(
return res, 0, err return res, 0, err
} }
if resp.StatusCode >= http.StatusBadRequest && resp.StatusCode < http.StatusInternalServerError { if resp.StatusCode >= http.StatusBadRequest &&
resp.StatusCode < http.StatusInternalServerError &&
resp.StatusCode != http.StatusServiceUnavailable {
return res, resp.StatusCode, CreateAPIError(res) return res, resp.StatusCode, CreateAPIError(res)
} }
@ -154,6 +278,7 @@ func (c *Client) PostRequest(
} }
return res, resp.StatusCode, nil return res, resp.StatusCode, nil
})
} }
func getReaderForPostData(postData interface{}) (io.Reader, error) { func getReaderForPostData(postData interface{}) (io.Reader, error) {
@ -1832,6 +1957,174 @@ func (c *Client) CorporateCustomerEdit(customer CorporateCustomer, by string, si
return resp, status, nil return resp, status, nil
} }
// ClearCart clears the current customer's shopping cart
//
// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-customer-interaction-site-cart-clear
//
// Example:
//
// var client = retailcrm.New("https://demo.url", "09jIJ")
//
// data, status, err := client.ClearCart("site_id", retailcrm.SiteFilter{SiteBy: "id"},
// retailcrm.ClearCartRequest{
// ClearedAt: time.Now().String(),
// Customer: retailcrm.CartCustomer{
// ID: 1,
// ExternalID: "ext_id",
// Site: "site",
// BrowserID: "browser_id",
// GaClientID: "ga_client_id",
// },
// Order: retailcrm.ClearCartOrder{
// ID: 1,
// ExternalID: "ext_id",
// Number: "abc123",
// },
// },
// )
//
// if err != nil {
// 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)
// }
func (c *Client) ClearCart(site string, filter SiteFilter, req ClearCartRequest) (
SuccessfulResponse, int, error,
) {
var resp SuccessfulResponse
updateJSON, err := json.Marshal(&req)
if err != nil {
return SuccessfulResponse{}, 0, err
}
p := url.Values{
"cart": {string(updateJSON)},
}
params, _ := query.Values(filter)
data, status, err := c.PostRequest(fmt.Sprintf("/customer-interaction/%s/cart/clear?%s", site, params.Encode()), p)
if err != nil {
return resp, status, err
}
err = json.Unmarshal(data, &resp)
if err != nil {
return resp, status, err
}
return resp, status, nil
}
// SetCart creates or overwrites shopping cart data
//
// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#post--api-v5-customer-interaction-site-cart-set
//
// Example:
//
// var client = retailcrm.New("https://demo.url", "09jIJ")
//
// data, status, err := client.SetCart("site_id", retailcrm.SiteFilter{SiteBy: "id"},
// retailcrm.SetCartRequest{
// ExternalID: "ext_id",
// DroppedAt: time.Now().String(),
// Link: "link",
// Customer: retailcrm.CartCustomer{
// ID: 1,
// ExternalID: "ext_id",
// Site: "site",
// BrowserID: "browser_id",
// GaClientID: "ga_client_id",
// },
// Items: []retailcrm.SetCartItem{
// {
// Quantity: 1,
// Price: 1.0,
// Offer: retailcrm.SetCartOffer{
// ID: 1,
// ExternalID: "ext_id",
// XMLID: "xml_id",
// },
// },
// },
// },
// )
//
// if err != nil {
// 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)
// }
func (c *Client) SetCart(site string, filter SiteFilter, req SetCartRequest) (
SuccessfulResponse, int, error,
) {
var resp SuccessfulResponse
updateJSON, err := json.Marshal(&req)
if err != nil {
return SuccessfulResponse{}, 0, err
}
p := url.Values{
"cart": {string(updateJSON)},
}
params, _ := query.Values(filter)
data, status, err := c.PostRequest(fmt.Sprintf("/customer-interaction/%s/cart/set?%s", site, params.Encode()), p)
if err != nil {
return resp, status, err
}
err = json.Unmarshal(data, &resp)
if err != nil {
return resp, status, err
}
return resp, status, nil
}
// GetCart returns the current customer's shopping cart
//
// For more information see https://docs.retailcrm.ru/Developers/API/APIVersions/APIv5#get--api-v5-customer-interaction-site-cart-customerId
//
// Example:
//
// var client = retailcrm.New("https://demo.url", "09jIJ")
//
// data, status, err := client.GetCart("site_id","customer_id",
// retailcrm.GetCartFilter{ SiteBy: "code", By: "externalId"})
//
// if err != nil {
// 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)
// }
func (c *Client) GetCart(site, customer string, filter GetCartFilter) (CartResponse, int, error) {
var resp CartResponse
params, _ := query.Values(filter)
data, status, err := c.GetRequest(fmt.Sprintf("/customer-interaction/%s/cart/%s?%s", site, customer, params.Encode()))
if err != nil {
return resp, status, err
}
err = json.Unmarshal(data, &resp)
if err != nil {
return resp, status, err
}
return resp, status, nil
}
// DeliveryTracking updates tracking data // DeliveryTracking updates tracking data
// //
// For more information see http://www.simla.com/docs/Developers/API/APIVersions/APIv5#post--api-v5-delivery-generic-subcode-tracking // For more information see http://www.simla.com/docs/Developers/API/APIVersions/APIv5#post--api-v5-delivery-generic-subcode-tracking
@ -2109,6 +2402,323 @@ func (c *Client) IntegrationModule(code string) (IntegrationModuleResponse, int,
return resp, status, nil return resp, status, nil
} }
// LinksCreate creates a link
//
// For more information see https://www.simla.com/docs/Developers/API/APIVersions/APIv5#post--api-v5-orders-links-create
//
// Example:
//
// var client = retailcrm.New("https://demo.url", "09jIJ")
//
// data, status, err := client.LinksCreate(retailcrm.SerializedOrderLink{
// Comment: "comment for link",
// Orders: []retailcrm.LinkedOrder{{ID: 10}, {ID: 12}},
// })
//
// if err != nil {
// 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 data.Success == true {
// log.Println("Creating a link")
// }
func (c *Client) LinksCreate(link SerializedOrderLink, site ...string) (SuccessfulResponse, int, error) {
var resp SuccessfulResponse
linkJSON, err := json.Marshal(link)
if err != nil {
return resp, http.StatusBadRequest, err
}
p := url.Values{
"link": {string(linkJSON)},
}
fillSite(&p, site)
data, status, err := c.PostRequest("/orders/links/create", p)
if err != nil {
return resp, status, err
}
err = json.Unmarshal(data, &resp)
if err != nil {
return resp, status, err
}
return resp, status, nil
}
// ClientIdsUpload uploading of web analytics clientId
//
// For more information see https://docs.simla.com/Developers/API/APIVersions/APIv5#post--api-v5-web-analytics-client-ids-upload
//
// Example:
//
// var client = retailcrm.New("https://demo.url", "09jIJ")
//
// data, status, err := client.ClientIdsUpload([]retailcrm.ClientID{
// {
// Value: "value",
// Order: LinkedOrder{ID: 10, ExternalID: "externalID", Number: "number"},
// Customer: SerializedEntityCustomer{ID: 10, ExternalID: "externalID"},
// Site: "site",
// },
// {
// Value: "value",
// Order: LinkedOrder{ID: 12, ExternalID: "externalID2", Number: "number2"},
// Customer: SerializedEntityCustomer{ID: 12, ExternalID: "externalID2"},
// Site: "site2",
// },
// })
//
// if err != nil {
// 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 data.Success == true {
// log.Println("Upload is successful")
// }
func (c *Client) ClientIdsUpload(clientIds []ClientID) (ClientIDResponse, int, error) {
var resp ClientIDResponse
clientIdsJSON, err := json.Marshal(&clientIds)
if err != nil {
return resp, http.StatusBadRequest, err
}
p := url.Values{
"clientIds": {string(clientIdsJSON)},
}
data, status, err := c.PostRequest("/web-analytics/client-ids/upload", p)
if err != nil {
return resp, status, err
}
err = json.Unmarshal(data, &resp)
if err != nil {
return resp, status, err
}
return resp, status, nil
}
// SourcesUpload uploading of sources
//
// For more information see https://docs.simla.com/Developers/API/APIVersions/APIv5#post--api-v5-web-analytics-sources-upload
//
// Example:
//
// var client = retailcrm.New("https://demo.url", "09jIJ")
//
// data, status, err := client.SourcesUpload([]retailcrm.Source{
// {
// Source: "source",
// Medium: "medium",
// Campaign: "campaign",
// Keyword: "keyword",
// Content: "content",
// ClientID: "10",
// Order: LinkedOrder{ID: 10, ExternalID: "externalId", Number: "number"},
// Customer: SerializedEntityCustomer{ID: 10, ExternalID: "externalId"},
// Site: "site",
// },
// })
//
// if err != nil {
// 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 data.Success == true {
// log.Println("Upload is successful!")
// }
func (c *Client) SourcesUpload(sources []Source) (SourcesResponse, int, error) {
var resp SourcesResponse
sourcesJSON, err := json.Marshal(&sources)
if err != nil {
return resp, http.StatusBadRequest, err
}
p := url.Values{
"sources": {string(sourcesJSON)},
}
data, status, err := c.PostRequest("/web-analytics/sources/upload", p)
if err != nil {
return resp, status, err
}
err = json.Unmarshal(data, &resp)
if err != nil {
return resp, status, err
}
return resp, status, nil
}
// Currencies returns a list of currencies
//
// For more information see https://docs.simla.com/Developers/API/APIVersions/APIv5#get--api-v5-reference-currencies
//
// Example:
//
// var client = retailcrm.New("https://demo.url", "09jIJ")
//
// data, status, err := client.Currencies()
//
// if err != nil {
// 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)
// }
//
// for _, value := range data.Currencies {
// log.Printf("%v\n", value)
// }
func (c *Client) Currencies() (CurrencyResponse, int, error) {
var resp CurrencyResponse
data, status, err := c.GetRequest("/reference/currencies")
if err != nil {
return resp, status, err
}
err = json.Unmarshal(data, &resp)
if err != nil {
return resp, status, err
}
return resp, status, nil
}
// CurrenciesCreate create currency
//
// For more information see https://docs.simla.com/Developers/API/APIVersions/APIv5#post--api-v5-reference-currencies-create
//
// Example:
//
// var client = retailcrm.New("https://demo.url", "09jIJ")
//
// data, status, err := client.CurrenciesCreate(retailcrm.Currency{
// Code: "RUB",
// IsBase: true,
// IsAutoConvert: true,
// AutoConvertExtraPercent: 1,
// ManualConvertNominal: 1,
// ManualConvertValue: 1,
// })
//
// if err != nil {
// 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 data.Success == true {
// log.Println("Create currency")
// }
func (c *Client) CurrenciesCreate(currency Currency) (CurrencyCreateResponse, int, error) {
var resp CurrencyCreateResponse
currencyJSON, err := json.Marshal(&currency)
if err != nil {
return resp, http.StatusBadRequest, err
}
p := url.Values{
"currency": {string(currencyJSON)},
}
data, status, err := c.PostRequest("/reference/currencies/create", p)
if err != nil {
return resp, status, err
}
err = json.Unmarshal(data, &resp)
if err != nil {
return resp, status, err
}
return resp, status, nil
}
// CurrenciesEdit edit an currency
//
// For more information see https://docs.simla.com/Developers/API/APIVersions/APIv5#post--api-v5-reference-currencies-id-edit
//
// Example:
//
// var client = retailcrm.New("https://demo.url", "09jIJ")
//
// data, status, err := client.CurrenciesEdit(
// retailcrm.Currency{
// ID: 10,
// Code: "RUB",
// IsBase: true,
// IsAutoConvert: true,
// AutoConvertExtraPercent: 1,
// ManualConvertNominal: 1,
// ManualConvertValue: 1,
// },
// )
//
// if err != nil {
// 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 data.Success == true {
// log.Println("Currency was edit")
// }
func (c *Client) CurrenciesEdit(currency Currency) (SuccessfulResponse, int, error) {
var resp SuccessfulResponse
var uid = strconv.Itoa(currency.ID)
currencyJSON, err := json.Marshal(&currency)
if err != nil {
return resp, http.StatusBadRequest, err
}
p := url.Values{
"currency": {string(currencyJSON)},
}
data, status, err := c.PostRequest(fmt.Sprintf("/reference/currencies/%s/edit", uid), p)
if err != nil {
return resp, status, err
}
err = json.Unmarshal(data, &resp)
if err != nil {
return resp, status, err
}
return resp, status, nil
}
// IntegrationModuleEdit integration module create/edit // IntegrationModuleEdit integration module create/edit
// //
// For more information see http://www.simla.com/docs/Developers/API/APIVersions/APIv5#get--api-v5-integration-modules-code // For more information see http://www.simla.com/docs/Developers/API/APIVersions/APIv5#get--api-v5-integration-modules-code
@ -4650,7 +5260,7 @@ func (c *Client) StaticticsUpdate() (SuccessfulResponse, int, error) {
// //
// data, status, err := client.Costs(CostsRequest{ // data, status, err := client.Costs(CostsRequest{
// Filter: CostsFilter{ // Filter: CostsFilter{
// Ids: []string{"1","2","3"}, // IDs: []string{"1","2","3"},
// MinSumm: "1000" // MinSumm: "1000"
// }, // },
// }) // })
@ -6445,14 +7055,13 @@ func (c *Client) EditProductsGroup(by, id, site string, group ProductGroup) (Act
// log.Printf("%s", fileData) // log.Printf("%s", fileData)
// } // }
func (c *Client) GetOrderPlate(by, orderID, site string, plateID int) (io.ReadCloser, int, error) { func (c *Client) GetOrderPlate(by, orderID, site string, plateID int) (io.ReadCloser, int, error) {
p := url.Values{ requestURL := fmt.Sprintf("%s/api/v5/orders/%s/plates/%d/print?%s", c.URL, orderID, plateID, url.Values{
"by": {checkBy(by)}, "by": {checkBy(by)},
"site": {site}, "site": {site},
} }.Encode())
requestURL := fmt.Sprintf("%s/api/v5/orders/%s/plates/%d/print?%s", c.URL, orderID, plateID, p.Encode()) return c.executeWithRetryReadCloser(requestURL, func() (interface{}, int, error) {
req, err := http.NewRequest("GET", requestURL, nil) req, err := http.NewRequest("GET", requestURL, nil)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@ -6469,12 +7078,14 @@ func (c *Client) GetOrderPlate(by, orderID, site string, plateID int) (io.ReadCl
return nil, 0, err return nil, 0, err
} }
if resp.StatusCode >= http.StatusInternalServerError { if resp.StatusCode >= http.StatusInternalServerError && resp.StatusCode != http.StatusServiceUnavailable {
return nil, resp.StatusCode, CreateGenericAPIError( return nil, resp.StatusCode, CreateGenericAPIError(
fmt.Sprintf("HTTP request error. Status code: %d.", resp.StatusCode)) fmt.Sprintf("HTTP request error. Status code: %d.", resp.StatusCode))
} }
if resp.StatusCode >= http.StatusBadRequest && resp.StatusCode < http.StatusInternalServerError { if resp.StatusCode >= http.StatusBadRequest &&
resp.StatusCode < http.StatusInternalServerError &&
resp.StatusCode != http.StatusServiceUnavailable {
res, err := buildRawResponse(resp) res, err := buildRawResponse(resp)
if err != nil { if err != nil {
@ -6484,14 +7095,12 @@ func (c *Client) GetOrderPlate(by, orderID, site string, plateID int) (io.ReadCl
return nil, resp.StatusCode, CreateAPIError(res) return nil, resp.StatusCode, CreateAPIError(res)
} }
reader := resp.Body
err = reader.Close()
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
return reader, resp.StatusCode, nil return resp.Body, resp.StatusCode, nil
})
} }
// NotificationsSend send a notification // NotificationsSend send a notification
@ -6531,3 +7140,85 @@ func (c *Client) NotificationsSend(req NotificationsSendRequest) (int, error) {
return status, nil return status, nil
} }
func (c *Client) ListMGChannelTemplates(channelID, page, limit int) (MGChannelTemplatesResponse, int, error) {
var resp MGChannelTemplatesResponse
values := url.Values{
"page": {fmt.Sprintf("%d", page)},
"limit": {fmt.Sprintf("%d", limit)},
"channel_id": {fmt.Sprintf("%d", channelID)},
}
data, code, err := c.GetRequest(fmt.Sprintf("/reference/mg-channels/templates?%s", values.Encode()))
if err != nil {
return resp, code, err
}
err = json.Unmarshal(data, &resp)
if err != nil {
return resp, code, err
}
return resp, code, nil
}
func (c *Client) EditMGChannelTemplate(req EditMGChannelTemplateRequest) (int, error) {
templates, err := json.Marshal(req.Templates)
if err != nil {
return 0, err
}
if string(templates) == "null" {
templates = []byte(`[]`)
}
removed, err := json.Marshal(req.Removed)
if err != nil {
return 0, err
}
if string(removed) == "null" {
removed = []byte(`[]`)
}
values := url.Values{
"templates": {string(templates)},
"removed": {string(removed)},
}
_, code, err := c.PostRequest("/reference/mg-channels/templates/edit", values)
if err != nil {
return code, err
}
return code, nil
}
func (c *Client) StoreOffers(req OffersRequest) (StoreOffersResponse, int, error) {
var result StoreOffersResponse
filter, err := query.Values(req)
if err != nil {
return StoreOffersResponse{}, 0, err
}
resp, status, err := c.GetRequest(fmt.Sprintf("/store/offers?%s", filter.Encode()))
if err != nil {
return StoreOffersResponse{}, status, err
}
err = json.Unmarshal(resp, &result)
if err != nil {
return StoreOffersResponse{}, status, err
}
return result, status, nil
}

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,8 @@ import (
var missingParameterMatcher = regexp.MustCompile(`^Parameter \'([\w\]\[\_\-]+)\' is missing$`) var missingParameterMatcher = regexp.MustCompile(`^Parameter \'([\w\]\[\_\-]+)\' is missing$`)
var ( var (
// ErrRateLimited will be returned if request was rate limited.
ErrRateLimited = NewAPIError("rate limit exceeded")
// ErrMissingCredentials will be returned if no API key was provided to the API. // ErrMissingCredentials will be returned if no API key was provided to the API.
ErrMissingCredentials = NewAPIError(`apiKey is missing`) ErrMissingCredentials = NewAPIError(`apiKey is missing`)
// ErrInvalidCredentials will be returned if provided API key is invalid. // ErrInvalidCredentials will be returned if provided API key is invalid.

View file

@ -199,6 +199,7 @@ type OrdersFilter struct {
PaymentStatuses []string `url:"paymentStatuses,omitempty,brackets"` PaymentStatuses []string `url:"paymentStatuses,omitempty,brackets"`
PaymentTypes []string `url:"paymentTypes,omitempty,brackets"` PaymentTypes []string `url:"paymentTypes,omitempty,brackets"`
DeliveryTypes []string `url:"deliveryTypes,omitempty,brackets"` DeliveryTypes []string `url:"deliveryTypes,omitempty,brackets"`
DeliveryServices []string `url:"deliveryServices,omitempty,brackets"`
OrderMethods []string `url:"orderMethods,omitempty,brackets"` OrderMethods []string `url:"orderMethods,omitempty,brackets"`
ShipmentStores []string `url:"shipmentStores,omitempty,brackets"` ShipmentStores []string `url:"shipmentStores,omitempty,brackets"`
Couriers []string `url:"couriers,omitempty,brackets"` Couriers []string `url:"couriers,omitempty,brackets"`
@ -351,6 +352,7 @@ type ProductsFilter struct {
ExternalID string `url:"externalId,omitempty"` ExternalID string `url:"externalId,omitempty"`
Manufacturer string `url:"manufacturer,omitempty"` Manufacturer string `url:"manufacturer,omitempty"`
URL string `url:"url,omitempty"` URL string `url:"url,omitempty"`
URLLike string `url:"urlLike,omitempty"`
PriceType string `url:"priceType,omitempty"` PriceType string `url:"priceType,omitempty"`
OfferExternalID string `url:"offerExternalId,omitempty"` OfferExternalID string `url:"offerExternalId,omitempty"`
Sites []string `url:"sites,omitempty,brackets"` Sites []string `url:"sites,omitempty,brackets"`
@ -379,22 +381,22 @@ type ShipmentFilter struct {
// CostsFilter type. // CostsFilter type.
type CostsFilter struct { type CostsFilter struct {
MinSumm string `url:"minSumm,omitempty"` MinSumm int `url:"minSumm,omitempty"`
MaxSumm string `url:"maxSumm,omitempty"` MaxSumm int `url:"maxSumm,omitempty"`
OrderNumber string `url:"orderNumber,omitempty"` OrderNumber string `url:"orderNumber,omitempty"`
Comment string `url:"orderNumber,omitempty"` Comment string `url:"orderNumber,omitempty"`
Ids []string `url:"ids,omitempty,brackets"` IDs []int `url:"ids,omitempty,brackets"`
Sites []string `url:"sites,omitempty,brackets"` Sites []string `url:"sites,omitempty,brackets"`
CreatedBy []string `url:"createdBy,omitempty,brackets"` CreatedBy []int `url:"createdBy,omitempty,brackets"`
CostGroups []string `url:"costGroups,omitempty,brackets"` CostGroups []string `url:"costGroups,omitempty,brackets"`
CostItems []string `url:"costItems,omitempty,brackets"` CostItems []string `url:"costItems,omitempty,brackets"`
Users []string `url:"users,omitempty,brackets"` Users []int `url:"users,omitempty,brackets"`
DateFrom string `url:"dateFrom,omitempty"` DateFrom string `url:"dateFrom,omitempty"`
DateTo string `url:"dateTo,omitempty"` DateTo string `url:"dateTo,omitempty"`
CreatedAtFrom string `url:"createdAtFrom,omitempty"` CreatedAtFrom string `url:"createdAtFrom,omitempty"`
CreatedAtTo string `url:"createdAtTo,omitempty"` CreatedAtTo string `url:"createdAtTo,omitempty"`
OrderIds []string `url:"orderIds,omitempty,brackets"` OrderIDs []int `url:"orderIds,omitempty,brackets"`
OrderExternalIds []string `url:"orderIds,omitempty,brackets"` OrderExternalIDs []string `url:"orderExternalIds,omitempty,brackets"`
} }
// FilesFilter type. // FilesFilter type.
@ -448,8 +450,8 @@ type LoyaltyAccountAPIFilter struct {
ID string `url:"id,omitempty"` ID string `url:"id,omitempty"`
Status string `url:"status,,omitempty"` Status string `url:"status,,omitempty"`
Customer string `url:"customer,omitempty"` Customer string `url:"customer,omitempty"`
MinOrderSum string `url:"minOrderSum,omitempty"` MinOrderSum string `url:"minOrdersSum,omitempty"`
MaxOrderSum string `url:"maxOrderSum,omitempty"` MaxOrderSum string `url:"maxOrdersSum,omitempty"`
MinAmount string `url:"minAmount,omitempty"` MinAmount string `url:"minAmount,omitempty"`
MaxAmount string `url:"maxAmount,omitempty"` MaxAmount string `url:"maxAmount,omitempty"`
PhoneNumber string `url:"phoneNumber,omitempty"` PhoneNumber string `url:"phoneNumber,omitempty"`
@ -473,3 +475,23 @@ type LoyaltyAPIFilter struct {
Ids []int `url:"ids,omitempty,brackets"` Ids []int `url:"ids,omitempty,brackets"`
Sites []string `url:"sites,omitempty,brackets"` Sites []string `url:"sites,omitempty,brackets"`
} }
type OffersFilter struct {
Ids []int `url:"ids,omitempty,brackets"`
Active *int `url:"active,omitempty"`
}
type SiteFilter struct {
// SiteBy contains information about what is betrayed site id or site code.
// id|code, default is code.
SiteBy string `url:"siteBy,omitempty"`
}
type GetCartFilter struct {
// SiteBy contains information about what is betrayed site id or site code.
// id|code, default is code.
SiteBy string `url:"siteBy,omitempty"`
// By contains information about what is betrayed: customer id or customer externalId.
// id|externalId, default is externalId.
By string `url:"by,omitempty"`
}

View file

@ -58,6 +58,30 @@ func (l *StringMap) UnmarshalJSON(data []byte) error {
return nil return nil
} }
func (l *CustomFieldMap) UnmarshalJSON(data []byte) error {
var i interface{}
var items CustomFieldMap
if err := json.Unmarshal(data, &i); err != nil {
return err
}
switch e := i.(type) {
case map[string]interface{}:
items = make(CustomFieldMap, len(e))
for idx, val := range e {
items[idx] = val
}
case []interface{}:
items = make(CustomFieldMap, len(e))
for idx, val := range e {
items[strconv.Itoa(idx)] = val
}
}
*l = items
return nil
}
func (p *OrderPayments) UnmarshalJSON(data []byte) error { func (p *OrderPayments) UnmarshalJSON(data []byte) error {
var i interface{} var i interface{}
var m OrderPayments var m OrderPayments

View file

@ -44,7 +44,7 @@ func TestAPIErrorsList_UnmarshalJSON(t *testing.T) {
} }
func TestCustomFieldsList_UnmarshalJSON(t *testing.T) { func TestCustomFieldsList_UnmarshalJSON(t *testing.T) {
var list StringMap var list CustomFieldMap
require.NoError(t, json.Unmarshal([]byte(`["first", "second"]`), &list)) require.NoError(t, json.Unmarshal([]byte(`["first", "second"]`), &list))
assert.Len(t, list, 2) assert.Len(t, list, 2)
@ -56,6 +56,13 @@ func TestCustomFieldsList_UnmarshalJSON(t *testing.T) {
assert.Equal(t, list["a"], "first") assert.Equal(t, list["a"], "first")
assert.Equal(t, list["b"], "second") assert.Equal(t, list["b"], "second")
require.NoError(t, json.Unmarshal([]byte(`{"a": ["first", "second"], "b": "second"}`), &list))
assert.Len(t, list, 2)
assert.Len(t, list["a"].([]interface{}), 2)
assert.Equal(t, list["a"].([]interface{})[0], "first")
assert.Equal(t, list["a"].([]interface{})[1], "second")
assert.Equal(t, list["b"], "second")
require.NoError(t, json.Unmarshal([]byte(`[]`), &list)) require.NoError(t, json.Unmarshal([]byte(`[]`), &list))
assert.Len(t, list, 0) assert.Len(t, list, 0)
} }

View file

@ -194,6 +194,22 @@ type DeliveryShipmentsRequest struct {
Page int `url:"page,omitempty"` Page int `url:"page,omitempty"`
} }
// ClearCartRequest type.
type ClearCartRequest struct {
ClearedAt string `url:"clearedAt,omitempty"`
Customer CartCustomer `url:"customer,omitempty"`
Order ClearCartOrder `url:"order,omitempty"`
}
// SetCartRequest type.
type SetCartRequest struct {
ExternalID string `url:"externalId,omitempty"`
DroppedAt string `url:"droppedAt,omitempty"`
Link string `url:"link,omitempty"`
Customer CartCustomer `url:"customer,omitempty"`
Items []SetCartItem `url:"items,omitempty"`
}
// CostsRequest type. // CostsRequest type.
type CostsRequest struct { type CostsRequest struct {
Filter CostsFilter `url:"filter,omitempty"` Filter CostsFilter `url:"filter,omitempty"`
@ -249,7 +265,7 @@ type AccountBonusOperationsRequest struct {
type LoyaltyBonusCreditRequest struct { type LoyaltyBonusCreditRequest struct {
Amount float64 `url:"amount"` Amount float64 `url:"amount"`
ActivationDate string `url:"activationDate,omitempty"` ActivationDate string `url:"activationDate,omitempty"`
ExpiredDate string `url:"expiredDate,omitempty"` ExpiredDate string `url:"expireDate,omitempty"`
Comment string `url:"comment,omitempty"` Comment string `url:"comment,omitempty"`
} }
@ -261,7 +277,7 @@ type LoyaltyBonusStatusDetailsRequest struct {
type LoyaltyAccountsRequest struct { type LoyaltyAccountsRequest struct {
Limit int `url:"limit,omitempty"` Limit int `url:"limit,omitempty"`
Page int `url:"limit,omitempty"` Page int `url:"page,omitempty"`
Filter LoyaltyAccountAPIFilter `url:"filter,omitempty"` Filter LoyaltyAccountAPIFilter `url:"filter,omitempty"`
} }
@ -284,6 +300,11 @@ type NotificationsSendRequest struct {
UserIDs []string `json:"userIds,omitempty"` UserIDs []string `json:"userIds,omitempty"`
} }
type EditMGChannelTemplateRequest struct {
Templates []MGChannelTemplate `json:"templates"`
Removed []int `json:"removed"`
}
// SystemURL returns system URL from the connection request without trailing slash. // SystemURL returns system URL from the connection request without trailing slash.
func (r ConnectRequest) SystemURL() string { func (r ConnectRequest) SystemURL() string {
if r.URL == "" { if r.URL == "" {
@ -305,3 +326,7 @@ func (r ConnectRequest) Verify(secret string) bool {
} }
return hmac.Equal([]byte(r.Token), []byte(hex.EncodeToString(mac.Sum(nil)))) return hmac.Equal([]byte(r.Token), []byte(hex.EncodeToString(mac.Sum(nil))))
} }
type OffersRequest struct {
OffersFilter `url:"filter,omitempty"`
}

View file

@ -407,6 +407,12 @@ type ProductsPropertiesResponse struct {
Properties []Property `json:"properties,omitempty"` Properties []Property `json:"properties,omitempty"`
} }
// CartResponse type.
type CartResponse struct {
SuccessfulResponse
Cart Cart `json:"cart"`
}
// DeliveryShipmentsResponse type. // DeliveryShipmentsResponse type.
type DeliveryShipmentsResponse struct { type DeliveryShipmentsResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
@ -596,6 +602,34 @@ type AccountBonusOperationsResponse struct {
BonusOperations []BonusOperation `json:"bonusOperations,omitempty"` BonusOperations []BonusOperation `json:"bonusOperations,omitempty"`
} }
// ClientIDResponse type.
type ClientIDResponse struct {
ErrorMsg string `json:"errorMsg,omitempty"`
Errors map[string]string `json:"errors,omitempty"`
FailedClientIds []ClientID `json:"failed_client_ids,omitempty"`
Success bool `json:"success"`
}
// SourcesResponse type.
type SourcesResponse struct {
ErrorMsg string `json:"errorMsg,omitempty"`
Errors map[string]string `json:"errors,omitempty"`
FailedSources []Source `json:"failed_sources,omitempty"`
Success bool `json:"success"`
}
// CurrencyResponse type.
type CurrencyResponse struct {
Currencies []Currency `json:"currencies,omitempty"`
Success bool `json:"success"`
}
// CurrencyCreateResponse type.
type CurrencyCreateResponse struct {
Success bool `json:"success"`
ID int `json:"id,omitempty"`
}
type LoyaltyAccountResponse struct { type LoyaltyAccountResponse struct {
SuccessfulResponse SuccessfulResponse
LoyaltyAccount `json:"loyaltyAccount"` LoyaltyAccount `json:"loyaltyAccount"`
@ -651,3 +685,15 @@ type ActionProductsGroupResponse struct {
SuccessfulResponse SuccessfulResponse
ID int `json:"id"` ID int `json:"id"`
} }
type MGChannelTemplatesResponse struct {
Pagination *Pagination `json:"pagination"`
Templates []MGChannelTemplate `json:"templates"`
SuccessfulResponse
}
type StoreOffersResponse struct {
Pagination *Pagination `json:"pagination"`
SuccessfulResponse
Offers []Offer `json:"offers,omitempty"`
}

30
system_time.go Normal file
View file

@ -0,0 +1,30 @@
package retailcrm
import (
"fmt"
"strings"
"time"
)
type SystemTime time.Time
const systemTimeLayout = "2006-01-02 15:04:05"
// UnmarshalJSON parses time.Time from system format.
func (st *SystemTime) UnmarshalJSON(b []byte) (err error) {
s := strings.Trim(string(b), `"`)
nt, err := time.Parse(systemTimeLayout, s)
*st = SystemTime(nt)
return
}
// MarshalJSON will marshal time.Time to system format.
func (st SystemTime) MarshalJSON() ([]byte, error) {
return []byte(st.String()), nil
}
// String returns the time in the custom format.
func (st *SystemTime) String() string {
t := time.Time(*st)
return fmt.Sprintf("%q", t.Format(systemTimeLayout))
}

122
template.go Normal file
View file

@ -0,0 +1,122 @@
package retailcrm
import (
"encoding/json"
"errors"
"fmt"
)
const (
// TemplateItemTypeText is a type for text chunk in template.
TemplateItemTypeText uint8 = iota
// TemplateItemTypeVar is a type for variable in template.
TemplateItemTypeVar
QuickReply ButtonType = "QUICK_REPLY"
PhoneNumber ButtonType = "PHONE_NUMBER"
URL ButtonType = "URL"
)
const (
// TemplateVarCustom is a custom variable type.
TemplateVarCustom = "custom"
// TemplateVarName is a name variable type.
TemplateVarName = "name"
// TemplateVarFirstName is a first name variable type.
TemplateVarFirstName = "first_name"
// TemplateVarLastName is a last name variable type.
TemplateVarLastName = "last_name"
)
// templateVarAssoc for checking variable validity, only for internal use.
var templateVarAssoc = map[string]interface{}{
TemplateVarCustom: nil,
TemplateVarName: nil,
TemplateVarFirstName: nil,
TemplateVarLastName: nil,
}
type Text struct {
Parts []TextTemplateItem `json:"parts"`
Example []string `json:"example,omitempty"`
}
type Media struct {
Example string `json:"example,omitempty"`
}
type Header struct {
Text *Text `json:"text,omitempty"`
Document *Media `json:"document,omitempty"`
Image *Media `json:"image,omitempty"`
Video *Media `json:"video,omitempty"`
}
type TemplateItemList []TextTemplateItem
// TextTemplateItem is a part of template.
type TextTemplateItem struct {
Text string
VarType string
Type uint8
}
// MarshalJSON controls how TextTemplateItem will be marshaled into JSON.
func (t TextTemplateItem) MarshalJSON() ([]byte, error) {
switch t.Type {
case TemplateItemTypeText:
return json.Marshal(t.Text)
case TemplateItemTypeVar:
return json.Marshal(map[string]interface{}{
"var": t.VarType,
})
}
return nil, errors.New("unknown TextTemplateItem type")
}
// UnmarshalJSON will correctly unmarshal TextTemplateItem.
func (t *TextTemplateItem) UnmarshalJSON(b []byte) error {
var obj interface{}
err := json.Unmarshal(b, &obj)
if err != nil {
return err
}
switch bodyPart := obj.(type) {
case string:
t.Type = TemplateItemTypeText
t.Text = bodyPart
case map[string]interface{}:
// {} case
if len(bodyPart) == 0 {
t.Type = TemplateItemTypeText
t.Text = "{}"
return nil
}
if varTypeCurr, ok := bodyPart["var"].(string); ok {
if _, ok := templateVarAssoc[varTypeCurr]; !ok {
return fmt.Errorf("invalid placeholder var '%s'", varTypeCurr)
}
t.Type = TemplateItemTypeVar
t.VarType = varTypeCurr
} else {
return errors.New("invalid TextTemplateItem")
}
default:
return errors.New("invalid TextTemplateItem")
}
return nil
}
type ButtonType string
type Button struct {
Type ButtonType `json:"type"`
URL string `json:"url,omitempty"`
Text string `json:"text,omitempty"`
PhoneNumber string `json:"phoneNumber,omitempty"`
Example []string `json:"example,omitempty"`
}

View file

@ -85,7 +85,7 @@ func getLoyaltyAccountCreate() SerializedCreateLoyaltyAccount {
return SerializedCreateLoyaltyAccount{ return SerializedCreateLoyaltyAccount{
SerializedBaseLoyaltyAccount: SerializedBaseLoyaltyAccount{ SerializedBaseLoyaltyAccount: SerializedBaseLoyaltyAccount{
PhoneNumber: "89151005004", PhoneNumber: "89151005004",
CustomFields: []string{"dog"}, CustomFields: []interface{}{"dog"},
}, },
Customer: SerializedEntityCustomer{ Customer: SerializedEntityCustomer{
ID: 123, ID: 123,
@ -103,7 +103,9 @@ func getLoyaltyAccountCreateResponse() CreateLoyaltyAccountResponse {
LoyaltyLevel: LoyaltyLevel{}, LoyaltyLevel: LoyaltyLevel{},
CreatedAt: "2022-11-24 12:39:37", CreatedAt: "2022-11-24 12:39:37",
ActivatedAt: "2022-11-24 12:39:37", ActivatedAt: "2022-11-24 12:39:37",
CustomFields: []string{"dog"}, CustomFields: map[string]interface{}{
"animal": "dog",
},
}, },
} }
} }
@ -118,7 +120,9 @@ func getLoyaltyAccountEditResponse() EditLoyaltyAccountResponse {
LoyaltyLevel: LoyaltyLevel{}, LoyaltyLevel: LoyaltyLevel{},
CreatedAt: "2022-11-24 12:39:37", CreatedAt: "2022-11-24 12:39:37",
ActivatedAt: "2022-11-24 12:39:37", ActivatedAt: "2022-11-24 12:39:37",
CustomFields: []string{"dog"}, CustomFields: map[string]interface{}{
"animal": "dog",
},
}, },
} }
} }
@ -134,7 +138,7 @@ func getLoyaltyAccountResponse() string {
}, },
"customer": { "customer": {
"id": 123, "id": 123,
"customFields": [], "customFields": {},
"firstName": "Руслан1", "firstName": "Руслан1",
"lastName": "Ефанов", "lastName": "Ефанов",
"patronymic": "" "patronymic": ""
@ -154,7 +158,12 @@ func getLoyaltyAccountResponse() string {
"createdAt": "2022-11-24 12:39:37", "createdAt": "2022-11-24 12:39:37",
"activatedAt": "2022-11-24 12:39:37", "activatedAt": "2022-11-24 12:39:37",
"status": "activated", "status": "activated",
"customFields": [] "customFields": {
"custom_multiselect": ["test1", "test3"],
"custom_select": "test2",
"custom_integer": 456,
"custom_float": 8.43
}
} }
}` }`
} }
@ -424,3 +433,133 @@ func getLoyaltyResponse() string {
} }
}` }`
} }
func getMGTemplatesResponse() string {
return `{
"success": true,
"pagination": {
"limit": 10,
"totalCount": 100,
"currentPage": 5,
"totalPageCount": 10
},
"templates": [
{
"id": 1,
"externalId": 0,
"channel": {
"allowedSendByPhone": false,
"id": 1,
"externalId": 1,
"type": "fbmessenger",
"active": true,
"name": "fbmessenger"
},
"code": "namespace#NAMEAAA#ru",
"name": "NAMEAAA",
"active": true,
"template": [
"Text_0",
{
"var": "custom"
}
],
"templateExample": ["Text_1"],
"namespace": "namespace_0",
"lang": "en",
"category": "test_0",
"header": {
"text": {
"parts": [
"JABAAA",
{
"var": "custom"
}
],
"example": [
"AAAAAA"
]
},
"image": {
"example": "https://example.com/file/123.png"
},
"document": {
"example": "https://example.com/file/123.pdf"
},
"video": {
"example": "https://example.com/file/123.mp4"
}
},
"footer": "footer_0",
"buttons": [
{
"type": "PHONE_NUMBER",
"text": "your-phone-button-text",
"phoneNumber": "+79895553535"
},
{
"type": "QUICK_REPLY",
"text": "Yes"
},
{
"type": "URL",
"url": "https://example.com/file/{{1}}",
"text": "button",
"example": [
"https://www.website.com/dynamic-url-example"
]
}
],
"verificationStatus": "APPROVED"
}
]
}`
}
func getMGTemplatesForEdit() string {
return `[{"header":{"text":{"parts":["Hello,",{"var":"custom"}],"example":["Henry"]},"document":{"example":"https://example.com/file/123.pdf"},"image":{"example":"https://example.com/file/123.png"},"video":{"example":"https://example.com/file/123.mp4"}},"lang":"en","category":"test_0","code":"namespace#name_0#ru","name":"name_0","namespace":"namespace","footer":"footer_0","verificationStatus":"REJECTED","template":["Text_0",{"var":"custom"}],"buttons":[{"type":"PHONE_NUMBER","text":"your-phone-button-text","phoneNumber":"+79895553535"},{"type":"QUICK_REPLY","text":"Yes"},{"type":"URL","url":"https://example.com/file/{{1}}","text":"button","example":["https://www.website.com/dynamic-url-example"]}],"templateExample":["WIU"],"id":1,"externalId":10,"mgChannelId":110,"active":true}]`
}
func getStoreOfferResponse() string {
return `{
"success": true,
"pagination": {
"limit": 20,
"totalCount": 1,
"currentPage": 1,
"totalPageCount": 1
},
"offers": [
{
"images": [
"https://s3-s1.retailcrm.tech/ru-central1/retailcrm/dev-vega-d32aea7f9a5bc26eba6ad986077cea03/product/65a92fa0bb737-test.jpeg"
],
"id": 76,
"site": "main",
"name": "Название\nПеревод строки",
"article": "Артикул",
"product": {
"type": "product",
"catalogId": 2,
"id": 222
},
"prices": [
{
"priceType": "base",
"price": 10000,
"ordering": 991,
"currency": "RUB"
}
],
"purchasePrice": 10,
"quantity": 5,
"active": true,
"unit": {
"code": "pc",
"name": "Штука",
"sym": "шт."
}
}
]
}`
}

305
types.go
View file

@ -5,14 +5,28 @@ import (
"net/http" "net/http"
"reflect" "reflect"
"strings" "strings"
"sync"
"time"
) )
// ByID is "id" constant to use as `by` property in methods. // ByID is "id" constant to use as `by` property in methods.
const ByID = "id" const ByID = "id"
// ByExternalId is "externalId" constant to use as `by` property in methods. // ByExternalID is "externalId" constant to use as `by` property in methods.
const ByExternalID = "externalId" const ByExternalID = "externalId"
// RateLimiter configuration constants.
const (
regularPathRPS = 10 // API rate limit (requests per second).
telephonyPathRPS = 40 // Telephony API endpoints rate limit (requests per second).
regularDelay = time.Second / regularPathRPS // Delay between regular requests.
telephonyDelay = time.Second / telephonyPathRPS // Delay between telephony requests.
)
// HTTPStatusUnknown can return for the method `/api/v5/customers/upload`, `/api/v5/customers-corporate/upload`,
// `/api/v5/orders/upload`.
const HTTPStatusUnknown = 460
// Client type. // Client type.
type Client struct { type Client struct {
URL string URL string
@ -20,6 +34,15 @@ type Client struct {
Debug bool Debug bool
httpClient *http.Client httpClient *http.Client
logger BasicLogger logger BasicLogger
limiter *RateLimiter
mutex sync.Mutex
}
// RateLimiter manages API request rates to prevent hitting rate limits.
type RateLimiter struct {
maxAttempts uint // Maximum number of retry attempts (0 = infinite).
lastRequest time.Time // Time of the last request.
mutex sync.Mutex
} }
// Pagination type. // Pagination type.
@ -53,13 +76,16 @@ type Address struct {
Text string `json:"text,omitempty"` Text string `json:"text,omitempty"`
} }
// GeoID type. Can be empty string.
type GeoID json.Number
// GeoHierarchyRow type. // GeoHierarchyRow type.
type GeoHierarchyRow struct { type GeoHierarchyRow struct {
Country string `json:"country,omitempty"` Country string `json:"country,omitempty"`
Region string `json:"region,omitempty"` Region string `json:"region,omitempty"`
RegionID int `json:"regionId,omitempty"` RegionID GeoID `json:"regionId,omitempty"`
City string `json:"city,omitempty"` City string `json:"city,omitempty"`
CityID int `json:"cityId,omitempty"` CityID GeoID `json:"cityId,omitempty"`
} }
// Source type. // Source type.
@ -69,6 +95,10 @@ type Source struct {
Campaign string `json:"campaign,omitempty"` Campaign string `json:"campaign,omitempty"`
Keyword string `json:"keyword,omitempty"` Keyword string `json:"keyword,omitempty"`
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
ClientID string `json:"client_id,omitempty"`
Site string `json:"site,omitempty"`
Order LinkedOrder `json:"order,omitempty"`
Customer SerializedEntityCustomer `json:"customer,omitempty"`
} }
// Contragent type. // Contragent type.
@ -155,7 +185,7 @@ type Customer struct {
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 StringMap `json:"customFields,omitempty"` CustomFields CustomFieldMap `json:"customFields,omitempty"`
Tags []Tag `json:"tags,omitempty"` Tags []Tag `json:"tags,omitempty"`
} }
@ -167,7 +197,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 StringMap `json:"customFields,omitempty"` CustomFields CustomFieldMap `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,6 +247,15 @@ type CorporateCustomerContactCustomer struct {
Site string `json:"site,omitempty"` Site string `json:"site,omitempty"`
} }
// CartCustomer type.
type CartCustomer struct {
ID int `json:"id,omitempty"`
ExternalID string `json:"externalId,omitempty"`
Site string `json:"site,omitempty"`
BrowserID string `json:"browserId,omitempty"`
GaClientID string `json:"gaClientId,omitempty"`
}
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"`
@ -228,7 +267,7 @@ type Company struct {
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 StringMap `json:"customFields,omitempty"` CustomFields CustomFieldMap `json:"customFields,omitempty"`
} }
// CorporateCustomerNote type. // CorporateCustomerNote type.
@ -251,9 +290,19 @@ type CustomerHistoryRecord struct {
Deleted bool `json:"deleted,omitempty"` Deleted bool `json:"deleted,omitempty"`
Source string `json:"source,omitempty"` Source string `json:"source,omitempty"`
Field string `json:"field,omitempty"` Field string `json:"field,omitempty"`
OldValue interface{} `json:"oldValue,omitempty"`
NewValue interface{} `json:"newValue,omitempty"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
APIKey *APIKey `json:"apiKey,omitempty"` APIKey *APIKey `json:"apiKey,omitempty"`
Customer *Customer `json:"customer,omitempty"` Customer *Customer `json:"customer,omitempty"`
Address *CustomerAddressWithIsMain `json:"address,omitempty"`
}
type CustomerAddressWithIsMain struct {
ID int `json:"id"`
ExternalID string `json:"externalId,omitempty"`
Name string `json:"name,omitempty"`
IsMain bool `json:"isMain"`
} }
// CorporateCustomerHistoryRecord type. // CorporateCustomerHistoryRecord type.
@ -264,6 +313,8 @@ type CorporateCustomerHistoryRecord struct {
Deleted bool `json:"deleted,omitempty"` Deleted bool `json:"deleted,omitempty"`
Source string `json:"source,omitempty"` Source string `json:"source,omitempty"`
Field string `json:"field,omitempty"` Field string `json:"field,omitempty"`
OldValue interface{} `json:"oldValue,omitempty"`
NewValue interface{} `json:"newValue,omitempty"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
APIKey *APIKey `json:"apiKey,omitempty"` APIKey *APIKey `json:"apiKey,omitempty"`
CorporateCustomer *CorporateCustomer `json:"corporateCustomer,omitempty"` CorporateCustomer *CorporateCustomer `json:"corporateCustomer,omitempty"`
@ -275,6 +326,7 @@ Order related types
type OrderPayments map[string]OrderPayment type OrderPayments map[string]OrderPayment
type StringMap map[string]string type StringMap map[string]string
type CustomFieldMap map[string]interface{}
type Properties map[string]Property type Properties map[string]Property
// Order type. // Order type.
@ -326,10 +378,61 @@ type Order struct {
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 StringMap `json:"customFields,omitempty"` CustomFields CustomFieldMap `json:"customFields,omitempty"`
Payments OrderPayments `json:"payments,omitempty"` Payments OrderPayments `json:"payments,omitempty"`
ApplyRound *bool `json:"applyRound,omitempty"` ApplyRound *bool `json:"applyRound,omitempty"`
PrivilegeType string `json:"privilegeType,omitempty"` PrivilegeType string `json:"privilegeType,omitempty"`
DialogID int `json:"dialogId,omitempty"`
Links []OrderLink `json:"links,omitempty"`
Currency string `json:"currency,omitempty"`
}
// LinkedOrder type.
type LinkedOrder struct {
Number string `json:"number,omitempty"`
ExternalID string `json:"externalID,omitempty"`
ID int `json:"id,omitempty"`
}
// OrderLink type.
type OrderLink struct {
Comment string `json:"comment,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
Order LinkedOrder `json:"order,omitempty"`
}
// SerializedOrderLink type.
type SerializedOrderLink struct {
Comment string `json:"comment,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
Orders []LinkedOrder `json:"orders,omitempty"`
}
// ClearCartOrder type.
type ClearCartOrder struct {
ID int `json:"id,omitempty"`
ExternalID string `json:"externalID,omitempty"`
Number string `json:"number,omitempty"`
}
// ClientID type.
type ClientID struct {
Value string `json:"value"`
CreateAt string `json:"createAt,omitempty"`
Site string `json:"site,omitempty"`
Customer SerializedEntityCustomer `json:"customer,omitempty"`
Order LinkedOrder `json:"order,omitempty"`
}
// Currency type.
type Currency struct {
Code string `json:"code,omitempty"`
ID int `json:"id,omitempty"`
ManualConvertNominal int `json:"manualConvertNominal,omitempty"`
AutoConvertExtraPercent int `json:"autoConvertExtraPercent,omitempty"`
IsBase bool `json:"isBase,omitempty"`
IsAutoConvert bool `json:"isAutoConvert,omitempty"`
ManualConvertValue float32 `json:"manualConvertValue,omitempty"`
} }
// OrdersStatus type. // OrdersStatus type.
@ -382,6 +485,59 @@ type OrderDeliveryData struct {
AdditionalFields map[string]interface{} AdditionalFields map[string]interface{}
} }
// SetCartItem type.
type SetCartItem struct {
Quantity float64 `json:"quantity,omitempty"`
Price float64 `json:"price,omitempty"`
Offer SetCartOffer `json:"offer,omitempty"`
}
// SetCartOffer type.
type SetCartOffer struct {
ID int `json:"id,omitempty"`
ExternalID string `json:"externalID,omitempty"`
XMLID string `json:"xmlId,omitempty"`
}
// Cart type.
type Cart struct {
Currency string `json:"currency,omitempty"`
ExternalID string `json:"externalId,omitempty"`
DroppedAt string `json:"droppedAt,omitempty"`
ClearedAt string `json:"clearedAt,omitempty"`
Link string `json:"link,omitempty"`
Items []CartItem `json:"items,omitempty"`
}
// CartItem type.
type CartItem struct {
ID int `json:"id,omitempty"`
Quantity float64 `json:"quantity,omitempty"`
Price float64 `json:"price,omitempty"`
Offer CartOffer `json:"offer,omitempty"`
}
// CartOffer type.
type CartOffer struct {
DisplayName string `json:"displayName,omitempty"`
ID int `json:"id,omitempty"`
ExternalID string `json:"externalId,omitempty"`
XMLID string `json:"xmlId,omitempty"`
Name string `json:"name,omitempty"`
Article string `json:"article,omitempty"`
VatRate string `json:"vatRate,omitempty"`
Properties StringMap `json:"properties,omitempty"`
Unit CartUnit `json:"unit,omitempty"`
Barcode string `json:"barcode,omitempty"`
}
// CartUnit type.
type CartUnit struct {
Code string `json:"code"`
Name string `json:"name"`
Sym string `json:"sym"`
}
// 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{}
@ -471,9 +627,15 @@ type OrdersHistoryRecord struct {
Deleted bool `json:"deleted,omitempty"` Deleted bool `json:"deleted,omitempty"`
Source string `json:"source,omitempty"` Source string `json:"source,omitempty"`
Field string `json:"field,omitempty"` Field string `json:"field,omitempty"`
OldValue interface{} `json:"oldValue,omitempty"`
NewValue interface{} `json:"newValue,omitempty"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
APIKey *APIKey `json:"apiKey,omitempty"` APIKey *APIKey `json:"apiKey,omitempty"`
Order *Order `json:"order,omitempty"` Order *Order `json:"order,omitempty"`
Ancestor *Order `json:"ancestor,omitempty"`
Item *OrderItem `json:"item,omitempty"`
Payment *Payment `json:"payment"`
CombinedTo *Order `json:"combinedTo,omitempty"`
} }
// Pack type. // Pack type.
@ -505,6 +667,8 @@ type PacksHistoryRecord struct {
Deleted bool `json:"deleted,omitempty"` Deleted bool `json:"deleted,omitempty"`
Source string `json:"source,omitempty"` Source string `json:"source,omitempty"`
Field string `json:"field,omitempty"` Field string `json:"field,omitempty"`
OldValue interface{} `json:"oldValue,omitempty"`
NewValue interface{} `json:"newValue,omitempty"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
Pack *Pack `json:"pack,omitempty"` Pack *Pack `json:"pack,omitempty"`
} }
@ -529,6 +693,7 @@ type Offer struct {
Prices []OfferPrice `json:"prices,omitempty"` Prices []OfferPrice `json:"prices,omitempty"`
Images []string `json:"images,omitempty"` Images []string `json:"images,omitempty"`
Unit *Unit `json:"unit,omitempty"` Unit *Unit `json:"unit,omitempty"`
Product *Product `json:"product,omitempty"`
} }
// Inventory type. // Inventory type.
@ -558,6 +723,7 @@ type OfferPrice struct {
Price float32 `json:"price,omitempty"` Price float32 `json:"price,omitempty"`
Ordering int `json:"ordering,omitempty"` Ordering int `json:"ordering,omitempty"`
PriceType string `json:"priceType,omitempty"` PriceType string `json:"priceType,omitempty"`
Currency string `json:"currency,omitempty"`
} }
// OfferPriceUpload type. // OfferPriceUpload type.
@ -597,6 +763,7 @@ type User struct {
CreatedAt string `json:"createdAt,omitempty"` CreatedAt string `json:"createdAt,omitempty"`
Active bool `json:"active,omitempty"` Active bool `json:"active,omitempty"`
Online bool `json:"online,omitempty"` Online bool `json:"online,omitempty"`
Position string `json:"position,omitempty"`
IsAdmin bool `json:"isAdmin,omitempty"` IsAdmin bool `json:"isAdmin,omitempty"`
IsManager bool `json:"isManager,omitempty"` IsManager bool `json:"isManager,omitempty"`
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
@ -705,10 +872,16 @@ type WorkTime struct {
LunchEndTime string `json:"lunch_end_time"` LunchEndTime string `json:"lunch_end_time"`
} }
// NonWorkingDays type.
type NonWorkingDays struct {
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
}
type SerializedBaseLoyaltyAccount struct { type SerializedBaseLoyaltyAccount struct {
PhoneNumber string `json:"phoneNumber,omitempty"` PhoneNumber string `json:"phoneNumber,omitempty"`
CardNumber string `json:"cardNumber,omitempty"` CardNumber string `json:"cardNumber,omitempty"`
CustomFields []string `json:"customFields,omitempty"` CustomFields []interface{} `json:"customFields,omitempty"`
} }
type SerializedCreateLoyaltyAccount struct { type SerializedCreateLoyaltyAccount struct {
@ -720,12 +893,29 @@ type SerializedEditLoyaltyAccount struct {
SerializedBaseLoyaltyAccount SerializedBaseLoyaltyAccount
} }
type ChannelSetting struct {
Site string `json:"site"`
OrderType string `json:"order_type"`
OrderMethod string `json:"order_method"`
}
type MgOrderCreationSettings struct {
Channels map[int]ChannelSetting `json:"channels"`
Default ChannelSetting `json:"default"`
}
type MgSettings struct {
OrderCreation MgOrderCreationSettings `json:"order_creation"`
}
// Settings type. Contains retailCRM configuration. // Settings type. Contains retailCRM configuration.
type Settings struct { type Settings struct {
DefaultCurrency SettingsNode `json:"default_currency"` DefaultCurrency SettingsNode `json:"default_currency"`
SystemLanguage SettingsNode `json:"system_language"` SystemLanguage SettingsNode `json:"system_language"`
Timezone SettingsNode `json:"timezone"` Timezone SettingsNode `json:"timezone"`
MgSettings MgSettings `json:"mg"`
WorkTimes []WorkTime `json:"work_times"` WorkTimes []WorkTime `json:"work_times"`
NonWorkingDays []NonWorkingDays `json:"non_working_days"`
} }
/** /**
@ -787,6 +977,7 @@ type DeliveryType struct {
DeliveryServices []string `json:"deliveryServices,omitempty"` DeliveryServices []string `json:"deliveryServices,omitempty"`
PaymentTypes []string `json:"paymentTypes,omitempty"` // Deprecated, use DeliveryPaymentTypes PaymentTypes []string `json:"paymentTypes,omitempty"` // Deprecated, use DeliveryPaymentTypes
DeliveryPaymentTypes []DeliveryPaymentType `json:"deliveryPaymentTypes,omitempty"` DeliveryPaymentTypes []DeliveryPaymentType `json:"deliveryPaymentTypes,omitempty"`
Currency string `json:"currency,omitempty"`
} }
type DeliveryPaymentType struct { type DeliveryPaymentType struct {
@ -818,7 +1009,7 @@ type LegalEntity struct {
type SerializedEntityCustomer struct { type SerializedEntityCustomer struct {
ID int `json:"id,omitempty"` ID int `json:"id,omitempty"`
ExternalID int `json:"externalId,omitempty"` ExternalID string `json:"externalId,omitempty"`
} }
// OrderMethod type. // OrderMethod type.
@ -876,6 +1067,7 @@ type PriceType struct {
Ordering int `json:"ordering,omitempty"` Ordering int `json:"ordering,omitempty"`
Groups []string `json:"groups,omitempty"` Groups []string `json:"groups,omitempty"`
Geo []GeoHierarchyRow `json:"geo,omitempty"` Geo []GeoHierarchyRow `json:"geo,omitempty"`
Currency string `json:"currency,omitempty"`
} }
// ProductStatus type. // ProductStatus type.
@ -929,6 +1121,7 @@ type Site struct {
IsDemo bool `json:"isDemo,omitempty"` IsDemo bool `json:"isDemo,omitempty"`
CatalogID string `json:"catalogId,omitempty"` CatalogID string `json:"catalogId,omitempty"`
IsCatalogMainSite bool `json:"isCatalogMainSite,omitempty"` IsCatalogMainSite bool `json:"isCatalogMainSite,omitempty"`
Currency string `json:"currency,omitempty"`
} }
// Store type. // Store type.
@ -961,6 +1154,7 @@ type ProductGroup struct {
// BaseProduct type. // BaseProduct type.
type BaseProduct struct { type BaseProduct struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Type ProductType `json:"type,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Article string `json:"article,omitempty"` Article string `json:"article,omitempty"`
ExternalID string `json:"externalId,omitempty"` ExternalID string `json:"externalId,omitempty"`
@ -974,10 +1168,18 @@ type BaseProduct struct {
Markable bool `json:"markable,omitempty"` Markable bool `json:"markable,omitempty"`
} }
type ProductType string
const (
RegularProduct ProductType = "product"
ServiceProduct ProductType = "service"
)
// Product type. // Product type.
type Product struct { type Product struct {
BaseProduct BaseProduct
ID int `json:"id,omitempty"` ID int `json:"id,omitempty"`
Type ProductType `json:"type"`
MaxPrice float32 `json:"maxPrice,omitempty"` MaxPrice float32 `json:"maxPrice,omitempty"`
MinPrice float32 `json:"minPrice,omitempty"` MinPrice float32 `json:"minPrice,omitempty"`
ImageURL string `json:"imageUrl,omitempty"` ImageURL string `json:"imageUrl,omitempty"`
@ -1149,10 +1351,20 @@ type Action struct {
// MgTransport type. // MgTransport type.
type MgTransport struct { type MgTransport struct {
WebhookURL string `json:"webhookUrl,omitempty"` WebhookURL string `json:"webhookUrl,omitempty"`
RefreshToken bool `json:"refreshToken,omitempty"`
Actions *MgTransportActions `json:"actions,omitempty"`
}
type MgTransportActions struct {
Visits string `json:"visits,omitempty"`
Online string `json:"online,omitempty"`
ManualTemplatesSync string `json:"manualTemplatesSync,omitempty"`
} }
// MgBot type. // MgBot type.
type MgBot struct{} type MgBot struct {
RefreshToken bool `json:"refreshToken,omitempty"`
}
/** /**
Cost related types Cost related types
@ -1213,11 +1425,13 @@ type CustomFields struct {
InGroupActions bool `json:"inGroupActions,omitempty"` InGroupActions bool `json:"inGroupActions,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Entity string `json:"entity,omitempty"` Entity string `json:"entity,omitempty"`
// Deprecated: Use DefaultTyped instead.
Default string `json:"default,omitempty"` Default string `json:"default,omitempty"`
Ordering int `json:"ordering,omitempty"` Ordering int `json:"ordering,omitempty"`
DisplayArea string `json:"displayArea,omitempty"` DisplayArea string `json:"displayArea,omitempty"`
ViewMode string `json:"viewMode,omitempty"` ViewMode string `json:"viewMode,omitempty"`
Dictionary string `json:"dictionary,omitempty"` Dictionary string `json:"dictionary,omitempty"`
DefaultTyped interface{} `json:"default_typed,omitempty"`
} }
/** /**
@ -1244,6 +1458,45 @@ type Activity struct {
Freeze bool `json:"freeze"` Freeze bool `json:"freeze"`
} }
type ChatCustomerOnline struct {
LastOnline SystemTime `json:"lastOnline"`
}
type ChatVisitsResponse struct {
UTM *ChatUTM `json:"utm,omitempty"`
Device ChatDevice `json:"device"`
Country string `json:"country"`
City string `json:"city"`
LastVisit ChatLastVisit `json:"lastVisit"`
CountVisits uint `json:"countVisits"`
}
type ChatLastVisit struct {
CreatedAt SystemTime `json:"createdAt"`
EndedAt *SystemTime `json:"endedAt,omitempty"`
Source string `json:"source"`
Pages []ChatVisitedPage `json:"pages"`
Duration uint `json:"duration"`
}
type ChatVisitedPage struct {
DateTime SystemTime `json:"dateTime"`
Title string `json:"title,omitempty"`
URL string `json:"url"`
}
type ChatDevice struct {
Lang string `json:"lang"`
Browser string `json:"browser"`
OS string `json:"os"`
}
type ChatUTM struct {
Source string `json:"source"`
Medium string `json:"medium"`
Campaign string `json:"campaign"`
}
// Tag struct. // Tag struct.
type Tag struct { type Tag struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
@ -1313,7 +1566,7 @@ type LoyaltyAccount struct {
ActivatedAt string `json:"activatedAt,omitempty"` ActivatedAt string `json:"activatedAt,omitempty"`
ConfirmedPhoneAt string `json:"confirmedPhoneAt,omitempty"` ConfirmedPhoneAt string `json:"confirmedPhoneAt,omitempty"`
LastCheckID int `json:"lastCheckId,omitempty"` LastCheckID int `json:"lastCheckId,omitempty"`
CustomFields []string `json:"customFields,omitempty"` CustomFields CustomFieldMap `json:"customFields,omitempty"`
Loyalty Loyalty `json:"loyalty,omitempty"` Loyalty Loyalty `json:"loyalty,omitempty"`
Customer Customer `json:"customer,omitempty"` Customer Customer `json:"customer,omitempty"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
@ -1334,6 +1587,7 @@ type Loyalty struct {
ActivatedAt string `json:"activatedAt,omitempty"` ActivatedAt string `json:"activatedAt,omitempty"`
DeactivatedAt string `json:"deactivatedAt,omitempty"` DeactivatedAt string `json:"deactivatedAt,omitempty"`
BlockedAt string `json:"blockedAt,omitempty"` BlockedAt string `json:"blockedAt,omitempty"`
Currency string `json:"currency,omitempty"`
} }
// LoyaltyLevel type. // LoyaltyLevel type.
@ -1377,6 +1631,7 @@ type SerializedLoyaltyOrder struct {
Delivery Delivery `json:"delivery,omitempty"` Delivery Delivery `json:"delivery,omitempty"`
Site string `json:"site,omitempty"` Site string `json:"site,omitempty"`
Items []LoyaltyItems `json:"items,omitempty"` Items []LoyaltyItems `json:"items,omitempty"`
Currency string `json:"currency,omitempty"`
} }
type LoyaltyEventDiscount struct { type LoyaltyEventDiscount struct {
@ -1434,3 +1689,31 @@ type ExternalID struct {
type UserGroupType string type UserGroupType string
type NotificationType string type NotificationType string
type MGChannel struct {
Type string `json:"type"`
Name string `json:"name"`
ID int `json:"id"`
ExternalID int `json:"externalId"`
AllowedSendByPhone bool `json:"allowedSendByPhone"`
Active bool `json:"active"`
}
type MGChannelTemplate struct {
Channel *MGChannel `json:"channel,omitempty"`
Header *Header `json:"header"`
Lang string `json:"lang"`
Category string `json:"category"`
Code string `json:"code,omitempty"`
Name string `json:"name"`
Namespace string `json:"namespace,omitempty"`
Footer string `json:"footer,omitempty"`
VerificationStatus string `json:"verificationStatus,omitempty"`
BodyTemplate TemplateItemList `json:"template"`
Buttons []Button `json:"buttons,omitempty"`
BodyTemplateExample []string `json:"templateExample"`
ID int `json:"id,omitempty"`
ExternalID int `json:"externalId,omitempty"`
MGChannelID int `json:"mgChannelId"`
Active bool `json:"active"`
}