Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for coupons when getting trial info #4649

Merged
merged 2 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions api/server/handlers/billing/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ func (c *ListPlansHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
telemetry.AttributeKV{Key: "subscription_id", Value: plan.ID},
)

endingBefore, err := c.Config().BillingManager.LagoClient.CheckCustomerCouponExpiration(ctx, proj.ID, proj.EnableSandbox)
if err != nil {
err := telemetry.Error(ctx, span, err, "error listing active coupons")
c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
return
}

// If the customer has a coupon, use its end date instead of the trial end date
if endingBefore != "" {
plan.TrialInfo.EndingBefore = endingBefore
}

telemetry.WithAttributes(span,
telemetry.AttributeKV{Key: "trial-ending-at", Value: plan.TrialInfo.EndingBefore},
)

c.WriteResult(w, r, plan)
}

Expand Down
8 changes: 8 additions & 0 deletions api/types/billing_usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ type BillingEvent struct {
Timestamp string `json:"timestamp"`
}

// AppliedCoupon represents an applied coupon in the billing system.
type AppliedCoupon struct {
Status string `json:"status"`
FrequencyDuration int `json:"frequency_duration"`
FrequencyDurationRemaining int `json:"frequency_duration_remaining"`
CreatedAt string `json:"created_at"`
}

// Wallet represents a customer credits wallet
type Wallet struct {
LagoID uuid.UUID `json:"lago_id,omitempty"`
Expand Down
62 changes: 61 additions & 1 deletion internal/billing/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,29 @@ func (m LagoClient) ListCustomerCredits(ctx context.Context, projectID uint, san
return response, nil
}

// CheckCustomerCouponExpiration will return the expiration date of the customer's coupon
func (m LagoClient) CheckCustomerCouponExpiration(ctx context.Context, projectID uint, sandboxEnabled bool) (trialEndDate string, err error) {
ctx, span := telemetry.NewSpan(ctx, "list-customer-coupons")
defer span.End()

if projectID == 0 {
return trialEndDate, telemetry.Error(ctx, span, err, "project id empty")
}
customerID := m.generateLagoID(CustomerIDPrefix, projectID, sandboxEnabled)
couponList, err := m.listCustomerAppliedCoupons(ctx, customerID)
if err != nil {
return trialEndDate, telemetry.Error(ctx, span, err, "failed to list customer coupons")
}

if len(couponList) == 0 {
return trialEndDate, nil
}

appliedCoupon := couponList[0]
trialEndDate = time.Now().UTC().AddDate(0, appliedCoupon.FrequencyDurationRemaining, 0).Format(time.RFC3339)
return trialEndDate, nil
}

// CreateCreditsGrant will create a new credit grant for the customer with the specified amount
func (m LagoClient) CreateCreditsGrant(ctx context.Context, projectID uint, name string, grantAmount int64, expiresAt *time.Time, sandboxEnabled bool) (err error) {
ctx, span := telemetry.NewSpan(ctx, "create-credits-grant")
Expand Down Expand Up @@ -498,7 +521,7 @@ func (m LagoClient) listCustomerWallets(ctx context.Context, customerID string)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return walletList, telemetry.Error(ctx, span, err, "failed to get customer credits")
return walletList, telemetry.Error(ctx, span, err, "failed to get customer wallets")
}

response := struct {
Expand All @@ -518,6 +541,43 @@ func (m LagoClient) listCustomerWallets(ctx context.Context, customerID string)
return response.Wallets, nil
}

func (m LagoClient) listCustomerAppliedCoupons(ctx context.Context, customerID string) (couponList []types.AppliedCoupon, err error) {
ctx, span := telemetry.NewSpan(ctx, "list-lago-customer-coupons")
defer span.End()

// We manually do the request in this function because the Lago client has an issue
// with types for this specific request
url := fmt.Sprintf("%s/api/v1/applied_coupons?external_customer_id=%s&status=%s", lagoBaseURL, customerID, lago.AppliedCouponStatusActive)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return couponList, telemetry.Error(ctx, span, err, "failed to create coupons list request")
}

req.Header.Set("Authorization", "Bearer "+m.lagoApiKey)

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return couponList, telemetry.Error(ctx, span, err, "failed to get customer coupons")
}

response := struct {
AppliedCoupons []types.AppliedCoupon `json:"applied_coupons"`
}{}

err = json.NewDecoder(resp.Body).Decode(&response)
if err != nil {
return couponList, telemetry.Error(ctx, span, err, "failed to decode coupons list response")
}

err = resp.Body.Close()
if err != nil {
return couponList, telemetry.Error(ctx, span, err, "failed to close response body")
}

return response.AppliedCoupons, nil
}

func createUsageFromLagoUsage(lagoUsage lago.CustomerUsage) types.Usage {
usage := types.Usage{}
usage.FromDatetime = lagoUsage.FromDatetime.Format(time.RFC3339)
Expand Down
Loading