Skip to content

Commit

Permalink
CNS-315: support subscription renewal
Browse files Browse the repository at this point in the history
Subscription renewal happens when buying a subscription for a consumer with an
existing subscription, with the same plan (index and version). The existing
subscription is extended by the new duration requested. The total duration is
limited to 1 year, but also 1 additional month to allow renewal with yearly
discount before an existing subscription ends.
  • Loading branch information
orenl-lava committed Mar 14, 2023
1 parent 48a1f9e commit c96d2d0
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 26 deletions.
89 changes: 63 additions & 26 deletions x/subscription/keeper/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,7 @@ func (k Keeper) CreateSubscription(
logger := k.Logger(ctx)
block := uint64(ctx.BlockHeight())

_, err = sdk.AccAddressFromBech32(consumer)
if err != nil {
if _, err = sdk.AccAddressFromBech32(consumer); err != nil {
details := map[string]string{
"consumer": consumer,
"error": err.Error(),
Expand All @@ -124,45 +123,83 @@ func (k Keeper) CreateSubscription(
return utils.LavaError(ctx, logger, "CreateSubscription", details, "invalid creator")
}

// only one subscription per consumer
if _, found := k.GetSubscription(ctx, consumer); found {
details := map[string]string{"consumer": consumer}
return utils.LavaError(ctx, logger, "CreateSubscription", details, "consumer has existing subscription")
}

plan, found := k.plansKeeper.GetPlan(ctx, planIndex)
if !found {
details := map[string]string{
"plan": planIndex,
"block": strconv.FormatInt(ctx.BlockHeight(), 10),
"block": strconv.FormatInt(int64(block), 10),
}
return utils.LavaError(ctx, logger, "CreateSubscription", details, "invalid plan")
}

err = k.projectsKeeper.CreateDefaultProject(ctx, consumer)
if err != nil {
details := map[string]string{
"err": err.Error(),
sub, found := k.GetSubscription(ctx, consumer)

// Subscription creation:
// When: if not already exists for consumer address)
// What: find plan, create default project, set duration, calculate price,
// charge fees, save subscription.
//
// Subscription renewal:
// When: if already exists and existing plan is the same as current plans
// ("same" means same index and same block of creation)
// What: find plan, update duration (total and remaining), calculate price,
// charge fees, save subscription.
//
// Subscription upgrade: (TBD)
//
// Subscription downgrade: (TBD)

if !found {
// creeate new subscription with this plan
sub = types.Subscription{
Creator: creator,
Consumer: consumer,
Block: block,
PlanIndex: planIndex,
PlanBlock: plan.Block,
}
return utils.LavaError(ctx, logger, "CreateSubscription", details, "failed to create default project")
}

sub := types.Subscription{
Creator: creator,
Consumer: consumer,
Block: block,
PlanIndex: planIndex,
PlanBlock: plan.Block,
DurationTotal: duration,
MonthCuTotal: plan.GetComputeUnits(),
sub.MonthCuTotal = plan.GetComputeUnits()
sub.MonthCuLeft = plan.GetComputeUnits()

// new subscription needs a default project
if err = k.projectsKeeper.CreateDefaultProject(ctx, consumer); err != nil {
details := map[string]string{
"err": err.Error(),
}
return utils.LavaError(ctx, logger, "CreateSubscription", details, "failed to create default project")
}
} else {
// allow renewal with the same plan ("same" means both plan index,block match);
// otherwise, only one subscription per consumer
if !(plan.Index == sub.PlanIndex && plan.Block == sub.PlanBlock) {
details := map[string]string{"consumer": consumer}
return utils.LavaError(ctx, logger, "CreateSubscription", details, "consumer has existing subscription")
}

// For now, allow renewal only by the same creator.
// TODO: after adding fixation, we can allow different creators
if creator != sub.Creator {
details := map[string]string{"creator": consumer}
return utils.LavaError(ctx, logger, "CreateSubscription", details, "existing subscription has different creator")
}

// The total duration may not exceed MAX_SUBSCRIPTION_DURATION, but allow an
// extra month to account for renwewals before the end of current subscription
if sub.DurationLeft+duration > types.MAX_SUBSCRIPTION_DURATION+1 {
details := map[string]string{"duration": strconv.FormatInt(int64(sub.DurationLeft), 10)}
msg := "duration would exceed limit (" + strconv.FormatInt(types.MAX_SUBSCRIPTION_DURATION, 10) + " months)"
return utils.LavaError(ctx, logger, "CreateSubscription", details, msg)
}
}

// update total (last requested) duration and remaining duration
sub.DurationTotal = duration
sub.DurationLeft += duration

// use current block's timestamp for subscription start-time
expiry := nextMonth(ctx.BlockTime().UTC())

sub.MonthExpiryTime = uint64(expiry.Unix())
sub.MonthCuLeft = plan.GetComputeUnits()
sub.DurationLeft = duration

if err := sub.ValidateSubscription(); err != nil {
return utils.LavaError(ctx, logger, "CreateSub", nil, err.Error())
Expand Down
34 changes: 34 additions & 0 deletions x/subscription/keeper/subscription_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,40 @@ func TestCreateSubscription(t *testing.T) {
}
}

func TestRenewSubscription(t *testing.T) {
ts := setupTestStruct(t, 1)
keeper := ts.keepers.Subscription

account := common.CreateNewAccount(ts._ctx, *ts.keepers, 10000)
creator := account.Addr.String()

err := keeper.CreateSubscription(ts.ctx, creator, creator, ts.plans[0].Index, 6)
require.Nil(t, err)

sub, found := keeper.GetSubscription(ts.ctx, creator)
require.True(t, found)

// fast-forward two months
sub = ts.expireSubscription(sub)
sub = ts.expireSubscription(sub)
sub = ts.expireSubscription(sub)
require.Equal(t, uint64(3), sub.DurationLeft)

// with 3 months duration left, asking for 12 more should fail
err = keeper.CreateSubscription(ts.ctx, creator, creator, ts.plans[0].Index, 12)
require.NotNil(t, err)

// but asking for additional 10 is fine
err = keeper.CreateSubscription(ts.ctx, creator, creator, ts.plans[0].Index, 10)
require.Nil(t, err)

sub, found = keeper.GetSubscription(ts.ctx, creator)
require.True(t, found)

require.Equal(t, uint64(13), sub.DurationLeft)
require.Equal(t, uint64(10), sub.DurationTotal)
}

func TestSubscriptionDefaultProject(t *testing.T) {
ts := setupTestStruct(t, 1)
keeper := ts.keepers.Subscription
Expand Down

0 comments on commit c96d2d0

Please sign in to comment.