|
|
@@ -3,6 +3,7 @@ package billing
|
|
|
import (
|
|
|
"context"
|
|
|
"fmt"
|
|
|
+ "log"
|
|
|
"strconv"
|
|
|
"time"
|
|
|
|
|
|
@@ -12,10 +13,11 @@ import (
|
|
|
)
|
|
|
|
|
|
const (
|
|
|
- defaultRewardAmountCents = 1000
|
|
|
- maxReferralRewards = 10
|
|
|
- defaultMaxRetries = 10
|
|
|
- maxIngestEventLimit = 100
|
|
|
+ defaultStarterCreditsCents = 500
|
|
|
+ defaultRewardAmountCents = 1000
|
|
|
+ maxReferralRewards = 10
|
|
|
+ defaultMaxRetries = 10
|
|
|
+ maxIngestEventLimit = 100
|
|
|
|
|
|
// porterStandardTrialDays is the number of days for the trial
|
|
|
porterStandardTrialDays = 15
|
|
|
@@ -48,8 +50,11 @@ type LagoClient struct {
|
|
|
|
|
|
// NewLagoClient returns a new Lago client
|
|
|
func NewLagoClient(lagoApiKey string, porterCloudPlanCode string, porterStandardPlanCode string, porterTrialCode string) (client LagoClient, err error) {
|
|
|
- lagoClient := lago.New().
|
|
|
- SetApiKey("__YOU_API_KEY__")
|
|
|
+ lagoClient := lago.New().SetApiKey(lagoApiKey)
|
|
|
+
|
|
|
+ if lagoClient == nil {
|
|
|
+ return client, fmt.Errorf("failed to create lago client")
|
|
|
+ }
|
|
|
|
|
|
return LagoClient{
|
|
|
client: *lagoClient,
|
|
|
@@ -66,6 +71,10 @@ func (m LagoClient) CreateCustomerWithPlan(ctx context.Context, userEmail string
|
|
|
ctx, span := telemetry.NewSpan(ctx, "add-lago-customer-plan")
|
|
|
defer span.End()
|
|
|
|
|
|
+ if projectID == 0 {
|
|
|
+ return telemetry.Error(ctx, span, err, "project id empty")
|
|
|
+ }
|
|
|
+
|
|
|
customerID, err := m.createCustomer(ctx, userEmail, projectName, projectID, billingID, sandboxEnabled)
|
|
|
if err != nil {
|
|
|
return telemetry.Error(ctx, span, err, "error while creating customer")
|
|
|
@@ -73,14 +82,23 @@ func (m LagoClient) CreateCustomerWithPlan(ctx context.Context, userEmail string
|
|
|
|
|
|
trialID := m.generateLagoID(TrialIDPrefix, projectID, sandboxEnabled)
|
|
|
subscriptionID := m.generateLagoID(SubscriptionIDPrefix, projectID, sandboxEnabled)
|
|
|
- now := time.Now()
|
|
|
- trialEndTime := now.Add(time.Hour * 24 * porterStandardTrialDays)
|
|
|
+
|
|
|
+ // The dates need to be at midnight UTC
|
|
|
+ now := time.Now().UTC()
|
|
|
+ now = time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
|
|
|
+ trialEndTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).Add(time.Hour * 24 * porterStandardTrialDays).UTC()
|
|
|
|
|
|
if sandboxEnabled {
|
|
|
err = m.addCustomerPlan(ctx, customerID, m.PorterCloudPlanCode, subscriptionID, &now, nil)
|
|
|
if err != nil {
|
|
|
return telemetry.Error(ctx, span, err, fmt.Sprintf("error while adding customer to plan %s", m.PorterCloudPlanCode))
|
|
|
}
|
|
|
+
|
|
|
+ starterWalletName := "Free Starter Credits"
|
|
|
+ expiresAt := time.Now().UTC().AddDate(0, 1, 0).Truncate(24 * time.Hour)
|
|
|
+
|
|
|
+ err = m.CreateCreditsGrant(ctx, projectID, starterWalletName, defaultStarterCreditsCents, &expiresAt, sandboxEnabled)
|
|
|
+
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
@@ -99,75 +117,91 @@ func (m LagoClient) CreateCustomerWithPlan(ctx context.Context, userEmail string
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
-// createCustomer will create the customer in Lago
|
|
|
-func (m LagoClient) createCustomer(ctx context.Context, userEmail string, projectName string, projectID uint, billingID string, sandboxEnabled bool) (customerID string, err error) {
|
|
|
- ctx, span := telemetry.NewSpan(ctx, "create-lago-customer")
|
|
|
+func (m LagoClient) CheckIfCustomerExists(ctx context.Context, projectID uint, enableSandbox bool) (exists bool, err error) {
|
|
|
+ ctx, span := telemetry.NewSpan(ctx, "check-lago-customer-exists")
|
|
|
defer span.End()
|
|
|
|
|
|
- customerID = m.generateLagoID(CustomerIDPrefix, projectID, sandboxEnabled)
|
|
|
-
|
|
|
- customerInput := &lago.CustomerInput{
|
|
|
- ExternalID: customerID,
|
|
|
- Name: projectName,
|
|
|
- Email: userEmail,
|
|
|
- BillingConfiguration: lago.CustomerBillingConfigurationInput{
|
|
|
- PaymentProvider: "stripe",
|
|
|
- ProviderCustomerID: billingID,
|
|
|
- },
|
|
|
+ if projectID == 0 {
|
|
|
+ return exists, telemetry.Error(ctx, span, err, "project id empty")
|
|
|
}
|
|
|
|
|
|
- _, lagoErr := m.client.Customer().Create(ctx, customerInput)
|
|
|
- if err != nil {
|
|
|
- return customerID, telemetry.Error(ctx, span, lagoErr.Err, "failed to create lago customer")
|
|
|
+ customerID := m.generateLagoID(CustomerIDPrefix, projectID, enableSandbox)
|
|
|
+ _, lagoErr := m.client.Customer().Get(ctx, customerID)
|
|
|
+ if lagoErr != nil {
|
|
|
+ return exists, telemetry.Error(ctx, span, fmt.Errorf(lagoErr.ErrorCode), "failed to get customer")
|
|
|
}
|
|
|
- return customerID, nil
|
|
|
+
|
|
|
+ return true, nil
|
|
|
}
|
|
|
|
|
|
-// addCustomerPlan will create a plan subscription for the customer
|
|
|
-func (m LagoClient) addCustomerPlan(ctx context.Context, projectID string, planID string, subscriptionID string, startingAt *time.Time, endingAt *time.Time) (err error) {
|
|
|
- ctx, span := telemetry.NewSpan(ctx, "add-lago-customer-plan")
|
|
|
+func (m LagoClient) GetCustomeActiveSubscription(ctx context.Context, projectID uint, sandboxEnabled bool) (subscriptionID string, err error) {
|
|
|
+ ctx, span := telemetry.NewSpan(ctx, "get-active-subscription")
|
|
|
defer span.End()
|
|
|
|
|
|
- if projectID == "" || planID == "" {
|
|
|
- return telemetry.Error(ctx, span, err, "project and plan id are required")
|
|
|
+ if projectID == 0 {
|
|
|
+ return subscriptionID, telemetry.Error(ctx, span, err, "project id empty")
|
|
|
}
|
|
|
|
|
|
- subscriptionInput := &lago.SubscriptionInput{
|
|
|
- ExternalCustomerID: projectID,
|
|
|
- ExternalID: subscriptionID,
|
|
|
- PlanCode: planID,
|
|
|
- SubscriptionAt: startingAt,
|
|
|
- EndingAt: endingAt,
|
|
|
- BillingTime: lago.Calendar,
|
|
|
+ if sandboxEnabled {
|
|
|
+ subscriptionID = m.generateLagoID(SubscriptionIDPrefix, projectID, sandboxEnabled)
|
|
|
+ return subscriptionID, nil
|
|
|
}
|
|
|
|
|
|
- _, lagoErr := m.client.Subscription().Create(ctx, subscriptionInput)
|
|
|
- if err != nil {
|
|
|
- return telemetry.Error(ctx, span, lagoErr.Err, "failed to create subscription")
|
|
|
+ customerID := m.generateLagoID(CustomerIDPrefix, projectID, sandboxEnabled)
|
|
|
+ subscriptionListInput := lago.SubscriptionListInput{
|
|
|
+ ExternalCustomerID: customerID,
|
|
|
+ Status: []string{"active"},
|
|
|
}
|
|
|
|
|
|
- return nil
|
|
|
+ activeSubscriptions, lagoErr := m.client.Subscription().GetList(ctx, subscriptionListInput)
|
|
|
+ if lagoErr != nil {
|
|
|
+ return subscriptionID, telemetry.Error(ctx, span, fmt.Errorf(lagoErr.ErrorCode), "failed to get active subscription")
|
|
|
+ }
|
|
|
+
|
|
|
+ if activeSubscriptions == nil {
|
|
|
+ return subscriptionID, telemetry.Error(ctx, span, err, "no active subscriptions found")
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(activeSubscriptions.Subscriptions) > 0 {
|
|
|
+ subscriptionID = activeSubscriptions.Subscriptions[0].ExternalID
|
|
|
+ }
|
|
|
+
|
|
|
+ return subscriptionID, nil
|
|
|
}
|
|
|
|
|
|
// ListCustomerPlan will return the current active plan to which the user is subscribed
|
|
|
-func (m LagoClient) ListCustomerPlan(ctx context.Context, projectID uint, sandboxEnabled bool) (plan types.Plan, err error) {
|
|
|
+func (m LagoClient) ListCustomerPlan(ctx context.Context, subscriptionID string) (plan types.Plan, err error) {
|
|
|
ctx, span := telemetry.NewSpan(ctx, "list-customer-plans")
|
|
|
defer span.End()
|
|
|
|
|
|
- if projectID == 0 {
|
|
|
+ if subscriptionID == "" {
|
|
|
return plan, telemetry.Error(ctx, span, err, "project id empty")
|
|
|
}
|
|
|
|
|
|
- subscriptionID := m.generateLagoID(SubscriptionIDPrefix, projectID, sandboxEnabled)
|
|
|
subscription, lagoErr := m.client.Subscription().Get(ctx, subscriptionID)
|
|
|
- if err != nil {
|
|
|
- return plan, telemetry.Error(ctx, span, lagoErr.Err, "failed to create subscription")
|
|
|
+ if lagoErr != nil {
|
|
|
+ return plan, telemetry.Error(ctx, span, fmt.Errorf(lagoErr.ErrorCode), "failed to get subscription")
|
|
|
+ }
|
|
|
+
|
|
|
+ if subscription == nil {
|
|
|
+ return plan, nil
|
|
|
}
|
|
|
|
|
|
- plan.StartingOn = subscription.StartedAt.Format(time.RFC3339)
|
|
|
- plan.EndingBefore = subscription.EndingAt.Format(time.RFC3339)
|
|
|
- plan.TrialInfo.EndingBefore = subscription.TrialEndedAt.Format(time.RFC3339)
|
|
|
+ log.Println("subscription", subscription)
|
|
|
|
|
|
+ if subscription.StartedAt != nil {
|
|
|
+ plan.StartingOn = subscription.StartedAt.Format(time.RFC3339)
|
|
|
+ }
|
|
|
+
|
|
|
+ if subscription.EndingAt != nil {
|
|
|
+ plan.EndingBefore = subscription.EndingAt.Format(time.RFC3339)
|
|
|
+ }
|
|
|
+
|
|
|
+ if subscription.TrialEndedAt != nil {
|
|
|
+ plan.TrialInfo.EndingBefore = subscription.TrialEndedAt.Format(time.RFC3339)
|
|
|
+ }
|
|
|
+
|
|
|
+ log.Println("plan", plan)
|
|
|
return plan, nil
|
|
|
}
|
|
|
|
|
|
@@ -186,8 +220,8 @@ func (m LagoClient) EndCustomerPlan(ctx context.Context, projectID uint) (err er
|
|
|
}
|
|
|
|
|
|
_, lagoErr := m.client.Subscription().Terminate(ctx, subscriptionTerminateInput)
|
|
|
- if lagoErr.Err != nil {
|
|
|
- return telemetry.Error(ctx, span, lagoErr.Err, "failed to terminate subscription")
|
|
|
+ if lagoErr != nil {
|
|
|
+ return telemetry.Error(ctx, span, fmt.Errorf(lagoErr.ErrorCode), "failed to terminate subscription")
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
@@ -221,7 +255,7 @@ func (m LagoClient) EndCustomerPlan(ctx context.Context, projectID uint) (err er
|
|
|
// }
|
|
|
|
|
|
// 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 string, sandboxEnabled bool) (err error) {
|
|
|
+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")
|
|
|
defer span.End()
|
|
|
|
|
|
@@ -230,23 +264,19 @@ func (m LagoClient) CreateCreditsGrant(ctx context.Context, projectID uint, name
|
|
|
}
|
|
|
|
|
|
customerID := m.generateLagoID(CustomerIDPrefix, projectID, sandboxEnabled)
|
|
|
- expiresAtTime, err := time.Parse(time.RFC3339, expiresAt)
|
|
|
- if err != nil {
|
|
|
- return telemetry.Error(ctx, span, err, "failed to parse credit expiration timestamp")
|
|
|
- }
|
|
|
-
|
|
|
walletInput := &lago.WalletInput{
|
|
|
ExternalCustomerID: customerID,
|
|
|
Name: name,
|
|
|
Currency: lago.USD,
|
|
|
GrantedCredits: strconv.FormatInt(grantAmount, 10),
|
|
|
- RateAmount: "1",
|
|
|
- ExpirationAt: &expiresAtTime,
|
|
|
+ // Rate is 1 credit = 1 cent
|
|
|
+ RateAmount: "0.01",
|
|
|
+ ExpirationAt: expiresAt,
|
|
|
}
|
|
|
|
|
|
_, lagoErr := m.client.Wallet().Create(ctx, walletInput)
|
|
|
- if lagoErr.Err != nil {
|
|
|
- return telemetry.Error(ctx, span, lagoErr.Err, "failed to create credits grant")
|
|
|
+ if lagoErr != nil {
|
|
|
+ return telemetry.Error(ctx, span, fmt.Errorf(lagoErr.ErrorCode), "failed to create credits grant")
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
@@ -268,15 +298,15 @@ func (m LagoClient) ListCustomerUsage(ctx context.Context, projectID uint, curre
|
|
|
|
|
|
customerID := m.generateLagoID(CustomerIDPrefix, projectID, sandboxEnabled)
|
|
|
_, lagoErr := m.client.Customer().CurrentUsage(ctx, customerID, customerUsageInput)
|
|
|
- if lagoErr.Err != nil {
|
|
|
- return usage, telemetry.Error(ctx, span, lagoErr.Err, "failed to get customer usage")
|
|
|
+ if lagoErr != nil {
|
|
|
+ return usage, telemetry.Error(ctx, span, fmt.Errorf(lagoErr.ErrorCode), "failed to get customer usage")
|
|
|
}
|
|
|
|
|
|
return usage, nil
|
|
|
}
|
|
|
|
|
|
// IngestEvents sends a list of billing events to Lago's ingest endpoint
|
|
|
-func (m LagoClient) IngestEvents(ctx context.Context, events []types.BillingEvent, enableSandbox bool) (err error) {
|
|
|
+func (m LagoClient) IngestEvents(ctx context.Context, subscriptionID string, events []types.BillingEvent, enableSandbox bool) (err error) {
|
|
|
ctx, span := telemetry.NewSpan(ctx, "ingets-billing-events")
|
|
|
defer span.End()
|
|
|
|
|
|
@@ -294,11 +324,17 @@ func (m LagoClient) IngestEvents(ctx context.Context, events []types.BillingEven
|
|
|
batchInput := make([]lago.EventInput, len(batch))
|
|
|
|
|
|
for i := range batch {
|
|
|
- customerID, err := strconv.ParseUint(batch[i].CustomerID, 10, 64)
|
|
|
- if err != nil {
|
|
|
- return telemetry.Error(ctx, span, err, "failed to parse customer ID")
|
|
|
+
|
|
|
+ externalSubscriptionID := subscriptionID
|
|
|
+ if enableSandbox {
|
|
|
+ // This hack has to be done because we can't infer the project id from the
|
|
|
+ // context in Porter Cloud
|
|
|
+ customerID, err := strconv.ParseUint(batch[i].CustomerID, 10, 64)
|
|
|
+ if err != nil {
|
|
|
+ return telemetry.Error(ctx, span, err, "failed to parse customer ID")
|
|
|
+ }
|
|
|
+ externalSubscriptionID = m.generateLagoID(SubscriptionIDPrefix, uint(customerID), enableSandbox)
|
|
|
}
|
|
|
- externalSubscriptionID := m.generateLagoID(SubscriptionIDPrefix, uint(customerID), enableSandbox)
|
|
|
|
|
|
event := lago.EventInput{
|
|
|
TransactionID: batch[i].TransactionID,
|
|
|
@@ -336,6 +372,58 @@ func (s StripeClient) ListCustomerInvoices(ctx context.Context, projectID uint)
|
|
|
return invoiceList, nil
|
|
|
}
|
|
|
|
|
|
+// createCustomer will create the customer in Lago
|
|
|
+func (m LagoClient) createCustomer(ctx context.Context, userEmail string, projectName string, projectID uint, billingID string, sandboxEnabled bool) (customerID string, err error) {
|
|
|
+ ctx, span := telemetry.NewSpan(ctx, "create-lago-customer")
|
|
|
+ defer span.End()
|
|
|
+
|
|
|
+ customerID = m.generateLagoID(CustomerIDPrefix, projectID, sandboxEnabled)
|
|
|
+
|
|
|
+ customerInput := &lago.CustomerInput{
|
|
|
+ ExternalID: customerID,
|
|
|
+ Name: projectName,
|
|
|
+ Email: userEmail,
|
|
|
+ BillingConfiguration: lago.CustomerBillingConfigurationInput{
|
|
|
+ PaymentProvider: lago.PaymentProviderStripe,
|
|
|
+ ProviderCustomerID: billingID,
|
|
|
+ Sync: false,
|
|
|
+ SyncWithProvider: false,
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ _, lagoErr := m.client.Customer().Create(ctx, customerInput)
|
|
|
+ if lagoErr != nil {
|
|
|
+ return customerID, telemetry.Error(ctx, span, fmt.Errorf(lagoErr.ErrorCode), "failed to create lago customer")
|
|
|
+ }
|
|
|
+ return customerID, nil
|
|
|
+}
|
|
|
+
|
|
|
+// addCustomerPlan will create a plan subscription for the customer
|
|
|
+func (m LagoClient) addCustomerPlan(ctx context.Context, customerID string, planID string, subscriptionID string, startingAt *time.Time, endingAt *time.Time) (err error) {
|
|
|
+ ctx, span := telemetry.NewSpan(ctx, "add-lago-customer-plan")
|
|
|
+ defer span.End()
|
|
|
+
|
|
|
+ if customerID == "" || planID == "" {
|
|
|
+ return telemetry.Error(ctx, span, err, "project and plan id are required")
|
|
|
+ }
|
|
|
+
|
|
|
+ subscriptionInput := &lago.SubscriptionInput{
|
|
|
+ ExternalCustomerID: customerID,
|
|
|
+ ExternalID: subscriptionID,
|
|
|
+ PlanCode: planID,
|
|
|
+ SubscriptionAt: startingAt,
|
|
|
+ EndingAt: endingAt,
|
|
|
+ BillingTime: lago.Calendar,
|
|
|
+ }
|
|
|
+
|
|
|
+ _, lagoErr := m.client.Subscription().Create(ctx, subscriptionInput)
|
|
|
+ if lagoErr != nil {
|
|
|
+ return telemetry.Error(ctx, span, fmt.Errorf(lagoErr.ErrorCode), "failed to create subscription")
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
func (m LagoClient) generateLagoID(prefix string, projectID uint, sandboxEnabled bool) string {
|
|
|
if sandboxEnabled {
|
|
|
return fmt.Sprintf("cloud_%s_%d", prefix, projectID)
|