From 34eee6b2149028ec46a8d5e92c40faead795b3a7 Mon Sep 17 00:00:00 2001 From: Peter Sanchez Date: Tue, 26 Nov 2024 16:24:21 -0600 Subject: [PATCH] Moving organization billing status and subscription_play type to enums --- admin/input.go | 4 +- admin/routes.go | 2 +- analytics/routes.go | 2 +- api/graph/generated.go | 75 +++++++++++++++++++++++++------- api/graph/model/models_gen.go | 47 ++++++++++++++++++++ api/graph/schema.graphqls | 12 ++++- api/graph/schema.resolvers.go | 74 +++++++++++++++++++++---------- billing/processors.go | 2 +- core/import.go | 2 +- core/routes.go | 12 ++--- core/samples/org_list.json | 6 +-- helpers.go | 2 +- list/routes.go | 2 +- mattermost/routes.go | 10 ++--- migrations/0001_initial.down.sql | 1 + migrations/0001_initial.up.sql | 11 ++++- migrations/test_migration.up.sql | 10 ++--- models/models.go | 2 +- models/organization.go | 16 +++---- models/schema.sql | 7 ++- models/subscription_plan.go | 4 +- short/routes.go | 2 +- slack/routes.go | 6 +-- 23 files changed, 228 insertions(+), 83 deletions(-) diff --git a/admin/input.go b/admin/input.go index 4a78018..7918875 100644 --- a/admin/input.go +++ b/admin/input.go @@ -85,13 +85,13 @@ func (d *DomainForm) Validate(c echo.Context) error { } type OrgTypeForm struct { - OrgType int `form:"org_type" validate:"oneof=0 1 2 3 4"` + OrgType string `form:"org_type" validate:"oneof=FREE PERSONAL BUSINESS OPEN_SOURCE SPONSORED"` } func (ot *OrgTypeForm) Validate(c echo.Context) error { errs := validate.FormFieldBinder(c, ot). FailFast(false). - Int("org_type", &ot.OrgType). + String("org_type", &ot.OrgType). BindErrors() if errs != nil { return validate.GetInputErrors(errs) diff --git a/admin/routes.go b/admin/routes.go index 97ec5e5..1899abb 100644 --- a/admin/routes.go +++ b/admin/routes.go @@ -250,7 +250,7 @@ func (s *Service) UpdateOrgType(c echo.Context) error { pd.Data["save"] = lt.Translate("Save") pd.Data["cancel"] = lt.Translate("Cancel") - orgTypes := map[int]string{ + orgTypes := map[string]string{ models.BillingStatusFree: lt.Translate("Free"), models.BillingStatusPersonal: lt.Translate("Personal"), models.BillingStatusBusiness: lt.Translate("Business"), diff --git a/analytics/routes.go b/analytics/routes.go index 3c012ef..42138cd 100644 --- a/analytics/routes.go +++ b/analytics/routes.go @@ -83,7 +83,7 @@ func (s *Service) Detail(c echo.Context) error { } var isRestricted bool - if org.IsRestricted([]int{models.BillingStatusFree, models.BillingStatusOpenSource}) { + if org.IsRestricted([]string{models.BillingStatusFree, models.BillingStatusOpenSource}) { isRestricted = true } diff --git a/api/graph/generated.go b/api/graph/generated.go index 72772ab..95ccca7 100644 --- a/api/graph/generated.go +++ b/api/graph/generated.go @@ -41,6 +41,7 @@ type Config struct { } type ResolverRoot interface { + BillingSettings() BillingSettingsResolver Domain() DomainResolver Mutation() MutationResolver OrgLink() OrgLinkResolver @@ -237,7 +238,7 @@ type ComplexityRoot struct { SendRegisterInvitation func(childComplexity int, toEmail string) int Unfollow func(childComplexity int, orgSlug string) int UpdateAdminDomain func(childComplexity int, input model.UpdateAdminDomainInput) int - UpdateAdminOrgType func(childComplexity int, orgSlug string, orgType int) int + UpdateAdminOrgType func(childComplexity int, orgSlug string, orgType model.OrgBillingStatus) int UpdateAdminUser func(childComplexity int, input *model.UpdateUserInput) int UpdateLink func(childComplexity int, input *model.UpdateLinkInput) int UpdateLinkShort func(childComplexity int, input *model.UpdateLinkShortInput) int @@ -431,6 +432,9 @@ type ComplexityRoot struct { } } +type BillingSettingsResolver interface { + Status(ctx context.Context, obj *models.BillingSettings) (model.OrgBillingStatus, error) +} type DomainResolver interface { OrgID(ctx context.Context, obj *models.Domain) (*model.NullInt, error) OrgSlug(ctx context.Context, obj *models.Domain) (*model.NullString, error) @@ -466,7 +470,7 @@ type MutationResolver interface { DeleteQRCode(ctx context.Context, id int) (*model.DeletePayload, error) Follow(ctx context.Context, orgSlug string) (*model.FollowPayload, error) Unfollow(ctx context.Context, orgSlug string) (*model.FollowPayload, error) - UpdateAdminOrgType(ctx context.Context, orgSlug string, orgType int) (*models.Organization, error) + UpdateAdminOrgType(ctx context.Context, orgSlug string, orgType model.OrgBillingStatus) (*models.Organization, error) AddAdminDomain(ctx context.Context, input model.AdminDomainInput) (*models.Domain, error) UpdateAdminDomain(ctx context.Context, input model.UpdateAdminDomainInput) (*models.Domain, error) UpdateAdminUser(ctx context.Context, input *model.UpdateUserInput) (*models.User, error) @@ -1509,7 +1513,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Mutation.UpdateAdminOrgType(childComplexity, args["orgSlug"].(string), args["orgType"].(int)), true + return e.complexity.Mutation.UpdateAdminOrgType(childComplexity, args["orgSlug"].(string), args["orgType"].(model.OrgBillingStatus)), true case "Mutation.updateAdminUser": if e.complexity.Mutation.UpdateAdminUser == nil { @@ -3153,10 +3157,10 @@ func (ec *executionContext) field_Mutation_updateAdminOrgType_args(ctx context.C } } args["orgSlug"] = arg0 - var arg1 int + var arg1 model.OrgBillingStatus if tmp, ok := rawArgs["orgType"]; ok { ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("orgType")) - arg1, err = ec.unmarshalNInt2int(ctx, tmp) + arg1, err = ec.unmarshalNOrgBillingStatus2linksᚋapiᚋgraphᚋmodelᚐOrgBillingStatus(ctx, tmp) if err != nil { return nil, err } @@ -5228,7 +5232,7 @@ func (ec *executionContext) _BillingSettings_status(ctx context.Context, field g }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.Status, nil + return ec.resolvers.BillingSettings().Status(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -5240,19 +5244,19 @@ func (ec *executionContext) _BillingSettings_status(ctx context.Context, field g } return graphql.Null } - res := resTmp.(int) + res := resTmp.(model.OrgBillingStatus) fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) + return ec.marshalNOrgBillingStatus2linksᚋapiᚋgraphᚋmodelᚐOrgBillingStatus(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_BillingSettings_status(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "BillingSettings", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Int does not have child fields") + return nil, errors.New("field of type OrgBillingStatus does not have child fields") }, } return fc, nil @@ -11609,7 +11613,7 @@ func (ec *executionContext) _Mutation_updateAdminOrgType(ctx context.Context, fi resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { directive0 := func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().UpdateAdminOrgType(rctx, fc.Args["orgSlug"].(string), fc.Args["orgType"].(int)) + return ec.resolvers.Mutation().UpdateAdminOrgType(rctx, fc.Args["orgSlug"].(string), fc.Args["orgType"].(model.OrgBillingStatus)) } directive1 := func(ctx context.Context) (interface{}, error) { if ec.directives.Admin == nil { @@ -23943,10 +23947,41 @@ func (ec *executionContext) _BillingSettings(ctx context.Context, sel ast.Select case "__typename": out.Values[i] = graphql.MarshalString("BillingSettings") case "status": - out.Values[i] = ec._BillingSettings_status(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._BillingSettings_status(ctx, field, obj) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -27967,6 +28002,16 @@ func (ec *executionContext) marshalNNullString2ᚖlinksᚋapiᚋgraphᚋmodelᚐ return ec._NullString(ctx, sel, v) } +func (ec *executionContext) unmarshalNOrgBillingStatus2linksᚋapiᚋgraphᚋmodelᚐOrgBillingStatus(ctx context.Context, v interface{}) (model.OrgBillingStatus, error) { + var res model.OrgBillingStatus + err := res.UnmarshalGQL(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNOrgBillingStatus2linksᚋapiᚋgraphᚋmodelᚐOrgBillingStatus(ctx context.Context, sel ast.SelectionSet, v model.OrgBillingStatus) graphql.Marshaler { + return v +} + func (ec *executionContext) marshalNOrgLink2linksᚋmodelsᚐOrgLink(ctx context.Context, sel ast.SelectionSet, v models.OrgLink) graphql.Marshaler { return ec._OrgLink(ctx, sel, &v) } diff --git a/api/graph/model/models_gen.go b/api/graph/model/models_gen.go index 3032dcd..799cba4 100644 --- a/api/graph/model/models_gen.go +++ b/api/graph/model/models_gen.go @@ -810,6 +810,53 @@ func (e MemberPermission) MarshalGQL(w io.Writer) { fmt.Fprint(w, strconv.Quote(e.String())) } +type OrgBillingStatus string + +const ( + OrgBillingStatusFree OrgBillingStatus = "FREE" + OrgBillingStatusPersonal OrgBillingStatus = "PERSONAL" + OrgBillingStatusBusiness OrgBillingStatus = "BUSINESS" + OrgBillingStatusOpenSource OrgBillingStatus = "OPEN_SOURCE" + OrgBillingStatusSponsored OrgBillingStatus = "SPONSORED" +) + +var AllOrgBillingStatus = []OrgBillingStatus{ + OrgBillingStatusFree, + OrgBillingStatusPersonal, + OrgBillingStatusBusiness, + OrgBillingStatusOpenSource, + OrgBillingStatusSponsored, +} + +func (e OrgBillingStatus) IsValid() bool { + switch e { + case OrgBillingStatusFree, OrgBillingStatusPersonal, OrgBillingStatusBusiness, OrgBillingStatusOpenSource, OrgBillingStatusSponsored: + return true + } + return false +} + +func (e OrgBillingStatus) String() string { + return string(e) +} + +func (e *OrgBillingStatus) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = OrgBillingStatus(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid OrgBillingStatus", str) + } + return nil +} + +func (e OrgBillingStatus) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + type OrgType string const ( diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls index a7ca3c4..c270102 100644 --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -102,6 +102,14 @@ enum OrgType { NORMAL } +enum OrgBillingStatus { + FREE + PERSONAL + BUSINESS + OPEN_SOURCE + SPONSORED +} + # Considering removing these Null* fields: # https://todo.code.netlandish.com/~netlandish/links/75 @@ -130,7 +138,7 @@ type User { } type BillingSettings { - status: Int! + status: OrgBillingStatus! } type OrganizationSettings { @@ -840,7 +848,7 @@ type Mutation { # Admin only. Not open to public calls # - updateAdminOrgType(orgSlug: String!, orgType: Int!): Organization! @admin + updateAdminOrgType(orgSlug: String!, orgType: OrgBillingStatus!): Organization! @admin addAdminDomain(input: AdminDomainInput!): Domain! @admin updateAdminDomain(input: UpdateAdminDomainInput!): Domain! @admin updateAdminUser(input: UpdateUserInput): User! @admin diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index 120fc78..fca7e73 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -50,6 +50,11 @@ import ( "netlandish.com/x/gobwebs/validate" ) +// Status is the resolver for the status field. +func (r *billingSettingsResolver) Status(ctx context.Context, obj *models.BillingSettings) (model.OrgBillingStatus, error) { + return model.OrgBillingStatus(obj.Status), nil +} + // OrgID is the resolver for the orgId field. func (r *domainResolver) OrgID(ctx context.Context, obj *models.Domain) (*model.NullInt, error) { return &model.NullInt{Int64: int(obj.OrgID.Int64), Valid: obj.OrgID.Valid}, nil @@ -107,7 +112,7 @@ func (r *mutationResolver) AddOrganization(ctx context.Context, input model.Orga Filter: sq.And{ sq.Eq{"o.owner_id": user.ID}, sq.Eq{"o.is_active": true}, - sq.Eq{"(o.settings->'billing'->'status')::integer": models.BillingStatusFree}, + sq.Eq{"(o.settings->'billing'->>'status')": models.BillingStatusFree}, }, } @@ -154,7 +159,12 @@ func (r *mutationResolver) AddOrganization(ctx context.Context, input model.Orga Name: input.Name, Slug: slug, IsActive: true, - Settings: models.OrganizationSettings{DefaultPerm: models.OrgLinkVisibilityPublic}, + Settings: models.OrganizationSettings{ + DefaultPerm: models.OrgLinkVisibilityPublic, + Billing: models.BillingSettings{ + Status: models.BillingStatusFree, + }, + }, } if input.Image != nil { @@ -262,7 +272,7 @@ func (r *mutationResolver) AddLink(ctx context.Context, input *model.LinkInput) srv := server.ForContext(ctx) // We want to restrict private links only to paid users if visibility == models.OrgLinkVisibilityPrivate && - links.BillingEnabled(srv.Config) && org.IsRestricted([]int{models.BillingStatusFree}) { + links.BillingEnabled(srv.Config) && org.IsRestricted([]string{models.BillingStatusFree}) { validator.Error(lt.Translate("Free organizations are not allowed to create private links. Please upgrade")). WithField("visibility"). WithCode(valid.ErrValidationCode) @@ -441,7 +451,7 @@ func (r *mutationResolver) UpdateLink(ctx context.Context, input *model.UpdateLi // We want to restrict private links only to paid users if string(*input.Visibility) == models.OrgLinkVisibilityPrivate && links.BillingEnabled(srv.Config) && - org.IsRestricted([]int{models.BillingStatusFree}) { + org.IsRestricted([]string{models.BillingStatusFree}) { validator.Error(lt.Translate("Free organizations are not allowed to create private links. Please upgrade")). WithField("visibility"). WithCode(valid.ErrValidationCode) @@ -684,7 +694,7 @@ func (r *mutationResolver) AddNote(ctx context.Context, input *model.NoteInput) srv := server.ForContext(ctx) // We want to restrict private links only to paid users if string(input.Visibility) == models.OrgLinkVisibilityPrivate && - links.BillingEnabled(srv.Config) && org.IsRestricted([]int{models.BillingStatusFree}) { + links.BillingEnabled(srv.Config) && org.IsRestricted([]string{models.BillingStatusFree}) { validator.Error(lt.Translate("Free organizations are not allowed to create private notes. Please upgrade")). WithField("visibility"). WithCode(valid.ErrValidationCode) @@ -819,7 +829,7 @@ func (r *mutationResolver) AddMember(ctx context.Context, input *model.MemberInp } srv := server.ForContext(ctx) - if links.BillingEnabled(srv.Config) && org.IsRestricted([]int{models.BillingStatusFree, + if links.BillingEnabled(srv.Config) && org.IsRestricted([]string{models.BillingStatusFree, models.BillingStatusOpenSource, models.BillingStatusPersonal}) { validator.Error(lt.Translate("This function is only allowed for business users.")). WithCode(valid.ErrRestrictedCode) @@ -1056,7 +1066,7 @@ func (r *mutationResolver) ConfirmMember(ctx context.Context, key string) (*mode opts := &database.FilterOptions{ Filter: sq.And{ sq.Eq{"o.id": orgID}, - sq.NotEq{"(o.settings->'billing'->'status')::integer": models.BillingStatusFree}, + sq.NotEq{"(o.settings->'billing'->>'status')": models.BillingStatusFree}, sq.Eq{"o.is_active": true}, }, Limit: 1, @@ -1238,6 +1248,12 @@ func (r *mutationResolver) Register(ctx context.Context, input *model.RegisterIn Name: user.Name, Slug: slug, IsActive: true, + Settings: models.OrganizationSettings{ + DefaultPerm: models.OrgLinkVisibilityPublic, + Billing: models.BillingSettings{ + Status: models.BillingStatusFree, + }, + }, } if err = org.Store(ctx); err != nil { @@ -1277,6 +1293,9 @@ func (r *mutationResolver) Register(ctx context.Context, input *model.RegisterIn } // CompleteRegister is the resolver for the completeRegister field. +// This is used when adding a member to an existing organization but the email account +// is not registered. When completing their acceptance they will register an account +// during the process. This method is for that process specifically. func (r *mutationResolver) CompleteRegister(ctx context.Context, input *model.CompleteRegisterInput) (*models.User, error) { lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), nil) lt := localizer.GetLocalizer(lang) @@ -1352,7 +1371,7 @@ func (r *mutationResolver) CompleteRegister(ctx context.Context, input *model.Co Filter: sq.And{ sq.Eq{"o.id": targetOrgID}, sq.Eq{"o.is_active": true}, - sq.NotEq{"(o.settings->'billing'->'status')::integer": models.BillingStatusFree}, + sq.NotEq{"(o.settings->'billing'->>'status')": models.BillingStatusFree}, }, Limit: 1, } @@ -1417,6 +1436,12 @@ func (r *mutationResolver) CompleteRegister(ctx context.Context, input *model.Co Name: user.Name, Slug: slug, IsActive: true, + Settings: models.OrganizationSettings{ + DefaultPerm: models.OrgLinkVisibilityPublic, + Billing: models.BillingSettings{ + Status: models.BillingStatusFree, + }, + }, } if err = userOrg.Store(ctx); err != nil { @@ -1679,7 +1704,7 @@ func (r *mutationResolver) UpdateOrganization(ctx context.Context, input *model. sq.NotEq{"o.id": org.ID}, sq.Eq{"o.owner_id": user.ID}, sq.Eq{"o.is_active": true}, - sq.Eq{"(o.settings->'billing'->'status')::integer": models.BillingStatusFree}, + sq.Eq{"(o.settings->'billing'->>'status')": models.BillingStatusFree}, }, } @@ -1783,7 +1808,7 @@ func (r *mutationResolver) AddDomain(ctx context.Context, input model.DomainInpu } srv := server.ForContext(ctx) - if links.BillingEnabled(srv.Config) && org.IsRestricted([]int{models.BillingStatusFree}) { + if links.BillingEnabled(srv.Config) && org.IsRestricted([]string{models.BillingStatusFree}) { validator.Error(lt.Translate("This function is only allowed for paid users.")). WithCode(valid.ErrRestrictedCode) return nil, nil @@ -2431,7 +2456,7 @@ func (r *mutationResolver) AddListing(ctx context.Context, input *model.AddListi srv := server.ForContext(ctx) // NOTE should we include BillinStatusOpenSource? - if links.BillingEnabled(srv.Config) && org.IsRestricted([]int{models.BillingStatusFree}) { + if links.BillingEnabled(srv.Config) && org.IsRestricted([]string{models.BillingStatusFree}) { opts := &database.FilterOptions{ Filter: sq.Eq{"l.org_id": org.ID}, Limit: 1, @@ -3343,7 +3368,7 @@ func (r *mutationResolver) AddQRCode(ctx context.Context, input model.AddQRCodeI if input.Image != nil { if links.BillingEnabled(srv.Config) && org.IsRestricted( - []int{models.BillingStatusFree, models.BillingStatusOpenSource}) { + []string{models.BillingStatusFree, models.BillingStatusOpenSource}) { validator.Error(lt.Translate("This function is only allowed for paid users.")). WithField("image"). WithCode(valid.ErrValidationCode) @@ -3390,7 +3415,7 @@ func (r *mutationResolver) AddQRCode(ctx context.Context, input model.AddQRCodeI options = append(options, standard.WithLogoImage(qrLogo)) } } else if links.BillingEnabled(srv.Config) && org.IsRestricted( - []int{models.BillingStatusFree, models.BillingStatusOpenSource}) { + []string{models.BillingStatusFree, models.BillingStatusOpenSource}) { // If not custom image is provided and the org is restricted // we want to add the app logo @@ -3636,7 +3661,7 @@ func (r *mutationResolver) Unfollow(ctx context.Context, orgSlug string) (*model } // UpdateAdminOrgType is the resolver for the updateAdminOrgType field. -func (r *mutationResolver) UpdateAdminOrgType(ctx context.Context, orgSlug string, orgType int) (*models.Organization, error) { +func (r *mutationResolver) UpdateAdminOrgType(ctx context.Context, orgSlug string, orgType model.OrgBillingStatus) (*models.Organization, error) { tokenUser := oauth2.ForContext(ctx) if tokenUser == nil { return nil, valid.ErrAuthorization @@ -3650,11 +3675,12 @@ func (r *mutationResolver) UpdateAdminOrgType(ctx context.Context, orgSlug strin WithCode(valid.ErrNotFoundCode) return nil, nil } - if orgType != models.BillingStatusFree && - orgType != models.BillingStatusPersonal && - orgType != models.BillingStatusBusiness && - orgType != models.BillingStatusOpenSource && - orgType != models.BillingStatusSponsored { + _orgType := string(orgType) + if _orgType != models.BillingStatusFree && + _orgType != models.BillingStatusPersonal && + _orgType != models.BillingStatusBusiness && + _orgType != models.BillingStatusOpenSource && + _orgType != models.BillingStatusSponsored { validator.Error(lt.Translate("Invalid orgType")). WithField("orgType"). WithCode(valid.ErrValidationCode) @@ -3679,7 +3705,7 @@ func (r *mutationResolver) UpdateAdminOrgType(ctx context.Context, orgSlug strin return nil, nil } org := orgs[0] - org.Settings.Billing.Status = orgType + org.Settings.Billing.Status = _orgType err = org.Store(ctx) if err != nil { return nil, err @@ -5731,7 +5757,7 @@ func (r *queryResolver) Analytics(ctx context.Context, input model.AnalyticsInpu } var isRestricted bool - if org.IsRestricted([]int{models.BillingStatusFree, models.BillingStatusOpenSource}) { + if org.IsRestricted([]string{models.BillingStatusFree, models.BillingStatusOpenSource}) { isRestricted = true } @@ -5965,7 +5991,7 @@ func (r *queryResolver) GetFeed(ctx context.Context, input *model.GetFeedInput) sq.And{ sq.Eq{"ou.user_id": user.ID}, sq.Eq{"o.is_active": true}, - sq.Eq{"(o.settings->'billing'->'status')::integer": models.BillingStatusBusiness}, + sq.Eq{"(o.settings->'billing'->>'status')": models.BillingStatusBusiness}, }, }, OrderBy: "ol.id DESC", @@ -6694,6 +6720,9 @@ func (r *userResolver) ID(ctx context.Context, obj *models.User) (int, error) { return int(obj.ID), nil } +// BillingSettings returns BillingSettingsResolver implementation. +func (r *Resolver) BillingSettings() BillingSettingsResolver { return &billingSettingsResolver{r} } + // Domain returns DomainResolver implementation. func (r *Resolver) Domain() DomainResolver { return &domainResolver{r} } @@ -6717,6 +6746,7 @@ func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } // User returns UserResolver implementation. func (r *Resolver) User() UserResolver { return &userResolver{r} } +type billingSettingsResolver struct{ *Resolver } type domainResolver struct{ *Resolver } type mutationResolver struct{ *Resolver } type orgLinkResolver struct{ *Resolver } diff --git a/billing/processors.go b/billing/processors.go index 9b968f3..bdc18f8 100644 --- a/billing/processors.go +++ b/billing/processors.go @@ -274,7 +274,7 @@ func ProcessSubscriptionDeletedTask(ctx context.Context, srv *server.Server, dat sq.Eq{"o.is_active": true}, sq.Eq{"o.owner_id": org.OwnerID}, sq.NotEq{"o.id": org.ID}, - sq.Eq{"(o.settings->'billing'->'status')::integer": models.BillingStatusFree}, + sq.Eq{"(o.settings->'billing'->>'status')": models.BillingStatusFree}, }, } freeOrgs, err := models.GetOrganizations(ctx, opts) diff --git a/core/import.go b/core/import.go index 0a12143..898204a 100644 --- a/core/import.go +++ b/core/import.go @@ -265,7 +265,7 @@ func processOrgLinks(obj importObj, baseURLMap map[string]int, var vis string if obj.IsPublic() { vis = models.OrgLinkVisibilityPublic - } else if billEnabled && org.IsRestricted([]int{models.BillingStatusFree}) { + } else if billEnabled && org.IsRestricted([]string{models.BillingStatusFree}) { vis = models.OrgLinkVisibilityRestricted } else { vis = models.OrgLinkVisibilityPrivate diff --git a/core/routes.go b/core/routes.go index 5cddb4b..9eac080 100644 --- a/core/routes.go +++ b/core/routes.go @@ -530,7 +530,7 @@ func (s *Service) DomainCreate(c echo.Context) error { } // NOTE should we include BillinStatusOpenSource? - if links.BillingEnabled(gctx.Server.Config) && curOrg.IsRestricted([]int{models.BillingStatusFree}) { + if links.BillingEnabled(gctx.Server.Config) && curOrg.IsRestricted([]string{models.BillingStatusFree}) { return links.RenderRestrictedTemplate(c) } return s.Render(c, http.StatusOK, "domain_create.html", gmap) @@ -812,7 +812,7 @@ func (s *Service) OrgCreate(c echo.Context) error { Filter: sq.And{ sq.Eq{"o.is_active": true}, sq.Eq{"o.owner_id": gctx.User.GetID()}, - sq.Eq{"(o.settings->'billing'->'status')::integer": models.BillingStatusFree}, + sq.Eq{"(o.settings->'billing'->>'status')": models.BillingStatusFree}, }, } @@ -1092,7 +1092,7 @@ func (s *Service) OrgMembersAdd(c echo.Context) error { } var isRestricted bool - if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]int{models.BillingStatusFree, + if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]string{models.BillingStatusFree, models.BillingStatusOpenSource, models.BillingStatusPersonal}) { isRestricted = true } @@ -1290,7 +1290,7 @@ func (s *Service) OrgMembersList(c echo.Context) error { org := orgs[0] var isRestricted bool - if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]int{models.BillingStatusFree, + if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]string{models.BillingStatusFree, models.BillingStatusOpenSource, models.BillingStatusPersonal}) { isRestricted = true } @@ -2604,7 +2604,7 @@ func (s *Service) QRRedirect(c echo.Context) error { } org := orgs[0] - if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]int{ + if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]string{ models.BillingStatusFree, models.BillingStatusOpenSource}) { lt := localizer.GetSessionLocalizer(c) pd := localizer.NewPageData(lt.Translate("QR Codes powered by Link Taco!")) @@ -2897,7 +2897,7 @@ func (s *Service) Integrations(c echo.Context) error { pd.Data["connected"] = lt.Translate("Connected") pd.Data["back"] = lt.Translate("Back") var isRestricted bool - if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]int{models.BillingStatusFree}) { + if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]string{models.BillingStatusFree}) { isRestricted = true } diff --git a/core/samples/org_list.json b/core/samples/org_list.json index 7387500..1e0c1ea 100644 --- a/core/samples/org_list.json +++ b/core/samples/org_list.json @@ -8,7 +8,7 @@ "isActive": true, "settings": { "billing": { - "status": 1 + "status": "PERSONAL" } } }, @@ -19,7 +19,7 @@ "isActive": true, "settings": { "billing": { - "status": 2 + "status": "BUSINESS" } } }, @@ -30,7 +30,7 @@ "isActive": true, "settings": { "billing": { - "status": 0 + "status": "FREE" } } } diff --git a/helpers.go b/helpers.go index 48198a4..1585d95 100644 --- a/helpers.go +++ b/helpers.go @@ -578,7 +578,7 @@ func GetDisabledElementsByOrg(ctx context.Context, org *models.Organization) (*C sq.Eq{"o.is_active": true}, sq.Eq{"o.owner_id": org.OwnerID}, sq.NotEq{"o.id": org.ID}, - sq.Eq{"(o.settings->'billing'->'status')::integer": models.BillingStatusFree}, + sq.Eq{"(o.settings->'billing'->>'status')": models.BillingStatusFree}, }, } freeOrgs, err := models.GetOrganizations(ctx, opts) diff --git a/list/routes.go b/list/routes.go index 9ee28dd..4c8063d 100644 --- a/list/routes.go +++ b/list/routes.go @@ -933,7 +933,7 @@ func (s *Service) ListingCreate(c echo.Context) error { c.Echo().Reverse(s.RouteName("listing_list"), org.Slug)) } // NOTE should we include BillinStatusOpenSource? - if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]int{models.BillingStatusFree}) { + if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]string{models.BillingStatusFree}) { opts := &database.FilterOptions{ Filter: sq.Eq{"l.org_id": org.ID}, Limit: 1, diff --git a/mattermost/routes.go b/mattermost/routes.go index de7cbb7..e9094d0 100644 --- a/mattermost/routes.go +++ b/mattermost/routes.go @@ -146,7 +146,7 @@ func (s *Service) Connect(c echo.Context) error { "org": org, "teamID": teamID, } - if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]int{models.BillingStatusFree}) { + if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]string{models.BillingStatusFree}) { gmap["isRestricted"] = true return s.Render(c, http.StatusOK, "connect_mattermost.html", gmap) } @@ -194,7 +194,7 @@ func (s *Service) ConnectUser(c echo.Context) error { org := orgs[0] gctx := c.(*server.Context) - if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]int{models.BillingStatusFree}) { + if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]string{models.BillingStatusFree}) { lt := localizer.GetSessionLocalizer(c) messages.Error(c, lt.Translate("Sorry, free accounts do not support Mattermost Integration. Please upgrade to continue")) return c.Redirect(http.StatusMovedPermanently, c.Echo().Reverse("billing:create_subscription", org.Slug)) @@ -291,7 +291,7 @@ func (s *Service) SearchCommand(c echo.Context) error { return fmt.Errorf("No org found") } org := orgs[0] - if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]int{models.BillingStatusFree}) { + if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]string{models.BillingStatusFree}) { msg := lt.Translate("Sorry, free accounts do not support Mattermost Integration. Please upgrade to continue") return c.JSON(http.StatusOK, apps.NewTextResponse(msg)) } @@ -402,7 +402,7 @@ func (s *Service) ShortCommand(c echo.Context) error { gctx := c.(*server.Context) org := orgs[0] - if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]int{models.BillingStatusFree}) { + if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]string{models.BillingStatusFree}) { msg := lt.Translate("Sorry, free accounts do not support Mattermost Integration. Please upgrade to continue") return c.JSON(http.StatusOK, apps.NewTextResponse(msg)) } @@ -543,7 +543,7 @@ func (s *Service) AddCommand(c echo.Context) error { gctx := c.(*server.Context) org := orgs[0] - if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]int{models.BillingStatusFree}) { + if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]string{models.BillingStatusFree}) { lt := localizer.GetSessionLocalizer(c) msg := lt.Translate("Sorry, free accounts do not support Mattermost Integration. Please upgrade to continue") return c.JSON(http.StatusOK, apps.NewTextResponse(msg)) diff --git a/migrations/0001_initial.down.sql b/migrations/0001_initial.down.sql index b3423b0..f8b879b 100644 --- a/migrations/0001_initial.down.sql +++ b/migrations/0001_initial.down.sql @@ -40,3 +40,4 @@ DROP TYPE IF EXISTS org_link_visibility; DROP TYPE IF EXISTS org_link_type; DROP TYPE IF EXISTS org_user_perm; DROP TYPE IF EXISTS org_type; +DROP TYPE IF EXISTS subscription_type; diff --git a/migrations/0001_initial.up.sql b/migrations/0001_initial.up.sql index ac4a756..8bbaee8 100644 --- a/migrations/0001_initial.up.sql +++ b/migrations/0001_initial.up.sql @@ -89,6 +89,15 @@ EXCEPTION WHEN duplicate_object THEN null; END $$; +DO $$ BEGIN + CREATE TYPE subscription_type AS ENUM ( + 'PERSONAL', + 'BUSINESS' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + CREATE TABLE users ( id SERIAL PRIMARY KEY, @@ -519,7 +528,7 @@ CREATE TABLE subscription_plans ( plan_id VARCHAR(150) NOT NULL, stripe_price_id VARCHAR(150) NOT NULL, price INT NOT NULL CHECK (price > 0), - "type" INT NOT NULL DEFAULT 0, + "type" subscription_type NOT NULL DEFAULT 'PERSONAL', is_active BOOLEAN DEFAULT true, created_on TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_on TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP diff --git a/migrations/test_migration.up.sql b/migrations/test_migration.up.sql index bc12ac9..5edff4e 100644 --- a/migrations/test_migration.up.sql +++ b/migrations/test_migration.up.sql @@ -2,10 +2,10 @@ INSERT INTO users (full_name, password, email, is_verified) VALUES ('user', 'qwe INSERT INTO users (full_name, password, email, is_verified) VALUES ('test_api_user', 'qwerty', 'test@api.com', true); INSERT INTO users (full_name, password, email, is_verified, is_superuser) VALUES ('superuser', 'qwerty', 'superuser@api.com', true, true); -INSERT INTO organizations (owner_id, name, slug) VALUES (1, 'personal org', 'personal-org'); -INSERT INTO organizations (owner_id, name, slug, org_type) VALUES (1, 'business org', 'business_org', 'NORMAL'); +INSERT INTO organizations (owner_id, name, slug, settings) VALUES (1, 'personal org', 'personal-org', '{"billing": {"status": "FREE"}, "default_perm": "PUBLIC"}'); +INSERT INTO organizations (owner_id, name, slug, org_type, settings) VALUES (1, 'business org', 'business_org', 'NORMAL', '{"billing": {"status": "FREE"}, "default_perm": "PUBLIC"}'); -INSERT INTO organizations (owner_id, name, slug) VALUES (2, 'api test org', 'api-test-org'); +INSERT INTO organizations (owner_id, name, slug, settings) VALUES (2, 'api test org', 'api-test-org', '{"billing": {"status": "FREE"}, "default_perm": "PUBLIC"}'); INSERT INTO base_urls (url, hash) VALUES ('http://base.com', 'abcdefg'); @@ -25,5 +25,5 @@ INSERT INTO listing_links (id, listing_id, url, description, link_order, user_id INSERT INTO qr_codes (id, hash_id, url, org_id, code_type, image_path, user_id, title) VALUES (100, 'a10x', 'http://short.com/abc', 1, 1, 'path/qr.png', 1, 'title'); INSERT INTO qr_codes (id, hash_id, url, org_id, code_type, image_path, user_id, title) VALUES (101, 'a99z', 'http://list.com/abc', 1, 0, 'path/qr.png', 1, 'title'); -INSERT INTO subscription_plans (id, name, plan_id, stripe_price_id, price, type) VALUES (1, 'Personal', 'plan_personal', 'price_personal', 1000, 1); -INSERT INTO subscription_plans (id, name, plan_id, stripe_price_id, price, type) VALUES (2, 'Business', 'plan_business', 'price_business', 2000, 2); +INSERT INTO subscription_plans (id, name, plan_id, stripe_price_id, price, type) VALUES (1, 'Personal', 'plan_personal', 'price_personal', 1000, 'PERSONAL'); +INSERT INTO subscription_plans (id, name, plan_id, stripe_price_id, price, type) VALUES (2, 'Business', 'plan_business', 'price_business', 2000, 'BUSINESS'); diff --git a/models/models.go b/models/models.go index 8b83e18..b919f8f 100644 --- a/models/models.go +++ b/models/models.go @@ -375,7 +375,7 @@ type SubscriptionPlan struct { PlanID string `db:"plan_id"` StripePriceID string `db:"stripe_price_id"` Price int `db:"price"` - Type int `db:"type"` + Type string `db:"type"` IsActive bool `db:"is_active"` CreatedOn time.Time `db:"created_on"` UpdatedOn time.Time `db:"updated_on"` diff --git a/models/organization.go b/models/organization.go index 76fdb88..79844e9 100644 --- a/models/organization.go +++ b/models/organization.go @@ -28,15 +28,15 @@ const ( // Billing status, used for org billing setting/metadata // and plan type const ( - BillingStatusFree int = iota - BillingStatusPersonal - BillingStatusBusiness - BillingStatusOpenSource - BillingStatusSponsored + BillingStatusFree string = "FREE" + BillingStatusPersonal string = "PERSONAL" + BillingStatusBusiness string = "BUSINESS" + BillingStatusOpenSource string = "OPEN_SOURCE" + BillingStatusSponsored string = "SPONSORED" ) type BillingSettings struct { - Status int `json:"status"` // BillingStatus + Status string `json:"status"` // BillingStatus } // OrganizationSettings ... @@ -253,7 +253,7 @@ func (o *Organization) CanAdminWrite(ctx context.Context, user *User) bool { return o.permCheck(ctx, user, OrgUserPermissionAdminWrite) } -func (o *Organization) IsRestricted(restrictedStatus []int) bool { +func (o *Organization) IsRestricted(restrictedStatus []string) bool { status := o.Settings.Billing.Status for _, i := range restrictedStatus { if i == status { @@ -269,7 +269,7 @@ func (o *Organization) IsFreeAccount() bool { func (o *Organization) DisplayBillingStatus(c echo.Context) string { lt := localizer.GetSessionLocalizer(c) - status := map[int]string{ + status := map[string]string{ BillingStatusFree: lt.Translate("Free"), BillingStatusPersonal: lt.Translate("Personal"), BillingStatusBusiness: lt.Translate("Business"), diff --git a/models/schema.sql b/models/schema.sql index 53372c1..ff9bacb 100644 --- a/models/schema.sql +++ b/models/schema.sql @@ -56,6 +56,11 @@ CREATE TYPE org_type AS ENUM ( 'NORMAL' ); +CREATE TYPE subscription_type AS ENUM ( + 'PERSONAL', + 'BUSINESS' +); + CREATE TABLE users ( id SERIAL PRIMARY KEY, @@ -486,7 +491,7 @@ CREATE TABLE subscription_plans ( plan_id VARCHAR(150) NOT NULL, stripe_price_id VARCHAR(150) NOT NULL, price INT NOT NULL CHECK (price > 0), - "type" INT NOT NULL DEFAULT 0, + "type" subscription_type NOT NULL DEFAULT 'PERSONAL', is_active BOOLEAN DEFAULT true, created_on TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_on TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP diff --git a/models/subscription_plan.go b/models/subscription_plan.go index 564ad6a..fb28a61 100644 --- a/models/subscription_plan.go +++ b/models/subscription_plan.go @@ -12,8 +12,8 @@ import ( ) const ( - SubscriptionPlanTypePersonal int = 1 - SubscriptionPlanTypeBusiness = 2 + SubscriptionPlanTypePersonal string = "PERSONAL" + SubscriptionPlanTypeBusiness string = "BUSINESS" ) func (s *SubscriptionPlan) ToLocalTZ(tz string) error { diff --git a/short/routes.go b/short/routes.go index 85f415a..e241865 100644 --- a/short/routes.go +++ b/short/routes.go @@ -883,7 +883,7 @@ func (r *RedirectService) LinkShort(c echo.Context) error { org := orgs[0] if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted( - []int{models.BillingStatusFree, models.BillingStatusOpenSource}) { + []string{models.BillingStatusFree, models.BillingStatusOpenSource}) { lt := localizer.GetSessionLocalizer(c) pd := localizer.NewPageData(lt.Translate("URL Shortening powered by Link Taco!")) pd.Data["redirected"] = lt.Translate("You will be redirected in 10 seconds.") diff --git a/slack/routes.go b/slack/routes.go index f3e72c7..879f132 100644 --- a/slack/routes.go +++ b/slack/routes.go @@ -117,7 +117,7 @@ func (s *Service) ConnectUser(c echo.Context) error { org := orgs[0] gctx := c.(*server.Context) - if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]int{models.BillingStatusFree}) { + if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]string{models.BillingStatusFree}) { lt := localizer.GetSessionLocalizer(c) messages.Error(c, lt.Translate("Sorry, free accounts do not support Slack Integration. Please upgrade to continue")) return c.Redirect(http.StatusMovedPermanently, c.Echo().Reverse("billing:create_subscription", org.Slug)) @@ -206,7 +206,7 @@ func (s *Service) SlashCommand(c echo.Context) error { return fmt.Errorf("No org found") } org := orgs[0] - if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]int{models.BillingStatusFree}) { + if links.BillingEnabled(gctx.Server.Config) && org.IsRestricted([]string{models.BillingStatusFree}) { lt := localizer.GetSessionLocalizer(c) msg := lt.Translate("Sorry, free accounts do not support Slack Integration. Please upgrade to continue") return c.JSON(http.StatusOK, sendErrorMsg(msg)) @@ -307,7 +307,7 @@ func (s *Service) ConnectSlack(c echo.Context) error { org := orgs[0] if links.BillingEnabled(gctx.Server.Config) && - org.IsRestricted([]int{models.BillingStatusFree}) { + org.IsRestricted([]string{models.BillingStatusFree}) { lt := localizer.GetSessionLocalizer(c) messages.Error(c, lt.Translate("Sorry, free accounts do not support Slack Integration. Please upgrade to continue")) return c.Redirect(http.StatusMovedPermanently, c.Echo().Reverse("billing:create_subscription", org.Slug)) -- 2.45.2