diff --git a/internal/commands/paged_activity_request_command.go b/internal/commands/paged_activity_request_command.go new file mode 100644 index 00000000..c5d42778 --- /dev/null +++ b/internal/commands/paged_activity_request_command.go @@ -0,0 +1,37 @@ +package commands + +import ( + "encoding/json" + "net/http" + "receipt-wrangler/api/internal/structs" + "receipt-wrangler/api/internal/utils" +) + +type PagedActivityRequestCommand struct { + PagedRequestCommand + GroupIds []uint `json:"groupIds"` +} + +func (command *PagedActivityRequestCommand) 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 + } + + return nil +} + +func (command *PagedActivityRequestCommand) Validate() structs.ValidatorError { + vErrs := command.PagedRequestCommand.Validate() + + if len(command.GroupIds) == 0 { + vErrs.Errors["groupIds"] = "Must provide at least one group id" + } + + return vErrs +} diff --git a/internal/handlers/generic_handler.go b/internal/handlers/generic_handler.go index 723c34f5..2be16346 100644 --- a/internal/handlers/generic_handler.go +++ b/internal/handlers/generic_handler.go @@ -43,6 +43,11 @@ func HandleRequest(handler structs.Handler) { } } + if len(handler.GroupRole) > 0 && len(handler.GroupId) == 0 && len(handler.GroupIds) == 0 { + utils.WriteCustomErrorResponse(handler.Writer, "Group ID is required to validate group role", http.StatusForbidden) + return + } + if len(handler.GroupRole) > 0 && len(handler.GroupId) > 0 { groupService := services.NewGroupService(nil) token := structs.GetJWT(handler.Request) @@ -60,6 +65,11 @@ func HandleRequest(handler structs.Handler) { } } + if len(handler.GroupRole) > 0 && len(handler.GroupIds) == 0 && len(handler.GroupId) == 0 { + utils.WriteCustomErrorResponse(handler.Writer, "Group IDs are required to validate group role", http.StatusForbidden) + return + } + if len(handler.GroupRole) > 0 && len(handler.GroupIds) > 0 { groupService := services.NewGroupService(nil) token := structs.GetJWT(handler.Request) diff --git a/internal/handlers/generic_handler_test.go b/internal/handlers/generic_handler_test.go index b3816573..7f163ed7 100644 --- a/internal/handlers/generic_handler_test.go +++ b/internal/handlers/generic_handler_test.go @@ -191,7 +191,6 @@ func TestShouldAcceptReceiptAccessBasedOnGroup(t *testing.T) { Writer: w, Request: r, ResponseType: constants.ApplicationJson, - GroupRole: models.OWNER, ReceiptId: "1", HandlerFunction: func(w http.ResponseWriter, r *http.Request) (int, error) { return 0, nil @@ -278,6 +277,64 @@ func TestShouldAcceptReceiptsAccessBasedOnGroup(t *testing.T) { } } +func TestShouldRejectAccessBasedOnEmptyGroupId(t *testing.T) { + defer tearDownGenericHandlerTest() + reader := strings.NewReader("") + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/api", reader) + + newContext := context.WithValue(r.Context(), jwtmiddleware.ContextKey{}, &validator.ValidatedClaims{CustomClaims: &structs.Claims{UserId: 1}}) + r = r.WithContext(newContext) + + repositories.CreateTestGroupWithUsers() + + handler := structs.Handler{ + Writer: w, + Request: r, + ResponseType: constants.ApplicationJson, + GroupRole: models.VIEWER, + GroupId: "", + HandlerFunction: func(w http.ResponseWriter, r *http.Request) (int, error) { + return 0, nil + }, + } + + HandleRequest(handler) + + if w.Result().StatusCode != http.StatusForbidden { + utils.PrintTestError(t, w.Result().StatusCode, http.StatusOK) + } +} + +func TestShouldRejectAccessBasedOnEmptyGroupIds(t *testing.T) { + defer tearDownGenericHandlerTest() + reader := strings.NewReader("") + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/api", reader) + + newContext := context.WithValue(r.Context(), jwtmiddleware.ContextKey{}, &validator.ValidatedClaims{CustomClaims: &structs.Claims{UserId: 1}}) + r = r.WithContext(newContext) + + repositories.CreateTestGroupWithUsers() + + handler := structs.Handler{ + Writer: w, + Request: r, + ResponseType: constants.ApplicationJson, + GroupRole: models.VIEWER, + GroupIds: []string{}, + HandlerFunction: func(w http.ResponseWriter, r *http.Request) (int, error) { + return 0, nil + }, + } + + HandleRequest(handler) + + if w.Result().StatusCode != http.StatusForbidden { + utils.PrintTestError(t, w.Result().StatusCode, http.StatusOK) + } +} + func TestShouldRejectReceiptAccessBasedOnWrongGroupRole(t *testing.T) { defer tearDownGenericHandlerTest() reader := strings.NewReader("") diff --git a/internal/handlers/receipts.go b/internal/handlers/receipts.go index 7232107d..f3800466 100644 --- a/internal/handlers/receipts.go +++ b/internal/handlers/receipts.go @@ -163,7 +163,7 @@ func CreateReceipt(w http.ResponseWriter, r *http.Request) { ResponseType: constants.ApplicationJson, HandlerFunction: func(w http.ResponseWriter, r *http.Request) (int, error) { receiptRepository := repositories.NewReceiptRepository(nil) - createdReceipt, err := receiptRepository.CreateReceipt(command, token.UserId) + createdReceipt, err := receiptRepository.CreateReceipt(command, token.UserId, true) if err != nil { return http.StatusInternalServerError, err } diff --git a/internal/handlers/system_task.go b/internal/handlers/system_task.go index 0ebdbe63..19f04deb 100644 --- a/internal/handlers/system_task.go +++ b/internal/handlers/system_task.go @@ -1,13 +1,17 @@ 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/logging" "receipt-wrangler/api/internal/models" "receipt-wrangler/api/internal/repositories" "receipt-wrangler/api/internal/structs" "receipt-wrangler/api/internal/utils" + "receipt-wrangler/api/internal/wranglerasynq" ) func GetSystemTasks(w http.ResponseWriter, r *http.Request) { @@ -60,3 +64,152 @@ func GetSystemTasks(w http.ResponseWriter, r *http.Request) { HandleRequest(handler) } + +func GetActivitiesForGroups(w http.ResponseWriter, r *http.Request) { + command := commands.PagedActivityRequestCommand{} + err := command.LoadDataFromRequest(w, r) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + stringGroupIds := make([]string, 0) + for _, groupId := range command.GroupIds { + stringGroupIds = append(stringGroupIds, utils.UintToString(groupId)) + } + + handler := structs.Handler{ + ErrorMessage: "Error getting group activities", + Writer: w, + Request: r, + GroupIds: stringGroupIds, + GroupRole: models.VIEWER, + ResponseType: constants.ApplicationJson, + HandlerFunction: func(w http.ResponseWriter, r *http.Request) (int, error) { + + vErr := command.Validate() + if len(vErr.Errors) > 0 { + structs.WriteValidatorErrorResponse(w, vErr, http.StatusBadRequest) + return 0, nil + } + + systemTaskRepository := repositories.NewSystemTaskRepository(nil) + activities, count, err := systemTaskRepository.GetPagedActivities(command) + if err != nil { + return http.StatusInternalServerError, err + } + + err = wranglerasynq.SetActivityCanBeRestarted(&activities) + if err != nil { + return http.StatusInternalServerError, err + } + + pagedData := structs.PagedData{} + data := make([]any, 0) + + for i := 0; i < len(activities); i++ { + data = append(data, activities[i]) + } + + pagedData.Data = data + pagedData.TotalCount = count + + responseBytes, err := utils.MarshalResponseData(pagedData) + if err != nil { + return http.StatusInternalServerError, err + } + + w.WriteHeader(http.StatusOK) + w.Write(responseBytes) + + return 0, nil + }, + } + + HandleRequest(handler) +} + +func RerunActivity(w http.ResponseWriter, r *http.Request) { + systemTaskRepository := repositories.NewSystemTaskRepository(nil) + inspector, err := wranglerasynq.GetAsynqInspector() + if err != nil { + logging.LogStd(logging.LOG_LEVEL_ERROR, err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + systemTaskId := chi.URLParam(r, "id") + systemTaskUintId, err := utils.StringToUint(systemTaskId) + if err != nil { + logging.LogStd(logging.LOG_LEVEL_ERROR, err.Error()) + w.WriteHeader(http.StatusBadRequest) + return + } + + systemTask, err := systemTaskRepository.GetSystemTaskById(systemTaskUintId) + if err != nil { + logging.LogStd(logging.LOG_LEVEL_ERROR, err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if systemTask.Type != models.QUICK_SCAN { + logging.LogStd(logging.LOG_LEVEL_ERROR, "Only quick scan activities can be rerun") + w.WriteHeader(http.StatusBadRequest) + return + } + + if systemTask.AssociatedSystemTaskId == nil { + logging.LogStd(logging.LOG_LEVEL_ERROR, "Associated system task id is required to rerun quick scan activity") + w.WriteHeader(http.StatusBadRequest) + return + } + + parentSystemTask, err := systemTaskRepository.GetSystemTaskById(*systemTask.AssociatedSystemTaskId) + if err != nil { + logging.LogStd(logging.LOG_LEVEL_ERROR, err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if parentSystemTask.AsynqTaskId == "" { + logging.LogStd(logging.LOG_LEVEL_ERROR, "Parent system task does not have an asynq task id") + w.WriteHeader(http.StatusBadRequest) + return + } + + taskInfo, err := inspector.GetTaskInfo(string(models.QuickScanQueue), parentSystemTask.AsynqTaskId) + if err != nil { + logging.LogStd(logging.LOG_LEVEL_ERROR, err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + var payload wranglerasynq.QuickScanTaskPayload + err = json.Unmarshal(taskInfo.Payload, &payload) + if err != nil { + logging.LogStd(logging.LOG_LEVEL_ERROR, err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + stringGroupId := utils.UintToString(payload.GroupId) + + handler := structs.Handler{ + ErrorMessage: "Error rerunning activity", + Writer: w, + Request: r, + GroupId: stringGroupId, + GroupRole: models.EDITOR, + HandlerFunction: func(w http.ResponseWriter, r *http.Request) (int, error) { + err = inspector.RunTask(string(models.QuickScanQueue), parentSystemTask.AsynqTaskId) + if err != nil { + return http.StatusInternalServerError, err + } + + return 0, nil + }, + } + + HandleRequest(handler) +} diff --git a/internal/models/widget_type.go b/internal/models/widget_type.go index 125546b8..a49f7db9 100644 --- a/internal/models/widget_type.go +++ b/internal/models/widget_type.go @@ -10,6 +10,7 @@ type WidgetType string const ( GROUP_SUMMARY WidgetType = "GROUP_SUMMARY" FILTERED_RECEIPTS WidgetType = "FILTERED_RECEIPTS" + GROUP_ACTIVITY WidgetType = "GROUP_ACTIVITY" ) func (widgetType *WidgetType) Scan(value string) error { @@ -18,7 +19,9 @@ func (widgetType *WidgetType) Scan(value string) error { } func (widgetType WidgetType) Value() (driver.Value, error) { - if widgetType != GROUP_SUMMARY && widgetType != FILTERED_RECEIPTS { + if widgetType != GROUP_SUMMARY && + widgetType != FILTERED_RECEIPTS && + widgetType != GROUP_ACTIVITY { return nil, errors.New("invalid widget type") } return string(widgetType), nil diff --git a/internal/repositories/dashboards.go b/internal/repositories/dashboards.go index f50eacf0..d8ab5ff6 100644 --- a/internal/repositories/dashboards.go +++ b/internal/repositories/dashboards.go @@ -29,12 +29,10 @@ func (repository *DashboardRepository) CreateDashboard(command commands.UpsertDa groupId, _ = utils.StringToUint(command.GroupId) for i, widget := range command.Widgets { - configuration := []byte("{}") - widgets[i] = models.Widget{ Name: widget.Name, WidgetType: widget.WidgetType, - Configuration: configuration, + Configuration: widget.Configuration, } } diff --git a/internal/repositories/receipts.go b/internal/repositories/receipts.go index 7e24d2c7..3febb5ac 100644 --- a/internal/repositories/receipts.go +++ b/internal/repositories/receipts.go @@ -289,7 +289,11 @@ func (repository ReceiptRepository) UpdateItemsToStatus(receipt *models.Receipt, return nil } -func (repository ReceiptRepository) CreateReceipt(command commands.UpsertReceiptCommand, createdByUserID uint) (models.Receipt, error) { +func (repository ReceiptRepository) CreateReceipt( + command commands.UpsertReceiptCommand, + createdByUserID uint, + createSystemTask bool, +) (models.Receipt, error) { db := repository.GetDB() notificationRepository := NewNotificationRepository(nil) receipt, err := command.ToReceipt() @@ -301,6 +305,14 @@ func (repository ReceiptRepository) CreateReceipt(command commands.UpsertReceipt receipt.CreatedBy = &createdByUserID } + systemTask := commands.UpsertSystemTaskCommand{ + Type: models.RECEIPT_UPLOADED, + AssociatedEntityType: models.RECEIPT, + StartedAt: time.Now(), + Status: models.SYSTEM_TASK_SUCCEEDED, + RanByUserId: &createdByUserID, + } + err = db.Transaction(func(tx *gorm.DB) error { repository.SetTransaction(tx) notificationRepository.SetTransaction(tx) @@ -328,15 +340,39 @@ func (repository ReceiptRepository) CreateReceipt(command commands.UpsertReceipt return nil }) if err != nil { + if !createSystemTask { + createFailedUpdateSystemTask(systemTask, err) + } return models.Receipt{}, err } var fullyLoadedReceipt models.Receipt err = db.Model(models.Receipt{}).Where("id = ?", receipt.ID).Preload(clause.Associations).Find(&fullyLoadedReceipt).Error if err != nil { + if !createSystemTask { + createFailedUpdateSystemTask(systemTask, err) + } return models.Receipt{}, err } + if createSystemTask { + endedAt := time.Now() + systemTask.EndedAt = &endedAt + systemTask.AssociatedSystemTaskId = &fullyLoadedReceipt.ID + newReceiptString, err := getReceiptString(fullyLoadedReceipt) + if err != nil { + return models.Receipt{}, err + } + + systemTask.ResultDescription = newReceiptString + + systemTaskRepository := NewSystemTaskRepository(nil) + _, err = systemTaskRepository.CreateSystemTask(systemTask) + if err != nil { + return models.Receipt{}, err + } + } + return fullyLoadedReceipt, nil } diff --git a/internal/repositories/system_task.go b/internal/repositories/system_task.go index 95984314..308f9dca 100644 --- a/internal/repositories/system_task.go +++ b/internal/repositories/system_task.go @@ -2,11 +2,11 @@ package repositories import ( "errors" + "gorm.io/gorm" "gorm.io/gorm/clause" "receipt-wrangler/api/internal/commands" "receipt-wrangler/api/internal/models" - - "gorm.io/gorm" + "receipt-wrangler/api/internal/structs" ) type SystemTaskRepository struct { @@ -59,6 +59,45 @@ func (repository SystemTaskRepository) GetPagedSystemTasks(command commands.GetS return results, count, nil } +func (repository SystemTaskRepository) GetPagedActivities(command commands.PagedActivityRequestCommand) ( + []structs.Activity, + int64, + error, +) { + db := repository.GetDB() + var results []structs.Activity + var count int64 + + if !isColumnNameValid(command.OrderBy) { + return nil, 0, errors.New("invalid column name") + } + + filteredSystemTaskTypes := []models.SystemTaskType{ + models.MAGIC_FILL, + models.SYSTEM_EMAIL_CONNECTIVITY_CHECK, + models.RECEIPT_PROCESSING_SETTINGS_CONNECTIVITY_CHECK, + models.META_ASSOCIATE_TASKS_TO_RECEIPT, + } + + query := db.Model(&models.SystemTask{}). + Distinct(). + Joins("LEFT JOIN users ON system_tasks.ran_by_user_id = users.id"). + Joins("LEFT JOIN group_members ON users.id = group_members.user_id"). + Joins("LEFT JOIN receipts ON system_tasks.associated_entity_type = 'RECEIPT' "+ + "AND system_tasks.associated_entity_id = receipts.id"). + Where("(group_members.group_id IN ?) OR receipts.group_id IN ?", command.GroupIds, command.GroupIds). + Where("system_tasks.type NOT IN ?", filteredSystemTaskTypes) + + query.Count(&count) + + query = repository.Sort(query, command.OrderBy, command.SortDirection) + query = query.Scopes(repository.Paginate(command.Page, command.PageSize)) + + query.Find(&results) + + return results, count, nil +} + func isColumnNameValid(columnName string) bool { return columnName == "type" || columnName == "status" || columnName == "associated_entity_type" || columnName == "associated_entity_id" || columnName == "started_at" || columnName == "ended_at" || columnName == "result_description" || columnName == "ran_by_user_id" } @@ -109,3 +148,15 @@ func (repository SystemTaskRepository) DeleteSystemTaskByAssociatedEntityId( return nil } + +func (repository SystemTaskRepository) GetSystemTaskById(id uint) (models.SystemTask, error) { + db := repository.GetDB() + var systemTask models.SystemTask + + err := db.Model(&models.SystemTask{}).Where("id = ?", id).First(&systemTask).Error + if err != nil { + return models.SystemTask{}, err + } + + return systemTask, nil +} diff --git a/internal/routers/system_task.go b/internal/routers/system_task.go index ba5c30f2..0741116d 100644 --- a/internal/routers/system_task.go +++ b/internal/routers/system_task.go @@ -13,6 +13,8 @@ func BuildSystemTaskRouter(tokenValidator *jwtmiddleware.JWTMiddleware) *chi.Mux systemTaskRouter.Use(middleware.MoveJWTCookieToHeader, tokenValidator.CheckJWT) systemTaskRouter.Post("/getPagedSystemTasks", handlers.GetSystemTasks) + systemTaskRouter.Post("/getPagedActivities", handlers.GetActivitiesForGroups) + systemTaskRouter.Post("/rerunActivity/{id}", handlers.RerunActivity) return systemTaskRouter } diff --git a/internal/services/groups.go b/internal/services/groups.go index ba836ecf..6cdf1348 100644 --- a/internal/services/groups.go +++ b/internal/services/groups.go @@ -47,7 +47,12 @@ func (service GroupService) GetGroupsForUser(userId string) ([]models.Group, err return nil, err } - err = db.Model(models.Group{}).Where("id IN ?", groupIds).Preload(clause.Associations).Order("is_all_group desc").Find(&groups).Error + err = db.Model(models.Group{}). + Where("id IN ?", groupIds). + Preload(clause.Associations). + Order("is_all_group desc"). + Find(&groups).Error + if err != nil { return nil, err } diff --git a/internal/services/receipts.go b/internal/services/receipts.go index e188216c..946ff824 100644 --- a/internal/services/receipts.go +++ b/internal/services/receipts.go @@ -134,7 +134,7 @@ func (service ReceiptService) QuickScan( groupIdString := utils.UintToString(groupId) now := time.Now() - receiptCommand, receiptProcessingMetadata, err := MagicFillFromImage(magicFillCommand, groupIdString) + receiptCommand, receiptProcessingMetadata, magicFillErr := MagicFillFromImage(magicFillCommand, groupIdString) finishedAt := time.Now() metaCombineSystemTask, err := systemTaskRepository.CreateSystemTask(commands.UpsertSystemTaskCommand{ @@ -161,8 +161,8 @@ func (service ReceiptService) QuickScan( return models.Receipt{}, taskErr } - if err != nil { - return models.Receipt{}, err + if magicFillErr != nil { + return models.Receipt{}, magicFillErr } if receiptCommand.PaidByUserID == 0 { @@ -181,7 +181,7 @@ func (service ReceiptService) QuickScan( systemTaskService.SetTransaction(tx) uploadStart := time.Now() - createdReceipt, err = receiptRepository.CreateReceipt(receiptCommand, token.UserId) + createdReceipt, err = receiptRepository.CreateReceipt(receiptCommand, token.UserId, false) uploadEnd := time.Now() _, taskErr := systemTaskService.CreateReceiptUploadedSystemTask( err, diff --git a/internal/structs/activity.go b/internal/structs/activity.go new file mode 100644 index 00000000..1bab6698 --- /dev/null +++ b/internal/structs/activity.go @@ -0,0 +1,17 @@ +package structs + +import ( + "receipt-wrangler/api/internal/models" + "time" +) + +type Activity struct { + Id uint `json:"id"` + Type models.SystemTaskType `json:"type"` + Status models.SystemTaskStatus `json:"status"` + StartedAt time.Time `json:"startedAt"` + EndedAt *time.Time `json:"endedAt"` + RanByUserId *uint `json:"ranByUserId"` + CanBeRestarted bool `json:"canBeRestarted"` + AssociatedSystemTaskId *uint `json:"-"` +} diff --git a/internal/wranglerasynq/email_process_handler.go b/internal/wranglerasynq/email_process_handler.go index 54ef3754..98ada97e 100644 --- a/internal/wranglerasynq/email_process_handler.go +++ b/internal/wranglerasynq/email_process_handler.go @@ -118,7 +118,7 @@ func HandleEmailProcessTask(context context.Context, task *asynq.Task) error { ) createReceiptStart := time.Now() - createdReceipt, err := receiptRepository.CreateReceipt(command, 0) + createdReceipt, err := receiptRepository.CreateReceipt(command, 0, false) _, taskErr := systemTaskService.CreateReceiptUploadedSystemTask( err, createdReceipt, diff --git a/internal/wranglerasynq/util.go b/internal/wranglerasynq/util.go new file mode 100644 index 00000000..4e29514e --- /dev/null +++ b/internal/wranglerasynq/util.go @@ -0,0 +1,42 @@ +package wranglerasynq + +import ( + "receipt-wrangler/api/internal/models" + "receipt-wrangler/api/internal/repositories" + "receipt-wrangler/api/internal/structs" +) + +func SetActivityCanBeRestarted(activities *[]structs.Activity) error { + inspector, err := GetAsynqInspector() + if err != nil { + return err + } + + archivedTasks, err := inspector.ListArchivedTasks(string(models.QuickScanQueue)) + if err != nil { + return err + + } + systemTaskRepository := repositories.NewSystemTaskRepository(nil) + + for i := range *activities { + activity := &(*activities)[i] + + if activity.Type == models.QUICK_SCAN && activity.AssociatedSystemTaskId != nil { + associatedSystemTask, err := systemTaskRepository.GetSystemTaskById(*activity.AssociatedSystemTaskId) + if err != nil { + return err + } + + for i := 0; i < len(archivedTasks); i++ { + task := archivedTasks[i] + if task.ID == associatedSystemTask.AsynqTaskId { + activity.CanBeRestarted = true + break + } + } + } + } + + return nil +} diff --git a/swagger.yml b/swagger.yml index 1658f63b..7eaf01e1 100644 --- a/swagger.yml +++ b/swagger.yml @@ -1807,6 +1807,60 @@ paths: $ref: "#/components/responses/Forbidden" security: - bearerAuth: [ ] + /systemTask/getPagedActivities: + post: + tags: + - SystemTask + summary: Gets paged activities + description: This will return paged activities for a list of groups + operationId: getPagedActivities + requestBody: + description: Paging and sorting data + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/PagedActivityRequestCommand" + responses: + 200: + description: Paged activities + content: + application/json: + schema: + $ref: "#/components/schemas/PagedData" + 500: + $ref: "#/components/responses/Internal" + 400: + $ref: "#/components/responses/BadRequest" + 403: + $ref: "#/components/responses/Forbidden" + security: + - bearerAuth: [ ] + /systemTask/rerunActivity/{id}: + post: + tags: + - SystemTask + summary: Attempts to rerun activity + description: This will rerun a failed activity + operationId: rerunActivity + parameters: + - in: path + name: id + schema: + type: integer + required: true + description: Id of activity to restart + responses: + 200: + $ref: "#/components/responses/Ok" + 500: + $ref: "#/components/responses/Internal" + 400: + $ref: "#/components/responses/BadRequest" + 403: + $ref: "#/components/responses/Forbidden" + security: + - bearerAuth: [ ] /prompt/: post: tags: @@ -2278,6 +2332,7 @@ components: enum: - "GROUP_SUMMARY" - "FILTERED_RECEIPTS" + - "GROUP_ACTIVITY" AiType: type: string enum: @@ -2309,6 +2364,7 @@ components: - "SYSTEM_EMAIL_CONNECTIVITY_CHECK" - "RECEIPT_PROCESSING_SETTINGS_CONNECTIVITY_CHECK" - "RECEIPT_UPLOADED" + - "RECEIPT_UPDATED" - "PROMPT_GENERATED" AssociatedEntityType: type: string @@ -2344,6 +2400,32 @@ components: - "email_polling" - "email_receipt_processing" - "email_receipt_image_cleanup" + Activity: + type: object + required: + - id + - type + - status + - startedAt + - endedAt + - ranByUserId + - childSystemTasks + properties: + id: + type: integer + type: + $ref: "#/components/schemas/SystemTaskType" + status: + $ref: "#/components/schemas/SystemTaskStatus" + startedAt: + type: string + endedAt: + type: string + ranByUserId: + type: integer + format: uint64 + canBeRestarted: + type: boolean BaseModel: type: object required: @@ -3107,6 +3189,15 @@ components: properties: filter: $ref: "#/components/schemas/GroupFilter" + PagedActivityRequestCommand: + allOf: + - $ref: "#/components/schemas/PagedRequestCommand" + - type: object + properties: + groupIds: + type: array + items: + type: integer GroupFilter: type: object properties: @@ -3203,6 +3294,7 @@ components: - $ref: "#/components/schemas/SystemTask" - $ref: "#/components/schemas/ReceiptProcessingSettings" - $ref: "#/components/schemas/SystemEmail" + - $ref: "#/components/schemas/Activity" totalCount: type: integer FeatureConfig: @@ -3768,7 +3860,6 @@ components: description: Comments associated to receipt items: $ref: "#/components/schemas/UpsertCommentCommand" - UpsertItemCommand: type: object required: