From c0b981ffc946e6bf323586cc9cb1697ec8d89152 Mon Sep 17 00:00:00 2001 From: PavelBrm Date: Sat, 14 Dec 2024 00:40:35 +1300 Subject: [PATCH] feat: add initial support for passing stripe discounts --- services/skus/handler/handler_test.go | 126 ++++++++++++++++++++++++++ services/skus/model/model.go | 2 + services/skus/service.go | 24 ++++- services/skus/service_nonint_test.go | 99 ++++++++++++++++++++ 4 files changed, 250 insertions(+), 1 deletion(-) diff --git a/services/skus/handler/handler_test.go b/services/skus/handler/handler_test.go index 247ba098a..3c50edbfc 100644 --- a/services/skus/handler/handler_test.go +++ b/services/skus/handler/handler_test.go @@ -482,6 +482,132 @@ func TestOrder_CreateNew(t *testing.T) { }, }, }, + + { + name: "success_dicounts_metadata", + given: tcGiven{ + svc: &mockOrderService{ + fnCreateOrder: func(ctx context.Context, req *model.CreateOrderRequestNew) (*model.Order, error) { + if len(req.Discounts) != 1 { + return nil, model.Error("unexpected_discounts") + } + + if req.Discounts[0] != "coup_id_01" { + return nil, model.Error("unexpected_discount_value") + } + + if val, ok := req.Metadata["key_01"]; !ok || val != "val_01" { + return nil, model.Error("unexpected_metadata_val") + } + + result := &model.Order{ + Location: datastore.NullString{ + NullString: sql.NullString{ + Valid: true, + String: "location", + }, + }, + Items: []model.OrderItem{ + { + SKU: "sku", + SKUVnt: "sku_vnt", + Quantity: 1, + Price: mustDecimalFromString("1"), + Subtotal: mustDecimalFromString("1"), + Location: datastore.NullString{ + NullString: sql.NullString{ + Valid: true, + String: "location", + }, + }, + Description: datastore.NullString{ + NullString: sql.NullString{ + Valid: true, + String: "description", + }, + }, + CredentialType: "credential_type", + ValidForISO: ptrTo("P1M"), + Metadata: datastore.Metadata{ + "stripe_product_id": "product_id", + "stripe_item_id": "item_id", + }, + }, + }, + TotalPrice: mustDecimalFromString("1"), + } + + return result, nil + }, + }, + body: `{ + "email": "you@example.com", + "currency": "USD", + "stripe_metadata": { + "success_uri": "https://example.com/success", + "cancel_uri": "https://example.com/cancel" + }, + "payment_methods": ["stripe"], + "discounts": ["coup_id_01"], + "items": [ + { + "quantity": 1, + "sku": "sku", + "sku_variant": "sku_vnt", + "location": "location", + "description": "description", + "credential_type": "credential_type", + "credential_valid_duration": "P1M", + "stripe_metadata": { + "product_id": "product_id", + "item_id": "item_id" + } + } + ], + "metadata": { + "key_01": "val_01" + } + }`, + }, + exp: tcExpected{ + result: &model.Order{ + Location: datastore.NullString{ + NullString: sql.NullString{ + Valid: true, + String: "location", + }, + }, + Items: []model.OrderItem{ + { + SKU: "sku", + SKUVnt: "sku_vnt", + Quantity: 1, + Price: mustDecimalFromString("1"), + Subtotal: mustDecimalFromString("1"), + Location: datastore.NullString{ + NullString: sql.NullString{ + Valid: true, + String: "location", + }, + }, + Description: datastore.NullString{ + NullString: sql.NullString{ + Valid: true, + String: "description", + }, + }, + CredentialType: "credential_type", + ValidForISO: ptrTo("P1M"), + Metadata: datastore.Metadata{ + "stripe_product_id": "product_id", + "stripe_item_id": "item_id", + }, + }, + }, + TotalPrice: mustDecimalFromString("1"), + }, + }, + }, } for i := range tests { diff --git a/services/skus/model/model.go b/services/skus/model/model.go index bdb6d1642..26f76c3c0 100644 --- a/services/skus/model/model.go +++ b/services/skus/model/model.go @@ -418,7 +418,9 @@ type CreateOrderRequestNew struct { StripeMetadata *OrderStripeMetadata `json:"stripe_metadata"` RadomMetadata *OrderRadomMetadata `json:"radom_metadata"` PaymentMethods []string `json:"payment_methods"` + Discounts []string `json:"discounts"` Items []OrderItemRequestNew `json:"items" validate:"required,gt=0,dive"` + Metadata map[string]string `json:"metadata"` } // OrderItemRequestNew represents an item in an order request. diff --git a/services/skus/service.go b/services/skus/service.go index 96e384ab3..ff2ae02b3 100644 --- a/services/skus/service.go +++ b/services/skus/service.go @@ -2026,6 +2026,8 @@ func (s *Service) createStripeSession(ctx context.Context, req *model.CreateOrde cancelURL: curl, trialDays: order.GetTrialDays(), items: buildStripeLineItems(order.Items), + discounts: buildStripeDiscounts(req.Discounts), + metadata: req.Metadata, } return createStripeSession(ctx, s.stripeCl, sreq) @@ -2815,6 +2817,8 @@ type createStripeSessionRequest struct { cancelURL string trialDays int64 items []*stripe.CheckoutSessionLineItemParams + discounts []*stripe.CheckoutSessionDiscountParams + metadata map[string]string } func createStripeSession(ctx context.Context, cl stripeClient, req createStripeSessionRequest) (string, error) { @@ -2826,6 +2830,7 @@ func createStripeSession(ctx context.Context, cl stripeClient, req createStripeS ClientReferenceID: &req.orderID, SubscriptionData: &stripe.CheckoutSessionSubscriptionDataParams{}, LineItems: req.items, + Discounts: req.discounts, } // Different processes can supply different info about customer: @@ -2850,9 +2855,14 @@ func createStripeSession(ctx context.Context, cl stripeClient, req createStripeS params.SubscriptionData.TrialPeriodDays = &req.trialDays } - params.SubscriptionData.AddMetadata("orderID", req.orderID) params.AddExtra("allow_promotion_codes", "true") + params.SubscriptionData.AddMetadata("orderID", req.orderID) + + for k, v := range req.metadata { + params.SubscriptionData.AddMetadata(k, v) + } + sess, err := cl.CreateSession(ctx, params) if err != nil { return "", err @@ -2879,6 +2889,18 @@ func buildStripeLineItems(items []model.OrderItem) []*stripe.CheckoutSessionLine return result } +func buildStripeDiscounts(discounts []string) []*stripe.CheckoutSessionDiscountParams { + var result []*stripe.CheckoutSessionDiscountParams + + for i := range discounts { + result = append(result, &stripe.CheckoutSessionDiscountParams{ + Coupon: ptrTo(discounts[i]), + }) + } + + return result +} + func handleRedeemFnError(ctx context.Context, w http.ResponseWriter, kind string, cred *cbr.CredentialRedemption, err error) *handlers.AppError { msg := err.Error() diff --git a/services/skus/service_nonint_test.go b/services/skus/service_nonint_test.go index dca5d150d..7fdc1feb1 100644 --- a/services/skus/service_nonint_test.go +++ b/services/skus/service_nonint_test.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "errors" + "fmt" "net/http" "net/http/httptest" "net/url" @@ -4635,6 +4636,61 @@ func TestCreateStripeSession(t *testing.T) { } tests := []testCase{ + { + name: "success_discounts_metadata", + given: tcGiven{ + cl: &xstripe.MockClient{ + FnCreateSession: func(ctx context.Context, params *stripe.CheckoutSessionParams) (*stripe.CheckoutSession, error) { + if len(params.Discounts) != 1 { + return nil, model.Error("unexpected_discounts") + } + + if coup := params.Discounts[0].Coupon; coup == nil || *coup != "coup_id_01" { + return nil, model.Error("unexpected_discount_val") + } + + if val, ok := params.SubscriptionData.Params.Metadata["key_01"]; !ok || val != "val_01" { + fmt.Println(params.SubscriptionData.Metadata) + return nil, model.Error("unexpected_metadata_val") + } + + 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"), + }, + }, + discounts: []*stripe.CheckoutSessionDiscountParams{ + { + Coupon: ptrTo("coup_id_01"), + }, + }, + metadata: map[string]string{ + "key_01": "val_01", + }, + }, + }, + exp: tcExpected{ + val: "cs_test_id", + }, + }, + { name: "success_cust_id", given: tcGiven{ @@ -4954,6 +5010,49 @@ func TestBuildStripeLineItems(t *testing.T) { } } +func TestBuildStripeDiscounts(t *testing.T) { + tests := []struct { + name string + given []string + exp []*stripe.CheckoutSessionDiscountParams + }{ + { + name: "nil", + }, + + { + name: "empty_nil", + given: []string{}, + }, + + { + name: "one", + given: []string{"coup_id_01"}, + exp: []*stripe.CheckoutSessionDiscountParams{ + {Coupon: ptrTo("coup_id_01")}, + }, + }, + + { + name: "two", + given: []string{"coup_id_01", "coup_id_02"}, + exp: []*stripe.CheckoutSessionDiscountParams{ + {Coupon: ptrTo("coup_id_01")}, + {Coupon: ptrTo("coup_id_02")}, + }, + }, + } + + for i := range tests { + tc := tests[i] + + t.Run(tc.name, func(t *testing.T) { + actual := buildStripeDiscounts(tc.given) + should.Equal(t, tc.exp, actual) + }) + } +} + func TestService_createRadomSessID(t *testing.T) { type tcExpected struct { sessionID string