209 lines
6.3 KiB
Go
209 lines
6.3 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stripe/stripe-go/v82"
|
|
"github.com/stripe/stripe-go/v82/account"
|
|
"github.com/stripe/stripe-go/v82/accountlink"
|
|
"github.com/stripe/stripe-go/v82/balance"
|
|
"github.com/stripe/stripe-go/v82/transfer"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
|
|
"veza-backend-api/internal/models"
|
|
)
|
|
|
|
var (
|
|
ErrStripeConnectDisabled = errors.New("Stripe Connect is not enabled")
|
|
ErrNoStripeAccount = errors.New("seller has no Stripe Connect account")
|
|
)
|
|
|
|
// BalanceResponse is the balance for a connected account
|
|
type BalanceResponse struct {
|
|
Connected bool `json:"connected"` // true if seller has completed Stripe Connect onboarding
|
|
Available int64 `json:"available"` // in minor units (cents)
|
|
Pending int64 `json:"pending"` // in minor units (cents)
|
|
}
|
|
|
|
// StripeConnectService handles Stripe Connect operations for seller payouts
|
|
type StripeConnectService struct {
|
|
db *gorm.DB
|
|
secretKey string
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewStripeConnectService creates a new Stripe Connect service
|
|
func NewStripeConnectService(db *gorm.DB, secretKey string, logger *zap.Logger) *StripeConnectService {
|
|
return &StripeConnectService{
|
|
db: db,
|
|
secretKey: secretKey,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// CreateOnboardingLink creates or retrieves a Stripe Express account and returns an onboarding URL
|
|
func (s *StripeConnectService) CreateOnboardingLink(ctx context.Context, userID uuid.UUID, returnURL, refreshURL string) (string, error) {
|
|
if s.secretKey == "" {
|
|
return "", ErrStripeConnectDisabled
|
|
}
|
|
stripe.Key = s.secretKey
|
|
|
|
// Get user email
|
|
var user models.User
|
|
if err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return "", fmt.Errorf("user not found: %w", err)
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
// Check if account already exists
|
|
var existing models.SellerStripeAccount
|
|
if err := s.db.WithContext(ctx).Where("user_id = ?", userID).First(&existing).Error; err == nil {
|
|
// Account exists, create new account link for onboarding/update
|
|
params := &stripe.AccountLinkParams{
|
|
Account: stripe.String(existing.StripeAccountID),
|
|
ReturnURL: stripe.String(returnURL),
|
|
RefreshURL: stripe.String(refreshURL),
|
|
Type: stripe.String(string(stripe.AccountLinkTypeAccountOnboarding)),
|
|
}
|
|
link, err := accountlink.New(params)
|
|
if err != nil {
|
|
return "", fmt.Errorf("create account link: %w", err)
|
|
}
|
|
return link.URL, nil
|
|
}
|
|
|
|
// Create new Express account
|
|
accountParams := &stripe.AccountParams{
|
|
Type: stripe.String(string(stripe.AccountTypeExpress)),
|
|
Email: stripe.String(user.Email),
|
|
}
|
|
acct, err := account.New(accountParams)
|
|
if err != nil {
|
|
return "", fmt.Errorf("create stripe account: %w", err)
|
|
}
|
|
|
|
// Save to DB
|
|
sa := models.SellerStripeAccount{
|
|
UserID: userID,
|
|
StripeAccountID: acct.ID,
|
|
OnboardingCompleted: false,
|
|
}
|
|
if err := s.db.WithContext(ctx).Create(&sa).Error; err != nil {
|
|
return "", fmt.Errorf("save seller stripe account: %w", err)
|
|
}
|
|
|
|
// Create account link
|
|
linkParams := &stripe.AccountLinkParams{
|
|
Account: stripe.String(acct.ID),
|
|
ReturnURL: stripe.String(returnURL),
|
|
RefreshURL: stripe.String(refreshURL),
|
|
Type: stripe.String(string(stripe.AccountLinkTypeAccountOnboarding)),
|
|
}
|
|
link, err := accountlink.New(linkParams)
|
|
if err != nil {
|
|
return "", fmt.Errorf("create account link: %w", err)
|
|
}
|
|
return link.URL, nil
|
|
}
|
|
|
|
// HandleOnboardingCallback updates the account status after Stripe redirect
|
|
func (s *StripeConnectService) HandleOnboardingCallback(ctx context.Context, userID uuid.UUID) error {
|
|
var sa models.SellerStripeAccount
|
|
if err := s.db.WithContext(ctx).Where("user_id = ?", userID).First(&sa).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return ErrNoStripeAccount
|
|
}
|
|
return err
|
|
}
|
|
|
|
if s.secretKey == "" {
|
|
return ErrStripeConnectDisabled
|
|
}
|
|
stripe.Key = s.secretKey
|
|
|
|
// Retrieve account from Stripe to get charges_enabled, payouts_enabled
|
|
params := &stripe.AccountParams{}
|
|
acct, err := account.GetByID(sa.StripeAccountID, params)
|
|
if err != nil {
|
|
return fmt.Errorf("get stripe account: %w", err)
|
|
}
|
|
|
|
sa.ChargesEnabled = acct.ChargesEnabled
|
|
sa.PayoutsEnabled = acct.PayoutsEnabled
|
|
sa.OnboardingCompleted = acct.DetailsSubmitted
|
|
if err := s.db.WithContext(ctx).Save(&sa).Error; err != nil {
|
|
return fmt.Errorf("update seller stripe account: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetBalance returns the balance for a seller's connected account
|
|
func (s *StripeConnectService) GetBalance(ctx context.Context, userID uuid.UUID) (*BalanceResponse, error) {
|
|
var sa models.SellerStripeAccount
|
|
if err := s.db.WithContext(ctx).Where("user_id = ?", userID).First(&sa).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return &BalanceResponse{Connected: false, Available: 0, Pending: 0}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
if s.secretKey == "" {
|
|
return nil, ErrStripeConnectDisabled
|
|
}
|
|
stripe.Key = s.secretKey
|
|
|
|
params := &stripe.BalanceParams{}
|
|
params.SetStripeAccount(sa.StripeAccountID)
|
|
bal, err := balance.Get(params)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get stripe balance: %w", err)
|
|
}
|
|
|
|
var available, pending int64
|
|
for _, a := range bal.Available {
|
|
available += a.Amount
|
|
}
|
|
for _, p := range bal.Pending {
|
|
pending += p.Amount
|
|
}
|
|
return &BalanceResponse{Connected: true, Available: available, Pending: pending}, nil
|
|
}
|
|
|
|
// CreateTransfer transfers funds to a connected account (for payout on sale)
|
|
func (s *StripeConnectService) CreateTransfer(ctx context.Context, sellerUserID uuid.UUID, amount int64, currency, orderID string) error {
|
|
var sa models.SellerStripeAccount
|
|
if err := s.db.WithContext(ctx).Where("user_id = ?", sellerUserID).First(&sa).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return ErrNoStripeAccount
|
|
}
|
|
return err
|
|
}
|
|
if !sa.PayoutsEnabled {
|
|
return fmt.Errorf("seller account does not have payouts enabled")
|
|
}
|
|
|
|
if s.secretKey == "" {
|
|
return ErrStripeConnectDisabled
|
|
}
|
|
stripe.Key = s.secretKey
|
|
|
|
params := &stripe.TransferParams{
|
|
Amount: stripe.Int64(amount),
|
|
Currency: stripe.String(currency),
|
|
Destination: stripe.String(sa.StripeAccountID),
|
|
}
|
|
if orderID != "" {
|
|
params.AddMetadata("order_id", orderID)
|
|
}
|
|
_, err := transfer.New(params)
|
|
if err != nil {
|
|
return fmt.Errorf("create stripe transfer: %w", err)
|
|
}
|
|
return nil
|
|
}
|