diff --git a/internal/api/v1beta1connect/billing_checkout.go b/internal/api/v1beta1connect/billing_checkout.go index 73f342348..a1b2eef4f 100644 --- a/internal/api/v1beta1connect/billing_checkout.go +++ b/internal/api/v1beta1connect/billing_checkout.go @@ -2,12 +2,14 @@ package v1beta1connect import ( "context" + "errors" "connectrpc.com/connect" "github.com/raystack/frontier/billing/checkout" "github.com/raystack/frontier/billing/product" "github.com/raystack/frontier/billing/subscription" frontierv1beta1 "github.com/raystack/frontier/proto/v1beta1" + "google.golang.org/protobuf/types/known/timestamppb" ) type CheckoutService interface { @@ -19,6 +21,83 @@ type CheckoutService interface { CreateSessionForCustomerPortal(ctx context.Context, ch checkout.Checkout) (checkout.Checkout, error) } +func (h *ConnectHandler) CreateCheckout(ctx context.Context, request *connect.Request[frontierv1beta1.CreateCheckoutRequest]) (*connect.Response[frontierv1beta1.CreateCheckoutResponse], error) { + // check if setup requested + if request.Msg.GetSetupBody() != nil && request.Msg.GetSetupBody().GetPaymentMethod() { + newCheckout, err := h.checkoutService.CreateSessionForPaymentMethod(ctx, checkout.Checkout{ + CustomerID: request.Msg.GetBillingId(), + SuccessUrl: request.Msg.GetSuccessUrl(), + CancelUrl: request.Msg.GetCancelUrl(), + }) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + + return connect.NewResponse(&frontierv1beta1.CreateCheckoutResponse{ + CheckoutSession: transformCheckoutToPB(newCheckout), + }), nil + } + + // check if customer portal requested + if request.Msg.GetSetupBody() != nil && request.Msg.GetSetupBody().GetCustomerPortal() { + newCheckout, err := h.checkoutService.CreateSessionForCustomerPortal(ctx, checkout.Checkout{ + CustomerID: request.Msg.GetBillingId(), + SuccessUrl: request.Msg.GetSuccessUrl(), + CancelUrl: request.Msg.GetCancelUrl(), + }) + if err != nil { + if errors.Is(err, checkout.ErrKycCompleted) { + return nil, connect.NewError(connect.CodeFailedPrecondition, ErrPortalChangesKycCompleted) + } + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + + return connect.NewResponse(&frontierv1beta1.CreateCheckoutResponse{ + CheckoutSession: transformCheckoutToPB(newCheckout), + }), nil + } + + // check if checkout requested (subscription or product) + if request.Msg.GetSubscriptionBody() == nil && request.Msg.GetProductBody() == nil { + return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) + } + planID := "" + var skipTrial bool + var cancelAfterTrial bool + if request.Msg.GetSubscriptionBody() != nil { + planID = request.Msg.GetSubscriptionBody().GetPlan() + skipTrial = request.Msg.GetSubscriptionBody().GetSkipTrial() + cancelAfterTrial = request.Msg.GetSubscriptionBody().GetCancelAfterTrial() + } + + var featureID string + var quantity int64 + if request.Msg.GetProductBody() != nil { + featureID = request.Msg.GetProductBody().GetProduct() + quantity = request.Msg.GetProductBody().GetQuantity() + } + newCheckout, err := h.checkoutService.Create(ctx, checkout.Checkout{ + CustomerID: request.Msg.GetBillingId(), + SuccessUrl: request.Msg.GetSuccessUrl(), + CancelUrl: request.Msg.GetCancelUrl(), + PlanID: planID, + ProductID: featureID, + Quantity: quantity, + SkipTrial: skipTrial, + CancelAfterTrial: cancelAfterTrial, + }) + if err != nil { + if errors.Is(err, product.ErrPerSeatLimitReached) { + return nil, connect.NewError(connect.CodeInvalidArgument, ErrPerSeatLimitReached) + } + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + + return connect.NewResponse(&frontierv1beta1.CreateCheckoutResponse{ + CheckoutSession: transformCheckoutToPB(newCheckout), + }), nil +} + func (h *ConnectHandler) DelegatedCheckout(ctx context.Context, request *connect.Request[frontierv1beta1.DelegatedCheckoutRequest]) (*connect.Response[frontierv1beta1.DelegatedCheckoutResponse], error) { var planID string var skipTrial bool @@ -67,3 +146,54 @@ func (h *ConnectHandler) DelegatedCheckout(ctx context.Context, request *connect Product: productPb, }), nil } + +func (h *ConnectHandler) ListCheckouts(ctx context.Context, request *connect.Request[frontierv1beta1.ListCheckoutsRequest]) (*connect.Response[frontierv1beta1.ListCheckoutsResponse], error) { + if request.Msg.GetOrgId() == "" { + return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) + } + + var checkouts []*frontierv1beta1.CheckoutSession + checkoutList, err := h.checkoutService.List(ctx, checkout.Filter{ + CustomerID: request.Msg.GetBillingId(), + }) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + for _, v := range checkoutList { + checkouts = append(checkouts, transformCheckoutToPB(v)) + } + + return connect.NewResponse(&frontierv1beta1.ListCheckoutsResponse{ + CheckoutSessions: checkouts, + }), nil +} + +func (h *ConnectHandler) GetCheckout(ctx context.Context, request *connect.Request[frontierv1beta1.GetCheckoutRequest]) (*connect.Response[frontierv1beta1.GetCheckoutResponse], error) { + if request.Msg.GetOrgId() == "" || request.Msg.GetId() == "" { + return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) + } + + ch, err := h.checkoutService.GetByID(ctx, request.Msg.GetId()) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + + return connect.NewResponse(&frontierv1beta1.GetCheckoutResponse{ + CheckoutSession: transformCheckoutToPB(ch), + }), nil +} + +func transformCheckoutToPB(ch checkout.Checkout) *frontierv1beta1.CheckoutSession { + return &frontierv1beta1.CheckoutSession{ + Id: ch.ID, + CheckoutUrl: ch.CheckoutUrl, + SuccessUrl: ch.SuccessUrl, + CancelUrl: ch.CancelUrl, + State: ch.State, + Plan: ch.PlanID, + Product: ch.ProductID, + CreatedAt: timestamppb.New(ch.CreatedAt), + UpdatedAt: timestamppb.New(ch.UpdatedAt), + ExpireAt: timestamppb.New(ch.ExpireAt), + } +} diff --git a/internal/api/v1beta1connect/billing_checkout_test.go b/internal/api/v1beta1connect/billing_checkout_test.go new file mode 100644 index 000000000..d56db0601 --- /dev/null +++ b/internal/api/v1beta1connect/billing_checkout_test.go @@ -0,0 +1,822 @@ +package v1beta1connect + +import ( + "context" + "errors" + "testing" + "time" + + "connectrpc.com/connect" + "github.com/google/uuid" + "github.com/raystack/frontier/billing/checkout" + "github.com/raystack/frontier/billing/product" + "github.com/raystack/frontier/billing/subscription" + "github.com/raystack/frontier/internal/api/v1beta1connect/mocks" + frontierv1beta1 "github.com/raystack/frontier/proto/v1beta1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + testCheckoutID = uuid.New().String() + testCheckout = checkout.Checkout{ + ID: testCheckoutID, + CheckoutUrl: "https://checkout.stripe.com/session123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + State: "open", + CustomerID: "customer-123", + PlanID: "plan-123", + ProductID: "product-123", + } +) + +func TestConnectHandler_CreateCheckout(t *testing.T) { + tests := []struct { + name string + setup func(cs *mocks.CheckoutService) + req *connect.Request[frontierv1beta1.CreateCheckoutRequest] + want *connect.Response[frontierv1beta1.CreateCheckoutResponse] + wantErr bool + wantErrCode connect.Code + wantErrMsg error + }{ + { + name: "should create payment method setup session successfully", + setup: func(cs *mocks.CheckoutService) { + cs.EXPECT().CreateSessionForPaymentMethod(mock.Anything, checkout.Checkout{ + CustomerID: "customer-123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + }).Return(testCheckout, nil) + }, + req: connect.NewRequest(&frontierv1beta1.CreateCheckoutRequest{ + BillingId: "customer-123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + SetupBody: &frontierv1beta1.CheckoutSetupBody{ + PaymentMethod: true, + }, + }), + want: connect.NewResponse(&frontierv1beta1.CreateCheckoutResponse{ + CheckoutSession: &frontierv1beta1.CheckoutSession{ + Id: testCheckoutID, + CheckoutUrl: "https://checkout.stripe.com/session123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + State: "open", + Plan: "plan-123", + Product: "product-123", + CreatedAt: ×tamppb.Timestamp{}, + UpdatedAt: ×tamppb.Timestamp{}, + ExpireAt: ×tamppb.Timestamp{}, + }, + }), + wantErr: false, + }, + { + name: "should create customer portal setup session successfully", + setup: func(cs *mocks.CheckoutService) { + cs.EXPECT().CreateSessionForCustomerPortal(mock.Anything, checkout.Checkout{ + CustomerID: "customer-123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + }).Return(testCheckout, nil) + }, + req: connect.NewRequest(&frontierv1beta1.CreateCheckoutRequest{ + BillingId: "customer-123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + SetupBody: &frontierv1beta1.CheckoutSetupBody{ + CustomerPortal: true, + }, + }), + want: connect.NewResponse(&frontierv1beta1.CreateCheckoutResponse{ + CheckoutSession: &frontierv1beta1.CheckoutSession{ + Id: testCheckoutID, + CheckoutUrl: "https://checkout.stripe.com/session123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + State: "open", + Plan: "plan-123", + Product: "product-123", + CreatedAt: ×tamppb.Timestamp{}, + UpdatedAt: ×tamppb.Timestamp{}, + ExpireAt: ×tamppb.Timestamp{}, + }, + }), + wantErr: false, + }, + { + name: "should create subscription checkout session successfully", + setup: func(cs *mocks.CheckoutService) { + cs.EXPECT().Create(mock.Anything, checkout.Checkout{ + CustomerID: "customer-123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + PlanID: "plan-123", + ProductID: "", + Quantity: 2, + SkipTrial: false, + CancelAfterTrial: true, + }).Return(testCheckout, nil) + }, + req: connect.NewRequest(&frontierv1beta1.CreateCheckoutRequest{ + BillingId: "customer-123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + SubscriptionBody: &frontierv1beta1.CheckoutSubscriptionBody{ + Plan: "plan-123", + SkipTrial: false, + CancelAfterTrial: true, + }, + ProductBody: &frontierv1beta1.CheckoutProductBody{ + Product: "", + Quantity: 2, + }, + }), + want: connect.NewResponse(&frontierv1beta1.CreateCheckoutResponse{ + CheckoutSession: &frontierv1beta1.CheckoutSession{ + Id: testCheckoutID, + CheckoutUrl: "https://checkout.stripe.com/session123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + State: "open", + Plan: "plan-123", + Product: "product-123", + CreatedAt: ×tamppb.Timestamp{}, + UpdatedAt: ×tamppb.Timestamp{}, + ExpireAt: ×tamppb.Timestamp{}, + }, + }), + wantErr: false, + }, + { + name: "should create product checkout session successfully", + setup: func(cs *mocks.CheckoutService) { + cs.EXPECT().Create(mock.Anything, checkout.Checkout{ + CustomerID: "customer-123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + PlanID: "", + ProductID: "product-123", + Quantity: 3, + SkipTrial: false, + CancelAfterTrial: false, + }).Return(testCheckout, nil) + }, + req: connect.NewRequest(&frontierv1beta1.CreateCheckoutRequest{ + BillingId: "customer-123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + ProductBody: &frontierv1beta1.CheckoutProductBody{ + Product: "product-123", + Quantity: 3, + }, + }), + want: connect.NewResponse(&frontierv1beta1.CreateCheckoutResponse{ + CheckoutSession: &frontierv1beta1.CheckoutSession{ + Id: testCheckoutID, + CheckoutUrl: "https://checkout.stripe.com/session123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + State: "open", + Plan: "plan-123", + Product: "product-123", + CreatedAt: ×tamppb.Timestamp{}, + UpdatedAt: ×tamppb.Timestamp{}, + ExpireAt: ×tamppb.Timestamp{}, + }, + }), + wantErr: false, + }, + { + name: "should return invalid argument error when no body provided", + setup: func(cs *mocks.CheckoutService) { + // No expectations set since no service call should be made + }, + req: connect.NewRequest(&frontierv1beta1.CreateCheckoutRequest{ + BillingId: "customer-123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInvalidArgument, + wantErrMsg: ErrBadRequest, + }, + { + name: "should return per seat limit reached error for subscription checkout", + setup: func(cs *mocks.CheckoutService) { + cs.EXPECT().Create(mock.Anything, mock.Anything).Return(checkout.Checkout{}, product.ErrPerSeatLimitReached) + }, + req: connect.NewRequest(&frontierv1beta1.CreateCheckoutRequest{ + BillingId: "customer-123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + SubscriptionBody: &frontierv1beta1.CheckoutSubscriptionBody{ + Plan: "plan-123", + SkipTrial: false, + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInvalidArgument, + wantErrMsg: ErrPerSeatLimitReached, + }, + { + name: "should return per seat limit reached error for product checkout", + setup: func(cs *mocks.CheckoutService) { + cs.EXPECT().Create(mock.Anything, mock.Anything).Return(checkout.Checkout{}, product.ErrPerSeatLimitReached) + }, + req: connect.NewRequest(&frontierv1beta1.CreateCheckoutRequest{ + BillingId: "customer-123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + ProductBody: &frontierv1beta1.CheckoutProductBody{ + Product: "product-123", + Quantity: 100, + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInvalidArgument, + wantErrMsg: ErrPerSeatLimitReached, + }, + { + name: "should return internal server error when payment method setup fails", + setup: func(cs *mocks.CheckoutService) { + cs.EXPECT().CreateSessionForPaymentMethod(mock.Anything, mock.Anything).Return(checkout.Checkout{}, errors.New("service error")) + }, + req: connect.NewRequest(&frontierv1beta1.CreateCheckoutRequest{ + BillingId: "customer-123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + SetupBody: &frontierv1beta1.CheckoutSetupBody{ + PaymentMethod: true, + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInternal, + wantErrMsg: ErrInternalServerError, + }, + { + name: "should return internal server error when customer portal setup fails", + setup: func(cs *mocks.CheckoutService) { + cs.EXPECT().CreateSessionForCustomerPortal(mock.Anything, mock.Anything).Return(checkout.Checkout{}, errors.New("service error")) + }, + req: connect.NewRequest(&frontierv1beta1.CreateCheckoutRequest{ + BillingId: "customer-123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + SetupBody: &frontierv1beta1.CheckoutSetupBody{ + CustomerPortal: true, + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInternal, + wantErrMsg: ErrInternalServerError, + }, + { + name: "should return internal server error when subscription checkout fails", + setup: func(cs *mocks.CheckoutService) { + cs.EXPECT().Create(mock.Anything, mock.Anything).Return(checkout.Checkout{}, errors.New("service error")) + }, + req: connect.NewRequest(&frontierv1beta1.CreateCheckoutRequest{ + BillingId: "customer-123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + SubscriptionBody: &frontierv1beta1.CheckoutSubscriptionBody{ + Plan: "plan-123", + SkipTrial: false, + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInternal, + wantErrMsg: ErrInternalServerError, + }, + { + name: "should return internal server error when product checkout fails", + setup: func(cs *mocks.CheckoutService) { + cs.EXPECT().Create(mock.Anything, mock.Anything).Return(checkout.Checkout{}, errors.New("service error")) + }, + req: connect.NewRequest(&frontierv1beta1.CreateCheckoutRequest{ + BillingId: "customer-123", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + ProductBody: &frontierv1beta1.CheckoutProductBody{ + Product: "product-123", + Quantity: 3, + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInternal, + wantErrMsg: ErrInternalServerError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockCheckoutSvc := mocks.NewCheckoutService(t) + tt.setup(mockCheckoutSvc) + + h := &ConnectHandler{ + checkoutService: mockCheckoutSvc, + } + + got, err := h.CreateCheckout(context.Background(), tt.req) + if tt.wantErr { + assert.Error(t, err) + if tt.wantErrCode != 0 { + assert.Equal(t, tt.wantErrCode, connect.CodeOf(err)) + } + if tt.wantErrMsg != nil { + assert.Contains(t, err.Error(), tt.wantErrMsg.Error()) + } + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want.Msg.GetCheckoutSession().GetId(), got.Msg.GetCheckoutSession().GetId()) + assert.Equal(t, tt.want.Msg.GetCheckoutSession().GetCheckoutUrl(), got.Msg.GetCheckoutSession().GetCheckoutUrl()) + assert.Equal(t, tt.want.Msg.GetCheckoutSession().GetSuccessUrl(), got.Msg.GetCheckoutSession().GetSuccessUrl()) + assert.Equal(t, tt.want.Msg.GetCheckoutSession().GetCancelUrl(), got.Msg.GetCheckoutSession().GetCancelUrl()) + assert.Equal(t, tt.want.Msg.GetCheckoutSession().GetState(), got.Msg.GetCheckoutSession().GetState()) + assert.Equal(t, tt.want.Msg.GetCheckoutSession().GetPlan(), got.Msg.GetCheckoutSession().GetPlan()) + assert.Equal(t, tt.want.Msg.GetCheckoutSession().GetProduct(), got.Msg.GetCheckoutSession().GetProduct()) + } + }) + } +} + +func TestConnectHandler_DelegatedCheckout(t *testing.T) { + tests := []struct { + name string + setup func(cs *mocks.CheckoutService) + req *connect.Request[frontierv1beta1.DelegatedCheckoutRequest] + want *connect.Response[frontierv1beta1.DelegatedCheckoutResponse] + wantErr bool + wantErrCode connect.Code + wantErrMsg error + }{ + { + name: "should delegate subscription checkout successfully", + setup: func(cs *mocks.CheckoutService) { + testSubs := &subscription.Subscription{ + ID: "sub-123", + CustomerID: "customer-123", + State: "active", + } + testProd := &product.Product{ + ID: "product-123", + Name: "test-product", + } + cs.EXPECT().Apply(mock.Anything, checkout.Checkout{ + CustomerID: "customer-123", + PlanID: "plan-123", + ProductID: "", + Quantity: 0, + SkipTrial: false, + CancelAfterTrial: true, + ProviderCouponID: "coupon-123", + }).Return(testSubs, testProd, nil) + }, + req: connect.NewRequest(&frontierv1beta1.DelegatedCheckoutRequest{ + BillingId: "customer-123", + SubscriptionBody: &frontierv1beta1.CheckoutSubscriptionBody{ + Plan: "plan-123", + SkipTrial: false, + CancelAfterTrial: true, + ProviderCouponId: "coupon-123", + }, + }), + wantErr: false, + }, + { + name: "should delegate product checkout successfully", + setup: func(cs *mocks.CheckoutService) { + testProd := &product.Product{ + ID: "product-123", + Name: "test-product", + } + cs.EXPECT().Apply(mock.Anything, checkout.Checkout{ + CustomerID: "customer-123", + PlanID: "", + ProductID: "product-123", + Quantity: 5, + SkipTrial: false, + CancelAfterTrial: false, + ProviderCouponID: "", + }).Return(nil, testProd, nil) + }, + req: connect.NewRequest(&frontierv1beta1.DelegatedCheckoutRequest{ + BillingId: "customer-123", + ProductBody: &frontierv1beta1.CheckoutProductBody{ + Product: "product-123", + Quantity: 5, + }, + }), + wantErr: false, + }, + { + name: "should return internal server error when apply fails", + setup: func(cs *mocks.CheckoutService) { + cs.EXPECT().Apply(mock.Anything, mock.Anything).Return(nil, nil, errors.New("service error")) + }, + req: connect.NewRequest(&frontierv1beta1.DelegatedCheckoutRequest{ + BillingId: "customer-123", + SubscriptionBody: &frontierv1beta1.CheckoutSubscriptionBody{ + Plan: "plan-123", + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInternal, + wantErrMsg: ErrInternalServerError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockCheckoutSvc := mocks.NewCheckoutService(t) + tt.setup(mockCheckoutSvc) + + h := &ConnectHandler{ + checkoutService: mockCheckoutSvc, + } + + got, err := h.DelegatedCheckout(context.Background(), tt.req) + if tt.wantErr { + assert.Error(t, err) + if tt.wantErrCode != 0 { + assert.Equal(t, tt.wantErrCode, connect.CodeOf(err)) + } + if tt.wantErrMsg != nil { + assert.Contains(t, err.Error(), tt.wantErrMsg.Error()) + } + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.NotNil(t, got) + } + }) + } +} + +func TestConnectHandler_ListCheckouts(t *testing.T) { + tests := []struct { + name string + setup func(cs *mocks.CheckoutService) + req *connect.Request[frontierv1beta1.ListCheckoutsRequest] + want *connect.Response[frontierv1beta1.ListCheckoutsResponse] + wantErr bool + wantErrCode connect.Code + wantErrMsg error + }{ + { + name: "should return error if org_id is empty", + setup: func(cs *mocks.CheckoutService) { + }, + req: connect.NewRequest(&frontierv1beta1.ListCheckoutsRequest{ + OrgId: "", + BillingId: "test-billing-id", + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInvalidArgument, + wantErrMsg: ErrBadRequest, + }, + { + name: "should return error if service returns error", + setup: func(cs *mocks.CheckoutService) { + cs.EXPECT().List(mock.Anything, checkout.Filter{ + CustomerID: "test-billing-id", + }).Return(nil, errors.New("service error")) + }, + req: connect.NewRequest(&frontierv1beta1.ListCheckoutsRequest{ + OrgId: "test-org-id", + BillingId: "test-billing-id", + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInternal, + wantErrMsg: ErrInternalServerError, + }, + { + name: "should return empty list if no checkouts found", + setup: func(cs *mocks.CheckoutService) { + cs.EXPECT().List(mock.Anything, checkout.Filter{ + CustomerID: "test-billing-id", + }).Return([]checkout.Checkout{}, nil) + }, + req: connect.NewRequest(&frontierv1beta1.ListCheckoutsRequest{ + OrgId: "test-org-id", + BillingId: "test-billing-id", + }), + want: connect.NewResponse(&frontierv1beta1.ListCheckoutsResponse{ + CheckoutSessions: []*frontierv1beta1.CheckoutSession{}, + }), + wantErr: false, + }, + { + name: "should return list of checkouts successfully", + setup: func(cs *mocks.CheckoutService) { + createdAt := time.Now() + updatedAt := time.Now() + expireAt := time.Now() + cs.EXPECT().List(mock.Anything, checkout.Filter{ + CustomerID: "test-billing-id", + }).Return([]checkout.Checkout{ + { + ID: "test-checkout-id-1", + CheckoutUrl: "https://checkout.stripe.com/session-1", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + State: "active", + PlanID: "plan-1", + ProductID: "", + CreatedAt: createdAt, + UpdatedAt: updatedAt, + ExpireAt: expireAt, + }, + { + ID: "test-checkout-id-2", + CheckoutUrl: "https://checkout.stripe.com/session-2", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + State: "expired", + PlanID: "", + ProductID: "product-1", + CreatedAt: createdAt, + UpdatedAt: updatedAt, + ExpireAt: expireAt, + }, + }, nil) + }, + req: connect.NewRequest(&frontierv1beta1.ListCheckoutsRequest{ + OrgId: "test-org-id", + BillingId: "test-billing-id", + }), + want: func() *connect.Response[frontierv1beta1.ListCheckoutsResponse] { + createdAt := time.Now() + updatedAt := time.Now() + expireAt := time.Now() + return connect.NewResponse(&frontierv1beta1.ListCheckoutsResponse{ + CheckoutSessions: []*frontierv1beta1.CheckoutSession{ + { + Id: "test-checkout-id-1", + CheckoutUrl: "https://checkout.stripe.com/session-1", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + State: "active", + Plan: "plan-1", + Product: "", + CreatedAt: timestamppb.New(createdAt), + UpdatedAt: timestamppb.New(updatedAt), + ExpireAt: timestamppb.New(expireAt), + }, + { + Id: "test-checkout-id-2", + CheckoutUrl: "https://checkout.stripe.com/session-2", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + State: "expired", + Plan: "", + Product: "product-1", + CreatedAt: timestamppb.New(createdAt), + UpdatedAt: timestamppb.New(updatedAt), + ExpireAt: timestamppb.New(expireAt), + }, + }, + }) + }(), + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkoutService := mocks.NewCheckoutService(t) + if tt.setup != nil { + tt.setup(checkoutService) + } + h := &ConnectHandler{ + checkoutService: checkoutService, + } + got, err := h.ListCheckouts(context.Background(), tt.req) + if tt.wantErr { + assert.Error(t, err) + connectErr := &connect.Error{} + assert.True(t, errors.As(err, &connectErr)) + assert.Equal(t, tt.wantErrCode, connectErr.Code()) + assert.Equal(t, tt.wantErrMsg.Error(), connectErr.Message()) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.NotNil(t, got) + assert.Equal(t, len(tt.want.Msg.GetCheckoutSessions()), len(got.Msg.GetCheckoutSessions())) + for i, wantSession := range tt.want.Msg.GetCheckoutSessions() { + gotSession := got.Msg.GetCheckoutSessions()[i] + assert.Equal(t, wantSession.GetId(), gotSession.GetId()) + assert.Equal(t, wantSession.GetCheckoutUrl(), gotSession.GetCheckoutUrl()) + assert.Equal(t, wantSession.GetSuccessUrl(), gotSession.GetSuccessUrl()) + assert.Equal(t, wantSession.GetCancelUrl(), gotSession.GetCancelUrl()) + assert.Equal(t, wantSession.GetState(), gotSession.GetState()) + assert.Equal(t, wantSession.GetPlan(), gotSession.GetPlan()) + assert.Equal(t, wantSession.GetProduct(), gotSession.GetProduct()) + } + } + }) + } +} + +func TestConnectHandler_GetCheckout(t *testing.T) { + tests := []struct { + name string + setup func(cs *mocks.CheckoutService) + req *connect.Request[frontierv1beta1.GetCheckoutRequest] + want *connect.Response[frontierv1beta1.GetCheckoutResponse] + wantErr bool + wantErrCode connect.Code + wantErrMsg error + }{ + { + name: "should return error if org_id is empty", + setup: func(cs *mocks.CheckoutService) { + }, + req: connect.NewRequest(&frontierv1beta1.GetCheckoutRequest{ + OrgId: "", + Id: "test-checkout-id", + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInvalidArgument, + wantErrMsg: ErrBadRequest, + }, + { + name: "should return error if id is empty", + setup: func(cs *mocks.CheckoutService) { + }, + req: connect.NewRequest(&frontierv1beta1.GetCheckoutRequest{ + OrgId: "test-org-id", + Id: "", + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInvalidArgument, + wantErrMsg: ErrBadRequest, + }, + { + name: "should return error if both org_id and id are empty", + setup: func(cs *mocks.CheckoutService) { + }, + req: connect.NewRequest(&frontierv1beta1.GetCheckoutRequest{ + OrgId: "", + Id: "", + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInvalidArgument, + wantErrMsg: ErrBadRequest, + }, + { + name: "should return error if service returns error", + setup: func(cs *mocks.CheckoutService) { + cs.EXPECT().GetByID(mock.Anything, "test-checkout-id").Return(checkout.Checkout{}, errors.New("service error")) + }, + req: connect.NewRequest(&frontierv1beta1.GetCheckoutRequest{ + OrgId: "test-org-id", + Id: "test-checkout-id", + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInternal, + wantErrMsg: ErrInternalServerError, + }, + { + name: "should return checkout successfully", + setup: func(cs *mocks.CheckoutService) { + createdAt := time.Now() + updatedAt := time.Now() + expireAt := time.Now() + cs.EXPECT().GetByID(mock.Anything, "test-checkout-id").Return(checkout.Checkout{ + ID: "test-checkout-id", + CheckoutUrl: "https://checkout.stripe.com/session-1", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + State: "active", + PlanID: "plan-1", + ProductID: "", + CreatedAt: createdAt, + UpdatedAt: updatedAt, + ExpireAt: expireAt, + }, nil) + }, + req: connect.NewRequest(&frontierv1beta1.GetCheckoutRequest{ + OrgId: "test-org-id", + Id: "test-checkout-id", + }), + want: func() *connect.Response[frontierv1beta1.GetCheckoutResponse] { + createdAt := time.Now() + updatedAt := time.Now() + expireAt := time.Now() + return connect.NewResponse(&frontierv1beta1.GetCheckoutResponse{ + CheckoutSession: &frontierv1beta1.CheckoutSession{ + Id: "test-checkout-id", + CheckoutUrl: "https://checkout.stripe.com/session-1", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + State: "active", + Plan: "plan-1", + Product: "", + CreatedAt: timestamppb.New(createdAt), + UpdatedAt: timestamppb.New(updatedAt), + ExpireAt: timestamppb.New(expireAt), + }, + }) + }(), + wantErr: false, + }, + { + name: "should return checkout with product successfully", + setup: func(cs *mocks.CheckoutService) { + createdAt := time.Now() + updatedAt := time.Now() + expireAt := time.Now() + cs.EXPECT().GetByID(mock.Anything, "test-checkout-id-2").Return(checkout.Checkout{ + ID: "test-checkout-id-2", + CheckoutUrl: "https://checkout.stripe.com/session-2", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + State: "expired", + PlanID: "", + ProductID: "product-1", + CreatedAt: createdAt, + UpdatedAt: updatedAt, + ExpireAt: expireAt, + }, nil) + }, + req: connect.NewRequest(&frontierv1beta1.GetCheckoutRequest{ + OrgId: "test-org-id", + Id: "test-checkout-id-2", + }), + want: func() *connect.Response[frontierv1beta1.GetCheckoutResponse] { + createdAt := time.Now() + updatedAt := time.Now() + expireAt := time.Now() + return connect.NewResponse(&frontierv1beta1.GetCheckoutResponse{ + CheckoutSession: &frontierv1beta1.CheckoutSession{ + Id: "test-checkout-id-2", + CheckoutUrl: "https://checkout.stripe.com/session-2", + SuccessUrl: "https://example.com/success", + CancelUrl: "https://example.com/cancel", + State: "expired", + Plan: "", + Product: "product-1", + CreatedAt: timestamppb.New(createdAt), + UpdatedAt: timestamppb.New(updatedAt), + ExpireAt: timestamppb.New(expireAt), + }, + }) + }(), + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkoutService := mocks.NewCheckoutService(t) + if tt.setup != nil { + tt.setup(checkoutService) + } + h := &ConnectHandler{ + checkoutService: checkoutService, + } + got, err := h.GetCheckout(context.Background(), tt.req) + if tt.wantErr { + assert.Error(t, err) + connectErr := &connect.Error{} + assert.True(t, errors.As(err, &connectErr)) + assert.Equal(t, tt.wantErrCode, connectErr.Code()) + assert.Equal(t, tt.wantErrMsg.Error(), connectErr.Message()) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.NotNil(t, got) + wantSession := tt.want.Msg.GetCheckoutSession() + gotSession := got.Msg.GetCheckoutSession() + assert.Equal(t, wantSession.GetId(), gotSession.GetId()) + assert.Equal(t, wantSession.GetCheckoutUrl(), gotSession.GetCheckoutUrl()) + assert.Equal(t, wantSession.GetSuccessUrl(), gotSession.GetSuccessUrl()) + assert.Equal(t, wantSession.GetCancelUrl(), gotSession.GetCancelUrl()) + assert.Equal(t, wantSession.GetState(), gotSession.GetState()) + assert.Equal(t, wantSession.GetPlan(), gotSession.GetPlan()) + assert.Equal(t, wantSession.GetProduct(), gotSession.GetProduct()) + } + }) + } +} diff --git a/internal/api/v1beta1connect/errors.go b/internal/api/v1beta1connect/errors.go index d1a6d58c3..3e9bfb096 100644 --- a/internal/api/v1beta1connect/errors.go +++ b/internal/api/v1beta1connect/errors.go @@ -53,4 +53,5 @@ var ( ErrGroupNotFound = errors.New("group doesn't exist") ErrOrgNotFound = errors.New("org doesn't exist") ErrGroupMinOwnerCount = errors.New("group must have at least one owner, consider adding another owner before removing") + ErrPortalChangesKycCompleted = errors.New("customer portal changes not allowed: organization kyc completed") ) diff --git a/internal/api/v1beta1connect/mocks/checkout_service.go b/internal/api/v1beta1connect/mocks/checkout_service.go new file mode 100644 index 000000000..e916345eb --- /dev/null +++ b/internal/api/v1beta1connect/mocks/checkout_service.go @@ -0,0 +1,394 @@ +// Code generated by mockery v2.53.5. DO NOT EDIT. + +package mocks + +import ( + context "context" + + checkout "github.com/raystack/frontier/billing/checkout" + product "github.com/raystack/frontier/billing/product" + subscription "github.com/raystack/frontier/billing/subscription" + mock "github.com/stretchr/testify/mock" +) + +// CheckoutService is an autogenerated mock type for the CheckoutService type +type CheckoutService struct { + mock.Mock +} + +type CheckoutService_Expecter struct { + mock *mock.Mock +} + +func (_m *CheckoutService) EXPECT() *CheckoutService_Expecter { + return &CheckoutService_Expecter{mock: &_m.Mock} +} + +// Apply provides a mock function with given fields: ctx, ch +func (_m *CheckoutService) Apply(ctx context.Context, ch checkout.Checkout) (*subscription.Subscription, *product.Product, error) { + ret := _m.Called(ctx, ch) + + if len(ret) == 0 { + panic("no return value specified for Apply") + } + + var r0 *subscription.Subscription + var r1 *product.Product + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, checkout.Checkout) (*subscription.Subscription, *product.Product, error)); ok { + return rf(ctx, ch) + } + if rf, ok := ret.Get(0).(func(context.Context, checkout.Checkout) *subscription.Subscription); ok { + r0 = rf(ctx, ch) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*subscription.Subscription) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, checkout.Checkout) *product.Product); ok { + r1 = rf(ctx, ch) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*product.Product) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, checkout.Checkout) error); ok { + r2 = rf(ctx, ch) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// CheckoutService_Apply_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Apply' +type CheckoutService_Apply_Call struct { + *mock.Call +} + +// Apply is a helper method to define mock.On call +// - ctx context.Context +// - ch checkout.Checkout +func (_e *CheckoutService_Expecter) Apply(ctx interface{}, ch interface{}) *CheckoutService_Apply_Call { + return &CheckoutService_Apply_Call{Call: _e.mock.On("Apply", ctx, ch)} +} + +func (_c *CheckoutService_Apply_Call) Run(run func(ctx context.Context, ch checkout.Checkout)) *CheckoutService_Apply_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(checkout.Checkout)) + }) + return _c +} + +func (_c *CheckoutService_Apply_Call) Return(_a0 *subscription.Subscription, _a1 *product.Product, _a2 error) *CheckoutService_Apply_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +func (_c *CheckoutService_Apply_Call) RunAndReturn(run func(context.Context, checkout.Checkout) (*subscription.Subscription, *product.Product, error)) *CheckoutService_Apply_Call { + _c.Call.Return(run) + return _c +} + +// Create provides a mock function with given fields: ctx, ch +func (_m *CheckoutService) Create(ctx context.Context, ch checkout.Checkout) (checkout.Checkout, error) { + ret := _m.Called(ctx, ch) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 checkout.Checkout + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, checkout.Checkout) (checkout.Checkout, error)); ok { + return rf(ctx, ch) + } + if rf, ok := ret.Get(0).(func(context.Context, checkout.Checkout) checkout.Checkout); ok { + r0 = rf(ctx, ch) + } else { + r0 = ret.Get(0).(checkout.Checkout) + } + + if rf, ok := ret.Get(1).(func(context.Context, checkout.Checkout) error); ok { + r1 = rf(ctx, ch) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CheckoutService_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type CheckoutService_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - ch checkout.Checkout +func (_e *CheckoutService_Expecter) Create(ctx interface{}, ch interface{}) *CheckoutService_Create_Call { + return &CheckoutService_Create_Call{Call: _e.mock.On("Create", ctx, ch)} +} + +func (_c *CheckoutService_Create_Call) Run(run func(ctx context.Context, ch checkout.Checkout)) *CheckoutService_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(checkout.Checkout)) + }) + return _c +} + +func (_c *CheckoutService_Create_Call) Return(_a0 checkout.Checkout, _a1 error) *CheckoutService_Create_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CheckoutService_Create_Call) RunAndReturn(run func(context.Context, checkout.Checkout) (checkout.Checkout, error)) *CheckoutService_Create_Call { + _c.Call.Return(run) + return _c +} + +// CreateSessionForCustomerPortal provides a mock function with given fields: ctx, ch +func (_m *CheckoutService) CreateSessionForCustomerPortal(ctx context.Context, ch checkout.Checkout) (checkout.Checkout, error) { + ret := _m.Called(ctx, ch) + + if len(ret) == 0 { + panic("no return value specified for CreateSessionForCustomerPortal") + } + + var r0 checkout.Checkout + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, checkout.Checkout) (checkout.Checkout, error)); ok { + return rf(ctx, ch) + } + if rf, ok := ret.Get(0).(func(context.Context, checkout.Checkout) checkout.Checkout); ok { + r0 = rf(ctx, ch) + } else { + r0 = ret.Get(0).(checkout.Checkout) + } + + if rf, ok := ret.Get(1).(func(context.Context, checkout.Checkout) error); ok { + r1 = rf(ctx, ch) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CheckoutService_CreateSessionForCustomerPortal_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateSessionForCustomerPortal' +type CheckoutService_CreateSessionForCustomerPortal_Call struct { + *mock.Call +} + +// CreateSessionForCustomerPortal is a helper method to define mock.On call +// - ctx context.Context +// - ch checkout.Checkout +func (_e *CheckoutService_Expecter) CreateSessionForCustomerPortal(ctx interface{}, ch interface{}) *CheckoutService_CreateSessionForCustomerPortal_Call { + return &CheckoutService_CreateSessionForCustomerPortal_Call{Call: _e.mock.On("CreateSessionForCustomerPortal", ctx, ch)} +} + +func (_c *CheckoutService_CreateSessionForCustomerPortal_Call) Run(run func(ctx context.Context, ch checkout.Checkout)) *CheckoutService_CreateSessionForCustomerPortal_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(checkout.Checkout)) + }) + return _c +} + +func (_c *CheckoutService_CreateSessionForCustomerPortal_Call) Return(_a0 checkout.Checkout, _a1 error) *CheckoutService_CreateSessionForCustomerPortal_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CheckoutService_CreateSessionForCustomerPortal_Call) RunAndReturn(run func(context.Context, checkout.Checkout) (checkout.Checkout, error)) *CheckoutService_CreateSessionForCustomerPortal_Call { + _c.Call.Return(run) + return _c +} + +// CreateSessionForPaymentMethod provides a mock function with given fields: ctx, ch +func (_m *CheckoutService) CreateSessionForPaymentMethod(ctx context.Context, ch checkout.Checkout) (checkout.Checkout, error) { + ret := _m.Called(ctx, ch) + + if len(ret) == 0 { + panic("no return value specified for CreateSessionForPaymentMethod") + } + + var r0 checkout.Checkout + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, checkout.Checkout) (checkout.Checkout, error)); ok { + return rf(ctx, ch) + } + if rf, ok := ret.Get(0).(func(context.Context, checkout.Checkout) checkout.Checkout); ok { + r0 = rf(ctx, ch) + } else { + r0 = ret.Get(0).(checkout.Checkout) + } + + if rf, ok := ret.Get(1).(func(context.Context, checkout.Checkout) error); ok { + r1 = rf(ctx, ch) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CheckoutService_CreateSessionForPaymentMethod_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateSessionForPaymentMethod' +type CheckoutService_CreateSessionForPaymentMethod_Call struct { + *mock.Call +} + +// CreateSessionForPaymentMethod is a helper method to define mock.On call +// - ctx context.Context +// - ch checkout.Checkout +func (_e *CheckoutService_Expecter) CreateSessionForPaymentMethod(ctx interface{}, ch interface{}) *CheckoutService_CreateSessionForPaymentMethod_Call { + return &CheckoutService_CreateSessionForPaymentMethod_Call{Call: _e.mock.On("CreateSessionForPaymentMethod", ctx, ch)} +} + +func (_c *CheckoutService_CreateSessionForPaymentMethod_Call) Run(run func(ctx context.Context, ch checkout.Checkout)) *CheckoutService_CreateSessionForPaymentMethod_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(checkout.Checkout)) + }) + return _c +} + +func (_c *CheckoutService_CreateSessionForPaymentMethod_Call) Return(_a0 checkout.Checkout, _a1 error) *CheckoutService_CreateSessionForPaymentMethod_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CheckoutService_CreateSessionForPaymentMethod_Call) RunAndReturn(run func(context.Context, checkout.Checkout) (checkout.Checkout, error)) *CheckoutService_CreateSessionForPaymentMethod_Call { + _c.Call.Return(run) + return _c +} + +// GetByID provides a mock function with given fields: ctx, id +func (_m *CheckoutService) GetByID(ctx context.Context, id string) (checkout.Checkout, error) { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetByID") + } + + var r0 checkout.Checkout + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (checkout.Checkout, error)); ok { + return rf(ctx, id) + } + if rf, ok := ret.Get(0).(func(context.Context, string) checkout.Checkout); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Get(0).(checkout.Checkout) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CheckoutService_GetByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetByID' +type CheckoutService_GetByID_Call struct { + *mock.Call +} + +// GetByID is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *CheckoutService_Expecter) GetByID(ctx interface{}, id interface{}) *CheckoutService_GetByID_Call { + return &CheckoutService_GetByID_Call{Call: _e.mock.On("GetByID", ctx, id)} +} + +func (_c *CheckoutService_GetByID_Call) Run(run func(ctx context.Context, id string)) *CheckoutService_GetByID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *CheckoutService_GetByID_Call) Return(_a0 checkout.Checkout, _a1 error) *CheckoutService_GetByID_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CheckoutService_GetByID_Call) RunAndReturn(run func(context.Context, string) (checkout.Checkout, error)) *CheckoutService_GetByID_Call { + _c.Call.Return(run) + return _c +} + +// List provides a mock function with given fields: ctx, filter +func (_m *CheckoutService) List(ctx context.Context, filter checkout.Filter) ([]checkout.Checkout, error) { + ret := _m.Called(ctx, filter) + + if len(ret) == 0 { + panic("no return value specified for List") + } + + var r0 []checkout.Checkout + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, checkout.Filter) ([]checkout.Checkout, error)); ok { + return rf(ctx, filter) + } + if rf, ok := ret.Get(0).(func(context.Context, checkout.Filter) []checkout.Checkout); ok { + r0 = rf(ctx, filter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]checkout.Checkout) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, checkout.Filter) error); ok { + r1 = rf(ctx, filter) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CheckoutService_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List' +type CheckoutService_List_Call struct { + *mock.Call +} + +// List is a helper method to define mock.On call +// - ctx context.Context +// - filter checkout.Filter +func (_e *CheckoutService_Expecter) List(ctx interface{}, filter interface{}) *CheckoutService_List_Call { + return &CheckoutService_List_Call{Call: _e.mock.On("List", ctx, filter)} +} + +func (_c *CheckoutService_List_Call) Run(run func(ctx context.Context, filter checkout.Filter)) *CheckoutService_List_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(checkout.Filter)) + }) + return _c +} + +func (_c *CheckoutService_List_Call) Return(_a0 []checkout.Checkout, _a1 error) *CheckoutService_List_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *CheckoutService_List_Call) RunAndReturn(run func(context.Context, checkout.Filter) ([]checkout.Checkout, error)) *CheckoutService_List_Call { + _c.Call.Return(run) + return _c +} + +// NewCheckoutService creates a new instance of CheckoutService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCheckoutService(t interface { + mock.TestingT + Cleanup(func()) +}) *CheckoutService { + mock := &CheckoutService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} \ No newline at end of file