mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
[public-api] handle customer.subscription.deleted event
This event is fired by Stripe when a customer cancels their subscription
This commit is contained in:
parent
9c9369b4e5
commit
1b76bca17d
@ -8,13 +8,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"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"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Interface interface {
|
type Interface interface {
|
||||||
FinalizeInvoice(ctx context.Context, invoiceId string) error
|
FinalizeInvoice(ctx context.Context, invoiceId string) error
|
||||||
|
CancelSubscription(ctx context.Context, subscriptionId string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@ -38,3 +39,12 @@ func (c *Client) FinalizeInvoice(ctx context.Context, invoiceId string) error {
|
|||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -51,3 +51,17 @@ func (mr *MockInterfaceMockRecorder) FinalizeInvoice(ctx, invoiceId interface{})
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FinalizeInvoice", reflect.TypeOf((*MockInterface)(nil).FinalizeInvoice), ctx, invoiceId)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@ -11,3 +11,7 @@ type NoOpClient struct{}
|
|||||||
func (c *NoOpClient) FinalizeInvoice(ctx context.Context, invoiceId string) error {
|
func (c *NoOpClient) FinalizeInvoice(ctx context.Context, invoiceId string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *NoOpClient) CancelSubscription(ctx context.Context, subscriptionId string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -5,11 +5,12 @@
|
|||||||
package webhooks
|
package webhooks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/gitpod-io/gitpod/common-go/log"
|
"github.com/gitpod-io/gitpod/common-go/log"
|
||||||
"github.com/gitpod-io/gitpod/public-api-server/pkg/billingservice"
|
"github.com/gitpod-io/gitpod/public-api-server/pkg/billingservice"
|
||||||
"github.com/stripe/stripe-go/v72/webhook"
|
"github.com/stripe/stripe-go/v72/webhook"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxBodyBytes = int64(65536)
|
const maxBodyBytes = int64(65536)
|
||||||
@ -56,22 +57,36 @@ func (h *webhookHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
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)
|
log.Errorf("Unexpected Stripe event type: %s", event.Type)
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
return
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,11 +8,12 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stripe/stripe-go/v72/webhook"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stripe/stripe-go/v72/webhook"
|
||||||
|
|
||||||
"github.com/gitpod-io/gitpod/common-go/baseserver"
|
"github.com/gitpod-io/gitpod/common-go/baseserver"
|
||||||
"github.com/gitpod-io/gitpod/public-api-server/pkg/billingservice"
|
"github.com/gitpod-io/gitpod/public-api-server/pkg/billingservice"
|
||||||
mockbillingservice "github.com/gitpod-io/gitpod/public-api-server/pkg/billingservice/mock_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
|
// https://stripe.com/docs/api/events/types
|
||||||
const (
|
const (
|
||||||
invoiceUpdatedEventType = "invoice.updated"
|
invoiceUpdatedEventType = "invoice.updated"
|
||||||
invoiceFinalizedEventType = "invoice.finalized"
|
invoiceFinalizedEventType = "invoice.finalized"
|
||||||
customerCreatedEventType = "customer.created"
|
customerCreatedEventType = "customer.created"
|
||||||
|
customerSubscriptionDeleted = "customer.subscription.deleted"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -80,6 +82,10 @@ func TestWebhookIgnoresIrrelevantEvents(t *testing.T) {
|
|||||||
EventType: invoiceFinalizedEventType,
|
EventType: invoiceFinalizedEventType,
|
||||||
ExpectedStatusCode: http.StatusOK,
|
ExpectedStatusCode: http.StatusOK,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
EventType: customerSubscriptionDeleted,
|
||||||
|
ExpectedStatusCode: http.StatusOK,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
EventType: invoiceUpdatedEventType,
|
EventType: invoiceUpdatedEventType,
|
||||||
ExpectedStatusCode: http.StatusBadRequest,
|
ExpectedStatusCode: http.StatusBadRequest,
|
||||||
@ -134,6 +140,26 @@ func TestWebhookInvokesFinalizeInvoiceRPC(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
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 {
|
func baseServerWithStripeWebhook(t *testing.T, billingService billingservice.Interface) *baseserver.Server {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@ -150,19 +176,14 @@ func baseServerWithStripeWebhook(t *testing.T, billingService billingservice.Int
|
|||||||
func payloadForStripeEvent(t *testing.T, eventType string) []byte {
|
func payloadForStripeEvent(t *testing.T, eventType string) []byte {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
if eventType != invoiceFinalizedEventType {
|
return []byte(`{
|
||||||
return []byte(`{}`)
|
"data": {
|
||||||
}
|
"object": {
|
||||||
return []byte(`
|
"id": "in_1LUQi7GadRXm50o36jWK7ehs"
|
||||||
{
|
}
|
||||||
"data": {
|
},
|
||||||
"object": {
|
"type": "` + eventType + `"
|
||||||
"id": "in_1LUQi7GadRXm50o36jWK7ehs"
|
}`)
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "invoice.finalized"
|
|
||||||
}
|
|
||||||
`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateHeader(payload []byte, secret string) string {
|
func generateHeader(payload []byte, secret string) string {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user