Skip to content

Commit d3e8a22

Browse files
authored
refactor: add support for stipe payment intent succeeded webhook and activate order (#2990)
* refactor: add support for stipe payment intent succeeded webhook and activate order * test: add parse test for stripe payment intent notification * refactor: rename is perpetual license to is one off payment * refactor: remove extra new line
1 parent 39df6b1 commit d3e8a22

File tree

7 files changed

+664
-38
lines changed

7 files changed

+664
-38
lines changed

services/skus/model/model.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ const (
5656
ErrRadomInvalidNumAssocSubs Error = "model: invalid number of associated subs"
5757
ErrRadomSubNotActive Error = "model: sub not active"
5858

59+
ErrOrderNotOneOffPayment = Error("model: order is not perpetual license")
60+
5961
errInvalidNumConversion Error = "model: invalid numeric conversion"
6062
)
6163

@@ -289,6 +291,18 @@ func (o *Order) UpdateCheckoutSessionID(id string) {
289291
o.Metadata["stripeCheckoutSessionId"] = id
290292
}
291293

294+
func (o *Order) IsOneOffPayment() bool {
295+
if o == nil {
296+
return false
297+
}
298+
299+
if len(o.Items) != 1 {
300+
return false
301+
}
302+
303+
return o.Items[0].IsOriginPL()
304+
}
305+
292306
// OrderItem represents a particular order item.
293307
type OrderItem struct {
294308
ID uuid.UUID `json:"id" db:"id"`
@@ -319,6 +333,10 @@ func (x *OrderItem) IsLeo() bool {
319333
return x.SKUVnt == "brave-leo-premium" || x.SKUVnt == "brave-leo-premium-year"
320334
}
321335

336+
func (x *OrderItem) IsOriginPL() bool {
337+
return x.SKUVnt == "brave-origin-premium-perpetual-license"
338+
}
339+
322340
func (x *OrderItem) StripeItemID() (string, bool) {
323341
itemID, ok := x.Metadata["stripe_item_id"].(string)
324342

services/skus/model/model_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1955,6 +1955,88 @@ func TestOrderItem_IsSearchAnnual(t *testing.T) {
19551955
}
19561956
}
19571957

1958+
func TestOrder_IsOneOffPayment(t *testing.T) {
1959+
type tcGiven struct {
1960+
o *model.Order
1961+
}
1962+
1963+
type tcExpected struct {
1964+
result bool
1965+
}
1966+
1967+
type testCase struct {
1968+
name string
1969+
given tcGiven
1970+
exp tcExpected
1971+
}
1972+
1973+
tests := []testCase{
1974+
{
1975+
name: "null_order",
1976+
},
1977+
1978+
{
1979+
name: "no_order_items",
1980+
given: tcGiven{
1981+
&model.Order{},
1982+
},
1983+
},
1984+
1985+
{
1986+
name: "not_perpetual",
1987+
given: tcGiven{
1988+
&model.Order{
1989+
Items: []model.OrderItem{
1990+
{
1991+
SKUVnt: "sku_vnt",
1992+
},
1993+
},
1994+
},
1995+
},
1996+
},
1997+
1998+
{
1999+
name: "not_perpetual_multiple_order_items",
2000+
given: tcGiven{
2001+
&model.Order{
2002+
Items: []model.OrderItem{
2003+
{
2004+
SKUVnt: "sku_vnt",
2005+
},
2006+
2007+
{
2008+
SKUVnt: "brave-origin-premium-perpetual-license",
2009+
},
2010+
},
2011+
},
2012+
},
2013+
},
2014+
2015+
{
2016+
name: "is_perpetual",
2017+
given: tcGiven{
2018+
&model.Order{
2019+
Items: []model.OrderItem{
2020+
{
2021+
SKUVnt: "brave-origin-premium-perpetual-license",
2022+
},
2023+
},
2024+
},
2025+
},
2026+
exp: tcExpected{result: true},
2027+
},
2028+
}
2029+
2030+
for i := range tests {
2031+
tc := tests[i]
2032+
2033+
t.Run(tc.name, func(t *testing.T) {
2034+
actual := tc.given.o.IsOneOffPayment()
2035+
should.Equal(t, tc.exp.result, actual)
2036+
})
2037+
}
2038+
}
2039+
19582040
func ptrTo[T any](v T) *T {
19592041
return &v
19602042
}

services/skus/service.go

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ var (
6464

6565
const (
6666
// TODO(pavelb): Gradually replace it everywhere.
67-
//
6867
// OrderStatusCanceled - string literal used in db for canceled status
6968
OrderStatusCanceled = model.OrderStatusCanceled
7069
// OrderStatusPaid - string literal used in db for canceled status
@@ -768,7 +767,7 @@ func (s *Service) newRadomSub(ctx context.Context, dbi sqlx.ExtContext, oid uuid
768767
return err
769768
}
770769

771-
if err := s.renewOrderWithExpPaidTimeTx(ctx, dbi, oid, expAt, paidAt); err != nil {
770+
if err := s.updateOrderWithExpPaidTimeTx(ctx, dbi, oid, expAt, paidAt); err != nil {
772771
return err
773772
}
774773

@@ -1877,7 +1876,6 @@ func (s *Service) processStripeNotificationTx(ctx context.Context, dbi sqlx.ExtC
18771876
return err
18781877
}
18791878

1880-
// Reset numPaymentFailed.
18811879
if err := s.resetNumPaymentFailed(ctx, dbi, oid); err != nil {
18821880
return err
18831881
}
@@ -1902,6 +1900,38 @@ func (s *Service) processStripeNotificationTx(ctx context.Context, dbi sqlx.ExtC
19021900

19031901
return s.recordPayFailureStripe(ctx, dbi, ord, subID)
19041902

1903+
case ntf.shouldActivatePL():
1904+
oid, err := ntf.orderID()
1905+
if err != nil {
1906+
return err
1907+
}
1908+
1909+
ord, err := s.getOrderFullTx(ctx, dbi, oid)
1910+
if err != nil {
1911+
return err
1912+
}
1913+
1914+
// Currently, we should only receive payment_intent.succeeded events for one-off payments
1915+
// i.e. those related to perpetual licenses. However, to safeguard we should
1916+
// check the order is definitely eligible before activating.
1917+
if !ord.IsOneOffPayment() {
1918+
return model.ErrOrderNotOneOffPayment
1919+
}
1920+
1921+
pid, err := ntf.paymentID()
1922+
if err != nil {
1923+
return err
1924+
}
1925+
1926+
paidt, err := ntf.paidAt()
1927+
if err != nil {
1928+
return err
1929+
}
1930+
1931+
expt := paidt.AddDate(100, 0, 0)
1932+
1933+
return s.activateStripePL(ctx, dbi, ord, pid, paidt, expt)
1934+
19051935
default:
19061936
return nil
19071937
}
@@ -1944,7 +1974,7 @@ func (s *Service) processAppStoreNotificationTx(ctx context.Context, dbi sqlx.Ex
19441974
expt := txn.expiresTime().Add(24 * time.Hour)
19451975
paidt := time.Now()
19461976

1947-
return s.renewOrderWithExpPaidTimeTx(ctx, dbi, ord.ID, expt, paidt)
1977+
return s.updateOrderWithExpPaidTimeTx(ctx, dbi, ord.ID, expt, paidt)
19481978

19491979
case ntf.shouldCancel():
19501980
return s.cancelOrderTx(ctx, dbi, ord.ID)
@@ -1994,7 +2024,7 @@ func (s *Service) processPlayStoreNotificationTx(ctx context.Context, dbi sqlx.E
19942024
expt := sub.expiresTime().Add(24 * time.Hour)
19952025
paidt := time.Now()
19962026

1997-
return s.renewOrderWithExpPaidTimeTx(ctx, dbi, ord.ID, expt, paidt)
2027+
return s.updateOrderWithExpPaidTimeTx(ctx, dbi, ord.ID, expt, paidt)
19982028

19992029
// Sub cancellation.
20002030
case ntf.SubscriptionNtf != nil && ntf.SubscriptionNtf.shouldCancel():
@@ -2293,24 +2323,22 @@ func (s *Service) createOrderTx(ctx context.Context, dbi sqlx.ExtContext, oreq *
22932323
return result, nil
22942324
}
22952325

2296-
func (s *Service) renewOrderWithExpPaidTime(ctx context.Context, id uuid.UUID, expt, paidt time.Time) error {
2326+
func (s *Service) updateOrderWithExpPaidTime(ctx context.Context, id uuid.UUID, expt, paidt time.Time) error {
22972327
tx, err := s.Datastore.RawDB().BeginTxx(ctx, nil)
22982328
if err != nil {
22992329
return err
23002330
}
23012331
defer func() { _ = tx.Rollback() }()
23022332

2303-
if err := s.renewOrderWithExpPaidTimeTx(ctx, tx, id, expt, paidt); err != nil {
2333+
if err := s.updateOrderWithExpPaidTimeTx(ctx, tx, id, expt, paidt); err != nil {
23042334
return err
23052335
}
23062336

23072337
return tx.Commit()
23082338
}
23092339

2310-
// renewOrderWithExpPaidTimeTx performs updates relevant to advancing a paid order forward after renewal.
2311-
//
23122340
// TODO: Add a repo method to update all three fields at once.
2313-
func (s *Service) renewOrderWithExpPaidTimeTx(ctx context.Context, dbi sqlx.ExecerContext, id uuid.UUID, expt, paidt time.Time) error {
2341+
func (s *Service) updateOrderWithExpPaidTimeTx(ctx context.Context, dbi sqlx.ExecerContext, id uuid.UUID, expt, paidt time.Time) error {
23142342
if err := s.orderRepo.SetStatus(ctx, dbi, id, model.OrderStatusPaid); err != nil {
23152343
return err
23162344
}
@@ -2416,7 +2444,7 @@ func (s *Service) createOrderWithReceipt(ctx context.Context, req model.ReceiptR
24162444
// Examples:
24172445
// - annual VPN users on mobile pre-July 2024;
24182446
// - mobile Developers and QAs using the same store id repeatedly.
2419-
if err := s.renewOrderWithExpPaidTime(ctx, ord.ID, rcpt.ExpiresAt, paidt); err != nil {
2447+
if err := s.updateOrderWithExpPaidTime(ctx, ord.ID, rcpt.ExpiresAt, paidt); err != nil {
24202448
return nil, err
24212449
}
24222450

@@ -2468,7 +2496,7 @@ func (s *Service) processSubmitReceipt(ctx context.Context, req model.ReceiptReq
24682496

24692497
paidt := time.Now()
24702498

2471-
if err := s.renewOrderWithExpPaidTimeTx(ctx, tx, oid, rcpt.ExpiresAt, paidt); err != nil {
2499+
if err := s.updateOrderWithExpPaidTimeTx(ctx, tx, oid, rcpt.ExpiresAt, paidt); err != nil {
24722500
return model.ReceiptData{}, err
24732501
}
24742502

@@ -2552,7 +2580,7 @@ func (s *Service) processRadomNotificationTx(ctx context.Context, dbi sqlx.ExtCo
25522580
return err
25532581
}
25542582

2555-
if err := s.renewOrderWithExpPaidTimeTx(ctx, dbi, ord.ID, expAt, paidAt); err != nil {
2583+
if err := s.updateOrderWithExpPaidTimeTx(ctx, dbi, ord.ID, expAt, paidAt); err != nil {
25562584
return err
25572585
}
25582586

@@ -2611,7 +2639,7 @@ func checkOrderReceipt(ctx context.Context, dbi sqlx.QueryerContext, repo orderS
26112639
// This interface exists because in its current form Service is hardly testable.
26122640
type paidOrderCreator interface {
26132641
createOrderPremium(ctx context.Context, req *model.CreateOrderRequestNew, ordNew *model.OrderNew, items []model.OrderItem) (*model.Order, error)
2614-
renewOrderWithExpPaidTime(ctx context.Context, id uuid.UUID, expt, paidt time.Time) error
2642+
updateOrderWithExpPaidTime(ctx context.Context, id uuid.UUID, expt, paidt time.Time) error
26152643
appendOrderMetadata(ctx context.Context, oid uuid.UUID, mdata datastore.Metadata) error
26162644
}
26172645

@@ -2662,7 +2690,7 @@ func createOrderWithReceipt(
26622690
}
26632691

26642692
// 4. Mark order as paid with proper expiration.
2665-
if err := svc.renewOrderWithExpPaidTime(ctx, order.ID, rcpt.ExpiresAt, paidt); err != nil {
2693+
if err := svc.updateOrderWithExpPaidTime(ctx, order.ID, rcpt.ExpiresAt, paidt); err != nil {
26662694
return nil, err
26672695
}
26682696

@@ -2708,7 +2736,7 @@ func (s *Service) renewOrderStripe(ctx context.Context, dbi sqlx.ExecerContext,
27082736
// Add 1-day leeway in case next billing cycle's webhook gets delayed.
27092737
expt = expt.Add(24 * time.Hour)
27102738

2711-
if err := s.renewOrderWithExpPaidTimeTx(ctx, dbi, ord.ID, expt, paidt); err != nil {
2739+
if err := s.updateOrderWithExpPaidTimeTx(ctx, dbi, ord.ID, expt, paidt); err != nil {
27122740
return err
27132741
}
27142742

@@ -2725,6 +2753,18 @@ func (s *Service) renewOrderStripe(ctx context.Context, dbi sqlx.ExecerContext,
27252753
return s.orderRepo.AppendMetadata(ctx, dbi, ord.ID, "paymentProcessor", model.StripePaymentMethod)
27262754
}
27272755

2756+
func (s *Service) activateStripePL(ctx context.Context, dbi sqlx.ExecerContext, ord *model.Order, paymentID string, paidt, expt time.Time) error {
2757+
if err := s.updateOrderWithExpPaidTimeTx(ctx, dbi, ord.ID, expt, paidt); err != nil {
2758+
return err
2759+
}
2760+
2761+
if err := s.orderRepo.AppendMetadata(ctx, dbi, ord.ID, "paymentProcessor", model.StripePaymentMethod); err != nil {
2762+
return err
2763+
}
2764+
2765+
return s.orderRepo.AppendMetadata(ctx, dbi, ord.ID, "stripePaymentId", paymentID)
2766+
}
2767+
27282768
func (s *Service) processStripeMtoA(ctx context.Context, dbi sqlx.ExtContext, ntf *stripeNotification) error {
27292769
umaData, err := ntf.umaData()
27302770
if err != nil {

0 commit comments

Comments
 (0)