Compare commits

...

58 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
Ruslan Efanov
49905ab9c6 update linter and it's rules 2022-12-30 11:19:20 +03:00
14 changed files with 2676 additions and 303 deletions

View file

@ -20,10 +20,10 @@ jobs:
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Set up Go 1.17
uses: actions/setup-go@v2
- name: Set up Go 1.23
uses: actions/setup-go@v3
with:
go-version: '1.17'
go-version: '1.23'
- name: Get dependencies
run: |
go mod tidy
@ -31,7 +31,7 @@ jobs:
- name: Lint code with golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.45.2
version: v1.62.2
only-new-issues: true
skip-pkg-cache: true
args: --build-tags=testutils
@ -40,13 +40,13 @@ jobs:
runs-on: ubuntu-latest
strategy:
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:
- go-version: '1.17'
- go-version: '1.23'
coverage: 1
steps:
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
- name: Check out code into the Go module directory
@ -59,17 +59,21 @@ jobs:
env:
COVERAGE: ${{ matrix.coverage }}
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
env:
COVERAGE: ${{ matrix.coverage }}
if: env.COVERAGE == 1
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
env:
COVERAGE: ${{ matrix.coverage }}
if: env.COVERAGE == 1
run: |
go install github.com/axw/gocov/gocov@latest
gocov convert ./coverage.txt | gocov report
bash <(curl -s https://codecov.io/bash)
rm coverage.txt

View file

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

899
client.go
View file

@ -17,10 +17,6 @@ import (
"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.
func New(url string, key string) *Client {
return &Client{
@ -36,6 +32,125 @@ func (c *Client) WithLogger(logger BasicLogger) *Client {
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.
func (c *Client) writeLog(format string, v ...interface{}) {
if c.logger != nil {
@ -48,7 +163,6 @@ func (c *Client) writeLog(format string, v ...interface{}) {
// GetRequest implements GET Request.
func (c *Client) GetRequest(urlWithParameters string, versioned ...bool) ([]byte, int, error) {
var res []byte
var prefix = "/api/v5"
if len(versioned) > 0 {
@ -57,41 +171,49 @@ func (c *Client) GetRequest(urlWithParameters string, versioned ...bool) ([]byte
}
}
req, err := http.NewRequest("GET", fmt.Sprintf("%s%s%s", c.URL, prefix, urlWithParameters), nil)
if err != nil {
return res, 0, err
}
uri := urlWithParameters
req.Header.Set("X-API-KEY", c.Key)
return c.executeWithRetryBytes(uri, func() (interface{}, int, error) {
var res []byte
if c.Debug {
c.writeLog("API Request: %s %s", fmt.Sprintf("%s%s%s", c.URL, prefix, urlWithParameters), c.Key)
}
req, err := http.NewRequest("GET", fmt.Sprintf("%s%s%s", c.URL, prefix, urlWithParameters), nil)
if err != nil {
return res, 0, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return res, 0, err
}
req.Header.Set("X-API-KEY", c.Key)
if resp.StatusCode >= http.StatusInternalServerError {
return res, resp.StatusCode, CreateGenericAPIError(
fmt.Sprintf("HTTP request error. Status code: %d.", resp.StatusCode))
}
if c.Debug {
c.writeLog("API Request: %s %s", fmt.Sprintf("%s%s%s", c.URL, prefix, urlWithParameters), c.Key)
}
res, err = buildRawResponse(resp)
if err != nil {
return res, 0, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return res, 0, err
}
if resp.StatusCode >= http.StatusBadRequest && resp.StatusCode < http.StatusInternalServerError {
return res, resp.StatusCode, CreateAPIError(res)
}
if resp.StatusCode >= http.StatusInternalServerError && resp.StatusCode != http.StatusServiceUnavailable {
return res, resp.StatusCode, CreateGenericAPIError(
fmt.Sprintf("HTTP request error. Status code: %d.", resp.StatusCode))
}
if c.Debug {
c.writeLog("API Response: %s", res)
}
res, err = buildRawResponse(resp)
if err != nil {
return res, 0, err
}
return res, resp.StatusCode, nil
if resp.StatusCode >= http.StatusBadRequest &&
resp.StatusCode < http.StatusInternalServerError &&
resp.StatusCode != http.StatusServiceUnavailable {
return res, resp.StatusCode, CreateAPIError(res)
}
if c.Debug {
c.writeLog("API Response: %s", res)
}
return res, resp.StatusCode, nil
})
}
// PostRequest implements POST Request with generic body data.
@ -100,12 +222,7 @@ func (c *Client) PostRequest(
postData interface{},
contType ...string,
) ([]byte, int, error) {
var (
res []byte
contentType string
)
prefix := "/api/v5"
var contentType string
if len(contType) > 0 {
contentType = contType[0]
@ -113,47 +230,55 @@ func (c *Client) PostRequest(
contentType = "application/x-www-form-urlencoded"
}
reader, err := getReaderForPostData(postData)
if err != nil {
return res, 0, err
}
prefix := "/api/v5"
req, err := http.NewRequest("POST", fmt.Sprintf("%s%s%s", c.URL, prefix, uri), reader)
if err != nil {
return res, 0, err
}
return c.executeWithRetryBytes(uri, func() (interface{}, int, error) {
var res []byte
req.Header.Set("Content-Type", contentType)
req.Header.Set("X-API-KEY", c.Key)
reader, err := getReaderForPostData(postData)
if err != nil {
return res, 0, err
}
if c.Debug {
c.writeLog("API Request: %s %s", uri, c.Key)
}
req, err := http.NewRequest("POST", fmt.Sprintf("%s%s%s", c.URL, prefix, uri), reader)
if err != nil {
return res, 0, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return res, 0, err
}
req.Header.Set("Content-Type", contentType)
req.Header.Set("X-API-KEY", c.Key)
if resp.StatusCode >= http.StatusInternalServerError {
return res, resp.StatusCode, CreateGenericAPIError(
fmt.Sprintf("HTTP request error. Status code: %d.", resp.StatusCode))
}
if c.Debug {
c.writeLog("API Request: %s %s", uri, c.Key)
}
res, err = buildRawResponse(resp)
if err != nil {
return res, 0, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return res, 0, err
}
if resp.StatusCode >= http.StatusBadRequest && resp.StatusCode < http.StatusInternalServerError {
return res, resp.StatusCode, CreateAPIError(res)
}
if resp.StatusCode >= http.StatusInternalServerError && resp.StatusCode != http.StatusServiceUnavailable {
return res, resp.StatusCode, CreateGenericAPIError(
fmt.Sprintf("HTTP request error. Status code: %d.", resp.StatusCode))
}
if c.Debug {
c.writeLog("API Response: %s", res)
}
res, err = buildRawResponse(resp)
if err != nil {
return res, 0, err
}
return res, resp.StatusCode, nil
if resp.StatusCode >= http.StatusBadRequest &&
resp.StatusCode < http.StatusInternalServerError &&
resp.StatusCode != http.StatusServiceUnavailable {
return res, resp.StatusCode, CreateAPIError(res)
}
if c.Debug {
c.writeLog("API Response: %s", res)
}
return res, resp.StatusCode, nil
})
}
func getReaderForPostData(postData interface{}) (io.Reader, error) {
@ -1832,6 +1957,174 @@ func (c *Client) CorporateCustomerEdit(customer CorporateCustomer, by string, si
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
//
// 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
}
// 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
//
// 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{
// Filter: CostsFilter{
// Ids: []string{"1","2","3"},
// IDs: []string{"1","2","3"},
// MinSumm: "1000"
// },
// })
@ -6445,53 +7055,52 @@ func (c *Client) EditProductsGroup(by, id, site string, group ProductGroup) (Act
// log.Printf("%s", fileData)
// }
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)},
"site": {site},
}
}.Encode())
requestURL := fmt.Sprintf("%s/api/v5/orders/%s/plates/%d/print?%s", c.URL, orderID, plateID, p.Encode())
req, err := http.NewRequest("GET", requestURL, nil)
return c.executeWithRetryReadCloser(requestURL, func() (interface{}, int, error) {
req, err := http.NewRequest("GET", requestURL, nil)
if err != nil {
return nil, 0, err
}
if err != nil {
return nil, 0, err
}
req.Header.Set("X-API-KEY", c.Key)
req.Header.Set("X-API-KEY", c.Key)
if c.Debug {
c.writeLog("API Request: %s %s", requestURL, c.Key)
}
if c.Debug {
c.writeLog("API Request: %s %s", requestURL, c.Key)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, 0, err
}
if resp.StatusCode >= http.StatusInternalServerError {
return nil, resp.StatusCode, CreateGenericAPIError(
fmt.Sprintf("HTTP request error. Status code: %d.", resp.StatusCode))
}
if resp.StatusCode >= http.StatusBadRequest && resp.StatusCode < http.StatusInternalServerError {
res, err := buildRawResponse(resp)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, 0, err
}
return nil, resp.StatusCode, CreateAPIError(res)
}
if resp.StatusCode >= http.StatusInternalServerError && resp.StatusCode != http.StatusServiceUnavailable {
return nil, resp.StatusCode, CreateGenericAPIError(
fmt.Sprintf("HTTP request error. Status code: %d.", resp.StatusCode))
}
reader := resp.Body
err = reader.Close()
if resp.StatusCode >= http.StatusBadRequest &&
resp.StatusCode < http.StatusInternalServerError &&
resp.StatusCode != http.StatusServiceUnavailable {
res, err := buildRawResponse(resp)
if err != nil {
return nil, 0, err
}
if err != nil {
return nil, 0, err
}
return reader, resp.StatusCode, nil
return nil, resp.StatusCode, CreateAPIError(res)
}
if err != nil {
return nil, 0, err
}
return resp.Body, resp.StatusCode, nil
})
}
// NotificationsSend send a notification
@ -6531,3 +7140,85 @@ func (c *Client) NotificationsSend(req NotificationsSendRequest) (int, error) {
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 (
// 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 = NewAPIError(`apiKey is missing`)
// 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"`
PaymentTypes []string `url:"paymentTypes,omitempty,brackets"`
DeliveryTypes []string `url:"deliveryTypes,omitempty,brackets"`
DeliveryServices []string `url:"deliveryServices,omitempty,brackets"`
OrderMethods []string `url:"orderMethods,omitempty,brackets"`
ShipmentStores []string `url:"shipmentStores,omitempty,brackets"`
Couriers []string `url:"couriers,omitempty,brackets"`
@ -351,6 +352,7 @@ type ProductsFilter struct {
ExternalID string `url:"externalId,omitempty"`
Manufacturer string `url:"manufacturer,omitempty"`
URL string `url:"url,omitempty"`
URLLike string `url:"urlLike,omitempty"`
PriceType string `url:"priceType,omitempty"`
OfferExternalID string `url:"offerExternalId,omitempty"`
Sites []string `url:"sites,omitempty,brackets"`
@ -379,22 +381,22 @@ type ShipmentFilter struct {
// CostsFilter type.
type CostsFilter struct {
MinSumm string `url:"minSumm,omitempty"`
MaxSumm string `url:"maxSumm,omitempty"`
MinSumm int `url:"minSumm,omitempty"`
MaxSumm int `url:"maxSumm,omitempty"`
OrderNumber 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"`
CreatedBy []string `url:"createdBy,omitempty,brackets"`
CreatedBy []int `url:"createdBy,omitempty,brackets"`
CostGroups []string `url:"costGroups,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"`
DateTo string `url:"dateTo,omitempty"`
CreatedAtFrom string `url:"createdAtFrom,omitempty"`
CreatedAtTo string `url:"createdAtTo,omitempty"`
OrderIds []string `url:"orderIds,omitempty,brackets"`
OrderExternalIds []string `url:"orderIds,omitempty,brackets"`
OrderIDs []int `url:"orderIds,omitempty,brackets"`
OrderExternalIDs []string `url:"orderExternalIds,omitempty,brackets"`
}
// FilesFilter type.
@ -448,8 +450,8 @@ type LoyaltyAccountAPIFilter struct {
ID string `url:"id,omitempty"`
Status string `url:"status,,omitempty"`
Customer string `url:"customer,omitempty"`
MinOrderSum string `url:"minOrderSum,omitempty"`
MaxOrderSum string `url:"maxOrderSum,omitempty"`
MinOrderSum string `url:"minOrdersSum,omitempty"`
MaxOrderSum string `url:"maxOrdersSum,omitempty"`
MinAmount string `url:"minAmount,omitempty"`
MaxAmount string `url:"maxAmount,omitempty"`
PhoneNumber string `url:"phoneNumber,omitempty"`
@ -473,3 +475,23 @@ type LoyaltyAPIFilter struct {
Ids []int `url:"ids,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
}
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 {
var i interface{}
var m OrderPayments

View file

@ -44,7 +44,7 @@ func TestAPIErrorsList_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))
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["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))
assert.Len(t, list, 0)
}

View file

@ -194,6 +194,22 @@ type DeliveryShipmentsRequest struct {
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.
type CostsRequest struct {
Filter CostsFilter `url:"filter,omitempty"`
@ -249,7 +265,7 @@ type AccountBonusOperationsRequest struct {
type LoyaltyBonusCreditRequest struct {
Amount float64 `url:"amount"`
ActivationDate string `url:"activationDate,omitempty"`
ExpiredDate string `url:"expiredDate,omitempty"`
ExpiredDate string `url:"expireDate,omitempty"`
Comment string `url:"comment,omitempty"`
}
@ -261,7 +277,7 @@ type LoyaltyBonusStatusDetailsRequest struct {
type LoyaltyAccountsRequest struct {
Limit int `url:"limit,omitempty"`
Page int `url:"limit,omitempty"`
Page int `url:"page,omitempty"`
Filter LoyaltyAccountAPIFilter `url:"filter,omitempty"`
}
@ -284,6 +300,11 @@ type NotificationsSendRequest struct {
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.
func (r ConnectRequest) SystemURL() string {
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))))
}
type OffersRequest struct {
OffersFilter `url:"filter,omitempty"`
}

View file

@ -407,6 +407,12 @@ type ProductsPropertiesResponse struct {
Properties []Property `json:"properties,omitempty"`
}
// CartResponse type.
type CartResponse struct {
SuccessfulResponse
Cart Cart `json:"cart"`
}
// DeliveryShipmentsResponse type.
type DeliveryShipmentsResponse struct {
Success bool `json:"success"`
@ -596,6 +602,34 @@ type AccountBonusOperationsResponse struct {
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 {
SuccessfulResponse
LoyaltyAccount `json:"loyaltyAccount"`
@ -651,3 +685,15 @@ type ActionProductsGroupResponse struct {
SuccessfulResponse
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{
SerializedBaseLoyaltyAccount: SerializedBaseLoyaltyAccount{
PhoneNumber: "89151005004",
CustomFields: []string{"dog"},
CustomFields: []interface{}{"dog"},
},
Customer: SerializedEntityCustomer{
ID: 123,
@ -103,7 +103,9 @@ func getLoyaltyAccountCreateResponse() CreateLoyaltyAccountResponse {
LoyaltyLevel: LoyaltyLevel{},
CreatedAt: "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{},
CreatedAt: "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": {
"id": 123,
"customFields": [],
"customFields": {},
"firstName": "Руслан1",
"lastName": "Ефанов",
"patronymic": ""
@ -154,7 +158,12 @@ func getLoyaltyAccountResponse() string {
"createdAt": "2022-11-24 12:39:37",
"activatedAt": "2022-11-24 12:39:37",
"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": "шт."
}
}
]
}`
}

500
types.go
View file

@ -5,14 +5,28 @@ import (
"net/http"
"reflect"
"strings"
"sync"
"time"
)
// ByID is "id" constant to use as `by` property in methods.
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"
// 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.
type Client struct {
URL string
@ -20,6 +34,15 @@ type Client struct {
Debug bool
httpClient *http.Client
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.
@ -53,22 +76,29 @@ type Address struct {
Text string `json:"text,omitempty"`
}
// GeoID type. Can be empty string.
type GeoID json.Number
// GeoHierarchyRow type.
type GeoHierarchyRow struct {
Country string `json:"country,omitempty"`
Region string `json:"region,omitempty"`
RegionID int `json:"regionId,omitempty"`
RegionID GeoID `json:"regionId,omitempty"`
City string `json:"city,omitempty"`
CityID int `json:"cityId,omitempty"`
CityID GeoID `json:"cityId,omitempty"`
}
// Source type.
type Source struct {
Source string `json:"source,omitempty"`
Medium string `json:"medium,omitempty"`
Campaign string `json:"campaign,omitempty"`
Keyword string `json:"keyword,omitempty"`
Content string `json:"content,omitempty"`
Source string `json:"source,omitempty"`
Medium string `json:"medium,omitempty"`
Campaign string `json:"campaign,omitempty"`
Keyword string `json:"keyword,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.
@ -122,41 +152,41 @@ Customer related types
// Customer type.
type Customer struct {
ID int `json:"id,omitempty"`
ExternalID string `json:"externalId,omitempty"`
FirstName string `json:"firstName,omitempty"`
LastName string `json:"lastName,omitempty"`
Patronymic string `json:"patronymic,omitempty"`
Sex string `json:"sex,omitempty"`
Email string `json:"email,omitempty"`
Phones []Phone `json:"phones,omitempty"`
Address *Address `json:"address,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
Birthday string `json:"birthday,omitempty"`
ManagerID int `json:"managerId,omitempty"`
Vip bool `json:"vip,omitempty"`
Bad bool `json:"bad,omitempty"`
Site string `json:"site,omitempty"`
Source *Source `json:"source,omitempty"`
Contragent *Contragent `json:"contragent,omitempty"`
PersonalDiscount float32 `json:"personalDiscount,omitempty"`
CumulativeDiscount float32 `json:"cumulativeDiscount,omitempty"`
DiscountCardNumber string `json:"discountCardNumber,omitempty"`
EmailMarketingUnsubscribedAt string `json:"emailMarketingUnsubscribedAt,omitempty"`
AvgMarginSumm float32 `json:"avgMarginSumm,omitempty"`
MarginSumm float32 `json:"marginSumm,omitempty"`
TotalSumm float32 `json:"totalSumm,omitempty"`
AverageSumm float32 `json:"averageSumm,omitempty"`
OrdersCount int `json:"ordersCount,omitempty"`
CostSumm float32 `json:"costSumm,omitempty"`
MaturationTime int `json:"maturationTime,omitempty"`
FirstClientID string `json:"firstClientId,omitempty"`
LastClientID string `json:"lastClientId,omitempty"`
BrowserID string `json:"browserId,omitempty"`
MgCustomerID string `json:"mgCustomerId,omitempty"`
PhotoURL string `json:"photoUrl,omitempty"`
CustomFields StringMap `json:"customFields,omitempty"`
Tags []Tag `json:"tags,omitempty"`
ID int `json:"id,omitempty"`
ExternalID string `json:"externalId,omitempty"`
FirstName string `json:"firstName,omitempty"`
LastName string `json:"lastName,omitempty"`
Patronymic string `json:"patronymic,omitempty"`
Sex string `json:"sex,omitempty"`
Email string `json:"email,omitempty"`
Phones []Phone `json:"phones,omitempty"`
Address *Address `json:"address,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
Birthday string `json:"birthday,omitempty"`
ManagerID int `json:"managerId,omitempty"`
Vip bool `json:"vip,omitempty"`
Bad bool `json:"bad,omitempty"`
Site string `json:"site,omitempty"`
Source *Source `json:"source,omitempty"`
Contragent *Contragent `json:"contragent,omitempty"`
PersonalDiscount float32 `json:"personalDiscount,omitempty"`
CumulativeDiscount float32 `json:"cumulativeDiscount,omitempty"`
DiscountCardNumber string `json:"discountCardNumber,omitempty"`
EmailMarketingUnsubscribedAt string `json:"emailMarketingUnsubscribedAt,omitempty"`
AvgMarginSumm float32 `json:"avgMarginSumm,omitempty"`
MarginSumm float32 `json:"marginSumm,omitempty"`
TotalSumm float32 `json:"totalSumm,omitempty"`
AverageSumm float32 `json:"averageSumm,omitempty"`
OrdersCount int `json:"ordersCount,omitempty"`
CostSumm float32 `json:"costSumm,omitempty"`
MaturationTime int `json:"maturationTime,omitempty"`
FirstClientID string `json:"firstClientId,omitempty"`
LastClientID string `json:"lastClientId,omitempty"`
BrowserID string `json:"browserId,omitempty"`
MgCustomerID string `json:"mgCustomerId,omitempty"`
PhotoURL string `json:"photoUrl,omitempty"`
CustomFields CustomFieldMap `json:"customFields,omitempty"`
Tags []Tag `json:"tags,omitempty"`
}
// CorporateCustomer type.
@ -167,7 +197,7 @@ type CorporateCustomer struct {
CreatedAt string `json:"createdAt,omitempty"`
Vip bool `json:"vip,omitempty"`
Bad bool `json:"bad,omitempty"`
CustomFields StringMap `json:"customFields,omitempty"`
CustomFields CustomFieldMap `json:"customFields,omitempty"`
PersonalDiscount float32 `json:"personalDiscount,omitempty"`
DiscountCardNumber string `json:"discountCardNumber,omitempty"`
ManagerID int `json:"managerId,omitempty"`
@ -217,6 +247,15 @@ type CorporateCustomerContactCustomer struct {
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 {
ID int `json:"id,omitempty"`
IsMain bool `json:"isMain,omitempty"`
@ -228,7 +267,7 @@ type Company struct {
CreatedAt string `json:"createdAt,omitempty"`
Contragent *Contragent `json:"contragent,omitempty"`
Address *IdentifiersPair `json:"address,omitempty"`
CustomFields StringMap `json:"customFields,omitempty"`
CustomFields CustomFieldMap `json:"customFields,omitempty"`
}
// CorporateCustomerNote type.
@ -245,15 +284,25 @@ type Phone struct {
// CustomerHistoryRecord type.
type CustomerHistoryRecord struct {
ID int `json:"id,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
Created bool `json:"created,omitempty"`
Deleted bool `json:"deleted,omitempty"`
Source string `json:"source,omitempty"`
Field string `json:"field,omitempty"`
User *User `json:"user,omitempty"`
APIKey *APIKey `json:"apiKey,omitempty"`
Customer *Customer `json:"customer,omitempty"`
ID int `json:"id,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
Created bool `json:"created,omitempty"`
Deleted bool `json:"deleted,omitempty"`
Source string `json:"source,omitempty"`
Field string `json:"field,omitempty"`
OldValue interface{} `json:"oldValue,omitempty"`
NewValue interface{} `json:"newValue,omitempty"`
User *User `json:"user,omitempty"`
APIKey *APIKey `json:"apiKey,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.
@ -264,6 +313,8 @@ type CorporateCustomerHistoryRecord struct {
Deleted bool `json:"deleted,omitempty"`
Source string `json:"source,omitempty"`
Field string `json:"field,omitempty"`
OldValue interface{} `json:"oldValue,omitempty"`
NewValue interface{} `json:"newValue,omitempty"`
User *User `json:"user,omitempty"`
APIKey *APIKey `json:"apiKey,omitempty"`
CorporateCustomer *CorporateCustomer `json:"corporateCustomer,omitempty"`
@ -275,6 +326,7 @@ Order related types
type OrderPayments map[string]OrderPayment
type StringMap map[string]string
type CustomFieldMap map[string]interface{}
type Properties map[string]Property
// Order type.
@ -326,10 +378,61 @@ type Order struct {
Delivery *OrderDelivery `json:"delivery,omitempty"`
Marketplace *OrderMarketplace `json:"marketplace,omitempty"`
Items []OrderItem `json:"items,omitempty"`
CustomFields StringMap `json:"customFields,omitempty"`
CustomFields CustomFieldMap `json:"customFields,omitempty"`
Payments OrderPayments `json:"payments,omitempty"`
ApplyRound *bool `json:"applyRound,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.
@ -382,6 +485,59 @@ type OrderDeliveryData struct {
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.
func (v *OrderDeliveryData) UnmarshalJSON(b []byte) error {
var additionalData map[string]interface{}
@ -465,15 +621,21 @@ type OrderItem struct {
// OrdersHistoryRecord type.
type OrdersHistoryRecord struct {
ID int `json:"id,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
Created bool `json:"created,omitempty"`
Deleted bool `json:"deleted,omitempty"`
Source string `json:"source,omitempty"`
Field string `json:"field,omitempty"`
User *User `json:"user,omitempty"`
APIKey *APIKey `json:"apiKey,omitempty"`
Order *Order `json:"order,omitempty"`
ID int `json:"id,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
Created bool `json:"created,omitempty"`
Deleted bool `json:"deleted,omitempty"`
Source string `json:"source,omitempty"`
Field string `json:"field,omitempty"`
OldValue interface{} `json:"oldValue,omitempty"`
NewValue interface{} `json:"newValue,omitempty"`
User *User `json:"user,omitempty"`
APIKey *APIKey `json:"apiKey,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.
@ -499,14 +661,16 @@ type PackItem struct {
// PacksHistoryRecord type.
type PacksHistoryRecord struct {
ID int `json:"id,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
Created bool `json:"created,omitempty"`
Deleted bool `json:"deleted,omitempty"`
Source string `json:"source,omitempty"`
Field string `json:"field,omitempty"`
User *User `json:"user,omitempty"`
Pack *Pack `json:"pack,omitempty"`
ID int `json:"id,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
Created bool `json:"created,omitempty"`
Deleted bool `json:"deleted,omitempty"`
Source string `json:"source,omitempty"`
Field string `json:"field,omitempty"`
OldValue interface{} `json:"oldValue,omitempty"`
NewValue interface{} `json:"newValue,omitempty"`
User *User `json:"user,omitempty"`
Pack *Pack `json:"pack,omitempty"`
}
// Offer type.
@ -529,6 +693,7 @@ type Offer struct {
Prices []OfferPrice `json:"prices,omitempty"`
Images []string `json:"images,omitempty"`
Unit *Unit `json:"unit,omitempty"`
Product *Product `json:"product,omitempty"`
}
// Inventory type.
@ -558,6 +723,7 @@ type OfferPrice struct {
Price float32 `json:"price,omitempty"`
Ordering int `json:"ordering,omitempty"`
PriceType string `json:"priceType,omitempty"`
Currency string `json:"currency,omitempty"`
}
// OfferPriceUpload type.
@ -597,6 +763,7 @@ type User struct {
CreatedAt string `json:"createdAt,omitempty"`
Active bool `json:"active,omitempty"`
Online bool `json:"online,omitempty"`
Position string `json:"position,omitempty"`
IsAdmin bool `json:"isAdmin,omitempty"`
IsManager bool `json:"isManager,omitempty"`
Email string `json:"email,omitempty"`
@ -712,9 +879,9 @@ type NonWorkingDays struct {
}
type SerializedBaseLoyaltyAccount struct {
PhoneNumber string `json:"phoneNumber,omitempty"`
CardNumber string `json:"cardNumber,omitempty"`
CustomFields []string `json:"customFields,omitempty"`
PhoneNumber string `json:"phoneNumber,omitempty"`
CardNumber string `json:"cardNumber,omitempty"`
CustomFields []interface{} `json:"customFields,omitempty"`
}
type SerializedCreateLoyaltyAccount struct {
@ -726,11 +893,27 @@ type SerializedEditLoyaltyAccount struct {
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.
type Settings struct {
DefaultCurrency SettingsNode `json:"default_currency"`
SystemLanguage SettingsNode `json:"system_language"`
Timezone SettingsNode `json:"timezone"`
MgSettings MgSettings `json:"mg"`
WorkTimes []WorkTime `json:"work_times"`
NonWorkingDays []NonWorkingDays `json:"non_working_days"`
}
@ -794,6 +977,7 @@ type DeliveryType struct {
DeliveryServices []string `json:"deliveryServices,omitempty"`
PaymentTypes []string `json:"paymentTypes,omitempty"` // Deprecated, use DeliveryPaymentTypes
DeliveryPaymentTypes []DeliveryPaymentType `json:"deliveryPaymentTypes,omitempty"`
Currency string `json:"currency,omitempty"`
}
type DeliveryPaymentType struct {
@ -824,8 +1008,8 @@ type LegalEntity struct {
}
type SerializedEntityCustomer struct {
ID int `json:"id,omitempty"`
ExternalID int `json:"externalId,omitempty"`
ID int `json:"id,omitempty"`
ExternalID string `json:"externalId,omitempty"`
}
// OrderMethod type.
@ -883,6 +1067,7 @@ type PriceType struct {
Ordering int `json:"ordering,omitempty"`
Groups []string `json:"groups,omitempty"`
Geo []GeoHierarchyRow `json:"geo,omitempty"`
Currency string `json:"currency,omitempty"`
}
// ProductStatus type.
@ -936,6 +1121,7 @@ type Site struct {
IsDemo bool `json:"isDemo,omitempty"`
CatalogID string `json:"catalogId,omitempty"`
IsCatalogMainSite bool `json:"isCatalogMainSite,omitempty"`
Currency string `json:"currency,omitempty"`
}
// Store type.
@ -967,24 +1153,33 @@ type ProductGroup struct {
// BaseProduct type.
type BaseProduct struct {
Name string `json:"name,omitempty"`
URL string `json:"url,omitempty"`
Article string `json:"article,omitempty"`
ExternalID string `json:"externalId,omitempty"`
Manufacturer string `json:"manufacturer,omitempty"`
Description string `json:"description,omitempty"`
Popular bool `json:"popular,omitempty"`
Stock bool `json:"stock,omitempty"`
Novelty bool `json:"novelty,omitempty"`
Recommended bool `json:"recommended,omitempty"`
Active bool `json:"active,omitempty"`
Markable bool `json:"markable,omitempty"`
Name string `json:"name,omitempty"`
Type ProductType `json:"type,omitempty"`
URL string `json:"url,omitempty"`
Article string `json:"article,omitempty"`
ExternalID string `json:"externalId,omitempty"`
Manufacturer string `json:"manufacturer,omitempty"`
Description string `json:"description,omitempty"`
Popular bool `json:"popular,omitempty"`
Stock bool `json:"stock,omitempty"`
Novelty bool `json:"novelty,omitempty"`
Recommended bool `json:"recommended,omitempty"`
Active bool `json:"active,omitempty"`
Markable bool `json:"markable,omitempty"`
}
type ProductType string
const (
RegularProduct ProductType = "product"
ServiceProduct ProductType = "service"
)
// Product type.
type Product struct {
BaseProduct
ID int `json:"id,omitempty"`
Type ProductType `json:"type"`
MaxPrice float32 `json:"maxPrice,omitempty"`
MinPrice float32 `json:"minPrice,omitempty"`
ImageURL string `json:"imageUrl,omitempty"`
@ -1155,11 +1350,21 @@ type Action struct {
// MgTransport type.
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.
type MgBot struct{}
type MgBot struct {
RefreshToken bool `json:"refreshToken,omitempty"`
}
/**
Cost related types
@ -1220,11 +1425,13 @@ type CustomFields struct {
InGroupActions bool `json:"inGroupActions,omitempty"`
Type string `json:"type,omitempty"`
Entity string `json:"entity,omitempty"`
Default string `json:"default,omitempty"`
Ordering int `json:"ordering,omitempty"`
DisplayArea string `json:"displayArea,omitempty"`
ViewMode string `json:"viewMode,omitempty"`
Dictionary string `json:"dictionary,omitempty"`
// Deprecated: Use DefaultTyped instead.
Default string `json:"default,omitempty"`
Ordering int `json:"ordering,omitempty"`
DisplayArea string `json:"displayArea,omitempty"`
ViewMode string `json:"viewMode,omitempty"`
Dictionary string `json:"dictionary,omitempty"`
DefaultTyped interface{} `json:"default_typed,omitempty"`
}
/**
@ -1251,6 +1458,45 @@ type Activity struct {
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.
type Tag struct {
Name string `json:"name,omitempty"`
@ -1310,22 +1556,22 @@ type DeliveryTypeInfo struct {
// LoyaltyAccount type.
type LoyaltyAccount struct {
Active bool `json:"active"`
ID int `json:"id"`
PhoneNumber string `json:"phoneNumber,omitempty"`
CardNumber string `json:"cardNumber,omitempty"`
Amount float64 `json:"amount,omitempty"`
LoyaltyLevel LoyaltyLevel `json:"level,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
ActivatedAt string `json:"activatedAt,omitempty"`
ConfirmedPhoneAt string `json:"confirmedPhoneAt,omitempty"`
LastCheckID int `json:"lastCheckId,omitempty"`
CustomFields []string `json:"customFields,omitempty"`
Loyalty Loyalty `json:"loyalty,omitempty"`
Customer Customer `json:"customer,omitempty"`
Status string `json:"status,omitempty"`
OrderSum float64 `json:"orderSum,omitempty"`
NextLevelSum float64 `json:"nextLevelSum,omitempty"`
Active bool `json:"active"`
ID int `json:"id"`
PhoneNumber string `json:"phoneNumber,omitempty"`
CardNumber string `json:"cardNumber,omitempty"`
Amount float64 `json:"amount,omitempty"`
LoyaltyLevel LoyaltyLevel `json:"level,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
ActivatedAt string `json:"activatedAt,omitempty"`
ConfirmedPhoneAt string `json:"confirmedPhoneAt,omitempty"`
LastCheckID int `json:"lastCheckId,omitempty"`
CustomFields CustomFieldMap `json:"customFields,omitempty"`
Loyalty Loyalty `json:"loyalty,omitempty"`
Customer Customer `json:"customer,omitempty"`
Status string `json:"status,omitempty"`
OrderSum float64 `json:"orderSum,omitempty"`
NextLevelSum float64 `json:"nextLevelSum,omitempty"`
}
// Loyalty type.
@ -1341,6 +1587,7 @@ type Loyalty struct {
ActivatedAt string `json:"activatedAt,omitempty"`
DeactivatedAt string `json:"deactivatedAt,omitempty"`
BlockedAt string `json:"blockedAt,omitempty"`
Currency string `json:"currency,omitempty"`
}
// LoyaltyLevel type.
@ -1384,6 +1631,7 @@ type SerializedLoyaltyOrder struct {
Delivery Delivery `json:"delivery,omitempty"`
Site string `json:"site,omitempty"`
Items []LoyaltyItems `json:"items,omitempty"`
Currency string `json:"currency,omitempty"`
}
type LoyaltyEventDiscount struct {
@ -1441,3 +1689,31 @@ type ExternalID struct {
type UserGroupType 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"`
}