diff --git a/ozon/ozon.go b/ozon/ozon.go index aa3a9a0..ea871ba 100644 --- a/ozon/ozon.go +++ b/ozon/ozon.go @@ -44,6 +44,7 @@ type Client struct { passes *Passes clusters *Clusters quants *Quants + reviews *Reviews } func (c Client) Analytics() *Analytics { @@ -134,6 +135,10 @@ func (c Client) Quants() *Quants { return c.quants } +func (c Client) Reviews() *Reviews { + return c.reviews +} + type ClientOption func(c *ClientOptions) func WithHttpClient(httpClient core.HttpClient) ClientOption { @@ -200,6 +205,7 @@ func NewClient(opts ...ClientOption) *Client { passes: &Passes{client: coreClient}, clusters: &Clusters{client: coreClient}, quants: &Quants{client: coreClient}, + reviews: &Reviews{client: coreClient}, } } @@ -230,5 +236,6 @@ func NewMockClient(handler http.HandlerFunc) *Client { passes: &Passes{client: coreClient}, clusters: &Clusters{client: coreClient}, quants: &Quants{client: coreClient}, + reviews: &Reviews{client: coreClient}, } } diff --git a/ozon/reviews.go b/ozon/reviews.go new file mode 100644 index 0000000..db65154 --- /dev/null +++ b/ozon/reviews.go @@ -0,0 +1,334 @@ +package ozon + +import ( + "context" + "net/http" + "time" + + core "github.com/diphantxm/ozon-api-client" +) + +type Reviews struct { + client *core.Client +} + +type LeaveCommentParams struct { + // Review status update + MarkReviewAsProcesses bool `json:"mark_review_as_processed"` + + // Identifier of the parent comment you're replying to + ParentCommentId string `json:"parent_comment_id"` + + // Review identifier + ReviewId string `json:"review_id"` + + // Comment text + Text string `json:"text"` +} + +type LeaveCommentResponse struct { + core.CommonResponse + + // Comment identifier + CommentId string `json:"comment_id"` +} + +// Only available to sellers with the Premium Plus subscription +func (c Reviews) LeaveComment(ctx context.Context, params *LeaveCommentParams) (*LeaveCommentResponse, error) { + url := "/v1/review/comment/create" + + resp := &LeaveCommentResponse{} + + response, err := c.client.Request(ctx, http.MethodPost, url, params, resp, nil) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type DeleteCommentParams struct { + // Comment identifier + CommentId string `json:"comment_id"` +} + +type DeleteCommentResponse struct { + core.CommonResponse +} + +// Only available to sellers with the Premium Plus subscription +func (c Reviews) DeleteComment(ctx context.Context, params *DeleteCommentParams) (*DeleteCommentResponse, error) { + url := "/v1/review/comment/delete" + + resp := &DeleteCommentResponse{} + + response, err := c.client.Request(ctx, http.MethodPost, url, params, resp, nil) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type ListCommentsParams struct { + // Limit of values in the response. Minimum is 20. Maximum is 100 + Limit int32 `json:"limit"` + + // Number of elements that is skipped in the response. + // For example, if offset = 10, the response starts with the 11th element found + Offset int32 `json:"offset"` + + // Review identifier + ReviewId string `json:"review_id"` + + // Sorting direction + SortDir Order `json:"sort_dir"` +} + +type ListCommentsResponse struct { + core.CommonResponse + + // Number of elements in the response + Offset int32 `json:"offset"` + + // Comment details + Comments []Comment `json:"comments"` +} + +type Comment struct { + // Comment identifier + Id string `json:"id"` + + // true, if the comment was left by an official, false if a customer left it + IsOfficial bool `json:"is_official"` + + // true, if the comment was left by a seller, false if a customer left it + IsOwner bool `json:"is_owner"` + + // Identifier of the parent comment to reply to + ParentCommentId string `json:"parent_comment_id"` + + // Date the comment was published + PublishedAt time.Time `json:"published_at"` + + // Comment text + Text string `json:"text"` +} + +// Only available to sellers with the Premium Plus subscription +// +// Method returns information about comments on reviews that have passed moderation +func (c Reviews) ListComments(ctx context.Context, params *ListCommentsParams) (*ListCommentsResponse, error) { + url := "/v1/review/comment/list" + + resp := &ListCommentsResponse{} + + response, err := c.client.Request(ctx, http.MethodPost, url, params, resp, nil) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +// Only available to sellers with the Premium Plus subscription +type ChangeStatusParams struct { + // Array with review identifiers from 1 to 100 + ReviewIds []string `json:"review_ids"` + + // Review status + Status string `json:"status"` +} + +type ChangeStatusResponse struct { + core.CommonResponse +} + +// Only available to sellers with the Premium Plus subscription +func (c Reviews) ChangeStatus(ctx context.Context, params *ChangeStatusParams) (*ChangeStatusResponse, error) { + url := "/v1/review/change-status" + + resp := &ChangeStatusResponse{} + + response, err := c.client.Request(ctx, http.MethodPost, url, params, resp, nil) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type CountReviewsResponse struct { + core.CommonResponse + + // Number of processed review + Processed int32 `json:"processed"` + + // Number of all reviews + Total int32 `json:"total"` + + // Number of unprocessed reviews + Unprocessed int32 `json:"unprocessed"` +} + +// Only available to sellers with the Premium Plus subscription +func (c Reviews) Count(ctx context.Context) (*CountReviewsResponse, error) { + url := "/v1/review/count" + + resp := &CountReviewsResponse{} + + response, err := c.client.Request(ctx, http.MethodPost, url, nil, resp, nil) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type GetReviewParams struct { + // Review identifier + ReviewId string `json:"review_id"` +} + +type GetReviewResponse struct { + core.CommonResponse + + ReviewDetails + + // Number of dislikes on the review + DislikesAmount int32 `json:"dislikes_amount"` + + // Number of likes on the review + LikesAmount int32 `json:"likes_amount"` + + // Image details + Photos []ReviewPhoto `json:"photos"` + + // Video details + Videos []ReviewVideo `json:"videos"` +} + +type ReviewDetails struct { + // Number of comments on the review + CommentsAmount int32 `json:"comments_amount"` + + // Review identifier + Id string `json:"id"` + + // true, if the review affects the rating calculation + IsRatingParticipant bool `json:"is_rating_participant"` + + // Status of the order for which the customer left a review + OrderStatus string `json:"order_status"` + + // Number of images in the review + PhotosAmount int32 `json:"photos_amount"` + + // Review publication date + PublishedAt time.Time `json:"published_at"` + + // Review rating + Rating int32 `json:"rating"` + + // Product identifier in the Ozon system, SKU + SKU int64 `json:"sku"` + + // Review status + Status string `json:"status"` + + // Review text + Text string `json:"text"` + + // Number of videos for the review + VideosAmount int32 `json:"videos_amount"` +} + +type ReviewPhoto struct { + // Height + Height int32 `json:"height"` + + // Link to image + URL string `json:"url"` + + // Width + Width int32 `json:"width"` +} + +type ReviewVideo struct { + // Height + Height int64 `json:"height"` + + // Link to video preview + PreviewURL string `json:"preview_url"` + + // Link to short video + ShortVideoPreviewURL string `json:"short_video_preview_url"` + + // Video link + URL string `json:"url"` + + // Width + Width int64 `json:"width"` +} + +// Only available to sellers with the Premium Plus subscription +func (c Reviews) Get(ctx context.Context, params *GetReviewParams) (*GetReviewResponse, error) { + url := "/v1/review/info" + + resp := &GetReviewResponse{} + + response, err := c.client.Request(ctx, http.MethodPost, url, nil, resp, nil) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} + +type ListReviewsParams struct { + // Identifier of the last review on the page + LastId string `json:"last_id"` + + // Number of reviews in the response. Minimum is 20, maximum is 100 + Limit int32 `json:"limit"` + + // Sorting direction + SortDir Order `json:"sort_dir"` + + // Review statuses + Status string `json:"status"` +} + +type ListReviewsResponse struct { + core.CommonResponse + + // true, if not all reviews were returned in the response + HasNext bool `json:"has_next"` + + // Identifier of the last review on the page + LastId string `json:"last_id"` + + // Review details + Reviews []ReviewDetails `json:"reviews"` +} + +// Only available to sellers with the Premium Plus subscription +func (c Reviews) List(ctx context.Context, params *ListReviewsParams) (*ListReviewsResponse, error) { + url := "/v1/review/list" + + resp := &ListReviewsResponse{} + + response, err := c.client.Request(ctx, http.MethodPost, url, nil, resp, nil) + if err != nil { + return nil, err + } + response.CopyCommonResponse(&resp.CommonResponse) + + return resp, nil +} diff --git a/ozon/reviews_test.go b/ozon/reviews_test.go new file mode 100644 index 0000000..9038a0c --- /dev/null +++ b/ozon/reviews_test.go @@ -0,0 +1,416 @@ +package ozon + +import ( + "context" + "net/http" + "testing" + + core "github.com/diphantxm/ozon-api-client" +) + +func TestLeaveComment(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *LeaveCommentParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &LeaveCommentParams{ + MarkReviewAsProcesses: true, + ParentCommentId: "string", + ReviewId: "string1", + Text: "some string", + }, + `{ + "comment_id": "string" + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &LeaveCommentParams{}, + `{ + "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)) + + ctx, _ := context.WithTimeout(context.Background(), testTimeout) + resp, err := c.Reviews().LeaveComment(ctx, test.params) + if err != nil { + t.Error(err) + continue + } + + compareJsonResponse(t, test.response, &LeaveCommentResponse{}) + + if resp.StatusCode != test.statusCode { + t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode) + } + } +} + +func TestDeleteComment(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *DeleteCommentParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &DeleteCommentParams{ + CommentId: "string", + }, + `{}`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &DeleteCommentParams{}, + `{ + "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)) + + ctx, _ := context.WithTimeout(context.Background(), testTimeout) + resp, err := c.Reviews().DeleteComment(ctx, test.params) + if err != nil { + t.Error(err) + continue + } + + compareJsonResponse(t, test.response, &DeleteCommentResponse{}) + + if resp.StatusCode != test.statusCode { + t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode) + } + } +} + +func TestListComments(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *ListCommentsParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &ListCommentsParams{ + Limit: 0, + Offset: 0, + ReviewId: "string", + SortDir: Ascending, + }, + `{ + "comments": [ + { + "id": "string", + "is_official": true, + "is_owner": true, + "parent_comment_id": "string", + "published_at": "2019-08-24T14:15:22Z", + "text": "string" + } + ], + "offset": 0 + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &ListCommentsParams{}, + `{ + "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)) + + ctx, _ := context.WithTimeout(context.Background(), testTimeout) + resp, err := c.Reviews().ListComments(ctx, test.params) + if err != nil { + t.Error(err) + continue + } + + compareJsonResponse(t, test.response, &ListCommentsResponse{}) + + if resp.StatusCode != test.statusCode { + t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode) + } + } +} + +func TestChangeStatus(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *ChangeStatusParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &ChangeStatusParams{ + ReviewIds: []string{"string"}, + Status: "PROCESSED", + }, + `{}`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &ChangeStatusParams{}, + `{ + "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)) + + ctx, _ := context.WithTimeout(context.Background(), testTimeout) + resp, err := c.Reviews().ChangeStatus(ctx, test.params) + if err != nil { + t.Error(err) + continue + } + + compareJsonResponse(t, test.response, &ChangeStatusResponse{}) + + if resp.StatusCode != test.statusCode { + t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode) + } + } +} + +func TestCountReviews(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + `{ + "processed": 2, + "total": 3, + "unprocessed": 1 + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + `{ + "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)) + + ctx, _ := context.WithTimeout(context.Background(), testTimeout) + resp, err := c.Reviews().Count(ctx) + if err != nil { + t.Error(err) + continue + } + + compareJsonResponse(t, test.response, &CountReviewsResponse{}) + + if resp.StatusCode != test.statusCode { + t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode) + } + } +} + +func TestGetReview(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *GetReviewParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &GetReviewParams{ + ReviewId: "string", + }, + `{ + "comments_amount": 0, + "dislikes_amount": 0, + "id": "string", + "is_rating_participant": true, + "likes_amount": 0, + "order_status": "string", + "photos": [ + { + "height": 0, + "url": "string", + "width": 0 + } + ], + "photos_amount": 0, + "published_at": "2019-08-24T14:15:22Z", + "rating": 0, + "sku": 0, + "status": "string", + "text": "string", + "videos": [ + { + "height": 0, + "preview_url": "string", + "short_video_preview_url": "string", + "url": "string", + "width": 0 + } + ], + "videos_amount": 0 + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &GetReviewParams{}, + `{ + "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)) + + ctx, _ := context.WithTimeout(context.Background(), testTimeout) + resp, err := c.Reviews().Get(ctx, test.params) + if err != nil { + t.Error(err) + continue + } + + compareJsonResponse(t, test.response, &GetReviewResponse{}) + + if resp.StatusCode != test.statusCode { + t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode) + } + } +} + +func TestListReviews(t *testing.T) { + t.Parallel() + + tests := []struct { + statusCode int + headers map[string]string + params *ListReviewsParams + response string + }{ + // Test Ok + { + http.StatusOK, + map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"}, + &ListReviewsParams{ + LastId: "string", + Limit: 0, + SortDir: Ascending, + Status: "ALL", + }, + `{ + "has_next": true, + "last_id": "string", + "reviews": [ + { + "comments_amount": 0, + "id": "string", + "is_rating_participant": true, + "order_status": "string", + "photos_amount": 0, + "published_at": "2019-08-24T14:15:22Z", + "rating": 0, + "sku": 0, + "status": "string", + "text": "string", + "videos_amount": 0 + } + ] + }`, + }, + // Test No Client-Id or Api-Key + { + http.StatusUnauthorized, + map[string]string{}, + &ListReviewsParams{}, + `{ + "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)) + + ctx, _ := context.WithTimeout(context.Background(), testTimeout) + resp, err := c.Reviews().List(ctx, test.params) + if err != nil { + t.Error(err) + continue + } + + compareJsonResponse(t, test.response, &ListReviewsResponse{}) + + if resp.StatusCode != test.statusCode { + t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode) + } + } +}