From 1f4c6890ebb56b8b6f8d5ade79e81f4c424c6400 Mon Sep 17 00:00:00 2001 From: Sinh Date: Fri, 6 Oct 2023 17:12:11 +0700 Subject: [PATCH] add kiotviet --- examples/kiotviet/main.go | 39 +++++ go.mod | 2 +- go.sum | 2 + partnerapi/kiotviet/const.go | 15 ++ partnerapi/kiotviet/env.go | 9 + partnerapi/kiotviet/error.go | 1 + partnerapi/kiotviet/kiotviet.go | 272 +++++++++++++++++++++++++++++++ partnerapi/kiotviet/model_req.go | 60 +++++++ partnerapi/kiotviet/model_res.go | 69 ++++++++ 9 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 examples/kiotviet/main.go create mode 100644 partnerapi/kiotviet/const.go create mode 100644 partnerapi/kiotviet/env.go create mode 100644 partnerapi/kiotviet/error.go create mode 100644 partnerapi/kiotviet/kiotviet.go create mode 100644 partnerapi/kiotviet/model_req.go create mode 100644 partnerapi/kiotviet/model_res.go diff --git a/examples/kiotviet/main.go b/examples/kiotviet/main.go new file mode 100644 index 0000000..ac6ebde --- /dev/null +++ b/examples/kiotviet/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + + "git.selly.red/Selly-Modules/natsio" + + "git.selly.red/Selly-Modules/3pl/partnerapi/kiotviet" + "git.selly.red/Selly-Modules/3pl/util/pjson" +) + +func main() { + var ( + client = "45575d3e-5785-***" + secret = "65ACE104EC4232***" + retailer = "****" + ) + if err := natsio.Connect(natsio.Config{URL: "localhost:4222"}); err != nil { + panic(err) + } + c, err := kiotviet.New(client, secret, retailer, natsio.GetServer()) + if err != nil { + panic(err) + } + data, err := c.GetBranches(kiotviet.ListBranchesReq{}) + if err != nil { + panic(err) + } + fmt.Println(pjson.ToJSONString(data)) + + prod, err := c.GetProductOnHands(kiotviet.ListProductOnHandsReq{ + PageSize: 10, + }) + if err != nil { + panic(err) + } + + fmt.Println(pjson.ToJSONString(prod)) +} diff --git a/go.mod b/go.mod index 7c32cb8..ef6a8b1 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.17 require ( git.selly.red/Selly-Modules/logger v0.0.2-0.20221010053254-567df039afdb - git.selly.red/Selly-Modules/natsio v1.0.2-0.20221010041139-c11419a3ad33 + git.selly.red/Selly-Modules/natsio v1.0.3-0.20231006093940-b3bde5cd0960 github.com/nats-io/nats.go v1.17.0 github.com/thoas/go-funk v0.9.2 ) diff --git a/go.sum b/go.sum index dbbf8f5..076c0ce 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ git.selly.red/Selly-Modules/logger v0.0.2-0.20221010053254-567df039afdb h1:AmcYd git.selly.red/Selly-Modules/logger v0.0.2-0.20221010053254-567df039afdb/go.mod h1:Q1//Z6HRmfa7VyjH2J6YyT0YV2jT8+K6SIgwnYuS4II= git.selly.red/Selly-Modules/natsio v1.0.2-0.20221010041139-c11419a3ad33 h1:GvQjelaV4XZm++AOihYAKOD6k9510aMAr6B6MGnrXPs= git.selly.red/Selly-Modules/natsio v1.0.2-0.20221010041139-c11419a3ad33/go.mod h1:KNODhfeBqxRmHHQHHU+p3JfH42t8s5aNxfgr6X8fr6g= +git.selly.red/Selly-Modules/natsio v1.0.3-0.20231006093940-b3bde5cd0960 h1:wL/BW1xGoB/EXeA2HtxT6Nr/cXpPJYVNfToV3aFtGls= +git.selly.red/Selly-Modules/natsio v1.0.3-0.20231006093940-b3bde5cd0960/go.mod h1:KNODhfeBqxRmHHQHHU+p3JfH42t8s5aNxfgr6X8fr6g= 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/kiotviet/const.go b/partnerapi/kiotviet/const.go new file mode 100644 index 0000000..1ae36d3 --- /dev/null +++ b/partnerapi/kiotviet/const.go @@ -0,0 +1,15 @@ +package kiotviet + +const ( + apiPathListBranches = "/branches" + apiPathListProductOnHands = "/productOnHands" + apiPathRegisterWebhook = "/webhooks" + apiPathUnregisterWebhook = "/webhooks/%d" // %s -> webhook id + + apiPathAuth = "/connect/token" +) + +const ( + baseURLProd = "https://public.kiotapi.com" + baseURLTokenProd = "https://id.kiotviet.vn" +) diff --git a/partnerapi/kiotviet/env.go b/partnerapi/kiotviet/env.go new file mode 100644 index 0000000..ca8270a --- /dev/null +++ b/partnerapi/kiotviet/env.go @@ -0,0 +1,9 @@ +package kiotviet + +// ENV ... +type ENV string + +const ( + // EnvStaging ENV = "STAGING" + EnvProd ENV = "PROD" +) diff --git a/partnerapi/kiotviet/error.go b/partnerapi/kiotviet/error.go new file mode 100644 index 0000000..c3aabe0 --- /dev/null +++ b/partnerapi/kiotviet/error.go @@ -0,0 +1 @@ +package kiotviet diff --git a/partnerapi/kiotviet/kiotviet.go b/partnerapi/kiotviet/kiotviet.go new file mode 100644 index 0000000..38800dd --- /dev/null +++ b/partnerapi/kiotviet/kiotviet.go @@ -0,0 +1,272 @@ +package kiotviet + +import ( + "fmt" + "log" + "net/http" + "net/url" + "strconv" + "time" + + "git.selly.red/Selly-Modules/natsio" + "git.selly.red/Selly-Modules/natsio/model" + "git.selly.red/Selly-Modules/natsio/subject" + + "git.selly.red/Selly-Modules/3pl/util/httputil" + "git.selly.red/Selly-Modules/3pl/util/pjson" +) + +const ( + logTarget = "kiotviet" +) + +type Client struct { + clientID string + secretKey string + retailer string + + natsClient natsio.Server + + auth *AuthRes + authExpireTime time.Time +} + +func New(clientID, secretKey, retailer string, natsClient natsio.Server) (*Client, error) { + if clientID == "" || secretKey == "" || retailer == "" { + return nil, fmt.Errorf("kiotviet: cannot create client with empty info") + } + return &Client{ + clientID: clientID, + secretKey: secretKey, + retailer: retailer, + natsClient: natsClient, + }, nil +} + +func (c *Client) GetProductOnHands(req ListProductOnHandsReq) (*ListProductOnHandsRes, error) { + apiURL := c.getURL(apiPathListProductOnHands) + query := map[string]string{ + "orderBy": req.OrderBy, + "lastModifiedFrom": req.LastModifiedFrom, + } + if req.PageSize > 0 { + query["pageSize"] = strconv.Itoa(req.PageSize) + } + if req.CurrentItem > 0 { + query["currentItem"] = strconv.Itoa(req.CurrentItem) + } + natsPayload := model.CommunicationRequestHttp{ + ResponseImmediately: true, + Payload: model.HttpRequest{ + URL: apiURL, + Method: http.MethodGet, + Query: query, + Header: c.getRequestHeader(), + }, + LogTarget: logTarget, + } + r, err := c.requestHttpViaNats(natsPayload) + if err != nil { + log.Printf("kiotviet.Client.GetProductOnHands - requestHttpViaNats: %v, %s\n", err, apiURL) + return nil, err + } + res := r.Response + if res.StatusCode >= http.StatusBadRequest { + return nil, fmt.Errorf("kiotviet.Client.GetProductOnHands - requestHttpViaNats bad request %s", res.Body) + } + var data ListProductOnHandsRes + if err = r.ParseResponseData(&data); err != nil { + return nil, fmt.Errorf("kiotviet.Client.GetProductOnHands - requestHttpViaNats parse response %v, %s", err, res.Body) + } + return &data, nil +} + +func (c *Client) GetBranches(req ListBranchesReq) (*ListBranchesRes, error) { + apiURL := c.getURL(apiPathListBranches) + query := map[string]string{ + "orderBy": req.OrderBy, + "lastModifiedFrom": req.LastModifiedFrom, + "orderDirection": req.OrderDirection, + "includeRemoveIds": req.IncludeRemoveIDs, + } + if req.PageSize > 0 { + query["pageSize"] = strconv.Itoa(req.PageSize) + } + if req.CurrentItem > 0 { + query["currentItem"] = strconv.Itoa(req.CurrentItem) + } + natsPayload := model.CommunicationRequestHttp{ + ResponseImmediately: true, + Payload: model.HttpRequest{ + URL: apiURL, + Method: http.MethodGet, + Query: query, + Header: c.getRequestHeader(), + }, + LogTarget: logTarget, + } + + r, err := c.requestHttpViaNats(natsPayload) + if err != nil { + log.Printf("kiotviet.Client.GetBranches - requestHttpViaNats: %v, %s\n", err, apiURL) + return nil, err + } + res := r.Response + if res.StatusCode >= http.StatusBadRequest { + return nil, fmt.Errorf("kiotviet.Client.GetBranches - requestHttpViaNats bad request %s", res.Body) + } + var data ListBranchesRes + if err = r.ParseResponseData(&data); err != nil { + return nil, fmt.Errorf("kiotviet.Client.GetBranches - requestHttpViaNats parse response %v, %s", err, res.Body) + } + return &data, nil +} + +func (c *Client) RegisterWebhook(req RegisterWebhookReq) (*RegisterWebhookRes, error) { + apiURL := c.getURL(apiPathRegisterWebhook) + natsPayload := model.CommunicationRequestHttp{ + ResponseImmediately: true, + Payload: model.HttpRequest{ + URL: apiURL, + Method: http.MethodPost, + Data: pjson.ToJSONString(req), + Header: c.getRequestHeader(), + }, + LogTarget: logTarget, + } + + r, err := c.requestHttpViaNats(natsPayload) + if err != nil { + log.Printf("kiotviet.Client.RegisterWebhook - requestHttpViaNats: %v, %s\n", err, apiURL) + return nil, err + } + res := r.Response + if res.StatusCode >= http.StatusBadRequest { + return nil, fmt.Errorf("kiotviet.Client.RegisterWebhook - requestHttpViaNats bad request %s", res.Body) + } + var data RegisterWebhookRes + if err = r.ParseResponseData(&data); err != nil { + return nil, fmt.Errorf("kiotviet.Client.RegisterWebhook - requestHttpViaNats parse response %v, %s", err, res.Body) + } + return &data, nil +} + +func (c *Client) UnregisterWebhook(req UnregisterWebhookReq) (*UnregisterWebhookRes, error) { + apiURL := c.getURL(fmt.Sprintf(apiPathUnregisterWebhook, req.ID)) + natsPayload := model.CommunicationRequestHttp{ + ResponseImmediately: true, + Payload: model.HttpRequest{ + URL: apiURL, + Method: http.MethodDelete, + Header: c.getRequestHeader(), + }, + LogTarget: logTarget, + } + + r, err := c.requestHttpViaNats(natsPayload) + if err != nil { + log.Printf("kiotviet.Client.UnregisterWebhook - requestHttpViaNats: %v, %s\n", err, apiURL) + return nil, err + } + res := r.Response + if res.StatusCode >= http.StatusBadRequest { + return nil, fmt.Errorf("kiotviet.Client.UnregisterWebhook - requestHttpViaNats bad request %s", res.Body) + } + var data UnregisterWebhookRes + if err = r.ParseResponseData(&data); err != nil { + return nil, fmt.Errorf("kiotviet.Client.UnregisterWebhook - requestHttpViaNats parse response %v, %s", err, res.Body) + } + return &data, nil +} + +func (c *Client) Auth() (*AuthRes, error) { + v := url.Values{} + v.Add("scopes", "PublicApi.Access") + v.Add("grant_type", "client_credentials") + v.Add("client_id", c.clientID) + v.Add("client_secret", c.secretKey) + + body := v.Encode() + header := map[string]string{ + httputil.HeaderKeyContentType: httputil.HeaderValueApplicationURLEncoded, + } + apiURL := baseURLTokenProd + apiPathAuth + natsPayload := model.CommunicationRequestHttp{ + ResponseImmediately: true, + Payload: model.HttpRequest{ + URL: apiURL, + Method: http.MethodPost, + Data: body, + Header: header, + }, + LogTarget: logTarget, + } + r, err := c.requestHttpViaNats(natsPayload) + if err != nil { + log.Printf("kiotviet.Client.Auth - requestHttpViaNats: %v, %s\n", err, apiURL) + return nil, err + } + res := r.Response + if res.StatusCode >= http.StatusBadRequest { + return nil, fmt.Errorf("kiotviet.Client.Auth - requestHttpViaNats %s", res.Body) + } + var data AuthRes + if err = r.ParseResponseData(&data); err != nil { + return nil, fmt.Errorf("kiotviet.Client.Auth - requestHttpViaNats parse response %v, %s", err, res.Body) + } + return &data, nil +} + +func (c *Client) getURL(path string) string { + return baseURLProd + path +} + +func (c *Client) requestHttpViaNats(data model.CommunicationRequestHttp) (*model.CommunicationHttpResponse, error) { + b := pjson.ToBytes(data) + msg, err := c.natsClient.Request(subject.Communication.RequestHTTP, b) + if err != nil { + return nil, fmt.Errorf("kiotviet.Client.requestHttpViaNats err: %v, url %s", err, data.Payload.URL) + } + var r model.CommunicationHttpResponse + if err = pjson.Unmarshal(msg.Data, &r); err != nil { + return nil, fmt.Errorf("kiotviet.Client.requestHttpViaNats parse data err: %v, url %s, data %s", err, data.Payload.URL, string(msg.Data)) + } + if r.Response == nil { + return nil, fmt.Errorf("kiotviet.Client.requestHttpViaNats empty reponse") + } + return &r, nil +} + +func (c *Client) getRequestHeader() map[string]string { + m := map[string]string{ + httputil.HeaderKeyContentType: httputil.HeaderValueApplicationJSON, + "Retailer": c.retailer, + } + + token, err := c.getToken() + if err != nil { + log.Printf("kiotviet.Client.getToken err %v\n", err) + } else { + m["Authorization"] = fmt.Sprintf("Bearer %s", token) + } + return m +} + +func (c *Client) getToken() (string, error) { + auth := c.auth + if auth != nil && c.authExpireTime.After(time.Now()) { + return auth.AccessToken, nil + } + data, err := c.Auth() + if err != nil { + return "", err + } + c.auth = data + d := time.Duration(data.ExpiresIn) * time.Second + if d.Minutes() > 30 { + d -= 30 * time.Minute + } + c.authExpireTime = time.Now().Add(d) + + return data.AccessToken, nil +} diff --git a/partnerapi/kiotviet/model_req.go b/partnerapi/kiotviet/model_req.go new file mode 100644 index 0000000..e933183 --- /dev/null +++ b/partnerapi/kiotviet/model_req.go @@ -0,0 +1,60 @@ +package kiotviet + +type AuthReq struct { + ClientID string + ClientSecret string + GrantType string + Scopes string +} + +type ListProductOnHandsReq struct { + OrderBy string `json:"orderBy,omitempty"` + LastModifiedFrom string `json:"lastModifiedFrom,omitempty"` + PageSize int `json:"pageSize,omitempty"` + CurrentItem int `json:"currentItem,omitempty"` +} + +type ListBranchesReq struct { + OrderBy string `json:"orderBy,omitempty"` + LastModifiedFrom string `json:"lastModifiedFrom,omitempty"` + PageSize int `json:"pageSize,omitempty"` + CurrentItem int `json:"currentItem,omitempty"` + OrderDirection string `json:"orderDirection,omitempty"` // Asc/ Desc. Default Desc + IncludeRemoveIDs string `json:"includeRemoveIds,omitempty"` // true/ false +} + +type WebhookReq struct { + Type string `json:"Type"` + Url string `json:"Url"` + IsActive bool `json:"IsActive"` +} + +type RegisterWebhookReq struct { + Webhook WebhookReq `json:"webhook"` +} + +type UnregisterWebhookReq struct { + ID int +} + +type WebhookBody struct { + ID string `json:"Id"` + Attempt int `json:"Attempt"` + Notifications WebhookNotification `json:"Notifications"` +} + +type WebhookNotification struct { + Action string `json:"Action"` + Data []WebhookStockUpdateData `json:"data"` +} + +type WebhookStockUpdateData struct { + ProductID int64 `json:"ProductId"` + ProductCode string `json:"ProductCode"` + ProductName string `json:"ProductName"` + BranchID int `json:"BranchId"` + BranchName string `json:"BranchName"` + Cost int64 `json:"Cost"` + OnHand int `json:"OnHand"` + Reserved int `json:"Reserved"` +} diff --git a/partnerapi/kiotviet/model_res.go b/partnerapi/kiotviet/model_res.go new file mode 100644 index 0000000..a13b2ed --- /dev/null +++ b/partnerapi/kiotviet/model_res.go @@ -0,0 +1,69 @@ +package kiotviet + +import "time" + +type AuthRes struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` +} + +type ListProductOnHandsRes struct { + Total int `json:"total"` + PageSize int `json:"pageSize"` + Data []ProductOnHand `json:"data"` + Timestamp time.Time `json:"timestamp"` +} + +type ProductOnHand struct { + ID int `json:"id"` + Code string `json:"code"` + CreatedDate string `json:"createdDate"` + ModifiedDate string `json:"modifiedDate"` + Inventories []ProductOnHandInventory `json:"inventories"` +} + +type ProductOnHandInventory struct { + BranchID int `json:"branchId"` + OnHand int `json:"onHand"` + Reserved int `json:"reserved"` +} + +type ListBranchesRes struct { + Total int `json:"total"` + PageSize int `json:"pageSize"` + Data []Branch `json:"data"` + Timestamp time.Time `json:"timestamp"` +} + +type Branch struct { + ID int `json:"id"` + BranchName string `json:"branchName"` + Address string `json:"address"` + LocationName string `json:"locationName"` + ContactNumber string `json:"contactNumber"` + RetailerID int `json:"retailerId"` + CreatedDate string `json:"createdDate"` +} + +type RegisterWebhookRes struct { + ID int `json:"id"` + Type string `json:"type"` + Url string `json:"url"` + IsActive bool `json:"isActive"` + RetailerID int `json:"retailerId"` +} + +type UnregisterWebhookRes struct { + Success bool +} + +type ResponseError struct { + ResponseStatus RError `json:"responseStatus"` +} + +type RError struct { + ErrorCode string `json:"errorCode"` + Message string `json:"message"` +}