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
141 lines
4.4 KiB
Go
141 lines
4.4 KiB
Go
// Copyright 2026 MarketAlly. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package monetize
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"maps"
|
|
|
|
stripe "github.com/stripe/stripe-go/v82"
|
|
"github.com/stripe/stripe-go/v82/customer"
|
|
"github.com/stripe/stripe-go/v82/paymentintent"
|
|
"github.com/stripe/stripe-go/v82/subscription"
|
|
"github.com/stripe/stripe-go/v82/webhook"
|
|
)
|
|
|
|
// StripeProvider implements PaymentProvider using Stripe.
|
|
type StripeProvider struct {
|
|
secretKey string
|
|
webhookSecret string
|
|
}
|
|
|
|
// NewStripeProvider creates a StripeProvider and sets the global API key.
|
|
func NewStripeProvider(secretKey, webhookSecret string) *StripeProvider {
|
|
stripe.Key = secretKey
|
|
return &StripeProvider{
|
|
secretKey: secretKey,
|
|
webhookSecret: webhookSecret,
|
|
}
|
|
}
|
|
|
|
func (s *StripeProvider) Type() ProviderType { return ProviderStripe }
|
|
|
|
// findOrCreateCustomer looks up or creates a Stripe customer by email.
|
|
func (s *StripeProvider) findOrCreateCustomer(email string, metadata map[string]string) (*stripe.Customer, error) {
|
|
// Try to find existing customer by email
|
|
params := &stripe.CustomerListParams{}
|
|
params.Filters.AddFilter("email", "", email)
|
|
params.Filters.AddFilter("limit", "", "1")
|
|
iter := customer.List(params)
|
|
for iter.Next() {
|
|
return iter.Customer(), nil
|
|
}
|
|
|
|
// Create new customer
|
|
createParams := &stripe.CustomerParams{
|
|
Email: stripe.String(email),
|
|
}
|
|
if metadata != nil {
|
|
createParams.Metadata = make(map[string]string)
|
|
maps.Copy(createParams.Metadata, metadata)
|
|
}
|
|
return customer.New(createParams)
|
|
}
|
|
|
|
// CreateSubscription creates a Stripe subscription with payment_behavior=default_incomplete
|
|
// so the frontend can confirm with Stripe Elements.
|
|
func (s *StripeProvider) CreateSubscription(ctx context.Context, customerEmail, stripePriceID string, metadata map[string]string) (*SubscriptionResult, error) {
|
|
cust, err := s.findOrCreateCustomer(customerEmail, metadata)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stripe: create customer: %w", err)
|
|
}
|
|
|
|
params := &stripe.SubscriptionParams{
|
|
Customer: stripe.String(cust.ID),
|
|
Items: []*stripe.SubscriptionItemsParams{
|
|
{Price: stripe.String(stripePriceID)},
|
|
},
|
|
PaymentBehavior: stripe.String("default_incomplete"),
|
|
}
|
|
params.AddExpand("latest_invoice.confirmation_secret")
|
|
if metadata != nil {
|
|
params.Metadata = make(map[string]string)
|
|
maps.Copy(params.Metadata, metadata)
|
|
}
|
|
|
|
sub, err := subscription.New(params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stripe: create subscription: %w", err)
|
|
}
|
|
|
|
var clientSecret string
|
|
if sub.LatestInvoice != nil && sub.LatestInvoice.ConfirmationSecret != nil {
|
|
clientSecret = sub.LatestInvoice.ConfirmationSecret.ClientSecret
|
|
}
|
|
|
|
return &SubscriptionResult{
|
|
ProviderSubscriptionID: sub.ID,
|
|
ClientSecret: clientSecret,
|
|
}, nil
|
|
}
|
|
|
|
// CreateOneTimePayment creates a Stripe PaymentIntent for lifetime purchases.
|
|
func (s *StripeProvider) CreateOneTimePayment(ctx context.Context, customerEmail string, amountCents int64, currency string, metadata map[string]string) (*OneTimePaymentResult, error) {
|
|
cust, err := s.findOrCreateCustomer(customerEmail, metadata)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stripe: create customer: %w", err)
|
|
}
|
|
|
|
params := &stripe.PaymentIntentParams{
|
|
Amount: stripe.Int64(amountCents),
|
|
Currency: stripe.String(currency),
|
|
Customer: stripe.String(cust.ID),
|
|
}
|
|
if metadata != nil {
|
|
params.Metadata = make(map[string]string)
|
|
maps.Copy(params.Metadata, metadata)
|
|
}
|
|
|
|
pi, err := paymentintent.New(params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stripe: create payment intent: %w", err)
|
|
}
|
|
|
|
return &OneTimePaymentResult{
|
|
ProviderPaymentID: pi.ID,
|
|
ClientSecret: pi.ClientSecret,
|
|
}, nil
|
|
}
|
|
|
|
// CancelSubscription cancels a Stripe subscription immediately.
|
|
func (s *StripeProvider) CancelSubscription(ctx context.Context, providerSubscriptionID string) error {
|
|
_, err := subscription.Cancel(providerSubscriptionID, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("stripe: cancel subscription: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// VerifyWebhookSignature verifies a Stripe webhook signature and returns the payload.
|
|
func (s *StripeProvider) VerifyWebhookSignature(payload []byte, signature string) ([]byte, error) {
|
|
_, err := webhook.ConstructEventWithOptions(payload, signature, s.webhookSecret, webhook.ConstructEventOptions{
|
|
IgnoreAPIVersionMismatch: true,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stripe: invalid webhook signature: %w", err)
|
|
}
|
|
return payload, nil
|
|
}
|