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

cache plan name #6338

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
3 changes: 3 additions & 0 deletions admin/billing/biller.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ type Biller interface {

// WebhookHandlerFunc returns a http.HandlerFunc that can be used to handle incoming webhooks from the payment provider. Return nil if you don't want to register any webhook handlers. jobs is used to enqueue jobs for processing the webhook events.
WebhookHandlerFunc(ctx context.Context, jobs jobs.Client) httputil.Handler

// GetCurrentPlanDisplayName this is specifically added for the UI to show the current plan name
GetCurrentPlanDisplayName(ctx context.Context, customerID string) (string, error)
}

type PlanType int
Expand Down
4 changes: 4 additions & 0 deletions admin/billing/noop.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,7 @@ func (n noop) GetReportingWorkerCron() string {
func (n noop) WebhookHandlerFunc(ctx context.Context, jc jobs.Client) httputil.Handler {
return nil
}

func (n noop) GetCurrentPlanDisplayName(ctx context.Context, customerID string) (string, error) {
return "", nil
}
41 changes: 40 additions & 1 deletion admin/billing/orb.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ const (

var ErrCustomerIDRequired = errors.New("customer id is required")

var planCache = make(map[string]planCacheEntry)

type planCacheEntry struct {
planDisplayName string
lastUpdated time.Time
}
Comment on lines +33 to +38
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is updated with webhooks, it should be consistent, right? So can it be stored in Postgres instead?

It would be useful to use Postgres because a) we have multiple replicas of the admin service, b) it makes introspection easier and provides easy access to the plan name in downstream pipelines


var _ Biller = &Orb{}

type Orb struct {
Expand Down Expand Up @@ -190,7 +197,12 @@ func (o *Orb) DeleteCustomer(ctx context.Context, customerID string) error {
}

func (o *Orb) CreateSubscription(ctx context.Context, customerID string, plan *Plan) (*Subscription, error) {
return o.createSubscription(ctx, customerID, plan)
sub, err := o.createSubscription(ctx, customerID, plan)
if err != nil {
return nil, err
}
planCache[customerID] = planCacheEntry{planDisplayName: sub.Plan.DisplayName, lastUpdated: time.Now()}
return sub, nil
}

func (o *Orb) GetActiveSubscription(ctx context.Context, customerID string) (*Subscription, error) {
Expand All @@ -200,13 +212,16 @@ func (o *Orb) GetActiveSubscription(ctx context.Context, customerID string) (*Su
}

if len(subs) == 0 {
planCache[customerID] = planCacheEntry{planDisplayName: "", lastUpdated: time.Now()}
return nil, ErrNotFound
}

if len(subs) > 1 {
return nil, fmt.Errorf("multiple active subscriptions (%d) found for customer %s", len(subs), customerID)
}

planCache[customerID] = planCacheEntry{planDisplayName: subs[0].Plan.DisplayName, lastUpdated: time.Now()}

return subs[0], nil
}

Expand All @@ -218,6 +233,9 @@ func (o *Orb) ChangeSubscriptionPlan(ctx context.Context, subscriptionID string,
if err != nil {
return nil, err
}
// NOTE - since change option is SubscriptionSchedulePlanChangeParamsChangeOptionImmediate, the plan change should be immediate
// if adding any other option then don't do this rather rely on webhook to update the cache
planCache[s.Customer.ExternalCustomerID] = planCacheEntry{planDisplayName: plan.DisplayName, lastUpdated: time.Now()}
return &Subscription{
ID: s.ID,
Customer: getBillingCustomerFromOrbCustomer(&s.Customer),
Expand Down Expand Up @@ -281,6 +299,12 @@ func (o *Orb) CancelSubscriptionsForCustomer(ctx context.Context, customerID str
cancelDate = sub.EndDate
}
}

if cancelOption == SubscriptionCancellationOptionImmediate {
delete(planCache, customerID)
// for end of subscription term rely on webhook
}

return cancelDate, nil
}

Expand Down Expand Up @@ -424,6 +448,21 @@ func (o *Orb) WebhookHandlerFunc(ctx context.Context, jc jobs.Client) httputil.H
return ow.handleWebhook
}

func (o *Orb) GetCurrentPlanDisplayName(ctx context.Context, customerID string) (string, error) {
if p, ok := planCache[customerID]; ok {
return p.planDisplayName, nil
}
sub, err := o.GetActiveSubscription(ctx, customerID)
if err != nil {
if errors.Is(err, ErrNotFound) || errors.Is(err, ErrCustomerIDRequired) {
return "", nil
}
return "", err
}

return sub.Plan.DisplayName, nil
}

