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: use customer id when present whilst creating a premium order #2715

Merged
merged 4 commits into from
Nov 21, 2024
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
1 change: 1 addition & 0 deletions services/skus/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ type OrderItemRequest struct {
// CreateOrderRequestNew includes information needed to create an order.
type CreateOrderRequestNew struct {
Email string `json:"email" validate:"required,email"`
CustomerID string `json:"customer_id"` // Optional.
Currency string `json:"currency" validate:"required,iso4217"`
StripeMetadata *OrderStripeMetadata `json:"stripe_metadata"`
RadomMetadata *OrderRadomMetadata `json:"radom_metadata"`
Expand Down
17 changes: 14 additions & 3 deletions services/skus/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2021,6 +2021,7 @@ func (s *Service) createStripeSession(ctx context.Context, req *model.CreateOrde
sreq := createStripeSessionRequest{
orderID: oid,
email: req.Email,
customerID: req.CustomerID,
successURL: surl,
cancelURL: curl,
trialDays: order.GetTrialDays(),
Expand Down Expand Up @@ -2572,6 +2573,7 @@ func (s *Service) recreateStripeSession(ctx context.Context, dbi sqlx.ExecerCont
req := createStripeSessionRequest{
orderID: ord.ID.String(),
email: email,
customerID: xstripe.CustomerIDFromSession(oldSess),
successURL: oldSess.SuccessURL,
cancelURL: oldSess.CancelURL,
trialDays: ord.GetTrialDays(),
Expand Down Expand Up @@ -2798,6 +2800,7 @@ func chooseStripeSessID(ord *model.Order, canBeNewSessID string) (string, bool)
type createStripeSessionRequest struct {
orderID string
email string
customerID string
successURL string
cancelURL string
trialDays int64
Expand All @@ -2815,9 +2818,17 @@ func createStripeSession(ctx context.Context, cl stripeClient, req createStripeS
LineItems: req.items,
}

// Email might not be given.
// This could happen while recreating a session, and the email was not extracted from the old one.
if req.email != "" {
// Different processes can supply different info about customer:
// - when customerID is present, it takes precedence;
// - when email is present:
// - first, search for customer;
// - fallback to using the email directly.
// Based on the rules above, if both are present, customerID wins.
switch {
case req.customerID != "":
params.Customer = &req.customerID

case req.customerID == "" && req.email != "":
if cust, ok := cl.FindCustomer(ctx, req.email); ok && cust.Email != "" {
params.Customer = &cust.ID
} else {
Expand Down
163 changes: 156 additions & 7 deletions services/skus/service_nonint_test.go
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for adding these tests in here - it made it much simpler to understand what this change was doing.

Original file line number Diff line number Diff line change
Expand Up @@ -4388,7 +4388,7 @@ func TestService_recreateStripeSession(t *testing.T) {
},

{
name: "success_email_from_session",
name: "success_email_cust_from_session",
given: tcGiven{
ordRepo: &repository.MockOrder{
FnAppendMetadata: func(ctx context.Context, dbi sqlx.ExecerContext, id uuid.UUID, key, val string) error {
Expand All @@ -4405,7 +4405,7 @@ func TestService_recreateStripeSession(t *testing.T) {
ID: "cs_test_id_old",
SuccessURL: "https://example.com/success",
CancelURL: "https://example.com/cancel",
Customer: &stripe.Customer{Email: "[email protected]"},
Customer: &stripe.Customer{ID: "cus_id", Email: "[email protected]"},
}

return result, nil
Expand Down Expand Up @@ -4455,6 +4455,79 @@ func TestService_recreateStripeSession(t *testing.T) {
},
},

{
name: "success_email_from_request_cust_without_email",
given: tcGiven{
ordRepo: &repository.MockOrder{
FnAppendMetadata: func(ctx context.Context, dbi sqlx.ExecerContext, id uuid.UUID, key, val string) error {
if key == "stripeCheckoutSessionId" && val == "cs_test_id" {
return nil
}

return model.Error("unexpected")
},
},
cl: &xstripe.MockClient{
FnSession: func(ctx context.Context, id string, params *stripe.CheckoutSessionParams) (*stripe.CheckoutSession, error) {
result := &stripe.CheckoutSession{
ID: "cs_test_id_old",
SuccessURL: "https://example.com/success",
CancelURL: "https://example.com/cancel",
Customer: &stripe.Customer{ID: "cus_id"},
}

return result, nil
},

FnFindCustomer: func(ctx context.Context, email string) (*stripe.Customer, bool) {
return nil, false
},

FnCreateSession: func(ctx context.Context, params *stripe.CheckoutSessionParams) (*stripe.CheckoutSession, error) {
if params.Customer != nil {
return nil, model.Error("unexpected_customer")
}

if *params.CustomerEmail != "[email protected]" {
return nil, model.Error("unexpected_customer_email")
}

result := &stripe.CheckoutSession{
ID: "cs_test_id",
PaymentMethodTypes: []string{"card"},
Mode: stripe.CheckoutSessionModeSubscription,
SuccessURL: *params.SuccessURL,
CancelURL: *params.CancelURL,
ClientReferenceID: *params.ClientReferenceID,
Subscription: &stripe.Subscription{
ID: "sub_id",
Metadata: map[string]string{
"orderID": *params.ClientReferenceID,
},
},
AllowPromotionCodes: true,
}

return result, nil
},
},
ord: &model.Order{
ID: uuid.Must(uuid.FromString("facade00-0000-4000-a000-000000000000")),
Items: []model.OrderItem{
{
Quantity: 1,
Metadata: datastore.Metadata{"stripe_item_id": "stripe_item_id"},
},
},
},
oldSessID: "cs_test_id_old",
email: "[email protected]",
},
exp: tcExpected{
val: "cs_test_id",
},
},

{
name: "success_email_from_request",
given: tcGiven{
Expand All @@ -4473,7 +4546,6 @@ func TestService_recreateStripeSession(t *testing.T) {
ID: "cs_test_id_old",
SuccessURL: "https://example.com/success",
CancelURL: "https://example.com/cancel",
Customer: &stripe.Customer{Email: "[email protected]"},
}

return result, nil
Expand Down Expand Up @@ -4564,7 +4636,84 @@ func TestCreateStripeSession(t *testing.T) {

tests := []testCase{
{
name: "success_found_customer",
name: "success_cust_id",
given: tcGiven{
cl: &xstripe.MockClient{
FnCreateSession: func(ctx context.Context, params *stripe.CheckoutSessionParams) (*stripe.CheckoutSession, error) {
if params.Customer == nil || *params.Customer != "cus_id" {
return nil, model.Error("unexpected")
}

result := &stripe.CheckoutSession{ID: "cs_test_id"}

return result, nil
},

FnFindCustomer: func(ctx context.Context, email string) (*stripe.Customer, bool) {
panic("unexpected_find_customer")
},
},

req: createStripeSessionRequest{
orderID: "facade00-0000-4000-a000-000000000000",
customerID: "cus_id",
successURL: "https://example.com/success",
cancelURL: "https://example.com/cancel",
trialDays: 7,
items: []*stripe.CheckoutSessionLineItemParams{
{
Quantity: ptrTo[int64](1),
Price: ptrTo("stripe_item_id"),
},
},
},
},
exp: tcExpected{
val: "cs_test_id",
},
},

{
name: "success_cust_id_email",
given: tcGiven{
cl: &xstripe.MockClient{
FnCreateSession: func(ctx context.Context, params *stripe.CheckoutSessionParams) (*stripe.CheckoutSession, error) {
if params.Customer == nil || *params.Customer != "cus_id" {
return nil, model.Error("unexpected")
}

result := &stripe.CheckoutSession{ID: "cs_test_id"}

return result, nil
},

FnFindCustomer: func(ctx context.Context, email string) (*stripe.Customer, bool) {
panic("unexpected_find_customer")
},
},

req: createStripeSessionRequest{
orderID: "facade00-0000-4000-a000-000000000000",
customerID: "cus_id",
email: "[email protected]",
successURL: "https://example.com/success",
cancelURL: "https://example.com/cancel",
trialDays: 7,
items: []*stripe.CheckoutSessionLineItemParams{
{
Quantity: ptrTo[int64](1),
Price: ptrTo("stripe_item_id"),
},
},
},
},
exp: tcExpected{
val: "cs_test_id",
},
},

{
name: "success_email_found_customer",
given: tcGiven{
cl: &xstripe.MockClient{
FnCreateSession: func(ctx context.Context, params *stripe.CheckoutSessionParams) (*stripe.CheckoutSession, error) {
Expand Down Expand Up @@ -4598,7 +4747,7 @@ func TestCreateStripeSession(t *testing.T) {
},

{
name: "success_customer_not_found",
name: "success_email_customer_not_found",
given: tcGiven{
cl: &xstripe.MockClient{
FnFindCustomer: func(ctx context.Context, email string) (*stripe.Customer, bool) {
Expand Down Expand Up @@ -4636,7 +4785,7 @@ func TestCreateStripeSession(t *testing.T) {
},

{
name: "success_no_customer_email",
name: "success_email_no_customer_email",
given: tcGiven{
cl: &xstripe.MockClient{
FnFindCustomer: func(ctx context.Context, email string) (*stripe.Customer, bool) {
Expand All @@ -4663,7 +4812,7 @@ func TestCreateStripeSession(t *testing.T) {
},

{
name: "success_no_trial_days",
name: "success_email_no_trial_days",
given: tcGiven{
cl: &xstripe.MockClient{},

Expand Down
5 changes: 4 additions & 1 deletion services/skus/xstripe/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ type MockClient struct {

func (c *MockClient) Session(ctx context.Context, id string, params *stripe.CheckoutSessionParams) (*stripe.CheckoutSession, error) {
if c.FnSession == nil {
result := &stripe.CheckoutSession{ID: id}
result := &stripe.CheckoutSession{
ID: id,
Customer: &stripe.Customer{ID: "cus_id", Email: "[email protected]"},
}

return result, nil
}
Expand Down
10 changes: 10 additions & 0 deletions services/skus/xstripe/xstripe.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,13 @@ func CustomerEmailFromSession(sess *stripe.CheckoutSession) string {
// Default to empty, Stripe will ask the customer.
return ""
}

func CustomerIDFromSession(sess *stripe.CheckoutSession) string {
// Return the customer id only if the customer is present AND it has email set.
// Without the email, the customer record is not fully formed, and does not suit the use case.
if sess.Customer != nil && sess.Customer.Email != "" {
return sess.Customer.ID
}

return ""
}
49 changes: 49 additions & 0 deletions services/skus/xstripe/xstripe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,52 @@ func TestCustomerEmailFromSession(t *testing.T) {
})
}
}

func TestCustomerIDFromSession(t *testing.T) {
tests := []struct {
name string
exp string
given *stripe.CheckoutSession
}{
{
name: "nil_customer_no_email",
given: &stripe.CheckoutSession{},
},

{
name: "customer_empty_email",
given: &stripe.CheckoutSession{
Customer: &stripe.Customer{},
},
},

{
name: "customer_email_no_id",
given: &stripe.CheckoutSession{
Customer: &stripe.Customer{
Email: "[email protected]",
},
},
},

{
name: "customer_email_id",
given: &stripe.CheckoutSession{
Customer: &stripe.Customer{
ID: "cus_id",
Email: "[email protected]",
},
},
exp: "cus_id",
},
}

for i := range tests {
tc := tests[i]

t.Run(tc.name, func(t *testing.T) {
actual := CustomerIDFromSession(tc.given)
should.Equal(t, tc.exp, actual)
})
}
}
Loading