Skip to content
Merged
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
2 changes: 2 additions & 0 deletions core/audit/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ const (
ResourceDeletedEvent EventName = "app.resource.deleted"

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

var systemEvents = []EventName{
Expand All @@ -111,6 +112,7 @@ var systemEvents = []EventName{
OrgCreatedEvent,
OrgDeletedEvent,
OrgDisabledEvent,
BillingCheckoutDeletedEvent,
}

func IsSystemEvent(event EventName) bool {
Expand Down
61 changes: 61 additions & 0 deletions core/deleter/mocks/checkout_service.go

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

33 changes: 33 additions & 0 deletions core/deleter/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (

"github.com/raystack/frontier/core/authenticate"

"github.com/raystack/frontier/billing/checkout"

"github.com/raystack/frontier/billing/invoice"

"github.com/raystack/frontier/billing/customer"
Expand Down Expand Up @@ -105,6 +107,7 @@ type InvoiceService interface {
}

type CheckoutService interface {
List(ctx context.Context, filter checkout.Filter) ([]checkout.Checkout, error)
DeleteByCustomer(ctx context.Context, customerID string) error
}

Expand Down Expand Up @@ -328,9 +331,39 @@ func (d Service) DeleteCustomers(ctx context.Context, id string) error {
if err := d.invoiceService.DeleteByCustomer(ctx, c); err != nil {
return fmt.Errorf("failed to delete org while deleting a billing account invoices[%s]: %w", c.ID, err)
}
checkouts, err := d.checkoutService.List(ctx, checkout.Filter{CustomerID: c.ID})
if err != nil {
return fmt.Errorf("failed to delete org while listing billing account checkouts[%s]: %w", c.ID, err)
}
if err := d.checkoutService.DeleteByCustomer(ctx, c.ID); err != nil {
return fmt.Errorf("failed to delete org while deleting a billing account checkouts[%s]: %w", c.ID, err)
}
// the checkout rows are gone after this; the audit records keep the
// provider references so the sessions can still be found on the provider.
// The records are only written when ctx carries the audit service (the
// API path seeds it); otherwise audit.NewLogger falls back to a noop
auditLogger := audit.NewLogger(ctx, id)
Comment thread
whoAbhishekSah marked this conversation as resolved.
for _, ch := range checkouts {
attrs := map[string]string{
"provider_id": ch.ProviderID,
"customer_id": ch.CustomerID,
"plan_id": ch.PlanID,
Comment thread
whoAbhishekSah marked this conversation as resolved.
"product_id": ch.ProductID,
"state": ch.State,
"payment_status": ch.PaymentStatus,
}
for k, v := range attrs {
if v == "" {
delete(attrs, k)
}
}
if err := auditLogger.LogWithAttrs(audit.BillingCheckoutDeletedEvent, audit.Target{
ID: ch.ID,
Type: "billing_checkout",
}, attrs); err != nil {
slog.WarnContext(ctx, "failed to write audit log", "error", err, "event", audit.BillingCheckoutDeletedEvent, "checkout_id", ch.ID)
}
}
if err := d.creditService.DeleteByAccountID(ctx, c.ID); err != nil {
return fmt.Errorf("failed to delete org while deleting a billing account transactions[%s]: %w", c.ID, err)
}
Expand Down
28 changes: 28 additions & 0 deletions core/deleter/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/google/uuid"
"github.com/raystack/frontier/billing/checkout"
"github.com/raystack/frontier/billing/customer"
"github.com/raystack/frontier/billing/invoice"
"github.com/raystack/frontier/core/deleter"
Expand Down Expand Up @@ -139,6 +140,8 @@ func TestDeleteOrganization(t *testing.T) {
// billing teardown
m.subSvc.EXPECT().DeleteByCustomer(mock.Anything, c).Return(nil)
m.invocSvc.EXPECT().DeleteByCustomer(mock.Anything, c).Return(nil)
m.checkoutSvc.EXPECT().List(mock.Anything, checkout.Filter{CustomerID: "cust-1"}).
Return([]checkout.Checkout{{ID: "chk-1", ProviderID: "cs_1", CustomerID: "cust-1"}}, nil)
m.checkoutSvc.EXPECT().DeleteByCustomer(mock.Anything, "cust-1").Return(nil)
m.creditSvc.EXPECT().DeleteByAccountID(mock.Anything, "cust-1").Return(nil)
m.custSvc.EXPECT().Delete(mock.Anything, "cust-1").Return(nil)
Expand Down Expand Up @@ -282,6 +285,11 @@ func TestDeleteCustomers(t *testing.T) {
Return([]customer.Customer{c}, nil)
m.subSvc.EXPECT().DeleteByCustomer(mock.Anything, c).Return(nil)
m.invocSvc.EXPECT().DeleteByCustomer(mock.Anything, c).Return(nil)
m.checkoutSvc.EXPECT().List(mock.Anything, checkout.Filter{CustomerID: "cust-1"}).
Return([]checkout.Checkout{
{ID: "chk-1", ProviderID: "cs_1", CustomerID: "cust-1", PlanID: "plan-1", State: "complete", PaymentStatus: "paid"},
{ID: "chk-2", ProviderID: "cs_2", CustomerID: "cust-1", State: "expired"},
}, nil)
m.checkoutSvc.EXPECT().DeleteByCustomer(mock.Anything, "cust-1").Return(nil)
m.creditSvc.EXPECT().DeleteByAccountID(mock.Anything, "cust-1").Return(nil)
m.custSvc.EXPECT().Delete(mock.Anything, "cust-1").Return(nil)
Expand All @@ -298,6 +306,8 @@ func TestDeleteCustomers(t *testing.T) {
Return([]customer.Customer{c}, nil)
m.subSvc.EXPECT().DeleteByCustomer(mock.Anything, c).Return(nil)
m.invocSvc.EXPECT().DeleteByCustomer(mock.Anything, c).Return(nil)
m.checkoutSvc.EXPECT().List(mock.Anything, checkout.Filter{CustomerID: "cust-no-provider"}).
Return([]checkout.Checkout{}, nil)
m.checkoutSvc.EXPECT().DeleteByCustomer(mock.Anything, "cust-no-provider").Return(nil)
m.creditSvc.EXPECT().DeleteByAccountID(mock.Anything, "cust-no-provider").Return(nil)
m.custSvc.EXPECT().Delete(mock.Anything, "cust-no-provider").Return(nil)
Expand All @@ -314,13 +324,31 @@ func TestDeleteCustomers(t *testing.T) {
Return([]customer.Customer{c}, nil)
m.subSvc.EXPECT().DeleteByCustomer(mock.Anything, c).Return(nil)
m.invocSvc.EXPECT().DeleteByCustomer(mock.Anything, c).Return(nil)
m.checkoutSvc.EXPECT().List(mock.Anything, checkout.Filter{CustomerID: "cust-1"}).
Return([]checkout.Checkout{}, nil)
m.checkoutSvc.EXPECT().DeleteByCustomer(mock.Anything, "cust-1").
Return(errors.New("checkout delete failed"))
// strict mocks: custSvc.Delete must not be called

err := m.build().DeleteCustomers(context.Background(), "org-1")
assert.ErrorContains(t, err, "checkout delete failed")
})

t.Run("checkout list failure stops the customer delete", func(t *testing.T) {
m := newMocks(t)

c := customer.Customer{ID: "cust-1", ProviderID: "stripe-1"}
m.custSvc.EXPECT().List(mock.Anything, customer.Filter{OrgID: "org-1"}).
Return([]customer.Customer{c}, nil)
m.subSvc.EXPECT().DeleteByCustomer(mock.Anything, c).Return(nil)
m.invocSvc.EXPECT().DeleteByCustomer(mock.Anything, c).Return(nil)
m.checkoutSvc.EXPECT().List(mock.Anything, checkout.Filter{CustomerID: "cust-1"}).
Return(nil, errors.New("checkout list failed"))
// strict mocks: checkoutSvc.DeleteByCustomer and custSvc.Delete must not be called

err := m.build().DeleteCustomers(context.Background(), "org-1")
assert.ErrorContains(t, err, "checkout list failed")
})
}

func TestRemoveUsersFromOrg(t *testing.T) {
Expand Down
Loading