7 Commits

Author SHA1 Message Date
diPhantxm
83fd8cf825 fix examlpe for notification server 2023-07-27 03:20:32 +03:00
Kirill
ebbc21b618 Notification Server
Push-Notifications
2023-07-27 02:51:36 +03:00
diPhantxm
f53b573d62 fix tests 2023-07-27 02:49:55 +03:00
diPhantxm
eb0ce6feb6 update readme 2023-07-27 00:44:36 +03:00
diPhantxm
9a41bb1196 add notification server, tests 2023-07-27 00:23:47 +03:00
diPhantxm
e76b9f3961 add notification server message types 2023-07-26 04:18:24 +03:00
diPhantxm
add4202b3e added new response fields to GetProductPriceInfo: result.items.acquiring, result.items.commissions.sales_percent_fbo/fbs 2023-07-25 17:41:27 +03:00
7 changed files with 716 additions and 5 deletions

View File

@@ -11,6 +11,7 @@ Read full [documentation](https://docs.ozon.ru/api/seller/en/#tag/Introduction)
You can check [list of supported endpoints](ENDPOINTS.md)
## How to start
### API
Get Client-Id and Api-Key in your seller profile [here](https://seller.ozon.ru/app/settings/api-keys?locale=en)
Just add dependency to your project and you're ready to go.
@@ -49,6 +50,43 @@ func main() {
}
```
### Notifications
Ozon can send push-notifications to your REST server. There is an implementation of REST server that handles notifications in this library.
[Official documentation](https://docs.ozon.ru/api/seller/en/#tag/push_intro)
How to use:
```Golang
package main
import (
"log"
"github.com/diphantxm/ozon-api-client/ozon/notifications"
)
func main() {
// Create server
port := 5000
server := notifications.NewNotificationServer(port)
// Register handlers passing message type and handler itself
server.Register(notifications.ChatClosedType, func(req interface{}) error {
notification := req.(*notifications.ChatClosed)
// Do something with the notification here...
log.Printf("chat %s has been closed\n", notification.ChatId)
return nil
})
// Run server
if err := server.Run(); err != nil {
log.Printf("error while running notification server: %s", err)
}
}
```
## Contribution
If you need some endpoints ASAP, create an issue and list all the endpoints. I will add them to library soon.

View File

@@ -0,0 +1,19 @@
package notifications
type MessageType string
const (
PingType MessageType = "TYPE_PING"
NewPostingType MessageType = "TYPE_NEW_POSTING"
PostingCancelledType MessageType = "TYPE_POSTING_CANCELLED"
StateChangedType MessageType = "TYPE_STATE_CHANGED"
CutoffDateChangedType MessageType = "TYPE_CUTOFF_DATE_CHANGED"
DeliveryDateChangedType MessageType = "TYPE_DELIVERY_DATE_CHANGED"
CreateOrUpdateType MessageType = "TYPE_CREATE_OR_UPDATE_ITEM"
PriceIndexChangedType MessageType = "TYPE_PRICE_INDEX_CHANGED"
StocksChangedType MessageType = "TYPE_STOCKS_CHANGED"
NewMessageType MessageType = "TYPE_NEW_MESSAGE"
UpdateMessageType MessageType = "TYPE_UPDATE_MESSAGE"
MessageReadType MessageType = "TYPE_MESSAGE_READ"
ChatClosedType MessageType = "TYPE_CHAT_CLOSED"
)

View File

@@ -0,0 +1,165 @@
package notifications
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
)
type Handler func(req interface{}) error
type NotificationServer struct {
port int
handlers map[MessageType]Handler
}
func NewNotificationServer(port int) *NotificationServer {
return &NotificationServer{
port: port,
handlers: map[MessageType]Handler{},
}
}
func (ns *NotificationServer) Run() error {
mux := http.NewServeMux()
mux.HandleFunc("/", ns.handler)
server := http.Server{
Addr: fmt.Sprintf("0.0.0.0:%d", ns.port),
Handler: mux,
}
return server.ListenAndServe()
}
func (ns *NotificationServer) handler(rw http.ResponseWriter, httpReq *http.Request) {
mt := &Common{}
content, err := ioutil.ReadAll(httpReq.Body)
if err != nil {
log.Print(err)
ns.error(rw, http.StatusBadRequest, err)
return
}
if err := json.Unmarshal(content, mt); err != nil {
log.Print(err)
ns.error(rw, http.StatusBadRequest, err)
return
}
if mt.MessageType == PingType {
resp := pingResponse{
Version: "1.0",
Name: "Ozon Seller API",
Time: time.Now(),
}
respJson, err := json.Marshal(resp)
if err != nil {
log.Print(err)
ns.error(rw, http.StatusInternalServerError, err)
return
}
rw.WriteHeader(http.StatusOK)
rw.Write(respJson)
return
}
req, err := ns.unmarshal(mt.MessageType, content)
if err != nil {
log.Print(err)
ns.result(rw, false)
//ns.error(rw, http.StatusInternalServerError, err)
return
}
h, ok := ns.handlers[mt.MessageType]
if !ok {
ns.result(rw, true)
log.Printf("handler for %s is not registered", mt.MessageType)
return
}
if err := h(req); err != nil {
log.Print(err)
ns.result(rw, false)
//ns.error(rw, http.StatusInternalServerError, err)
return
}
ns.result(rw, true)
}
func (ns *NotificationServer) Register(mt MessageType, handler func(req interface{}) error) {
ns.handlers[mt] = handler
}
func (ns *NotificationServer) unmarshal(messageType MessageType, content []byte) (interface{}, error) {
switch messageType {
case NewPostingType:
v := &NewPosting{}
err := json.Unmarshal(content, v)
return v, err
case PostingCancelledType:
v := &PostingCancelled{}
err := json.Unmarshal(content, v)
return v, err
case StateChangedType:
v := &StateChanged{}
err := json.Unmarshal(content, v)
return v, err
case CutoffDateChangedType:
v := &CutoffDateChanged{}
err := json.Unmarshal(content, v)
return v, err
case DeliveryDateChangedType:
v := &DeliveryDateChanged{}
err := json.Unmarshal(content, v)
return v, err
case CreateOrUpdateType:
v := &CreateOrUpdateItem{}
err := json.Unmarshal(content, v)
return v, err
case PriceIndexChangedType:
v := &PriceIndexChanged{}
err := json.Unmarshal(content, v)
return v, err
case StocksChangedType:
v := &StocksChanged{}
err := json.Unmarshal(content, v)
return v, err
case NewMessageType:
v := &NewMessage{}
err := json.Unmarshal(content, v)
return v, err
case UpdateMessageType:
v := &UpdateMessage{}
err := json.Unmarshal(content, v)
return v, err
case MessageReadType:
v := &MessageRead{}
err := json.Unmarshal(content, v)
return v, err
case ChatClosedType:
v := &ChatClosed{}
err := json.Unmarshal(content, v)
return v, err
default:
return nil, fmt.Errorf("unsupported type: %s", messageType)
}
}
func (ns *NotificationServer) error(rw http.ResponseWriter, statusCode int, err error) {
errResp := errorResponse{
Data: errorData{
Code: fmt.Sprintf("%d", statusCode),
Message: err.Error(),
},
}
errJson, _ := json.Marshal(errResp)
rw.WriteHeader(statusCode)
rw.Write(errJson)
}
func (ns *NotificationServer) result(rw http.ResponseWriter, res bool) {
rw.WriteHeader(http.StatusOK)
rw.Write([]byte(fmt.Sprintf(`{"result": %t}`, res)))
}

View File

@@ -0,0 +1,133 @@
package notifications
import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"reflect"
"strings"
"testing"
"time"
)
func TestNotificationServer(t *testing.T) {
testCases := []struct {
request string
response string
}{
// PING
{
`{
"message_type": "TYPE_PING",
"time": "2019-08-24T14:15:22Z"
}`,
`{
"version": "1.0",
"name": "Ozon Seller API"
}`,
},
// REGISTERED HANDLER
{
`{
"message_type": "TYPE_CHAT_CLOSED",
"chat_id": "b646d975-0c9c-4872-9f41-8b1e57181063",
"chat_type": "Buyer_Seller",
"user": {
"id": "115568",
"type": "Сustomer"
},
"seller_id": "7"
}`,
`{
"result": true
}`,
},
// UNREGISTERED HANDLER
{
`{
"message_type": "TYPE_MESSAGE_READ",
"chat_id": "b646d975-0c9c-4872-9f41-8b1e57181063",
"chat_type": "Buyer_Seller",
"message_id": "3000000000817031942",
"created_at": "2022-07-18T20:58:04.528Z",
"user": {
"id": "115568",
"type": "Сustomer"
},
"last_read_message_id": "3000000000817031942",
"seller_id": "7"
}`,
`{
"result": true
}`,
},
}
port := getFreePort()
client := http.Client{}
server := NewNotificationServer(port)
server.Register(ChatClosedType, func(req interface{}) error {
_, ok := req.(*ChatClosed)
if !ok {
return fmt.Errorf("req is not of ChatClosed type")
}
return nil
})
go func() {
if err := server.Run(); err != nil {
t.Fatalf("notification server is down: %s", err)
}
}()
// TODO: get rid of it
// Needed to make sure server is running
time.Sleep(3 * time.Second)
for _, testCase := range testCases {
httpResp, err := client.Post(fmt.Sprintf("http://0.0.0.0:%d/", port), "application/json", strings.NewReader(testCase.request))
if err != nil {
t.Error(err)
continue
}
gotJson, err := ioutil.ReadAll(httpResp.Body)
if err != nil {
t.Error(err)
continue
}
expected := map[string]interface{}{}
got := map[string]interface{}{}
err = json.Unmarshal(gotJson, &got)
if err != nil {
t.Error(err)
}
err = json.Unmarshal([]byte(testCase.response), &expected)
if err := compare(expected, got); err != nil {
t.Error(err)
continue
}
}
}
func compare(expected map[string]interface{}, got map[string]interface{}) error {
for k, v := range expected {
if gotValue, ok := got[k]; !ok {
return fmt.Errorf("key %s is expected to present", k)
} else if !reflect.DeepEqual(gotValue, v) {
return fmt.Errorf("key %s is not equal, got: %v, want: %v", k, gotValue, v)
}
}
return nil
}
func getFreePort() int {
listener, _ := net.Listen("tcp", ":0")
defer listener.Close()
return listener.Addr().(*net.TCPAddr).Port
}

327
ozon/notifications/types.go Normal file
View File

@@ -0,0 +1,327 @@
package notifications
import "time"
// Checking if the service is ready at initial connection and periodically after it
type pingRequest struct {
Common
// Date and time when the notification was sent in UTC format
Time time.Time `json:"time"`
}
type pingResponse struct {
// Application version
Version string `json:"version"`
// Application name
Name string `json:"name"`
// Date and time when notification processing started in UTC format
Time time.Time `json:"time"`
}
type Common struct {
MessageType MessageType `json:"message_type"`
}
// New shipment
type NewPosting struct {
Common
// Shipment number
PostingNumber string `json:"posting_number"`
// Products information
Products []Product `json:"products"`
// Date and time when the shipment processing started in the UTC format
InProccessAt time.Time `json:"in_process_at"`
// Warehouse identifier where the products for this shipment are stored
WarehouseId int64 `json:"warehouse_id"`
// Seller identifier
SellerId string `json:"seller_id"`
}
type Product struct {
// Product SKU
SKU int64 `json:"sku"`
// Product quantity
Quantity int64 `json:"quantity"`
}
// Shipment cancellation
type PostingCancelled struct {
Common
// Shipment number
PostingNumber string `json:"posting_number"`
// Products information
Products []Product `json:"products"`
// Previous shipment status
OldState string `json:"old_state"`
// New shipment status: posting_canceled—canceled
NewState string `json:"new_state"`
// Date and time when the shipment status was changed in UTC format
ChangedStateDate time.Time `json:"changed_state_date"`
// Information about cancellation reason
Reason Reason `json:"reason"`
// Warehouse identifier where the products for this shipment are stored
WarehouseId int64 `json:"warehouse_id"`
// Seller identifier
SellerId string `json:"seller_id"`
}
type Reason struct {
// Cancellation reason identifier
Id int64 `json:"id"`
// Cancellation reason
Message string `json:"message"`
}
// Shipment status change
type StateChanged struct {
Common
// Shipment number
PostingNumber string `json:"posting_number"`
// New shipment status
NewState string `json:"new_state"`
// Date and time when the shipment status was changed in UTC format
ChangedStateDate time.Time `json:"chagned_state_date"`
// Warehouse identifier where the products for this shipment are stored
WarehouseId int64 `json:"warehouse_id"`
// Seller identifier
SellerId string `json:"seller_id"`
}
// Shipment shipping date change
type CutoffDateChanged struct {
Common
// Shipment number
PostingNumber string `json:"posting_number"`
// New shipping date and time in UTC format
NewCutoffDate time.Time `json:"new_cutoff_date"`
// Previous shipping date and time in UTC format
OldCutoffDate time.Time `json:"old_cutoff_date"`
// Warehouse identifier where the products for this shipment are stored
WarehouseId int64 `json:"warehouse_id"`
// Seller identifier
SellerId string `json:"seller_id"`
}
// Shipment delivery date change
type DeliveryDateChanged struct {
Common
// Shipment number
PostingNumber string `json:"posting_number"`
// New delivery start date and time in UTC format
NewDeliveryDateBegin time.Time `json:"new_delivery_date_begin"`
// New delivery end date and time in UTC format
NewDeliveryDateEnd time.Time `json:"new_delivery_date_end"`
// Previous delivery start date and time in UTC format
OldDeliveryDateBegin time.Time `json:"old_delivery_date_begin"`
// Previous delivery end date and time in UTC format
OldDeliveryDateEnd time.Time `json:"old_delivery_date_end"`
// Warehouse identifier where the products for this shipment are stored
WarehouseId int64 `json:"warehouse_id"`
// Seller identifier
SellerId string `json:"seller_id"`
}
// Product creation and update or processing error
type CreateOrUpdateItem struct {
Common
// Product identifier in the seller's system
OfferId string `json:"offer_id"`
// Product identifier
ProductId int64 `json:"product_id"`
// An indication that errors occurred during the product creation or update
IsError bool `json:"is_error"`
// Update date and time
ChangedAt time.Time `json:"changed_at"`
// Seller identifier
SellerId string `json:"seller_id"`
}
// Product price index change
type PriceIndexChanged struct {
Common
// Date and time of price index change
UpdatedAt time.Time `json:"updated_at"`
// Product SKU
SKU int64 `json:"sku"`
// Product identifier
ProductId int64 `json:"product_id"`
// Price index
PriceIndex int64 `json:"price_index"`
// Seller identifier
SellerId string `json:"seller_id"`
}
// Stock change at the seller's warehouse
type StocksChanged struct {
Common
// Array with products data
Items []Item `json:"items"`
// Seller identifier
SellerId string `json:"seller_id"`
}
type Item struct {
// Update date and time
UpdatedAt time.Time `json:"updated_at"`
// Product SKU when working under the FBS or rFBS schemes
SKU int64 `json:"sku"`
// Product identifier
ProductId int64 `json:"product_id"`
// Array with product stocks data
Stocks []Stock `json:"stocks"`
}
type Stock struct {
// Warehouse identifier
WarehouseId int64 `json:"warehouse_id"`
// Total product stocks at the warehouse
Present int64 `json:"present"`
// Number of reserved products at the warehouse
Reserved int64 `json:"reserved"`
}
// New message in chat
type NewMessage struct {
Common
// Chat identifier
ChatId string `json:"chat_id"`
// Chat type
ChatType string `json:"chat_type"`
// Message identifier
MessageId string `json:"message_id"`
// Message creation date
CreatedAt time.Time `json:"created_at"`
// Information about message sender
User User `json:"user"`
// Array with message content in Markdown format
Data []string `json:"data"`
// Seller identifier
SellerId string `json:"seller_id"`
}
type User struct {
// Sender identifier
Id string `json:"id"`
// Sender type
Type string `json:"type"`
}
// Message in chat has changed
type UpdateMessage struct {
NewMessage
// Message update date
UpdatedAt time.Time `json:"updated_at"`
}
// Customer or support read your message
type MessageRead struct {
NewMessage
// Last read message identifier
LastReadMessageId string `json:"last_read_message_id"`
}
// Chat is closed
type ChatClosed struct {
Common
// Chat identifier
ChatId string `json:"chat_id"`
// Chat type
ChatType string `json:"chat_type"`
// Information about the user who closed the chat
User User `json:"user"`
// User identifier
Id string `json:"id"`
// User type
Type string `json:"type"`
// Seller identifier
SellerId string `json:"seller_id"`
}
type Response struct {
// Notification is received
Result bool `json:"result"`
}
type errorResponse struct {
// Information about the error
Data errorData `json:"error"`
}
type errorData struct {
// Error code
Code string `json:"code"`
// Detailed error description
Message string `json:"message"`
// Additional information
Details string `json:"details"`
}

View File

@@ -1912,6 +1912,9 @@ type GetProductPriceInfoResult struct {
}
type GetPRoductPriceInfoResultItem struct {
// Maximum acquiring fee
Acquiring int32 `json:"acquiring"`
// Commissions information
Commissions GetProductPriceInfoResultItemCommission `json:"commissions"`
@@ -1970,10 +1973,10 @@ type GetProductPriceInfoResultItemCommission struct {
// Pipeline from (FBS)
FBSPipelineFrom float64 `json:"fbs_direct_flow_trans_min_amount"`
// Shipment processing fee to (FBS)
// Minimal shipment processing fee (FBS) — 0 rubles
FBSShipmentProcessingToFee float64 `json:"fbs_first_mile_min_amount"`
// Shipment processing fee from (FBS)
// Maximal shipment processing fee (FBS) — 25 rubles
FBSShipmentProcessingFromFee float64 `json:"Shipment processing fee from (FBS)"`
// Return and cancellation fees, shipment processing (FBS)
@@ -1985,7 +1988,13 @@ type GetProductPriceInfoResultItemCommission struct {
// Return and cancellation fees, pipeline from (FBS)
FBSReturnCancellationFromFees float64 `json:"fbs_return_flow_trans_min_amount"`
// Sales commission percentage (FBO and FBS)
// Sales commission percentage (FBO)
SalesCommissionFBORate float64 `json:"sales_percent_fbo"`
// Sales commission percentage (FBS)
SalesCommissionFBSRate float64 `json:"sales_percent_fbs"`
// Larger sales commission percentage among FBO and FBS
SalesCommissionRate float64 `json:"sales_percent"`
}

View File

@@ -2240,6 +2240,7 @@ func TestGetProductPriceInfo(t *testing.T) {
"result": {
"items": [
{
"acquiring": 0,
"product_id": 243686911,
"offer_id": "356792",
"price": {
@@ -2255,9 +2256,28 @@ func TestGetProductPriceInfo(t *testing.T) {
"marketing_seller_price": "",
"auto_action_enabled": true
},
"price_index": "0.00",
"price_indexes": {
"external_index_data": {
"minimal_price": "string",
"minimal_price_currency": "string",
"price_index_value": 0
},
"ozon_index_data": {
"minimal_price": "string",
"minimal_price_currency": "string",
"price_index_value": 0
},
"price_index": "WITHOUT_INDEX",
"self_marketplaces_index_data": {
"minimal_price": "string",
"minimal_price_currency": "string",
"price_index_value": 0
}
},
"commissions": {
"sales_percent": 15,
"sales_percent_fbo": 15,
"sales_percent_fbs": 0,
"fbo_fulfillment_amount": 0,
"fbo_direct_flow_trans_min_amount": 31,
"fbo_direct_flow_trans_max_amount": 46.5,
@@ -2266,7 +2286,7 @@ func TestGetProductPriceInfo(t *testing.T) {
"fbo_return_flow_trans_min_amount": 21.7,
"fbo_return_flow_trans_max_amount": 21.7,
"fbs_first_mile_min_amount": 0,
"fbs_first_mile_max_amount": 0,
"fbs_first_mile_max_amount": 25,
"fbs_direct_flow_trans_min_amount": 41,
"fbs_direct_flow_trans_max_amount": 61.5,
"fbs_deliv_to_customer_amount": 60,