2
0
Files
logikonline d1f20f6b46
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
feat(ci): add repository subscription monetization system
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
2026-01-31 13:37:07 -05:00

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
}