From 4f422db1a0df9090e9521f55d97933a30de384dd Mon Sep 17 00:00:00 2001 From: Mohammed Alquraini Date: Fri, 19 Dec 2025 21:17:14 +0400 Subject: [PATCH] refactor: abstract billing operations behind a new `domain.BillingProvider` interface and update services to use it. --- .../services/consume_invoice_quota_service.go | 2 +- .../src/app/billing/app/services/module.go | 8 +++--- .../app/services/subscription_service_dec.go | 26 +++++++------------ .../app/services/sync_subscription_service.go | 2 +- .../app/services/verify_payment_service.go | 4 +-- .../src/app/billing/domain/repository.go | 9 +++++++ .../app/billing/infra/polar/polar_adapter.go | 5 +++- 7 files changed, 30 insertions(+), 26 deletions(-) diff --git a/go-b2b-starter/src/app/billing/app/services/consume_invoice_quota_service.go b/go-b2b-starter/src/app/billing/app/services/consume_invoice_quota_service.go index 95aff0c..48c17e3 100644 --- a/go-b2b-starter/src/app/billing/app/services/consume_invoice_quota_service.go +++ b/go-b2b-starter/src/app/billing/app/services/consume_invoice_quota_service.go @@ -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, diff --git a/go-b2b-starter/src/app/billing/app/services/module.go b/go-b2b-starter/src/app/billing/app/services/module.go index 4777037..d32cf71 100644 --- a/go-b2b-starter/src/app/billing/app/services/module.go +++ b/go-b2b-starter/src/app/billing/app/services/module.go @@ -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 } diff --git a/go-b2b-starter/src/app/billing/app/services/subscription_service_dec.go b/go-b2b-starter/src/app/billing/app/services/subscription_service_dec.go index 95aa9ce..5ab0960 100644 --- a/go-b2b-starter/src/app/billing/app/services/subscription_service_dec.go +++ b/go-b2b-starter/src/app/billing/app/services/subscription_service_dec.go @@ -71,30 +71,22 @@ type BillingService interface { } type billingService struct { - repo domain.SubscriptionRepository - orgAdapter domain.OrganizationAdapter - polarAdapter PolarAdapter - logger logger.Logger + repo domain.SubscriptionRepository + orgAdapter domain.OrganizationAdapter + 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, - logger: logger, + repo: repo, + orgAdapter: orgAdapter, + 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 -} diff --git a/go-b2b-starter/src/app/billing/app/services/sync_subscription_service.go b/go-b2b-starter/src/app/billing/app/services/sync_subscription_service.go index 2dc2d00..33e533e 100644 --- a/go-b2b-starter/src/app/billing/app/services/sync_subscription_service.go +++ b/go-b2b-starter/src/app/billing/app/services/sync_subscription_service.go @@ -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) } diff --git a/go-b2b-starter/src/app/billing/app/services/verify_payment_service.go b/go-b2b-starter/src/app/billing/app/services/verify_payment_service.go index b852cd8..4200823 100644 --- a/go-b2b-starter/src/app/billing/app/services/verify_payment_service.go +++ b/go-b2b-starter/src/app/billing/app/services/verify_payment_service.go @@ -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) } diff --git a/go-b2b-starter/src/app/billing/domain/repository.go b/go-b2b-starter/src/app/billing/domain/repository.go index 1f57de9..9f98519 100644 --- a/go-b2b-starter/src/app/billing/domain/repository.go +++ b/go-b2b-starter/src/app/billing/domain/repository.go @@ -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 +} diff --git a/go-b2b-starter/src/app/billing/infra/polar/polar_adapter.go b/go-b2b-starter/src/app/billing/infra/polar/polar_adapter.go index a739c17..c311371 100644 --- a/go-b2b-starter/src/app/billing/infra/polar/polar_adapter.go +++ b/go-b2b-starter/src/app/billing/infra/polar/polar_adapter.go @@ -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, }