Skip to content

Commit

Permalink
feat: use customer id when present whilst creating a premium order (#…
Browse files Browse the repository at this point in the history
…2715)

* feat: use customer id when present whilst creating a premium order

* test: fix test for setOrderTrialDays

* fix: use customer id only when email is present on old session whilst recreating
  • Loading branch information
pavelbrm authored Nov 21, 2024
1 parent 38efbd6 commit 2e8f6a4
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 11 deletions.
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
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)
})
}
}

0 comments on commit 2e8f6a4

Please sign in to comment.