Skip to content

Commit

Permalink
feat: add initial support for passing stripe discounts
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelbrm committed Dec 13, 2024
1 parent 33f7a36 commit c0b981f
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 1 deletion.
126 changes: 126 additions & 0 deletions services/skus/handler/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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": "[email protected]",
"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 {
Expand Down
2 changes: 2 additions & 0 deletions services/skus/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 23 additions & 1 deletion services/skus/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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()

Expand Down
99 changes: 99 additions & 0 deletions services/skus/service_nonint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit c0b981f

Please sign in to comment.