package services import ( "context" "database/sql" "encoding/json" "errors" "fmt" "history-api/internal/dtos/request" "history-api/internal/dtos/response" "history-api/internal/gen/sqlc" "history-api/internal/models" "history-api/internal/repositories" "history-api/pkg/ai" "history-api/pkg/cache" "history-api/pkg/constants" "history-api/pkg/convert" "slices" "strconv" "github.com/gofiber/fiber/v3" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" "github.com/rs/zerolog/log" "golang.org/x/sync/errgroup" ) type SubmissionService interface { CreateSubmission(ctx context.Context, userID string, dto *request.CreateSubmissionDto) (*response.SubmissionResponse, *fiber.Error) UpdateSubmissionStatus(ctx context.Context, reviewerID string, submissionID string, dto *request.UpdateSubmissionStatusDto) (*response.SubmissionResponse, *fiber.Error) GetSubmissionByID(ctx context.Context, id string) (*response.SubmissionResponse, *fiber.Error) SearchSubmissions(ctx context.Context, dto *request.SearchSubmissionDto) (*response.PaginatedResponse, *fiber.Error) DeleteSubmission(ctx context.Context, userID string, id string, claims *response.JWTClaims) *fiber.Error } type submissionService struct { submissionRepo repositories.SubmissionRepository projectRepo repositories.ProjectRepository commitRepo repositories.CommitRepository userRepo repositories.UserRepository wikiRepo repositories.WikiRepository geometryRepo repositories.GeometryRepository entityRepo repositories.EntityRepository ragRepo repositories.RagRepository ragUtils *ai.RagUtils db *pgxpool.Pool c cache.Cache } func NewSubmissionService( submissionRepo repositories.SubmissionRepository, projectRepo repositories.ProjectRepository, commitRepo repositories.CommitRepository, userRepo repositories.UserRepository, wikiRepo repositories.WikiRepository, geometryRepo repositories.GeometryRepository, entityRepo repositories.EntityRepository, ragRepo repositories.RagRepository, ragUtils *ai.RagUtils, db *pgxpool.Pool, c cache.Cache, ) SubmissionService { return &submissionService{ submissionRepo: submissionRepo, projectRepo: projectRepo, commitRepo: commitRepo, userRepo: userRepo, wikiRepo: wikiRepo, geometryRepo: geometryRepo, entityRepo: entityRepo, ragRepo: ragRepo, ragUtils: ragUtils, db: db, c: c, } } func (s *submissionService) CreateSubmission(ctx context.Context, userID string, dto *request.CreateSubmissionDto) (*response.SubmissionResponse, *fiber.Error) { projectUUID, err := convert.StringToUUID(dto.ProjectID) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID") } commitUUID, err := convert.StringToUUID(dto.CommitID) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid commit ID") } userUUID, err := convert.StringToUUID(userID) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid user ID") } commit, err := s.commitRepo.GetByID(ctx, commitUUID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Commit not found") } if commit.ProjectID != dto.ProjectID { return nil, fiber.NewError(fiber.StatusBadRequest, "Commit does not belong to project") } var snapshotData request.CommitSnapshot if err := json.Unmarshal(commit.SnapshotJson, &snapshotData); err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to parse commit snapshot") } for _, entity := range snapshotData.Entities { if entity.Slug != nil { exist, err := s.entityRepo.GetBySlug(ctx, *entity.Slug) if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get entity") } if exist != nil { return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Entity %s already exists", *entity.Slug)) } } } for _, wiki := range snapshotData.Wikis { if wiki.Slug != nil { exist, err := s.wikiRepo.GetBySlug(ctx, *wiki.Slug) if err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get wiki") } if exist != nil { return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Wiki %s already exists", *wiki.Slug)) } } } project, err := s.projectRepo.GetByID(ctx, projectUUID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Project not found") } for _, sub := range project.Submissions { if sub.Status == constants.StatusTypePending { return nil, fiber.NewError(fiber.StatusConflict, "There is already a pending submission for this project") } } arg := sqlc.CreateSubmissionParams{ ProjectID: projectUUID, CommitID: commitUUID, UserID: userUUID, Status: constants.StatusTypePending.Int16(), Content: convert.StringToText(dto.Content), } submission, err := s.submissionRepo.Create(ctx, arg) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create submission") } return submission.ToResponse(), nil } func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewerID string, submissionID string, dto *request.UpdateSubmissionStatusDto) (*response.SubmissionResponse, *fiber.Error) { tx, err := s.db.Begin(ctx) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction") } defer tx.Rollback(ctx) submissionRepo := s.submissionRepo.WithTx(tx) commitRepo := s.commitRepo.WithTx(tx) entityRepo := s.entityRepo.WithTx(tx) geometryRepo := s.geometryRepo.WithTx(tx) wikiRepo := s.wikiRepo.WithTx(tx) submissionUUID, err := convert.StringToUUID(submissionID) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid submission ID") } reviewerUUID, err := convert.StringToUUID(reviewerID) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid reviewer ID") } status := constants.ParseStatusTypeText(dto.Status) if status == constants.StatusTypeUnknown { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid status") } submission, err := s.submissionRepo.GetByID(ctx, submissionUUID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Submission not found") } if submission.Status != constants.StatusTypePending { return nil, fiber.NewError(fiber.StatusBadRequest, "Submission already processed") } commitUUID, err := convert.StringToUUID(submission.CommitID) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid commit ID") } commit, err := s.commitRepo.GetByID(ctx, commitUUID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Commit not found") } if commit.ProjectID != submission.ProjectID { return nil, fiber.NewError(fiber.StatusBadRequest, "Commit does not belong to project") } listDeleteEntities := make([]pgtype.UUID, 0) listDeleteWikis := make([]pgtype.UUID, 0) listDeleteGeometries := make([]pgtype.UUID, 0) var snapshotData request.CommitSnapshot err = json.Unmarshal(commit.SnapshotJson, &snapshotData) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to parse commit snapshot") } if status == constants.StatusTypeApproved { projectUUID, err := convert.StringToUUID(commit.ProjectID) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID") } currentEntity, err := s.entityRepo.GetByProjectID(ctx, projectUUID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Entity not found") } currentGeometry, err := s.geometryRepo.GetByProjectID(ctx, projectUUID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Geometry not found") } currentWiki, err := s.wikiRepo.GetByProjectID(ctx, projectUUID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Wiki not found") } persistItemIDs := make(map[string]struct{}) for _, item := range snapshotData.Entities { persistItemIDs[item.ID] = struct{}{} } for _, item := range snapshotData.Geometries { persistItemIDs[item.ID] = struct{}{} } for _, item := range snapshotData.Wikis { persistItemIDs[item.ID] = struct{}{} } persistCurrentItemIDs := make(map[string]struct{}) for _, item := range currentEntity { persistCurrentItemIDs[item.ID] = struct{}{} } for _, item := range currentGeometry { persistCurrentItemIDs[item.ID] = struct{}{} } for _, item := range currentWiki { persistCurrentItemIDs[item.ID] = struct{}{} } for _, e := range currentEntity { if _, ok := persistItemIDs[e.ID]; !ok { itemUUID, err := convert.StringToUUID(e.ID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid entity ID") } listDeleteEntities = append(listDeleteEntities, itemUUID) delete(persistCurrentItemIDs, e.ID) } } for _, g := range currentGeometry { if _, ok := persistItemIDs[g.ID]; !ok { itemUUID, err := convert.StringToUUID(g.ID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid geometry ID") } listDeleteGeometries = append(listDeleteGeometries, itemUUID) delete(persistCurrentItemIDs, g.ID) } } for _, w := range currentWiki { if _, ok := persistItemIDs[w.ID]; !ok { itemUUID, err := convert.StringToUUID(w.ID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid wiki ID") } listDeleteWikis = append(listDeleteWikis, itemUUID) delete(persistCurrentItemIDs, w.ID) } } if len(listDeleteEntities) > 0 { if err = entityRepo.DeleteByIDs(ctx, listDeleteEntities); err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete entities") } } if len(listDeleteGeometries) > 0 { if err = geometryRepo.DeleteByIDs(ctx, listDeleteGeometries); err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete geometries") } } if len(listDeleteWikis) > 0 { if err = wikiRepo.DeleteByIDs(ctx, listDeleteWikis); err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete wikis") } } refEntityIDs := []string{} for _, e := range snapshotData.Entities { if e.Source == "ref" { refEntityIDs = append(refEntityIDs, e.ID) } } refEntities, _ := s.entityRepo.GetByIDs(ctx, refEntityIDs) refEntityMap := make(map[string]bool) for _, e := range refEntities { refEntityMap[e.ID] = true } newEntities := make([]*request.EntitySnapshot, 0, len(snapshotData.Entities)) for i, entity := range snapshotData.Entities { if entity.Operation == "delete" { continue } entityUUID, err := convert.StringToUUID(entity.ID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid entity ID") } if _, ok := persistCurrentItemIDs[entity.ID]; ok { _, err := entityRepo.Update(ctx, sqlc.UpdateEntityParams{ Name: convert.StringToText(entity.Name), Description: convert.StringToText(entity.Description), Slug: convert.PtrToText(entity.Slug), Status: convert.PtrToInt2(entity.Status), TimeStart: convert.PtrFloat64ToInt4(entity.TimeStart), TimeEnd: convert.PtrFloat64ToInt4(entity.TimeEnd), ID: entityUUID, }) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update entity: "+err.Error()) } newEntities = append(newEntities, snapshotData.Entities[i]) } else if entity.Source == "inline" { _, err := entityRepo.Create(ctx, sqlc.CreateEntityParams{ ID: entityUUID, Name: entity.Name, Description: convert.StringToText(entity.Description), ProjectID: projectUUID, Slug: convert.PtrToText(entity.Slug), Status: convert.PtrToInt2(entity.Status), TimeStart: convert.PtrFloat64ToInt4(entity.TimeStart), TimeEnd: convert.PtrFloat64ToInt4(entity.TimeEnd), }) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create entity: "+err.Error()) } newEntities = append(newEntities, snapshotData.Entities[i]) } else if entity.Source == "ref" { if !refEntityMap[entity.ID] { continue } newEntities = append(newEntities, snapshotData.Entities[i]) } } snapshotData.Entities = newEntities refGeometryIDs := []string{} for _, g := range snapshotData.Geometries { if g.Source == "ref" { refGeometryIDs = append(refGeometryIDs, g.ID) } } refGeometries, _ := s.geometryRepo.GetByIDs(ctx, refGeometryIDs) refGeometryMap := make(map[string]bool) for _, g := range refGeometries { refGeometryMap[g.ID] = true } newGeometries := make([]*request.GeometrySnapshot, 0, len(snapshotData.Geometries)) for i, geo := range snapshotData.Geometries { if geo.Operation == "delete" { continue } geometryUUID, err := convert.StringToUUID(geo.ID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid geometry ID") } binding, _ := json.Marshal(geo.Binding) geoTypeCode := int16(0) if geo.Type != "" { if n, err := strconv.ParseInt(geo.Type, 10, 16); err == nil { geoTypeCode = int16(n) } } if _, ok := persistCurrentItemIDs[geo.ID]; ok { params := sqlc.UpdateGeometryParams{ ID: geometryUUID, GeoType: pgtype.Int2{Int16: geoTypeCode, Valid: true}, DrawGeometry: geo.DrawGeometry, Binding: binding, TimeStart: convert.PtrFloat64ToInt4(geo.TimeStart), TimeEnd: convert.PtrFloat64ToInt4(geo.TimeEnd), ProjectID: projectUUID, } if geo.BBox != nil { params.UpdateBbox = pgtype.Bool{Bool: true, Valid: true} params.MinLng = pgtype.Float8{Float64: geo.BBox.MinLng, Valid: true} params.MinLat = pgtype.Float8{Float64: geo.BBox.MinLat, Valid: true} params.MaxLng = pgtype.Float8{Float64: geo.BBox.MaxLng, Valid: true} params.MaxLat = pgtype.Float8{Float64: geo.BBox.MaxLat, Valid: true} } _, err := geometryRepo.Update(ctx, params) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update geometry: "+err.Error()) } newGeometries = append(newGeometries, snapshotData.Geometries[i]) } else if geo.Source == "inline" { params := sqlc.CreateGeometryParams{ ID: geometryUUID, GeoType: geoTypeCode, DrawGeometry: geo.DrawGeometry, Binding: binding, TimeStart: convert.PtrFloat64ToInt4(geo.TimeStart), TimeEnd: convert.PtrFloat64ToInt4(geo.TimeEnd), ProjectID: projectUUID, } if geo.BBox != nil { params.MinLng = geo.BBox.MinLng params.MinLat = geo.BBox.MinLat params.MaxLng = geo.BBox.MaxLng params.MaxLat = geo.BBox.MaxLat } _, err := geometryRepo.Create(ctx, params) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create geometry: "+err.Error()) } newGeometries = append(newGeometries, snapshotData.Geometries[i]) } else if geo.Source == "ref" { if !refGeometryMap[geo.ID] { continue } newGeometries = append(newGeometries, snapshotData.Geometries[i]) } } snapshotData.Geometries = newGeometries refWikiIDs := []string{} for _, w := range snapshotData.Wikis { if w.Source == "ref" { refWikiIDs = append(refWikiIDs, w.ID) } } refWikis, _ := s.wikiRepo.GetByIDs(ctx, refWikiIDs) refWikiMap := make(map[string]bool) for _, w := range refWikis { refWikiMap[w.ID] = true } newWikis := make([]*request.WikiSnapshot, 0, len(snapshotData.Wikis)) for i, wiki := range snapshotData.Wikis { if wiki.Operation == "delete" { continue } wikiUUID, err := convert.StringToUUID(wiki.ID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid wiki ID") } if _, ok := persistCurrentItemIDs[wiki.ID]; ok { _, err := wikiRepo.Update(ctx, sqlc.UpdateWikiParams{ ID: wikiUUID, Title: convert.StringToText(wiki.Title), Slug: convert.PtrToText(wiki.Slug), Content: convert.StringToText(wiki.Doc), ProjectID: projectUUID, }) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update wiki: "+err.Error()) } newWikis = append(newWikis, snapshotData.Wikis[i]) } else if wiki.Source == "inline" { _, err := wikiRepo.Create(ctx, sqlc.CreateWikiParams{ ID: wikiUUID, Title: convert.StringToText(wiki.Title), Slug: convert.PtrToText(wiki.Slug), Content: convert.StringToText(wiki.Doc), ProjectID: projectUUID, }) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki: "+err.Error()) } newWikis = append(newWikis, snapshotData.Wikis[i]) } else if wiki.Source == "ref" { if !refWikiMap[wiki.ID] { continue } newWikis = append(newWikis, snapshotData.Wikis[i]) } } snapshotData.Wikis = newWikis err = geometryRepo.DeleteEntityGeometriesByProjectID(ctx, projectUUID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete geometry entity: "+err.Error()) } err = wikiRepo.DeleteEntityWikisByProjectID(ctx, projectUUID) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete wiki entity: "+err.Error()) } validEntities := make(map[string]bool) for _, e := range snapshotData.Entities { validEntities[e.ID] = true } validGeometries := make(map[string]bool) for _, g := range snapshotData.Geometries { validGeometries[g.ID] = true } validWikis := make(map[string]bool) for _, w := range snapshotData.Wikis { validWikis[w.ID] = true } if len(snapshotData.GeometryEntity) > 0 { geomLinks := make(map[string][]pgtype.UUID) for _, link := range snapshotData.GeometryEntity { if !validEntities[link.EntityID] || !validGeometries[link.GeometryID] { continue } gID, _ := convert.StringToUUID(link.GeometryID) geomLinks[link.EntityID] = append(geomLinks[link.EntityID], gID) } for eIDStr, gIDs := range geomLinks { eID, _ := convert.StringToUUID(eIDStr) err = geometryRepo.CreateEntityGeometries(ctx, sqlc.CreateEntityGeometriesParams{ EntityID: eID, GeometryIds: gIDs, ProjectID: projectUUID, }) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create geometry entity: "+err.Error()) } } } if len(snapshotData.EntityWiki) > 0 { wikiLinks := make(map[string][]pgtype.UUID) for _, link := range snapshotData.EntityWiki { if link.Operation == "delete" || (link.IsDeleted != nil && *link.IsDeleted == 1) { continue } if !validEntities[link.EntityID] || !validWikis[link.WikiID] { continue } wID, _ := convert.StringToUUID(link.WikiID) wikiLinks[link.EntityID] = append(wikiLinks[link.EntityID], wID) } for eIDStr, wIDs := range wikiLinks { eID, _ := convert.StringToUUID(eIDStr) err = wikiRepo.CreateEntityWikis(ctx, sqlc.CreateEntityWikisParams{ EntityID: eID, WikiIds: wIDs, ProjectID: projectUUID, }) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki entity: "+err.Error()) } } } newSnapshot, err := json.Marshal(snapshotData) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to marshal snapshot") } _, err = commitRepo.UpdateSnapshot(ctx, commitUUID, newSnapshot) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update snapshot: "+err.Error()) } } if status == constants.StatusTypeApproved { wikiDeleteIDs := make([]string, 0) entityDeleteIDs := make([]string, 0) for _, id := range listDeleteWikis { wikiDeleteIDs = append(wikiDeleteIDs, convert.UUIDToString(id)) } for _, id := range listDeleteEntities { entityDeleteIDs = append(entityDeleteIDs, convert.UUIDToString(id)) } for _, wiki := range snapshotData.Wikis { if wiki.Operation == "delete" { wikiDeleteIDs = append(wikiDeleteIDs, wiki.ID) } } for _, entity := range snapshotData.Entities { if entity.Operation == "delete" { entityDeleteIDs = append(entityDeleteIDs, entity.ID) } } ragTask := models.RagIndexTask{ ProjectID: commit.ProjectID, DeleteWikiIDs: wikiDeleteIDs, DeleteEntityIDs: entityDeleteIDs, } for _, wiki := range snapshotData.Wikis { ragTask.Wikis = append(ragTask.Wikis, &models.RagWikiItem{ ID: wiki.ID, Title: wiki.Title, Doc: wiki.Doc, Source: wiki.Source, }) } for _, entity := range snapshotData.Entities { ragTask.Entities = append(ragTask.Entities, &models.RagEntityItem{ ID: entity.ID, Name: entity.Name, Description: entity.Description, Source: entity.Source, }) } if err := s.c.PublishTask(ctx, constants.StreamRagName, constants.TaskTypeRagIndexSubmission, ragTask); err != nil { log.Error().Err(err).Str("project_id", commit.ProjectID).Msg("Failed to publish RAG index task") } } arg := sqlc.UpdateSubmissionParams{ ID: submissionUUID, Status: pgtype.Int2{Int16: status.Int16(), Valid: true}, ReviewedBy: reviewerUUID, ReviewNote: convert.StringToText(dto.ReviewNote), } updatedSubmission, err := submissionRepo.Update(ctx, arg) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update submission status: "+err.Error()) } err = tx.Commit(ctx) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction: "+err.Error()) } if status == constants.StatusTypeApproved { go func() { bgCtx := context.Background() _ = s.c.DelByPattern(bgCtx, "entity:search*") _ = s.c.DelByPattern(bgCtx, "geometry:search*") _ = s.c.DelByPattern(bgCtx, "geometry:search:entity*") _ = s.c.DelByPattern(bgCtx, "wiki:search*") }() } _ = s.c.Del(ctx, fmt.Sprintf("project:id:%s", submission.ProjectID)) return updatedSubmission.ToResponse(), nil } func (m *submissionService) fillSearchArgs(arg *sqlc.SearchSubmissionsParams, dto *request.SearchSubmissionDto) { if dto.Sort != "" { arg.Sort = pgtype.Text{String: dto.Sort, Valid: true} } else { arg.Sort = pgtype.Text{String: "id", Valid: true} } arg.Order = pgtype.Text{String: "asc", Valid: true} if dto.Order == "desc" { arg.Order = pgtype.Text{String: "desc", Valid: true} } if len(dto.Statuses) > 0 { for _, id := range dto.Statuses { if u := constants.ParseStatusTypeText(id); u != constants.StatusTypeUnknown { arg.Statuses = append(arg.Statuses, u.Int16()) } } } if len(dto.UserIDs) > 0 { for _, id := range dto.UserIDs { if u, err := convert.StringToUUID(id); err == nil { arg.UserIds = append(arg.UserIds, u) } } } if dto.ReviewedBy != nil { if rvID, err := convert.StringToUUID(*dto.ReviewedBy); err == nil { arg.ReviewedBy = rvID } } if dto.CreatedFrom != nil { arg.CreatedFrom = pgtype.Timestamptz{Time: *dto.CreatedFrom, Valid: true} } if dto.CreatedTo != nil { arg.CreatedTo = pgtype.Timestamptz{Time: *dto.CreatedTo, Valid: true} } if dto.Search != "" { arg.SearchText = pgtype.Text{String: dto.Search, Valid: true} } } func (s *submissionService) GetSubmissionByID(ctx context.Context, id string) (*response.SubmissionResponse, *fiber.Error) { submissionUUID, err := convert.StringToUUID(id) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid submission ID") } submission, err := s.submissionRepo.GetByID(ctx, submissionUUID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Submission not found") } return submission.ToResponse(), nil } func (s *submissionService) SearchSubmissions(ctx context.Context, dto *request.SearchSubmissionDto) (*response.PaginatedResponse, *fiber.Error) { if dto.Page < 1 { dto.Page = 1 } if dto.Limit == 0 { dto.Limit = 20 } offset := (dto.Page - 1) * dto.Limit arg := sqlc.SearchSubmissionsParams{ Limit: int32(dto.Limit), Offset: int32(offset), } s.fillSearchArgs(&arg, dto) var rows []*models.SubmissionEntity var totalRecords int64 g, gCtx := errgroup.WithContext(ctx) g.Go(func() error { var err error rows, err = s.submissionRepo.Search(gCtx, arg) return err }) g.Go(func() error { countArg := sqlc.CountSubmissionsParams{ UserIds: arg.UserIds, Statuses: arg.Statuses, ReviewedBy: arg.ReviewedBy, CreatedFrom: arg.CreatedFrom, CreatedTo: arg.CreatedTo, SearchText: arg.SearchText, } var err error totalRecords, err = s.submissionRepo.Count(gCtx, countArg) return err }) if err := g.Wait(); err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to search submissions") } submissions := models.SubmissionsEntityToResponse(rows) return response.BuildPaginatedResponse(submissions, totalRecords, dto.Page, dto.Limit), nil } func (s *submissionService) DeleteSubmission(ctx context.Context, userID string, id string, claims *response.JWTClaims) *fiber.Error { submissionUUID, err := convert.StringToUUID(id) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Invalid submission ID") } submission, err := s.submissionRepo.GetByID(ctx, submissionUUID) if err != nil { return fiber.NewError(fiber.StatusNotFound, "Submission not found") } shoudDelete := false if slices.Contains(claims.Roles, constants.RoleTypeAdmin) || slices.Contains(claims.Roles, constants.RoleTypeMod) { shoudDelete = true } if submission.UserID == claims.UId && submission.Status == constants.StatusTypePending { shoudDelete = true } if !shoudDelete { return fiber.NewError(fiber.StatusForbidden, "You don't have permission to delete this submission") } err = s.submissionRepo.Delete(ctx, submissionUUID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete submission") } _ = s.c.Del(ctx, fmt.Sprintf("project:id:%s", submission.ProjectID)) return nil }