Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23ca98fedd | ||
|
|
eae6f54e71 | ||
|
|
8a6cd20b95 | ||
|
|
1706575a34 | ||
|
|
3430ead143 | ||
|
|
2164eff0a6 | ||
|
|
b6af642636 | ||
|
|
67898a4738 | ||
|
|
8c07540d28 |
@@ -219,3 +219,54 @@ func (c Analytics) GetStocksOnWarehouses(ctx context.Context, params *GetStocksO
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type GetProductTurnoverParams struct {
|
||||
// Number of values in the response
|
||||
Limit int64 `json:"limit"`
|
||||
|
||||
// Number of elements to skip in the response.
|
||||
//
|
||||
// For example, if offset = 10, the response starts with the 11th element found
|
||||
Offset int32 `json:"offset"`
|
||||
|
||||
// Product identifiers in the Ozon system, SKU
|
||||
SKU []string `json:"sku"`
|
||||
}
|
||||
|
||||
type GetProductTurnoverResponse struct {
|
||||
core.CommonResponse
|
||||
|
||||
// Products
|
||||
Items []ProductTurnoverItem `json:"items"`
|
||||
}
|
||||
|
||||
type ProductTurnoverItem struct {
|
||||
// Average daily number of product items sold over the last 60 days
|
||||
Ads float64 `json:"ads"`
|
||||
|
||||
// Product stock, pcs
|
||||
CurrentStock int64 `json:"current_stock"`
|
||||
|
||||
// Number of days the stock will last based on your average daily sales
|
||||
IDC float64 `json:"idc"`
|
||||
|
||||
// Product stock level
|
||||
IDCGrade string `json:"idc_grade"`
|
||||
}
|
||||
|
||||
// Use the method to get the product turnover rate and the number of days the current stock will last.
|
||||
//
|
||||
// If you request a list of products by sku, the limit and offset parameters are optional.
|
||||
func (c Analytics) GetProductTurnover(ctx context.Context, params *GetProductTurnoverParams) (*GetProductTurnoverResponse, error) {
|
||||
url := "/v1/analytics/turnover/stocks"
|
||||
|
||||
resp := &GetProductTurnoverResponse{}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -145,3 +145,67 @@ func TestGetStocksOnWarehouses(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetProductTurnover(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
statusCode int
|
||||
headers map[string]string
|
||||
params *GetProductTurnoverParams
|
||||
response string
|
||||
}{
|
||||
// Test Ok
|
||||
{
|
||||
http.StatusOK,
|
||||
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
|
||||
&GetProductTurnoverParams{
|
||||
Limit: 1,
|
||||
SKU: []string{"string"},
|
||||
},
|
||||
`{
|
||||
"items": [
|
||||
{
|
||||
"ads": 0,
|
||||
"current_stock": 0,
|
||||
"idc": 0,
|
||||
"idc_grade": "GRADES_NONE"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
// Test No Client-Id or Api-Key
|
||||
{
|
||||
http.StatusUnauthorized,
|
||||
map[string]string{},
|
||||
&GetProductTurnoverParams{},
|
||||
`{
|
||||
"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.Analytics().GetProductTurnover(ctx, test.params)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
compareJsonResponse(t, test.response, &GetProductTurnoverResponse{})
|
||||
|
||||
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.Items) > int(test.params.Limit) {
|
||||
t.Errorf("Length of items is bigger than limit")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
112
ozon/common.go
112
ozon/common.go
@@ -453,6 +453,8 @@ const (
|
||||
ReadyForShipment GetFBSReturnsFilterStatus = "ready_for_shipment"
|
||||
Disposing GetFBSReturnsFilterStatus = "disposing"
|
||||
Disposed GetFBSReturnsFilterStatus = "disposed"
|
||||
ArrivedForResale GetFBSReturnsFilterStatus = "arrived_for_resale"
|
||||
MovingToResale GetFBSReturnsFilterStatus = "moving_to_resale"
|
||||
)
|
||||
|
||||
type GetFBOReturnsFilterStatus string
|
||||
@@ -722,6 +724,8 @@ const (
|
||||
// picking up products for removal by the seller
|
||||
TransactionServiceReturnFromStock TransactionOperationService = "MarketplaceServiceItemReturnFromStock"
|
||||
|
||||
TransactionServiceStarsMembership TransactionOperationService = "ItemAgentServiceStarsMembership"
|
||||
|
||||
// forwarding trade
|
||||
TransactionItemAdForSupplierLogisticSeller TransactionOperationService = "ItemAdvertisementForSupplierLogisticSeller"
|
||||
|
||||
@@ -792,7 +796,7 @@ const (
|
||||
TransactionServiceRedistributionReturnsPVZ TransactionOperationService = "MarketplaceServiceItemRedistributionReturnsPVZ"
|
||||
|
||||
// Agregator 3PL Globalagency service tariffication
|
||||
TransactionServiceAgencyFeeAggregator3PLGlobal TransactionOperationService = "OperationMarketplaceAgencyFeeAggregator3PLGlobal "
|
||||
TransactionServiceAgencyFeeAggregator3PLGlobal TransactionOperationService = "OperationMarketplaceAgencyFeeAggregator3PLGlobal"
|
||||
)
|
||||
|
||||
type PaymentTypeGroupName string
|
||||
@@ -807,3 +811,109 @@ const (
|
||||
PaymentTypeGroupPaymentToCurrentAccount PaymentTypeGroupName = "payment to current account"
|
||||
PaymentTypeGroupSberpay PaymentTypeGroupName = "Sberpay"
|
||||
)
|
||||
|
||||
type VisualStatus string
|
||||
|
||||
const (
|
||||
// dispute with the customer has been opened
|
||||
VisualStatusDisputeOpened VisualStatus = "DisputeOpened"
|
||||
|
||||
// pending with the seller
|
||||
VisualStatusOnSellerApproval VisualStatus = "OnSellerApproval"
|
||||
|
||||
// at the pick-up point
|
||||
VisualStatusArrivedAtReturnPlace VisualStatus = "ArrivedAtReturnPlace"
|
||||
|
||||
// pending clarification by the seller
|
||||
VisualStatusOnSellerClarification VisualStatus = "OnSellerClarification"
|
||||
|
||||
// pending clarification by the seller after partial compensation
|
||||
VisualStatusOnSellerClarificationPartial VisualStatus = "OnSellerClarificationAfterPartialCompensation"
|
||||
|
||||
// partial compensation offered
|
||||
VisualStatusOfferedPartial VisualStatus = "OfferedPartialCompensation"
|
||||
|
||||
// refund approved
|
||||
VisualStatusReturnMoneyApproved VisualStatus = "ReturnMoneyApproved"
|
||||
|
||||
// partial compensation provided
|
||||
VisualStatusPartialReturned VisualStatus = "PartialCompensationReturned"
|
||||
|
||||
// refund rejected, dispute isn't opened
|
||||
VisualStatusCancelledDisputeNotOpen VisualStatus = "CancelledDisputeNotOpen"
|
||||
|
||||
// request rejected
|
||||
VisualStatusRejected VisualStatus = "Rejected"
|
||||
|
||||
// request rejected by Ozon
|
||||
VisualStatusCrmRejected VisualStatus = "CrmRejected"
|
||||
|
||||
// request canceled
|
||||
VisualStatusCancelled VisualStatus = "Cancelled"
|
||||
|
||||
// request approved by the seller
|
||||
VisualStatusApproved VisualStatus = "Approved"
|
||||
|
||||
// request approved by Ozon
|
||||
VisualStatusApprovedByOzon VisualStatus = "ApprovedByOzon"
|
||||
|
||||
// seller received the return
|
||||
VisualStatusReceivedBySeller VisualStatus = "ReceivedBySeller"
|
||||
|
||||
// return is on its way to the seller
|
||||
VisualStatusMovingToSeller VisualStatus = "MovingToSeller"
|
||||
|
||||
// seller received the refund
|
||||
VisualStatusReturnCompensated VisualStatus = "ReturnCompensated"
|
||||
|
||||
// courier is taking the return to the seller
|
||||
VisualStatusReturningByCourier VisualStatus = "ReturningByCourier"
|
||||
|
||||
// on disposal
|
||||
VisualStatusUtilizing VisualStatus = "Utilizing"
|
||||
|
||||
// disposed of
|
||||
VisualStatusUtilized VisualStatus = "Utilized"
|
||||
|
||||
// customer received full refund
|
||||
VisualStatusMoneyReturned VisualStatus = "MoneyReturned"
|
||||
|
||||
// partial refund has been approved
|
||||
VisualStatusPartialInProcess VisualStatus = "PartialCompensationInProcess"
|
||||
|
||||
// seller opened a dispute
|
||||
VisualStatusDisputeYouOpened VisualStatus = "DisputeYouOpened"
|
||||
|
||||
// compensation rejected
|
||||
VisualStatusCompensationRejected VisualStatus = "CompensationRejected"
|
||||
|
||||
// support request sent
|
||||
VisualStatusDisputeOpening VisualStatus = "DisputeOpening"
|
||||
|
||||
// awaiting your decision on compensation
|
||||
VisualStatusCompensationOffered VisualStatus = "CompensationOffered"
|
||||
|
||||
// awaiting compensation
|
||||
VisualStatusWaitingCompensation VisualStatus = "WaitingCompensation"
|
||||
|
||||
// an error occurred when sending the support request
|
||||
VisualStatusSendingError VisualStatus = "SendingError"
|
||||
|
||||
// decision period has expired
|
||||
VisualStatusCompensationRejectedBySla VisualStatus = "CompensationRejectedBySla"
|
||||
|
||||
// seller has refused compensation
|
||||
VisualStatusCompensationRejectedBySeller VisualStatus = "CompensationRejectedBySeller"
|
||||
|
||||
// on the way to the Ozon warehouse
|
||||
VisualStatusMovingToOzon VisualStatus = "MovingToOzon"
|
||||
|
||||
// arrived at the Ozon warehouse
|
||||
VisualStatusReturnedToOzon VisualStatus = "ReturnedToOzon"
|
||||
|
||||
// quick refund
|
||||
VisualStatusMoneyReturnedBySystem VisualStatus = "MoneyReturnedBySystem"
|
||||
|
||||
// awaiting shipping
|
||||
VisualStatusWaitingShipment VisualStatus = "WaitingShipment"
|
||||
)
|
||||
|
||||
122
ozon/fbs.go
122
ozon/fbs.go
@@ -106,6 +106,9 @@ type FBSPosting struct {
|
||||
// Analytics data
|
||||
AnalyticsData FBSPostingAnalyticsData `json:"analytics_data"`
|
||||
|
||||
// Available actions and shipment information
|
||||
AvailableActions []string `json:"available_actions"`
|
||||
|
||||
// Shipment barcodes
|
||||
Barcodes FBSBarcode `json:"barcodes"`
|
||||
|
||||
@@ -912,6 +915,9 @@ type GetShipmentDataByIdentifierResult struct {
|
||||
// Analytics data
|
||||
AnalyticsData GetShipmentDataByIdentifierResultAnalyticsData `json:"analytics_data"`
|
||||
|
||||
// Available actions and shipment information
|
||||
AvailableActions []string `json:"available_actions"`
|
||||
|
||||
// Shipment barcodes
|
||||
Barcodes FBSBarcode `json:"barcodes"`
|
||||
|
||||
@@ -3016,3 +3022,119 @@ func (c FBS) GetCarriage(ctx context.Context, params *GetCarriageParams) (*GetCa
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type GetCancellationReasonsResponse struct {
|
||||
core.CommonResponse
|
||||
|
||||
// Method result
|
||||
Result []GetCancellationReasonsResult `json:"result"`
|
||||
}
|
||||
|
||||
type GetCancellationReasonsResult struct {
|
||||
// Cancellation reasons identifier
|
||||
Id int64 `json:"id"`
|
||||
|
||||
// Shipment cancellation result. true if the request is available for cancellation
|
||||
IsAvailableForCancellation bool `json:"is_available_for_cancellation"`
|
||||
|
||||
// Category name
|
||||
Title string `json:"title"`
|
||||
|
||||
// Shipment cancellation initiator
|
||||
TypeId string `json:"type_id"`
|
||||
}
|
||||
|
||||
func (c FBS) GetCancellationReasons(ctx context.Context) (*GetCancellationReasonsResponse, error) {
|
||||
url := "/v1/posting/fbo/cancel-reason/list"
|
||||
|
||||
resp := &GetCancellationReasonsResponse{}
|
||||
|
||||
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 SetShippingDateParams struct {
|
||||
// New shipping date
|
||||
NewCutoffDate time.Time `json:"new_cutoff_date"`
|
||||
|
||||
// Shipment number
|
||||
PostingNumber string `json:"posting_number"`
|
||||
}
|
||||
|
||||
type SetShippingDateResponse struct {
|
||||
core.CommonResponse
|
||||
|
||||
// true if the new date is set
|
||||
Result bool `json:"result"`
|
||||
}
|
||||
|
||||
func (c FBS) SetShippingDate(ctx context.Context, params *SetShippingDateParams) (*SetShippingDateResponse, error) {
|
||||
url := "/v1/posting/cutoff/set"
|
||||
|
||||
resp := &SetShippingDateResponse{}
|
||||
|
||||
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 SplitOrderParams struct {
|
||||
// Shipment number
|
||||
PostingNumber string `json:"posting_number"`
|
||||
|
||||
// Shipments list the order will be split into. You can split one order per one request
|
||||
Postings []SplitOrderParamPosting `json:"postings"`
|
||||
}
|
||||
|
||||
type SplitOrderParamPosting struct {
|
||||
Products []SplitOrderPostingProduct `json:"products"`
|
||||
}
|
||||
|
||||
type SplitOrderResponse struct {
|
||||
core.CommonResponse
|
||||
|
||||
// Original shipment details
|
||||
ParentPosting SplitOrderPosting `json:"parent_posting"`
|
||||
|
||||
// List of shipments the order was split into
|
||||
Postings []SplitOrderPosting `json:"postings"`
|
||||
}
|
||||
|
||||
type SplitOrderPosting struct {
|
||||
// Shipment number
|
||||
PostingNumber string `json:"posting_number"`
|
||||
|
||||
// List of products in the shipment
|
||||
Products []SplitOrderPostingProduct `json:"products"`
|
||||
}
|
||||
|
||||
type SplitOrderPostingProduct struct {
|
||||
// FBS product identifier in the Ozon system, SKU
|
||||
ProductId int64 `json:"product_id"`
|
||||
|
||||
// Product quantity
|
||||
Quantity int64 `json:"quantity"`
|
||||
}
|
||||
|
||||
func (c FBS) SplitOrder(ctx context.Context, params *SplitOrderParams) (*SplitOrderResponse, error) {
|
||||
url := "/v1/posting/fbs/split"
|
||||
|
||||
resp := &SplitOrderResponse{}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
199
ozon/fbs_test.go
199
ozon/fbs_test.go
@@ -3010,3 +3010,202 @@ func TestGetCarriage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCancellationReasons(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"},
|
||||
`{
|
||||
"result": [
|
||||
{
|
||||
"id": 352,
|
||||
"title": "The products ran out at the seller's warehouse",
|
||||
"type_id": "seller",
|
||||
"is_available_for_cancellation": true
|
||||
},
|
||||
{
|
||||
"id": 401,
|
||||
"title": "Seller rejects arbitration",
|
||||
"type_id": "seller",
|
||||
"is_available_for_cancellation": false
|
||||
},
|
||||
{
|
||||
"id": 402,
|
||||
"title": "Other (seller's fault)",
|
||||
"type_id": "seller",
|
||||
"is_available_for_cancellation": true
|
||||
},
|
||||
{
|
||||
"id": 666,
|
||||
"title": "Return from the delivery service: there is no delivery to the specified region",
|
||||
"type_id": "seller",
|
||||
"is_available_for_cancellation": false
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
// 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.FBS().GetCancellationReasons(ctx)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
compareJsonResponse(t, test.response, &GetCancellationReasonsResponse{})
|
||||
|
||||
if resp.StatusCode != test.statusCode {
|
||||
t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetShippingDate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
statusCode int
|
||||
headers map[string]string
|
||||
params *SetShippingDateParams
|
||||
response string
|
||||
}{
|
||||
// Test Ok
|
||||
{
|
||||
http.StatusOK,
|
||||
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
|
||||
&SetShippingDateParams{
|
||||
NewCutoffDate: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2019-08-24T14:15:22Z"),
|
||||
},
|
||||
`{
|
||||
"result": true
|
||||
}`,
|
||||
},
|
||||
// Test No Client-Id or Api-Key
|
||||
{
|
||||
http.StatusUnauthorized,
|
||||
map[string]string{},
|
||||
&SetShippingDateParams{},
|
||||
`{
|
||||
"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.FBS().SetShippingDate(ctx, test.params)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
compareJsonResponse(t, test.response, &SetShippingDateResponse{})
|
||||
|
||||
if resp.StatusCode != test.statusCode {
|
||||
t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitOrder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
statusCode int
|
||||
headers map[string]string
|
||||
params *SplitOrderParams
|
||||
response string
|
||||
}{
|
||||
// Test Ok
|
||||
{
|
||||
http.StatusOK,
|
||||
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
|
||||
&SplitOrderParams{
|
||||
PostingNumber: "string",
|
||||
Postings: []SplitOrderParamPosting{
|
||||
{
|
||||
Products: []SplitOrderPostingProduct{
|
||||
{
|
||||
ProductId: 1,
|
||||
Quantity: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
`{
|
||||
"parent_posting": {
|
||||
"posting_number": "string",
|
||||
"products": [
|
||||
{
|
||||
"product_id": 0,
|
||||
"quantity": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"postings": [
|
||||
{
|
||||
"posting_number": "string",
|
||||
"products": [
|
||||
{
|
||||
"product_id": 0,
|
||||
"quantity": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
// Test No Client-Id or Api-Key
|
||||
{
|
||||
http.StatusUnauthorized,
|
||||
map[string]string{},
|
||||
&SplitOrderParams{},
|
||||
`{
|
||||
"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.FBS().SplitOrder(ctx, test.params)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
compareJsonResponse(t, test.response, &SplitOrderResponse{})
|
||||
|
||||
if resp.StatusCode != test.statusCode {
|
||||
t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -822,6 +822,12 @@ type CreateOrUpdateProductItem struct {
|
||||
// - IS_NO_CODE_SERVICE
|
||||
ServiceType string `json:"service_type" default:"IS_CODE_SERVICE"`
|
||||
|
||||
// Product type identifier.
|
||||
// You can get values from the type_id parameter in the `/v1/description-category/tree` method response.
|
||||
// When filling this parameter in,
|
||||
// you can leave out the attributes attribute if it has the `id:8229` parameter
|
||||
TypeId int64 `json:"type_id"`
|
||||
|
||||
// VAT rate for the product:
|
||||
// - 0 — not subject to VAT,
|
||||
// - 0.1 — 10%,
|
||||
@@ -1412,9 +1418,6 @@ type GetDescriptionOfProductResult struct {
|
||||
// Product characteristic identifier
|
||||
Id int64 `json:"id"`
|
||||
|
||||
// Identifier for subsequent batch loading of images
|
||||
ImageGroupId string `json:"image_group_id"`
|
||||
|
||||
// Array of links to product images
|
||||
Images []GetDescriptionOfProductResultImage `json:"images"`
|
||||
|
||||
@@ -1430,6 +1433,9 @@ type GetDescriptionOfProductResult struct {
|
||||
// Array of PDF files
|
||||
PDFList []GetDescriptionOfProductResultPDF `json:"pdf_list"`
|
||||
|
||||
// Product type identifier
|
||||
TypeId int64 `json:"type_id"`
|
||||
|
||||
// Weight of product in the package
|
||||
Weight int32 `json:"weight"`
|
||||
|
||||
@@ -1756,67 +1762,6 @@ func (c Products) RemoveProductWithoutSKU(ctx context.Context, params *RemovePro
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type ListGeoRestrictionsParams struct {
|
||||
// Filter. To get all geo-restrictions, leave names blank and specify true in the only_visible parameter
|
||||
Filter ListGeoRestrictionsFilter `json:"filter"`
|
||||
|
||||
// Order number of geo-restriction from which to output data in the response.
|
||||
//
|
||||
// If you specify 23 in this parameter, the first item in the restrictions list will output order_number = 24.
|
||||
// If you want to get all geo-restrictions, pass 0 in this parameter
|
||||
LastOrderNumber int64 `json:"last_order_number"`
|
||||
|
||||
// Number of items in the response
|
||||
Limit int64 `json:"limit"`
|
||||
}
|
||||
|
||||
type ListGeoRestrictionsFilter struct {
|
||||
// List with city names
|
||||
Names []string `json:"names"`
|
||||
|
||||
// Value visibility. We recommend always passing true in this parameter
|
||||
OnlyVisible bool `json:"only_visible"`
|
||||
}
|
||||
|
||||
type ListGeoRestrictionsResponse struct {
|
||||
core.CommonResponse
|
||||
|
||||
// Restrictions
|
||||
Restrictions []ListGeoRestrictionsRestriction `json:"restrictions"`
|
||||
}
|
||||
|
||||
type ListGeoRestrictionsRestriction struct {
|
||||
// Geo-restriction identifier
|
||||
Id string `json:"id"`
|
||||
|
||||
// Item visibility
|
||||
IsVisible bool `json:"is_visible"`
|
||||
|
||||
// City name
|
||||
Name string `json:"name"`
|
||||
|
||||
// Geo-restriction order number.
|
||||
//
|
||||
// If you specify 23 in the last_order_number parameter in the request,
|
||||
// the first item in the restrictions list will have order_number = 24
|
||||
OrderNumber int64 `json:"order_number"`
|
||||
}
|
||||
|
||||
// Deprecated: Get a list of geo-restrictions for services
|
||||
func (c Products) ListGeoRestrictions(ctx context.Context, params *ListGeoRestrictionsParams) (*ListGeoRestrictionsResponse, error) {
|
||||
url := "/v1/products/geo-restrictions-catalog-by-filter"
|
||||
|
||||
resp := &ListGeoRestrictionsResponse{}
|
||||
|
||||
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 UploadActivationCodesParams struct {
|
||||
// Digital activation codes
|
||||
DigitalCodes []string `json:"digital_codes"`
|
||||
|
||||
@@ -1611,7 +1611,6 @@ func TestGetDescriptionOfProduct(t *testing.T) {
|
||||
"index": 2
|
||||
}
|
||||
],
|
||||
"image_group_id": "",
|
||||
"images360": [],
|
||||
"pdf_list": [],
|
||||
"attributes": [
|
||||
@@ -2141,79 +2140,6 @@ func TestRemoveProductWithoutSKU(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListGeoRestrictions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
statusCode int
|
||||
headers map[string]string
|
||||
params *ListGeoRestrictionsParams
|
||||
response string
|
||||
}{
|
||||
// Test Ok
|
||||
{
|
||||
http.StatusOK,
|
||||
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
|
||||
&ListGeoRestrictionsParams{
|
||||
Filter: ListGeoRestrictionsFilter{
|
||||
OnlyVisible: true,
|
||||
},
|
||||
LastOrderNumber: 0,
|
||||
Limit: 3,
|
||||
},
|
||||
`{
|
||||
"restrictions": [
|
||||
{
|
||||
"id": "world",
|
||||
"name": "Весь Мир",
|
||||
"is_visible": true,
|
||||
"order_number": 1
|
||||
},
|
||||
{
|
||||
"id": "42fb1c32-0cfe-5c96-9fb5-7f8e8449f28c",
|
||||
"name": "Все города РФ",
|
||||
"is_visible": true,
|
||||
"order_number": 2
|
||||
},
|
||||
{
|
||||
"id": "moscow",
|
||||
"name": "Москва",
|
||||
"is_visible": true,
|
||||
"order_number": 3
|
||||
}
|
||||
]
|
||||
}`,
|
||||
},
|
||||
// Test No Client-Id or Api-Key
|
||||
{
|
||||
http.StatusUnauthorized,
|
||||
map[string]string{},
|
||||
&ListGeoRestrictionsParams{},
|
||||
`{
|
||||
"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.Products().ListGeoRestrictions(ctx, test.params)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
compareJsonResponse(t, test.response, &ListGeoRestrictionsResponse{})
|
||||
|
||||
if resp.StatusCode != test.statusCode {
|
||||
t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUploadActivationCodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -18,6 +18,11 @@ type GetCurrentSellerRatingInfoResponse struct {
|
||||
// Rating groups list
|
||||
Groups []GetCurrentSellerRatingInfoGroup `json:"groups"`
|
||||
|
||||
// Localization index details.
|
||||
// If you had no sales in the last 14 days,
|
||||
// the parameter fields will be empty
|
||||
LocalizationIndex []LocalizationIndex `json:"localization_index"`
|
||||
|
||||
// An indication that the penalty points balance is exceeded
|
||||
PenaltyScoreExceeded bool `json:"penalty_score_exceeded"`
|
||||
|
||||
@@ -25,6 +30,14 @@ type GetCurrentSellerRatingInfoResponse struct {
|
||||
Premium bool `json:"premium"`
|
||||
}
|
||||
|
||||
type LocalizationIndex struct {
|
||||
// Date of localization index calculation
|
||||
CalculationDate time.Time `json:"calculation_date"`
|
||||
|
||||
// Localization index value
|
||||
LocalizationPercentage int32 `json:"localization_percentage"`
|
||||
}
|
||||
|
||||
type GetCurrentSellerRatingInfoGroup struct {
|
||||
// Ratings group name
|
||||
GroupName string `json:"group_name"`
|
||||
|
||||
@@ -41,6 +41,12 @@ func TestGetCurrentRatingInfo(t *testing.T) {
|
||||
]
|
||||
}
|
||||
],
|
||||
"localization_index": [
|
||||
{
|
||||
"calculation_date": "2019-08-24T14:15:22Z",
|
||||
"localization_percentage": 0
|
||||
}
|
||||
],
|
||||
"penalty_score_exceeded": true,
|
||||
"premium": true
|
||||
}`,
|
||||
|
||||
245
ozon/returns.go
245
ozon/returns.go
@@ -943,7 +943,250 @@ func (c Returns) FBSQuantity(ctx context.Context, params *GetFBSQuantityReturnsP
|
||||
|
||||
resp := &GetFBSQuantityReturnsResponse{}
|
||||
|
||||
response, err := c.client.Request(ctx, http.MethodPost, url, nil, resp, nil)
|
||||
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 ListReturnsParams struct {
|
||||
// Filter
|
||||
Filter *ListReturnsFilter `json:"filter,omitempty"`
|
||||
|
||||
// Number of loaded returns. The maximum value is 500
|
||||
Limit int32 `json:"limit"`
|
||||
|
||||
// Identifier of the last loaded return
|
||||
LastId int64 `json:"last_id"`
|
||||
}
|
||||
|
||||
type ListReturnsFilter struct {
|
||||
// Filter by return creation date
|
||||
LogisticReturnDate *GetFBSReturnsFilterTimeRange `json:"logistic_return_date"`
|
||||
|
||||
// Filter by storage fees start date
|
||||
StorageTarifficationDate *GetFBSReturnsFilterTimeRange `json:"storage_tariffication_start_date"`
|
||||
|
||||
// Filter by date the return status changed
|
||||
VisualStatusChangeMoment *GetFBSReturnsFilterTimeRange `json:"visual_status_change_moment"`
|
||||
|
||||
// Filter by order identifier
|
||||
OrderId int64 `json:"order_id,omitempty"`
|
||||
|
||||
// Filter by shipment number
|
||||
PostingNumbers []string `json:"posting_numbers,omitempty"`
|
||||
|
||||
// Filter by product name
|
||||
ProductName string `json:"product_name,omitempty"`
|
||||
|
||||
// Filter by product identifier in the seller's system
|
||||
OfferId string `json:"offer_id,omitempty"`
|
||||
|
||||
// Filter by return status
|
||||
VisualStatusName VisualStatus `json:"visual_status_name,omitempty"`
|
||||
|
||||
// Filter by warehouse identifier
|
||||
WarehouseId int64 `json:"warehouse_id,omitempty"`
|
||||
|
||||
// Filter by return label barcode
|
||||
Barcode string `json:"barcode,omitempty"`
|
||||
|
||||
// Filter by delivery scheme: FBS or FBO
|
||||
ReturnSchema string `json:"return_schema,omitempty"`
|
||||
}
|
||||
|
||||
type ListReturnsResponse struct {
|
||||
core.CommonResponse
|
||||
|
||||
// Returns details
|
||||
Returns []Return `json:"returns"`
|
||||
|
||||
// true, if the seller has other returns
|
||||
HasNext bool `json:"has_next"`
|
||||
}
|
||||
|
||||
type Return struct {
|
||||
// Product items data
|
||||
Exemplars []ReturnExemplar `json:"exemplars"`
|
||||
|
||||
// Return identifier
|
||||
Id int64 `json:"id"`
|
||||
|
||||
// Company identifier
|
||||
CompanyId int64 `json:"company_id"`
|
||||
|
||||
// Return reason
|
||||
ReturnReasonName string `json:"return_reason_name"`
|
||||
|
||||
// Return type
|
||||
Type string `json:"type"`
|
||||
|
||||
// Return scheme
|
||||
Schema string `json:"schema"`
|
||||
|
||||
// Order identifier
|
||||
OrderId int64 `json:"order_id"`
|
||||
|
||||
// Order number
|
||||
OrderNumber string `json:"order_number"`
|
||||
|
||||
// Warehouse where the return is stored
|
||||
Place ReturnPlace `json:"place"`
|
||||
|
||||
// Warehouse where returns are sent to
|
||||
TargetPlace ReturnPlace `json:"target_place"`
|
||||
|
||||
// Storage details
|
||||
Storage ReturnStorage `json:"storage"`
|
||||
|
||||
// Product details
|
||||
Product ReturnProduct `json:"product"`
|
||||
|
||||
// Return details
|
||||
Logistic ReturnLogistic `json:"logistic"`
|
||||
|
||||
// Return status details
|
||||
Visual ReturnVisual `json:"visual"`
|
||||
|
||||
// Additional information
|
||||
AdditionalInfo ReturnAdditionalInfo `json:"additional_info"`
|
||||
|
||||
// Previous return identifier
|
||||
SourceId int64 `json:"source_id"`
|
||||
|
||||
// Shipment number
|
||||
PostingNumber string `json:"posting_number"`
|
||||
|
||||
// Original shipment barcode
|
||||
ClearingId int64 `json:"clearing_id"`
|
||||
|
||||
// Package unit identifier in the Ozon logistics system
|
||||
ReturnClearingId int64 `json:"return_clearing_id"`
|
||||
}
|
||||
|
||||
type ReturnExemplar struct {
|
||||
// Product identifier
|
||||
Id int64 `json:"id"`
|
||||
}
|
||||
|
||||
type ReturnPlace struct {
|
||||
// Warehouse identifier
|
||||
Id int64 `json:"id"`
|
||||
|
||||
// Warehouse name
|
||||
Name string `json:"name"`
|
||||
|
||||
// Warehouse address
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
type ReturnStorage struct {
|
||||
// Storage cost details
|
||||
Sum ReturnSum `json:"sum"`
|
||||
|
||||
// First day of charging for storage
|
||||
TarifficationsFirstDate time.Time `json:"tariffication_first_date"`
|
||||
|
||||
// Start date for storage fees
|
||||
TarifficationsStartDate time.Time `json:"tariffication_start_date"`
|
||||
|
||||
// Date when the return was ready for handover
|
||||
ArrivedMoment time.Time `json:"arrived_moment"`
|
||||
|
||||
// Number of days the return has been waiting for handover
|
||||
Days int64 `json:"days"`
|
||||
|
||||
// Disposal cost details
|
||||
UtilizationSum ReturnSum `json:"utilization_sum"`
|
||||
|
||||
// Planned disposal date
|
||||
UtilizationForecastDate string `json:"utilization_forecast_date"`
|
||||
}
|
||||
|
||||
type ReturnSum struct {
|
||||
// Currency
|
||||
CurrencyCode string `json:"currency_code"`
|
||||
|
||||
// Disposal cost
|
||||
Price float64 `json:"price"`
|
||||
}
|
||||
|
||||
type ReturnProduct struct {
|
||||
// Product identifier in the Ozon system, SKU
|
||||
SKU int64 `json:"sku"`
|
||||
|
||||
// Product identifier in the seller's system
|
||||
OfferId string `json:"offer_id"`
|
||||
|
||||
// product name
|
||||
Name string `json:"name"`
|
||||
|
||||
// Product price details
|
||||
Price ReturnSum `json:"price"`
|
||||
|
||||
// Product cost without commission
|
||||
PriceWithoutCommission ReturnSum `json:"price_without_commission"`
|
||||
|
||||
// Sales commission by category
|
||||
CommissionPercent float64 `json:"commission_percent"`
|
||||
|
||||
// Commission details
|
||||
Commission ReturnSum `json:"commission"`
|
||||
}
|
||||
|
||||
type ReturnLogistic struct {
|
||||
// Date when the order was placed for technical return
|
||||
TechnicalReturnMoment time.Time `json:"technical_return_moment"`
|
||||
|
||||
// Date when the return arrived to the warehouse or was handed over to the seller
|
||||
FinalMoment time.Time `json:"final_moment"`
|
||||
|
||||
// Date when the seller received compensation for the return
|
||||
CancelledWithCompensationMoment time.Time `json:"cancelled_with_compensation_moment"`
|
||||
|
||||
// Date when the customer returned the product
|
||||
ReturnDate time.Time `json:"return_date"`
|
||||
|
||||
// Return label barcode
|
||||
Barcode string `json:"barcode"`
|
||||
}
|
||||
|
||||
type ReturnVisual struct {
|
||||
// Return status
|
||||
Status ReturnVisualStatus `json:"status"`
|
||||
|
||||
// Date the return status changed
|
||||
ChangeMoment time.Time `json:"change_moment"`
|
||||
}
|
||||
|
||||
type ReturnVisualStatus struct {
|
||||
// Return status identifier
|
||||
Id int32 `json:"id"`
|
||||
|
||||
// Return status name
|
||||
DisplayName string `json:"display_name"`
|
||||
|
||||
// System name of the return status
|
||||
SystemName string `json:"sys_name"`
|
||||
}
|
||||
|
||||
type ReturnAdditionalInfo struct {
|
||||
// true, if the return package is opened
|
||||
IsOpened bool `json:"is_opened"`
|
||||
|
||||
// true, if the return belongs to Super Economy products
|
||||
IsSuperEconom bool `json:"is_super_econom"`
|
||||
}
|
||||
|
||||
func (c Returns) List(ctx context.Context, params *ListReturnsParams) (*ListReturnsResponse, error) {
|
||||
url := "/v1/returns/list"
|
||||
|
||||
resp := &ListReturnsResponse{}
|
||||
|
||||
response, err := c.client.Request(ctx, http.MethodPost, url, params, resp, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1059,3 +1059,153 @@ func TestFBSQuantity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestListReturns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
statusCode int
|
||||
headers map[string]string
|
||||
params *ListReturnsParams
|
||||
response string
|
||||
}{
|
||||
// Test Ok
|
||||
{
|
||||
http.StatusOK,
|
||||
map[string]string{"Client-Id": "my-client-id", "Api-Key": "my-api-key"},
|
||||
&ListReturnsParams{
|
||||
Filter: &ListReturnsFilter{
|
||||
LogisticReturnDate: &GetFBSReturnsFilterTimeRange{
|
||||
TimeFrom: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2019-08-24T14:15:22Z"),
|
||||
TimeTo: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2019-08-24T14:15:22Z"),
|
||||
},
|
||||
StorageTarifficationDate: &GetFBSReturnsFilterTimeRange{
|
||||
TimeFrom: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2019-08-24T14:15:22Z"),
|
||||
TimeTo: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2019-08-24T14:15:22Z"),
|
||||
},
|
||||
VisualStatusChangeMoment: &GetFBSReturnsFilterTimeRange{
|
||||
TimeFrom: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2019-08-24T14:15:22Z"),
|
||||
TimeTo: core.TimeFromString(t, "2006-01-02T15:04:05Z", "2019-08-24T14:15:22Z"),
|
||||
},
|
||||
WarehouseId: 911,
|
||||
ReturnSchema: "FBO",
|
||||
ProductName: "string",
|
||||
},
|
||||
Limit: 500,
|
||||
LastId: 0,
|
||||
},
|
||||
`{
|
||||
"returns": [
|
||||
{
|
||||
"exemplars": [
|
||||
{
|
||||
"id": 1019562967545956
|
||||
}
|
||||
],
|
||||
"id": 1000015552,
|
||||
"company_id": 3058,
|
||||
"return_reason_name": "Customer refused on receipt: not satisfied with the quality of the product",
|
||||
"type": "FullReturn",
|
||||
"schema": "Fbs",
|
||||
"order_id": 24540784250,
|
||||
"order_number": "58544282-0057",
|
||||
"place": {
|
||||
"id": 23869688194000,
|
||||
"name": "СЦ_Львовский_Возвраты",
|
||||
"address": "Россия, обл. Московская, г. Подольск, промышленная зона Львовский, ул. Московская, д. 69, стр. 5"
|
||||
},
|
||||
"target_place": {
|
||||
"id": 23869688194000,
|
||||
"name": "СЦ_Львовский_Возвраты",
|
||||
"address": "Россия, обл. Московская, г. Подольск, промышленная зона Львовский, ул. Московская, д. 69, стр. 5"
|
||||
},
|
||||
"storage": {
|
||||
"sum": {
|
||||
"currency_code": "RUB",
|
||||
"price": 1231
|
||||
},
|
||||
"tariffication_first_date": "2024-07-30T06:15:48.998146Z",
|
||||
"tariffication_start_date": "2024-07-29T06:15:48.998146Z",
|
||||
"arrived_moment": "2024-07-29T06:15:48.998146Z",
|
||||
"days": 0,
|
||||
"utilization_sum": {
|
||||
"currency_code": "RUB",
|
||||
"price": 1231
|
||||
},
|
||||
"utilization_forecast_date": "2024-07-29T06:15:48.998146Z"
|
||||
},
|
||||
"product": {
|
||||
"sku": 1100526203,
|
||||
"offer_id": "81451",
|
||||
"name": "Кукла Дотти Плачущий младенец Cry Babies Dressy Dotty",
|
||||
"price": {
|
||||
"currency_code": "RUB",
|
||||
"price": 3318
|
||||
},
|
||||
"price_without_commission": {
|
||||
"currency_code": "RUB",
|
||||
"price": 3318
|
||||
},
|
||||
"commission_percent": 1.2,
|
||||
"commission": {
|
||||
"currency_code": "RUB",
|
||||
"price": 2312
|
||||
}
|
||||
},
|
||||
"logistic": {
|
||||
"technical_return_moment": "2024-07-29T06:15:48.998146Z",
|
||||
"final_moment": "2024-07-29T06:15:48.998146Z",
|
||||
"cancelled_with_compensation_moment": "2024-07-29T06:15:48.998146Z",
|
||||
"return_date": "2024-07-29T06:15:48.998146Z",
|
||||
"barcode": "ii5275210303"
|
||||
},
|
||||
"visual": {
|
||||
"status": {
|
||||
"id": 3,
|
||||
"display_name": "At the pick-up point",
|
||||
"sys_name": "ArrivedAtReturnPlace"
|
||||
},
|
||||
"change_moment": "2024-07-29T06:15:48.998146Z"
|
||||
},
|
||||
"additional_info": {
|
||||
"is_opened": true,
|
||||
"is_super_econom": false
|
||||
},
|
||||
"source_id": 90426223,
|
||||
"posting_number": "58544282-0057-1",
|
||||
"clearing_id": 21190893156000,
|
||||
"return_clearing_id": null
|
||||
}
|
||||
],
|
||||
"has_next": false
|
||||
}`,
|
||||
},
|
||||
// Test No Client-Id or Api-Key
|
||||
{
|
||||
http.StatusUnauthorized,
|
||||
map[string]string{},
|
||||
&ListReturnsParams{},
|
||||
`{
|
||||
"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.Returns().List(ctx, test.params)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
compareJsonResponse(t, test.response, &ListReturnsResponse{})
|
||||
|
||||
if resp.StatusCode != test.statusCode {
|
||||
t.Errorf("got wrong status code: got: %d, expected: %d", resp.StatusCode, test.statusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user