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"
// Amount: 1 (one invoice processed)
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{
"organization_id": organizationID,
"external_id": externalID,

View file

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

View file

@ -73,28 +73,20 @@ type BillingService interface {
type billingService struct {
repo domain.SubscriptionRepository
orgAdapter domain.OrganizationAdapter
polarAdapter PolarAdapter
billingProvider domain.BillingProvider
logger logger.Logger
}
func NewBillingService(
repo domain.SubscriptionRepository,
orgAdapter domain.OrganizationAdapter,
polarAdapter PolarAdapter,
billingProvider domain.BillingProvider,
logger logger.Logger,
) BillingService {
return &billingService{
repo: repo,
orgAdapter: orgAdapter,
polarAdapter: polarAdapter,
billingProvider: billingProvider,
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
subscription, err := s.polarAdapter.GetSubscription(ctx, externalID)
subscription, err := s.billingProvider.GetSubscription(ctx, externalID)
if err != nil {
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) {
// 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 {
fmt.Printf("❌ [VerifyPayment] Failed to verify checkout session %s: %v\n", sessionID, 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
subscription, err := s.polarAdapter.GetSubscription(ctx, externalCustomerID)
subscription, err := s.billingProvider.GetSubscription(ctx, externalCustomerID)
if err != nil {
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)
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"
)
// Ensure polarAdapter implements domain.BillingProvider at compile time
var _ domain.BillingProvider = (*polarAdapter)(nil)
type polarAdapter struct {
client *polarpkg.Client
}
func NewPolarAdapter(client *polarpkg.Client) *polarAdapter {
func NewPolarAdapter(client *polarpkg.Client) domain.BillingProvider {
return &polarAdapter{
client: client,
}