From 09a35c0b995579aeb207f2a6510cb39b3f5c3fe6 Mon Sep 17 00:00:00 2001 From: Trey t Date: Wed, 18 Feb 2026 21:37:38 -0600 Subject: [PATCH] Align document handlers/repo with contract updates --- docs/openapi.yaml | 28 +++ internal/handlers/document_handler.go | 13 +- internal/handlers/document_handler_test.go | 17 +- .../integration/kmp_model_contract_test.go | 210 +++++++++--------- internal/repositories/document_repo.go | 19 +- 5 files changed, 156 insertions(+), 131 deletions(-) diff --git a/docs/openapi.yaml b/docs/openapi.yaml index a3ee33e..2e9907e 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1571,6 +1571,34 @@ paths: summary: List all documents accessible to the user security: - tokenAuth: [] + parameters: + - in: query + name: residence + description: Filter by residence ID + schema: + type: integer + format: uint + - in: query + name: document_type + description: Filter by document type + schema: + type: string + - in: query + name: is_active + description: Filter by active/inactive status (default true) + schema: + type: boolean + - in: query + name: expiring_soon + description: Return warranties expiring in N days + schema: + type: integer + minimum: 1 + - in: query + name: search + description: Case-insensitive search over title/description + schema: + type: string responses: '200': description: List of documents diff --git a/internal/handlers/document_handler.go b/internal/handlers/document_handler.go index d3f9c4e..10b8176 100644 --- a/internal/handlers/document_handler.go +++ b/internal/handlers/document_handler.go @@ -36,16 +36,13 @@ func NewDocumentHandler(documentService *services.DocumentService, storageServic func (h *DocumentHandler) ListDocuments(c echo.Context) error { user := c.Get(middleware.AuthUserKey).(*models.User) - // Build filter from query params (matching KMP DocumentApi parameters) + // Build filter from supported query params. var filter *repositories.DocumentFilter if c.QueryParam("residence") != "" || c.QueryParam("document_type") != "" || - c.QueryParam("category") != "" || c.QueryParam("contractor") != "" || c.QueryParam("is_active") != "" || c.QueryParam("expiring_soon") != "" || - c.QueryParam("tags") != "" || c.QueryParam("search") != "" { + c.QueryParam("search") != "" { filter = &repositories.DocumentFilter{ DocumentType: c.QueryParam("document_type"), - Category: c.QueryParam("category"), - Tags: c.QueryParam("tags"), Search: c.QueryParam("search"), } if rid := c.QueryParam("residence"); rid != "" { @@ -54,12 +51,6 @@ func (h *DocumentHandler) ListDocuments(c echo.Context) error { filter.ResidenceID = &residenceID } } - if cid := c.QueryParam("contractor"); cid != "" { - if parsed, err := strconv.ParseUint(cid, 10, 32); err == nil { - contractorID := uint(parsed) - filter.ContractorID = &contractorID - } - } if ia := c.QueryParam("is_active"); ia != "" { isActive := ia == "true" || ia == "1" filter.IsActive = &isActive diff --git a/internal/handlers/document_handler_test.go b/internal/handlers/document_handler_test.go index 1f0279b..76054e5 100644 --- a/internal/handlers/document_handler_test.go +++ b/internal/handlers/document_handler_test.go @@ -31,7 +31,9 @@ func TestDocumentHandler_ListDocuments(t *testing.T) { handler, e, db := setupDocumentHandler(t) user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "password") residence := testutil.CreateTestResidence(t, db, user.ID, "Test House") - testutil.CreateTestDocument(t, db, residence.ID, user.ID, "Test Doc") + activeDoc := testutil.CreateTestDocument(t, db, residence.ID, user.ID, "Test Doc") + inactiveDoc := testutil.CreateTestDocument(t, db, residence.ID, user.ID, "Old Doc") + require.NoError(t, db.Model(&inactiveDoc).Update("is_active", false).Error) authGroup := e.Group("/api/documents") authGroup.Use(testutil.MockAuthMiddleware(user)) @@ -45,7 +47,18 @@ func TestDocumentHandler_ListDocuments(t *testing.T) { err := json.Unmarshal(w.Body.Bytes(), &response) require.NoError(t, err) assert.Len(t, response, 1) - assert.Equal(t, "Test Doc", response[0]["title"]) + assert.Equal(t, activeDoc.Title, response[0]["title"]) + }) + + t.Run("can list inactive documents when requested", func(t *testing.T) { + w := testutil.MakeRequest(e, "GET", "/api/documents/?is_active=false", nil, "test-token") + testutil.AssertStatusCode(t, w, http.StatusOK) + + var response []map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err) + assert.Len(t, response, 1) + assert.Equal(t, inactiveDoc.Title, response[0]["title"]) }) } diff --git a/internal/integration/kmp_model_contract_test.go b/internal/integration/kmp_model_contract_test.go index 926610e..628ee8b 100644 --- a/internal/integration/kmp_model_contract_test.go +++ b/internal/integration/kmp_model_contract_test.go @@ -162,9 +162,9 @@ type classMapping struct { var schemaToKMPClass = map[string]classMapping{ // Auth - "LoginRequest": {kmpClassName: "LoginRequest"}, - "RegisterRequest": {kmpClassName: "RegisterRequest"}, - "ForgotPasswordRequest": {kmpClassName: "ForgotPasswordRequest"}, + "LoginRequest": {kmpClassName: "LoginRequest"}, + "RegisterRequest": {kmpClassName: "RegisterRequest"}, + "ForgotPasswordRequest": {kmpClassName: "ForgotPasswordRequest"}, "VerifyResetCodeRequest": {kmpClassName: "VerifyResetCodeRequest"}, "ResetPasswordRequest": {kmpClassName: "ResetPasswordRequest"}, "UpdateProfileRequest": {kmpClassName: "UpdateProfileRequest"}, @@ -180,41 +180,41 @@ var schemaToKMPClass = map[string]classMapping{ "VerifyResetCodeResponse": {kmpClassName: "VerifyResetCodeResponse"}, // Lookups - "ResidenceTypeResponse": {kmpClassName: "ResidenceType"}, - "TaskCategoryResponse": {kmpClassName: "TaskCategory"}, - "TaskPriorityResponse": {kmpClassName: "TaskPriority"}, - "TaskFrequencyResponse": {kmpClassName: "TaskFrequency"}, - "ContractorSpecialtyResponse": {kmpClassName: "ContractorSpecialty"}, - "SeededDataResponse": {kmpClassName: "SeededDataResponse"}, - "TaskTemplateResponse": {kmpClassName: "TaskTemplate"}, - "TaskTemplateCategoryGroup": {kmpClassName: "TaskTemplateCategoryGroup"}, + "ResidenceTypeResponse": {kmpClassName: "ResidenceType"}, + "TaskCategoryResponse": {kmpClassName: "TaskCategory"}, + "TaskPriorityResponse": {kmpClassName: "TaskPriority"}, + "TaskFrequencyResponse": {kmpClassName: "TaskFrequency"}, + "ContractorSpecialtyResponse": {kmpClassName: "ContractorSpecialty"}, + "SeededDataResponse": {kmpClassName: "SeededDataResponse"}, + "TaskTemplateResponse": {kmpClassName: "TaskTemplate"}, + "TaskTemplateCategoryGroup": {kmpClassName: "TaskTemplateCategoryGroup"}, "TaskTemplatesGroupedResponse": {kmpClassName: "TaskTemplatesGroupedResponse"}, // Residence - "CreateResidenceRequest": {kmpClassName: "ResidenceCreateRequest"}, - "UpdateResidenceRequest": {kmpClassName: "ResidenceUpdateRequest"}, - "JoinWithCodeRequest": {kmpClassName: "JoinResidenceRequest"}, - "GenerateShareCodeRequest": {kmpClassName: "GenerateShareCodeRequest"}, - "ResidenceUserResponse": {kmpClassName: "ResidenceUserResponse"}, - "ResidenceResponse": {kmpClassName: "ResidenceResponse"}, - "TotalSummary": {kmpClassName: "TotalSummary"}, - "MyResidencesResponse": {kmpClassName: "MyResidencesResponse"}, - "ShareCodeResponse": {kmpClassName: "ShareCodeResponse"}, - "JoinResidenceResponse": {kmpClassName: "JoinResidenceResponse"}, + "CreateResidenceRequest": {kmpClassName: "ResidenceCreateRequest"}, + "UpdateResidenceRequest": {kmpClassName: "ResidenceUpdateRequest"}, + "JoinWithCodeRequest": {kmpClassName: "JoinResidenceRequest"}, + "GenerateShareCodeRequest": {kmpClassName: "GenerateShareCodeRequest"}, + "ResidenceUserResponse": {kmpClassName: "ResidenceUserResponse"}, + "ResidenceResponse": {kmpClassName: "ResidenceResponse"}, + "TotalSummary": {kmpClassName: "TotalSummary"}, + "MyResidencesResponse": {kmpClassName: "MyResidencesResponse"}, + "ShareCodeResponse": {kmpClassName: "ShareCodeResponse"}, + "JoinResidenceResponse": {kmpClassName: "JoinResidenceResponse"}, "GenerateShareCodeResponse": {kmpClassName: "GenerateShareCodeResponse"}, // Task - "CreateTaskRequest": {kmpClassName: "TaskCreateRequest"}, - "UpdateTaskRequest": {kmpClassName: "TaskUpdateRequest"}, - "TaskUserResponse": {kmpClassName: "TaskUserResponse"}, - "TaskResponse": {kmpClassName: "TaskResponse"}, + "CreateTaskRequest": {kmpClassName: "TaskCreateRequest"}, + "UpdateTaskRequest": {kmpClassName: "TaskUpdateRequest"}, + "TaskUserResponse": {kmpClassName: "TaskUserResponse"}, + "TaskResponse": {kmpClassName: "TaskResponse"}, "KanbanColumnResponse": {kmpClassName: "TaskColumn"}, "KanbanBoardResponse": {kmpClassName: "TaskColumnsResponse"}, // Task Completion - "CreateTaskCompletionRequest": {kmpClassName: "TaskCompletionCreateRequest"}, - "TaskCompletionImageResponse": {kmpClassName: "TaskCompletionImage"}, - "TaskCompletionResponse": {kmpClassName: "TaskCompletionResponse"}, + "CreateTaskCompletionRequest": {kmpClassName: "TaskCompletionCreateRequest"}, + "TaskCompletionImageResponse": {kmpClassName: "TaskCompletionImage"}, + "TaskCompletionResponse": {kmpClassName: "TaskCompletionResponse"}, // Contractor "CreateContractorRequest": {kmpClassName: "ContractorCreateRequest"}, @@ -228,15 +228,16 @@ var schemaToKMPClass = map[string]classMapping{ "DocumentResponse": {kmpClassName: "Document"}, // Notification - "RegisterDeviceRequest": {kmpClassName: "DeviceRegistrationRequest"}, - "DeviceResponse": {kmpClassName: "DeviceRegistrationResponse"}, - "NotificationPreference": {kmpClassName: "NotificationPreference"}, - "UpdatePreferencesRequest": {kmpClassName: "UpdateNotificationPreferencesRequest"}, - "Notification": {kmpClassName: "Notification"}, - "NotificationListResponse": {kmpClassName: "NotificationListResponse"}, + "RegisterDeviceRequest": {kmpClassName: "DeviceRegistrationRequest"}, + "DeviceResponse": {kmpClassName: "DeviceRegistrationResponse"}, + "NotificationPreference": {kmpClassName: "NotificationPreference"}, + "UpdatePreferencesRequest": {kmpClassName: "UpdateNotificationPreferencesRequest"}, + "Notification": {kmpClassName: "Notification"}, + "NotificationListResponse": {kmpClassName: "NotificationListResponse"}, // Subscription "SubscriptionStatusResponse": {kmpClassName: "SubscriptionStatus"}, + "SubscriptionResponse": {kmpClassName: "VerificationSubscriptionInfo"}, "UsageResponse": {kmpClassName: "UsageStats"}, "TierLimitsClientResponse": {kmpClassName: "TierLimits"}, "FeatureBenefit": {kmpClassName: "FeatureBenefit"}, @@ -249,43 +250,42 @@ var schemaToKMPClass = map[string]classMapping{ // excludedSchemas are spec schemas intentionally not mapped to KMP classes. var excludedSchemas = map[string]string{ - "TaskWithSummaryResponse": "KMP uses generic WithSummaryResponse", - "DeleteWithSummaryResponse": "KMP uses generic WithSummaryResponse", - "ResidenceWithSummaryResponse": "KMP uses generic WithSummaryResponse", + "TaskWithSummaryResponse": "KMP uses generic WithSummaryResponse", + "DeleteWithSummaryResponse": "KMP uses generic WithSummaryResponse", + "ResidenceWithSummaryResponse": "KMP uses generic WithSummaryResponse", "ResidenceDeleteWithSummaryResponse": "KMP uses generic WithSummaryResponse", - "TaskCompletionWithSummaryResponse": "KMP uses generic WithSummaryResponse", - "ProcessPurchaseRequest": "KMP splits into platform-specific requests", - "CurrentUserResponse": "KMP unifies into User class", - "DocumentType": "Enum — KMP uses DocumentType enum class", - "NotificationType": "Enum — KMP uses String", - "ToggleFavoriteResponse": "Simple message+bool, not worth a dedicated mapping", - "SharePackageResponse": "Used for .casera file sharing, handled by SharedContractor", - "UnregisterDeviceRequest": "Simple oneoff request", - "UpdateTaskCompletionRequest": "Not yet used in KMP", - "SubscriptionResponse": "Inline in purchase/restore handler — KMP maps via VerificationResponse.subscription", - "UpgradeTriggerResponse": "Shape differs from KMP UpgradeTriggerData", - "UploadResult": "Handled inline in upload response parsing", + "TaskCompletionWithSummaryResponse": "KMP uses generic WithSummaryResponse", + "ProcessPurchaseRequest": "KMP splits into platform-specific requests", + "CurrentUserResponse": "KMP unifies into User class", + "DocumentType": "Enum — KMP uses DocumentType enum class", + "NotificationType": "Enum — KMP uses String", + "ToggleFavoriteResponse": "Simple message+bool, not worth a dedicated mapping", + "SharePackageResponse": "Used for .casera file sharing, handled by SharedContractor", + "UnregisterDeviceRequest": "Simple oneoff request", + "UpdateTaskCompletionRequest": "Not yet used in KMP", + "UpgradeTriggerResponse": "Shape differs from KMP UpgradeTriggerData", + "UploadResult": "Handled inline in upload response parsing", } // knownTypeOverrides documents intentional type differences. var knownTypeOverrides = map[string]string{ // Spec says string (decimal), KMP uses Double for form binding - "TaskResponse.estimated_cost": "KMP uses Double for numeric form binding", - "TaskResponse.actual_cost": "KMP uses Double for numeric form binding", - "CreateTaskRequest.estimated_cost": "KMP uses Double for numeric form binding", - "UpdateTaskRequest.estimated_cost": "KMP uses Double for numeric form binding", - "UpdateTaskRequest.actual_cost": "KMP uses Double for numeric form binding", - "ResidenceResponse.bathrooms": "KMP uses Double for numeric form binding", - "ResidenceResponse.lot_size": "KMP uses Double for numeric form binding", - "ResidenceResponse.purchase_price": "KMP uses Double for numeric form binding", - "CreateResidenceRequest.bathrooms": "KMP uses Double for numeric form binding", - "CreateResidenceRequest.lot_size": "KMP uses Double for numeric form binding", - "CreateResidenceRequest.purchase_price": "KMP uses Double for numeric form binding", - "UpdateResidenceRequest.bathrooms": "KMP uses Double for numeric form binding", - "UpdateResidenceRequest.lot_size": "KMP uses Double for numeric form binding", - "UpdateResidenceRequest.purchase_price": "KMP uses Double for numeric form binding", + "TaskResponse.estimated_cost": "KMP uses Double for numeric form binding", + "TaskResponse.actual_cost": "KMP uses Double for numeric form binding", + "CreateTaskRequest.estimated_cost": "KMP uses Double for numeric form binding", + "UpdateTaskRequest.estimated_cost": "KMP uses Double for numeric form binding", + "UpdateTaskRequest.actual_cost": "KMP uses Double for numeric form binding", + "ResidenceResponse.bathrooms": "KMP uses Double for numeric form binding", + "ResidenceResponse.lot_size": "KMP uses Double for numeric form binding", + "ResidenceResponse.purchase_price": "KMP uses Double for numeric form binding", + "CreateResidenceRequest.bathrooms": "KMP uses Double for numeric form binding", + "CreateResidenceRequest.lot_size": "KMP uses Double for numeric form binding", + "CreateResidenceRequest.purchase_price": "KMP uses Double for numeric form binding", + "UpdateResidenceRequest.bathrooms": "KMP uses Double for numeric form binding", + "UpdateResidenceRequest.lot_size": "KMP uses Double for numeric form binding", + "UpdateResidenceRequest.purchase_price": "KMP uses Double for numeric form binding", "CreateTaskCompletionRequest.actual_cost": "KMP uses Double for numeric form binding", - "TaskCompletionResponse.actual_cost": "KMP uses Double for numeric form binding", + "TaskCompletionResponse.actual_cost": "KMP uses Double for numeric form binding", // Spec says nullable Boolean, KMP uses non-nullable Boolean (defaults to false) "CreateContractorRequest.is_favorite": "KMP defaults is_favorite to false, not nullable", @@ -308,50 +308,50 @@ var knownTypeOverrides = map[string]string{ // knownMissingFromKMP: spec fields intentionally absent from KMP. var knownMissingFromKMP = map[string]string{ - "ErrorResponse.details": "KMP uses 'errors' field with different type", - "TaskTemplateResponse.created_at": "KMP doesn't use template timestamps", - "TaskTemplateResponse.updated_at": "KMP doesn't use template timestamps", - "Notification.user_id": "KMP doesn't need user_id on notifications", - "Notification.error_message": "KMP doesn't surface notification error messages", - "Notification.updated_at": "KMP doesn't use notification updated_at", - "NotificationPreference.id": "KMP doesn't need preference record ID", - "NotificationPreference.user_id": "KMP doesn't need user_id on preferences", - "FeatureBenefit.id": "KMP doesn't use benefit record ID", - "FeatureBenefit.display_order": "KMP doesn't use benefit display order", - "FeatureBenefit.is_active": "KMP doesn't filter by active status", - "Promotion.id": "KMP uses promotion_id string instead", - "Promotion.start_date": "KMP doesn't filter by promotion dates", - "Promotion.end_date": "KMP doesn't filter by promotion dates", - "Promotion.target_tier": "KMP doesn't filter by target tier", - "Promotion.is_active": "KMP doesn't filter by active status", - "LoginRequest.email": "Spec allows email login, KMP only sends username", + "ErrorResponse.details": "KMP uses 'errors' field with different type", + "TaskTemplateResponse.created_at": "KMP doesn't use template timestamps", + "TaskTemplateResponse.updated_at": "KMP doesn't use template timestamps", + "Notification.user_id": "KMP doesn't need user_id on notifications", + "Notification.error_message": "KMP doesn't surface notification error messages", + "Notification.updated_at": "KMP doesn't use notification updated_at", + "NotificationPreference.id": "KMP doesn't need preference record ID", + "NotificationPreference.user_id": "KMP doesn't need user_id on preferences", + "FeatureBenefit.id": "KMP doesn't use benefit record ID", + "FeatureBenefit.display_order": "KMP doesn't use benefit display order", + "FeatureBenefit.is_active": "KMP doesn't filter by active status", + "Promotion.id": "KMP uses promotion_id string instead", + "Promotion.start_date": "KMP doesn't filter by promotion dates", + "Promotion.end_date": "KMP doesn't filter by promotion dates", + "Promotion.target_tier": "KMP doesn't filter by target tier", + "Promotion.is_active": "KMP doesn't filter by active status", + "LoginRequest.email": "Spec allows email login, KMP only sends username", // Document create/update file fields — KMP handles file upload via multipart, not JSON - "CreateDocumentRequest.file_name": "KMP handles file upload via multipart, not JSON fields", - "CreateDocumentRequest.file_size": "KMP handles file upload via multipart, not JSON fields", - "CreateDocumentRequest.mime_type": "KMP handles file upload via multipart, not JSON fields", - "CreateDocumentRequest.file_url": "KMP handles file upload via multipart, not JSON fields", - "UpdateDocumentRequest.file_name": "KMP handles file upload via multipart, not JSON fields", - "UpdateDocumentRequest.file_size": "KMP handles file upload via multipart, not JSON fields", - "UpdateDocumentRequest.mime_type": "KMP handles file upload via multipart, not JSON fields", - "UpdateDocumentRequest.file_url": "KMP handles file upload via multipart, not JSON fields", + "CreateDocumentRequest.file_name": "KMP handles file upload via multipart, not JSON fields", + "CreateDocumentRequest.file_size": "KMP handles file upload via multipart, not JSON fields", + "CreateDocumentRequest.mime_type": "KMP handles file upload via multipart, not JSON fields", + "CreateDocumentRequest.file_url": "KMP handles file upload via multipart, not JSON fields", + "UpdateDocumentRequest.file_name": "KMP handles file upload via multipart, not JSON fields", + "UpdateDocumentRequest.file_size": "KMP handles file upload via multipart, not JSON fields", + "UpdateDocumentRequest.mime_type": "KMP handles file upload via multipart, not JSON fields", + "UpdateDocumentRequest.file_url": "KMP handles file upload via multipart, not JSON fields", } // knownExtraInKMP: KMP fields not in the spec (client-side additions). var knownExtraInKMP = map[string]string{ // Document client-side fields - "DocumentResponse.category": "Client-side field for UI grouping", - "DocumentResponse.tags": "Client-side field", - "DocumentResponse.notes": "Client-side field", - "DocumentResponse.item_name": "Client-side warranty field", - "DocumentResponse.provider": "Client-side warranty field", - "DocumentResponse.provider_contact": "Client-side warranty field", - "DocumentResponse.claim_phone": "Client-side warranty field", - "DocumentResponse.claim_email": "Client-side warranty field", - "DocumentResponse.claim_website": "Client-side warranty field", - "DocumentResponse.start_date": "Client-side warranty field", + "DocumentResponse.category": "Client-side field for UI grouping", + "DocumentResponse.tags": "Client-side field", + "DocumentResponse.notes": "Client-side field", + "DocumentResponse.item_name": "Client-side warranty field", + "DocumentResponse.provider": "Client-side warranty field", + "DocumentResponse.provider_contact": "Client-side warranty field", + "DocumentResponse.claim_phone": "Client-side warranty field", + "DocumentResponse.claim_email": "Client-side warranty field", + "DocumentResponse.claim_website": "Client-side warranty field", + "DocumentResponse.start_date": "Client-side warranty field", "DocumentResponse.days_until_expiration": "Client-side computed field", - "DocumentResponse.warranty_status": "Client-side computed field", + "DocumentResponse.warranty_status": "Client-side computed field", // DocumentImage extra fields "DocumentImageResponse.uploaded_at": "KMP has uploaded_at for display", @@ -387,11 +387,11 @@ type specSchema struct { } type specField struct { - typeName string - format string - nullable bool - isRef bool - isArray bool + typeName string + format string + nullable bool + isRef bool + isArray bool hasAdditionalProperties bool } diff --git a/internal/repositories/document_repo.go b/internal/repositories/document_repo.go index 32156a4..419acaa 100644 --- a/internal/repositories/document_repo.go +++ b/internal/repositories/document_repo.go @@ -12,11 +12,8 @@ import ( type DocumentFilter struct { ResidenceID *uint DocumentType string - Category string - ContractorID *uint IsActive *bool ExpiringSoon *int - Tags string Search string } @@ -72,18 +69,17 @@ func (r *DocumentRepository) FindByUserFiltered(residenceIDs []uint, filter *Doc query := r.db.Preload("CreatedBy"). Preload("Residence"). Preload("Images"). - Where("residence_id IN ? AND is_active = ?", residenceIDs, true) + Where("residence_id IN ?", residenceIDs) + + // Default behavior is active-only unless explicitly overridden. + if filter == nil || filter.IsActive == nil { + query = query.Where("is_active = ?", true) + } if filter != nil { if filter.DocumentType != "" { query = query.Where("document_type = ?", filter.DocumentType) } - if filter.Category != "" { - query = query.Where("category = ?", filter.Category) - } - if filter.ContractorID != nil { - query = query.Where("contractor_id = ?", *filter.ContractorID) - } if filter.IsActive != nil { query = query.Where("is_active = ?", *filter.IsActive) } @@ -92,9 +88,6 @@ func (r *DocumentRepository) FindByUserFiltered(residenceIDs []uint, filter *Doc threshold := now.AddDate(0, 0, *filter.ExpiringSoon) query = query.Where("expiry_date IS NOT NULL AND expiry_date > ? AND expiry_date <= ?", now, threshold) } - if filter.Tags != "" { - query = query.Where("tags LIKE ?", "%"+filter.Tags+"%") - } if filter.Search != "" { searchPattern := "%" + filter.Search + "%" query = query.Where("(title ILIKE ? OR description ILIKE ?)", searchPattern, searchPattern)