Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: p/subscription #2116

Merged
merged 17 commits into from
Aug 22, 2024
6 changes: 6 additions & 0 deletions examples/gno.land/p/demo/subscription/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module gno.land/p/demo/subscription

require (
gno.land/p/demo/avl v0.0.0-latest
gno.land/p/demo/ufmt v0.0.0-latest
)
123 changes: 123 additions & 0 deletions examples/gno.land/p/demo/subscription/subscription.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Package subscription provides a library for managing different types of
// subscriptions in Gno applications. It supports both recurring and
// lifetime subscriptions, enabling users to access services based on their
// subscription status. This package uses a tree-based data structure to
// efficiently track and manage subscription statuses.

// Example Usage:
//
// import "gno.land/p/demo/subscription"
//
// Create a recurring subscription for 30 days costing 100 units.
// recurringSub := subscription.NewRecurringSubscription(time.Hour*24*30, 100)
//
// Create a lifetime subscription for a one-time payment of 500 units.
// lifetimeSub := subscription.NewLifetimeSubscription(500)
//
// func HandleRequest(caller std.Address) {
// Check access for a recurring subscription.
// recurringSub.CheckAccess(caller)
//
// Perform payment for a lifetime subscription.
// lifetimeSub.ProcessPayment(caller, 500)
// }

package subscription // import "gno.land/p/demo/subscription"

import (
"gno.land/p/demo/ufmt"
"gno.land/p/demo/avl"
"time"
"std"
)

// Subscription interface defines standard methods that all subscription types must implement.
type Subscription interface {
CheckAccess(std.Address)
ProcessPayment(std.Address, int64)
}

// RecurringSubscription represents a subscription that requires periodic payments.
// It includes the duration of the subscription and the amount required per period.
type RecurringSubscription struct {
duration time.Duration
amount int64
subs *avl.Tree // std.Address -> time.Time
}

// LifetimeSubscription represents a subscription that requires only a one-time payment.
// It grants permanent access to a service or product.
type LifetimeSubscription struct {
amount int64
subs *avl.Tree // std.Address -> bool
}

// NewRecurringSubscription creates and returns a new recurring subscription.
func NewRecurringSubscription(duration time.Duration, amount int64) *RecurringSubscription {
return &RecurringSubscription{
duration: duration,
amount: amount,
subs: avl.NewTree(),
}
}

// CheckAccess verifies if the caller has an active recurring subscription.
func (rs *RecurringSubscription) CheckAccess(caller std.Address) {
send := std.PrevRealm().Addr()
sendAmount := send.AmountOf("ugnot")

if sendAmount < rs.amount {
panic(ufmt.Sprintf("you need to send at least %d units to access this feature", rs.amount))
}

expTime, exists := rs.subs.Get(caller.String())
if !exists || time.Now().After(expTime.(time.Time)) {
panic("your subscription has expired or does not exist")
}
}

// ProcessPayment processes the payment for a recurring subscription and extends its validity.
func (rs *RecurringSubscription) ProcessPayment(caller std.Address, paymentAmount int64) {
if paymentAmount < rs.amount {
panic("insufficient payment")
}

expiration := time.Now().Add(rs.duration)
rs.subs.Set(caller.String(), expiration)
}

// GetExpiration returns the expiration date of the recurring subscription for a given caller.
func (rs *RecurringSubscription) GetExpiration(caller std.Address) time.Time {
expTime, exists := rs.subs.Get(caller.String())
if !exists {
panic("no subscription found")
}

return expTime.(time.Time)
}

// NewLifetimeSubscription creates and returns a new lifetime subscription.
func NewLifetimeSubscription(amount int64) *LifetimeSubscription {
return &LifetimeSubscription{
amount: amount,
subs: avl.NewTree(),
}
}

// ProcessPayment processes the payment for a lifetime subscription.
func (ls *LifetimeSubscription) ProcessPayment(caller std.Address, paymentAmount int64) {
if paymentAmount < ls.amount {
panic(ufmt.Sprintf("insufficient payment, required %d units", ls.amount))
}

ls.subs.Set(caller.String(), true)
}

