add methods for chats with customers

This commit is contained in:
diPhantxm
2023-03-23 22:45:37 +03:00
parent 3f1f0711c8
commit 89816cb239
4 changed files with 813 additions and 7 deletions

View File

@@ -138,13 +138,13 @@
- [x] Reject a rFBS cancellation request
## Chats with customers
- [ ] Chats list
- [ ] Send message
- [ ] Send file
- [ ] Chat history
- [ ] Update chat
- [ ] Create a new chat
- [ ] Mark messages as read
- [x] Chats list
- [x] Send message
- [x] Send file
- [x] Chat history
- [x] Update chat
- [x] Create a new chat
- [x] Mark messages as read
## Invoices
- [x] Create or edit proforma invoice link

390
ozon/chats.go Normal file
View File

@@ -0,0 +1,390 @@
package ozon
import (
"net/http"
"time"
core "github.com/diphantxm/ozon-api-client"
)
type Chats struct {
client *core.Client
}
type ListChatsParams struct {
// Chats filter
Filter ListChatsFilter `json:"filter"`
// Number of values in the response. Default value is 1
Limit int64 `json:"limit" default:"1"`
// Number of elements that will be skipped in the response.
// For example, if offset=10, the response will start with the 11th element found
Offset int64 `json:"offset"`
}
type ListChatsFilter struct {
// Filter by chat status:
// - All
// - Opened
// - Closed
ChatStatus string `json:"chat_status" default:"ALL"`
// Filter by chats with unread messages
UnreadOnly bool `json:"unread_only"`
}
type ListChatsResponse struct {
core.CommonResponse
// Chats data
Chats []struct {
// Chat data
Chat struct {
// Chat identifier
ChatId string `json:"chat_id"`
// Chat status:
// - All
// - Opened
// - Closed
ChatStatus string `json:"chat_status"`
// Chat type:
// - Seller_Support — support chat
// - Buyer_Seller — chat with a customer
ChatType string `json:"chat_type"`
// Chat creation date
CreatedAt time.Time `json:"created_at"`
} `json:"chat"`
// Identifier of the first unread chat message
FirstUnreadMessageId string `json:"first_unread_message_id"`
// Identifier of the last message in the chat
LastMessageId string `json:"last_message_id"`
// Number of unread messages in the chat
UnreadCount int64 `json:"unread_count"`
} `json:"chats"`
// Total number of chats
TotalChatsCount int64 `json:"total_chats_count"`
// Total number of unread messages
TotalUnreadCount int64 `json:"total_unread_count"`
}
// Returns information about chats by specified filters
func (c Chats) List(params *ListChatsParams) (*ListChatsResponse, error) {
url := "/v2/chat/list"
resp := &ListChatsResponse{}
response, err := c.client.Request(http.MethodPost, url, params, resp)
if err != nil {
return nil, err
}
response.CopyCommonResponse(&resp.CommonResponse)
return resp, nil
}
type SendMessageParams struct {
// Chat identifier
ChatId string `json:"chat_id"`
// Message text in the plain text format
Text string `json:"text"`
}
type SendMessageResponse struct {
core.CommonResponse
// Method result
Result string `json:"result"`
}
// Sends a message to an existing chat by its identifier
func (c Chats) SendMessage(params *SendMessageParams) (*SendMessageResponse, error) {
url := "/v1/chat/send/message"
resp := &SendMessageResponse{}
response, err := c.client.Request(http.MethodPost, url, params, resp)
if err != nil {
return nil, err
}
response.CopyCommonResponse(&resp.CommonResponse)
return resp, nil
}
type SendFileParams struct {
// File as a base64 string
Base64Content string `json:"base64_content"`
// Chat identifier
ChatId string `json:"chat_id"`
// File name with extension
Name string `json:"name"`
}
type SendFileResponse struct {
core.CommonResponse
// Method result
Result string `json:"result"`
}
// Sends a file to an existing chat by its identifier
func (c Chats) SendFile(params *SendFileParams) (*SendFileResponse, error) {
url := "/v1/chat/send/file"
resp := &SendFileResponse{}
response, err := c.client.Request(http.MethodPost, url, params, resp)
if err != nil {
return nil, err
}
response.CopyCommonResponse(&resp.CommonResponse)
return resp, nil
}
type ChatHistoryParams struct {
// Chat idenitifier
ChatId string `json:"chat_id"`
// Messages sorting direction:
// - Forward—from old messages to new ones.
// - Backward—from new messages to old ones.
// The default value is `Backward`. You can set the number of messages in the limit parameter
Direction string `json:"direction" default:"Backward"`
// Identifier of the message from which the chat history will be displayed.
// Default value is the last visible message
FromMessageId string `json:"from_message_id"`
// Number of messages in the response. The default value is 50
Limit int64 `json:"limit" default:"50"`
}
type ChatHistoryResponse struct {
core.CommonResponse
// Indicates that the response returned only a part of messages
HasNext bool `json:"has_next"`
// An array of messages sorted according to the direction parameter in the request body
Messages []struct {
// Message creation date
CreatedAt time.Time `json:"created_at"`
// Array with message content in Markdown format
Data []string `json:"data"`
// Indication of the read message
IsRead bool `json:"is_read"`
// Message identifier
MessageId string `json:"message_id"`
// Chat participant identifier
User struct {
// Chat participant identifier
Id string `json:"id"`
// Chat participant type:
// - customer
// - seller
// - crm—system messages
// - courier
// - support
Type string `json:"type"`
} `json:"user"`
} `json:"messages"`
}
// Chat history
func (c Chats) History(params *ChatHistoryParams) (*ChatHistoryResponse, error) {
url := "/v2/chat/history"
resp := &ChatHistoryResponse{}
response, err := c.client.Request(http.MethodPost, url, params, resp)
if err != nil {
return nil, err
}
response.CopyCommonResponse(&resp.CommonResponse)
return resp, nil
}
type UpdateChatParams struct {
// Chat identifier
ChatId string `json:"chat_id"`
// Message identifier
FromMessageId uint64 `json:"from_message_id"`
// Number of messages in the response
Limit int64 `json:"limit"`
}
type UpdateChatResponse struct {
core.CommonResponse
// Method result
Result []struct {
// An order or a product user wrote about in the chat
Context struct {
// Product inforamtion
Item struct {
// Product identifier in the Ozon system, SKU
SKU int64 `json:"sku"`
} `json:"item"`
// Order information
Order struct {
// Order number
OrderNumber string `json:"order_number"`
// Shipment information
Postings []struct {
// Delivery scheme:
// - FBO
// - FBS
// - RFBS
// - Crossborder
DeliverySchema string `json:"delivery_schema"`
// Shipment number
PostingNumber string `json:"posting_number"`
// List of product identifiers in the shipment
SKUList []int64 `json:"sku_list"`
} `json:"postings"`
} `json:"order"`
} `json:"context"`
// Creation date and time
CreatedAt time.Time `json:"created_at"`
// Information about the file in the chat. Displayed only for `type = file`
File struct {
// File type
Mime string `json:"mime"`
// File name
Name string `json:"name"`
// File size in bytes
Size int64 `json:"size"`
// File URL
URL string `json:"url"`
} `json:"file"`
// File identifier
Id uint64 `json:"id"`
// Message. Displayed only for `type = text`
Text string `json:"text"`
// Message type:
// - text
// - file
Type string `json:"type"`
// Chat participant information
User struct {
// Chat participant identifier
Id string `json:"id"`
// Chat participant chat:
// - customer
// - seller
// - crm—system messages
// - courier
Type string `json:"type"`
} `json:"user"`
} `json:"result"`
}
// Update chat
func (c Chats) Update(params *UpdateChatParams) (*UpdateChatResponse, error) {
url := "/v1/chat/updates"
resp := &UpdateChatResponse{}
response, err := c.client.Request(http.MethodPost, url, params, resp)
if err != nil {
return nil, err
}
response.CopyCommonResponse(&resp.CommonResponse)
return resp, nil
}
type CreateNewChatParams struct {
// Shipment identifier
PostingNumber string `json:"posting_number"`
}
type CreateNewChatResponse struct {
core.CommonResponse
//Method result
Result struct {
// Chat identifier
ChatId string `json:"chat_id"`
} `json:"result"`
}
// Creates a new chat on the shipment with the customer. For example, to clarify the address or the product model
func (c Chats) Create(params *CreateNewChatParams) (*CreateNewChatResponse, error) {
url := "/v1/chat/start"
resp := &CreateNewChatResponse{}
response, err := c.client.Request(http.MethodPost, url, params, resp)
if err != nil {
return nil, err
}
response.CopyCommonResponse(&resp.CommonResponse)
return resp, nil
}
type MarkAsReadParams struct {
// Chat identifier
Chatid string `json:"chat_id"`
// Message identifier
FromMessageId uint64 `json:"from_message_id"`
}
type MarkAsReadResponse struct {
core.CommonResponse
// Number of unread messages in the chat
UnreadCount int64 `json:"unread_count"`
}
// A method for marking the selected message and messages before it as read
func (c Chats) MarkAsRead(params *MarkAsReadParams) (*MarkAsReadResponse, error) {
url := "/v2/chat/read"
resp := &MarkAsReadResponse{}
response, err := c.client.Request(http.MethodPost, url, params, resp)
if err != nil {
return nil, err
}
response.CopyCommonResponse(&resp.CommonResponse)
return resp, nil
}

