refactor: abstract billing operations behind a new domain.BillingProvider interface and update services to use it.

This commit is contained in:
Mohammed Alquraini 2025-12-19 21:17:14 +04:00
parent 4838c6d37c
commit 4f422db1a0
7 changed files with 30 additions and 26 deletions

View file

@ -82,7 +82,7 @@ func (s *billingService) ingestMeterEventToPolar(ctx context.Context, organizati
// Filter: name equals "invoice.processed" // Filter: name equals "invoice.processed"
// Amount: 1 (one invoice processed) // Amount: 1 (one invoice processed)
meterSlug := invoicesProcessedMeterSlug // Event name MUST match meter filter exactly (with dot) meterSlug := invoicesProcessedMeterSlug // Event name MUST match meter filter exactly (with dot)
if err := s.polarAdapter.IngestMeterEvent(ctx, externalID, meterSlug, 1); err != nil { if err := s.billingProvider.IngestMeterEvent(ctx, externalID, meterSlug, 1); err != nil {
s.logger.Error("Failed to ingest meter event to Polar", map[string]any{ s.logger.Error("Failed to ingest meter event to Polar", map[string]any{
"organization_id": organizationID, "organization_id": organizationID,
"external_id": externalID, "external_id": externalID,

View file

@ -34,8 +34,8 @@ func (m *Module) Configure(container *dig.Container) error {
return err return err
} }
// Register PolarAdapter // Register BillingProvider (Polar implementation)
if err := container.Provide(func(client *polarpkg.Client) PolarAdapter { if err := container.Provide(func(client *polarpkg.Client) domain.BillingProvider {
return polar.NewPolarAdapter(client) return polar.NewPolarAdapter(client)
}); err != nil { }); err != nil {
return err return err
@ -45,10 +45,10 @@ func (m *Module) Configure(container *dig.Container) error {
if err := container.Provide(func( if err := container.Provide(func(
repo domain.SubscriptionRepository, repo domain.SubscriptionRepository,
orgAdapter domain.OrganizationAdapter, orgAdapter domain.OrganizationAdapter,
polarAdapter PolarAdapter, billingProvider domain.BillingProvider,
logger logger.Logger, logger logger.Logger,
) BillingService { ) BillingService {
return NewBillingService(repo, orgAdapter, polarAdapter, logger) return NewBillingService(repo, orgAdapter, billingProvider, logger)
}); err != nil { }); err != nil {
return err return err
} }

View file

@ -71,30 +71,22 @@ type BillingService interface {
} }
type billingService struct { type billingService struct {
repo domain.SubscriptionRepository repo domain.SubscriptionRepository
orgAdapter domain.OrganizationAdapter orgAdapter domain.OrganizationAdapter
polarAdapter PolarAdapter billingProvider domain.BillingProvider
logger logger.Logger logger logger.Logger
} }
func NewBillingService( func NewBillingService(
repo domain.SubscriptionRepository, repo domain.SubscriptionRepository,
orgAdapter domain.OrganizationAdapter, orgAdapter domain.OrganizationAdapter,
polarAdapter PolarAdapter, billingProvider domain.BillingProvider,
logger logger.Logger, logger logger.Logger,
) BillingService { ) BillingService {
return &billingService{ return &billingService{
repo: repo, repo: repo,
orgAdapter: orgAdapter, orgAdapter: orgAdapter,
polarAdapter: polarAdapter, billingProvider: billingProvider,
logger: logger, logger: logger,
} }
} }
// PolarAdapter defines the interface for Polar API operations
type PolarAdapter interface {
GetSubscription(ctx context.Context, externalCustomerID string) (*domain.Subscription, error)
GetCheckoutSession(ctx context.Context, sessionID string) (*domain.CheckoutSessionResponse, error)
GetCheckoutSessionWithPolling(ctx context.Context, sessionID string) (*domain.CheckoutSessionResponse, error)
IngestMeterEvent(ctx context.Context, externalCustomerID string, meterSlug string, amount int32) error
}

View file

@ -17,7 +17,7 @@ func (s *billingService) SyncSubscriptionFromPolar(ctx context.Context, organiza
} }
// Fetch subscription from Polar // Fetch subscription from Polar
subscription, err := s.polarAdapter.GetSubscription(ctx, externalID) subscription, err := s.billingProvider.GetSubscription(ctx, externalID)
if err != nil { if err != nil {
return fmt.Errorf("failed to fetch subscription from Polar: %w", err) return fmt.Errorf("failed to fetch subscription from Polar: %w", err)
} }

View file

@ -11,7 +11,7 @@ import (
func (s *billingService) VerifyPaymentFromCheckout(ctx context.Context, sessionID string) (*domain.BillingStatus, error) { func (s *billingService) VerifyPaymentFromCheckout(ctx context.Context, sessionID string) (*domain.BillingStatus, error) {
// Step 1: Get checkout session from Polar with polling // Step 1: Get checkout session from Polar with polling
checkoutSession, err := s.polarAdapter.GetCheckoutSessionWithPolling(ctx, sessionID) checkoutSession, err := s.billingProvider.GetCheckoutSessionWithPolling(ctx, sessionID)
if err != nil { if err != nil {
fmt.Printf("❌ [VerifyPayment] Failed to verify checkout session %s: %v\n", sessionID, err) fmt.Printf("❌ [VerifyPayment] Failed to verify checkout session %s: %v\n", sessionID, err)
return nil, fmt.Errorf("failed to get checkout session: %w", err) return nil, fmt.Errorf("failed to get checkout session: %w", err)
@ -42,7 +42,7 @@ func (s *billingService) VerifyPaymentFromCheckout(ctx context.Context, sessionI
} }
// Step 5: Fetch full subscription details from Polar // Step 5: Fetch full subscription details from Polar
subscription, err := s.polarAdapter.GetSubscription(ctx, externalCustomerID) subscription, err := s.billingProvider.GetSubscription(ctx, externalCustomerID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch subscription from Polar: %w", err) return nil, fmt.Errorf("failed to fetch subscription from Polar: %w", err)
} }

View file

@ -23,3 +23,12 @@ type OrganizationAdapter interface {
GetStytchOrgID(ctx context.Context, organizationID int32) (string, error) GetStytchOrgID(ctx context.Context, organizationID int32) (string, error)
GetOrganizationIDByStytchOrgID(ctx context.Context, stytchOrgID string) (int32, error) GetOrganizationIDByStytchOrgID(ctx context.Context, stytchOrgID string) (int32, error)
} }
// BillingProvider defines operations for external billing providers
// This interface abstracts the billing provider (e.g., Polar.sh) from the app layer
type BillingProvider interface {
GetSubscription(ctx context.Context, externalCustomerID string) (*Subscription, error)
GetCheckoutSession(ctx context.Context, sessionID string) (*CheckoutSessionResponse, error)
GetCheckoutSessionWithPolling(ctx context.Context, sessionID string) (*CheckoutSessionResponse, error)
IngestMeterEvent(ctx context.Context, externalCustomerID string, meterSlug string, amount int32) error
}

View file

@ -13,11 +13,14 @@ import (
polarpkg "github.com/moasq/go-b2b-starter/pkg/polar" polarpkg "github.com/moasq/go-b2b-starter/pkg/polar"
) )
// Ensure polarAdapter implements domain.BillingProvider at compile time
var _ domain.BillingProvider = (*polarAdapter)(nil)
type polarAdapter struct { type polarAdapter struct {
client *polarpkg.Client client *polarpkg.Client
} }
func NewPolarAdapter(client *polarpkg.Client) *polarAdapter { func NewPolarAdapter(client *polarpkg.Client) domain.BillingProvider {
return &polarAdapter{ return &polarAdapter{
client: client, client: client,
} }