Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 0 additions & 47 deletions billing/credit/mocks/transaction_repository.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 0 additions & 7 deletions billing/credit/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ type TransactionRepository interface {
GetByID(ctx context.Context, id string) (Transaction, error)
GetBalanceForRange(ctx context.Context, accountID string, start time.Time, end time.Time) (int64, error)
GetBalanceForRangeWithoutOverdraft(ctx context.Context, accountID string, start time.Time, end time.Time) (int64, error)
DeleteByAccountID(ctx context.Context, accountID string) error
}

type CustomerRepository interface {
Expand Down Expand Up @@ -176,12 +175,6 @@ func (s Service) GetByID(ctx context.Context, id string) (Transaction, error) {
return s.transactionRepository.GetByID(ctx, id)
}

// DeleteByAccountID marks every credit transaction of a billing account as deleted. It is
// meant for account teardown.
func (s Service) DeleteByAccountID(ctx context.Context, accountID string) error {
return s.transactionRepository.DeleteByAccountID(ctx, accountID)
}

// createAuditRecord creates an audit record for billing transaction events.
func (s Service) createAuditRecord(ctx context.Context, customerID string, eventType pkgAuditRecord.Event, txID string, txEntry Transaction) error {
customerAcc, err := s.customerRepository.GetByID(ctx, customerID)
Expand Down
10 changes: 10 additions & 0 deletions billing/customer/customer.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,13 @@ func GetStripeWebhookSignatureFromContext(ctx context.Context) (string, bool) {
u, ok := ctx.Value(consts.BillingStripeWebhookSignatureContextKey).(string)
return u, ok
}

type DeleteOptions struct {
OrgTeardown bool
}

const (
DeleteRecordForfeitedTokensKey = "forfeited_tokens"
DeleteRecordPurchasedTokensKey = "purchased_tokens"
DeleteRecordOrgTeardownKey = "org_teardown"
)
1 change: 1 addition & 0 deletions billing/customer/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ var (
ErrDisabled = errors.New("billing customer is disabled")
ErrExistingAccountWithPendingDues = errors.New("existing account with pending dues found")
ErrHasLiveRecords = errors.New("billing account still has live subscriptions, invoices or checkouts")
ErrHasTokenDebt = errors.New("billing account owes tokens, settle the balance before deleting it")
)
78 changes: 68 additions & 10 deletions billing/customer/mocks/repository.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 20 additions & 3 deletions billing/customer/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ type Repository interface {
List(ctx context.Context, filter Filter) ([]Customer, error)
Create(ctx context.Context, customer Customer) (Customer, error)
UpdateByID(ctx context.Context, customer Customer) (Customer, error)
Delete(ctx context.Context, id string) error
Delete(ctx context.Context, id string, opts DeleteOptions) error
HasLiveRecords(ctx context.Context, id string) (bool, error)
UpdateCreditMinByID(ctx context.Context, customerID string, limit int64) (Details, error)
GetDetailsByID(ctx context.Context, customerID string) (Details, error)
UpdateDetailsByID(ctx context.Context, customerID string, details Details) (Details, error)
Expand Down Expand Up @@ -267,12 +268,28 @@ func (s *Service) Disable(ctx context.Context, id string) error {
return err
}

func (s *Service) Delete(ctx context.Context, id string) error {
func (s *Service) Delete(ctx context.Context, id string, opts DeleteOptions) error {
customer, err := s.repository.GetByID(ctx, id)
if err != nil {
return err
}

balance, err := s.creditService.GetBalance(ctx, id)
if err != nil {
return fmt.Errorf("failed to get token balance of billing account[%s]: %w", id, err)
}
if balance < 0 {
return ErrHasTokenDebt
}

hasLiveRecords, err := s.repository.HasLiveRecords(ctx, id)
if err != nil {
return fmt.Errorf("failed to check live records of billing account[%s]: %w", id, err)
}
if hasLiveRecords {
return ErrHasLiveRecords
}

// TODO: cancel and delete all subscriptions before deleting the customer

if customer.ProviderID != "" {
Expand All @@ -290,7 +307,7 @@ func (s *Service) Delete(ctx context.Context, id string) error {
}
}

return s.repository.Delete(ctx, id)
return s.repository.Delete(ctx, id, opts)
}

func (s *Service) ListPaymentMethods(ctx context.Context, id string) ([]PaymentMethod, error) {
Expand Down
43 changes: 43 additions & 0 deletions billing/customer/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/raystack/frontier/billing"
Expand Down Expand Up @@ -509,3 +510,45 @@ func TestService_List(t *testing.T) {
})
}
}

func TestService_Delete(t *testing.T) {
ctx := context.Background()
account := customer.Customer{ID: "acc-1", OrgID: "org-1"}

t.Run("refuses while the account owes tokens", func(t *testing.T) {
_, _, repo, creditSvc := mockService(t)
repo.EXPECT().GetByID(ctx, "acc-1").Return(account, nil)
creditSvc.EXPECT().GetBalance(ctx, "acc-1").Return(int64(-50), nil)
svc := customer.NewService(slog.Default(), nil, repo, billing.Config{}, creditSvc)

err := svc.Delete(ctx, "acc-1", customer.DeleteOptions{OrgTeardown: true})

assert.ErrorIs(t, err, customer.ErrHasTokenDebt)
})

t.Run("refuses before the billing provider is called while live records exist", func(t *testing.T) {
stripeClient, _, repo, creditSvc := mockService(t)
online := customer.Customer{ID: "acc-1", OrgID: "org-1", ProviderID: "cus_1"}
repo.EXPECT().GetByID(ctx, "acc-1").Return(online, nil)
creditSvc.EXPECT().GetBalance(ctx, "acc-1").Return(int64(0), nil)
repo.EXPECT().HasLiveRecords(ctx, "acc-1").Return(true, nil)
svc := customer.NewService(slog.Default(), stripeClient, repo, billing.Config{}, creditSvc)

err := svc.Delete(ctx, "acc-1", customer.DeleteOptions{OrgTeardown: true})

assert.ErrorIs(t, err, customer.ErrHasLiveRecords)
})

t.Run("deletes the billing provider customer when no live records exist", func(t *testing.T) {
stripeClient, stripeBackend, repo, creditSvc := mockService(t)
online := customer.Customer{ID: "acc-1", OrgID: "org-1", ProviderID: "cus_1"}
repo.EXPECT().GetByID(ctx, "acc-1").Return(online, nil)
creditSvc.EXPECT().GetBalance(ctx, "acc-1").Return(int64(0), nil)
repo.EXPECT().HasLiveRecords(ctx, "acc-1").Return(false, nil)
stripeBackend.EXPECT().Call("DELETE", "/v1/customers/cus_1", "key_123", mock.Anything, mock.Anything).Return(nil)
repo.EXPECT().Delete(ctx, "acc-1", customer.DeleteOptions{}).Return(nil)
svc := customer.NewService(slog.Default(), stripeClient, repo, billing.Config{}, creditSvc)

require.NoError(t, svc.Delete(ctx, "acc-1", customer.DeleteOptions{}))
})
}
2 changes: 1 addition & 1 deletion cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ func buildAPIDependencies(
cascadeDeleter := deleter.NewCascadeDeleter(organizationService, projectService, resourceService,
groupService, membershipService, policyService, roleService, invitationService, userService, userPATService,
domainService, serviceUserService, customerService, subscriptionService, invoiceService, checkoutService,
creditService, orgKycService, planService,
creditService, orgKycService, planService, auditRecordService,
mailDialer, cfg.Billing.OrgDeleteNotice, cfg.App.Deletion,
)

Expand Down
2 changes: 0 additions & 2 deletions core/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ const (

BillingAccountDetailsUpdatedEvent EventName = "app.billing.account.details.updated"
BillingCheckoutDeletedEvent EventName = "app.billing.checkout.deleted"
BillingTokensForfeitedEvent EventName = "app.billing.tokens.forfeited"
)

var systemEvents = []EventName{
Expand All @@ -114,7 +113,6 @@ var systemEvents = []EventName{
OrgDeletedEvent,
OrgDisabledEvent,
BillingCheckoutDeletedEvent,
BillingTokensForfeitedEvent,
}

func IsSystemEvent(event EventName) bool {
Expand Down
Loading
Loading