Skip to content

Commit

Permalink
fix: set email when creating new session for trial days (#2710)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelbrm authored Nov 19, 2024
1 parent 3d48415 commit 36136d6
Show file tree
Hide file tree
Showing 11 changed files with 560 additions and 157 deletions.
16 changes: 5 additions & 11 deletions services/skus/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"os"
"strconv"
"time"

"github.com/asaskevich/govalidator"
"github.com/go-chi/chi"
Expand Down Expand Up @@ -310,11 +311,6 @@ func VoteRouter(service *Service, instrumentHandler middleware.InstrumentHandler
return r
}

type setTrialDaysRequest struct {
TrialDays int64 `json:"trialDays"`
}

// TODO: refactor this to avoid multiple fetches of an order.
func handleSetOrderTrialDays(svc *Service) handlers.AppHandler {
return handlers.AppHandler(func(w http.ResponseWriter, r *http.Request) *handlers.AppError {
ctx := r.Context()
Expand All @@ -324,21 +320,19 @@ func handleSetOrderTrialDays(svc *Service) handlers.AppHandler {
return handlers.ValidationError("request", map[string]interface{}{"orderID": err.Error()})
}

if err := svc.validateOrderMerchantAndCaveats(ctx, orderID); err != nil {
return handlers.ValidationError("merchant and caveats", map[string]interface{}{"orderMerchantAndCaveats": err.Error()})
}

data, err := io.ReadAll(io.LimitReader(r.Body, reqBodyLimit10MB))
if err != nil {
return handlers.WrapError(err, "failed to read request body", http.StatusBadRequest)
}

req := &setTrialDaysRequest{}
req := &model.SetTrialDaysRequest{}
if err := json.Unmarshal(data, req); err != nil {
return handlers.WrapError(err, "failed to parse request", http.StatusBadRequest)
}

if err := svc.SetOrderTrialDays(ctx, &orderID, req.TrialDays); err != nil {
now := time.Now().UTC()

if err := svc.setOrderTrialDays(ctx, orderID, req, now); err != nil {
return handlers.WrapError(err, "Error setting the trial days on the order", http.StatusInternalServerError)
}

Expand Down
29 changes: 1 addition & 28 deletions services/skus/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ type Datastore interface {
datastore.Datastore

CreateOrder(ctx context.Context, dbi sqlx.ExtContext, oreq *model.OrderNew, items []model.OrderItem) (*model.Order, error)
// SetOrderTrialDays - set the number of days of free trial for this order
SetOrderTrialDays(ctx context.Context, orderID *uuid.UUID, days int64) (*Order, error)

// GetOrder by ID
GetOrder(orderID uuid.UUID) (*Order, error)
// GetOrderByExternalID by the external id from the purchase vendor
Expand Down Expand Up @@ -99,7 +98,6 @@ type orderStore interface {
GetByExternalID(ctx context.Context, dbi sqlx.QueryerContext, extID string) (*model.Order, error)
Create(ctx context.Context, dbi sqlx.QueryerContext, oreq *model.OrderNew) (*model.Order, error)
SetLastPaidAt(ctx context.Context, dbi sqlx.ExecerContext, id uuid.UUID, when time.Time) error
SetTrialDays(ctx context.Context, dbi sqlx.QueryerContext, id uuid.UUID, ndays int64) (*model.Order, error)
SetStatus(ctx context.Context, dbi sqlx.ExecerContext, id uuid.UUID, status string) error
GetExpiresAtAfterISOPeriod(ctx context.Context, dbi sqlx.QueryerContext, id uuid.UUID) (time.Time, error)
SetExpiresAt(ctx context.Context, dbi sqlx.ExecerContext, id uuid.UUID, when time.Time) error
Expand Down Expand Up @@ -265,31 +263,6 @@ func (pg *Postgres) GetKey(id uuid.UUID, showExpired bool) (*Key, error) {
return &key, nil
}

// SetOrderTrialDays sets the number of days of free trial for this order and returns the updated result.
func (pg *Postgres) SetOrderTrialDays(ctx context.Context, orderID *uuid.UUID, days int64) (*Order, error) {
tx, err := pg.RawDB().BeginTxx(ctx, nil)
if err != nil {
return nil, fmt.Errorf("failed to create db tx: %w", err)
}
defer pg.RollbackTx(tx)

result, err := pg.orderRepo.SetTrialDays(ctx, tx, *orderID, days)
if err != nil {
return nil, fmt.Errorf("failed to execute tx: %w", err)
}

result.Items, err = pg.orderItemRepo.FindByOrderID(ctx, tx, *orderID)
if err != nil {
return nil, err
}

if err := tx.Commit(); err != nil {
return nil, err
}

return result, nil
}

// CreateOrder creates orders for Auto Contribute and Search Captcha.
//
// Deprecated: This method MUST NOT be used for Premium orders.
Expand Down
14 changes: 0 additions & 14 deletions services/skus/instrumented_datastore.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 0 additions & 30 deletions services/skus/mockdatastore.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 17 additions & 3 deletions services/skus/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,21 @@ func (o *Order) IsRadomPayable() bool {
return Slice[string](o.AllowedPaymentMethods).Contains(RadomPaymentMethod)
}

func (o *Order) ShouldSetTrialDays() bool {
return !o.IsPaid() && o.IsStripePayable()
func (o *Order) ShouldCreateTrialSessionStripe(now time.Time) bool {
return !o.IsPaidAt(now) && o.IsStripePayable()
}

// IsPaid returns true if the order is paid.
//
// TODO: Update all callers of the method to pass time explicitly.
func (o *Order) IsPaid() bool {
return o.IsPaidAt(time.Now())
}

// IsPaidAt returns true if the order is paid.
//
// If canceled, it checks if expires_at is in the future.
func (o *Order) IsPaidAt(now time.Time) bool {
switch o.Status {
case OrderStatusPaid:
// The order is paid if the status is paid.
Expand All @@ -134,7 +143,7 @@ func (o *Order) IsPaid() bool {
return false
}

return o.ExpiresAt.After(time.Now())
return o.ExpiresAt.After(now)
default:
return false
}
Expand Down Expand Up @@ -731,6 +740,11 @@ type VerifyCredentialOpaque struct {
Version float64 `json:"version" validate:"-"`
}

type SetTrialDaysRequest struct {
Email string `json:"email"` // TODO: Make it required.
TrialDays int64 `json:"trialDays"`
}

func addURLParam(src, name, val string) (string, error) {
raw, err := url.Parse(src)
if err != nil {
Expand Down
137 changes: 114 additions & 23 deletions services/skus/model/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"testing"
"time"

"github.com/lib/pq"
uuid "github.com/satori/go.uuid"
Expand Down Expand Up @@ -1157,63 +1158,153 @@ func TestOrder_Vendor(t *testing.T) {
}
}

func TestOrder_ShouldSetTrialDays(t *testing.T) {
func TestOrder_ShouldCreateTrialSessionStripe(t *testing.T) {
type tcGiven struct {
ord *model.Order
now time.Time
}

type testCase struct {
name string
given model.Order
given tcGiven
exp bool
}

tests := []testCase{
{
name: "not_paid",
given: model.Order{Status: model.OrderStatusPending},
name: "false_paid_not_stripe",
given: tcGiven{
ord: &model.Order{
Status: model.OrderStatusPaid,
AllowedPaymentMethods: pq.StringArray{"radom"},
},
},
},

{
name: "not_paid_not_stripe",
given: model.Order{
Status: model.OrderStatusPending,
AllowedPaymentMethods: pq.StringArray{"something"},
name: "false_not_paid_not_stripe",
given: tcGiven{
ord: &model.Order{
Status: model.OrderStatusPending,
AllowedPaymentMethods: pq.StringArray{"radom"},
},
},
},

{
name: "paid",
given: model.Order{Status: model.OrderStatusPaid},
name: "false_paid_stripe",
given: tcGiven{
ord: &model.Order{
Status: model.OrderStatusPaid,
AllowedPaymentMethods: pq.StringArray{"stripe"},
},
},
},

{
name: "paid_not_stripe",
given: model.Order{
Status: model.OrderStatusPaid,
AllowedPaymentMethods: pq.StringArray{"something"},
name: "false_canceled_not_expired_stripe",
given: tcGiven{
ord: &model.Order{
Status: model.OrderStatusPaid,
AllowedPaymentMethods: pq.StringArray{"stripe"},
ExpiresAt: ptrTo(time.Date(2024, time.November, 1, 0, 0, 0, 0, time.UTC)),
},
now: time.Date(2024, time.October, 1, 0, 0, 0, 0, time.UTC),
},
},

{
name: "paid_stripe",
given: model.Order{
Status: model.OrderStatusPaid,
AllowedPaymentMethods: pq.StringArray{"stripe"},
name: "true_pending_stripe",
given: tcGiven{
ord: &model.Order{
Status: model.OrderStatusPending,
AllowedPaymentMethods: pq.StringArray{"stripe"},
},
},
exp: true,
},
}

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

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

func TestOrder_IsPaidAt(t *testing.T) {
type tcGiven struct {
ord *model.Order
now time.Time
}

type testCase struct {
name string
given tcGiven
exp bool
}

tests := []testCase{
{
name: "not_paid_stripe",
given: model.Order{
Status: model.OrderStatusPending,
AllowedPaymentMethods: pq.StringArray{"stripe"},
name: "true_paid",
given: tcGiven{
ord: &model.Order{
Status: model.OrderStatusPaid,
},
},
exp: true,
},

{
name: "false_canceled_no_expiry",
given: tcGiven{
ord: &model.Order{
Status: model.OrderStatusCanceled,
},
},
},

{
name: "true_canceled_expires_later",
given: tcGiven{
ord: &model.Order{
Status: model.OrderStatusCanceled,
ExpiresAt: ptrTo(time.Date(2024, time.November, 1, 0, 0, 0, 0, time.UTC)),
},
now: time.Date(2024, time.October, 1, 0, 0, 0, 0, time.UTC),
},
exp: true,
},

{
name: "false_canceled_expired",
given: tcGiven{
ord: &model.Order{
Status: model.OrderStatusCanceled,
ExpiresAt: ptrTo(time.Date(2024, time.November, 1, 0, 0, 0, 0, time.UTC)),
},
now: time.Date(2024, time.December, 1, 0, 0, 0, 0, time.UTC),
},
},

{
name: "false_pending",
given: tcGiven{
ord: &model.Order{
Status: model.OrderStatusPending,
},
},
},
}

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

t.Run(tc.name, func(t *testing.T) {
actual := tc.given.ShouldSetTrialDays()
actual := tc.given.ord.IsPaidAt(tc.given.now)
should.Equal(t, tc.exp, actual)
})
}
Expand Down
Loading

0 comments on commit 36136d6

Please sign in to comment.