[public-api] handle customer.subscription.deleted event

This event is fired by Stripe when a customer cancels their subscription
This commit is contained in:
Sven Efftinge 2022-09-16 06:43:31 +00:00 committed by Robo Quat
parent 9c9369b4e5
commit 1b76bca17d
5 changed files with 97 additions and 33 deletions

View File

@ -8,13 +8,14 @@ import (
"context"
"fmt"
"github.com/gitpod-io/gitpod/usage-api/v1"
v1 "github.com/gitpod-io/gitpod/usage-api/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
type Interface interface {
FinalizeInvoice(ctx context.Context, invoiceId string) error
CancelSubscription(ctx context.Context, subscriptionId string) error
}
type Client struct {
@ -38,3 +39,12 @@ func (c *Client) FinalizeInvoice(ctx context.Context, invoiceId string) error {
return nil
}
func (c *Client) CancelSubscription(ctx context.Context, subscriptionId string) error {
_, err := c.b.CancelSubscription(ctx, &v1.CancelSubscriptionRequest{SubscriptionId: subscriptionId})
if err != nil {
return fmt.Errorf("failed RPC to billing service: %s", err)
}
return nil
}

View File

@ -51,3 +51,17 @@ func (mr *MockInterfaceMockRecorder) FinalizeInvoice(ctx, invoiceId interface{})
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FinalizeInvoice", reflect.TypeOf((*MockInterface)(nil).FinalizeInvoice), ctx, invoiceId)
}
// CancelSubscription mocks base method.
func (m *MockInterface) CancelSubscription(ctx context.Context, subscriptionId string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CancelSubscription", ctx, subscriptionId)
ret0, _ := ret[0].(error)
return ret0
}
// CancelSubscription indicates an expected call of CancelSubscription.
func (mr *MockInterfaceMockRecorder) CancelSubscription(ctx, subscriptionId interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelSubscription", reflect.TypeOf((*MockInterface)(nil).CancelSubscription), ctx, subscriptionId)
}

View File

@ -11,3 +11,7 @@ type NoOpClient struct{}
func (c *NoOpClient) FinalizeInvoice(ctx context.Context, invoiceId string) error {
return nil
}
func (c *NoOpClient) CancelSubscription(ctx context.Context, subscriptionId string) error {
return nil
}

View File

@ -5,11 +5,12 @@
package webhooks
import (
"io"
"net/http"
"github.com/gitpod-io/gitpod/common-go/log"
"github.com/gitpod-io/gitpod/public-api-server/pkg/billingservice"
"github.com/stripe/stripe-go/v72/webhook"
"io"
"net/http"
)
const maxBodyBytes = int64(65536)
@ -56,22 +57,36 @@ func (h *webhookHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}
if event.Type != "invoice.finalized" {
switch event.Type {
case "invoice.finalized":
invoiceId, ok := event.Data.Object["id"].(string)
if !ok {
log.Error("failed to find invoice id in Stripe event payload")
w.WriteHeader(http.StatusBadRequest)
}
err = h.billingService.FinalizeInvoice(req.Context(), invoiceId)
if err != nil {
log.WithError(err).Error("Failed to finalize invoice")
w.WriteHeader(http.StatusInternalServerError)
return
}
case "customer.subscription.deleted":
subscriptionId, ok := event.Data.Object["id"].(string)
if !ok {
log.Error("failed to find subscriptionId id in Stripe event payload")
w.WriteHeader(http.StatusBadRequest)
}
err = h.billingService.CancelSubscription(req.Context(), subscriptionId)
if err != nil {
log.WithError(err).Error("Failed to cancel subscription")
w.WriteHeader(http.StatusInternalServerError)
return
}
default:
log.Errorf("Unexpected Stripe event type: %s", event.Type)
w.WriteHeader(http.StatusBadRequest)
return
}
invoiceId, ok := event.Data.Object["id"].(string)
if !ok {
log.Error("failed to find invoice id in Stripe event payload")
w.WriteHeader(http.StatusBadRequest)
}
err = h.billingService.FinalizeInvoice(req.Context(), invoiceId)
if err != nil {
log.WithError(err).Error("Failed to finalize invoice")
w.WriteHeader(http.StatusInternalServerError)
return
}
}

View File

@ -8,11 +8,12 @@ import (
"bytes"
"encoding/hex"
"fmt"
"github.com/stripe/stripe-go/v72/webhook"
"net/http"
"testing"
"time"
"github.com/stripe/stripe-go/v72/webhook"
"github.com/gitpod-io/gitpod/common-go/baseserver"
"github.com/gitpod-io/gitpod/public-api-server/pkg/billingservice"
mockbillingservice "github.com/gitpod-io/gitpod/public-api-server/pkg/billingservice/mock_billingservice"
@ -22,9 +23,10 @@ import (
// https://stripe.com/docs/api/events/types
const (
invoiceUpdatedEventType = "invoice.updated"
invoiceFinalizedEventType = "invoice.finalized"
customerCreatedEventType = "customer.created"
invoiceUpdatedEventType = "invoice.updated"
invoiceFinalizedEventType = "invoice.finalized"
customerCreatedEventType = "customer.created"
customerSubscriptionDeleted = "customer.subscription.deleted"
)
const (
@ -80,6 +82,10 @@ func TestWebhookIgnoresIrrelevantEvents(t *testing.T) {
EventType: invoiceFinalizedEventType,
ExpectedStatusCode: http.StatusOK,
},
{
EventType: customerSubscriptionDeleted,
ExpectedStatusCode: http.StatusOK,
},
{
EventType: invoiceUpdatedEventType,
ExpectedStatusCode: http.StatusBadRequest,
@ -134,6 +140,26 @@ func TestWebhookInvokesFinalizeInvoiceRPC(t *testing.T) {
require.Equal(t, http.StatusOK, resp.StatusCode)
}
func TestWebhookInvokesCancelSubscriptionRPC(t *testing.T) {
ctrl := gomock.NewController(t)
m := mockbillingservice.NewMockInterface(ctrl)
m.EXPECT().CancelSubscription(gomock.Any(), gomock.Eq("in_1LUQi7GadRXm50o36jWK7ehs"))
srv := baseServerWithStripeWebhook(t, m)
url := fmt.Sprintf("%s%s", srv.HTTPAddress(), "/webhook")
payload := payloadForStripeEvent(t, customerSubscriptionDeleted)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload))
require.NoError(t, err)
req.Header.Set("Stripe-Signature", generateHeader(payload, testWebhookSecret))
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
}
func baseServerWithStripeWebhook(t *testing.T, billingService billingservice.Interface) *baseserver.Server {
t.Helper()
@ -150,19 +176,14 @@ func baseServerWithStripeWebhook(t *testing.T, billingService billingservice.Int
func payloadForStripeEvent(t *testing.T, eventType string) []byte {
t.Helper()
if eventType != invoiceFinalizedEventType {
return []byte(`{}`)
}
return []byte(`
{
"data": {
"object": {
"id": "in_1LUQi7GadRXm50o36jWK7ehs"
}
},
"type": "invoice.finalized"
}
`)
return []byte(`{
"data": {
"object": {
"id": "in_1LUQi7GadRXm50o36jWK7ehs"
}
},
"type": "` + eventType + `"
}`)
}
func generateHeader(payload []byte, secret string) string {