diff --git a/cmd/dev/sravnicli/products.go b/cmd/dev/sravnicli/products.go index d883e6e..aaf2a52 100644 --- a/cmd/dev/sravnicli/products.go +++ b/cmd/dev/sravnicli/products.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "strconv" + "strings" "git.loyso.art/frx/kurious/internal/common/client/sravni" "git.loyso.art/frx/kurious/internal/common/errors" @@ -35,9 +36,17 @@ func setupAPICommand(ctx context.Context) cli.Command { WithOption(offsetOption). WithAction(newListProductAction(ctx)) }) + apiEducationFilterCount := buildCLICommand(func() cli.Command { + return cli.NewCommand("filter_count", "Loads counts of returned entities"). + WithOption(learningTypeOpt). + WithOption(courseThematic). + WithOption(learningSelectionOpt). + WithAction(newProductsFilterCountAction(ctx)) + }) apiEducation := cli.NewCommand("education", "Education related category"). - WithCommand(apiEducationListProducts) + WithCommand(apiEducationListProducts). + WithCommand(apiEducationFilterCount) return cli.NewCommand("api", "Interaction with API"). WithCommand(apiEducation) @@ -127,7 +136,7 @@ func (a *listProductsAction) parse(args []string, options map[string]string) err func (a *listProductsAction) handle() error { params := sravni.ListEducationProductsParams{ LearningType: a.params.learningType, - CoursesThematics: a.params.courseThematic, + CoursesThematics: []string{a.params.courseThematic}, Limit: a.params.limit, Offset: a.params.offset, } @@ -140,3 +149,65 @@ func (a *listProductsAction) handle() error { return nil } + +type productsFilterCountActionParams struct { + learningType string + courseThematic []string + learningSelectionType string +} + +type productsFilterCountAction struct { + *baseAction + + client sravni.Client + params productsFilterCountActionParams +} + +func newProductsFilterCountAction(ctx context.Context) cli.Action { + action := &productsFilterCountAction{ + baseAction: newBaseAction(ctx), + } + + return asCLIAction(action) +} + +func (a *productsFilterCountAction) parse(args []string, options map[string]string) error { + err := a.baseAction.parse(args, options) + if err != nil { + return err + } + + var ok bool + + a.params.learningType, ok = options[learningTypeOptName] + if !ok { + return errors.SimpleError(learningTypeOptName + " is empty") + } + + a.params.courseThematic = strings.Split(options[courseThematicOptName], ",") + a.params.learningSelectionType = options[learningTypeSelectionOptName] + + client, err := makeSravniClient(a.ctx, a.log, options) + if err != nil { + return fmt.Errorf("making sravni client: %w", err) + } + + a.client = client + + return nil +} + +func (a *productsFilterCountAction) handle() error { + params := sravni.ListEducationProductsParams{ + LearningType: a.params.learningType, + CoursesThematics: a.params.courseThematic, + } + result, err := a.client.ListEducationalProductsFilterCount(a.ctx, params) + if err != nil { + return fmt.Errorf("listing education products: %w", err) + } + + a.log.InfoContext(a.ctx, "list education products result", slog.Any("result", result)) + + return nil +} diff --git a/internal/common/client/sravni/client.go b/internal/common/client/sravni/client.go index a00c2bd..0b9603d 100644 --- a/internal/common/client/sravni/client.go +++ b/internal/common/client/sravni/client.go @@ -29,7 +29,11 @@ type Client interface { ListEducationalProducts( ctx context.Context, params ListEducationProductsParams, - ) (result ListEducationProductsResponse, err error) + ) (result listEducationProductsResponse, err error) + ListEducationalProductsFilterCount( + ctx context.Context, + params ListEducationProductsParams, + ) (result ProductsFilterCount, err error) } func NewClient(ctx context.Context, log *slog.Logger, debug bool) (c *client, err error) { @@ -75,7 +79,12 @@ func (c *client) GetMainPageState() (*PageState, error) { type ListEducationProductsParams struct { LearningType string - CoursesThematics string + CoursesThematics []string + CourseGraphics []string + CourseLevels []string + CourseFormats []string + CourseDurations []string + CourseTypes []string SortBy string Limit int @@ -125,7 +134,7 @@ const ( FilterGraphicTerm FilterGraphic = "courseTimeTermNew" ) -type ListEducationProductsRequest struct { +type listEducationProductsRequest struct { Fingerprint string `json:"fingerPrint,omitempty"` ProductName string `json:"productName,omitempty"` Location string `json:"location"` @@ -163,7 +172,7 @@ type ListEducationProductsRequest struct { SortDirection string `json:"sortDirection"` } -type ListEducationProductsResponse struct { +type listEducationProductsResponse struct { Items []Course `json:"items"` Organizations map[string]Organization `json:"organizations"` @@ -174,7 +183,7 @@ type ListEducationProductsResponse struct { func (c *client) ListEducationalProducts( ctx context.Context, params ListEducationProductsParams, -) (result ListEducationProductsResponse, err error) { +) (result listEducationProductsResponse, err error) { const urlPath = "/v1/education/products" const defaultLimit = 1 const defaultSortProp = "advertising.position" @@ -186,15 +195,17 @@ func (c *client) ListEducationalProducts( } if !c.validLearningTypes.hasValue(params.LearningType) { - return result, errors.NewValidationError("learning_type", "bad value") + return result, errors.NewValidationError("learning_type", "unknown value") } - if params.CoursesThematics != "" && !c.validCourseThematics.hasValue(params.CoursesThematics) { - return result, errors.NewValidationError("courses_thematics", "bad value") + for _, ct := range params.CoursesThematics { + if !c.validCourseThematics.hasValue(ct) { + return result, errors.NewValidationError("courses_thematics", "unknown value "+ct) + } } - reqParams := ListEducationProductsRequest{ + reqParams := listEducationProductsRequest{ LearningType: valueAsArray(params.LearningType), - CoursesThematics: valueAsArray(params.CoursesThematics), + CoursesThematics: params.CoursesThematics, ProductName: productName, Fields: defaultProductFields, SortProperty: defaultSortProp, // mayber sort by price? @@ -212,12 +223,11 @@ func (c *client) ListEducationalProducts( Offset: params.Offset, } - req := c.http.R(). + resp, err := c.http.R(). SetBody(reqParams). SetResult(&result). - EnableTrace() - - resp, err := req.Post(c.makeEducationURL(urlPath)) + EnableTrace(). + Post(c.makeEducationURL(urlPath)) if err != nil { return result, fmt.Errorf("making request: %w", err) } @@ -229,6 +239,88 @@ func (c *client) ListEducationalProducts( return result, nil } +type educationProductFilterCountRequest struct { + Filters educationProductFilter `json:"filters"` +} + +type educationProductFilter struct { + AdvertisingOnly bool `json:"advertisingOnly"` + Location string `json:"location"` + LearningTypes []string `json:"learningTypes"` + CoursesThematics []string `json:"coursesThematics"` + CourseGraphics []string `json:"courseGraphics"` + CourseLevels []string `json:"courseLevels"` + CourseFormats []string `json:"courseFormats"` + CourseDurations []string `json:"courseDurations"` + CourseTypes []string `json:"courseTypes"` +} + +type boolableDict map[int]int +type nameableDict map[string]int + +type ProductsFilterCount struct { + IsCourseProfession boolableDict `json:"isCourseProfession"` // 0: count, 1: count eq to false + true + CourseLevels nameableDict `json:"courseLevels"` + CourseGraphics nameableDict `json:"courseGraphics"` + OrganizationIDs nameableDict `json:"organizationIds"` + HasTrialPeriod boolableDict `json:"hasTrialPeriod"` + HasMentor boolableDict `json:"hasMentor"` + HasJobGuarantee boolableDict `json:"hasJobGuarantee"` + CourseFormats nameableDict `json:"courseFormats"` + CourseDurations nameableDict `json:"courseDurations"` + CoursesThematics nameableDict `json:"coursesThematics"` + LearningTypes nameableDict `json:"learningTypes"` +} + +func (c *client) ListEducationalProductsFilterCount( + ctx context.Context, + params ListEducationProductsParams, +) (result ProductsFilterCount, err error) { + const urlPath = "/v2/education/prodicts/fitter/count" + if err = c.checkClientInited(); err != nil { + return result, err + } + + if !c.validLearningTypes.hasValue(params.LearningType) { + return result, errors.NewValidationError("learning_type", "unknown value") + } + for _, ct := range params.CoursesThematics { + if !c.validCourseThematics.hasValue(ct) { + return result, errors.NewValidationError("courses_thematics", "unknown value "+ct) + } + } + + reqParams := educationProductFilterCountRequest{ + Filters: educationProductFilter{ + AdvertisingOnly: false, + Location: "", + LearningTypes: valueAsArray(params.LearningType), + CoursesThematics: params.CoursesThematics, + CourseGraphics: params.CourseGraphics, + CourseLevels: params.CourseLevels, + CourseFormats: params.CourseFormats, + CourseDurations: params.CourseDurations, + CourseTypes: params.CourseTypes, + }, + } + + var respData DataContainer[ProductsFilterCount] + resp, err := c.http.R(). + SetBody(reqParams). + SetResult(&result). + EnableTrace(). + Post(c.makeEducationURL(urlPath)) + if err != nil { + return result, fmt.Errorf("making request: %w", err) + } + + if resp.IsError() { + return result, fmt.Errorf("bad status code %d: %w", resp.StatusCode(), errors.ErrUnexpectedStatus) + } + + return respData.Data, nil +} + func (c *client) makeEducationURL(path string) string { if c.cachedMainPageInfo == nil { return "" diff --git a/internal/common/client/sravni/noop.go b/internal/common/client/sravni/noop.go index d694cb4..28044c9 100644 --- a/internal/common/client/sravni/noop.go +++ b/internal/common/client/sravni/noop.go @@ -12,6 +12,6 @@ func (NoopClient) GetMainPageState() (*PageState, error) { return nil, errors.ErrNotImplemented } -func (NoopClient) ListEducationalProducts(context.Context, ListEducationProductsParams) (ListEducationProductsResponse, error) { - return ListEducationProductsResponse{}, errors.ErrNotImplemented +func (NoopClient) ListEducationalProducts(context.Context, ListEducationProductsParams) (listEducationProductsResponse, error) { + return listEducationProductsResponse{}, errors.ErrNotImplemented }