apigen: project delete endpoint

Implemented project delete endpoint for REST API.
Added project usage status check service method to indicate if project can be deleted.
Updated project invoice status check method to indicate if project can be deleted.

Change-Id: I57dc96efb072517144252001ab5405446c9cdeb4
This commit is contained in:
Vitalii 2022-04-28 18:59:55 +03:00
parent 911cc1e163
commit f56504de2a
8 changed files with 343 additions and 166 deletions

View File

@ -58,6 +58,11 @@ func (eg *EndpointGroup) Post(path string, endpoint *Endpoint) {
eg.addEndpoint(path, http.MethodPost, endpoint)
}
// Delete adds new DELETE endpoint to endpoints group.
func (eg *EndpointGroup) Delete(path string, endpoint *Endpoint) {
eg.addEndpoint(path, http.MethodDelete, endpoint)
}
// addEndpoint adds new endpoint to endpoints list.
func (eg *EndpointGroup) addEndpoint(path, method string, endpoint *Endpoint) {
pathMethod := PathMethod{

View File

@ -104,12 +104,17 @@ func (a *API) generateGo() ([]byte, error) {
for _, group := range a.EndpointGroups {
p("type %sService interface {", group.Name)
for _, e := range group.Endpoints {
responseType := reflect.TypeOf(e.Response)
var params string
for _, param := range e.Params {
params += param.Type.String() + ", "
}
p("%s(context.Context, "+params+") (%s, api.HTTPError)", e.MethodName, a.handleTypesPackage(responseType))
if e.Response != nil {
responseType := reflect.TypeOf(e.Response)
p("%s(context.Context, "+params+") (%s, api.HTTPError)", e.MethodName, a.handleTypesPackage(responseType))
} else {
p("%s(context.Context, "+params+") (api.HTTPError)", e.MethodName)
}
}
p("}")
p("")
@ -184,68 +189,41 @@ func (a *API) generateGo() ([]byte, error) {
for _, param := range endpoint.Params {
switch param.Type {
case reflect.TypeOf(uuid.UUID{}):
p("%s, err := uuid.FromString(r.URL.Query().Get(\"%s\"))", param.Name, param.Name)
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
handleUUIDQuery(p, param)
continue
case reflect.TypeOf(time.Time{}):
p("%s, err := time.Parse(dateLayout, r.URL.Query().Get(\"%s\"))", param.Name, param.Name)
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
handleTimeQuery(p, param)
continue
case reflect.TypeOf(""):
p("%s := r.URL.Query().Get(\"%s\")", param.Name, param.Name)
p("if %s == \"\" {", param.Name)
p("api.ServeError(h.log, w, http.StatusBadRequest, errs.New(\"parameter '%s' can't be empty\"))", param.Name)
p("return")
p("}")
p("")
handleStringQuery(p, param)
continue
}
}
case http.MethodPatch:
for _, param := range endpoint.Params {
if param.Type == reflect.TypeOf(uuid.UUID{}) {
p("%sParam, ok := mux.Vars(r)[\"%s\"]", param.Name, param.Name)
p("if !ok {")
p("api.ServeError(h.log, w, http.StatusBadRequest, errs.New(\"missing %s route param\"))", param.Name)
p("return")
p("}")
p("")
p("%s, err := uuid.FromString(%sParam)", param.Name, param.Name)
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
handleUUIDParam(p, param)
} else {
p("%s := &%s{}", param.Name, param.Type)
p("if err = json.NewDecoder(r.Body).Decode(&%s); err != nil {", param.Name)
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
handleBody(p, param)
}
}
case http.MethodPost:
for _, param := range endpoint.Params {
p("%s := &%s{}", param.Name, param.Type)
p("if err = json.NewDecoder(r.Body).Decode(&%s); err != nil {", param.Name)
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
handleBody(p, param)
}
case http.MethodDelete:
for _, param := range endpoint.Params {
handleUUIDParam(p, param)
}
}
methodFormat := "retVal, httpErr := h.service.%s(ctx, "
var methodFormat string
if endpoint.Response != nil {
methodFormat = "retVal, httpErr := h.service.%s(ctx, "
} else {
methodFormat = "httpErr := h.service.%s(ctx, "
}
switch pathMethod.Method {
case http.MethodGet:
for _, methodParam := range endpoint.Params {
@ -263,22 +241,30 @@ func (a *API) generateGo() ([]byte, error) {
for _, methodParam := range endpoint.Params {
methodFormat += "*" + methodParam.Name + ", "
}
case http.MethodDelete:
for _, methodParam := range endpoint.Params {
methodFormat += methodParam.Name + ", "
}
}
methodFormat += ")"
p(methodFormat, endpoint.MethodName)
p("if httpErr.Err != nil {")
p("api.ServeError(h.log, w, httpErr.Status, httpErr.Err)")
if endpoint.Response == nil {
p("}")
p("}")
continue
}
p("return")
p("}")
p("")
p("")
p("err = json.NewEncoder(w).Encode(retVal)")
p("if err != nil {")
p("h.log.Debug(\"failed to write json %s response\", zap.Error(Err%sAPI.Wrap(err)))", endpoint.MethodName, cases.Title(language.Und).String(group.Prefix))
p("}")
p("}")
p("")
}
}
@ -292,7 +278,7 @@ func (a *API) generateGo() ([]byte, error) {
// handleTypesPackage handles the way some type is used in generated code.
// If type is from the same package then we use only type's name.
// If type is from external package then we use type along with it's appropriate package name.
// If type is from external package then we use type along with its appropriate package name.
func (a *API) handleTypesPackage(t reflect.Type) interface{} {
if strings.HasPrefix(t.String(), a.PackageName) {
return t.Elem().Name()
@ -300,3 +286,60 @@ func (a *API) handleTypesPackage(t reflect.Type) interface{} {
return t
}
// handleStringQuery handles request query param of type string.
func handleStringQuery(p func(format string, a ...interface{}), param Param) {
p("%s := r.URL.Query().Get(\"%s\")", param.Name, param.Name)
p("if %s == \"\" {", param.Name)
p("api.ServeError(h.log, w, http.StatusBadRequest, errs.New(\"parameter '%s' can't be empty\"))", param.Name)
p("return")
p("}")
p("")
}
// handleUUIDQuery handles request query param of type uuid.UUID.
func handleUUIDQuery(p func(format string, a ...interface{}), param Param) {
p("%s, err := uuid.FromString(r.URL.Query().Get(\"%s\"))", param.Name, param.Name)
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
}
// handleTimeQuery handles request query param of type time.Time.
func handleTimeQuery(p func(format string, a ...interface{}), param Param) {
p("%s, err := time.Parse(dateLayout, r.URL.Query().Get(\"%s\"))", param.Name, param.Name)
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
}
// handleUUIDParam handles request inline param of type uuid.UUID.
func handleUUIDParam(p func(format string, a ...interface{}), param Param) {
p("%sParam, ok := mux.Vars(r)[\"%s\"]", param.Name, param.Name)
p("if !ok {")
p("api.ServeError(h.log, w, http.StatusBadRequest, errs.New(\"missing %s route param\"))", param.Name)
p("return")
p("}")
p("")
p("%s, err := uuid.FromString(%sParam)", param.Name, param.Name)
p("if err != nil {")
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
}
// handleBody handles request body.
func handleBody(p func(format string, a ...interface{}), param Param) {
p("%s := &%s{}", param.Name, param.Type)
p("if err = json.NewDecoder(r.Body).Decode(&%s); err != nil {", param.Name)
p("api.ServeError(h.log, w, http.StatusBadRequest, err)")
p("return")
p("}")
p("")
}

View File

@ -26,11 +26,12 @@ var ErrApikeysAPI = errs.Class("consoleapi apikeys api")
var ErrUsersAPI = errs.Class("consoleapi users api")
type ProjectManagementService interface {
GenGetUsersProjects(context.Context) ([]console.Project, api.HTTPError)
GenGetSingleBucketUsageRollup(context.Context, uuid.UUID, string, time.Time, time.Time) (*accounting.BucketUsageRollup, api.HTTPError)
GenGetBucketUsageRollups(context.Context, uuid.UUID, time.Time, time.Time) ([]accounting.BucketUsageRollup, api.HTTPError)
GenCreateProject(context.Context, console.ProjectInfo) (*console.Project, api.HTTPError)
GenUpdateProject(context.Context, uuid.UUID, console.ProjectInfo) (*console.Project, api.HTTPError)
GenDeleteProject(context.Context, uuid.UUID) api.HTTPError
GenGetUsersProjects(context.Context) ([]console.Project, api.HTTPError)
}
type APIKeyManagementService interface {
@ -70,11 +71,12 @@ func NewProjectManagement(log *zap.Logger, service ProjectManagementService, rou
}
projectsRouter := router.PathPrefix("/api/v0/projects").Subrouter()
projectsRouter.HandleFunc("/bucket-rollups", handler.handleGenGetBucketUsageRollups).Methods("GET")
projectsRouter.HandleFunc("/create", handler.handleGenCreateProject).Methods("POST")
projectsRouter.HandleFunc("/update/{id}", handler.handleGenUpdateProject).Methods("PATCH")
projectsRouter.HandleFunc("/delete/{id}", handler.handleGenDeleteProject).Methods("DELETE")
projectsRouter.HandleFunc("/", handler.handleGenGetUsersProjects).Methods("GET")
projectsRouter.HandleFunc("/bucket-rollup", handler.handleGenGetSingleBucketUsageRollup).Methods("GET")
projectsRouter.HandleFunc("/bucket-rollups", handler.handleGenGetBucketUsageRollups).Methods("GET")
return handler
}
@ -105,6 +107,55 @@ func NewUserManagement(log *zap.Logger, service UserManagementService, router *m
return handler
}
func (h *ProjectManagementHandler) handleGenGetSingleBucketUsageRollup(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
if err != nil {
api.ServeError(h.log, w, http.StatusUnauthorized, err)
return
}
projectID, err := uuid.FromString(r.URL.Query().Get("projectID"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
bucket := r.URL.Query().Get("bucket")
if bucket == "" {
api.ServeError(h.log, w, http.StatusBadRequest, errs.New("parameter 'bucket' can't be empty"))
return
}
since, err := time.Parse(dateLayout, r.URL.Query().Get("since"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
before, err := time.Parse(dateLayout, r.URL.Query().Get("before"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
retVal, httpErr := h.service.GenGetSingleBucketUsageRollup(ctx, projectID, bucket, since, before)
if httpErr.Err != nil {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
return
}
err = json.NewEncoder(w).Encode(retVal)
if err != nil {
h.log.Debug("failed to write json GenGetSingleBucketUsageRollup response", zap.Error(ErrProjectsAPI.Wrap(err)))
}
}
func (h *ProjectManagementHandler) handleGenGetBucketUsageRollups(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
@ -222,6 +273,37 @@ func (h *ProjectManagementHandler) handleGenUpdateProject(w http.ResponseWriter,
}
}
func (h *ProjectManagementHandler) handleGenDeleteProject(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
if err != nil {
api.ServeError(h.log, w, http.StatusUnauthorized, err)
return
}
idParam, ok := mux.Vars(r)["id"]
if !ok {
api.ServeError(h.log, w, http.StatusBadRequest, errs.New("missing id route param"))
return
}
id, err := uuid.FromString(idParam)
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
httpErr := h.service.GenDeleteProject(ctx, id)
if httpErr.Err != nil {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
}
}
func (h *ProjectManagementHandler) handleGenGetUsersProjects(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
@ -247,55 +329,6 @@ func (h *ProjectManagementHandler) handleGenGetUsersProjects(w http.ResponseWrit
}
}
func (h *ProjectManagementHandler) handleGenGetSingleBucketUsageRollup(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
ctx, err = h.auth.IsAuthenticated(ctx, r, true, true)
if err != nil {
api.ServeError(h.log, w, http.StatusUnauthorized, err)
return
}
projectID, err := uuid.FromString(r.URL.Query().Get("projectID"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
bucket := r.URL.Query().Get("bucket")
if bucket == "" {
api.ServeError(h.log, w, http.StatusBadRequest, errs.New("parameter 'bucket' can't be empty"))
return
}
since, err := time.Parse(dateLayout, r.URL.Query().Get("since"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
before, err := time.Parse(dateLayout, r.URL.Query().Get("before"))
if err != nil {
api.ServeError(h.log, w, http.StatusBadRequest, err)
return
}
retVal, httpErr := h.service.GenGetSingleBucketUsageRollup(ctx, projectID, bucket, since, before)
if httpErr.Err != nil {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
return
}
err = json.NewEncoder(w).Encode(retVal)
if err != nil {
h.log.Debug("failed to write json GenGetSingleBucketUsageRollup response", zap.Error(ErrProjectsAPI.Wrap(err)))
}
}
func (h *APIKeyManagementHandler) handleGenCreateAPIKey(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error

View File

@ -12,7 +12,7 @@ curl -i -L \
```
## Successful responses
All the requests a non-empty body for the resource that we're interacting with.
All the requests (except DELETE) have a non-empty response body for the resource that you're interacting with.
Example:
@ -184,6 +184,13 @@ A successful response body:
}
```
#### DELETE /api/v0/projects/delete/{uuid string}
Deletes project by id.
Note: all the buckets and access grants have to be deleted first and there should not be any usage during current month for paid tier users.
!!!WARNING!!! Project ID is used as encryption salt. Please don't send it to anyone. We're going to fix it soon.
### Macaroon API key API Endpoints
#### POST /api/v0/apikeys/create
Creates new macaroon API key.

View File

@ -25,6 +25,37 @@ func main() {
{
g := a.Group("ProjectManagement", "projects")
g.Post("/create", &apigen.Endpoint{
Name: "Create new Project",
Description: "Creates new Project with given info",
MethodName: "GenCreateProject",
Response: &console.Project{},
Params: []apigen.Param{
apigen.NewParam("projectInfo", console.ProjectInfo{}),
},
})
g.Patch("/update/{id}", &apigen.Endpoint{
Name: "Update Project",
Description: "Updates project with given info",
MethodName: "GenUpdateProject",
Response: &console.Project{},
Params: []apigen.Param{
apigen.NewParam("id", uuid.UUID{}),
apigen.NewParam("projectInfo", console.ProjectInfo{}),
},
})
g.Delete("/delete/{id}", &apigen.Endpoint{
Name: "Delete Project",
Description: "Deletes project by id",
MethodName: "GenDeleteProject",
Response: nil,
Params: []apigen.Param{
apigen.NewParam("id", uuid.UUID{}),
},
})
g.Get("/", &apigen.Endpoint{
Name: "Get Projects",
Description: "Gets all projects user has",
@ -56,27 +87,6 @@ func main() {
apigen.NewParam("before", time.Time{}),
},
})
g.Post("/create", &apigen.Endpoint{
Name: "Create new Project",
Description: "Creates new Project with given info",
MethodName: "GenCreateProject",
Response: &console.Project{},
Params: []apigen.Param{
apigen.NewParam("projectInfo", console.ProjectInfo{}),
},
})
g.Patch("/update/{id}", &apigen.Endpoint{
Name: "Update Project",
Description: "Updates project with given info",
MethodName: "GenUpdateProject",
Response: &console.Project{},
Params: []apigen.Param{
apigen.NewParam("id", uuid.UUID{}),
apigen.NewParam("projectInfo", console.ProjectInfo{}),
},
})
}
{

View File

@ -538,19 +538,31 @@ func (payment Payments) checkOutstandingInvoice(ctx context.Context) (err error)
return nil
}
// checkProjectInvoicingStatus returns if for the given project there are outstanding project records and/or usage
// checkProjectInvoicingStatus returns error if for the given project there are outstanding project records and/or usage
// which have not been applied/invoiced yet (meaning sent over to stripe).
func (payment Payments) checkProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (unpaidUsage bool, err error) {
func (payment Payments) checkProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (err error) {
defer mon.Task()(&ctx)(&err)
_, err = payment.service.getAuthAndAuditLog(ctx, "project charges")
_, err = payment.service.getAuthAndAuditLog(ctx, "project invoicing status")
if err != nil {
return false, Error.Wrap(err)
return Error.Wrap(err)
}
return payment.service.accounts.CheckProjectInvoicingStatus(ctx, projectID)
}
// checkProjectUsageStatus returns error if for the given project there is some usage for current or previous month.
func (payment Payments) checkProjectUsageStatus(ctx context.Context, projectID uuid.UUID) (err error) {
defer mon.Task()(&ctx)(&err)
_, err = payment.service.getAuthAndAuditLog(ctx, "project usage status")
if err != nil {
return Error.Wrap(err)
}
return payment.service.accounts.CheckProjectUsageStatus(ctx, projectID)
}
// ApplyCouponCode applies a coupon code to a Stripe customer
// and returns the coupon corresponding to the code.
func (payment Payments) ApplyCouponCode(ctx context.Context, couponCode string) (coupon *payments.Coupon, err error) {
@ -1421,6 +1433,7 @@ func (s *Service) GenCreateProject(ctx context.Context, projectInfo ProjectInfo)
// DeleteProject is a method for deleting project by id.
func (s *Service) DeleteProject(ctx context.Context, projectID uuid.UUID) (err error) {
defer mon.Task()(&ctx)(&err)
auth, err := s.getAuthAndAuditLog(ctx, "delete project", zap.String("projectID", projectID.String()))
if err != nil {
return Error.Wrap(err)
@ -1431,7 +1444,7 @@ func (s *Service) DeleteProject(ctx context.Context, projectID uuid.UUID) (err e
return Error.Wrap(err)
}
err = s.checkProjectCanBeDeleted(ctx, projectID)
err = s.checkProjectCanBeDeleted(ctx, auth.User, projectID)
if err != nil {
return Error.Wrap(err)
}
@ -1444,6 +1457,46 @@ func (s *Service) DeleteProject(ctx context.Context, projectID uuid.UUID) (err e
return nil
}
// GenDeleteProject is a method for deleting project by id for generated API.
func (s *Service) GenDeleteProject(ctx context.Context, projectID uuid.UUID) (httpError api.HTTPError) {
var err error
defer mon.Task()(&ctx)(&err)
auth, err := s.getAuthAndAuditLog(ctx, "delete project", zap.String("projectID", projectID.String()))
if err != nil {
return api.HTTPError{
Status: http.StatusUnauthorized,
Err: Error.Wrap(err),
}
}
_, err = s.isProjectOwner(ctx, auth.User.ID, projectID)
if err != nil {
return api.HTTPError{
Status: http.StatusUnauthorized,
Err: Error.Wrap(err),
}
}
err = s.checkProjectCanBeDeleted(ctx, auth.User, projectID)
if err != nil {
return api.HTTPError{
Status: http.StatusConflict,
Err: Error.Wrap(err),
}
}
err = s.store.Projects().Delete(ctx, projectID)
if err != nil {
return api.HTTPError{
Status: http.StatusInternalServerError,
Err: Error.Wrap(err),
}
}
return httpError
}
// UpdateProject is a method for updating project name and description by id.
func (s *Service) UpdateProject(ctx context.Context, projectID uuid.UUID, projectInfo ProjectInfo) (p *Project, err error) {
defer mon.Task()(&ctx)(&err)
@ -2452,10 +2505,10 @@ func (s *Service) keyAuth(ctx context.Context, r *http.Request) (context.Context
// checkProjectCanBeDeleted ensures that all data, api-keys and buckets are deleted and usage has been accounted.
// no error means the project status is clean.
func (s *Service) checkProjectCanBeDeleted(ctx context.Context, project uuid.UUID) (err error) {
func (s *Service) checkProjectCanBeDeleted(ctx context.Context, user User, projectID uuid.UUID) (err error) {
defer mon.Task()(&ctx)(&err)
buckets, err := s.buckets.CountBuckets(ctx, project)
buckets, err := s.buckets.CountBuckets(ctx, projectID)
if err != nil {
return err
}
@ -2463,19 +2516,27 @@ func (s *Service) checkProjectCanBeDeleted(ctx context.Context, project uuid.UUI
return ErrUsage.New("some buckets still exist")
}
keys, err := s.store.APIKeys().GetPagedByProjectID(ctx, project, APIKeyCursor{Limit: 1, Page: 1})
keys, err := s.store.APIKeys().GetPagedByProjectID(ctx, projectID, APIKeyCursor{Limit: 1, Page: 1})
if err != nil {
return err
}
if keys.TotalCount > 0 {
return ErrUsage.New("some api-keys still exist")
return ErrUsage.New("some api keys still exist")
}
outstanding, err := s.Payments().checkProjectInvoicingStatus(ctx, project)
if outstanding {
return ErrUsage.New("there is outstanding usage that is not charged yet")
if user.PaidTier {
err = s.Payments().checkProjectUsageStatus(ctx, projectID)
if err != nil {
return ErrUsage.Wrap(err)
}
}
return ErrUsage.Wrap(err)
err = s.Payments().checkProjectInvoicingStatus(ctx, projectID)
if err != nil {
return ErrUsage.Wrap(err)
}
return nil
}
// checkProjectLimit is used to check if user is able to create a new project.

View File

@ -29,9 +29,12 @@ type Accounts interface {
// ProjectCharges returns how much money current user will be charged for each project.
ProjectCharges(ctx context.Context, userID uuid.UUID, since, before time.Time) ([]ProjectCharge, error)
// CheckProjectInvoicingStatus returns true if for the given project there are outstanding project records and/or usage
// CheckProjectInvoicingStatus returns error if for the given project there are outstanding project records and/or usage
// which have not been applied/invoiced yet (meaning sent over to stripe).
CheckProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (unpaidUsage bool, err error)
CheckProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) error
// CheckProjectUsageStatus returns error if for the given project there is some usage for current or previous month.
CheckProjectUsageStatus(ctx context.Context, projectID uuid.UUID) error
// Charges returns list of all credit card charges related to account.
Charges(ctx context.Context, userID uuid.UUID) ([]Charge, error)

View File

@ -5,10 +5,10 @@ package stripecoinpayments
import (
"context"
"errors"
"time"
"github.com/stripe/stripe-go/v72"
"github.com/zeebo/errs"
"storj.io/common/uuid"
"storj.io/storj/satellite/payments"
@ -147,50 +147,65 @@ func (accounts *accounts) ProjectCharges(ctx context.Context, userID uuid.UUID,
return charges, nil
}
// CheckProjectInvoicingStatus returns true if for the given project there are outstanding project records and/or usage
// CheckProjectInvoicingStatus returns error if for the given project there are outstanding project records and/or usage
// which have not been applied/invoiced yet (meaning sent over to stripe).
func (accounts *accounts) CheckProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (unpaidUsage bool, err error) {
func (accounts *accounts) CheckProjectInvoicingStatus(ctx context.Context, projectID uuid.UUID) (err error) {
defer mon.Task()(&ctx)(&err)
// we do not want to delete projects that have usage for the current month.
year, month, _ := accounts.service.nowFn().UTC().Date()
firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
// Check if an invoice project record exists already
err = accounts.service.db.ProjectRecords().Check(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth)
if errs.Is(err, ErrProjectRecordExists) {
record, err := accounts.service.db.ProjectRecords().Get(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth)
if err != nil {
return err
}
// state = 0 means unapplied and not invoiced yet.
if record.State == 0 {
return errs.New("unapplied project invoice record exist")
}
// Record has been applied, so project can be deleted.
return nil
}
if err != nil {
return err
}
return nil
}
// CheckProjectUsageStatus returns error if for the given project there is some usage for current or previous month.
func (accounts *accounts) CheckProjectUsageStatus(ctx context.Context, projectID uuid.UUID) error {
var err error
defer mon.Task()(&ctx)(&err)
year, month, _ := accounts.service.nowFn().UTC().Date()
firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
// check current month usage and do not allow deletion if usage exists
currentUsage, err := accounts.service.usageDB.GetProjectTotal(ctx, projectID, firstOfMonth, accounts.service.nowFn())
if err != nil {
return false, err
return err
}
if currentUsage.Storage > 0 || currentUsage.Egress > 0 || currentUsage.SegmentCount > 0 {
return true, errors.New("usage for current month exists")
return errs.New("usage for current month exists")
}
// if usage of last month exist, make sure to look for billing records
// check usage for last month, if exists, ensure we have an invoice item created.
lastMonthUsage, err := accounts.service.usageDB.GetProjectTotal(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.AddDate(0, 0, -1))
if err != nil {
return false, err
return err
}
if lastMonthUsage.Storage > 0 || lastMonthUsage.Egress > 0 || lastMonthUsage.SegmentCount > 0 {
err = accounts.service.db.ProjectRecords().Check(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth)
if !errs.Is(err, ErrProjectRecordExists) {
return errs.New("usage for last month exist, but is not billed yet")
}
}
if lastMonthUsage.Storage > 0 || lastMonthUsage.Egress > 0 || lastMonthUsage.SegmentCount > 0 {
// time passed into the check function need to be the UTC midnight dates of the first and last day of the month
err = accounts.service.db.ProjectRecords().Check(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.Add(-time.Hour*24))
if errors.Is(err, ErrProjectRecordExists) {
record, err := accounts.service.db.ProjectRecords().Get(ctx, projectID, firstOfMonth.AddDate(0, -1, 0), firstOfMonth.Add(-time.Hour*24))
if err != nil {
return true, err
}
// state = 0 means unapplied and not invoiced yet.
if record.State == 0 {
return true, errors.New("unapplied project invoice record exist")
}
// Record has been applied, so project can be deleted.
return false, nil
}
if err != nil {
return true, err
}
return true, errors.New("usage for last month exist, but is not billed yet")
}
return false, nil
return nil
}
// Charges returns list of all credit card charges related to account.