Compare commits

...

52 Commits

Author SHA1 Message Date
sinhluu 5c20536492 Merge pull request 'feat: update URLs and authentication base across files' (#20) from viettel-ffm into master
Reviewed-on: #20
2024-07-18 09:16:25 +00:00
Sinh 4b621b9b41 feat: update URLs and authentication base across files
- Update production URLs in `viettel_ffm.go`
- Change the authentication base URLs for staging and production

Signed-off-by: Sinh <luuvansinh555@gmail.com>
2024-07-18 16:15:58 +07:00
sinhluu 91e69993af Merge pull request 'viettel-ffm' (#19) from viettel-ffm into master
Reviewed-on: #19
2024-07-11 03:04:15 +00:00
Sinh b7195b364f feat: refactor webhook handling for ViettelFFM partner API
- Add structs `Webhook`, `WebhookData` to handle webhook data in ViettelFFM partner API

Signed-off-by: Sinh <luuvansinh555@gmail.com>
2024-06-26 16:53:44 +07:00
Sinh 624cb96239 update VTP FFM model 2024-06-25 14:53:10 +07:00
Sinh e5619ae68c update VTP FFM api path 2024-06-25 14:13:40 +07:00
Sinh 00ec7e1f2f update VTP FFM model 2024-06-21 15:37:59 +07:00
Sinh fa6198d7e3 update VTP FFM model 2024-06-21 15:23:51 +07:00
Sinh d70f298734 change api path 2024-06-21 14:36:53 +07:00
Sinh ee60175a4d feat: integrate VTP FFM 2024-06-21 14:32:52 +07:00
Sinh baf7be0820 update model 2024-06-20 10:09:35 +07:00
Sinh 178652fcef feat: refactor logistics functions in Viettel FFM module
- Add a new type `CancelORPayload` to the file `model.go`
- Add a new constant `pathCancelOR` in the `viettel_ffm.go` file
- Change the method from `POST` to `PUT` in the `UpdateORLogisticInfo` function in `viettel_ffm.go`
- Add a new function `CancelOR` in the `viettel_ffm.go` file

Signed-off-by: Sinh <luuvansinh555@gmail.com>
2024-06-18 16:02:52 +07:00
Sinh 1034d2e077 feat: refactor ViettelFFM partner API integration
- Add the `const.go` file defining `ENV` type and constants `EnvStaging` and `EnvProd`
- Update `go.mod` to use `go 1.20` instead of `go 1.17`
- Introduce `model.go` file for ViettelFFM partner API structs
- Implement client methods for creating outbound requests and updating logistics information
- Implement an authentication method for the client with `AuthRes` struct
- Define constants for base URLs and authentication paths in ViettelFFM client
- Implement helper methods for getting base URL and making HTTP requests via NATS
- Update `util/httputil/const.go` to include `HeaderKeyAuthorization` constant

Signed-off-by: Sinh <luuvansinh555@gmail.com>
2024-06-14 17:31:02 +07:00
sinhluu 756740a2d7 feat(onpoint): get inventories (#17)
Co-authored-by: Sinh <luuvansinh555@gmail.com>
Reviewed-on: #17
2024-05-08 07:26:17 +00:00
sinhluu 9fb4f0c6f3 Merge pull request 'feat(jnt): update create order response' (#18) from jnt-update-response into master
Reviewed-on: #18
2024-04-15 03:50:25 +00:00
Sinh ac8aa89d70 feat(jnt): update create order response 2024-04-15 10:50:04 +07:00
sinhluu d4915875e9 Merge pull request 'change code best' (#16) from change-code-best into master
Reviewed-on: #16
2024-02-05 03:42:33 +00:00
Sinh a33809e092 change code best 2024-02-05 10:39:55 +07:00
sinhluu d549cc5103 Merge pull request 'onpoint' (#15) from onpoint into master
Reviewed-on: #15
2023-12-19 03:43:57 +00:00
Sinh 1c6be73d04 fix(onpoint): update order item model 2023-12-08 09:10:20 +07:00
Sinh a9b7f39b09 update onpoint 2023-12-01 14:39:56 +07:00
Sinh 71e2f0b842 update onpoint status 2023-11-09 09:58:57 +07:00
sinhluu aa202e8fcb Merge pull request 'jtexpress' (#13) from jtexpress into master
Reviewed-on: #13
2023-10-25 02:19:50 +00:00
Sinh 51afbc9c0e Merge branch 'master' of https://git.selly.red/Selly-Modules/3pl into jtexpress 2023-10-23 15:43:02 +07:00
Sinh 83201d37af update jtexpress 2023-10-23 15:42:55 +07:00
sinhluu 007796e668 Merge pull request 'kiotviet' (#11) from kiotviet into master
Reviewed-on: #11
2023-10-20 09:15:33 +00:00
Sinh fb417551f4 change json tag 2023-10-20 16:15:14 +07:00
Sinh efd91a81dd update nats 2023-10-20 16:12:29 +07:00
Sinh 8a0e7c2cb7 update parse response 2023-10-16 10:23:45 +07:00
Sinh 89d2f1c8fb fix cancel j&t 2023-10-13 14:12:22 +07:00
Sinh 6bf4914ade change body form data 2023-10-13 11:36:13 +07:00
Sinh 844b01604a jtexpress update create order 2023-10-13 10:42:03 +07:00
Sinh 03b68fd7e4 kiotviet get list webhooks 2023-10-12 16:24:02 +07:00
Sinh cd317aa9a2 update webhook payload 2023-10-12 11:04:46 +07:00
Sinh fc532e5b0c add kiotviet const 2023-10-11 14:00:36 +07:00
Sinh 1f4c6890eb add kiotviet 2023-10-06 17:12:11 +07:00
Sinh 5257cd21bc change host jtexpress 2023-10-06 09:25:43 +07:00
Sinh a929e52324 fix jtexpress digest 2023-09-14 15:23:50 +07:00
Sinh 9e15bfc101 update jt express 2023-09-14 14:07:34 +07:00
Sinh b15d883542 add jt express 2023-09-13 17:42:06 +07:00
sinhluu 94d6e0c606 Merge pull request 'update odn logistic info' (#10) from change-api-tnc-to-odn into master
Reviewed-on: #10
2023-08-23 06:49:54 +00:00
Sinh 7d1d63039d update odn logistic info 2023-08-23 13:49:24 +07:00
Sinh 5b1b55ee78 update odn logistic info 2023-08-23 13:48:16 +07:00
sinhluu ba6851fe19 Merge pull request 'update handle err OR' (#9) from change-api-tnc-to-odn into master
Reviewed-on: #9
2023-08-23 04:28:59 +00:00
Sinh 4b8e6c4178 update handle err OR 2023-08-23 11:28:30 +07:00
sinhluu 13eec58c2b Merge pull request 'change api odn host' (#8) from change-api-tnc-to-odn into master
Reviewed-on: #8
2023-08-22 06:52:11 +00:00
Sinh 04c8804c9d change api odn host 2023-08-22 13:51:55 +07:00
sinhluu 01a693f36a Merge pull request 'change-api-tnc-to-odn' (#7) from change-api-tnc-to-odn into master
Reviewed-on: #7
2023-08-22 06:44:10 +00:00
Sinh 1e16cae1e3 change api odn host 2023-08-22 13:43:30 +07:00
Sinh 83b1969941 update tnc path 2023-08-16 11:58:56 +07:00
Sinh 2d7b0b5662 change tnc api path 2023-08-16 10:01:10 +07:00
trunglam 93651891ab Merge pull request 'add fundiin' (#6) from fundiin into master
Reviewed-on: #6
2023-05-10 07:08:29 +00:00
25 changed files with 1273 additions and 35 deletions

9
const.go Normal file
View File

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

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

6
go.mod
View File

@ -1,10 +1,11 @@
module git.selly.red/Selly-Modules/3pl module git.selly.red/Selly-Modules/3pl
go 1.17 go 1.20
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/go-resty/resty/v2 v2.7.0
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
) )
@ -34,6 +35,7 @@ require (
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
golang.org/x/mod v0.5.1 // indirect golang.org/x/mod v0.5.1 // indirect
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/sys v0.0.0-20211102192858-4dd72447c267 // indirect golang.org/x/sys v0.0.0-20211102192858-4dd72447c267 // indirect
golang.org/x/tools v0.1.7 // indirect golang.org/x/tools v0.1.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect

8
go.sum
View File

@ -1,7 +1,7 @@
git.selly.red/Selly-Modules/logger v0.0.2-0.20221010053254-567df039afdb h1:AmcYd88IcdSkH+NEvKyJLT7psidSkjJQT/nAg/KuzFk= git.selly.red/Selly-Modules/logger v0.0.2-0.20221010053254-567df039afdb h1:AmcYd88IcdSkH+NEvKyJLT7psidSkjJQT/nAg/KuzFk=
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.3-0.20231020090841-5edec97ee393 h1:43kE03FW3NONfE6hXlghafS1d233dfc7grlFqd+15SA=
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.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=
@ -17,6 +17,8 @@ github.com/elastic/go-sysinfo v1.1.1/go.mod h1:i1ZYdU10oLNfRzq4vq62BEwD2fH8KaWh6
github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU= github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0=
github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
@ -142,6 +144,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@ -0,0 +1,21 @@
package jtexpress
const (
apiHostProd = "https://ylstandard.jtexpress.vn"
apiHostDev = "https://demo-ylstandard.jtexpress.vn"
apiPathEstimateFee = "/yuenan-interface-web/jtpos/inquiry!freight.action"
apiPathCreateOrder = "/yuenan-interface-web/order/orderAction!createOrder.action"
apiPathCancelOrder = "/yuenan-interface-web/order/orderAction!createOrder.action"
apiPathTrackingOrder = ""
)
const (
statusSuccess = "true"
)
const (
msgTypeEstimateFee = "FREIGHTQUERY"
msgTypeCreateOrder = "ORDERCREATE"
msgTypeCancelOrder = "UPDATE"
)

View File

@ -0,0 +1,120 @@
package jtexpress
import (
"crypto/md5"
"fmt"
"io"
"github.com/go-resty/resty/v2"
"git.selly.red/Selly-Modules/3pl/util/base64"
"git.selly.red/Selly-Modules/3pl/util/pjson"
)
func New(digestKey, companyID string, isProd, debug bool) *Client {
host := apiHostDev
if isProd {
host = apiHostProd
}
c := &Client{
DigestKey: digestKey,
EccompanyID: companyID,
IsProduction: isProd,
Debug: debug,
host: host,
httpClient: resty.New().SetDebug(debug),
}
return c
}
type Client struct {
DigestKey string
EccompanyID string
IsProduction bool
Debug bool
host string
httpClient *resty.Client
}
func (c *Client) EstimateFee(req *EstimateFeeReq) (r Response) {
path := c.host + apiPathEstimateFee
data := pjson.ToJSONString(req)
body := map[string]string{
"logistics_interface": data,
"data_digest": c.getDigest(data),
"msg_type": msgTypeEstimateFee,
"eccompanyid": c.EccompanyID,
}
r.Request.Body = pjson.ToBytes(body)
r.Request.URL = path
resp, err := c.httpClient.R().
SetMultipartFormData(body).
Post(path)
if err != nil {
r.Error = fmt.Errorf("jtepxress: request %s, err %v", path, err)
return r
}
r.Response.StatusCode = resp.StatusCode()
r.Response.Body = resp.Body()
return r
}
func (c *Client) CancelOrder(req *CancelOrderReq) (r Response) {
path := c.host + apiPathCancelOrder
data := pjson.ToJSONString(req)
body := map[string]string{
"logistics_interface": data,
"data_digest": c.getDigest(data),
"msg_type": msgTypeCancelOrder,
"eccompanyid": c.EccompanyID,
}
r.Request.Body = pjson.ToBytes(body)
r.Request.URL = path
resp, err := c.httpClient.R().
SetMultipartFormData(body).
Post(path)
if err != nil {
r.Error = fmt.Errorf("jtepxress: request %s, err %v", path, err)
return r
}
r.Response.StatusCode = resp.StatusCode()
r.Response.Body = resp.Body()
return r
}
func (c *Client) CreateOrder(req *CreateOrderReq) (r Response) {
path := c.host + apiPathCreateOrder
data := pjson.ToJSONString(req)
body := map[string]string{
"logistics_interface": data,
"data_digest": c.getDigest(data),
"msg_type": msgTypeCreateOrder,
"eccompanyid": c.EccompanyID,
}
r.Request.Body = pjson.ToBytes(body)
r.Request.URL = path
resp, err := c.httpClient.R().
SetMultipartFormData(body).
Post(path)
if err != nil {
r.Error = fmt.Errorf("jtepxress: request %s, err %v", path, err)
return r
}
r.Response.StatusCode = resp.StatusCode()
r.Response.Body = resp.Body()
return r
}
func (c *Client) getDigest(data string) string {
s := data + c.DigestKey
h := md5.New()
io.WriteString(h, s)
return base64.Encode([]byte(fmt.Sprintf("%x", h.Sum(nil))))
}

View File

@ -0,0 +1,76 @@
package jtexpress
type EstimateFeeLocation struct {
Prov string `json:"prov"`
City string `json:"city"`
Area string `json:"area"`
}
type EstimateFeeReq struct {
Cusname string `json:"cusname"` // = customerid
SelfAddress int `json:"selfAddress"`
ProductType string `json:"producttype"`
GoodsValue string `json:"goodsvalue"`
ItemsValue string `json:"itemsvalue"`
Weight string `json:"weight"`
Sender EstimateFeeLocation `json:"sender"`
Receiver EstimateFeeLocation `json:"receiver"`
Decs string `json:"decs"`
FeeType string `json:"feetype"`
}
type CreateOrderReq struct {
Eccompanyid string `json:"eccompanyid"`
Customerid string `json:"customerid"`
Txlogisticid string `json:"txlogisticid"`
Ordertype int `json:"ordertype"`
Servicetype int `json:"servicetype"`
SelfAddress int `json:"selfAddress"`
Special string `json:"special"`
Partsign string `json:"partsign"`
Sender CreateOrderLocation `json:"sender"`
Receiver CreateOrderLocation `json:"receiver"`
Createordertime string `json:"createordertime"`
Sendstarttime string `json:"sendstarttime"`
Sendendtime string `json:"sendendtime"`
Paytype string `json:"paytype"`
Itemsvalue string `json:"itemsvalue"`
Goodsvalue string `json:"goodsvalue"`
IsInsured string `json:"isInsured"`
Items []OrderItem `json:"items"`
Weight string `json:"weight"`
Volume string `json:"volume"`
Remark string `json:"remark"`
}
type CreateOrderLocation struct {
Name string `json:"name"`
Phone string `json:"phone"`
Mobile string `json:"mobile"`
Prov string `json:"prov"`
City string `json:"city"`
Area string `json:"area"`
Address string `json:"address"`
}
type OrderItem struct {
Itemname string `json:"itemname"`
EnglishName string `json:"englishName"`
Number string `json:"number"`
Itemvalue string `json:"itemvalue"`
Desc string `json:"desc"`
}
type CancelOrderReq struct {
Eccompanyid string `json:"eccompanyid"`
Customerid string `json:"customerid"`
Logisticproviderid string `json:"logisticproviderid"`
Fieldlist []CancelOrderFieldList `json:"fieldlist"`
}
type CancelOrderFieldList struct {
Txlogisticid string `json:"txlogisticid"`
Fieldname string `json:"fieldname"`
Fieldvalue string `json:"fieldvalue"`
Remark string `json:"remark"`
}

View File

@ -0,0 +1,62 @@
package jtexpress
type EstimateFeeItemRes struct {
ProductType string `json:"producttype"`
Price string `json:"price"`
CodFee string `json:"codfee"`
InsuranceFee string `json:"insurancefee"`
DiscountFee string `json:"discountFee"`
Success string `json:"success"`
Reason string `json:"reason"`
}
type EstimateFeeRes struct {
LogisticProviderID string `json:"logisticproviderid"`
ResponseItems []*EstimateFeeItemRes `json:"responseitems"`
}
type CreateOrderRes struct {
LogisticProviderID string `json:"logisticproviderid"`
ResponseItems []*CreateOrderItemRes `json:"responseitems"`
}
type CreateOrderItemRes struct {
Billcode string `json:"billcode"`
CodFee string `json:"codFee"`
Code string `json:"code"`
DiscountFee string `json:"discountFee"`
DispatchSite string `json:"dispatchSite"`
InquiryFee string `json:"inquiryFee"`
Insurancefee string `json:"insurancefee"`
Reason string `json:"reason"`
Reportnewurl string `json:"reportnewurl"`
Reporturl string `json:"reporturl"`
ReporturlJT string `json:"reporturlJT"`
Success string `json:"success"`
Transport string `json:"transport"`
Txlogisticid string `json:"txlogisticid"`
SortLine string `json:"sortLine"`
}
type CancelOrderRes struct {
LogisticProviderID string `json:"logisticproviderid"`
ResponseItems []*CreateOrderItemRes `json:"responseitems"`
}
type Response struct {
Request RequestInfo
Response ResponseInfo
Error error
}
type ResponseInfo struct {
StatusCode int
Body []byte
}
type RequestInfo struct {
Method string
URL string
Headers map[string]string
Body []byte
}

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"`
}

View File

@ -21,13 +21,14 @@ const (
apiPathUpdateDelivery = "/v1/orders/update_delivery" apiPathUpdateDelivery = "/v1/orders/update_delivery"
apiPathCancelOrder = "/v1/orders/cancel" apiPathCancelOrder = "/v1/orders/cancel"
apiPathGetChannels = "/v1/channels" apiPathGetChannels = "/v1/channels"
apiPathGetInventories = "/v1/inventories"
headerXAPIKey = "x-api-key" headerXAPIKey = "x-api-key"
headerXTimestamp = "x-timestamp" headerXTimestamp = "x-timestamp"
headerXSignature = "x-signature" headerXSignature = "x-signature"
webhookEventUpdateOrderStatus = "UPDATE_ORDER_STATUS" webhookEventUpdateOrderStatus = "UPDATE_ORDER_STATUS"
webhookEventUpdateInventory = "update_inventory" webhookEventUpdateInventory = "UPDATE_INVENTORY"
) )
var ( var (

View File

@ -12,6 +12,8 @@ import (
// CreateOrderRequest ... // CreateOrderRequest ...
type CreateOrderRequest struct { type CreateOrderRequest struct {
StoreCode string `json:"store_code"`
DeliveryPlatform string `json:"delivery_platform"`
OrderCode string `json:"order_code"` OrderCode string `json:"order_code"`
OrderDate time.Time `json:"order_date"` OrderDate time.Time `json:"order_date"`
PickupLocationCode string `json:"pickup_location_code"` PickupLocationCode string `json:"pickup_location_code"`
@ -31,7 +33,7 @@ type OrderItem struct {
Amount int `json:"amount"` Amount int `json:"amount"`
Name string `json:"name"` Name string `json:"name"`
PartnerSku string `json:"sku"` PartnerSku string `json:"sku"`
DiscountPrice int `json:"discount_price"` DiscountedPrice int `json:"discounted_price"`
} }
// UpdateOrderDeliveryRequest ... // UpdateOrderDeliveryRequest ...
@ -48,6 +50,14 @@ type CancelOrderRequest struct {
OrderNo string `json:"order_code"` OrderNo string `json:"order_code"`
} }
type ListInventoriesReq struct {
UpdatedFrom time.Time
UpdatedTo time.Time
SKUList []string
Size int
Page int
}
/** /**
* WEBHOOK ONPOINT * WEBHOOK ONPOINT
*/ */
@ -55,12 +65,9 @@ type CancelOrderRequest struct {
// WebhookDataUpdateInventory ... // WebhookDataUpdateInventory ...
type WebhookDataUpdateInventory struct { type WebhookDataUpdateInventory struct {
Sku string `json:"sku"` Sku string `json:"sku"`
PartnerSku string `json:"partner_sku"`
WarehouseCode string `json:"warehouse_code"`
AvailableQuantity int `json:"available_quantity"` AvailableQuantity int `json:"available_quantity"`
CommittedQuantity int `json:"committed_quantity"` PickupLocationCode string `json:"pickup_location_code"`
TotalQuantity int `json:"total_quantity"` UpdatedAt time.Time `json:"updated_at"`
UpdatedAt string `json:"updated_at"`
} }
// WebhookDataUpdateOrderStatus ... // WebhookDataUpdateOrderStatus ...

View File

@ -1,5 +1,7 @@
package onpoint package onpoint
import "time"
// CreateOrderResponse ... // CreateOrderResponse ...
type CreateOrderResponse struct { type CreateOrderResponse struct {
OrderCode string `json:"order_code"` OrderCode string `json:"order_code"`
@ -34,3 +36,34 @@ type ChannelResponse struct {
Code string `json:"code"` Code string `json:"code"`
Name string `json:"name"` Name string `json:"name"`
} }
type ListInventoriesRes struct {
Code string `json:"code"`
Data struct {
Entries []InventoryEntry `json:"entries"`
Page int `json:"page"`
Size int `json:"size"`
TotalEntries int `json:"total_entries"`
TotalPages int `json:"total_pages"`
} `json:"data"`
}
type InventoryEntry struct {
AvailableQuantity int `json:"available_quantity"`
PickupLocation Pickup `json:"pickup_location"`
Product Product `json:"product"`
UpdatedAt time.Time `json:"updated_at"`
}
type Product struct {
Name string `json:"name"`
OriginalPrice int `json:"original_price"`
SellingPrice int `json:"selling_price"`
Sku string `json:"sku"`
Uom interface{} `json:"uom"`
}
type Pickup struct {
Code string `json:"code"`
Name string `json:"name"`
}

View File

@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -152,19 +153,77 @@ func (c *Client) CancelOrder(p CancelOrderRequest) (*CancelOrderResponse, error)
return &dataRes.Data, nil return &dataRes.Data, nil
} }
// GetInventories ...
func (c *Client) GetInventories(req ListInventoriesReq) (*ListInventoriesRes, error) {
url := c.getBaseURL() + apiPathGetInventories
q := map[string]string{}
if !req.UpdatedFrom.IsZero() {
q["updated_from"] = req.UpdatedFrom.Format(TimeLayout)
}
if !req.UpdatedTo.IsZero() {
q["updated_to"] = req.UpdatedTo.Format(TimeLayout)
}
if len(req.SKUList) > 0 {
q["sku_list"] = strings.Join(req.SKUList, ",")
}
if req.Page > 0 {
q["page"] = strconv.Itoa(req.Page)
}
if req.Size > 0 {
q["size"] = strconv.Itoa(req.Size)
}
natsPayload := model.CommunicationRequestHttp{
ResponseImmediately: true,
Payload: model.HttpRequest{
URL: url,
Method: http.MethodGet,
Query: q,
},
}
var (
r model.CommunicationHttpResponse
errRes Error
dataRes ListInventoriesRes
)
if err := c.requestHttpViaNats(natsPayload, &r); err != nil {
return nil, err
}
res := r.Response
if res == nil {
return nil, fmt.Errorf("onpoint.Client.GetInventories: empty_response")
}
if res.StatusCode >= http.StatusBadRequest {
if err := r.ParseResponseData(&errRes); err != nil {
return nil, fmt.Errorf("onpoint.Client.GetInventories: parse_response_err: %v", err)
}
return nil, errRes
}
if err := r.ParseResponseData(&dataRes); err != nil {
return nil, fmt.Errorf("onpoint.Client.GetInventories: parse_response_data: %v", err)
}
return &dataRes, nil
}
func (c *Client) requestHttpViaNats(data model.CommunicationRequestHttp, res interface{}) error { func (c *Client) requestHttpViaNats(data model.CommunicationRequestHttp, res interface{}) error {
ec, err := c.natsClient.NewJSONEncodedConn() ec, err := c.natsClient.NewJSONEncodedConn()
if err != nil { if err != nil {
return fmt.Errorf("onpoint: request via nats %v", err) return fmt.Errorf("onpoint: request via nats %v", err)
} }
qs := "" u, err := url.ParseRequestURI(data.Payload.URL)
for k, v := range data.Payload.Query { if err != nil {
qs += k + "=" + v return fmt.Errorf("onpoint: request via nats %v", err)
} }
q := u.Query()
for k, v := range data.Payload.Query {
q.Set(k, v)
}
u.RawQuery = q.Encode()
now := time.Now().Unix() now := time.Now().Unix()
ts := strconv.FormatInt(now, 10) ts := strconv.FormatInt(now, 10)
arr := []string{ arr := []string{
qs, u.RawQuery,
data.Payload.Data, data.Payload.Data,
ts, ts,
} }
@ -177,6 +236,8 @@ func (c *Client) requestHttpViaNats(data model.CommunicationRequestHttp, res int
headerXTimestamp: ts, headerXTimestamp: ts,
httputil.HeaderKeyContentType: httputil.HeaderValueApplicationJSON, httputil.HeaderKeyContentType: httputil.HeaderValueApplicationJSON,
} }
data.Payload.Query = map[string]string{}
data.Payload.URL = u.String()
return ec.Request(subject.Communication.RequestHTTP, data, res) return ec.Request(subject.Communication.RequestHTTP, data, res)
} }

View File

@ -3,6 +3,7 @@ package onpoint
const ( const (
OrderStatusNew = "new" OrderStatusNew = "new"
OrderStatusPendingWarehouse = "pending_warehouse" OrderStatusPendingWarehouse = "pending_warehouse"
OrderStatusWhPending = "wh_pending"
OrderStatusWhProcessing = "wh_processing" OrderStatusWhProcessing = "wh_processing"
OrderStatusWhCompleted = "wh_completed" OrderStatusWhCompleted = "wh_completed"
OrderStatusDlPending = "dl_pending" OrderStatusDlPending = "dl_pending"

View File

@ -15,7 +15,7 @@ const (
TPLCodeGHN = "GHN" TPLCodeGHN = "GHN"
TPLCodeGHTK = "GHTK" TPLCodeGHTK = "GHTK"
TPLCodeBest = "BEST" TPLCodeBest = "BEX"
TPLCodeSnappy = "SPY" TPLCodeSnappy = "SPY"
TPLCodeViettelPost = "VTP" TPLCodeViettelPost = "VTP"
TPLCodeSellyExpress = "SE" TPLCodeSellyExpress = "SE"
@ -30,11 +30,11 @@ const (
) )
const ( const (
baseURLAuthStaging = "https://auth.stg.tnclog.vn" baseURLAuthStaging = "https://auth.stg.vnfai.com"
baseURLStaging = "https://ext-api.stg.tnclog.vn" baseURLStaging = "https://ext.stg.vnfai.com"
baseURLAuthProd = "https://auth.tnclog.vn" baseURLAuthProd = "https://auth.vnfai.com"
baseURLProd = "https://ext-api.tnclog.vn" baseURLProd = "https://ext-api.vnfai.com"
) )
const ( const (

View File

@ -46,7 +46,13 @@ type OutboundRequestPayload struct {
// UpdateORLogisticInfoPayload ... // UpdateORLogisticInfoPayload ...
type UpdateORLogisticInfoPayload struct { type UpdateORLogisticInfoPayload struct {
OrID int `json:"orId"` OrID int `json:"orId"`
TPLCode string `json:"tplCode"`
TrackingCode string `json:"trackingCode"` TrackingCode string `json:"trackingCode"`
ShippingLabel string `json:"shippingLabel"` ShippingLabels []LogisticInfoLabel `json:"shippingLabels"`
SlaShipDate string `json:"slaShipDate"` }
type LogisticInfoLabel struct {
Caption string `json:"caption"`
URI string `json:"uri"`
} }

View File

@ -67,7 +67,6 @@ func (c *Client) CreateOutboundRequest(p OutboundRequestPayload) (*OutboundReque
} }
var ( var (
r model.CommunicationHttpResponse r model.CommunicationHttpResponse
errRes Error
dataRes []OutboundRequestRes dataRes []OutboundRequestRes
) )
if err = pjson.Unmarshal(msg.Data, &r); err != nil { if err = pjson.Unmarshal(msg.Data, &r); err != nil {
@ -78,6 +77,7 @@ func (c *Client) CreateOutboundRequest(p OutboundRequestPayload) (*OutboundReque
return nil, fmt.Errorf("tnc.Client.CreateOutboundRequest: empty_response") return nil, fmt.Errorf("tnc.Client.CreateOutboundRequest: empty_response")
} }
if res.StatusCode >= http.StatusBadRequest { if res.StatusCode >= http.StatusBadRequest {
var errRes Error
if err = r.ParseResponseData(&errRes); err != nil { 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: parse_response_err: %v", err)
} }
@ -92,7 +92,7 @@ func (c *Client) CreateOutboundRequest(p OutboundRequestPayload) (*OutboundReque
item := &dataRes[0] item := &dataRes[0]
e := item.Error e := item.Error
if e != nil { if e != nil {
return nil, errRes return nil, e
} }
return item, err return item, err

View File

@ -0,0 +1,79 @@
package viettelffm
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"`
}
type UpdateLogisticInfoPayload struct {
PrintLabelLink string `json:"print_label_link"`
TrackingCode string `json:"tracking_code"`
OrCode string `json:"or_code,omitempty"`
}
type CancelORPayload struct {
OrID int `json:"or_id"`
}
type ORPayload struct {
OrProductLines []ORProductLine `json:"or_product_lines"`
AmountPaid float64 `json:"amount_paid"`
CodAmount float64 `json:"cod_amount"`
CodType string `json:"cod_type"`
Note string `json:"note"`
OrCode string `json:"or_code"`
OrType string `json:"or_type"`
ShippingType string `json:"shipping_type"`
CustomerName string `json:"customer_name"`
CustomerEmail string `json:"customer_email"`
PackType string `json:"pack_type"`
PriorityType string `json:"priority_type"`
WarehouseCode string `json:"warehouse_code"`
}
type ORProductLine struct {
Discount float64 `json:"discount"`
Price float64 `json:"price"`
Product Product `json:"product"`
Quantity int `json:"quantity"`
SpecifiedProductLine SpecifiedProductLine `json:"specified_product_line"`
Unit string `json:"unit"`
}
type SpecifiedProductLine struct {
ProductConditionType ProdCondType `json:"product_condition_type"`
}
type Product struct {
PartnerSku string `json:"partner_sku"`
SKU string `json:"sku"`
}
type ProdCondType struct {
Name string `json:"name"`
}
type ORResult struct {
OrCode string `json:"or_code"`
OrId int `json:"or_id"`
Status string `json:"status"`
}
type Webhook struct {
Data WebhookData `json:"data"`
}
type WebhookData struct {
OrId int `json:"or_id"`
OrCode string `json:"or_code"`
PartnerOrCode string `json:"partner_or_code"`
Status string `json:"status"`
TotalPrice float64 `json:"total_price"`
ShippingFee int `json:"shipping_fee"`
}

View File

@ -0,0 +1,232 @@
package viettelffm
import (
"errors"
"fmt"
"net/http"
"net/url"
"time"
"git.selly.red/Selly-Modules/natsio"
"git.selly.red/Selly-Modules/natsio/model"
"git.selly.red/Selly-Modules/natsio/subject"
tpl "git.selly.red/Selly-Modules/3pl"
"git.selly.red/Selly-Modules/3pl/util/httputil"
"git.selly.red/Selly-Modules/3pl/util/pjson"
)
const (
baseURLStag = "https://stg-gw.viettelpost.vn"
baseURLAuthStag = "https://stg-keycloak.viettelpost.vn"
baseURLProd = "https://gw.viettelpost.vn"
baseURLAuthProd = "https://dws-sso.viettelpost.vn"
pathAuth = "/realms/wms/protocol/openid-connect/token"
pathUpdateORLogisticInfo = "/wms-core/api/v1/obms/outbound-request/outbound-request-partner/%s"
pathCreateOR = "/wms-core/api/v1/obms/outbound-request/outbound-request-partner/hab"
pathCancelOR = "/wms-core/api/v1/obms/outbound-request/cancel"
logTarget = "viettel-ffm"
)
var (
baseURLENVMapping = map[tpl.ENV]string{
tpl.EnvProd: baseURLProd,
tpl.EnvStaging: baseURLStag,
}
)
type Client struct {
env tpl.ENV
username string
password string
natsClient natsio.Server
authInfo *AuthRes
authTokenExpireTime time.Time
}
// NewClient generate OnPoint client
func NewClient(env tpl.ENV, user, pwd string, nc natsio.Server) (*Client, error) {
if user == "" || pwd == "" {
return nil, errors.New("viettelffm: cannot init with empty api key")
}
return &Client{
env: env,
username: user,
password: pwd,
natsClient: nc,
}, nil
}
func (c *Client) CreateOR(p ORPayload) (*ORResult, error) {
apiURL := c.getBaseURL() + pathCreateOR
token, err := c.getToken()
if err != nil {
return nil, err
}
natsPayload := model.CommunicationRequestHttp{
ResponseImmediately: true,
Payload: model.HttpRequest{
URL: apiURL,
Method: http.MethodPost,
Data: pjson.ToJSONString(p),
Header: map[string]string{
httputil.HeaderKeyAuthorization: fmt.Sprintf("Bearer %s", token),
httputil.HeaderKeyContentType: httputil.HeaderValueApplicationJSON,
},
},
LogTarget: logTarget,
}
r, err := c.requestHttpViaNats(natsPayload)
if err != nil {
return nil, fmt.Errorf("viettelffm.Client.CreateOR - requestHttpViaNats %v", err)
}
res := r.Response
if res.StatusCode >= http.StatusBadRequest {
return nil, fmt.Errorf("viettelffm.Client.CreateOR - requestHttpViaNats %s", res.Body)
}
var data ORResult
if err = r.ParseResponseData(&data); err != nil {
return nil, fmt.Errorf("viettelffm.Client.CreateOR - requestHttpViaNats parse response %v, %s", err, res.Body)
}
return &data, nil
}
func (c *Client) UpdateORLogisticInfo(p UpdateLogisticInfoPayload) error {
apiURL := c.getBaseURL() + fmt.Sprintf(pathUpdateORLogisticInfo, p.OrCode)
p.OrCode = ""
token, err := c.getToken()
if err != nil {
return err
}
natsPayload := model.CommunicationRequestHttp{
ResponseImmediately: true,
Payload: model.HttpRequest{
URL: apiURL,
Method: http.MethodPut,
Data: pjson.ToJSONString(p),
Header: map[string]string{
httputil.HeaderKeyAuthorization: fmt.Sprintf("Bearer %s", token),
httputil.HeaderKeyContentType: httputil.HeaderValueApplicationJSON,
},
},
LogTarget: logTarget,
}
r, err := c.requestHttpViaNats(natsPayload)
if err != nil {
return err
}
res := r.Response
if res.StatusCode >= http.StatusBadRequest {
return fmt.Errorf("viettelffm.Client.UpdateOutboundRequestLogisticInfo - requestHttpViaNats %s", res.Body)
}
return nil
}
func (c *Client) CancelOR(p CancelORPayload) error {
apiURL := c.getBaseURL() + pathCancelOR
token, err := c.getToken()
if err != nil {
return err
}
natsPayload := model.CommunicationRequestHttp{
ResponseImmediately: true,
Payload: model.HttpRequest{
URL: apiURL,
Method: http.MethodPost,
Data: pjson.ToJSONString(p),
Header: map[string]string{
httputil.HeaderKeyAuthorization: fmt.Sprintf("Bearer %s", token),
httputil.HeaderKeyContentType: httputil.HeaderValueApplicationJSON,
},
},
LogTarget: logTarget,
}
r, err := c.requestHttpViaNats(natsPayload)
if err != nil {
return err
}
res := r.Response
if res.StatusCode >= http.StatusBadRequest {
return fmt.Errorf("viettelffm.Client.CancelOR - requestHttpViaNats %s", res.Body)
}
return nil
}
// Auth ...
func (c *Client) Auth() (*AuthRes, error) {
v := url.Values{}
v.Set("username", c.username)
v.Set("password", c.password)
v.Set("grant_type", "password")
v.Set("client_id", "wms_account")
b := v.Encode()
header := map[string]string{
httputil.HeaderKeyContentType: httputil.HeaderValueApplicationURLEncoded,
}
bURL := baseURLAuthStag
if c.env == tpl.EnvProd {
bURL = baseURLAuthProd
}
api := bURL + pathAuth
natsPayload := model.CommunicationRequestHttp{
ResponseImmediately: true,
Payload: model.HttpRequest{
URL: api,
Method: http.MethodPost,
Data: b,
Header: header,
},
LogTarget: logTarget,
}
r, err := c.requestHttpViaNats(natsPayload)
if err != nil {
return nil, fmt.Errorf("viettelffm.Client.Auth - requestHttpViaNats: payload %+v, err %v\n", natsPayload, err)
}
res := r.Response
if res.StatusCode >= http.StatusBadRequest {
return nil, fmt.Errorf("viettelffm.Client.Auth - requestHttpViaNats %s", res.Body)
}
var data AuthRes
if err = r.ParseResponseData(&data); err != nil {
return nil, fmt.Errorf("viettelffm.Client.Auth - requestHttpViaNats parse response %v, %s", err, res.Body)
}
return &data, nil
}
func (c *Client) getToken() (string, error) {
if c.authInfo == nil || time.Now().After(c.authTokenExpireTime) {
auth, err := c.Auth()
if err != nil {
return "", err
}
c.authInfo = auth
c.authTokenExpireTime = time.Now().Add(time.Duration(auth.ExpiresIn) * time.Second).Add(-time.Minute)
}
return c.authInfo.AccessToken, nil
}
func (c *Client) getBaseURL() string {
return baseURLENVMapping[c.env]
}
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("viettelffm.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("viettelffm.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("viettelffm.Client.requestHttpViaNats empty reponse")
}
return &r, nil
}

View File

@ -2,6 +2,7 @@ package httputil
const ( const (
HeaderKeyContentType = "Content-Type" HeaderKeyContentType = "Content-Type"
HeaderKeyAuthorization = "Authorization"
HeaderValueApplicationJSON = "application/json" HeaderValueApplicationJSON = "application/json"
HeaderValueApplicationURLEncoded = "application/x-www-form-urlencoded" HeaderValueApplicationURLEncoded = "application/x-www-form-urlencoded"