Skip to content

Commit

Permalink
feat: support Pricings flow (casdoor#2250)
Browse files Browse the repository at this point in the history
* feat: fix price display

* feat: support subscription

* feat: fix select-plan-> signup -> buy-plan -> login flow

* feat: support paid-user to login and jump to the pricing page

* feat: support more subscription state

* feat: add payment providers for plan

* feat: format code

* feat: gofumpt

* feat: redirect to buy-plan-result page when user have pending subscription

* feat: response err when pricing don't exit

* Update PricingListPage.js

* Update ProductBuyPage.js

* Update LoginPage.js

---------

Co-authored-by: hsluoyz <[email protected]>
  • Loading branch information
Chinoholo0807 and hsluoyz authored Aug 24, 2023
1 parent 8073dfa commit 05b2f00
Show file tree
Hide file tree
Showing 31 changed files with 745 additions and 281 deletions.
1 change: 1 addition & 0 deletions authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ p, *, *, GET, /api/get-prometheus-info, *, *
p, *, *, *, /api/metrics, *, *
p, *, *, GET, /api/get-pricing, *, *
p, *, *, GET, /api/get-plan, *, *
p, *, *, GET, /api/get-subscription, *, *
p, *, *, GET, /api/get-organization-names, *, *
`

Expand Down
23 changes: 12 additions & 11 deletions controllers/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,22 @@ func (c *ApiController) Signup() {
return
}

userType := "normal-user"
if authForm.Plan != "" && authForm.Pricing != "" {
err = object.CheckPricingAndPlan(authForm.Organization, authForm.Pricing, authForm.Plan)
if err != nil {
c.ResponseError(err.Error())
return
}
userType = "paid-user"
}

user := &object.User{
Owner: authForm.Organization,
Name: username,
CreatedTime: util.GetCurrentTime(),
Id: id,
Type: "normal-user",
Type: userType,
Password: authForm.Password,
DisplayName: authForm.Name,
Avatar: organization.DefaultAvatar,
Expand Down Expand Up @@ -210,7 +220,7 @@ func (c *ApiController) Signup() {
return
}

if application.HasPromptPage() {
if application.HasPromptPage() && user.Type == "normal-user" {
// The prompt page needs the user to be signed in
c.SetSessionUsername(user.GetId())
}
Expand All @@ -227,15 +237,6 @@ func (c *ApiController) Signup() {
return
}

isSignupFromPricing := authForm.Plan != "" && authForm.Pricing != ""
if isSignupFromPricing {
_, err = object.Subscribe(organization.Name, user.Name, authForm.Plan, authForm.Pricing)
if err != nil {
c.ResponseError(err.Error())
return
}
}

record := object.NewRecord(c.Ctx)
record.Organization = application.Organization
record.User = user.Name
Expand Down
40 changes: 40 additions & 0 deletions controllers/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,46 @@ func (c *ApiController) HandleLoggedIn(application *object.Application, user *ob
}
}

// check whether paid-user have active subscription
if user.Type == "paid-user" {
subscriptions, err := object.GetSubscriptionsByUser(user.Owner, user.Name)
if err != nil {
c.ResponseError(err.Error())
return
}
existActiveSubscription := false
for _, subscription := range subscriptions {
if subscription.State == object.SubStateActive {
existActiveSubscription = true
break
}
}
if !existActiveSubscription {
// check pending subscription
for _, sub := range subscriptions {
if sub.State == object.SubStatePending {
c.ResponseOk("BuyPlanResult", sub)
return
}
}
// paid-user does not have active or pending subscription, find the default pricing of application
pricing, err := object.GetApplicationDefaultPricing(application.Organization, application.Name)
if err != nil {
c.ResponseError(err.Error())
return
}
if pricing == nil {
c.ResponseError(fmt.Sprintf(c.T("auth:paid-user %s does not have active or pending subscription and the application: %s does not have default pricing"), user.Name, application.Name))
return
} else {
// let the paid-user select plan
c.ResponseOk("SelectPlan", pricing)
return
}

}
}

if form.Type == ResponseTypeLogin {
c.SetSessionUsername(userId)
util.LogInfo(c.Ctx, "API: [%s] signed in", userId)
Expand Down
38 changes: 34 additions & 4 deletions controllers/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package controllers

import (
"encoding/json"
"fmt"

"github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
Expand Down Expand Up @@ -82,7 +83,10 @@ func (c *ApiController) GetPlan() {
c.ResponseError(err.Error())
return
}

if plan == nil {
c.ResponseError(fmt.Sprintf(c.T("plan:The plan: %s does not exist"), id))
return
}
if includeOption {
options, err := object.GetPermissionsByRole(plan.Role)
if err != nil {
Expand Down Expand Up @@ -117,7 +121,20 @@ func (c *ApiController) UpdatePlan() {
c.ResponseError(err.Error())
return
}

if plan.Product != "" {
planId := util.GetId(plan.Owner, plan.Product)
product, err := object.GetProduct(planId)
if err != nil {
c.ResponseError(err.Error())
return
}
object.UpdateProductForPlan(&plan, product)
_, err = object.UpdateProduct(planId, product)
if err != nil {
c.ResponseError(err.Error())
return
}
}
c.Data["json"] = wrapActionResponse(object.UpdatePlan(id, &plan))
c.ServeJSON()
}
Expand All @@ -136,7 +153,14 @@ func (c *ApiController) AddPlan() {
c.ResponseError(err.Error())
return
}

// Create a related product for plan
product := object.CreateProductForPlan(&plan)
_, err = object.AddProduct(product)
if err != nil {
c.ResponseError(err.Error())
return
}
plan.Product = product.Name
c.Data["json"] = wrapActionResponse(object.AddPlan(&plan))
c.ServeJSON()
}
Expand All @@ -155,7 +179,13 @@ func (c *ApiController) DeletePlan() {
c.ResponseError(err.Error())
return
}

if plan.Product != "" {
_, err = object.DeleteProduct(&object.Product{Owner: plan.Owner, Name: plan.Product})
if err != nil {
c.ResponseError(err.Error())
return
}
}
c.Data["json"] = wrapActionResponse(object.DeletePlan(&plan))
c.ServeJSON()
}
6 changes: 5 additions & 1 deletion controllers/pricing.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package controllers

import (
"encoding/json"
"fmt"

"github.com/beego/beego/utils/pagination"
"github.com/casdoor/casdoor/object"
Expand Down Expand Up @@ -80,7 +81,10 @@ func (c *ApiController) GetPricing() {
c.ResponseError(err.Error())
return
}

if pricing == nil {
c.ResponseError(fmt.Sprintf(c.T("pricing:The pricing: %s does not exist"), id))
return
}
c.ResponseOk(pricing)
}

Expand Down
16 changes: 11 additions & 5 deletions controllers/product.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,17 @@ func (c *ApiController) DeleteProduct() {
// @router /buy-product [post]
func (c *ApiController) BuyProduct() {
id := c.Input().Get("id")
providerName := c.Input().Get("providerName")
host := c.Ctx.Request.Host

userId := c.GetSessionUsername()
providerName := c.Input().Get("providerName")
// buy `pricingName/planName` for `paidUserName`
pricingName := c.Input().Get("pricingName")
planName := c.Input().Get("planName")
paidUserName := c.Input().Get("userName")
owner, _ := util.GetOwnerAndNameFromId(id)
userId := util.GetId(owner, paidUserName)
if paidUserName == "" {
userId = c.GetSessionUsername()
}
if userId == "" {
c.ResponseError(c.T("general:Please login first"))
return
Expand All @@ -175,13 +182,12 @@ func (c *ApiController) BuyProduct() {
c.ResponseError(err.Error())
return
}

if user == nil {
c.ResponseError(fmt.Sprintf(c.T("general:The user: %s doesn't exist"), userId))
return
}

payUrl, orderId, err := object.BuyProduct(id, providerName, user, host)
payUrl, orderId, err := object.BuyProduct(id, user, providerName, pricingName, planName, host)
if err != nil {
c.ResponseError(err.Error())
return
Expand Down
49 changes: 10 additions & 39 deletions object/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,21 @@ type Plan struct {
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Description string `xorm:"varchar(100)" json:"description"`

PricePerMonth float64 `json:"pricePerMonth"`
PricePerYear float64 `json:"pricePerYear"`
Currency string `xorm:"varchar(100)" json:"currency"`
IsEnabled bool `json:"isEnabled"`
PricePerMonth float64 `json:"pricePerMonth"`
PricePerYear float64 `json:"pricePerYear"`
Currency string `xorm:"varchar(100)" json:"currency"`
Product string `json:"product"` // related product id
PaymentProviders []string `xorm:"varchar(100)" json:"paymentProviders"` // payment providers for related product
IsEnabled bool `json:"isEnabled"`

Role string `xorm:"varchar(100)" json:"role"`
Options []string `xorm:"-" json:"options"`
}

func (plan *Plan) GetId() string {
return fmt.Sprintf("%s/%s", plan.Owner, plan.Name)
}

func GetPlanCount(owner, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&Plan{})
Expand Down Expand Up @@ -114,38 +120,3 @@ func DeletePlan(plan *Plan) (bool, error) {
}
return affected != 0, nil
}

func (plan *Plan) GetId() string {
return fmt.Sprintf("%s/%s", plan.Owner, plan.Name)
}

func Subscribe(owner string, user string, plan string, pricing string) (*Subscription, error) {
selectedPricing, err := GetPricing(fmt.Sprintf("%s/%s", owner, pricing))
if err != nil {
return nil, err
}

valid := selectedPricing != nil && selectedPricing.IsEnabled

if !valid {
return nil, nil
}

planBelongToPricing, err := selectedPricing.HasPlan(owner, plan)
if err != nil {
return nil, err
}

if planBelongToPricing {
newSubscription := NewSubscription(owner, user, plan, selectedPricing.TrialDuration)
affected, err := AddSubscription(newSubscription)
if err != nil {
return nil, err
}

if affected {
return newSubscription, nil
}
}
return nil, nil
}
Loading

0 comments on commit 05b2f00

Please sign in to comment.