Skip to content

Commit f182987

Browse files
authored
feat: allow adding user id while reporting usage (#538)
Signed-off-by: Kush Sharma <thekushsharma@gmail.com>
1 parent 893b8a3 commit f182987

18 files changed

+457
-223
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ TAG := $(shell git rev-list --tags --max-count=1)
44
VERSION := $(shell git describe --tags ${TAG})
55
.PHONY: build check fmt lint test test-race vet test-cover-html help install proto ui
66
.DEFAULT_GOAL := build
7-
PROTON_COMMIT := "f15c96f0a0ba9b834164b2bcf0d5ce4199a87ee5"
7+
PROTON_COMMIT := "7b7b4b803e0e030782f9203acf5c3687148f6bab"
88

99
ui:
1010
@echo " > generating ui build"

billing/checkout/service.go

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"text/template"
1111
"time"
1212

13+
"github.com/raystack/frontier/core/authenticate"
14+
1315
"github.com/robfig/cron/v3"
1416

1517
"github.com/raystack/frontier/billing/credit"
@@ -35,6 +37,7 @@ const (
3537
AmountSubscriptionMetadataKey = "amount_total"
3638
CurrencySubscriptionMetadataKey = "currency"
3739
ProviderIDSubscriptionMetadataKey = "provider_subscription_id"
40+
InitiatorIDMetadataKey = "initiated_by"
3841
)
3942

4043
type Repository interface {
@@ -73,6 +76,10 @@ type OrganizationService interface {
7376
MemberCount(ctx context.Context, orgID string) (int64, error)
7477
}
7578

79+
type AuthnService interface {
80+
GetPrincipal(ctx context.Context, assertions ...authenticate.ClientAssertion) (authenticate.Principal, error)
81+
}
82+
7683
type Service struct {
7784
stripeAutoTax bool
7885
stripeClient *client.API
@@ -83,6 +90,7 @@ type Service struct {
8390
creditService CreditService
8491
productService ProductService
8592
orgService OrganizationService
93+
authnService AuthnService
8694

8795
syncLimiter *debounce.Limiter
8896
syncJob *cron.Cron
@@ -92,7 +100,8 @@ type Service struct {
92100
func NewService(stripeClient *client.API, stripeAutoTax bool, repository Repository,
93101
customerService CustomerService, planService PlanService,
94102
subscriptionService SubscriptionService, productService ProductService,
95-
creditService CreditService, orgService OrganizationService) *Service {
103+
creditService CreditService, orgService OrganizationService,
104+
authnService AuthnService) *Service {
96105
s := &Service{
97106
stripeClient: stripeClient,
98107
stripeAutoTax: stripeAutoTax,
@@ -103,6 +112,7 @@ func NewService(stripeClient *client.API, stripeAutoTax bool, repository Reposit
103112
creditService: creditService,
104113
productService: productService,
105114
orgService: orgService,
115+
authnService: authnService,
106116
syncLimiter: debounce.New(2 * time.Second),
107117
}
108118
return s
@@ -159,6 +169,11 @@ func (s *Service) Create(ctx context.Context, ch Checkout) (Checkout, error) {
159169
return Checkout{}, err
160170
}
161171

172+
currentPrincipal, err := s.authnService.GetPrincipal(ctx)
173+
if err != nil {
174+
return Checkout{}, err
175+
}
176+
162177
// checkout could be for a plan or a product
163178
if ch.PlanID != "" {
164179
plan, err := s.planService.GetByID(ctx, ch.PlanID)
@@ -218,7 +233,7 @@ func (s *Service) Create(ctx context.Context, ch Checkout) (Checkout, error) {
218233

219234
var trialDays *int64 = nil
220235
// if trial is enabled and user has not trialed before, set trial days
221-
userHasTrialedBefore, err := s.hasUserTrialedBefore(ctx, billingCustomer.ID, plan.ID)
236+
userHasTrialedBefore, err := s.hasUserSubscribedBefore(ctx, billingCustomer.ID, plan.ID)
222237
if err != nil {
223238
return Checkout{}, err
224239
}
@@ -238,18 +253,20 @@ func (s *Service) Create(ctx context.Context, ch Checkout) (Checkout, error) {
238253
Customer: stripe.String(billingCustomer.ProviderID),
239254
LineItems: subsItems,
240255
Metadata: map[string]string{
241-
"org_id": billingCustomer.OrgID,
242-
"plan_id": ch.PlanID,
243-
"checkout_id": checkoutID,
244-
"managed_by": "frontier",
256+
"org_id": billingCustomer.OrgID,
257+
"plan_id": ch.PlanID,
258+
"checkout_id": checkoutID,
259+
InitiatorIDMetadataKey: currentPrincipal.ID,
260+
"managed_by": "frontier",
245261
},
246262
Mode: stripe.String(string(stripe.CheckoutSessionModeSubscription)),
247263
SubscriptionData: &stripe.CheckoutSessionSubscriptionDataParams{
248264
Description: stripe.String(fmt.Sprintf("Checkout for %s", plan.Name)),
249265
Metadata: map[string]string{
250-
"org_id": billingCustomer.OrgID,
251-
"checkout_id": checkoutID,
252-
"managed_by": "frontier",
266+
"org_id": billingCustomer.OrgID,
267+
"checkout_id": checkoutID,
268+
subscription.InitiatorIDMetadataKey: currentPrincipal.ID,
269+
"managed_by": "frontier",
253270
},
254271
TrialPeriodDays: trialDays,
255272
TrialSettings: &stripe.CheckoutSessionSubscriptionDataTrialSettingsParams{
@@ -320,11 +337,12 @@ func (s *Service) Create(ctx context.Context, ch Checkout) (Checkout, error) {
320337
LineItems: subsItems,
321338
Mode: stripe.String(string(stripe.CheckoutSessionModePayment)),
322339
Metadata: map[string]string{
323-
"org_id": billingCustomer.OrgID,
324-
"product_name": chProduct.Name,
325-
"credit_amount": fmt.Sprintf("%d", chProduct.Config.CreditAmount),
326-
"checkout_id": checkoutID,
327-
"managed_by": "frontier",
340+
"org_id": billingCustomer.OrgID,
341+
"product_name": chProduct.Name,
342+
"credit_amount": fmt.Sprintf("%d", chProduct.Config.CreditAmount),
343+
"checkout_id": checkoutID,
344+
InitiatorIDMetadataKey: currentPrincipal.ID,
345+
"managed_by": "frontier",
328346
},
329347
CancelURL: stripe.String(ch.CancelUrl),
330348
SuccessURL: stripe.String(ch.SuccessUrl),
@@ -385,16 +403,25 @@ func (s *Service) templatizeUrls(ch Checkout, checkoutID string) (Checkout, erro
385403
return ch, nil
386404
}
387405

388-
func (s *Service) hasUserTrialedBefore(ctx context.Context, customerID string, planID string) (bool, error) {
406+
func (s *Service) hasUserSubscribedBefore(ctx context.Context, customerID string, planID string) (bool, error) {
389407
subs, err := s.subscriptionService.List(ctx, subscription.Filter{
390408
CustomerID: customerID,
391-
PlanID: planID,
392409
})
393410
if err != nil {
394411
return false, err
395412
}
396413
for _, sub := range subs {
397-
if sub.TrialEndsAt.Unix() > 0 {
414+
isPlanUsedBefore := false
415+
if sub.PlanID == planID {
416+
isPlanUsedBefore = true
417+
}
418+
for _, history := range sub.PlanHistory {
419+
if history.PlanID == planID {
420+
isPlanUsedBefore = true
421+
}
422+
}
423+
424+
if isPlanUsedBefore {
398425
return true, nil
399426
}
400427
}
@@ -489,12 +516,18 @@ func (s *Service) ensureCreditsForProduct(ctx context.Context, ch Checkout) erro
489516
description = fmt.Sprintf("addition of %d credits for %s at %d[%s]", chProduct.Config.CreditAmount, chProduct.Title, price, currency)
490517
}
491518
}
519+
initiatorID := ""
520+
if id, ok := ch.Metadata[InitiatorIDMetadataKey].(string); ok {
521+
initiatorID = id
522+
}
492523
if err := s.creditService.Add(ctx, credit.Credit{
493524
ID: ch.ID,
494525
AccountID: ch.CustomerID,
495526
Amount: chProduct.Config.CreditAmount,
496527
Metadata: ch.Metadata,
497528
Description: description,
529+
Source: credit.SourceSystemBuyEvent,
530+
UserID: initiatorID,
498531
}); err != nil && !errors.Is(err, credit.ErrAlreadyApplied) {
499532
return err
500533
}

billing/credit/credit.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ var (
1919

2020
// TxNamespaceUUID is the namespace for generating transaction UUIDs deterministically
2121
TxNamespaceUUID = uuid.MustParse("967416d0-716e-4308-b58f-2468ac14f20a")
22+
23+
SourceSystemBuyEvent = "system.buy"
24+
SourceSystemOnboardEvent = "system.starter"
2225
)
2326

2427
type TransactionType string
@@ -29,21 +32,29 @@ const (
2932
)
3033

3134
type Transaction struct {
32-
ID string
33-
AccountID string
34-
Amount int64
35-
Type TransactionType
35+
ID string
36+
AccountID string
37+
Amount int64
38+
Type TransactionType
39+
40+
// Source is the source app or event that caused the transaction
3641
Source string
3742
Description string
38-
Metadata metadata.Metadata
39-
CreatedAt time.Time
40-
UpdatedAt time.Time
43+
44+
// UserID is the user who initiated the transaction
45+
UserID string
46+
47+
Metadata metadata.Metadata
48+
CreatedAt time.Time
49+
UpdatedAt time.Time
4150
}
4251

4352
type Credit struct {
4453
ID string
4554
AccountID string
4655
Amount int64
56+
UserID string
57+
Source string
4758
Description string
4859

4960
Metadata metadata.Metadata

billing/credit/service.go

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,32 +26,34 @@ func NewService(repository TransactionRepository) *Service {
2626
}
2727

2828
func (s Service) Add(ctx context.Context, cred Credit) error {
29+
if cred.ID == "" {
30+
return fmt.Errorf("credit id is empty, it is required to create a transaction")
31+
}
2932
// check if already credited
3033
if t, err := s.transactionRepository.GetByID(ctx, cred.ID); err == nil && t.ID != "" {
3134
return ErrAlreadyApplied
3235
}
33-
34-
if cred.ID == "" {
35-
return fmt.Errorf("credit id is empty, it is required to create a transaction")
36-
}
37-
description := cred.Description
38-
if description == "" {
39-
description = "addition of credits"
36+
txSource := "system"
37+
if cred.Source != "" {
38+
txSource = cred.Source
4039
}
40+
4141
_, err := s.transactionRepository.CreateEntry(ctx, Transaction{
4242
AccountID: schema.PlatformOrgID.String(),
4343
Type: TypeDebit,
4444
Amount: cred.Amount,
45-
Description: description,
46-
Source: "system",
45+
Description: cred.Description,
46+
Source: txSource,
47+
UserID: cred.UserID,
4748
Metadata: cred.Metadata,
4849
}, Transaction{
4950
ID: cred.ID,
5051
Type: TypeCredit,
5152
AccountID: cred.AccountID,
5253
Amount: cred.Amount,
53-
Description: description,
54-
Source: "system",
54+
Description: cred.Description,
55+
Source: txSource,
56+
UserID: cred.UserID,
5557
Metadata: cred.Metadata,
5658
})
5759
if err != nil {
@@ -64,7 +66,7 @@ func (s Service) Deduct(ctx context.Context, u usage.Usage) error {
6466
if u.ID == "" {
6567
return fmt.Errorf("usage id is empty, it is required to create a transaction")
6668
}
67-
if u.Type != usage.TypeCredit {
69+
if u.Type != usage.CreditType {
6870
return fmt.Errorf("usage is not of credit type")
6971
}
7072

@@ -77,25 +79,27 @@ func (s Service) Deduct(ctx context.Context, u usage.Usage) error {
7779
return ErrNotEnough
7880
}
7981

80-
description := u.Description
81-
if description == "" {
82-
description = "utilization of credits"
82+
txSource := "system"
83+
if u.Source != "" {
84+
txSource = u.Source
8385
}
8486

8587
if _, err := s.transactionRepository.CreateEntry(ctx, Transaction{
8688
ID: u.ID,
8789
AccountID: u.CustomerID,
8890
Type: TypeDebit,
8991
Amount: u.Amount,
90-
Description: description,
91-
Source: u.Source,
92+
Description: u.Description,
93+
Source: txSource,
94+
UserID: u.UserID,
9295
Metadata: u.Metadata,
9396
}, Transaction{
9497
Type: TypeCredit,
9598
AccountID: schema.PlatformOrgID.String(),
9699
Amount: u.Amount,
97-
Description: description,
98-
Source: u.Source,
100+
Description: u.Description,
101+
Source: txSource,
102+
UserID: u.UserID,
99103
Metadata: u.Metadata,
100104
}); err != nil {
101105
return fmt.Errorf("failed to sub credits: %w", err)

billing/subscription/service.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import (
3333

3434
const (
3535
SyncDelay = time.Second * 60
36+
37+
InitiatorIDMetadataKey = "initiated_by"
3638
)
3739

3840
type Repository interface {
@@ -217,6 +219,14 @@ func (s *Service) SyncWithProvider(ctx context.Context, customr customer.Custome
217219

218220
if sub.PlanID != currentPlanID {
219221
sub.PlanID = currentPlanID
222+
223+
// update plan history
224+
if sub.PlanID != "" {
225+
sub.PlanHistory = append(sub.PlanHistory, Phase{
226+
EndsAt: time.Now().UTC(),
227+
PlanID: sub.PlanID,
228+
})
229+
}
220230
updateNeeded = true
221231
}
222232

@@ -258,7 +268,7 @@ func (s *Service) SyncWithProvider(ctx context.Context, customr customer.Custome
258268
}
259269

260270
// subscription can also be complimented with free credits
261-
if err := s.ensureCreditsForPlan(ctx, customr.ID, subPlan); err != nil {
271+
if err := s.ensureCreditsForPlan(ctx, sub, subPlan); err != nil {
262272
return fmt.Errorf("ensureCreditsForPlan: %w", err)
263273
}
264274
}
@@ -986,7 +996,8 @@ func (s *Service) findPlanByStripePhase(ctx context.Context, stripePhase *stripe
986996
return plans[0], nil
987997
}
988998

989-
func (s *Service) ensureCreditsForPlan(ctx context.Context, customerID string, subPlan plan.Plan) error {
999+
func (s *Service) ensureCreditsForPlan(ctx context.Context, sub Subscription, subPlan plan.Plan) error {
1000+
customerID := sub.CustomerID
9901001
txID := uuid.NewSHA1(credit.TxNamespaceUUID, []byte(fmt.Sprintf("%s:%s", subPlan.ID, customerID))).String()
9911002
if subPlan.OnStartCredits == 0 {
9921003
// no such product
@@ -1000,13 +1011,20 @@ func (s *Service) ensureCreditsForPlan(ctx context.Context, customerID string, s
10001011
return nil
10011012
}
10021013

1014+
initiatorID := ""
1015+
if id, ok := sub.Metadata[InitiatorIDMetadataKey].(string); ok {
1016+
initiatorID = id
1017+
}
1018+
10031019
description := fmt.Sprintf("addition of %d credits for %s", subPlan.OnStartCredits, subPlan.Title)
10041020
if err := s.creditService.Add(ctx, credit.Credit{
10051021
ID: txID,
10061022
AccountID: customerID,
10071023
Amount: subPlan.OnStartCredits,
1024+
Source: credit.SourceSystemOnboardEvent,
10081025
Metadata: subPlan.Metadata,
10091026
Description: description,
1027+
UserID: initiatorID,
10101028
}); err != nil && !errors.Is(err, credit.ErrAlreadyApplied) {
10111029
return err
10121030
}

billing/subscription/subscription.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const (
3030

3131
type Phase struct {
3232
EffectiveAt time.Time
33+
EndsAt time.Time
3334
PlanID string
3435
}
3536

@@ -50,7 +51,8 @@ type Subscription struct {
5051

5152
Metadata metadata.Metadata
5253

53-
Phase Phase
54+
Phase Phase
55+
PlanHistory []Phase
5456

5557
CreatedAt time.Time
5658
UpdatedAt time.Time

0 commit comments

Comments
 (0)