From 4df6bbc764dc5b4e199a6ef0caaa86c9d44c8218 Mon Sep 17 00:00:00 2001 From: Nick Misasi Date: Fri, 30 Oct 2026 12:20:38 -0400 Subject: [PATCH] Remove unused cloud invoice endpoints (#39523) * Remove unused cloud invoice endpoints Remove GET /api/v4/cloud/subscription/invoices and /invoices/{invoice_id}/pdf, their CloudInterface methods and the Client4.GetInvoicesForSubscription method. Mark the invoice model types as deprecated since server/public is a published module. Co-authored-by: Nick Misasi * Clarify deprecation notes on cloud invoice model types Co-authored-by: Nick Misasi --------- Co-authored-by: Cursor Agent --- api/v4/source/cloud.yaml | 63 ----------------- api/v4/source/definitions.yaml | 50 -------------- server/channels/api4/cloud.go | 79 ---------------------- server/channels/api4/handlers.go | 2 +- server/channels/web/context.go | 12 ---- server/channels/web/params.go | 4 -- server/einterfaces/cloud.go | 2 - server/einterfaces/mocks/CloudInterface.go | 67 ------------------ server/public/model/client4.go | 9 --- server/public/model/cloud.go | 29 +++++--- 10 files changed, 19 insertions(+), 298 deletions(-) diff --git a/api/v4/source/cloud.yaml b/api/v4/source/cloud.yaml index d6a7c3fb1e8..27f7cd8b2e2 100644 --- a/api/v4/source/cloud.yaml +++ b/api/v4/source/cloud.yaml @@ -215,69 +215,6 @@ $ref: "#/components/responses/Forbidden" "501": $ref: "#/components/responses/NotImplemented" - /api/v4/cloud/subscription/invoices: - get: - tags: - - cloud - summary: Get cloud subscription invoices - description: > - Retrieves the invoices for the subscription bound to this installation. - - ##### Permissions - - Must have `manage_system` permission and be licensed for Cloud. - - __Minimum server version__: 5.30 - __Note:__ This is intended for internal use and is subject to change. - operationId: GetInvoicesForSubscription - responses: - "200": - description: Subscription invoices returned successfully - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/Invoice" - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "501": - $ref: "#/components/responses/NotImplemented" - /api/v4/cloud/subscription/invoices/{invoice_id}/pdf: - get: - tags: - - cloud - summary: Get cloud invoice PDF - description: > - Retrieves the PDF for the invoice passed as parameter - - ##### Permissions - - Must have `manage_system` permission and be licensed for Cloud. - - __Minimum server version__: 5.30 - __Note:__ This is intended for internal use and is subject to change. - operationId: GetInvoiceForSubscriptionAsPdf - parameters: - - name: invoice_id - in: path - description: Invoice ID - required: true - schema: - type: string - responses: - "400": - $ref: "#/components/responses/BadRequest" - "401": - $ref: "#/components/responses/Unauthorized" - "403": - $ref: "#/components/responses/Forbidden" - "501": - $ref: "#/components/responses/NotImplemented" /api/v4/hosted_customer/signup_available: get: tags: diff --git a/api/v4/source/definitions.yaml b/api/v4/source/definitions.yaml index b0b30b6f142..f532c69fa8a 100644 --- a/api/v4/source/definitions.yaml +++ b/api/v4/source/definitions.yaml @@ -3676,56 +3676,6 @@ components: type: integer is_paid_tier: type: string - Invoice: - type: object - properties: - id: - type: string - number: - type: string - create_at: - type: integer - format: int64 - total: - type: integer - format: int64 - tax: - type: integer - format: int64 - status: - type: string - period_start: - type: integer - format: int64 - period_end: - type: integer - format: int64 - subscription_id: - type: string - item: - type: array - items: - $ref: "#/components/schemas/InvoiceLineItem" - InvoiceLineItem: - type: object - properties: - price_id: - type: string - total: - type: integer - format: int64 - quantity: - type: integer - format: int64 - price_per_unit: - type: integer - format: int64 - description: - type: string - metadata: - type: array - items: - type: string Group: type: object properties: diff --git a/server/channels/api4/cloud.go b/server/channels/api4/cloud.go index 7aea2aa52b1..06764bdf7f6 100644 --- a/server/channels/api4/cloud.go +++ b/server/channels/api4/cloud.go @@ -4,16 +4,12 @@ package api4 import ( - "bytes" - "encoding/binary" "encoding/json" "io" "net/http" - "time" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" - "github.com/mattermost/mattermost/server/v8/platform/shared/web" ) func (api *API) InitCloud() { @@ -27,8 +23,6 @@ func (api *API) InitCloud() { // GET /api/v4/cloud/subscription api.BaseRoutes.Cloud.Handle("/subscription", api.APISessionRequired(getSubscription)).Methods(http.MethodGet) - api.BaseRoutes.Cloud.Handle("/subscription/invoices", api.APISessionRequired(getInvoicesForSubscription)).Methods(http.MethodGet) - api.BaseRoutes.Cloud.Handle("/subscription/invoices/{invoice_id:[_A-Za-z0-9]+}/pdf", api.APISessionRequiredGzip(getSubscriptionInvoicePDF)).Methods(http.MethodGet) // GET /api/v4/cloud/validate-business-email api.BaseRoutes.Cloud.Handle("/validate-business-email", api.APISessionRequired(validateBusinessEmail)).Methods(http.MethodPost) @@ -372,79 +366,6 @@ func getInstallation(c *Context, w http.ResponseWriter, r *http.Request) { } } -func getInvoicesForSubscription(c *Context, w http.ResponseWriter, r *http.Request) { - ensured := ensureCloudInterface(c, "Api4.getInvoicesForSubscription") - if !ensured { - return - } - - if !c.App.Channels().License().IsCloud() { - c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.license_error", nil, "", http.StatusForbidden) - return - } - - if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) { - c.SetPermissionError(model.PermissionSysconsoleReadBilling) - return - } - - invoices, appErr := c.App.Cloud().GetInvoicesForSubscription(c.AppContext.Session().UserId) - if appErr != nil { - c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr) - return - } - - json, err := json.Marshal(invoices) - if err != nil { - c.Err = model.NewAppError("Api4.getInvoicesForSubscription", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err) - return - } - - if _, err := w.Write(json); err != nil { - c.Logger.Warn("Error while writing response", mlog.Err(err)) - } -} - -func getSubscriptionInvoicePDF(c *Context, w http.ResponseWriter, r *http.Request) { - ensured := ensureCloudInterface(c, "Api4.getSubscriptionInvoicePDF") - if !ensured { - return - } - - if !c.App.Channels().License().IsCloud() { - c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.license_error", nil, "", http.StatusForbidden) - return - } - - c.RequireInvoiceId() - if c.Err != nil { - return - } - - if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionSysconsoleReadBilling) { - c.SetPermissionError(model.PermissionSysconsoleReadBilling) - return - } - - pdfData, filename, appErr := c.App.Cloud().GetInvoicePDF(c.AppContext.Session().UserId, c.Params.InvoiceId) - if appErr != nil { - c.Err = model.NewAppError("Api4.getSubscriptionInvoicePDF", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr) - return - } - - web.WriteFileResponse( - filename, - "application/pdf", - int64(binary.Size(pdfData)), - time.Now(), - *c.App.Config().ServiceSettings.WebserverMode, - bytes.NewReader(pdfData), - false, - w, - r, - ) -} - func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) { ensured := ensureCloudInterface(c, "Api4.handleCWSWebhook") if !ensured { diff --git a/server/channels/api4/handlers.go b/server/channels/api4/handlers.go index dbf4cabebf1..b57eb7f101f 100644 --- a/server/channels/api4/handlers.go +++ b/server/channels/api4/handlers.go @@ -79,7 +79,7 @@ func (api *API) APISessionRequired(h handlerFunc, opts ...APIHandlerOption) http } // APISessionRequiredGzip is APISessionRequired without brotli: large file downloads -// (e.g. log files, invoice PDFs) compress about as well under gzip as under brotli, +// (e.g. log files) compress about as well under gzip as under brotli, // and brotli's memory footprint scales with response size, so these stay on gzip // rather than paying that cost. func (api *API) APISessionRequiredGzip(h handlerFunc, opts ...APIHandlerOption) http.Handler { diff --git a/server/channels/web/context.go b/server/channels/web/context.go index 70385ae4143..5f737b365c5 100644 --- a/server/channels/web/context.go +++ b/server/channels/web/context.go @@ -790,18 +790,6 @@ func (c *Context) RequireBotUserId() *Context { return c } -func (c *Context) RequireInvoiceId() *Context { - if c.Err != nil { - return c - } - - if len(c.Params.InvoiceId) != 27 && c.Params.InvoiceId != model.UpcomingInvoice { - c.SetInvalidURLParam("invoice_id") - } - - return c -} - func (c *Context) RequireContentReviewerId() *Context { if c.Err != nil { return c diff --git a/server/channels/web/params.go b/server/channels/web/params.go index 69d0b530e0f..cece2cc94a0 100644 --- a/server/channels/web/params.go +++ b/server/channels/web/params.go @@ -121,9 +121,6 @@ type Params struct { // Views ViewId string - // Cloud - InvoiceId string - // Custom Profile Attributes FieldId string @@ -191,7 +188,6 @@ func ParamsFromRequest(r *http.Request) *Params { params.SchemeId = props["scheme_id"] params.GroupId = props["group_id"] params.RemoteId = props["remote_id"] - params.InvoiceId = props["invoice_id"] params.OutgoingOAuthConnectionID = props["outgoing_oauth_connection_id"] params.ExcludeOffline, _ = strconv.ParseBool(query.Get("exclude_offline")) params.InChannel = query.Get("in_channel") diff --git a/server/einterfaces/cloud.go b/server/einterfaces/cloud.go index b2b0bf99510..85b2a441a4e 100644 --- a/server/einterfaces/cloud.go +++ b/server/einterfaces/cloud.go @@ -18,8 +18,6 @@ type CloudInterface interface { GetCloudCustomer(userID string) (*model.CloudCustomer, error) GetSubscription(userID string) (*model.Subscription, error) - GetInvoicesForSubscription(userID string) ([]*model.Invoice, error) - GetInvoicePDF(userID, invoiceID string) ([]byte, string, error) ChangeSubscription(userID, subscriptionID string, subscriptionChange *model.SubscriptionChange) (*model.Subscription, error) diff --git a/server/einterfaces/mocks/CloudInterface.go b/server/einterfaces/mocks/CloudInterface.go index 0fb7ff69c14..5ac6e759226 100644 --- a/server/einterfaces/mocks/CloudInterface.go +++ b/server/einterfaces/mocks/CloudInterface.go @@ -322,73 +322,6 @@ func (_m *CloudInterface) GetInstallation(userID string) (*model.Installation, e return r0, r1 } -// GetInvoicePDF provides a mock function with given fields: userID, invoiceID -func (_m *CloudInterface) GetInvoicePDF(userID string, invoiceID string) ([]byte, string, error) { - ret := _m.Called(userID, invoiceID) - - if len(ret) == 0 { - panic("no return value specified for GetInvoicePDF") - } - - var r0 []byte - var r1 string - var r2 error - if rf, ok := ret.Get(0).(func(string, string) ([]byte, string, error)); ok { - return rf(userID, invoiceID) - } - if rf, ok := ret.Get(0).(func(string, string) []byte); ok { - r0 = rf(userID, invoiceID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - if rf, ok := ret.Get(1).(func(string, string) string); ok { - r1 = rf(userID, invoiceID) - } else { - r1 = ret.Get(1).(string) - } - - if rf, ok := ret.Get(2).(func(string, string) error); ok { - r2 = rf(userID, invoiceID) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// GetInvoicesForSubscription provides a mock function with given fields: userID -func (_m *CloudInterface) GetInvoicesForSubscription(userID string) ([]*model.Invoice, error) { - ret := _m.Called(userID) - - if len(ret) == 0 { - panic("no return value specified for GetInvoicesForSubscription") - } - - var r0 []*model.Invoice - var r1 error - if rf, ok := ret.Get(0).(func(string) ([]*model.Invoice, error)); ok { - return rf(userID) - } - if rf, ok := ret.Get(0).(func(string) []*model.Invoice); ok { - r0 = rf(userID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*model.Invoice) - } - } - - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(userID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetSelfHostedProducts provides a mock function with given fields: userID func (_m *CloudInterface) GetSelfHostedProducts(userID string) ([]*model.Product, error) { ret := _m.Called(userID) diff --git a/server/public/model/client4.go b/server/public/model/client4.go index bdbaaf96d52..bc733aafb83 100644 --- a/server/public/model/client4.go +++ b/server/public/model/client4.go @@ -7478,15 +7478,6 @@ func (c *Client4) GetSubscription(ctx context.Context) (*Subscription, *Response return DecodeJSONFromResponse[*Subscription](r) } -func (c *Client4) GetInvoicesForSubscription(ctx context.Context) ([]*Invoice, *Response, error) { - r, err := c.doAPIGet(ctx, c.cloudRoute().Join("subscription", "invoices"), "") - if err != nil { - return nil, BuildResponse(r), err - } - defer closeBody(r) - return DecodeJSONFromResponse[[]*Invoice](r) -} - func (c *Client4) ListImports(ctx context.Context) ([]string, *Response, error) { r, err := c.doAPIGet(ctx, c.importsRoute(), "") if err != nil { diff --git a/server/public/model/cloud.go b/server/public/model/cloud.go index a2da17c43c4..dca46a11a3b 100644 --- a/server/public/model/cloud.go +++ b/server/public/model/cloud.go @@ -18,6 +18,7 @@ const ( EventTypeTriggerDelinquencyEmail = "trigger-delinquency-email" ) +// Deprecated: The cloud subscription invoice endpoints have been removed. const UpcomingInvoice = "upcoming" var MockCWS string @@ -170,17 +171,19 @@ type PaymentMethod struct { // Subscription model represents a subscription on the system. type Subscription struct { - ID string `json:"id"` - CustomerID string `json:"customer_id"` - ProductID string `json:"product_id"` - AddOns []string `json:"add_ons"` - StartAt int64 `json:"start_at"` - EndAt int64 `json:"end_at"` - CreateAt int64 `json:"create_at"` - Seats int `json:"seats"` - Status string `json:"status"` - DNS string `json:"dns"` - LastInvoice *Invoice `json:"last_invoice"` + ID string `json:"id"` + CustomerID string `json:"customer_id"` + ProductID string `json:"product_id"` + AddOns []string `json:"add_ons"` + StartAt int64 `json:"start_at"` + EndAt int64 `json:"end_at"` + CreateAt int64 `json:"create_at"` + Seats int `json:"seats"` + Status string `json:"status"` + DNS string `json:"dns"` + // Deprecated: Retained for compatibility; the cloud subscription invoice endpoints have been removed. + LastInvoice *Invoice `json:"last_invoice"` + // Deprecated: Retained for compatibility; the cloud subscription invoice endpoints have been removed. UpcomingInvoice *Invoice `json:"upcoming_invoice"` IsFreeTrial string `json:"is_free_trial"` TrialEndAt int64 `json:"trial_end_at"` @@ -227,6 +230,8 @@ func (s *Subscription) GetWorkSpaceNameFromDNS() string { } // Invoice model represents a cloud invoice +// +// Deprecated: The cloud subscription invoice endpoints have been removed. type Invoice struct { ID string `json:"id"` Number string `json:"number"` @@ -243,6 +248,8 @@ type Invoice struct { } // InvoiceLineItem model represents a cloud invoice lineitem tied to an invoice. +// +// Deprecated: The cloud subscription invoice endpoints have been removed. type InvoiceLineItem struct { PriceID string `json:"price_id"` Total int64 `json:"total"`