All checks were successful
Build and Release / Create Release (push) Successful in 0s
Build and Release / Unit Tests (push) Successful in 3m10s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 5m13s
Build and Release / Lint (push) Successful in 5m25s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m13s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 8h5m42s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 7m30s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 7m55s
Build and Release / Build Binary (linux/arm64) (push) Successful in 7m36s
Implement complete subscription monetization system for repositories with Stripe and PayPal integration. Includes: - Database models and migrations for monetization settings, subscription products, and user subscriptions - Payment provider abstraction layer with Stripe and PayPal implementations - Admin UI for configuring payment providers and viewing subscriptions - Repository settings UI for managing subscription products and tiers - Subscription checkout flow and webhook handlers for payment events - Access control to gate repository code behind active subscriptions
210 lines
6.5 KiB
Go
210 lines
6.5 KiB
Go
// Copyright 2026 MarketAlly. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package monetize
|
|
|
|
import (
|
|
"context"
|
|
|
|
"code.gitcaddy.com/server/v3/models/db"
|
|
repo_model "code.gitcaddy.com/server/v3/models/repo"
|
|
user_model "code.gitcaddy.com/server/v3/models/user"
|
|
"code.gitcaddy.com/server/v3/modules/timeutil"
|
|
)
|
|
|
|
// SubscriptionStatus represents the lifecycle state of a subscription.
|
|
type SubscriptionStatus int
|
|
|
|
const (
|
|
SubscriptionStatusActive SubscriptionStatus = 0
|
|
SubscriptionStatusCancelled SubscriptionStatus = 1
|
|
SubscriptionStatusExpired SubscriptionStatus = 2
|
|
SubscriptionStatusPastDue SubscriptionStatus = 3
|
|
)
|
|
|
|
// String returns a human-readable label for the subscription status.
|
|
func (s SubscriptionStatus) String() string {
|
|
switch s {
|
|
case SubscriptionStatusActive:
|
|
return "active"
|
|
case SubscriptionStatusCancelled:
|
|
return "cancelled"
|
|
case SubscriptionStatusExpired:
|
|
return "expired"
|
|
case SubscriptionStatusPastDue:
|
|
return "past_due"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// RepoSubscription tracks a user's paid access to a repository.
|
|
type RepoSubscription struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
RepoID int64 `xorm:"INDEX NOT NULL"`
|
|
UserID int64 `xorm:"INDEX NOT NULL"`
|
|
ProductID int64 `xorm:"INDEX NOT NULL"`
|
|
Status SubscriptionStatus `xorm:"NOT NULL DEFAULT 0"`
|
|
PaymentProvider string `xorm:"VARCHAR(20) NOT NULL DEFAULT ''"`
|
|
StripeSubscriptionID string `xorm:"VARCHAR(255)"`
|
|
PayPalSubscriptionID string `xorm:"VARCHAR(255)"`
|
|
CurrentPeriodStart timeutil.TimeStamp `xorm:""`
|
|
CurrentPeriodEnd timeutil.TimeStamp `xorm:""`
|
|
IsLifetime bool `xorm:"NOT NULL DEFAULT false"`
|
|
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
|
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
|
|
|
User *user_model.User `xorm:"-"`
|
|
Repo *repo_model.Repository `xorm:"-"`
|
|
Product *RepoSubscriptionProduct `xorm:"-"`
|
|
}
|
|
|
|
func init() {
|
|
db.RegisterModel(new(RepoSubscription))
|
|
}
|
|
|
|
// HasActiveSubscription checks if a user has active (or lifetime) access to a repo.
|
|
func HasActiveSubscription(ctx context.Context, userID, repoID int64) (bool, error) {
|
|
return db.GetEngine(ctx).Where(
|
|
"user_id = ? AND repo_id = ? AND (status = ? OR is_lifetime = ?)",
|
|
userID, repoID, SubscriptionStatusActive, true,
|
|
).Exist(new(RepoSubscription))
|
|
}
|
|
|
|
// GetSubscriptionsByRepoID returns paginated subscriptions for a repo.
|
|
func GetSubscriptionsByRepoID(ctx context.Context, repoID int64, page, pageSize int) ([]*RepoSubscription, int64, error) {
|
|
count, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Count(new(RepoSubscription))
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
subs := make([]*RepoSubscription, 0, pageSize)
|
|
err = db.GetEngine(ctx).Where("repo_id = ?", repoID).
|
|
OrderBy("created_unix DESC").
|
|
Limit(pageSize, (page-1)*pageSize).
|
|
Find(&subs)
|
|
return subs, count, err
|
|
}
|
|
|
|
// GetSubscriptionsByUserID returns paginated subscriptions for a user.
|
|
func GetSubscriptionsByUserID(ctx context.Context, userID int64, page, pageSize int) ([]*RepoSubscription, int64, error) {
|
|
count, err := db.GetEngine(ctx).Where("user_id = ?", userID).Count(new(RepoSubscription))
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
subs := make([]*RepoSubscription, 0, pageSize)
|
|
err = db.GetEngine(ctx).Where("user_id = ?", userID).
|
|
OrderBy("created_unix DESC").
|
|
Limit(pageSize, (page-1)*pageSize).
|
|
Find(&subs)
|
|
return subs, count, err
|
|
}
|
|
|
|
// GetAllSubscriptions returns all subscriptions system-wide (for admin view).
|
|
func GetAllSubscriptions(ctx context.Context, page, pageSize int) ([]*RepoSubscription, int64, error) {
|
|
count, err := db.GetEngine(ctx).Count(new(RepoSubscription))
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
subs := make([]*RepoSubscription, 0, pageSize)
|
|
err = db.GetEngine(ctx).
|
|
OrderBy("created_unix DESC").
|
|
Limit(pageSize, (page-1)*pageSize).
|
|
Find(&subs)
|
|
return subs, count, err
|
|
}
|
|
|
|
// GetMonetizedRepos returns repos that have subscriptions enabled (for admin view).
|
|
func GetMonetizedRepos(ctx context.Context, page, pageSize int) ([]*repo_model.Repository, int64, error) {
|
|
count, err := db.GetEngine(ctx).Where("subscriptions_enabled = ?", true).Count(new(repo_model.Repository))
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
repos := make([]*repo_model.Repository, 0, pageSize)
|
|
err = db.GetEngine(ctx).Where("subscriptions_enabled = ?", true).
|
|
OrderBy("updated_unix DESC").
|
|
Limit(pageSize, (page-1)*pageSize).
|
|
Find(&repos)
|
|
return repos, count, err
|
|
}
|
|
|
|
// LoadUser loads the associated user for a subscription.
|
|
func (s *RepoSubscription) LoadUser(ctx context.Context) error {
|
|
if s.User != nil {
|
|
return nil
|
|
}
|
|
u, err := user_model.GetUserByID(ctx, s.UserID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.User = u
|
|
return nil
|
|
}
|
|
|
|
// LoadRepo loads the associated repository for a subscription.
|
|
func (s *RepoSubscription) LoadRepo(ctx context.Context) error {
|
|
if s.Repo != nil {
|
|
return nil
|
|
}
|
|
r, err := repo_model.GetRepositoryByID(ctx, s.RepoID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.Repo = r
|
|
return nil
|
|
}
|
|
|
|
// LoadProduct loads the associated product for a subscription.
|
|
func (s *RepoSubscription) LoadProduct(ctx context.Context) error {
|
|
if s.Product != nil {
|
|
return nil
|
|
}
|
|
p, err := GetProductByID(ctx, s.ProductID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.Product = p
|
|
return nil
|
|
}
|
|
|
|
// GetSubscriptionByStripeID finds a subscription by Stripe subscription ID.
|
|
func GetSubscriptionByStripeID(ctx context.Context, stripeSubID string) (*RepoSubscription, error) {
|
|
s := &RepoSubscription{}
|
|
has, err := db.GetEngine(ctx).Where("stripe_subscription_id = ?", stripeSubID).Get(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !has {
|
|
return nil, nil
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// GetSubscriptionByPayPalID finds a subscription by PayPal subscription ID.
|
|
func GetSubscriptionByPayPalID(ctx context.Context, paypalSubID string) (*RepoSubscription, error) {
|
|
s := &RepoSubscription{}
|
|
has, err := db.GetEngine(ctx).Where("paypal_subscription_id = ?", paypalSubID).Get(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !has {
|
|
return nil, nil
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// UpdateSubscriptionStatus changes the status of a subscription.
|
|
func UpdateSubscriptionStatus(ctx context.Context, id int64, status SubscriptionStatus) error {
|
|
_, err := db.GetEngine(ctx).ID(id).Cols("status").Update(&RepoSubscription{Status: status})
|
|
return err
|
|
}
|
|
|
|
// CreateSubscription inserts a new subscription record.
|
|
func CreateSubscription(ctx context.Context, s *RepoSubscription) error {
|
|
_, err := db.GetEngine(ctx).Insert(s)
|
|
return err
|
|
}
|