409
ozon/chats_test.go Normal file
View File

@@ -0,0 +1,409 @@
package ozon
import (
"net/http"
"testing"
core "github.com/diphantxm/ozon-api-client"
)
func TestListChats(t *testing.T) {
t.Parallel()
tests := []struct {
statusCode int
headers map[string]string
params *ListChatsParams
response string
}{
// Test Ok
{
http.StatusOK,
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
&ListChatsParams{
Filter: ListChatsFilter{
ChatStatus: "Opened",
UnreadOnly: true,
},
Limit: 1,
Offset: 0,
},
`{
"chats": [
{
"chat": {
"created_at": "2022-07-22T08:07:19.581Z",
"chat_id": "5e767w03-b400-4y1b-a841-75319ca8a5c8",
"chat_status": "Opened",
"chat_type": "Seller_Support"
},
"first_unread_message_id": "3000000000118021931",
"last_message_id": "30000000001280042740",
"unread_count": 1
}
],
"total_chats_count": 25,
"total_unread_count": 5
}`,
},
// Test No Client-Id or Api-Key
{
http.StatusUnauthorized,
map[string]string{},
&ListChatsParams{},
`{
"code": 16,
"message": "Client-Id and Api-Key headers are required"
}`,
},
}
for _, test := range tests {
c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers))
resp, err := c.Chats().List(test.params)
if err != nil {
t.Error(err)
}
if resp.StatusCode != test.statusCode {
t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode)
}
if resp.StatusCode == http.StatusOK {
if len(resp.Chats) > 0 {
if resp.Chats[0].Chat.ChatStatus == "" {
t.Errorf("Chat status cannot be empty")
}
if resp.Chats[0].Chat.ChatType == "" {
t.Errorf("Chat type cannot be empty")
}
}
}
}
}
func TestSendMessage(t *testing.T) {
t.Parallel()
tests := []struct {
statusCode int
headers map[string]string
params *SendMessageParams
response string
}{
// Test Ok
{
http.StatusOK,
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
&SendMessageParams{
ChatId: "99feb3fc-a474-469f-95d5-268b470cc607",
Text: "test",
},
`{
"result": "success"
}`,
},
// Test No Client-Id or Api-Key
{
http.StatusUnauthorized,
map[string]string{},
&SendMessageParams{},
`{
"code": 16,
"message": "Client-Id and Api-Key headers are required"
}`,
},
}
for _, test := range tests {
c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers))
resp, err := c.Chats().SendMessage(test.params)
if err != nil {
t.Error(err)
}
if resp.StatusCode != test.statusCode {
t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode)
}
}
}
func TestSendFile(t *testing.T) {
t.Parallel()
tests := []struct {
statusCode int
headers map[string]string
params *SendFileParams
response string
}{
// Test Ok
{
http.StatusOK,
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
&SendFileParams{
ChatId: "99feb3fc-a474-469f-95d5-268b470cc607",
Name: "tempor",
Base64Content: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
},
`{
"result": "success"
}`,
},
// Test No Client-Id or Api-Key
{
http.StatusUnauthorized,
map[string]string{},
&SendFileParams{},
`{
"code": 16,
"message": "Client-Id and Api-Key headers are required"
}`,
},
}
for _, test := range tests {
c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers))
resp, err := c.Chats().SendFile(test.params)
if err != nil {
t.Error(err)
}
if resp.StatusCode != test.statusCode {
t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode)
}
}
}
func TestChatHistory(t *testing.T) {
t.Parallel()
tests := []struct {
statusCode int
headers map[string]string
params *ChatHistoryParams
response string
}{
// Test Ok
{
http.StatusOK,
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
&ChatHistoryParams{
ChatId: "18b8e1f9-4ae7-461c-84ea-8e1f54d1a45e",
Direction: "Forward",
FromMessageId: "3000000000118032000",
Limit: 1,
},
`{
"has_next": true,
"messages": [
{
"message_id": "3000000000817031942",
"user": {
"id": "115568",
"type": "Сustomer"
},
"created_at": "2022-07-18T20:58:04.528Z",
"is_read": true,
"data": [
"Здравствуйте, у меня вопрос по вашему товару \"Стекло защитное для смартфонов\", артикул 11223. Подойдет ли он на данную [ модель ](https://www.ozon.ru/product/smartfon-samsung-galaxy-a03s-4-64-gb-chernyy) телефона?"
]
}
]
}`,
},
// Test No Client-Id or Api-Key
{
http.StatusUnauthorized,
map[string]string{},
&ChatHistoryParams{},
`{
"code": 16,
"message": "Client-Id and Api-Key headers are required"
}`,
},
}
for _, test := range tests {
c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers))
resp, err := c.Chats().History(test.params)
if err != nil {
t.Error(err)
}
if resp.StatusCode != test.statusCode {
t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode)
}
}
}
func TestUpdateChat(t *testing.T) {
t.Parallel()
tests := []struct {
statusCode int
headers map[string]string
params *UpdateChatParams
response string
}{
// Test Ok
{
http.StatusOK,
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
&UpdateChatParams{
ChatId: "99feb3fc-a474-469f-95d5-268b470cc607",
FromMessageId: 0,
Limit: 1000,
},
`{
"result": [
{
"id": 3000000000012735500,
"user": {
"id": "15",
"type": "seller"
},
"type": "file",
"text": "",
"file": {
"url": "https://cdn-stg.ozonru.me/s3/fs-chat-api/108a0370-4dfa-11ec-bd02-06a332735108.png",
"mime": "image/png",
"size": 68,
"name": "tempor"
},
"created_at": "2021-11-25T14:14:55Z",
"context": null
}
]
}`,
},
// Test No Client-Id or Api-Key
{
http.StatusUnauthorized,
map[string]string{},
&UpdateChatParams{},
`{
"code": 16,
"message": "Client-Id and Api-Key headers are required"
}`,
},
}
for _, test := range tests {
c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers))
resp, err := c.Chats().Update(test.params)
if err != nil {
t.Error(err)
}
if resp.StatusCode != test.statusCode {
t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode)
}
}
}
func TestCreateNewChat(t *testing.T) {
t.Parallel()
tests := []struct {
statusCode int
headers map[string]string
params *CreateNewChatParams
response string
}{
// Test Ok
{
http.StatusOK,
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
&CreateNewChatParams{
PostingNumber: "47873153-0052-1",
},
`{
"result": {
"chat_id": "5969c331-2e64-44b7-8a0e-ff9526762c62"
}
}`,
},
// Test No Client-Id or Api-Key
{
http.StatusUnauthorized,
map[string]string{},
&CreateNewChatParams{},
`{
"code": 16,
"message": "Client-Id and Api-Key headers are required"
}`,
},
}
for _, test := range tests {
c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers))
resp, err := c.Chats().Create(test.params)
if err != nil {
t.Error(err)
}
if resp.StatusCode != test.statusCode {
t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode)
}
if resp.StatusCode == http.StatusOK {
if resp.Result.ChatId == "" {
t.Errorf("Chat id cannot be empty")
}
}
}
}
func TestMarkAsRead(t *testing.T) {
t.Parallel()
tests := []struct {
statusCode int
headers map[string]string
params *MarkAsReadParams
response string
}{
// Test Ok
{
http.StatusOK,
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
&MarkAsReadParams{
Chatid: "99feb3fc-a474-469f-95d5-268b470cc607",
FromMessageId: 3000000000118032000,
},
`{
"unread_count": 0
}`,
},
// Test No Client-Id or Api-Key
{
http.StatusUnauthorized,
map[string]string{},
&MarkAsReadParams{},
`{
"code": 16,
"message": "Client-Id and Api-Key headers are required"
}`,
},
}
for _, test := range tests {
c := NewMockClient(core.NewMockHttpHandler(test.statusCode, test.response, test.headers))
resp, err := c.Chats().MarkAsRead(test.params)
if err != nil {
t.Error(err)
}
if resp.StatusCode != test.statusCode {
t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode)
}
}
}

View File

@@ -28,6 +28,7 @@ type Client struct {
polygons *Polygons
invoices *Invoices
brands *Brands
chats *Chats
}
func (c Client) Analytics() *Analytics {
@@ -90,6 +91,10 @@ func (c Client) Brands() *Brands {
return c.brands
}
func (c Client) Chats() *Chats {
return c.chats
}
func NewClient(clientId, apiKey string) *Client {
coreClient := core.NewClient(DefaultAPIBaseUrl, map[string]string{
"Client-Id": clientId,
@@ -113,6 +118,7 @@ func NewClient(clientId, apiKey string) *Client {
polygons: &Polygons{client: coreClient},
invoices: &Invoices{client: coreClient},
brands: &Brands{client: coreClient},
chats: &Chats{client: coreClient},
}
}
@@ -136,5 +142,6 @@ func NewMockClient(handler http.HandlerFunc) *Client {
polygons: &Polygons{client: coreClient},
invoices: &Invoices{client: coreClient},
brands: &Brands{client: coreClient},
chats: &Chats{client: coreClient},
}
}