diff --git a/model/natsio/communication_response.go b/model/natsio/communication_response.go index eb52be1..ab8a512 100644 --- a/model/natsio/communication_response.go +++ b/model/natsio/communication_response.go @@ -4,14 +4,23 @@ import "github.com/Selly-Modules/tpl/util/pjson" // NatsResponse ... type NatsResponse struct { - Data interface{} `json:"data"` - Error bool `json:"error"` - Message string `json:"message"` - RequestID string `json:"requestId"` + Response *HttpResponse `json:"response"` + Error bool `json:"error"` + Message string `json:"message"` + RequestID string `json:"requestId"` } -// ParseData ... -func (r *NatsResponse) ParseData(result interface{}) error { - b := pjson.ToBytes(r.Data) +// ParseResponseData ... +func (r *NatsResponse) ParseResponseData(result interface{}) error { + if r.Response == nil { + return nil + } + b := pjson.ToBytes(r.Response.Body) return pjson.Unmarshal(b, result) } + +// HttpResponse ... +type HttpResponse struct { + Body string `json:"body"` + StatusCode int `json:"statusCode"` +} diff --git a/partnerapi/globalcare/globale_care.go b/partnerapi/globalcare/globale_care.go index 76de1cc..847bc84 100644 --- a/partnerapi/globalcare/globale_care.go +++ b/partnerapi/globalcare/globale_care.go @@ -85,7 +85,7 @@ func (c *Client) CreateOrder(p CreateOrderPayload) (*CommonResponse, error) { if err = pjson.Unmarshal(msg.Data, &r); err != nil { return nil, err } - err = r.ParseData(&res) + err = r.ParseResponseData(&res) return &res, err } @@ -114,7 +114,7 @@ func (c *Client) GetOrder(orderCode string) (*CommonResponse, error) { if err = pjson.Unmarshal(msg.Data, &r); err != nil { return nil, err } - err = r.ParseData(&res) + err = r.ParseResponseData(&res) return &res, err } diff --git a/partnerapi/tnc/const.go b/partnerapi/tnc/const.go new file mode 100644 index 0000000..00f417d --- /dev/null +++ b/partnerapi/tnc/const.go @@ -0,0 +1,41 @@ +package tnc + +const ( + TimeLayout = "2006-01-02 15:04:05" + + apiPathCreateOutboundRequest = "/api/v1/ors" + apiPathGetOutboundRequest = "/api/v1/ors/%d" + apiPathCancelOutboundRequest = "/api/v1/ors/%d/cancel" + apiPathAuth = "/auth/realms/%s/protocol/openid-connect/token" + + PriorityUrgent = 3 + PriorityHigh = 2 + PriorityNormal = 1 + + TPLCodeGHN = "GHN" + TPLCodeGHTK = "GHTK" + TPLCodeBest = "BEST" + TPLCodeSnappy = "SPY" + TPLCodeViettelPost = "VTP" + TPLCodeSellyExpress = "SE" + TPLCodeJTExpress = "JTE" +) + +const ( + baseURLAuthStaging = "https://auth.stg.tnclog.vn" + baseURLStaging = "https://ext-api.stg.tnclog.vn" + + baseURLAuthProd = "https://auth.tnclog.vn" + baseURLProd = "https://ext-api.tnclog.vn" +) + +var ( + baseURLENVMapping = map[ENV]string{ + EnvProd: baseURLProd, + EnvStaging: baseURLStaging, + } + baseURLAuthENVMapping = map[ENV]string{ + EnvProd: baseURLAuthProd, + EnvStaging: baseURLAuthStaging, + } +) diff --git a/partnerapi/tnc/env.go b/partnerapi/tnc/env.go new file mode 100644 index 0000000..e192212 --- /dev/null +++ b/partnerapi/tnc/env.go @@ -0,0 +1,9 @@ +package tnc + +// ENV ... +type ENV string + +const ( + EnvStaging ENV = "STAGING" + EnvProd ENV = "PROD" +) diff --git a/partnerapi/tnc/model_request.go b/partnerapi/tnc/model_request.go new file mode 100644 index 0000000..52ee55d --- /dev/null +++ b/partnerapi/tnc/model_request.go @@ -0,0 +1,44 @@ +package tnc + +// 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"` + DistrictCode string `json:"districtCode"` + WardCode string `json:"wardCode"` +} + +// 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"` +} diff --git a/partnerapi/tnc/model_response.go b/partnerapi/tnc/model_response.go new file mode 100644 index 0000000..a13dec4 --- /dev/null +++ b/partnerapi/tnc/model_response.go @@ -0,0 +1,63 @@ +package tnc + +// OutboundRequestRes ... +type OutboundRequestRes struct { + OrID int `json:"orId"` + OrCode string `json:"orCode"` + PartnerORCode string `json:"partnerORCode"` + Error *ErrRes `json:"error"` +} + +// ErrRes ... +type ErrRes struct { + Code string `json:"code"` + ErrorMessage string `json:"errorMessage"` +} + +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/tnc/tnc.go b/partnerapi/tnc/tnc.go new file mode 100644 index 0000000..1faa2db --- /dev/null +++ b/partnerapi/tnc/tnc.go @@ -0,0 +1,268 @@ +package tnc + +import ( + "fmt" + "net/http" + "net/url" + "time" + + "github.com/Selly-Modules/logger" + "github.com/Selly-Modules/natsio" + "github.com/nats-io/nats.go" + + "github.com/Selly-Modules/tpl/constant" + natsiomodel "github.com/Selly-Modules/tpl/model/natsio" + "github.com/Selly-Modules/tpl/util/pjson" +) + +// Client ... +type Client struct { + // Auth info + 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("tnc.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 := natsiomodel.NatsRequestHTTP{ + ResponseImmediately: true, + Payload: natsiomodel.HTTPPayload{ + URL: apiURL, + Method: http.MethodGet, + Data: pjson.ToJSONString(p), + Header: c.getRequestHeader(), + }, + } + msg, err := c.requestHttpViaNats(natsPayload) + if err != nil { + logger.Error("tnc.Client.CreateOutboundRequest - requestHttpViaNats", logger.LogData{ + "err": err.Error(), + "payload": natsPayload, + }) + return nil, err + } + var ( + r natsiomodel.NatsResponse + errRes ErrRes + dataRes []OutboundRequestRes + ) + if err = pjson.Unmarshal(msg.Data, &r); err != nil { + return nil, fmt.Errorf("tnc.Client.CreateOutboundRequest: parse_data %v", err) + } + res := r.Response + if res == nil { + return nil, fmt.Errorf("tnc.Client.CreateOutboundRequest: empty_response") + } + if res.StatusCode >= http.StatusBadRequest { + if err = r.ParseResponseData(&errRes); err != nil { + return nil, fmt.Errorf("tnc.Client.CreateOutboundRequest: parse_response_err: %v", err) + } + return nil, fmt.Errorf("tnc.Client.CreateOutboundRequest: failed code %s, message %s", errRes.Code, errRes.ErrorMessage) + } + if err = r.ParseResponseData(&dataRes); err != nil { + return nil, fmt.Errorf("tnc.Client.CreateOutboundRequest: parse_response_data: %v", err) + } + if len(dataRes) == 0 { + return nil, fmt.Errorf("tnc.Client.CreateOutboundRequest: empty_result") + } + + return &dataRes[0], err +} + +// GetOutboundRequestByID ... +func (c *Client) GetOutboundRequestByID(requestID int) (*OutboundRequestInfo, error) { + apiURL := c.getBaseURL() + fmt.Sprintf(apiPathGetOutboundRequest, requestID) + natsPayload := natsiomodel.NatsRequestHTTP{ + ResponseImmediately: true, + Payload: natsiomodel.HTTPPayload{ + URL: apiURL, + Method: http.MethodGet, + Header: c.getRequestHeader(), + }, + } + msg, err := c.requestHttpViaNats(natsPayload) + if err != nil { + logger.Error("tnc.Client.GetOutboundRequestByID - requestHttpViaNats", logger.LogData{ + "err": err.Error(), + "payload": natsPayload, + }) + return nil, err + } + var ( + r natsiomodel.NatsResponse + errRes ErrRes + outboundRequest OutboundRequestInfo + ) + if err = pjson.Unmarshal(msg.Data, &r); err != nil { + return nil, fmt.Errorf("tnc.Client.GetOutboundRequestByID: parse_data %v", err) + } + res := r.Response + if res == nil { + return nil, fmt.Errorf("tnc.Client.GetOutboundRequestByID: empty_response") + } + if res.StatusCode >= http.StatusBadRequest { + if err = r.ParseResponseData(&errRes); err != nil { + return nil, fmt.Errorf("tnc.Client.GetOutboundRequestByID: parse_response_err: %v", err) + } + return nil, fmt.Errorf("tnc.Client.GetOutboundRequestByID: failed code %s, message %s", errRes.Code, errRes.ErrorMessage) + } + if err = r.ParseResponseData(outboundRequest); err != nil { + return nil, fmt.Errorf("tnc.Client.GetOutboundRequestByID: parse_response_data: %v", err) + } + return &outboundRequest, nil +} + +// CancelOutboundRequest ... +func (c *Client) CancelOutboundRequest(requestID int) error { + apiURL := c.getBaseURL() + fmt.Sprintf(apiPathCancelOutboundRequest, requestID) + natsPayload := natsiomodel.NatsRequestHTTP{ + ResponseImmediately: true, + Payload: natsiomodel.HTTPPayload{ + URL: apiURL, + Method: http.MethodPost, + Header: c.getRequestHeader(), + }, + } + msg, err := c.requestHttpViaNats(natsPayload) + if err != nil { + logger.Error("tnc.Client.CancelOutboundRequest - requestHttpViaNats", logger.LogData{ + "err": err.Error(), + "payload": natsPayload, + }) + return err + } + var ( + r natsiomodel.NatsResponse + errRes ErrRes + ) + if err = pjson.Unmarshal(msg.Data, &r); err != nil { + return fmt.Errorf("tnc.Client.CancelOutboundRequest: parse_data %v", err) + } + res := r.Response + if res == nil { + return fmt.Errorf("tnc.Client.CancelOutboundRequest: empty_response") + } + if res.StatusCode >= http.StatusBadRequest { + if err = r.ParseResponseData(&errRes); err != nil { + return fmt.Errorf("tnc.Client.CancelOutboundRequest: parse_response_err: %v", err) + } + return fmt.Errorf("tnc.Client.CancelOutboundRequest: failed code %s, message %s", errRes.Code, errRes.ErrorMessage) + } + 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{ + "Content-Type": "application/x-www-form-urlencoded", + } + apiURL := baseURLAuthENVMapping[c.env] + fmt.Sprintf(apiPathAuth, c.realm) + natsPayload := natsiomodel.NatsRequestHTTP{ + ResponseImmediately: true, + Payload: natsiomodel.HTTPPayload{ + URL: apiURL, + Method: http.MethodPost, + Data: body, + Header: header, + }, + } + msg, err := c.requestHttpViaNats(natsPayload) + if err != nil { + logger.Error("tnc.Client.auth - requestHttpViaNats", logger.LogData{ + "err": err.Error(), + "payload": natsPayload, + }) + return nil, err + } + var ( + r natsiomodel.NatsResponse + errRes ErrRes + data authRes + ) + if err = pjson.Unmarshal(msg.Data, &r); err != nil { + return nil, fmt.Errorf("tnc.Client.auth: parse_data %v", err) + } + res := r.Response + if res == nil { + return nil, fmt.Errorf("tnc.Client.auth: empty_response") + } + if res.StatusCode >= http.StatusBadRequest { + if err = r.ParseResponseData(&errRes); err != nil { + return nil, fmt.Errorf("tnc.Client.auth: parse_response_err: %v", err) + } + return nil, fmt.Errorf("tnc.Client.auth: failed code %s, message %s", errRes.Code, errRes.ErrorMessage) + } + if err = r.ParseResponseData(data); err != nil { + return nil, fmt.Errorf("tnc.Client.auth: parse_response_data: %v", err) + } + return &data, nil +} + +func (c *Client) getRequestHeader() map[string]string { + m := map[string]string{ + "Content-Type": "application/json", + } + token, err := c.getToken() + if err != nil { + logger.Error("tnc.Client.getToken", logger.LogData{"err": err.Error()}) + } else { + m["Authorization"] = fmt.Sprintf("Bearer %s", token) + } + return m +} + +func (c *Client) requestHttpViaNats(data natsiomodel.NatsRequestHTTP) (*nats.Msg, error) { + s := constant.NatsCommunicationSubjectRequestHTTP + b := pjson.ToBytes(data) + return c.natsClient.Request(s, 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 +}