~netlandish/links

34eee6b2149028ec46a8d5e92c40faead795b3a7 — Peter Sanchez 30 days ago 829bbc1
Moving organization billing status and subscription_play type to enums
M admin/input.go => admin/input.go +2 -2
@@ 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)

M admin/routes.go => admin/routes.go +1 -1
@@ 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"),

M analytics/routes.go => analytics/routes.go +1 -1
@@ 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
	}


M api/graph/generated.go => api/graph/generated.go +60 -15
@@ 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)
}

M api/graph/model/models_gen.go => api/graph/model/models_gen.go +47 -0
@@ 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 (

M api/graph/schema.graphqls => api/graph/schema.graphqls +10 -2
@@ 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

M api/graph/schema.resolvers.go => api/graph/schema.resolvers.go +52 -22
@@ 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 }

M billing/processors.go => billing/processors.go +1 -1
@@ 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)

M core/import.go => core/import.go +1 -1
@@ 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

M core/routes.go => core/routes.go +6 -6
@@ 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
	}


M core/samples/org_list.json => core/samples/org_list.json +3 -3
@@ 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"
                    }
                }
            }

M helpers.go => helpers.go +1 -1
@@ 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)

M list/routes.go => list/routes.go +1 -1
@@ 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,

M mattermost/routes.go => mattermost/routes.go +5 -5
@@ 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))

M migrations/0001_initial.down.sql => migrations/0001_initial.down.sql +1 -0
@@ 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;

M migrations/0001_initial.up.sql => migrations/0001_initial.up.sql +10 -1
@@ 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

M migrations/test_migration.up.sql => migrations/test_migration.up.sql +5 -5
@@ 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');

M models/models.go => models/models.go +1 -1
@@ 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"`

M models/organization.go => models/organization.go +8 -8
@@ 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<X>
	Status string `json:"status"` // BillingStatus<X>
}

// 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"),

M models/schema.sql => models/schema.sql +6 -1
@@ 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

M models/subscription_plan.go => models/subscription_plan.go +2 -2
@@ 12,8 12,8 @@ import (
)

const (
	SubscriptionPlanTypePersonal int = 1
	SubscriptionPlanTypeBusiness     = 2
	SubscriptionPlanTypePersonal string = "PERSONAL"
	SubscriptionPlanTypeBusiness string = "BUSINESS"
)

func (s *SubscriptionPlan) ToLocalTZ(tz string) error {

M short/routes.go => short/routes.go +1 -1
@@ 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.")

M slack/routes.go => slack/routes.go +3 -3
@@ 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))