Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(app): stripe app validate customer data #2158

Merged
merged 2 commits into from
Jan 27, 2025
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
51 changes: 50 additions & 1 deletion openmeter/app/stripe/adapter/customer.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,57 @@ func (a adapter) UpsertStripeCustomerData(ctx context.Context, input appstripeen
}
}

// Get the stripe app client
stripeAppData, stripeAppClient, err := a.getStripeAppClient(ctx, input.AppID)
if err != nil {
return fmt.Errorf("failed to get stripe app client: %w", err)
}

// Check if the Stripe customer exists in the stripe account
_, err = stripeAppClient.GetCustomer(ctx, input.StripeCustomerID)
if err != nil {
if _, ok := err.(stripeclient.StripeCustomerNotFoundError); ok {
return app.AppCustomerPreConditionError{
AppID: input.AppID,
AppType: appentitybase.AppTypeStripe,
CustomerID: input.CustomerID,
Condition: fmt.Sprintf("stripe customer %s not found in stripe account: %s", input.StripeCustomerID, stripeAppData.StripeAccountID),
}
}

return fmt.Errorf("failed to get stripe customer: %w", err)
}

// Check if the Stripe payment method exists in the stripe account
if input.StripeDefaultPaymentMethodID != nil {
paymentMethod, err := stripeAppClient.GetPaymentMethod(ctx, *input.StripeDefaultPaymentMethodID)
if err != nil {
if _, ok := err.(stripeclient.StripePaymentMethodNotFoundError); ok {
return app.AppProviderPreConditionError{
AppID: input.AppID,
Condition: fmt.Sprintf("stripe payment method %s not found in stripe account: %s", *input.StripeDefaultPaymentMethodID, stripeAppData.StripeAccountID),
}
}

return fmt.Errorf("failed to get stripe payment method: %w", err)
}

// Check if the payment method belongs to the customer
if paymentMethod.StripeCustomerID == nil || *paymentMethod.StripeCustomerID != input.StripeCustomerID {
return app.AppProviderPreConditionError{
AppID: input.AppID,
Condition: fmt.Sprintf(
"stripe payment method %s does not belong to stripe customer %s in stripe account: %s",
*input.StripeDefaultPaymentMethodID,
input.StripeCustomerID,
stripeAppData.StripeAccountID,
),
}
}
}

