mirror of
https://github.com/Receipt-Wrangler/receipt-wrangler-api.git
synced 2026-04-27 12:39:58 -05:00
Merge pull request #380 from Receipt-Wrangler/feature/custom-fields
Feature/custom fields
This commit is contained in:
@@ -2,5 +2,5 @@ export DB_FILENAME=wrangler.db
|
||||
export DB_ENGINE=sqlite
|
||||
export ENCRYPTION_KEY=test
|
||||
export SECRET_KEY=test
|
||||
export REDIS_HOST=172.17.0.3
|
||||
export REDIS_HOST=172.17.0.2
|
||||
export REDIS_PORT=6379
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"receipt-wrangler/api/internal/models"
|
||||
"receipt-wrangler/api/internal/structs"
|
||||
"receipt-wrangler/api/internal/utils"
|
||||
)
|
||||
|
||||
type UpsertCustomFieldCommand struct {
|
||||
Name string `json:"name"`
|
||||
Type models.CustomFieldType `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Options []UpsertCustomFieldOptionCommand `json:"options"`
|
||||
}
|
||||
|
||||
func (command *UpsertCustomFieldCommand) LoadDataFromRequest(w http.ResponseWriter, r *http.Request) error {
|
||||
bytes, err := utils.GetBodyData(w, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(bytes, &command)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if command.Type != models.SELECT && len(command.Options) > 0 {
|
||||
command.Options = []UpsertCustomFieldOptionCommand{}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (command *UpsertCustomFieldCommand) Validate() structs.ValidatorError {
|
||||
errors := make(map[string]string)
|
||||
vErr := structs.ValidatorError{}
|
||||
|
||||
if len(command.Name) == 0 {
|
||||
errors["name"] = "Name is required"
|
||||
}
|
||||
|
||||
if len(command.Type) == 0 {
|
||||
errors["type"] = "Type is required"
|
||||
}
|
||||
|
||||
if command.Type == models.SELECT && len(command.Options) == 0 {
|
||||
errors["options"] = "Options are required"
|
||||
}
|
||||
|
||||
vErr.Errors = errors
|
||||
return vErr
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package commands
|
||||
|
||||
type UpsertCustomFieldOptionCommand struct {
|
||||
Value string `json:"value"`
|
||||
CustomFieldId uint `json:"custom_field_id"`
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/shopspring/decimal"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UpsertCustomFieldValueCommand struct {
|
||||
ReceiptId uint `json:"receiptId"`
|
||||
CustomFieldId uint `json:"customFieldId"`
|
||||
StringValue *string `json:"stringValue"`
|
||||
DateValue *time.Time `json:"dateValue"`
|
||||
SelectValue *uint `json:"selectValue"`
|
||||
CurrencyValue *decimal.Decimal `json:"currencyValue"`
|
||||
BooleanValue *bool `json:"booleanValue"`
|
||||
}
|
||||
@@ -12,17 +12,18 @@ import (
|
||||
)
|
||||
|
||||
type UpsertReceiptCommand struct {
|
||||
Name string `json:"name"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
Date time.Time `json:"date"`
|
||||
GroupId uint `json:"groupId"`
|
||||
PaidByUserID uint `json:"paidByUserId"`
|
||||
Status models.ReceiptStatus `json:"status"`
|
||||
Categories []UpsertCategoryCommand `json:"categories"`
|
||||
Tags []UpsertTagCommand `json:"tags"`
|
||||
Items []UpsertItemCommand `json:"receiptItems"`
|
||||
Comments []UpsertCommentCommand `json:"comments"`
|
||||
CreatedByString string `json:"createdByString"`
|
||||
Name string `json:"name"`
|
||||
Amount decimal.Decimal `json:"amount"`
|
||||
Date time.Time `json:"date"`
|
||||
GroupId uint `json:"groupId"`
|
||||
PaidByUserID uint `json:"paidByUserId"`
|
||||
Status models.ReceiptStatus `json:"status"`
|
||||
Categories []UpsertCategoryCommand `json:"categories"`
|
||||
Tags []UpsertTagCommand `json:"tags"`
|
||||
Items []UpsertItemCommand `json:"receiptItems"`
|
||||
Comments []UpsertCommentCommand `json:"comments"`
|
||||
CustomFields []UpsertCustomFieldValueCommand `json:"customFields"`
|
||||
CreatedByString string `json:"createdByString"`
|
||||
}
|
||||
|
||||
func (receipt *UpsertReceiptCommand) LoadDataFromRequest(w http.ResponseWriter, r *http.Request) error {
|
||||
|
||||
@@ -18,6 +18,7 @@ func GetAllCategories(w http.ResponseWriter, r *http.Request) {
|
||||
ErrorMessage: "Error retrieving categories",
|
||||
Writer: w,
|
||||
Request: r,
|
||||
UserRole: models.USER,
|
||||
ResponseType: constants.ApplicationJson,
|
||||
HandlerFunction: func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
categoriesRepository := repositories.NewCategoryRepository(nil)
|
||||
@@ -46,6 +47,7 @@ func CreateCategory(w http.ResponseWriter, r *http.Request) {
|
||||
ErrorMessage: "Error creating category",
|
||||
Writer: w,
|
||||
Request: r,
|
||||
UserRole: models.USER,
|
||||
ResponseType: constants.ApplicationJson,
|
||||
HandlerFunction: func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
category := models.Category{}
|
||||
@@ -81,6 +83,7 @@ func GetPagedCategories(w http.ResponseWriter, r *http.Request) {
|
||||
Writer: w,
|
||||
Request: r,
|
||||
ResponseType: constants.ApplicationJson,
|
||||
UserRole: models.USER,
|
||||
HandlerFunction: func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
pagedData := structs.PagedData{}
|
||||
pagedRequestCommand := commands.PagedRequestCommand{}
|
||||
|
||||
@@ -33,6 +33,9 @@ func TestShouldGetAllCategories(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/api", reader)
|
||||
|
||||
newContext := context.WithValue(r.Context(), jwtmiddleware.ContextKey{}, &validator.ValidatedClaims{CustomClaims: &structs.Claims{UserId: 2, UserRole: models.USER}})
|
||||
r = r.WithContext(newContext)
|
||||
|
||||
GetAllCategories(w, r)
|
||||
|
||||
err := json.Unmarshal(w.Body.Bytes(), &categories)
|
||||
@@ -64,6 +67,9 @@ func TestShouldCreateCategory(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/api", reader)
|
||||
|
||||
newContext := context.WithValue(r.Context(), jwtmiddleware.ContextKey{}, &validator.ValidatedClaims{CustomClaims: &structs.Claims{UserId: 2, UserRole: models.USER}})
|
||||
r = r.WithContext(newContext)
|
||||
|
||||
CreateCategory(w, r)
|
||||
|
||||
err := json.Unmarshal(w.Body.Bytes(), &category)
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
"receipt-wrangler/api/internal/commands"
|
||||
"receipt-wrangler/api/internal/constants"
|
||||
"receipt-wrangler/api/internal/models"
|
||||
"receipt-wrangler/api/internal/repositories"
|
||||
"receipt-wrangler/api/internal/structs"
|
||||
"receipt-wrangler/api/internal/utils"
|
||||
)
|
||||
|
||||
func GetPagedCustomFields(w http.ResponseWriter, r *http.Request) {
|
||||
handler := structs.Handler{
|
||||
ErrorMessage: "Error getting custom fields",
|
||||
Writer: w,
|
||||
Request: r,
|
||||
ResponseType: constants.ApplicationJson,
|
||||
UserRole: models.USER,
|
||||
HandlerFunction: func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
pagedData := structs.PagedData{}
|
||||
pagedRequestCommand := commands.PagedRequestCommand{}
|
||||
err := pagedRequestCommand.LoadDataFromRequest(w, r)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
vErrs := pagedRequestCommand.Validate()
|
||||
if len(vErrs.Errors) > 0 {
|
||||
structs.WriteValidatorErrorResponse(w, vErrs, http.StatusBadRequest)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
customFieldsRepository := repositories.NewCustomFieldRepository(nil)
|
||||
customFields, count, err := customFieldsRepository.GetPagedCustomFields(pagedRequestCommand)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
anyData := make([]any, len(customFields))
|
||||
for i := 0; i < len(customFields); i++ {
|
||||
anyData[i] = customFields[i]
|
||||
}
|
||||
|
||||
pagedData.Data = anyData
|
||||
pagedData.TotalCount = count
|
||||
|
||||
bytes, err := utils.MarshalResponseData(pagedData)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(bytes)
|
||||
|
||||
return 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
HandleRequest(handler)
|
||||
}
|
||||
|
||||
func CreateCustomField(w http.ResponseWriter, r *http.Request) {
|
||||
handler := structs.Handler{
|
||||
ErrorMessage: "Error creating custom field",
|
||||
Writer: w,
|
||||
Request: r,
|
||||
ResponseType: constants.ApplicationJson,
|
||||
UserRole: models.USER,
|
||||
HandlerFunction: func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
command := commands.UpsertCustomFieldCommand{}
|
||||
err := command.LoadDataFromRequest(w, r)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
vErrs := command.Validate()
|
||||
if len(vErrs.Errors) > 0 {
|
||||
structs.WriteValidatorErrorResponse(w, vErrs, http.StatusBadRequest)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
token := structs.GetJWT(r)
|
||||
customFieldsRepository := repositories.NewCustomFieldRepository(nil)
|
||||
customField, err := customFieldsRepository.CreateCustomField(command, &token.UserId)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
bytes, err := json.Marshal(customField)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(bytes)
|
||||
|
||||
return 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
HandleRequest(handler)
|
||||
}
|
||||
|
||||
func GetCustomFieldById(w http.ResponseWriter, r *http.Request) {
|
||||
handler := structs.Handler{
|
||||
ErrorMessage: "Error getting custom field",
|
||||
Writer: w,
|
||||
Request: r,
|
||||
ResponseType: constants.ApplicationJson,
|
||||
UserRole: models.USER,
|
||||
HandlerFunction: func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
customFieldId := chi.URLParam(r, "id")
|
||||
customFieldIdUint, err := utils.StringToUint(customFieldId)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
customFieldsRepository := repositories.NewCustomFieldRepository(nil)
|
||||
customField, err := customFieldsRepository.GetCustomFieldById(customFieldIdUint)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(customField)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(bytes)
|
||||
|
||||
return 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
HandleRequest(handler)
|
||||
}
|
||||
|
||||
func DeleteCustomField(w http.ResponseWriter, r *http.Request) {
|
||||
handler := structs.Handler{
|
||||
ErrorMessage: "Error deleting custom field",
|
||||
Writer: w,
|
||||
Request: r,
|
||||
ResponseType: constants.ApplicationJson,
|
||||
UserRole: models.ADMIN,
|
||||
HandlerFunction: func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
customFieldId := chi.URLParam(r, "id")
|
||||
customFieldIdUint, err := utils.StringToUint(customFieldId)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
customFieldsRepository := repositories.NewCustomFieldRepository(nil)
|
||||
err = customFieldsRepository.DeleteCustomField(customFieldIdUint)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
return 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
HandleRequest(handler)
|
||||
}
|
||||
@@ -0,0 +1,604 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"receipt-wrangler/api/internal/commands"
|
||||
"receipt-wrangler/api/internal/models"
|
||||
"receipt-wrangler/api/internal/repositories"
|
||||
"receipt-wrangler/api/internal/structs"
|
||||
"receipt-wrangler/api/internal/utils"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
jwtmiddleware "github.com/auth0/go-jwt-middleware/v2"
|
||||
"github.com/auth0/go-jwt-middleware/v2/validator"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func setupCustomFieldHandlerTest() {
|
||||
createTestCustomFields()
|
||||
}
|
||||
|
||||
func createTestCustomFields() {
|
||||
db := repositories.GetDB()
|
||||
|
||||
// Create custom field with TEXT type
|
||||
textField := models.CustomField{
|
||||
Name: "Test Text Field",
|
||||
Type: models.TEXT,
|
||||
Description: "A test text field",
|
||||
}
|
||||
db.Create(&textField)
|
||||
|
||||
// Create custom field with DATE type
|
||||
dateField := models.CustomField{
|
||||
Name: "Test Date Field",
|
||||
Type: models.DATE,
|
||||
Description: "A test date field",
|
||||
}
|
||||
db.Create(&dateField)
|
||||
|
||||
// Create custom field with SELECT type and options
|
||||
selectField := models.CustomField{
|
||||
Name: "Test Select Field",
|
||||
Type: models.SELECT,
|
||||
Description: "A test select field",
|
||||
}
|
||||
db.Create(&selectField)
|
||||
|
||||
// Add options to the SELECT field
|
||||
option1 := models.CustomFieldOption{
|
||||
Value: "Option 1",
|
||||
CustomFieldId: selectField.ID,
|
||||
}
|
||||
option2 := models.CustomFieldOption{
|
||||
Value: "Option 2",
|
||||
CustomFieldId: selectField.ID,
|
||||
}
|
||||
db.Create(&option1)
|
||||
db.Create(&option2)
|
||||
|
||||
// Create custom field with CURRENCY type
|
||||
currencyField := models.CustomField{
|
||||
Name: "Test Currency Field",
|
||||
Type: models.CURRENCY,
|
||||
Description: "A test currency field",
|
||||
}
|
||||
db.Create(¤cyField)
|
||||
}
|
||||
|
||||
func teardownCustomFieldHandlerTest() {
|
||||
repositories.TruncateTestDb()
|
||||
}
|
||||
|
||||
func createCustomFieldHandlerTestRequest(method, url string, body string) (*http.Request, *httptest.ResponseRecorder) {
|
||||
var bodyReader *strings.Reader
|
||||
if body != "" {
|
||||
bodyReader = strings.NewReader(body)
|
||||
} else {
|
||||
bodyReader = strings.NewReader("")
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(method, url, bodyReader)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Add JWT context with mock user claims
|
||||
var vClaims validator.ValidatedClaims
|
||||
vClaims.CustomClaims = &structs.Claims{
|
||||
UserId: 1,
|
||||
Username: "testuser",
|
||||
Displayname: "Test User",
|
||||
UserRole: models.ADMIN,
|
||||
}
|
||||
|
||||
// Set the JWT context
|
||||
ctx := req.Context()
|
||||
ctx = context.WithValue(ctx, jwtmiddleware.ContextKey{}, &vClaims)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
return req, httptest.NewRecorder()
|
||||
}
|
||||
|
||||
func TestGetPagedCustomFieldsHandler(t *testing.T) {
|
||||
defer teardownCustomFieldHandlerTest()
|
||||
setupCustomFieldHandlerTest()
|
||||
|
||||
// Create request with paged request payload
|
||||
pagedRequestCommand := commands.PagedRequestCommand{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
OrderBy: "name",
|
||||
SortDirection: commands.ASCENDING,
|
||||
}
|
||||
pagedRequestJSON, _ := json.Marshal(pagedRequestCommand)
|
||||
|
||||
req, rr := createCustomFieldHandlerTestRequest(
|
||||
"POST",
|
||||
"/api/customField/getPagedCustomFields",
|
||||
string(pagedRequestJSON),
|
||||
)
|
||||
|
||||
// Call the handler
|
||||
GetPagedCustomFields(rr, req)
|
||||
|
||||
// Check response status
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
utils.PrintTestError(t, status, http.StatusOK)
|
||||
}
|
||||
|
||||
// Parse response
|
||||
var response structs.PagedData
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err.Error(), nil)
|
||||
}
|
||||
|
||||
// Should return 4 custom fields
|
||||
if response.TotalCount != 4 {
|
||||
utils.PrintTestError(t, response.TotalCount, 4)
|
||||
}
|
||||
|
||||
if len(response.Data) != 4 {
|
||||
utils.PrintTestError(t, len(response.Data), 4)
|
||||
}
|
||||
|
||||
// Convert data to CustomField array
|
||||
customFieldsBytes, _ := json.Marshal(response.Data)
|
||||
var customFields []models.CustomField
|
||||
json.Unmarshal(customFieldsBytes, &customFields)
|
||||
|
||||
// Check if fields are correctly ordered by name
|
||||
if customFields[0].Name != "Test Currency Field" {
|
||||
utils.PrintTestError(t, customFields[0].Name, "Test Currency Field")
|
||||
}
|
||||
|
||||
if customFields[1].Name != "Test Date Field" {
|
||||
utils.PrintTestError(t, customFields[1].Name, "Test Date Field")
|
||||
}
|
||||
|
||||
if customFields[2].Name != "Test Select Field" {
|
||||
utils.PrintTestError(t, customFields[2].Name, "Test Select Field")
|
||||
}
|
||||
|
||||
if customFields[3].Name != "Test Text Field" {
|
||||
utils.PrintTestError(t, customFields[3].Name, "Test Text Field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPagedCustomFieldsHandlerWithPagination(t *testing.T) {
|
||||
defer teardownCustomFieldHandlerTest()
|
||||
setupCustomFieldHandlerTest()
|
||||
|
||||
// Create request with paged request payload
|
||||
pagedRequestCommand := commands.PagedRequestCommand{
|
||||
Page: 1,
|
||||
PageSize: 2,
|
||||
OrderBy: "name",
|
||||
SortDirection: commands.ASCENDING,
|
||||
}
|
||||
pagedRequestJSON, _ := json.Marshal(pagedRequestCommand)
|
||||
|
||||
req, rr := createCustomFieldHandlerTestRequest(
|
||||
"POST",
|
||||
"/api/customField/getPagedCustomFields",
|
||||
string(pagedRequestJSON),
|
||||
)
|
||||
|
||||
// Call the handler
|
||||
GetPagedCustomFields(rr, req)
|
||||
|
||||
// Check response status
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
utils.PrintTestError(t, status, http.StatusOK)
|
||||
}
|
||||
|
||||
// Parse response
|
||||
var response structs.PagedData
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err.Error(), nil)
|
||||
}
|
||||
|
||||
// Total count should still be 4
|
||||
if response.TotalCount != 4 {
|
||||
utils.PrintTestError(t, response.TotalCount, 4)
|
||||
}
|
||||
|
||||
// But we should only get 2 items per page
|
||||
if len(response.Data) != 2 {
|
||||
utils.PrintTestError(t, len(response.Data), 2)
|
||||
}
|
||||
|
||||
// Test second page
|
||||
pagedRequestCommand.Page = 2
|
||||
pagedRequestJSON, _ = json.Marshal(pagedRequestCommand)
|
||||
|
||||
req, rr = createCustomFieldHandlerTestRequest(
|
||||
"POST",
|
||||
"/api/customField/getPagedCustomFields",
|
||||
string(pagedRequestJSON),
|
||||
)
|
||||
|
||||
// Call the handler
|
||||
GetPagedCustomFields(rr, req)
|
||||
|
||||
// Check response status
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
utils.PrintTestError(t, status, http.StatusOK)
|
||||
}
|
||||
|
||||
// Parse response
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err.Error(), nil)
|
||||
}
|
||||
|
||||
// Total count should still be 4
|
||||
if response.TotalCount != 4 {
|
||||
utils.PrintTestError(t, response.TotalCount, 4)
|
||||
}
|
||||
|
||||
// And we should get the other 2 items
|
||||
if len(response.Data) != 2 {
|
||||
utils.PrintTestError(t, len(response.Data), 2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPagedCustomFieldsHandlerWithTypeOrdering(t *testing.T) {
|
||||
defer teardownCustomFieldHandlerTest()
|
||||
setupCustomFieldHandlerTest()
|
||||
|
||||
// Create request with paged request payload
|
||||
pagedRequestCommand := commands.PagedRequestCommand{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
OrderBy: "type",
|
||||
SortDirection: commands.ASCENDING,
|
||||
}
|
||||
pagedRequestJSON, _ := json.Marshal(pagedRequestCommand)
|
||||
|
||||
req, rr := createCustomFieldHandlerTestRequest(
|
||||
"POST",
|
||||
"/api/customField/getPagedCustomFields",
|
||||
string(pagedRequestJSON),
|
||||
)
|
||||
|
||||
// Call the handler
|
||||
GetPagedCustomFields(rr, req)
|
||||
|
||||
// Check response status
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
utils.PrintTestError(t, status, http.StatusOK)
|
||||
}
|
||||
|
||||
// Parse response
|
||||
var response structs.PagedData
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err.Error(), nil)
|
||||
}
|
||||
|
||||
// Convert data to CustomField array
|
||||
customFieldsBytes, _ := json.Marshal(response.Data)
|
||||
var customFields []models.CustomField
|
||||
json.Unmarshal(customFieldsBytes, &customFields)
|
||||
|
||||
// Check ordering by type
|
||||
if customFields[0].Type != models.CURRENCY {
|
||||
utils.PrintTestError(t, string(customFields[0].Type), string(models.CURRENCY))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPagedCustomFieldsHandlerWithInvalidOrderBy(t *testing.T) {
|
||||
defer teardownCustomFieldHandlerTest()
|
||||
setupCustomFieldHandlerTest()
|
||||
|
||||
// Create request with invalid orderBy
|
||||
pagedRequestCommand := commands.PagedRequestCommand{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
OrderBy: "invalid_field",
|
||||
SortDirection: commands.ASCENDING,
|
||||
}
|
||||
pagedRequestJSON, _ := json.Marshal(pagedRequestCommand)
|
||||
|
||||
req, rr := createCustomFieldHandlerTestRequest(
|
||||
"POST",
|
||||
"/api/customField/getPagedCustomFields",
|
||||
string(pagedRequestJSON),
|
||||
)
|
||||
|
||||
// Call the handler
|
||||
GetPagedCustomFields(rr, req)
|
||||
|
||||
// Check response status - should be error
|
||||
if status := rr.Code; status != http.StatusInternalServerError {
|
||||
utils.PrintTestError(t, status, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Parse error response
|
||||
var errorResponse map[string]string
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &errorResponse)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err.Error(), nil)
|
||||
}
|
||||
|
||||
// Check error message
|
||||
if errorMsg, exists := errorResponse["errorMsg"]; !exists || !strings.Contains(errorMsg, "Error getting custom fields") {
|
||||
utils.PrintTestError(t, errorMsg, "Error getting custom fields")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPagedCustomFieldsHandlerWithInvalidBody(t *testing.T) {
|
||||
defer teardownCustomFieldHandlerTest()
|
||||
setupCustomFieldHandlerTest()
|
||||
|
||||
// Create request with invalid JSON body
|
||||
req, rr := createCustomFieldHandlerTestRequest(
|
||||
"POST",
|
||||
"/api/customField/getPagedCustomFields",
|
||||
"{invalid json",
|
||||
)
|
||||
|
||||
// Call the handler
|
||||
GetPagedCustomFields(rr, req)
|
||||
|
||||
// Check response status - should be error
|
||||
if status := rr.Code; status != http.StatusInternalServerError {
|
||||
utils.PrintTestError(t, status, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPagedCustomFieldsHandlerWithEmptyBody(t *testing.T) {
|
||||
defer teardownCustomFieldHandlerTest()
|
||||
setupCustomFieldHandlerTest()
|
||||
|
||||
// Create request with empty body
|
||||
req, rr := createCustomFieldHandlerTestRequest(
|
||||
"POST",
|
||||
"/api/customField/getPagedCustomFields",
|
||||
"",
|
||||
)
|
||||
|
||||
// Call the handler
|
||||
GetPagedCustomFields(rr, req)
|
||||
|
||||
// Check response status - should be error
|
||||
if status := rr.Code; status != http.StatusInternalServerError {
|
||||
utils.PrintTestError(t, status, http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCustomFieldByIdHandler(t *testing.T) {
|
||||
defer teardownCustomFieldHandlerTest()
|
||||
setupCustomFieldHandlerTest()
|
||||
|
||||
// Get a custom field from the test DB to use its ID
|
||||
db := repositories.GetDB()
|
||||
var customField models.CustomField
|
||||
db.Where("name = ?", "Test Text Field").First(&customField)
|
||||
|
||||
// Create request to get custom field by ID
|
||||
req, rr := createCustomFieldHandlerTestRequest(
|
||||
"GET",
|
||||
"/api/customField/"+utils.UintToString(customField.ID),
|
||||
"",
|
||||
)
|
||||
|
||||
// Set URL parameter
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, chi.NewRouteContext()))
|
||||
chiCtx := chi.RouteContext(req.Context())
|
||||
chiCtx.URLParams.Add("id", utils.UintToString(customField.ID))
|
||||
|
||||
// Call the handler
|
||||
GetCustomFieldById(rr, req)
|
||||
|
||||
// Check response status
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
utils.PrintTestError(t, status, http.StatusOK)
|
||||
}
|
||||
|
||||
// Parse response
|
||||
var responseCustomField models.CustomField
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &responseCustomField)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err.Error(), nil)
|
||||
}
|
||||
|
||||
// Check if the returned custom field has the correct ID and properties
|
||||
if responseCustomField.ID != customField.ID {
|
||||
utils.PrintTestError(t, responseCustomField.ID, customField.ID)
|
||||
}
|
||||
|
||||
if responseCustomField.Name != "Test Text Field" {
|
||||
utils.PrintTestError(t, responseCustomField.Name, "Test Text Field")
|
||||
}
|
||||
|
||||
if responseCustomField.Type != models.TEXT {
|
||||
utils.PrintTestError(t, string(responseCustomField.Type), string(models.TEXT))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCustomFieldByIdHandlerWithInvalidId(t *testing.T) {
|
||||
defer teardownCustomFieldHandlerTest()
|
||||
setupCustomFieldHandlerTest()
|
||||
|
||||
// Create request with invalid ID
|
||||
req, rr := createCustomFieldHandlerTestRequest(
|
||||
"GET",
|
||||
"/api/customField/invalid",
|
||||
"",
|
||||
)
|
||||
|
||||
// Set URL parameter
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, chi.NewRouteContext()))
|
||||
chiCtx := chi.RouteContext(req.Context())
|
||||
chiCtx.URLParams.Add("id", "invalid")
|
||||
|
||||
// Call the handler
|
||||
GetCustomFieldById(rr, req)
|
||||
|
||||
// Check response status - should be error
|
||||
if status := rr.Code; status != http.StatusInternalServerError {
|
||||
utils.PrintTestError(t, status, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Parse error response
|
||||
var errorResponse map[string]string
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &errorResponse)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err.Error(), nil)
|
||||
}
|
||||
|
||||
// Check error message
|
||||
if errorMsg, exists := errorResponse["errorMsg"]; !exists || !strings.Contains(errorMsg, "Error getting custom field") {
|
||||
utils.PrintTestError(t, errorMsg, "Error getting custom field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCustomFieldByIdHandlerWithNonExistentId(t *testing.T) {
|
||||
defer teardownCustomFieldHandlerTest()
|
||||
setupCustomFieldHandlerTest()
|
||||
|
||||
// Use a non-existent ID (999)
|
||||
nonExistentId := "999"
|
||||
|
||||
// Create request with non-existent ID
|
||||
req, rr := createCustomFieldHandlerTestRequest(
|
||||
"GET",
|
||||
"/api/customField/"+nonExistentId,
|
||||
"",
|
||||
)
|
||||
|
||||
// Set URL parameter
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, chi.NewRouteContext()))
|
||||
chiCtx := chi.RouteContext(req.Context())
|
||||
chiCtx.URLParams.Add("id", nonExistentId)
|
||||
|
||||
// Call the handler
|
||||
GetCustomFieldById(rr, req)
|
||||
|
||||
// Check response status - should be error
|
||||
if status := rr.Code; status != http.StatusInternalServerError {
|
||||
utils.PrintTestError(t, status, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Parse error response
|
||||
var errorResponse map[string]string
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &errorResponse)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err.Error(), nil)
|
||||
}
|
||||
|
||||
// Check error message
|
||||
if errorMsg, exists := errorResponse["errorMsg"]; !exists || !strings.Contains(errorMsg, "Error getting custom field") {
|
||||
utils.PrintTestError(t, errorMsg, "Error getting custom field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteCustomFieldHandler(t *testing.T) {
|
||||
defer teardownCustomFieldHandlerTest()
|
||||
setupCustomFieldHandlerTest()
|
||||
|
||||
db := repositories.GetDB()
|
||||
|
||||
// Get a custom field from the test DB to use its ID
|
||||
var customField models.CustomField
|
||||
db.Where("name = ?", "Test Text Field").First(&customField)
|
||||
customFieldId := customField.ID
|
||||
|
||||
// Create request to delete custom field
|
||||
req, rr := createCustomFieldHandlerTestRequest(
|
||||
"DELETE",
|
||||
"/api/customField/"+utils.UintToString(customFieldId),
|
||||
"",
|
||||
)
|
||||
|
||||
// Set URL parameter
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, chi.NewRouteContext()))
|
||||
chiCtx := chi.RouteContext(req.Context())
|
||||
chiCtx.URLParams.Add("id", utils.UintToString(customFieldId))
|
||||
|
||||
// Call the handler
|
||||
DeleteCustomField(rr, req)
|
||||
|
||||
// Check response status
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
utils.PrintTestError(t, status, http.StatusOK)
|
||||
}
|
||||
|
||||
// Verify the custom field was deleted
|
||||
var count int64
|
||||
db.Model(&models.CustomField{}).Where("id = ?", customFieldId).Count(&count)
|
||||
if count != 0 {
|
||||
utils.PrintTestError(t, count, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteCustomFieldHandlerWithInvalidId(t *testing.T) {
|
||||
defer teardownCustomFieldHandlerTest()
|
||||
setupCustomFieldHandlerTest()
|
||||
|
||||
// Create request with invalid ID
|
||||
req, rr := createCustomFieldHandlerTestRequest(
|
||||
"DELETE",
|
||||
"/api/customField/invalid",
|
||||
"",
|
||||
)
|
||||
|
||||
// Set URL parameter
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, chi.NewRouteContext()))
|
||||
chiCtx := chi.RouteContext(req.Context())
|
||||
chiCtx.URLParams.Add("id", "invalid")
|
||||
|
||||
// Call the handler
|
||||
DeleteCustomField(rr, req)
|
||||
|
||||
// Check response status - should be error
|
||||
if status := rr.Code; status != http.StatusInternalServerError {
|
||||
utils.PrintTestError(t, status, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// Parse error response
|
||||
var errorResponse map[string]string
|
||||
err := json.Unmarshal(rr.Body.Bytes(), &errorResponse)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err.Error(), nil)
|
||||
}
|
||||
|
||||
// Check error message
|
||||
if errorMsg, exists := errorResponse["errorMsg"]; !exists || !strings.Contains(errorMsg, "Error deleting custom field") {
|
||||
utils.PrintTestError(t, errorMsg, "Error deleting custom field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteCustomFieldHandlerWithNonExistentId(t *testing.T) {
|
||||
defer teardownCustomFieldHandlerTest()
|
||||
setupCustomFieldHandlerTest()
|
||||
|
||||
// Use a non-existent ID (999)
|
||||
nonExistentId := "999"
|
||||
|
||||
// Create request with non-existent ID
|
||||
req, rr := createCustomFieldHandlerTestRequest(
|
||||
"DELETE",
|
||||
"/api/customField/"+nonExistentId,
|
||||
"",
|
||||
)
|
||||
|
||||
// Set URL parameter
|
||||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, chi.NewRouteContext()))
|
||||
chiCtx := chi.RouteContext(req.Context())
|
||||
chiCtx.URLParams.Add("id", nonExistentId)
|
||||
|
||||
// Call the handler
|
||||
DeleteCustomField(rr, req)
|
||||
|
||||
// Should still return OK because deleting a non-existent record is not an error in this implementation
|
||||
if status := rr.Code; status != http.StatusOK {
|
||||
utils.PrintTestError(t, status, http.StatusOK)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
type CustomField struct {
|
||||
BaseModel
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Type CustomFieldType `gorm:"not null" json:"type"`
|
||||
Description string `json:"description"`
|
||||
Options []CustomFieldOption `json:"options"`
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package models
|
||||
|
||||
type CustomFieldOption struct {
|
||||
BaseModel
|
||||
Value string `gorm:"not null" json:"value"`
|
||||
CustomField CustomField `json:"-"`
|
||||
CustomFieldId uint `gorm:"not null" json:"customFieldId"`
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type CustomFieldType string
|
||||
|
||||
const (
|
||||
TEXT CustomFieldType = "TEXT"
|
||||
DATE CustomFieldType = "DATE"
|
||||
SELECT CustomFieldType = "SELECT"
|
||||
CURRENCY CustomFieldType = "CURRENCY"
|
||||
BOOLEAN CustomFieldType = "BOOLEAN"
|
||||
)
|
||||
|
||||
func (fieldType *CustomFieldType) Scan(value string) error {
|
||||
*fieldType = CustomFieldType(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fieldType CustomFieldType) Value() (driver.Value, error) {
|
||||
if len(fieldType) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if fieldType != TEXT &&
|
||||
fieldType != DATE &&
|
||||
fieldType != SELECT &&
|
||||
fieldType != CURRENCY &&
|
||||
fieldType != BOOLEAN {
|
||||
return "", errors.New("invalid custom field type")
|
||||
}
|
||||
return string(fieldType), nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/shopspring/decimal"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CustomFieldValue struct {
|
||||
BaseModel
|
||||
Receipt Receipt `json:"-"`
|
||||
ReceiptId uint `json:"receiptId"`
|
||||
CustomField CustomField `json:"-"`
|
||||
CustomFieldId uint `json:"customFieldId"`
|
||||
StringValue *string `json:"stringValue"`
|
||||
DateValue *time.Time `json:"dateValue"`
|
||||
SelectValue *uint `json:"selectValue"`
|
||||
CurrencyValue *decimal.Decimal `json:"currencyValue"`
|
||||
BooleanValue *bool `json:"booleanValue"`
|
||||
}
|
||||
+15
-14
@@ -8,20 +8,21 @@ import (
|
||||
|
||||
type Receipt struct {
|
||||
BaseModel
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Amount decimal.Decimal `gorm:"type:decimal(10,2);not null" json:"amount"`
|
||||
Date time.Time `gorm:"not null" json:"date"`
|
||||
ResolvedDate *time.Time `json:"resolvedDate"`
|
||||
PaidByUserID uint `json:"paidByUserId"`
|
||||
PaidByUser User `json:"-"`
|
||||
Status ReceiptStatus `gorm:"default:'OPEN';not null" json:"status"`
|
||||
GroupId uint `gorm:"not null" json:"groupId"`
|
||||
Group Group `json:"-"`
|
||||
Categories []Category `gorm:"many2many:receipt_categories" json:"categories"`
|
||||
Tags []Tag `gorm:"many2many:receipt_tags" json:"tags"`
|
||||
ImageFiles []FileData `json:"imageFiles"`
|
||||
ReceiptItems []Item `json:"receiptItems"`
|
||||
Comments []Comment `json:"comments"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Amount decimal.Decimal `gorm:"type:decimal(10,2);not null" json:"amount"`
|
||||
Date time.Time `gorm:"not null" json:"date"`
|
||||
ResolvedDate *time.Time `json:"resolvedDate"`
|
||||
PaidByUserID uint `json:"paidByUserId"`
|
||||
PaidByUser User `json:"-"`
|
||||
Status ReceiptStatus `gorm:"default:'OPEN';not null" json:"status"`
|
||||
GroupId uint `gorm:"not null" json:"groupId"`
|
||||
Group Group `json:"-"`
|
||||
Categories []Category `gorm:"many2many:receipt_categories" json:"categories"`
|
||||
Tags []Tag `gorm:"many2many:receipt_tags" json:"tags"`
|
||||
ImageFiles []FileData `json:"imageFiles"`
|
||||
ReceiptItems []Item `json:"receiptItems"`
|
||||
Comments []Comment `json:"comments"`
|
||||
CustomFields []CustomFieldValue `json:"customFields"`
|
||||
}
|
||||
|
||||
func (r *Receipt) ToString() (string, error) {
|
||||
|
||||
@@ -8,6 +8,14 @@ func BuildGroupMap() map[GroupRole]int {
|
||||
return groupMap
|
||||
}
|
||||
|
||||
func HasRole(role UserRole, roleToCheck UserRole) bool {
|
||||
return role == roleToCheck
|
||||
func BuildUserRoleMap() map[UserRole]int {
|
||||
userRoleMap := make(map[UserRole]int)
|
||||
userRoleMap[USER] = 0
|
||||
userRoleMap[ADMIN] = 1
|
||||
return userRoleMap
|
||||
}
|
||||
|
||||
func HasRole(role UserRole, roleToCheck UserRole) bool {
|
||||
userRoleMap := BuildUserRoleMap()
|
||||
return userRoleMap[role] <= userRoleMap[roleToCheck]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"receipt-wrangler/api/internal/commands"
|
||||
"receipt-wrangler/api/internal/models"
|
||||
)
|
||||
|
||||
type CustomFieldRepository struct {
|
||||
BaseRepository
|
||||
}
|
||||
|
||||
func NewCustomFieldRepository(tx *gorm.DB) CustomFieldRepository {
|
||||
repository := CustomFieldRepository{BaseRepository: BaseRepository{
|
||||
DB: GetDB(),
|
||||
TX: tx,
|
||||
}}
|
||||
return repository
|
||||
}
|
||||
|
||||
func (repository CustomFieldRepository) GetPagedCustomFields(
|
||||
pagedRequestCommand commands.PagedRequestCommand,
|
||||
) ([]models.CustomField, int64, error) {
|
||||
db := repository.GetDB()
|
||||
var customFields []models.CustomField
|
||||
|
||||
err := repository.validateOrderBy(pagedRequestCommand.OrderBy)
|
||||
if err != nil {
|
||||
return customFields, 0, err
|
||||
}
|
||||
|
||||
query := repository.Sort(db, pagedRequestCommand.OrderBy, pagedRequestCommand.SortDirection)
|
||||
query = query.Scopes(repository.Paginate(pagedRequestCommand.Page, pagedRequestCommand.PageSize))
|
||||
|
||||
err = query.Model(&models.CustomField{}).Preload("Options").Find(&customFields).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var count int64
|
||||
err = db.Model(&models.CustomField{}).Count(&count).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return customFields, count, nil
|
||||
}
|
||||
|
||||
func (repository CustomFieldRepository) CreateCustomField(
|
||||
command commands.UpsertCustomFieldCommand,
|
||||
createdBy *uint,
|
||||
) (models.CustomField, error) {
|
||||
db := repository.GetDB()
|
||||
|
||||
options := make([]models.CustomFieldOption, 0, len(command.Options))
|
||||
for _, optionCommand := range command.Options {
|
||||
option := models.CustomFieldOption{
|
||||
CustomFieldId: optionCommand.CustomFieldId,
|
||||
Value: optionCommand.Value,
|
||||
}
|
||||
options = append(options, option)
|
||||
}
|
||||
|
||||
customFieldToCreate := models.CustomField{
|
||||
BaseModel: models.BaseModel{
|
||||
CreatedBy: createdBy,
|
||||
},
|
||||
Name: command.Name,
|
||||
Type: command.Type,
|
||||
Description: command.Description,
|
||||
Options: options,
|
||||
}
|
||||
|
||||
err := db.Create(&customFieldToCreate).Error
|
||||
if err != nil {
|
||||
return models.CustomField{}, err
|
||||
}
|
||||
|
||||
return customFieldToCreate, nil
|
||||
}
|
||||
|
||||
func (repository CustomFieldRepository) GetCustomFieldById(id uint) (models.CustomField, error) {
|
||||
db := repository.GetDB()
|
||||
var customField models.CustomField
|
||||
|
||||
err := db.Preload("Options").First(&customField, id).Error
|
||||
if err != nil {
|
||||
return models.CustomField{}, err
|
||||
}
|
||||
|
||||
return customField, nil
|
||||
}
|
||||
|
||||
func (repository CustomFieldRepository) DeleteCustomField(id uint) error {
|
||||
db := repository.GetDB()
|
||||
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
err := tx.Delete(&models.CustomFieldOption{}, "custom_field_id = ?", id).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Delete(&models.CustomFieldValue{}, "custom_field_id = ?", id).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tx.Delete(&models.CustomField{}, id).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repository CustomFieldRepository) validateOrderBy(orderBy string) error {
|
||||
if orderBy != "name" && orderBy != "type" && orderBy != "description" {
|
||||
return errors.New("invalid orderBy")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"receipt-wrangler/api/internal/commands"
|
||||
"receipt-wrangler/api/internal/models"
|
||||
"receipt-wrangler/api/internal/utils"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func createTestCustomFields() {
|
||||
db := GetDB()
|
||||
|
||||
// Create custom field with TEXT type
|
||||
textField := models.CustomField{
|
||||
Name: "Test Text Field",
|
||||
Type: models.TEXT,
|
||||
Description: "A test text field",
|
||||
}
|
||||
db.Create(&textField)
|
||||
|
||||
// Create custom field with DATE type
|
||||
dateField := models.CustomField{
|
||||
Name: "Test Date Field",
|
||||
Type: models.DATE,
|
||||
Description: "A test date field",
|
||||
}
|
||||
db.Create(&dateField)
|
||||
|
||||
// Create custom field with SELECT type and options
|
||||
selectField := models.CustomField{
|
||||
Name: "Test Select Field",
|
||||
Type: models.SELECT,
|
||||
Description: "A test select field",
|
||||
}
|
||||
db.Create(&selectField)
|
||||
|
||||
// Add options to the SELECT field
|
||||
option1 := models.CustomFieldOption{
|
||||
Value: "Option 1",
|
||||
CustomFieldId: selectField.ID,
|
||||
}
|
||||
option2 := models.CustomFieldOption{
|
||||
Value: "Option 2",
|
||||
CustomFieldId: selectField.ID,
|
||||
}
|
||||
db.Create(&option1)
|
||||
db.Create(&option2)
|
||||
|
||||
// Create custom field with CURRENCY type
|
||||
currencyField := models.CustomField{
|
||||
Name: "Test Currency Field",
|
||||
Type: models.CURRENCY,
|
||||
Description: "A test currency field",
|
||||
}
|
||||
db.Create(¤cyField)
|
||||
}
|
||||
|
||||
func setupCustomFieldRepositoryTest() {
|
||||
createTestCustomFields()
|
||||
}
|
||||
|
||||
func teardownCustomFieldRepositoryTest() {
|
||||
TruncateTestDb()
|
||||
}
|
||||
|
||||
func TestShouldCreateCustomField(t *testing.T) {
|
||||
defer teardownCustomFieldRepositoryTest()
|
||||
|
||||
repository := NewCustomFieldRepository(nil)
|
||||
createdBy := uint(1)
|
||||
|
||||
// Create a TEXT type custom field
|
||||
command := commands.UpsertCustomFieldCommand{
|
||||
Name: "Test New Text Field",
|
||||
Type: models.TEXT,
|
||||
Description: "A new test text field",
|
||||
Options: []commands.UpsertCustomFieldOptionCommand{},
|
||||
}
|
||||
|
||||
customField, err := repository.CreateCustomField(command, &createdBy)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the created custom field
|
||||
if customField.ID == 0 {
|
||||
utils.PrintTestError(t, "Custom field ID should not be 0", nil)
|
||||
}
|
||||
|
||||
if customField.Name != "Test New Text Field" {
|
||||
utils.PrintTestError(t, customField.Name, "Test New Text Field")
|
||||
}
|
||||
|
||||
if customField.Type != models.TEXT {
|
||||
utils.PrintTestError(t, customField.Type, models.TEXT)
|
||||
}
|
||||
|
||||
if customField.Description != "A new test text field" {
|
||||
utils.PrintTestError(t, customField.Description, "A new test text field")
|
||||
}
|
||||
|
||||
if *customField.CreatedBy != createdBy {
|
||||
utils.PrintTestError(t, *customField.CreatedBy, createdBy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldCreateCustomFieldWithOptions(t *testing.T) {
|
||||
defer teardownCustomFieldRepositoryTest()
|
||||
|
||||
repository := NewCustomFieldRepository(nil)
|
||||
createdBy := uint(1)
|
||||
|
||||
// Create a SELECT type custom field with options
|
||||
command := commands.UpsertCustomFieldCommand{
|
||||
Name: "Test New Select Field",
|
||||
Type: models.SELECT,
|
||||
Description: "A new test select field",
|
||||
Options: []commands.UpsertCustomFieldOptionCommand{
|
||||
{
|
||||
Value: "Option A",
|
||||
},
|
||||
{
|
||||
Value: "Option B",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
customField, err := repository.CreateCustomField(command, &createdBy)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the created custom field
|
||||
if customField.ID == 0 {
|
||||
utils.PrintTestError(t, "Custom field ID should not be 0", nil)
|
||||
}
|
||||
|
||||
if customField.Type != models.SELECT {
|
||||
utils.PrintTestError(t, customField.Type, models.SELECT)
|
||||
}
|
||||
|
||||
if len(customField.Options) != 2 {
|
||||
utils.PrintTestError(t, len(customField.Options), 2)
|
||||
return
|
||||
}
|
||||
|
||||
if customField.Options[0].Value != "Option A" {
|
||||
utils.PrintTestError(t, customField.Options[0].Value, "Option A")
|
||||
}
|
||||
|
||||
if customField.Options[1].Value != "Option B" {
|
||||
utils.PrintTestError(t, customField.Options[1].Value, "Option B")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldGetPagedCustomFieldsWithDefaultSorting(t *testing.T) {
|
||||
defer teardownCustomFieldRepositoryTest()
|
||||
setupCustomFieldRepositoryTest()
|
||||
|
||||
repository := NewCustomFieldRepository(nil)
|
||||
|
||||
// Create paged request with default settings
|
||||
pagedRequest := commands.PagedRequestCommand{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
OrderBy: "name",
|
||||
SortDirection: commands.ASCENDING,
|
||||
}
|
||||
|
||||
customFields, count, err := repository.GetPagedCustomFields(pagedRequest)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Should return 4 custom fields
|
||||
if count != 4 {
|
||||
utils.PrintTestError(t, count, 4)
|
||||
}
|
||||
|
||||
if len(customFields) != 4 {
|
||||
utils.PrintTestError(t, len(customFields), 4)
|
||||
}
|
||||
|
||||
// Check if fields are correctly ordered by name
|
||||
if customFields[0].Name != "Test Currency Field" {
|
||||
utils.PrintTestError(t, customFields[0].Name, "Test Currency Field")
|
||||
}
|
||||
|
||||
if customFields[1].Name != "Test Date Field" {
|
||||
utils.PrintTestError(t, customFields[1].Name, "Test Date Field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldGetPagedCustomFieldsWithLimit(t *testing.T) {
|
||||
defer teardownCustomFieldRepositoryTest()
|
||||
setupCustomFieldRepositoryTest()
|
||||
|
||||
repository := NewCustomFieldRepository(nil)
|
||||
|
||||
// Create paged request with limit
|
||||
pagedRequest := commands.PagedRequestCommand{
|
||||
Page: 1,
|
||||
PageSize: 2,
|
||||
OrderBy: "name",
|
||||
SortDirection: commands.ASCENDING,
|
||||
}
|
||||
|
||||
customFields, count, err := repository.GetPagedCustomFields(pagedRequest)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Total count should be 4
|
||||
if count != 4 {
|
||||
utils.PrintTestError(t, count, 4)
|
||||
}
|
||||
|
||||
// But only 2 items should be returned
|
||||
if len(customFields) != 2 {
|
||||
utils.PrintTestError(t, len(customFields), 2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldGetPagedCustomFieldsWithSecondPage(t *testing.T) {
|
||||
defer teardownCustomFieldRepositoryTest()
|
||||
setupCustomFieldRepositoryTest()
|
||||
|
||||
repository := NewCustomFieldRepository(nil)
|
||||
|
||||
// Create paged request for second page
|
||||
pagedRequest := commands.PagedRequestCommand{
|
||||
Page: 2,
|
||||
PageSize: 2,
|
||||
OrderBy: "name",
|
||||
SortDirection: commands.ASCENDING,
|
||||
}
|
||||
|
||||
customFields, count, err := repository.GetPagedCustomFields(pagedRequest)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Total count should be 4
|
||||
if count != 4 {
|
||||
utils.PrintTestError(t, count, 4)
|
||||
}
|
||||
|
||||
// 2 items on the second page
|
||||
if len(customFields) != 2 {
|
||||
utils.PrintTestError(t, len(customFields), 2)
|
||||
}
|
||||
|
||||
// The items on the second page should be different from the first page
|
||||
if customFields[0].Name != "Test Select Field" {
|
||||
utils.PrintTestError(t, customFields[0].Name, "Test Select Field")
|
||||
}
|
||||
|
||||
if customFields[1].Name != "Test Text Field" {
|
||||
utils.PrintTestError(t, customFields[1].Name, "Test Text Field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldGetPagedCustomFieldsWithDescendingOrder(t *testing.T) {
|
||||
defer teardownCustomFieldRepositoryTest()
|
||||
setupCustomFieldRepositoryTest()
|
||||
|
||||
repository := NewCustomFieldRepository(nil)
|
||||
|
||||
// Create paged request with descending order
|
||||
pagedRequest := commands.PagedRequestCommand{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
OrderBy: "name",
|
||||
SortDirection: commands.DESCENDING,
|
||||
}
|
||||
|
||||
customFields, count, err := repository.GetPagedCustomFields(pagedRequest)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if count != 4 {
|
||||
utils.PrintTestError(t, count, 4)
|
||||
}
|
||||
|
||||
// Check if fields are correctly ordered by name in descending order
|
||||
if customFields[0].Name != "Test Text Field" {
|
||||
utils.PrintTestError(t, customFields[0].Name, "Test Text Field")
|
||||
}
|
||||
|
||||
if customFields[3].Name != "Test Currency Field" {
|
||||
utils.PrintTestError(t, customFields[3].Name, "Test Currency Field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldReturnErrorWithInvalidOrderBy(t *testing.T) {
|
||||
defer teardownCustomFieldRepositoryTest()
|
||||
setupCustomFieldRepositoryTest()
|
||||
|
||||
repository := NewCustomFieldRepository(nil)
|
||||
|
||||
// Create paged request with invalid orderBy field
|
||||
pagedRequest := commands.PagedRequestCommand{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
OrderBy: "invalid_column",
|
||||
SortDirection: commands.ASCENDING,
|
||||
}
|
||||
|
||||
_, _, err := repository.GetPagedCustomFields(pagedRequest)
|
||||
|
||||
// Should return an error
|
||||
if err == nil {
|
||||
utils.PrintTestError(t, "Expected error for invalid orderBy", nil)
|
||||
}
|
||||
|
||||
if err.Error() != "invalid orderBy" {
|
||||
utils.PrintTestError(t, err.Error(), "invalid orderBy")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldValidateOrderByColumn(t *testing.T) {
|
||||
repository := NewCustomFieldRepository(nil)
|
||||
|
||||
// Valid orderBy values
|
||||
if err := repository.validateOrderBy("name"); err != nil {
|
||||
utils.PrintTestError(t, err, nil)
|
||||
}
|
||||
|
||||
if err := repository.validateOrderBy("type"); err != nil {
|
||||
utils.PrintTestError(t, err, nil)
|
||||
}
|
||||
|
||||
if err := repository.validateOrderBy("description"); err != nil {
|
||||
utils.PrintTestError(t, err, nil)
|
||||
}
|
||||
|
||||
// Invalid orderBy values
|
||||
if err := repository.validateOrderBy("id"); err == nil {
|
||||
utils.PrintTestError(t, "Expected error for invalid orderBy 'id'", nil)
|
||||
}
|
||||
|
||||
if err := repository.validateOrderBy("created_at"); err == nil {
|
||||
utils.PrintTestError(t, "Expected error for invalid orderBy 'created_at'", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldGetCustomFieldById(t *testing.T) {
|
||||
defer teardownCustomFieldRepositoryTest()
|
||||
setupCustomFieldRepositoryTest()
|
||||
|
||||
repository := NewCustomFieldRepository(nil)
|
||||
db := GetDB()
|
||||
|
||||
// Get a custom field from the test DB to use its ID
|
||||
var expectedCustomField models.CustomField
|
||||
db.Where("name = ?", "Test Text Field").First(&expectedCustomField)
|
||||
|
||||
// Get the custom field by ID
|
||||
customField, err := repository.GetCustomFieldById(expectedCustomField.ID)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the fetched custom field
|
||||
if customField.ID != expectedCustomField.ID {
|
||||
utils.PrintTestError(t, customField.ID, expectedCustomField.ID)
|
||||
}
|
||||
|
||||
if customField.Name != "Test Text Field" {
|
||||
utils.PrintTestError(t, customField.Name, "Test Text Field")
|
||||
}
|
||||
|
||||
if customField.Type != models.TEXT {
|
||||
utils.PrintTestError(t, string(customField.Type), string(models.TEXT))
|
||||
}
|
||||
|
||||
if customField.Description != "A test text field" {
|
||||
utils.PrintTestError(t, customField.Description, "A test text field")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldGetCustomFieldByIdWithSelectType(t *testing.T) {
|
||||
defer teardownCustomFieldRepositoryTest()
|
||||
setupCustomFieldRepositoryTest()
|
||||
|
||||
repository := NewCustomFieldRepository(nil)
|
||||
db := GetDB()
|
||||
|
||||
// Get a SELECT type custom field from the test DB
|
||||
var expectedCustomField models.CustomField
|
||||
db.Where("name = ?", "Test Select Field").First(&expectedCustomField)
|
||||
|
||||
// Get the custom field by ID
|
||||
customField, err := repository.GetCustomFieldById(expectedCustomField.ID)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the fetched custom field
|
||||
if customField.ID != expectedCustomField.ID {
|
||||
utils.PrintTestError(t, customField.ID, expectedCustomField.ID)
|
||||
}
|
||||
|
||||
if customField.Name != "Test Select Field" {
|
||||
utils.PrintTestError(t, customField.Name, "Test Select Field")
|
||||
}
|
||||
|
||||
if customField.Type != models.SELECT {
|
||||
utils.PrintTestError(t, string(customField.Type), string(models.SELECT))
|
||||
}
|
||||
|
||||
// Verify that options are preloaded
|
||||
if len(customField.Options) != 2 {
|
||||
utils.PrintTestError(t, len(customField.Options), 2)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify option values
|
||||
foundOption1 := false
|
||||
foundOption2 := false
|
||||
|
||||
for _, option := range customField.Options {
|
||||
if option.Value == "Option 1" {
|
||||
foundOption1 = true
|
||||
}
|
||||
if option.Value == "Option 2" {
|
||||
foundOption2 = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundOption1 {
|
||||
utils.PrintTestError(t, "Missing Option 1", "Option 1 should be present")
|
||||
}
|
||||
|
||||
if !foundOption2 {
|
||||
utils.PrintTestError(t, "Missing Option 2", "Option 2 should be present")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldReturnErrorForNonExistentCustomFieldId(t *testing.T) {
|
||||
defer teardownCustomFieldRepositoryTest()
|
||||
setupCustomFieldRepositoryTest()
|
||||
|
||||
repository := NewCustomFieldRepository(nil)
|
||||
|
||||
// Try to get a custom field with a non-existent ID
|
||||
nonExistentId := uint(999)
|
||||
_, err := repository.GetCustomFieldById(nonExistentId)
|
||||
|
||||
// Should return an error
|
||||
if err == nil {
|
||||
utils.PrintTestError(t, "Expected error for non-existent ID", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldDeleteCustomField(t *testing.T) {
|
||||
defer teardownCustomFieldRepositoryTest()
|
||||
setupCustomFieldRepositoryTest()
|
||||
|
||||
repository := NewCustomFieldRepository(nil)
|
||||
db := GetDB()
|
||||
|
||||
// Get a TEXT type custom field to delete
|
||||
var customField models.CustomField
|
||||
db.Where("name = ?", "Test Text Field").First(&customField)
|
||||
customFieldId := customField.ID
|
||||
|
||||
// Delete the custom field
|
||||
err := repository.DeleteCustomField(customFieldId)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Try to get the deleted custom field - should return error
|
||||
_, err = repository.GetCustomFieldById(customFieldId)
|
||||
if err == nil {
|
||||
utils.PrintTestError(t, "Expected error when getting deleted custom field", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldDeleteCustomFieldWithOptions(t *testing.T) {
|
||||
defer teardownCustomFieldRepositoryTest()
|
||||
setupCustomFieldRepositoryTest()
|
||||
|
||||
repository := NewCustomFieldRepository(nil)
|
||||
db := GetDB()
|
||||
|
||||
// Get a SELECT type custom field with options
|
||||
var customField models.CustomField
|
||||
db.Where("name = ?", "Test Select Field").First(&customField)
|
||||
customFieldId := customField.ID
|
||||
|
||||
// Verify options exist before deletion
|
||||
var optionsCount int64
|
||||
db.Model(&models.CustomFieldOption{}).Where("custom_field_id = ?", customFieldId).Count(&optionsCount)
|
||||
if optionsCount == 0 {
|
||||
utils.PrintTestError(t, "Expected options to exist before deletion", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete the custom field
|
||||
err := repository.DeleteCustomField(customFieldId)
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify custom field was deleted
|
||||
var customFieldExists bool
|
||||
err = db.Model(&models.CustomField{}).
|
||||
Select("count(*) > 0").
|
||||
Where("id = ?", customFieldId).
|
||||
Find(&customFieldExists).
|
||||
Error
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err, nil)
|
||||
return
|
||||
}
|
||||
if customFieldExists {
|
||||
utils.PrintTestError(t, "Custom field should be deleted", nil)
|
||||
}
|
||||
|
||||
// Verify options were deleted
|
||||
var optionsExist bool
|
||||
err = db.Model(&models.CustomFieldOption{}).
|
||||
Select("count(*) > 0").
|
||||
Where("custom_field_id = ?", customFieldId).
|
||||
Find(&optionsExist).
|
||||
Error
|
||||
if err != nil {
|
||||
utils.PrintTestError(t, err, nil)
|
||||
return
|
||||
}
|
||||
if optionsExist {
|
||||
utils.PrintTestError(t, "Custom field options should be deleted", nil)
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,9 @@ func MakeMigrations() error {
|
||||
err := db.AutoMigrate(
|
||||
&models.RefreshToken{},
|
||||
&models.User{},
|
||||
&models.CustomField{},
|
||||
&models.CustomFieldValue{},
|
||||
&models.CustomFieldOption{},
|
||||
&models.Receipt{},
|
||||
&models.Item{},
|
||||
&models.FileData{},
|
||||
|
||||
@@ -155,6 +155,11 @@ func (repository ReceiptRepository) UpdateReceipt(id string, command commands.Up
|
||||
return txErr
|
||||
}
|
||||
|
||||
txErr = tx.Model(¤tReceipt).Association("CustomFields").Replace(&updatedReceipt.CustomFields)
|
||||
if txErr != nil {
|
||||
return txErr
|
||||
}
|
||||
|
||||
for _, item := range updatedReceipt.ReceiptItems {
|
||||
txErr = tx.Model(&item).Association("Categories").Replace(&item.Categories)
|
||||
if txErr != nil {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"receipt-wrangler/api/internal/handlers"
|
||||
"receipt-wrangler/api/internal/middleware"
|
||||
|
||||
jwtmiddleware "github.com/auth0/go-jwt-middleware/v2"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func BuildCustomFieldRouter(tokenValidator *jwtmiddleware.JWTMiddleware) *chi.Mux {
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.MoveJWTCookieToHeader, tokenValidator.CheckJWT)
|
||||
|
||||
router.Get("/{id}", handlers.GetCustomFieldById)
|
||||
router.Delete("/{id}", handlers.DeleteCustomField)
|
||||
router.Post("/getPagedCustomFields", handlers.GetPagedCustomFields)
|
||||
router.Post("/", handlers.CreateCustomField)
|
||||
|
||||
return router
|
||||
}
|
||||
@@ -124,5 +124,9 @@ func BuildRootRouter() *chi.Mux {
|
||||
exportRouter := BuildExportRouter(tokenValidatorMiddleware)
|
||||
rootRouter.Mount("/api/export", exportRouter)
|
||||
|
||||
// Custom Field router
|
||||
customFieldRouter := BuildCustomFieldRouter(tokenValidatorMiddleware)
|
||||
rootRouter.Mount("/api/customField", customFieldRouter)
|
||||
|
||||
return rootRouter
|
||||
}
|
||||
|
||||
+238
@@ -309,6 +309,102 @@ paths:
|
||||
$ref: "#/components/responses/Internal"
|
||||
security:
|
||||
- bearerAuth: [ ]
|
||||
/customField/:
|
||||
post:
|
||||
tags:
|
||||
- CustomField
|
||||
summary: Create custom field
|
||||
description: This will create a custom field
|
||||
operationId: createCustomField
|
||||
requestBody:
|
||||
description: Custom field to create
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UpsertCustomFieldCommand"
|
||||
responses:
|
||||
200:
|
||||
description: Created custom field
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CustomField"
|
||||
403:
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
500:
|
||||
$ref: "#/components/responses/Internal"
|
||||
security:
|
||||
- bearerAuth: [ ]
|
||||
/customField/{customFieldId}:
|
||||
parameters:
|
||||
- in: path
|
||||
name: customFieldId
|
||||
schema:
|
||||
type: integer
|
||||
required: true
|
||||
description: Custom field Id to get
|
||||
get:
|
||||
tags:
|
||||
- CustomField
|
||||
summary: Get custom field
|
||||
description: This will get a custom field by id
|
||||
operationId: getCustomFieldById
|
||||
responses:
|
||||
200:
|
||||
description: The custom field
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CustomField"
|
||||
403:
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
500:
|
||||
$ref: "#/components/responses/Internal"
|
||||
security:
|
||||
- bearerAuth: [ ]
|
||||
delete:
|
||||
tags:
|
||||
- CustomField
|
||||
summary: Delete custom field
|
||||
description: This will delete a custom field by id
|
||||
operationId: deleteCustomField
|
||||
responses:
|
||||
200:
|
||||
$ref: "#/components/responses/Ok"
|
||||
403:
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
500:
|
||||
$ref: "#/components/responses/Internal"
|
||||
security:
|
||||
- bearerAuth: [ ]
|
||||
/customField/getPagedCustomFields:
|
||||
post:
|
||||
tags:
|
||||
- CustomField
|
||||
summary: Get paged custom fields
|
||||
description: This will return paged custom fields
|
||||
operationId: getPagedCustomFields
|
||||
requestBody:
|
||||
description: Paging and sorting data
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PagedRequestCommand"
|
||||
responses:
|
||||
200:
|
||||
description: Paged categories
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/PagedData"
|
||||
403:
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
500:
|
||||
$ref: "#/components/responses/Internal"
|
||||
security:
|
||||
- bearerAuth: [ ]
|
||||
/export/{groupId}:
|
||||
post:
|
||||
tags:
|
||||
@@ -2477,6 +2573,14 @@ components:
|
||||
type: string
|
||||
enum:
|
||||
- "CSV"
|
||||
CustomFieldType:
|
||||
type: string
|
||||
enum:
|
||||
- "TEXT"
|
||||
- "DATE"
|
||||
- "SELECT"
|
||||
- "CURRENCY"
|
||||
- "BOOLEAN"
|
||||
Activity:
|
||||
type: object
|
||||
required:
|
||||
@@ -2893,6 +2997,11 @@ components:
|
||||
- name
|
||||
- paidByUserId
|
||||
- status
|
||||
- customFields
|
||||
- categories
|
||||
- tags
|
||||
- receiptItems
|
||||
- comments
|
||||
- id
|
||||
type: object
|
||||
properties:
|
||||
@@ -2909,6 +3018,11 @@ components:
|
||||
description: Comments associated to receipt
|
||||
items:
|
||||
$ref: "#/components/schemas/Comment"
|
||||
customFields:
|
||||
type: array
|
||||
description: Custom fields associated to receipt
|
||||
items:
|
||||
$ref: "#/components/schemas/CustomFieldValue"
|
||||
createdAt:
|
||||
type: string
|
||||
createdBy:
|
||||
@@ -3377,6 +3491,7 @@ components:
|
||||
- $ref: "#/components/schemas/ReceiptProcessingSettings"
|
||||
- $ref: "#/components/schemas/SystemEmail"
|
||||
- $ref: "#/components/schemas/Activity"
|
||||
- $ref: "#/components/schemas/CustomField"
|
||||
totalCount:
|
||||
type: integer
|
||||
FeatureConfig:
|
||||
@@ -3942,6 +4057,11 @@ components:
|
||||
description: Comments associated to receipt
|
||||
items:
|
||||
$ref: "#/components/schemas/UpsertCommentCommand"
|
||||
customFields:
|
||||
type: array
|
||||
description: Custom fields associated to receipt
|
||||
items:
|
||||
$ref: "#/components/schemas/UpsertCustomFieldCommand"
|
||||
UpsertItemCommand:
|
||||
type: object
|
||||
required:
|
||||
@@ -4361,6 +4481,124 @@ components:
|
||||
version:
|
||||
type: string
|
||||
description: Version
|
||||
CustomFieldOption:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/BaseModel"
|
||||
- type: object
|
||||
required:
|
||||
- customFieldId
|
||||
properties:
|
||||
value:
|
||||
type: string
|
||||
description: Custom Field Option value
|
||||
customFieldId:
|
||||
type: integer
|
||||
description: Custom Field Id
|
||||
UpsertCustomFieldOptionCommand:
|
||||
type: object
|
||||
required:
|
||||
- customFieldId
|
||||
properties:
|
||||
value:
|
||||
type: string
|
||||
description: Custom Field Option value
|
||||
customFieldId:
|
||||
type: integer
|
||||
description: Custom Field Id
|
||||
CustomField:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/BaseModel"
|
||||
- type: object
|
||||
required:
|
||||
- name
|
||||
- type
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Custom Field name
|
||||
type:
|
||||
$ref: "#/components/schemas/CustomFieldType"
|
||||
description:
|
||||
type: string
|
||||
description: Custom Field description
|
||||
options:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/CustomFieldOption"
|
||||
UpsertCustomFieldCommand:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- type
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Custom Field name
|
||||
type:
|
||||
$ref: "#/components/schemas/CustomFieldType"
|
||||
description:
|
||||
type: string
|
||||
description: Custom Field description
|
||||
options:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/UpsertCustomFieldOptionCommand"
|
||||
CustomFieldValue:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/BaseModel"
|
||||
- type: object
|
||||
required:
|
||||
- receiptId
|
||||
- customFieldId
|
||||
properties:
|
||||
receiptId:
|
||||
type: integer
|
||||
description: Receipt Id
|
||||
customFieldId:
|
||||
type: integer
|
||||
description: Custom Field ID
|
||||
stringValue:
|
||||
type: string
|
||||
description: Custom Field String Value
|
||||
dateValue:
|
||||
type: string
|
||||
description: Custom Field Date Value
|
||||
selectValue:
|
||||
type: integer
|
||||
description: Custom Field Select Value
|
||||
currencyValue:
|
||||
type: string
|
||||
description: Custom Field Currency Value
|
||||
booleanValue:
|
||||
type: boolean
|
||||
description: Custom Field Boolean Value
|
||||
UpsertCustomFieldValueCommand:
|
||||
type: object
|
||||
required:
|
||||
- receiptId
|
||||
- customFieldId
|
||||
properties:
|
||||
receiptId:
|
||||
type: integer
|
||||
description: Receipt Id
|
||||
customFieldId:
|
||||
type: integer
|
||||
description: Custom Field ID
|
||||
stringValue:
|
||||
type: string
|
||||
description: Custom Field String Value
|
||||
dateValue:
|
||||
type: string
|
||||
description: Custom Field Date Value
|
||||
selectValue:
|
||||
type: integer
|
||||
description: Custom Field Select Value
|
||||
currencyValue:
|
||||
type: string
|
||||
description: Custom Field Currency Value
|
||||
booleanValue:
|
||||
type: boolean
|
||||
description: Custom Field Boolean Value
|
||||
|
||||
InternalErrorResponse:
|
||||
type: object
|
||||
|
||||
Reference in New Issue
Block a user