diff --git a/partnerapi/globalcare/globale_care.go b/partnerapi/globalcare/globale_care.go index d3cf669..fd3744e 100644 --- a/partnerapi/globalcare/globale_care.go +++ b/partnerapi/globalcare/globale_care.go @@ -167,7 +167,7 @@ func (c *Client) GetOrder(orderCode string) (*GetOrderResponseDecoded, error) { } func (c *Client) requestNats(subject string, data interface{}) (*nats.Msg, error) { - b := toBytes(data) + b := pjson.ToBytes(data) return c.natsClient.Request(subject, b) } diff --git a/partnerapi/globalcare/util.go b/partnerapi/globalcare/util.go index 600d4ac..e7f2755 100644 --- a/partnerapi/globalcare/util.go +++ b/partnerapi/globalcare/util.go @@ -3,27 +3,10 @@ package globalcare import ( "crypto/rsa" "crypto/x509" - "encoding/json" "encoding/pem" "fmt" - - "git.selly.red/Selly-Modules/logger" ) -// toBytes ... -func toBytes(data interface{}) []byte { - b, err := json.Marshal(data) - if err != nil { - logger.Error("pjson.toBytes", logger.LogData{"payload": data}) - } - return b -} - -// toJSONString ... -func toJSONString(data interface{}) string { - return string(toBytes(data)) -} - // GeneratePublicKeyFromBytes ... func generatePublicKeyFromBytes(b []byte) (*rsa.PublicKey, error) { pubPem, _ := pem.Decode(b) diff --git a/partnerapi/shiip/const.go b/partnerapi/shiip/const.go new file mode 100644 index 0000000..0fbeab1 --- /dev/null +++ b/partnerapi/shiip/const.go @@ -0,0 +1,54 @@ +package shiip + +const ( + TimeLayout = "2006-01-02 15:04:05" + + apiPathCreateOutboundRequest = "/v1/api/external/vietful/outbound/requests" + apiPathGetOutboundRequest = "/v1/api/external/vietful/outbound/requests/%d" + apiPathCancelOutboundRequest = "/v1/api/external/outbound/requests/%d/cancel" + apiPathUpdateLogisticInfoOutboundRequest = "/v1/api/external/outbound/requests/%d/logistic-info" + apiPathAuth = "/v1/api/external/vietful/auth/access-token" + + PriorityUrgent = 3 + PriorityHigh = 2 + PriorityNormal = 1 + + TPLCodeGHN = "GHN" + TPLCodeGHTK = "GHTK" + TPLCodeBest = "BEST" + TPLCodeSnappy = "SPY" + TPLCodeViettelPost = "VTP" + TPLCodeSellyExpress = "SE" + TPLCodeJTExpress = "JTE" + + ShippingServiceCodeSTD = "STD" + ORTypeOrder = 1 + ShippingTypeSelfShip = 1 + PackTypeNormal = 1 + BizTypeB2C = 1 + ConditionTypeCodeNew = "NEW" +) + +const ( + baseURLAuthStaging = "https://api.shiip.vn" + baseURLStaging = "https://api.shiip.vn" + + // TODO: add base URL + baseURLAuthProd = "" + baseURLProd = "" +) + +const ( + ErrCodeExistPartnerCode = "exist_partner_code" +) + +var ( + baseURLENVMapping = map[ENV]string{ + EnvProd: baseURLProd, + EnvStaging: baseURLStaging, + } + baseURLAuthENVMapping = map[ENV]string{ + EnvProd: baseURLAuthProd, + EnvStaging: baseURLAuthStaging, + } +) diff --git a/partnerapi/shiip/env.go b/partnerapi/shiip/env.go new file mode 100644 index 0000000..cdea36f --- /dev/null +++ b/partnerapi/shiip/env.go @@ -0,0 +1,9 @@ +package shiip + +// ENV ... +type ENV string + +const ( + EnvStaging ENV = "STAGING" + EnvProd ENV = "PROD" +) diff --git a/partnerapi/shiip/error.go b/partnerapi/shiip/error.go new file mode 100644 index 0000000..e0e7167 --- /dev/null +++ b/partnerapi/shiip/error.go @@ -0,0 +1,22 @@ +package shiip + +import ( + "fmt" +) + +// Error ... +type Error struct { + Code string `json:"code"` + Message string `json:"errorMessage"` +} + +// Error ... +func (e Error) Error() string { + return fmt.Sprintf("tnc_err: code %s, messsage %s", e.Code, e.Message) +} + +// IsErrExistPartnerCode ... +func IsErrExistPartnerCode(err error) bool { + e, ok := err.(Error) + return ok && e.Code == ErrCodeExistPartnerCode +} diff --git a/partnerapi/shiip/model_request.go b/partnerapi/shiip/model_request.go new file mode 100644 index 0000000..9a46c37 --- /dev/null +++ b/partnerapi/shiip/model_request.go @@ -0,0 +1,57 @@ +package shiip + +// Product ... +type Product struct { + PartnerSKU string `json:"partnerSKU"` + UnitCode string `json:"unitCode"` + ConditionTypeCode string `json:"conditionTypeCode"` + Quantity int64 `json:"quantity"` +} + +// Address ... +type Address struct { + AddressNo string `json:"addressNo"` + ProvinceCode string `json:"provinceCode,omitempty"` + DistrictCode string `json:"districtCode,omitempty"` + WardCode string `json:"wardCode,omitempty"` +} + +// OutboundRequestPayload ... +type OutboundRequestPayload struct { + WarehouseCode string `json:"warehouseCode"` + ShippingServiceCode string `json:"shippingServiceCode"` + PartnerORCode string `json:"partnerORCode"` + PartnerRefId string `json:"partnerRefId"` + RefCode string `json:"refCode"` + CodAmount float64 `json:"codAmount"` + PriorityType int `json:"priorityType"` + CustomerName string `json:"customerName"` + CustomerPhoneNumber string `json:"customerPhoneNumber"` + Type int `json:"type"` + ShippingType int `json:"shippingType"` + VehicleNumber string `json:"vehicleNumber"` + ContainerNumber string `json:"containerNumber"` + PackType int `json:"packType"` + PackingNote string `json:"packingNote"` + CustomLabel bool `json:"customLabel"` + BizType int `json:"bizType"` + Note string `json:"note"` + ShippingAddress Address `json:"shippingAddress"` + Products []Product `json:"products"` + PartnerCreationTime string `json:"partnerCreationTime"` + TPLCode string `json:"tplCode"` + TrackingCode string `json:"trackingCode"` +} + +// UpdateORLogisticInfoPayload ... +type UpdateORLogisticInfoPayload struct { + OrID int `json:"orId"` + TrackingCode string `json:"trackingCode"` + ShippingLabels []Label `json:"shippingLabels"` + TPLCode string `json:"tplCode"` +} + +type Label struct { + Caption string `json:"caption"` + URI string `json:"uri"` +} diff --git a/partnerapi/shiip/model_response.go b/partnerapi/shiip/model_response.go new file mode 100644 index 0000000..7abefd3 --- /dev/null +++ b/partnerapi/shiip/model_response.go @@ -0,0 +1,57 @@ +package shiip + +// OutboundRequestRes ... +type OutboundRequestRes struct { + OrID int `json:"orId"` + OrCode string `json:"orCode"` + PartnerORCode string `json:"partnerORCode"` + Error *Error `json:"error"` +} + +type authRes struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + RefreshExpiresIn int `json:"refresh_expires_in"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` + NotBeforePolicy int `json:"not-before-policy"` + SessionState string `json:"session_state"` + Scope string `json:"scope"` +} + +// Webhook ... +type Webhook struct { + OrId int `json:"orId"` + PartnerORCode string `json:"partnerORCode"` + ErrorCode string `json:"errorCode"` + ErrorMessage string `json:"errorMessage"` + Event string `json:"event"` + Id string `json:"id"` + Timestamp int64 `json:"timestamp"` + Note string `json:"note"` + OrCode string `json:"orCode"` +} + +// OutboundRequestInfo ... +type OutboundRequestInfo struct { + OrId int `json:"orId"` + OrCode string `json:"orCode"` + PartnerORCode string `json:"partnerORCode"` + OriginalPartnerOrCode string `json:"originalPartnerOrCode"` + PartnerRefId string `json:"partnerRefId"` + RefCode string `json:"refCode"` + WarehouseCode string `json:"warehouseCode"` + Status string `json:"status"` + Note string `json:"note"` + ShippingType int `json:"shippingType"` + PriorityType int `json:"priorityType"` + PackType int `json:"packType"` + BizType int `json:"bizType"` + CustomerName string `json:"customerName"` + CustomerPhoneNumber string `json:"customerPhoneNumber"` + ShippingFullAddress string `json:"shippingFullAddress"` + CodAmount float64 `json:"codAmount"` + ExpectedDeliveryTime string `json:"expectedDeliveryTime"` + CreatedDate string `json:"createdDate"` + ErrorMessage string `json:"errorMessage"` +} diff --git a/partnerapi/shiip/shiip.go b/partnerapi/shiip/shiip.go new file mode 100644 index 0000000..6ba7352 --- /dev/null +++ b/partnerapi/shiip/shiip.go @@ -0,0 +1,298 @@ +package shiip + +import ( + "fmt" + "log" + "net/http" + "time" + + "git.selly.red/Selly-Modules/natsio" + "git.selly.red/Selly-Modules/natsio/model" + "git.selly.red/Selly-Modules/natsio/subject" + "github.com/nats-io/nats.go" + + "git.selly.red/Selly-Modules/3pl/util/httputil" + "git.selly.red/Selly-Modules/3pl/util/pjson" +) + +// Client ... +type Client struct { + realm string + clientID string + clientSecret string + + env ENV + natsClient natsio.Server + + token string + tokenExpireAt time.Time +} + +// NewClient ... +func NewClient(env ENV, clientID, clientSecret, realm string, natsClient natsio.Server) (*Client, error) { + if env != EnvProd && env != EnvStaging { + return nil, fmt.Errorf("shiip.NewClient: invalid_env %s", env) + } + return &Client{ + realm: realm, + clientID: clientID, + clientSecret: clientSecret, + token: "", + env: env, + natsClient: natsClient, + }, nil +} + +// CreateOutboundRequest ... +func (c *Client) CreateOutboundRequest(p OutboundRequestPayload) (*OutboundRequestRes, error) { + apiURL := c.getBaseURL() + apiPathCreateOutboundRequest + natsPayload := model.CommunicationRequestHttp{ + ResponseImmediately: true, + Payload: model.HttpRequest{ + URL: apiURL, + Method: http.MethodPost, + Data: pjson.ToJSONString([]OutboundRequestPayload{p}), + Header: c.getRequestHeader(), + }, + } + msg, err := c.requestHttpViaNats(natsPayload) + if err != nil { + log.Printf("shiip.Client.CreateOutboundRequest - requestHttpViaNats: %v, %+v\n", err, natsPayload) + return nil, err + } + var ( + r model.CommunicationHttpResponse + errRes Error + dataRes []OutboundRequestRes + ) + if err = pjson.Unmarshal(msg.Data, &r); err != nil { + return nil, fmt.Errorf("shiip.Client.CreateOutboundRequest: parse_data %v", err) + } + res := r.Response + if res == nil { + return nil, fmt.Errorf("shiip.Client.CreateOutboundRequest: empty_response") + } + if res.StatusCode >= http.StatusBadRequest { + if err = r.ParseResponseData(&errRes); err != nil { + return nil, fmt.Errorf("shiip.Client.CreateOutboundRequest: parse_response_err: %v", err) + } + return nil, errRes + } + if err = r.ParseResponseData(&dataRes); err != nil { + return nil, fmt.Errorf("shiip.Client.CreateOutboundRequest: parse_response_data: %v", err) + } + if len(dataRes) == 0 { + return nil, fmt.Errorf("shiip.Client.CreateOutboundRequest: empty_result") + } + item := &dataRes[0] + e := item.Error + if e != nil { + return nil, errRes + } + + return item, err +} + +// UpdateOutboundRequestLogisticInfo ... +func (c *Client) UpdateOutboundRequestLogisticInfo(p UpdateORLogisticInfoPayload) error { + apiURL := c.getBaseURL() + fmt.Sprintf(apiPathUpdateLogisticInfoOutboundRequest, p.OrID) + natsPayload := model.CommunicationRequestHttp{ + ResponseImmediately: true, + Payload: model.HttpRequest{ + URL: apiURL, + Method: http.MethodPost, + Header: c.getRequestHeader(), + Data: pjson.ToJSONString(p), + }, + } + msg, err := c.requestHttpViaNats(natsPayload) + if err != nil { + log.Printf("shiip.Client.UpdateOutboundRequestLogisticInfo - requestHttpViaNats: %v, %+v\n", err, natsPayload) + return err + } + var ( + r model.CommunicationHttpResponse + errRes Error + ) + if err = pjson.Unmarshal(msg.Data, &r); err != nil { + return fmt.Errorf("shiip.Client.UpdateOutboundRequestLogisticInfo: parse_data %v", err) + } + res := r.Response + if res == nil { + return fmt.Errorf("shiip.Client.UpdateOutboundRequestLogisticInfo: empty_response") + } + if res.StatusCode >= http.StatusBadRequest { + if err = r.ParseResponseData(&errRes); err != nil { + return fmt.Errorf("shiip.Client.UpdateOutboundRequestLogisticInfo: parse_response_err: %v", err) + } + return errRes + } + return nil +} + +// GetOutboundRequestByID ... +func (c *Client) GetOutboundRequestByID(requestID int) (*OutboundRequestInfo, error) { + apiURL := c.getBaseURL() + fmt.Sprintf(apiPathGetOutboundRequest, requestID) + natsPayload := model.CommunicationRequestHttp{ + ResponseImmediately: true, + Payload: model.HttpRequest{ + URL: apiURL, + Method: http.MethodGet, + Header: c.getRequestHeader(), + }, + } + msg, err := c.requestHttpViaNats(natsPayload) + if err != nil { + log.Printf("shiip.Client.GetOutboundRequestByID - requestHttpViaNats: %v, %+v\n", err, natsPayload) + return nil, err + } + var ( + r model.CommunicationHttpResponse + errRes Error + outboundRequest OutboundRequestInfo + ) + if err = pjson.Unmarshal(msg.Data, &r); err != nil { + return nil, fmt.Errorf("shiip.Client.GetOutboundRequestByID: parse_data %v", err) + } + res := r.Response + if res == nil { + return nil, fmt.Errorf("shiip.Client.GetOutboundRequestByID: empty_response") + } + if res.StatusCode >= http.StatusBadRequest { + if err = r.ParseResponseData(&errRes); err != nil { + return nil, fmt.Errorf("shiip.Client.GetOutboundRequestByID: parse_response_err: %v", err) + } + return nil, errRes + } + if err = r.ParseResponseData(&outboundRequest); err != nil { + return nil, fmt.Errorf("shiip.Client.GetOutboundRequestByID: parse_response_data: %v", err) + } + return &outboundRequest, nil +} + +// CancelOutboundRequest ... +func (c *Client) CancelOutboundRequest(requestID int, note string) error { + apiURL := c.getBaseURL() + fmt.Sprintf(apiPathCancelOutboundRequest, requestID) + data := map[string]string{"note": note} + natsPayload := model.CommunicationRequestHttp{ + ResponseImmediately: true, + Payload: model.HttpRequest{ + URL: apiURL, + Method: http.MethodPost, + Header: c.getRequestHeader(), + Data: pjson.ToJSONString(data), + }, + } + msg, err := c.requestHttpViaNats(natsPayload) + if err != nil { + log.Printf("shiip.Client.CancelOutboundRequest - requestHttpViaNats: %v, %+v\n", err, natsPayload) + return err + } + var ( + r model.CommunicationHttpResponse + errRes Error + ) + if err = pjson.Unmarshal(msg.Data, &r); err != nil { + return fmt.Errorf("shiip.Client.CancelOutboundRequest: parse_data %v", err) + } + res := r.Response + if res == nil { + return fmt.Errorf("shiip.Client.CancelOutboundRequest: empty_response") + } + if res.StatusCode >= http.StatusBadRequest { + if err = r.ParseResponseData(&errRes); err != nil { + return fmt.Errorf("shiip.Client.CancelOutboundRequest: parse_response_err: %v", err) + } + return errRes + } + return nil +} + +func (c *Client) auth() (*authRes, error) { + // v := url.Values{} + // v.Add("realm", c.realm) + // v.Add("grant_type", "client_credentials") + // v.Add("client_id", c.clientID) + // v.Add("client_secret", c.clientSecret) + // + // body := v.Encode() + header := map[string]string{ + httputil.HeaderKeyContentType: httputil.HeaderValueApplicationJSON, + } + apiURL := baseURLAuthENVMapping[c.env] + apiPathAuth + natsPayload := model.CommunicationRequestHttp{ + ResponseImmediately: true, + Payload: model.HttpRequest{ + URL: apiURL, + Method: http.MethodGet, + // Data: body, + Header: header, + }, + } + msg, err := c.requestHttpViaNats(natsPayload) + if err != nil { + log.Printf("shiip.Client.auth - requestHttpViaNats: %v, %+v\n", err, natsPayload) + return nil, err + } + var ( + r model.CommunicationHttpResponse + errRes Error + data authRes + ) + if err = pjson.Unmarshal(msg.Data, &r); err != nil { + return nil, fmt.Errorf("shiip.Client.auth: parse_data %v", err) + } + res := r.Response + if res == nil { + return nil, fmt.Errorf("shiip.Client.auth: empty_response") + } + if res.StatusCode >= http.StatusBadRequest { + if err = r.ParseResponseData(&errRes); err != nil { + return nil, fmt.Errorf("shiip.Client.auth: parse_response_err: %v", err) + } + return nil, errRes + } + if err = r.ParseResponseData(&data); err != nil { + return nil, fmt.Errorf("shiip.Client.auth: parse_response_data: %v", err) + } + return &data, nil +} + +func (c *Client) getRequestHeader() map[string]string { + m := map[string]string{ + httputil.HeaderKeyContentType: httputil.HeaderValueApplicationJSON, + } + token, err := c.getToken() + if err != nil { + log.Printf("shiip.Client.getToken: %v\n", err) + } else { + m["Authorization"] = fmt.Sprintf("Bearer %s", token) + } + return m +} + +func (c *Client) requestHttpViaNats(data model.CommunicationRequestHttp) (*nats.Msg, error) { + b := pjson.ToBytes(data) + return c.natsClient.Request(subject.Communication.RequestHTTP, b) +} + +func (c *Client) getBaseURL() string { + return baseURLENVMapping[c.env] +} + +func (c *Client) getToken() (string, error) { + if c.token != "" || c.tokenExpireAt.After(time.Now()) { + return c.token, nil + } + data, err := c.auth() + if err != nil { + return "", err + } + c.token = data.AccessToken + d := time.Duration(data.ExpiresIn) * time.Second + if d.Minutes() > 30 { + d -= 30 * time.Minute + } + c.tokenExpireAt = time.Now().Add(d) + return c.token, nil +} diff --git a/util/pjson/bytes.go b/util/pjson/bytes.go index f8d12da..f1c2874 100644 --- a/util/pjson/bytes.go +++ b/util/pjson/bytes.go @@ -2,15 +2,14 @@ package pjson import ( "encoding/json" - - "git.selly.red/Selly-Modules/logger" + "log" ) // ToBytes ... func ToBytes(data interface{}) []byte { b, err := json.Marshal(data) if err != nil { - logger.Error("pjson.ToBytes", logger.LogData{"payload": data}) + log.Printf("3pl/util/pjson.ToBytes.Marshal: %v\n", err) } return b }