master #12

Merged
sinhluu merged 4 commits from master into develop 2023-10-23 07:41:48 +00:00
9 changed files with 528 additions and 1 deletions
Showing only changes of commit 007796e668 - Show all commits

39
examples/kiotviet/main.go Normal file
View File

@ -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))
}

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.17
require ( require (
git.selly.red/Selly-Modules/logger v0.0.2-0.20221010053254-567df039afdb 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.20231020090841-5edec97ee393
github.com/nats-io/nats.go v1.17.0 github.com/nats-io/nats.go v1.17.0
github.com/thoas/go-funk v0.9.2 github.com/thoas/go-funk v0.9.2
) )

4
go.sum
View File

@ -2,6 +2,10 @@ 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/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 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.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=
git.selly.red/Selly-Modules/natsio v1.0.3-0.20231020090841-5edec97ee393 h1:43kE03FW3NONfE6hXlghafS1d233dfc7grlFqd+15SA=
git.selly.red/Selly-Modules/natsio v1.0.3-0.20231020090841-5edec97ee393/go.mod h1:KNODhfeBqxRmHHQHHU+p3JfH42t8s5aNxfgr6X8fr6g=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= 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/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=

View File

@ -0,0 +1,26 @@
package kiotviet
const (
apiPathListBranches = "/branches"
apiPathListProductOnHands = "/productOnHands"
apiPathListWebhook = "/webhooks"
apiPathRegisterWebhook = "/webhooks"
apiPathUnregisterWebhook = "/webhooks/%d" // %s -> webhook id
apiPathAuth = "/connect/token"
)
const (
baseURLProd = "https://public.kiotapi.com"
baseURLTokenProd = "https://id.kiotviet.vn"
)
const (
WebhookTypeCustomerUpdate = "customer.update"
WebhookTypeCustomerDelete = "customer.delete"
WebhookTypeProductUpdate = "product.update"
WebhookTypeProductDelete = "product.delete"
WebhookTypeStockUpdate = "stock.update"
WebhookTypeOrderUpdate = "order.update"
WebhookTypeInvoiceUpdate = "invoice.update"
)

View File

@ -0,0 +1,9 @@
package kiotviet
// ENV ...
type ENV string
const (
// EnvStaging ENV = "STAGING"
EnvProd ENV = "PROD"
)

View File

@ -0,0 +1 @@
package kiotviet

View File

@ -0,0 +1,300 @@
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) ListWebhooks(req ListWebhookReq) (*ListWebhookRes, error) {
apiURL := c.getURL(apiPathListWebhook)
natsPayload := model.CommunicationRequestHttp{
ResponseImmediately: true,
Payload: model.HttpRequest{
URL: apiURL,
Method: http.MethodGet,
Header: c.getRequestHeader(),
},
LogTarget: logTarget,
}
r, err := c.requestHttpViaNats(natsPayload)
if err != nil {
log.Printf("kiotviet.Client.ListWebhooks - requestHttpViaNats: %v, %s\n", err, apiURL)
return nil, err
}
res := r.Response
if res.StatusCode >= http.StatusBadRequest {
return nil, fmt.Errorf("kiotviet.Client.ListWebhooks - requestHttpViaNats bad request %s", res.Body)
}
var data ListWebhookRes
if err = r.ParseResponseData(&data); err != nil {
return nil, fmt.Errorf("kiotviet.Client.ListWebhooks - 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.Success); 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
}

View File

@ -0,0 +1,63 @@
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 ListWebhookReq struct {
}
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"`
}

View File

@ -0,0 +1,85 @@
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"`
}
type Webhook struct {
Id int `json:"id"`
Type string `json:"type"`
Url string `json:"url"`
IsActive bool `json:"isActive"`
RetailerId int `json:"retailerId"`
ModifiedDate time.Time `json:"modifiedDate"`
}
type ListWebhookRes struct {
Total int `json:"total"`
PageSize int `json:"pageSize"`
Data []Webhook `json:"data"`
Timestamp time.Time `json:"timestamp"`
}