func (o *Orb) createSubscription(ctx context.Context, customerID string, plan *Plan) (*Subscription, error) {
sub, err := o.client.Subscriptions.New(ctx, orb.SubscriptionNewParams{
ExternalCustomerID: orb.String(customerID),
Expand Down
70 changes: 69 additions & 1 deletion admin/billing/orb_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const (
maxBodyBytes = int64(65536)
)

var interestingEvents = []string{"invoice.payment_succeeded", "invoice.payment_failed", "invoice.issue_failed"}
var interestingEvents = []string{"invoice.payment_succeeded", "invoice.payment_failed", "invoice.issue_failed", "subscription.started", "subscription.ended", "subscription.plan_changed"}

type orbWebhook struct {
orb *Orb
Expand Down Expand Up @@ -86,6 +86,27 @@ func (o *orbWebhook) handleWebhook(w http.ResponseWriter, r *http.Request) error
}
// inefficient one time conversion to named logger as its rare event and no need to log every thing else with named logger
o.orb.logger.Named("billing").Warn("invoice issue failed", zap.String("customer_id", ie.OrbInvoice.Customer.ExternalCustomerID), zap.String("invoice_id", ie.OrbInvoice.ID), zap.String("props", fmt.Sprintf("%v", ie.Properties)))
case "subscription.started":
var se subscriptionEvent
err = json.Unmarshal(payload, &se)
if err != nil {
return httputil.Errorf(http.StatusBadRequest, "error parsing event data: %w", err)
}
o.handleSubscriptionStarted(se) // as of now we are just using this to update plan cache
case "subscription.ended":
var se subscriptionEvent
err = json.Unmarshal(payload, &se)
if err != nil {
return httputil.Errorf(http.StatusBadRequest, "error parsing event data: %w", err)
}
o.handleSubscriptionEnded(se) // as of now we are just using this to update plan cache
case "subscription.plan_changed":
var se subscriptionEvent
err = json.Unmarshal(payload, &se)
if err != nil {
return httputil.Errorf(http.StatusBadRequest, "error parsing event data: %w", err)
}
o.handleSubscriptionPlanChanged(se) // as of now we are just using this to update plan cache
default:
// do nothing
}
Expand Down Expand Up @@ -125,6 +146,45 @@ func (o *orbWebhook) handleInvoicePaymentFailed(ctx context.Context, ie invoiceE
return nil
}

func (o *orbWebhook) handleSubscriptionStarted(se subscriptionEvent) {
if se.OrbSubscription.Customer.ExternalCustomerID == "" {
return
}
if pe, ok := planCache[se.OrbSubscription.Customer.ExternalCustomerID]; !ok || pe.lastUpdated.After(se.CreatedAt) {
// don't have plan cached for this customer, ignore as we are not handling this events in persistent job
return
}
planCache[se.OrbSubscription.Customer.ExternalCustomerID] = planCacheEntry{
planDisplayName: getPlanDisplayName(se.OrbSubscription.Plan.ExternalPlanID),
lastUpdated: se.CreatedAt,
}
}

func (o *orbWebhook) handleSubscriptionEnded(se subscriptionEvent) {
if se.OrbSubscription.Customer.ExternalCustomerID == "" {
return
}
if pe, ok := planCache[se.OrbSubscription.Customer.ExternalCustomerID]; !ok || pe.lastUpdated.After(se.CreatedAt) {
// don't have plan cached for this customer, ignore as we are not handling this events in persistent job
return
}
delete(planCache, se.OrbSubscription.Customer.ExternalCustomerID)
}

func (o *orbWebhook) handleSubscriptionPlanChanged(se subscriptionEvent) {
if se.OrbSubscription.Customer.ExternalCustomerID == "" {
return
}
if pe, ok := planCache[se.OrbSubscription.Customer.ExternalCustomerID]; !ok || pe.lastUpdated.After(se.CreatedAt) {
// don't have plan cached for this customer, ignore as we are not handling this events in persistent job
return
}
planCache[se.OrbSubscription.Customer.ExternalCustomerID] = planCacheEntry{
planDisplayName: getPlanDisplayName(se.OrbSubscription.Plan.ExternalPlanID),
lastUpdated: se.CreatedAt,
}
}

// Validates whether or not the webhook payload was sent by Orb.
func (o *orbWebhook) verifySignature(payload []byte, headers http.Header, now time.Time) error {
if o.orb.webhookSecret == "" {
Expand Down Expand Up @@ -192,3 +252,11 @@ type invoiceEvent struct {
Properties interface{} `json:"properties"`
OrbInvoice orb.Invoice `json:"invoice"`
}

type subscriptionEvent struct {
ID string `json:"id"`
CreatedAt time.Time `json:"created_at"`
Type string `json:"type"`
Properties interface{} `json:"properties"`
OrbSubscription orb.Subscription `json:"subscription"`
}
10 changes: 8 additions & 2 deletions admin/server/organizations.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,15 @@ func (s *Server) GetOrganization(ctx context.Context, req *adminv1.GetOrganizati
perms.ReadProjects = true
}

planDisplayName, err := s.admin.Biller.GetCurrentPlanDisplayName(ctx, org.BillingCustomerID)
if err != nil {
return nil, err
}

return &adminv1.GetOrganizationResponse{
Organization: s.organizationToDTO(org, perms.ManageOrg),
Permissions: perms,
Organization: s.organizationToDTO(org, perms.ManageOrg),
Permissions: perms,
PlanDisplayName: planDisplayName,
}, nil
}

Expand Down
1 change: 1 addition & 0 deletions cli/cmd/sudo/org/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func ShowCmd(ch *cmdutil.Helper) *cobra.Command {
}

fmt.Println(string(data))
fmt.Printf("\nPlan: %s\n", res.PlanDisplayName)

return nil
},
Expand Down
2 changes: 2 additions & 0 deletions proto/gen/rill/admin/v1/admin.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4249,6 +4249,8 @@ definitions:
$ref: '#/definitions/v1Organization'
permissions:
$ref: '#/definitions/v1OrganizationPermissions'
planDisplayName:
type: string
v1GetPaymentsPortalURLResponse:
type: object
properties:
Expand Down
Loading
Loading