github.com/shoshinnikita/budget-manager@v0.7.1-0.20220131195411-8c46ff1c6778/internal/web/api/spend_type.go (about)

     1  package api
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  
     7  	"github.com/ShoshinNikita/budget-manager/internal/db"
     8  	"github.com/ShoshinNikita/budget-manager/internal/logger"
     9  	"github.com/ShoshinNikita/budget-manager/internal/pkg/errors"
    10  	"github.com/ShoshinNikita/budget-manager/internal/pkg/reqid"
    11  	"github.com/ShoshinNikita/budget-manager/internal/web/api/models"
    12  	"github.com/ShoshinNikita/budget-manager/internal/web/utils"
    13  )
    14  
    15  type SpendTypesHandlers struct {
    16  	db  SpendTypesDB
    17  	log logger.Logger
    18  }
    19  
    20  type SpendTypesDB interface {
    21  	GetSpendTypes(ctx context.Context) ([]db.SpendType, error)
    22  	AddSpendType(ctx context.Context, args db.AddSpendTypeArgs) (id uint, err error)
    23  	EditSpendType(ctx context.Context, args db.EditSpendTypeArgs) error
    24  	RemoveSpendType(ctx context.Context, id uint) error
    25  }
    26  
    27  // @Summary Get All Spend Types
    28  // @Tags Spend Types
    29  // @Router /api/spend-types [get]
    30  // @Produce json
    31  // @Success 200 {object} models.GetSpendTypesResp
    32  // @Failure 500 {object} models.Response "Internal error"
    33  //
    34  func (h SpendTypesHandlers) GetSpendTypes(w http.ResponseWriter, r *http.Request) {
    35  	ctx := r.Context()
    36  	log := reqid.FromContextToLogger(ctx, h.log)
    37  
    38  	// Process
    39  	types, err := h.db.GetSpendTypes(ctx)
    40  	if err != nil {
    41  		utils.EncodeInternalError(ctx, w, log, "couldn't get Spend Types", err)
    42  		return
    43  	}
    44  
    45  	resp := &models.GetSpendTypesResp{
    46  		SpendTypes: types,
    47  	}
    48  	utils.Encode(ctx, w, log, utils.EncodeResponse(resp))
    49  }
    50  
    51  // @Summary Create Spend Type
    52  // @Tags Spend Types
    53  // @Router /api/spend-types [post]
    54  // @Accept json
    55  // @Param body body models.AddSpendTypeReq true "New Spend Type"
    56  // @Produce json
    57  // @Success 201 {object} models.AddSpendTypeResp
    58  // @Failure 400 {object} models.Response "Invalid request"
    59  // @Failure 500 {object} models.Response "Internal error"
    60  //
    61  func (h SpendTypesHandlers) AddSpendType(w http.ResponseWriter, r *http.Request) {
    62  	ctx := r.Context()
    63  	log := reqid.FromContextToLogger(ctx, h.log)
    64  
    65  	// Decode
    66  	req := &models.AddSpendTypeReq{}
    67  	if ok := utils.DecodeRequest(w, r, log, req); !ok {
    68  		return
    69  	}
    70  	log = log.WithRequest(req)
    71  
    72  	// Process
    73  	args := db.AddSpendTypeArgs{
    74  		Name:     req.Name,
    75  		ParentID: req.ParentID,
    76  	}
    77  	id, err := h.db.AddSpendType(ctx, args)
    78  	if err != nil {
    79  		utils.EncodeInternalError(ctx, w, log, "couldn't add Spend Type", err)
    80  		return
    81  	}
    82  	log = log.WithField("id", id)
    83  	log.Debug("Spend Type was successfully added")
    84  
    85  	resp := &models.AddSpendTypeResp{
    86  		ID: id,
    87  	}
    88  	utils.Encode(ctx, w, log, utils.EncodeResponse(resp), utils.EncodeStatusCode(http.StatusCreated))
    89  }
    90  
    91  // @Summary Edit Spend Type
    92  // @Tags Spend Types
    93  // @Router /api/spend-types [put]
    94  // @Accept json
    95  // @Param body body models.EditSpendTypeReq true "Updated Spend Type"
    96  // @Produce json
    97  // @Success 200 {object} models.Response
    98  // @Failure 400 {object} models.Response "Invalid request"
    99  // @Failure 404 {object} models.Response "Spend Type doesn't exist"
   100  // @Failure 500 {object} models.Response "Internal error"
   101  //
   102  func (h SpendTypesHandlers) EditSpendType(w http.ResponseWriter, r *http.Request) {
   103  	ctx := r.Context()
   104  	log := reqid.FromContextToLogger(ctx, h.log)
   105  
   106  	// Decode
   107  	req := &models.EditSpendTypeReq{}
   108  	if ok := utils.DecodeRequest(w, r, log, req); !ok {
   109  		return
   110  	}
   111  	log = log.WithRequest(req)
   112  
   113  	if req.ParentID != nil && *req.ParentID != 0 {
   114  		allSpendTypes, err := h.db.GetSpendTypes(ctx)
   115  		if err != nil {
   116  			utils.EncodeInternalError(ctx, w, log, "couldn't get all Spend Types to check for a cycle", err)
   117  			return
   118  		}
   119  
   120  		hasCycle, err := checkSpendTypeForCycle(allSpendTypes, req.ID, *req.ParentID)
   121  		if hasCycle {
   122  			err = errors.New("Spend Type with new parent type will have a cycle")
   123  		} else if err != nil {
   124  			err = errors.Wrap(err, "check for a cycle failed")
   125  		}
   126  		if err != nil {
   127  			utils.EncodeError(ctx, w, log, err, http.StatusBadRequest)
   128  			return
   129  		}
   130  	}
   131  
   132  	// Process
   133  	args := db.EditSpendTypeArgs{
   134  		ID:       req.ID,
   135  		Name:     req.Name,
   136  		ParentID: req.ParentID,
   137  	}
   138  	err := h.db.EditSpendType(ctx, args)
   139  	if err != nil {
   140  		switch {
   141  		case errors.Is(err, db.ErrSpendTypeNotExist):
   142  			utils.EncodeError(ctx, w, log, err, http.StatusNotFound)
   143  		default:
   144  			utils.EncodeInternalError(ctx, w, log, "couldn't edit Spend Type", err)
   145  		}
   146  		return
   147  	}
   148  	log.Debug("Spend Type was successfully edited")
   149  
   150  	utils.Encode(ctx, w, log)
   151  }
   152  
   153  func checkSpendTypeForCycle(spendTypesSlice []db.SpendType, originalID, newParentID uint) (hasCycle bool, _ error) {
   154  	spendTypes := make(map[uint]db.SpendType, len(spendTypesSlice))
   155  	for _, t := range spendTypesSlice {
   156  		spendTypes[t.ID] = t
   157  	}
   158  
   159  	parentType := spendTypes[newParentID]
   160  	// Max depth is 15
   161  	for i := 0; i < 15; i++ {
   162  		if parentType.ID == 0 {
   163  			// Unexpected error
   164  			return false, errors.New("invalid Spend Type")
   165  		}
   166  		if parentType.ID == originalID {
   167  			// Has cycle
   168  			return true, nil
   169  		}
   170  		if parentType.ParentID == 0 {
   171  			// No more parents
   172  			return false, nil
   173  		}
   174  
   175  		parentType = spendTypes[parentType.ParentID]
   176  	}
   177  
   178  	return false, errors.New("Spend Type has too many parents or already has a cycle")
   179  }
   180  
   181  // @Summary Remove Spend Type
   182  // @Tags Spend Types
   183  // @Router /api/spend-types [delete]
   184  // @Accept json
   185  // @Param body body models.RemoveSpendTypeReq true "Spend Type id"
   186  // @Produce json
   187  // @Success 200 {object} models.Response
   188  // @Failure 400 {object} models.Response "Invalid request"
   189  // @Failure 404 {object} models.Response "Spend Type doesn't exist"
   190  // @Failure 500 {object} models.Response "Internal error"
   191  //
   192  func (h SpendTypesHandlers) RemoveSpendType(w http.ResponseWriter, r *http.Request) {
   193  	ctx := r.Context()
   194  	log := reqid.FromContextToLogger(ctx, h.log)
   195  
   196  	// Decode
   197  	req := &models.RemoveSpendTypeReq{}
   198  	if ok := utils.DecodeRequest(w, r, log, req); !ok {
   199  		return
   200  	}
   201  	log = log.WithRequest(req)
   202  
   203  	// Process
   204  	err := h.db.RemoveSpendType(ctx, req.ID)
   205  	if err != nil {
   206  		switch {
   207  		case errors.Is(err, db.ErrSpendTypeNotExist):
   208  			utils.EncodeError(ctx, w, log, err, http.StatusNotFound)
   209  		case errors.Is(err, db.ErrSpendTypeIsUsed):
   210  			utils.EncodeError(ctx, w, log, err, http.StatusBadRequest)
   211  		default:
   212  			utils.EncodeInternalError(ctx, w, log, "couldn't remove Spend Type", err)
   213  		}
   214  		return
   215  	}
   216  	log.Debug("Spend Type was successfully removed")
   217  
   218  	utils.Encode(ctx, w, log)
   219  }