commit e14d4b188bb6b236063c08d3fadf22b177354a36 Author: Pavel Sinitsin Date: Sat Sep 7 19:14:29 2024 +0300 Add new API and client modules for category and product management This commit introduces new API and client modules that provide functionality for managing categories and products. It includes the creation of `.gitignore`, necessary data models, API endpoints, and client authentication using go-resty. This forms the foundation for further enhancements to the category and product management features. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..861a20d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +cmd/ +.vscode/ +.idea/ \ No newline at end of file diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..c332b77 --- /dev/null +++ b/api/api.go @@ -0,0 +1,23 @@ +package api + +import ( + "gitea.24example.ru/spavelit/bpiek/client" + "gitea.24example.ru/spavelit/bpiek/model" +) + +type Api struct { + Client *client.Client +} + +type IApi interface { + GetParentCategories() ([]model.Category, error) + GetCategories() ([]model.Category, error) + GetProducts() ([]model.Product, error) +} + +func NewApi(credentials client.Credentials) IApi { + client := client.NewClient(credentials) + return &Api{ + Client: client, + } +} diff --git a/api/categories.go b/api/categories.go new file mode 100644 index 0000000..0d24182 --- /dev/null +++ b/api/categories.go @@ -0,0 +1,77 @@ +package api + +import ( + "errors" + + "gitea.24example.ru/spavelit/bpiek/model" +) + +var categories []model.Category +var parentCategories []model.Category + +func (a *Api) GetCategories() ([]model.Category, error) { + if len(categories) > 0 { + return categories, nil + } + + parentCategoris, err := a.GetParentCategories() + + if err != nil { + return nil, err + } + + for _, category := range parentCategoris { + categoriesAndProduct, err := a.GetCategoriesAndProductsBySlugParentCategory(category.Slug) + if err != nil { + return nil, err + } + + if len(categoriesAndProduct.Categories) != 0 { + categories = append(categories, categoriesAndProduct.Categories...) + } + } + + return categories, nil +} + +func (a *Api) GetParentCategories() ([]model.Category, error) { + + if len(parentCategories) > 0 { + return parentCategories, nil + } + + apiResponse, err := a.Client.HTTPClient.R().SetResult(&model.CategoryResponse{}).Get("client/catalog") + if err != nil { + return nil, err + } + + if apiResponse.StatusCode() != 200 { + return nil, errors.New("failed to get categories") + } + + result := apiResponse.Result().(*model.CategoryResponse).Categories + + if len(result) == 0 { + return nil, errors.New("no categories found") + } + + parentCategories = result + + return parentCategories, nil +} + +func (a *Api) GetCategoriesAndProductsBySlugParentCategory(slug string) (*model.CategoriesAndProductsBySlugParentCategoryResponse, error) { + apiResponse, err := a.Client.HTTPClient. + R(). + SetResult(&model.CategoriesAndProductsBySlugParentCategoryResponse{}). + Get("client/category/" + slug + "/json") + if err != nil { + return nil, err + } + + if apiResponse.StatusCode() != 200 { + return nil, errors.New("failed to get categories and products") + } + + return apiResponse.Result().(*model.CategoriesAndProductsBySlugParentCategoryResponse), nil +} diff --git a/api/products.go b/api/products.go new file mode 100644 index 0000000..eb56af5 --- /dev/null +++ b/api/products.go @@ -0,0 +1,31 @@ +package api + +import "gitea.24example.ru/spavelit/bpiek/model" + +var products []model.Product + +func (a *Api) GetProducts() ([]model.Product, error) { + + if len(products) > 0 { + return products, nil + } + + parentCategoris, err := a.GetParentCategories() + + if err != nil { + return nil, err + } + + for _, category := range parentCategoris { + categoriesAndProduct, err := a.GetCategoriesAndProductsBySlugParentCategory(category.Slug) + if err != nil { + return nil, err + } + + if len(categoriesAndProduct.Products) != 0 { + products = append(products, categoriesAndProduct.Products...) + } + } + + return products, nil +} diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000..11551ef --- /dev/null +++ b/client/client.go @@ -0,0 +1,57 @@ +package client + +import "github.com/go-resty/resty/v2" + +// import ( +// "net/http" +// "net/url" +// ) + +const ( + AUTH_API_URL = "https://bp.iek.ru/oauth/login" + BASE_API_URL = "https://bp.iek.ru/api/catalog/v1/" +) + +type ( + Client struct { + HTTPClient *resty.Client + } + + AuthSuccess struct { + TokenType string `json:"token_type"` + AccessToken string `json:"access_token"` + IdToken string `json:"id_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` + } +) + +func NewClient(credentials Credentials) *Client { + authSuccess := &AuthSuccess{} + client := resty.New() + client.SetBaseURL(BASE_API_URL) + + response, err := client.R(). + SetFormData(map[string]string{ + "username": credentials.GetUsername(), + "password": credentials.GetPassword(), + }). + SetResult(&authSuccess). + Post(AUTH_API_URL) + + if err != nil { + panic(err) + } + + if response.StatusCode() != 200 { + panic("Auth failed. Invalid credentials") + } + + client.SetHeader("Content-Type", "application/json") + client.SetHeader("Authorization", "Bearer "+authSuccess.AccessToken) + + return &Client{ + HTTPClient: client, + } +} diff --git a/client/credential.go b/client/credential.go new file mode 100644 index 0000000..1ba865a --- /dev/null +++ b/client/credential.go @@ -0,0 +1,17 @@ +package client + +type ( + // Credentials provides constant credential values. + Credentials struct { + Username string `json:"username"` + Password string `json:"password"` + } +) + +func (c *Credentials) GetUsername() string { + return c.Username +} + +func (c *Credentials) GetPassword() string { + return c.Password +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f6a7cf4 --- /dev/null +++ b/go.mod @@ -0,0 +1,6 @@ +module gitea.24example.ru/spavelit/bpiek + +go 1.23.1 + +require github.com/go-resty/resty/v2 v2.14.0 +require golang.org/x/net v0.27.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..63440e8 --- /dev/null +++ b/go.sum @@ -0,0 +1,74 @@ +github.com/go-resty/resty/v2 v2.11.0/go.mod h1:iiP/OpA0CkcL3IGt1O0+/SIItFUbkkyw5BGXiVdTu+A= +github.com/go-resty/resty/v2 v2.14.0 h1:/rhkzsAqGQkozwfKS5aFAbb6TyKd3zyFRWcdRXLPCAU= +github.com/go-resty/resty/v2 v2.14.0/go.mod h1:IW6mekUOsElt9C7oWr0XRt9BNSD6D5rr9mhk6NjmNHg= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/model/category.go b/model/category.go new file mode 100644 index 0000000..02e3be7 --- /dev/null +++ b/model/category.go @@ -0,0 +1,8 @@ +package model + +type Category struct { + Slug string `json:"slug"` + Name string `json:"name"` + Url string `json:"url"` + ApiUrl string `json:"apiUrl"` +} diff --git a/model/product.go b/model/product.go new file mode 100644 index 0000000..c607944 --- /dev/null +++ b/model/product.go @@ -0,0 +1,164 @@ +package model + +type VideosType string +type WarehouseDataIncomingType string + +const ( + VideoTypeFile VideosType = "file" + VideoTypeUrl VideosType = "url" +) + +const ( + WarehouseDataIncomingTypeShipping WarehouseDataIncomingType = "shipping" + WarehouseDataIncomingTypeProduction WarehouseDataIncomingType = "production" +) + +type ( + ImageVariant struct { + Url string `json:"url"` + Ext string `json:"ext"` + Width int `json:"width"` + } + + Complects struct { + Article string `json:"article"` + Name string `json:"name"` + Quantity int `json:"quantity"` + } + + LeftPeriod struct { + Name string `json:"name"` + Value string `json:"value"` + } + + LeftPeriodRaw struct { + Lifespan interface{} `json:"lifespan"` + Warranty interface{} `json:"warranty"` + } + + DesignFeatures struct { + ImageUrl string `json:"imageUrl"` + Description string `json:"description"` + } + + Video struct { + Name string `json:"name"` + Description string `json:"description"` + Url string `json:"url"` + Type VideosType `json:"type"` + } + + Software struct { + Name string `json:"name"` + Description string `json:"description"` + Url string `json:"url"` + Size int `json:"size"` + } + + Analog struct { + Article string `json:"article"` + Name string `json:"name"` + ShortName string `json:"shortName"` + Description string `json:"description"` + ImageUrl string `json:"imageUrl"` + ImageUrls []string `json:"imageUrls"` + ImageVariants []ImageVariant `json:"imageVariants"` + IsArchived bool `json:"isArchived"` + Tm string `json:"tm"` + } + + WarehouseData struct { + WarehouseId string `json:"warehouseId"` + WarehouseName string `json:"warehouseName"` + AvailableAmount int `json:"availableAmount"` + Incoming []struct { + DateBegan string `json:"dateBegan"` + DateEnd string `json:"dateEnd"` + Amount int `json:"amount"` + Type WarehouseDataIncomingType `json:"type"` + } + } + + Etim struct { + Features []struct { + Id string `json:"id"` + Name string `json:"name"` + Sort int `json:"sort"` + Unit string `json:"unit"` + Value string `json:"value"` + } `json:"features"` + + Class struct { + Id string `json:"id"` + Name string `json:"name"` + } `json:"class"` + } + + LogisticParams struct { + Name string `json:"name"` + NameOrig string `json:"nameOrig"` + Value struct { + Group string `json:"group"` + Individual string `json:"individual"` + Transport string `json:"transport"` + } `json:"value"` + } + + LogisticParamsData struct { + SinglePackage struct { + Multiplicity int `json:"multiplicity"` + Unit string `json:"unit"` + } `json:"singlePackage"` + } + + ShortProduct struct { + Article string `json:"article"` + Name string `json:"name"` + Multiplicity int `json:"multiplicity"` + PriceBase int `json:"priceBase"` + PriceRrc int `json:"priceRrc"` + Available int `json:"available"` + Units string `json:"units"` + WarehouseData []WarehouseData `json:"warehouseData"` + } + + Product struct { + ShortName string `json:"shortName"` + Description string `json:"description"` + CategoryName string `json:"categoryName"` + Category string `json:"category"` + Slug string `json:"slug"` + Tm string `json:"tm"` + Url string `json:"url"` + IsArchived bool `json:"isArchived"` + ImageUrl string `json:"imageUrl"` + ImageUrls []string `json:"imageUrls"` + ImageVariants []ImageVariant `json:"imageVariants"` + Advantages string `json:"advantages"` + Etim Etim `json:"etim"` + Complects []Complects `json:"complects"` + Complectations string `json:"complectations"` + Files []interface{} `json:"files"` + LeftPeriod []LeftPeriod `json:"leftPeriod"` + LeftPeriodRaw LeftPeriodRaw `json:"leftPeriodRaw"` + LogisticParams []LogisticParams `json:"logisticParams"` + LogisticParamsData LogisticParamsData `json:"logisticParamsData"` + Novelty bool `json:"novelty"` + DesignFeatures []DesignFeatures `json:"designFeatures"` + Videos []Video `json:"videos"` + Software []Software `json:"software"` + Banner string `json:"banner"` + LastModified string `json:"lastModified"` + CountryOfProduction string `json:"countryOfProduction"` + FirstSaleDate string `json:"firstSaleDate"` + Feacn string `json:"feacn"` + Family string `json:"family"` + Series string `json:"series"` + IndPacking []string `json:"indPacking"` + Analogs []Analog `json:"analogs"` + Related []Analog `json:"related"` + QrCode string `json:"qrCode"` + IsOutOfAssortment bool `json:"isOutOfAssortment"` + IsOutOfProduction bool `json:"isOutOfProduction"` + } +) diff --git a/model/response.go b/model/response.go new file mode 100644 index 0000000..552271b --- /dev/null +++ b/model/response.go @@ -0,0 +1,37 @@ +package model + +type CategoryResponse struct { + Categories []Category `json:"categories"` +} + +type CategoriesAndProductsBySlugParentCategoryResponse struct { + Date string `json:"date"` + Slug string `json:"slug"` + Name string `json:"name"` + Url string `json:"url"` + Categories []Category `json:"categories"` + Products []Product `json:"products"` +} + +type NewProductsResponse struct { + Data struct { + Products []Product `json:"products"` + } `json:"data"` + + Meta struct { + Page int `json:"page"` + TotalPages int `json:"totalPages"` + TotalCount int `json:"totalCount"` + PageSize int `json:"pageSize"` + } `json:"_meta"` +} + +type RemainsAndPlanresiduesResponse struct { + Date string `json:"date"` + Products []ShortProduct `json:"products"` +} + +type TreeCategoriesResponse struct { + Category + Children []Category `json:"children"` +}