From 5f58965975cc4202a09f5783d09197c29eadcec1 Mon Sep 17 00:00:00 2001 From: Sinh Date: Fri, 16 Sep 2022 17:43:13 +0700 Subject: [PATCH] define onpoint client --- go.mod | 2 +- go.sum | 2 + partnerapi/onpoint/const.go | 21 +++ partnerapi/onpoint/env.go | 9 ++ partnerapi/onpoint/error.go | 14 ++ partnerapi/onpoint/model_request.go | 83 ++++++++++ partnerapi/onpoint/model_response.go | 52 +++++++ partnerapi/onpoint/onpoint.go | 220 +++++++++++++++++++++++++++ partnerapi/onpoint/status.go | 17 +++ partnerapi/onpoint/util.go | 18 +++ 10 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 partnerapi/onpoint/const.go create mode 100644 partnerapi/onpoint/env.go create mode 100644 partnerapi/onpoint/error.go create mode 100644 partnerapi/onpoint/model_request.go create mode 100644 partnerapi/onpoint/model_response.go create mode 100644 partnerapi/onpoint/onpoint.go create mode 100644 partnerapi/onpoint/status.go create mode 100644 partnerapi/onpoint/util.go diff --git a/go.mod b/go.mod index 881b3a0..926675b 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.17 require ( github.com/Selly-Modules/logger v0.0.1 - github.com/Selly-Modules/natsio v1.0.2-0.20220826163751-df340fefda0a + github.com/Selly-Modules/natsio v1.0.2-0.20220913022818-738089755ab0 github.com/nats-io/nats.go v1.13.0 github.com/thoas/go-funk v0.9.1 ) diff --git a/go.sum b/go.sum index bb59017..68d9a75 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/Selly-Modules/logger v0.0.1 h1:dwLLtW53FfVBlklhdtFRB63eP0ofIh0IUQ/Gjg github.com/Selly-Modules/logger v0.0.1/go.mod h1:RWhSQ3F01an8KD00VjzRBZOMcE5eV2Cy0/l4ZkeieyU= github.com/Selly-Modules/natsio v1.0.2-0.20220826163751-df340fefda0a h1:QmJ8iqksbU+1vUa2SijsAMSLnw1C5X4R8PYjuNPNCCE= github.com/Selly-Modules/natsio v1.0.2-0.20220826163751-df340fefda0a/go.mod h1:q9dqmiMyl9MUVYZsvAWDI85083rnLEGAEFfYajLOLUU= +github.com/Selly-Modules/natsio v1.0.2-0.20220913022818-738089755ab0 h1:viwbSgCFHHsiWIR2aOr6f0fuKgkWs9DR8A+xQgyoCDs= +github.com/Selly-Modules/natsio v1.0.2-0.20220913022818-738089755ab0/go.mod h1:q9dqmiMyl9MUVYZsvAWDI85083rnLEGAEFfYajLOLUU= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= diff --git a/partnerapi/onpoint/const.go b/partnerapi/onpoint/const.go new file mode 100644 index 0000000..cc7fda3 --- /dev/null +++ b/partnerapi/onpoint/const.go @@ -0,0 +1,21 @@ +package onpoint + +const ( + baseURLStaging = "https://dev-api.onpoint.vn" + + apiPathCreateOrder = "/v1/orders" + apiPathUpdateDelivery = "/v1/orders/delivery/update" + apiPathCancelOrder = "/v1/orders/cancel" + apiPathGetChannels = "/v1/channels" + + headerXAPIKey = "x-api-key" + headerXTimestamp = "x-timestamp" + headerXSignature = "x-signature" +) + +var ( + baseURLENVMapping = map[ENV]string{ + // EnvProd: baseURLProd, + EnvStaging: baseURLStaging, + } +) diff --git a/partnerapi/onpoint/env.go b/partnerapi/onpoint/env.go new file mode 100644 index 0000000..ebcc363 --- /dev/null +++ b/partnerapi/onpoint/env.go @@ -0,0 +1,9 @@ +package onpoint + +// ENV ... +type ENV string + +const ( + EnvStaging ENV = "STAGING" + EnvProd ENV = "PROD" +) diff --git a/partnerapi/onpoint/error.go b/partnerapi/onpoint/error.go new file mode 100644 index 0000000..660d39a --- /dev/null +++ b/partnerapi/onpoint/error.go @@ -0,0 +1,14 @@ +package onpoint + +import "fmt" + +// Error ... +type Error struct { + Message string `json:"message"` + Status string `json:"status"` +} + +// Error ... +func (e Error) Error() string { + return fmt.Sprintf("onpoint_err: status %s, message: %s", e.Status, e.Message) +} diff --git a/partnerapi/onpoint/model_request.go b/partnerapi/onpoint/model_request.go new file mode 100644 index 0000000..cd5754a --- /dev/null +++ b/partnerapi/onpoint/model_request.go @@ -0,0 +1,83 @@ +package onpoint + +import "time" + +/* + * Request payload + */ + +// CreateOrderRequest ... +type CreateOrderRequest struct { + PartnerOrderCode string `json:"partner_order_code"` + OrderDate time.Time `json:"order_date"` + ChannelCode string `json:"channel_code"` + FullName string `json:"full_name"` + Email string `json:"email"` + Phone string `json:"phone"` + Address string `json:"address"` + DistrictCode string `json:"district_code"` + WardCode string `json:"ward_code"` + ProvinceCode string `json:"province_code"` + Note string `json:"note"` + SubtotalPrice int `json:"subtotal_price"` + ShippingFee int `json:"shipping_fee"` + TotalDiscounts int `json:"total_discounts"` + TotalPrice int `json:"total_price"` + PaymentMethod string `json:"payment_method"` + DeliveryPlatform string `json:"delivery_platform"` + Items []OrderItem `json:"items"` +} + +// OrderItem ... +type OrderItem struct { + SellingPrice int `json:"selling_price"` + Quantity int `json:"quantity"` + Uom string `json:"uom"` + Amount int `json:"amount"` + Name string `json:"name"` + PartnerSku string `json:"partner_sku"` +} + +// UpdateOrderDeliveryRequest ... +type UpdateOrderDeliveryRequest struct { + OrderNo string `json:"order_no"` // required + DeliveryPlatform string `json:"delivery_platform"` // required + DeliveryTrackingNumber string `json:"delivery_tracking_number"` + ShippingLabel string `json:"shipping_label"` +} + +// CancelOrderRequest ... +type CancelOrderRequest struct { + OrderNo string `json:"order_no"` +} + +/** + * WEBHOOK ONPOINT + */ + +// WebhookPayload ... +type WebhookPayload struct { + Event string `json:"event"` + RequestedAt time.Time `json:"requested_at"` + Data interface{} `json:"data"` +} + +// WebhookDataUpdateInventory ... +type WebhookDataUpdateInventory struct { + Sku string `json:"sku"` + PartnerSku string `json:"partner_sku"` + WarehouseCode string `json:"warehouse_code"` + AvailableQuantity int `json:"available_quantity"` + CommittedQuantity int `json:"committed_quantity"` + TotalQuantity int `json:"total_quantity"` + UpdatedAt time.Time `json:"updated_at"` +} + +// WebhookDataUpdateOrderStatus ... +type WebhookDataUpdateOrderStatus struct { + PartnerOrderCode string `json:"partner_order_code"` + OrderNo string `json:"order_no"` + Status string `json:"status"` + DeliveryStatus string `json:"delivery_status"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/partnerapi/onpoint/model_response.go b/partnerapi/onpoint/model_response.go new file mode 100644 index 0000000..784042d --- /dev/null +++ b/partnerapi/onpoint/model_response.go @@ -0,0 +1,52 @@ +package onpoint + +import "time" + +// CreateOrderResponse ... +type CreateOrderResponse struct { + PartnerOrderCode string `json:"partner_order_code"` + OrderNo string `json:"order_no"` + OrderDate time.Time `json:"order_date"` + ChannelCode string `json:"channel_code"` + FullName string `json:"full_name"` + Email string `json:"email"` + Phone string `json:"phone"` + Address string `json:"address"` + FullAddress string `json:"full_address"` + District string `json:"district"` + Ward string `json:"ward"` + Province string `json:"province"` + DistrictCode string `json:"district_code"` + WardCode string `json:"ward_code"` + ProvinceCode string `json:"province_code"` + Note string `json:"note"` + SubtotalPrice int `json:"subtotal_price"` + ShippingFee int `json:"shipping_fee"` + TotalDiscounts int `json:"total_discounts"` + TotalPrice int `json:"total_price"` + PaymentMethod string `json:"payment_method"` + DeliveryPlatform string `json:"delivery_platform"` + Status string `json:"status"` + UpdatedAt time.Time `json:"updated_at"` + InsertedAt time.Time `json:"inserted_at"` + Items []OrderItem `json:"items"` +} + +// UpdateOrderDeliveryResponse ... +type UpdateOrderDeliveryResponse struct { + DeliveryPlatform string `json:"delivery_platform"` + DeliveryTrackingNumber string `json:"delivery_tracking_number"` + ShippingLabel string `json:"shipping_label"` +} + +// CancelOrderResponse ... +type CancelOrderResponse struct { + OrderNo string `json:"order_no"` + Status string `json:"status"` +} + +// ChannelResponse ... +type ChannelResponse struct { + Code string `json:"code"` + Name string `json:"name"` +} diff --git a/partnerapi/onpoint/onpoint.go b/partnerapi/onpoint/onpoint.go new file mode 100644 index 0000000..f8ce7f9 --- /dev/null +++ b/partnerapi/onpoint/onpoint.go @@ -0,0 +1,220 @@ +package onpoint + +import ( + "errors" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/Selly-Modules/natsio" + "github.com/Selly-Modules/natsio/model" + "github.com/Selly-Modules/natsio/subject" + + "github.com/Selly-Modules/3pl/util/pjson" +) + +// Client ... +type Client struct { + env ENV + apiKey string + secretKey string + natsClient natsio.Server +} + +// NewClient generate OnPoint client +func NewClient(env ENV, apiKey, secretKey string, nc natsio.Server) (*Client, error) { + if apiKey == "" { + return nil, errors.New("onpoint: cannot init with empty api key") + } + return &Client{ + env: env, + apiKey: apiKey, + secretKey: secretKey, + natsClient: nc, + }, nil +} + +// CreateOrder ... +func (c *Client) CreateOrder(p CreateOrderRequest) (*CreateOrderResponse, error) { + url := c.getBaseURL() + apiPathCreateOrder + natsPayload := model.CommunicationRequestHttp{ + ResponseImmediately: true, + Payload: model.HttpRequest{ + URL: url, + Method: http.MethodPost, + Data: pjson.ToJSONString(p), + }, + } + var ( + r model.CommunicationHttpResponse + errRes Error + dataRes struct { + Data CreateOrderResponse `json:"data"` + } + ) + if err := c.requestHttpViaNats(natsPayload, &r); err != nil { + return nil, err + } + res := r.Response + if res == nil { + return nil, fmt.Errorf("onpoint.Client.CreateOrder: empty_response") + } + if res.StatusCode >= http.StatusBadRequest { + if err := r.ParseResponseData(&errRes); err != nil { + return nil, fmt.Errorf("onpoint.Client.CreateOrder: parse_response_err: %v", err) + } + return nil, errRes + } + if err := r.ParseResponseData(&dataRes); err != nil { + return nil, fmt.Errorf("onpoint.Client.CreateOrder: parse_response_data: %v", err) + } + + return &dataRes.Data, nil +} + +// UpdateDelivery ... +func (c *Client) UpdateDelivery(p UpdateOrderDeliveryRequest) (*UpdateOrderDeliveryResponse, error) { + url := c.getBaseURL() + apiPathUpdateDelivery + natsPayload := model.CommunicationRequestHttp{ + ResponseImmediately: true, + Payload: model.HttpRequest{ + URL: url, + Method: http.MethodPost, + Data: pjson.ToJSONString(p), + }, + } + var ( + r model.CommunicationHttpResponse + errRes Error + dataRes struct { + Data UpdateOrderDeliveryResponse `json:"data"` + } + ) + if err := c.requestHttpViaNats(natsPayload, &r); err != nil { + return nil, err + } + res := r.Response + if res == nil { + return nil, fmt.Errorf("onpoint.Client.UpdateDelivery: empty_response") + } + if res.StatusCode >= http.StatusBadRequest { + if err := r.ParseResponseData(&errRes); err != nil { + return nil, fmt.Errorf("onpoint.Client.UpdateDelivery: parse_response_err: %v", err) + } + return nil, errRes + } + if err := r.ParseResponseData(&dataRes); err != nil { + return nil, fmt.Errorf("onpoint.Client.UpdateDelivery: parse_response_data: %v", err) + } + + return &dataRes.Data, nil +} + +// CancelOrder ... +func (c *Client) CancelOrder(p CancelOrderRequest) (*CancelOrderResponse, error) { + url := c.getBaseURL() + apiPathCancelOrder + natsPayload := model.CommunicationRequestHttp{ + ResponseImmediately: true, + Payload: model.HttpRequest{ + URL: url, + Method: http.MethodPost, + Data: pjson.ToJSONString(p), + }, + } + var ( + r model.CommunicationHttpResponse + errRes Error + dataRes struct { + Data CancelOrderResponse `json:"data"` + } + ) + if err := c.requestHttpViaNats(natsPayload, &r); err != nil { + return nil, err + } + res := r.Response + if res == nil { + return nil, fmt.Errorf("onpoint.Client.CancelOrder: empty_response") + } + if res.StatusCode >= http.StatusBadRequest { + if err := r.ParseResponseData(&errRes); err != nil { + return nil, fmt.Errorf("onpoint.Client.CancelOrder: parse_response_err: %v", err) + } + return nil, errRes + } + if err := r.ParseResponseData(&dataRes); err != nil { + return nil, fmt.Errorf("onpoint.Client.CancelOrder: parse_response_data: %v", err) + } + + return &dataRes.Data, nil +} + +// GetChannels ... +func (c *Client) GetChannels() ([]ChannelResponse, error) { + url := c.getBaseURL() + apiPathGetChannels + natsPayload := model.CommunicationRequestHttp{ + ResponseImmediately: true, + Payload: model.HttpRequest{ + URL: url, + Method: http.MethodGet, + }, + } + var ( + r model.CommunicationHttpResponse + errRes Error + dataRes struct { + Data []ChannelResponse `json:"data"` + } + ) + if err := c.requestHttpViaNats(natsPayload, &r); err != nil { + return nil, err + } + res := r.Response + if res == nil { + return nil, fmt.Errorf("onpoint.Client.GetChannels: empty_response") + } + if res.StatusCode >= http.StatusBadRequest { + if err := r.ParseResponseData(&errRes); err != nil { + return nil, fmt.Errorf("onpoint.Client.GetChannels: parse_response_err: %v", err) + } + return nil, errRes + } + if err := r.ParseResponseData(&dataRes); err != nil { + return nil, fmt.Errorf("onpoint.Client.GetChannels: parse_response_data: %v", err) + } + + return dataRes.Data, nil +} + +func (c *Client) requestHttpViaNats(data model.CommunicationRequestHttp, res interface{}) error { + ec, err := c.natsClient.NewJSONEncodedConn() + if err != nil { + return fmt.Errorf("onpoint: request via nats %v", err) + } + qs := "" + for k, v := range data.Payload.Query { + qs += k + "=" + v + } + now := time.Now().Unix() + ts := strconv.FormatInt(now, 10) + arr := []string{ + qs, + data.Payload.Data, + ts, + } + s := strings.Join(arr, ".") + // sign data + sign := hashSHA256AndUppercase(s, c.secretKey) + data.Payload.Header = map[string]string{ + headerXAPIKey: c.apiKey, + headerXSignature: sign, + headerXTimestamp: ts, + } + + return ec.Request(subject.Communication.RequestHTTP, data, res) +} + +func (c *Client) getBaseURL() string { + return baseURLENVMapping[c.env] +} diff --git a/partnerapi/onpoint/status.go b/partnerapi/onpoint/status.go new file mode 100644 index 0000000..96cc880 --- /dev/null +++ b/partnerapi/onpoint/status.go @@ -0,0 +1,17 @@ +package onpoint + +const ( + OrderStatusNew = "new" + OrderStatusPendingWarehouse = "pending_warehouse" + OrderStatusWhProcessing = "wh_processing" + OrderStatusWhCompleted = "wh_completed" + OrderStatusDlPending = "dl_pending" + OrderStatusDlIntransit = "dl_intransit" + OrderStatusDLDelivered = "dl_delivered" + OrderStatusDLReturning = "dl_returning" + OrderStatusReturned = "returned" + OrderStatusPartialCancelled = "partial_cancelled" + OrderStatusCancelled = "cancelled" + OrderStatusCompleted = "completed" + OrderStatusUnknown = "unknown" +) diff --git a/partnerapi/onpoint/util.go b/partnerapi/onpoint/util.go new file mode 100644 index 0000000..1e0ee51 --- /dev/null +++ b/partnerapi/onpoint/util.go @@ -0,0 +1,18 @@ +package onpoint + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "strings" +) + +func hashSHA256(data, key string) string { + h := hmac.New(sha256.New, []byte(key)) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} + +func hashSHA256AndUppercase(data, key string) string { + return strings.ToUpper(hashSHA256(data, key)) +}