// Start transaction
_, err := entutils.TransactingRepo(ctx, a, func(ctx context.Context, repo *adapter) (any, error) {
_, err = entutils.TransactingRepo(ctx, a, func(ctx context.Context, repo *adapter) (any, error) {
// Make sure the customer has an app relationship
err := a.appService.EnsureCustomer(ctx, app.EnsureCustomerInput{
AppID: input.AppID,
Expand Down
14 changes: 7 additions & 7 deletions openmeter/app/stripe/adapter/stripe.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ func (a adapter) GetSupplierContact(ctx context.Context, input appstripeentity.G
}

// Get Stripe App client
stripeAppClient, err := a.getStripeAppClient(ctx, input.AppID)
_, stripeAppClient, err := a.getStripeAppClient(ctx, input.AppID)
if err != nil {
return billing.SupplierContact{}, fmt.Errorf("failed to get stripe app client: %w", err)
}
Expand Down Expand Up @@ -582,24 +582,24 @@ func (a adapter) GetMaskedSecretAPIKey(secretAPIKeyID secretentity.SecretID) (st
}

// getStripeAppClient returns a Stripe App Client based on App ID
func (a adapter) getStripeAppClient(ctx context.Context, appID appentitybase.AppID) (stripeclient.StripeAppClient, error) {
func (a adapter) getStripeAppClient(ctx context.Context, appID appentitybase.AppID) (appstripeentity.AppData, stripeclient.StripeAppClient, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit maybe return a single type instead of returning three values

// Validate app id
if err := appID.Validate(); err != nil {
return nil, fmt.Errorf("app id: %w", err)
return appstripeentity.AppData{}, nil, fmt.Errorf("app id: %w", err)
}

// Get the stripe app data
stripeAppData, err := a.GetStripeAppData(ctx, appstripeentity.GetStripeAppDataInput{
AppID: appID,
})
if err != nil {
return nil, fmt.Errorf("failed to get stripe app data: %w", err)
return stripeAppData, nil, fmt.Errorf("failed to get stripe app data: %w", err)
}

// Get Stripe API Key
apiKeySecret, err := a.secretService.GetAppSecret(ctx, stripeAppData.APIKey)
if err != nil {
return nil, fmt.Errorf("failed to get stripe api key secret: %w", err)
return stripeAppData, nil, fmt.Errorf("failed to get stripe api key secret: %w", err)
}

// Stripe Client
Expand All @@ -609,10 +609,10 @@ func (a adapter) getStripeAppClient(ctx context.Context, appID appentitybase.App
APIKey: apiKeySecret.Value,
})
if err != nil {
return nil, fmt.Errorf("failed to create stripe client: %w", err)
return stripeAppData, nil, fmt.Errorf("failed to create stripe client: %w", err)
}

return stripeClient, nil
return stripeAppData, stripeClient, nil
}

// mapAppStripeData maps stripe app data from the database
Expand Down
4 changes: 4 additions & 0 deletions openmeter/app/stripe/client/appclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ func toStripePaymentMethod(stripePaymentMethod *stripe.PaymentMethod) StripePaym
ID: stripePaymentMethod.ID,
}

if stripePaymentMethod.Customer != nil {
paymentMethod.StripeCustomerID = &stripePaymentMethod.Customer.ID
}

if stripePaymentMethod.BillingDetails != nil && stripePaymentMethod.BillingDetails.Address != nil {
address := *stripePaymentMethod.BillingDetails.Address

Expand Down
9 changes: 5 additions & 4 deletions openmeter/app/stripe/client/stripe.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ type StripeCustomer struct {
}

type StripePaymentMethod struct {
ID string
Name string
Email string
BillingAddress *models.Address
ID string
StripeCustomerID *string
Name string
Email string
BillingAddress *models.Address
}

type StripeCheckoutSession struct {
Expand Down
130 changes: 129 additions & 1 deletion test/app/stripe/appstripe.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,15 +363,135 @@ func (s *AppHandlerTestSuite) TestCustomerData(ctx context.Context, t *testing.T
require.Equal(t, testApp.GetID(), listCustomerData.Items[0].App.GetID(), "App ID must match")

// Update customer data
newStripeCustomerID := "cus_456"
newStripePaymentMethodID := "pm_456"

s.Env.StripeAppClient().
On("GetCustomer", newStripeCustomerID).
Return(stripeclient.StripeCustomer{
StripeCustomerID: newStripeCustomerID,
}, nil)

s.Env.StripeAppClient().
On("GetPaymentMethod", newStripePaymentMethodID).
Return(stripeclient.StripePaymentMethod{
ID: newStripePaymentMethodID,
StripeCustomerID: &newStripeCustomerID,
Name: "ACME Inc.",
Email: "[email protected]",
}, nil)

defer s.Env.StripeAppClient().Restore()

err = testApp.UpsertCustomerData(ctx, appentity.UpsertAppInstanceCustomerDataInput{
CustomerID: customer.GetID(),
Data: appstripeentity.CustomerData{
StripeCustomerID: "cus_456",
StripeCustomerID: newStripeCustomerID,
StripeDefaultPaymentMethodID: &newStripePaymentMethodID,
},
})

require.NoError(t, err, "Update customer data must not return error")

// Update customer data with non existing stripe customer should return error
stripeAppData, err := s.Env.AppStripe().GetStripeAppData(ctx, appstripeentity.GetStripeAppDataInput{
AppID: testApp.GetID(),
})
require.NoError(t, err, "Get stripe app data must not return error")

nonExistingStripeCustomerID := "cus_789"

s.Env.StripeAppClient().
On("GetCustomer", nonExistingStripeCustomerID).
Return(stripeclient.StripeCustomer{}, stripeclient.StripeCustomerNotFoundError{
StripeCustomerID: nonExistingStripeCustomerID,
})

defer s.Env.StripeAppClient().Restore()

err = testApp.UpsertCustomerData(ctx, appentity.UpsertAppInstanceCustomerDataInput{
CustomerID: customer.GetID(),
Data: appstripeentity.CustomerData{
StripeCustomerID: nonExistingStripeCustomerID,
},
})

require.ErrorIs(t, err, app.AppCustomerPreConditionError{
AppID: testApp.GetID(),
AppType: appentitybase.AppTypeStripe,
CustomerID: customer.GetID(),
Condition: fmt.Sprintf("stripe customer %s not found in stripe account: %s", nonExistingStripeCustomerID, stripeAppData.StripeAccountID),
})

// Updated customer data with non existing payment method should return error
nonExistingStripePaymentMethodID := "pm_789"

s.Env.StripeAppClient().Restore()

s.Env.StripeAppClient().
On("GetCustomer", newStripeCustomerID).
Return(stripeclient.StripeCustomer{
StripeCustomerID: newStripeCustomerID,
}, nil)

s.Env.StripeAppClient().
On("GetPaymentMethod", nonExistingStripePaymentMethodID).
Return(stripeclient.StripePaymentMethod{}, stripeclient.StripePaymentMethodNotFoundError{
StripePaymentMethodID: nonExistingStripePaymentMethodID,
})

defer s.Env.StripeAppClient().Restore()

err = testApp.UpsertCustomerData(ctx, appentity.UpsertAppInstanceCustomerDataInput{
CustomerID: customer.GetID(),
Data: appstripeentity.CustomerData{
StripeCustomerID: newStripeCustomerID,
StripeDefaultPaymentMethodID: &nonExistingStripePaymentMethodID,
},
})

require.ErrorIs(t, err, app.AppProviderPreConditionError{
AppID: testApp.GetID(),
Condition: fmt.Sprintf("stripe payment method %s not found in stripe account: %s", nonExistingStripePaymentMethodID, stripeAppData.StripeAccountID),
})

// Updated customer data with payment method that does not belong to the customer should return errors
s.Env.StripeAppClient().Restore()

s.Env.StripeAppClient().
On("GetCustomer", newStripeCustomerID).
Return(stripeclient.StripeCustomer{
StripeCustomerID: newStripeCustomerID,
}, nil)

s.Env.StripeAppClient().
On("GetPaymentMethod", newStripePaymentMethodID).
Return(stripeclient.StripePaymentMethod{
ID: newStripePaymentMethodID,
StripeCustomerID: lo.ToPtr("cus_different"),
Name: "ACME Inc.",
}, nil)

defer s.Env.StripeAppClient().Restore()

err = testApp.UpsertCustomerData(ctx, appentity.UpsertAppInstanceCustomerDataInput{
CustomerID: customer.GetID(),
Data: appstripeentity.CustomerData{
StripeCustomerID: newStripeCustomerID,
StripeDefaultPaymentMethodID: &newStripePaymentMethodID,
},
})

require.ErrorIs(t, err, app.AppProviderPreConditionError{
AppID: testApp.GetID(),
Condition: fmt.Sprintf(
"stripe payment method %s does not belong to stripe customer %s in stripe account: %s",
newStripePaymentMethodID,
newStripeCustomerID,
stripeAppData.StripeAccountID,
),
})

// Updated customer data must match
getCustomerData, err = testApp.GetCustomerData(ctx, appentity.GetAppInstanceCustomerDataInput{
CustomerID: customer.GetID(),
Expand Down Expand Up @@ -414,6 +534,14 @@ func (s *AppHandlerTestSuite) TestCustomerData(ctx context.Context, t *testing.T
})

// Restore customer data
s.Env.StripeAppClient().
On("GetCustomer", customerData.StripeCustomerID).
Return(stripeclient.StripeCustomer{
StripeCustomerID: customerData.StripeCustomerID,
}, nil)

defer s.Env.StripeAppClient().Restore()

err = testApp.UpsertCustomerData(ctx, appentity.UpsertAppInstanceCustomerDataInput{
CustomerID: customer.GetID(),
Data: appstripeentity.CustomerData{
Expand Down
24 changes: 18 additions & 6 deletions test/app/stripe/fixture.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,21 @@ func NewFixture(
app app.Service,
customer customer.Service,
stripeClient *StripeClientMock,
stripeAppClient *StripeAppClientMock,
) *Fixture {
return &Fixture{
app: app,
customer: customer,
stripeClient: stripeClient,
app: app,
customer: customer,
stripeClient: stripeClient,
stripeAppClient: stripeAppClient,
}
}

type Fixture struct {
app app.Service
customer customer.Service
stripeClient *StripeClientMock
app app.Service
customer customer.Service
stripeClient *StripeClientMock
stripeAppClient *StripeAppClientMock
}

// setupAppWithCustomer creates a stripe app and a customer with customer data
Expand Down Expand Up @@ -109,6 +112,15 @@ func (s *Fixture) setupAppCustomerData(ctx context.Context, app appentity.App, c
StripeCustomerID: "cus_123",
}

s.stripeAppClient.
On("GetCustomer", data.StripeCustomerID).
Return(stripeclient.StripeCustomer{
StripeCustomerID: data.StripeCustomerID,
}, nil)

// TODO: do not share env between tests
defer s.stripeAppClient.Restore()

err := app.UpsertCustomerData(ctx, appentity.UpsertAppInstanceCustomerDataInput{
CustomerID: customer.GetID(),
Data: data,
Expand Down
2 changes: 1 addition & 1 deletion test/app/stripe/invoice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (s *StripeInvoiceTestSuite) SetupSuite() {
s.AppStripeService = appStripeService

// Fixture
s.Fixture = NewFixture(s.AppService, s.CustomerService, stripeClient)
s.Fixture = NewFixture(s.AppService, s.CustomerService, stripeClient, stripeAppClient)
}

type ubpFeatures struct {
Expand Down
2 changes: 1 addition & 1 deletion test/app/stripe/testenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ func NewTestEnv(t *testing.T, ctx context.Context) (TestEnv, error) {
app: appService,
appstripe: appStripeService,
customer: customerService,
fixture: NewFixture(appService, customerService, stripeClientMock),
fixture: NewFixture(appService, customerService, stripeClientMock, stripeAppClientMock),
secret: secretService,
closerFunc: closerFunc,
stripeClient: stripeClientMock,
Expand Down
Loading