// CheckAccess verifies if the caller has a lifetime subscription.
func (ls *LifetimeSubscription) CheckAccess(caller std.Address) {
_, exists := ls.subs.Get(caller.String())

if !exists {
panic("you do not have a lifetime subscription")
}
}
126 changes: 126 additions & 0 deletions examples/gno.land/p/demo/subscription/subscription_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package subscription

import (
"std"
"testing"
"time"

"gno.land/p/demo/avl"
)

// Test the initialization of a recurring subscription.
func TestNewRecurringSubscription(t *testing.T) {
duration := time.Hour * 24 * 30 // 30 days
amount := int64(100)
rs := NewRecurringSubscription(duration, amount)
if rs.duration != duration {
t.Errorf("Expected duration %v, got %v", duration, rs.duration)
}
if rs.amount != amount {
t.Errorf("Expected amount %d, got %d", amount, rs.amount)
}
}

// Test access check for an active subscription with better error handling.
func TestRecurringSubscriptionCheckAccessActive(t *testing.T) {
rs := NewRecurringSubscription(time.Hour*24*30, 100)
caller := std.Address("test-address")
// Assuming the subscription expects a previous payment, simulate it:
rs.subs.Set(caller.String(), time.Now().Add(time.Hour*24*31)) // Set expiration in the future

defer func() {
if r := recover(); r != nil {
t.Errorf("Access check failed unexpectedly: %v", r)
}
}()
// Simulate sending the correct amount to access the feature
std.TestSetOrigSend(std.Coins{{"ugnot", 100}}, nil)
rs.CheckAccess(caller)
}


// Test access check for an expired subscription.
func TestRecurringSubscriptionCheckAccessExpired(t *testing.T) {
rs := NewRecurringSubscription(time.Hour*24*30, 100)
caller := std.Address("test-address")
rs.subs.Set(caller.String(), time.Now().Add(-time.Hour)) // Set expiration in the past
defer func() {
if r := recover(); r == nil {
t.Errorf("Expected access check to fail for expired subscription")
}
}()
rs.CheckAccess(caller)
}

// Test processing payments correctly extends the subscription.
func TestRecurringSubscriptionProcessPayment(t *testing.T) {
rs := NewRecurringSubscription(time.Hour*24*30, 100)
caller := std.Address("test-address")
// Initial payment processing
rs.ProcessPayment(caller, 100)
expiration := rs.GetExpiration(caller)
if time.Now().After(expiration) {
t.Errorf("Payment did not extend subscription as expected")
}
}


// Test the initialization of a lifetime subscription.
func TestNewLifetimeSubscription(t *testing.T) {
amount := int64(500)
ls := NewLifetimeSubscription(amount)
if ls.amount != amount {
t.Errorf("Expected amount %d, got %d", amount, ls.amount)
}
}

// Test processing payments for lifetime subscription.
func TestLifetimeSubscriptionProcessPayment(t *testing.T) {
ls := NewLifetimeSubscription(500)
caller := std.Address("test-address")

// Test insufficient payment.
defer func() {
if r := recover(); r == nil {
t.Errorf("Expected payment error for insufficient amount")
}
}()
ls.ProcessPayment(caller, 400)

// Test exact payment.
defer func() {
if r := recover(); r != nil {
t.Errorf("Payment failed unexpectedly: %v", r)
}
}()
ls.ProcessPayment(caller, 500)

_, exists := ls.subs.Get(caller.String())
if !exists {
t.Errorf("Lifetime subscription was not recorded")
}
}

// Test access check for lifetime subscription.
func TestLifetimeSubscriptionCheckAccess(t *testing.T) {
ls := NewLifetimeSubscription(500)
caller := std.Address("test-address")

// Simulate payment and check access.
ls.subs.Set(caller.String(), true)
defer func() {
if r := recover(); r != nil {
t.Errorf("Access denied unexpectedly")
}
}()
ls.CheckAccess(caller)

// Check access without payment.
ls.subs = avl.NewTree() // Reset the tree to simulate no payment
defer func() {
if r := recover(); r == nil {
t.Errorf("Expected access to be denied for unpaid subscription")
}
}()
ls.CheckAccess(caller)
}
Loading