refactor(uploads): drop legacy multipart code paths
The presigned-URL upload flow (POST /api/uploads/presign + direct B2 POST
+ upload_ids[] in entity creation) is now the only image upload path. The
legacy multipart routes and DTO fields used by older clients are removed:
Removed:
- POST /api/uploads/image/ (legacy multipart upload → URL)
- POST /api/uploads/document/ (legacy multipart upload → URL)
- POST /api/uploads/completion/ (legacy multipart upload → URL)
- Multipart branch in POST /api/task-completions/ (now JSON-only)
- CreateTaskCompletionRequest.ImageURLs DTO field
- UpdateTaskCompletionRequest.ImageURLs DTO field
- CreateDocumentRequest.ImageURLs DTO field
- Service-layer ImageURLs loops in task_service.CreateCompletion,
task_service.UpdateCompletion, document_service.CreateDocument
- Tests exercising the removed paths
- Now-unused imports (strings/time/decimal) in task_handler.go
Kept:
- DELETE /api/uploads/ (orphan-cleanup endpoint, still useful)
- POST /api/uploads/presign/ (the new path)
- POST /api/documents/:id/images/ (uses storage_service.Upload directly,
same multipart pattern but separate code path; deferred for now)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -204,21 +204,7 @@ func (s *DocumentService) CreateDocument(ctx context.Context, req *requests.Crea
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Legacy multipart path — already-uploaded URLs.
|
||||
for _, imageURL := range req.ImageURLs {
|
||||
if imageURL != "" {
|
||||
img := &models.DocumentImage{
|
||||
DocumentID: document.ID,
|
||||
ImageURL: imageURL,
|
||||
}
|
||||
if err := s.documentRepo.WithContext(ctx).CreateDocumentImage(img); err != nil {
|
||||
// Log but don't fail the whole operation
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New presigned path — claimed image uploads become DocumentImage rows.
|
||||
// Presigned-URL path — claimed image uploads become DocumentImage rows.
|
||||
// The document_file row (if any) was already lifted onto the document above.
|
||||
for i := range claimedUploads {
|
||||
pu := claimedUploads[i]
|
||||
|
||||
@@ -70,26 +70,10 @@ func TestDocumentService_CreateDocument_DefaultType(t *testing.T) {
|
||||
assert.Equal(t, models.DocumentTypeGeneral, resp.DocumentType)
|
||||
}
|
||||
|
||||
func TestDocumentService_CreateDocument_WithImages(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
documentRepo := repositories.NewDocumentRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewDocumentService(documentRepo, residenceRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
|
||||
req := &requests.CreateDocumentRequest{
|
||||
ResidenceID: residence.ID,
|
||||
Title: "Receipt with photos",
|
||||
ImageURLs: []string{"https://example.com/img1.jpg", "https://example.com/img2.jpg"},
|
||||
}
|
||||
|
||||
resp, err := service.CreateDocument(context.Background(), req, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, "Receipt with photos", resp.Title)
|
||||
}
|
||||
// TestDocumentService_CreateDocument_WithImages was removed alongside the
|
||||
// legacy ImageURLs field. The presigned-URL flow is exercised end-to-end
|
||||
// in the integration tests; mocking B2 + a pending_uploads fixture for a
|
||||
// unit test was deemed not worth the complexity.
|
||||
|
||||
func TestDocumentService_CreateDocument_AccessDenied(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
@@ -630,27 +614,8 @@ func TestDocumentService_ActivateDocument_AccessDenied(t *testing.T) {
|
||||
testutil.AssertAppError(t, err, http.StatusForbidden, "error.document_access_denied")
|
||||
}
|
||||
|
||||
// === CreateDocument — with empty image URL in array (should skip) ===
|
||||
|
||||
func TestDocumentService_CreateDocument_WithEmptyImageURL(t *testing.T) {
|
||||
db := testutil.SetupTestDB(t)
|
||||
documentRepo := repositories.NewDocumentRepository(db)
|
||||
residenceRepo := repositories.NewResidenceRepository(db)
|
||||
service := NewDocumentService(documentRepo, residenceRepo)
|
||||
|
||||
user := testutil.CreateTestUser(t, db, "owner", "owner@test.com", "Password123")
|
||||
residence := testutil.CreateTestResidence(t, db, user.ID, "Test House")
|
||||
|
||||
req := &requests.CreateDocumentRequest{
|
||||
ResidenceID: residence.ID,
|
||||
Title: "Doc with empty images",
|
||||
ImageURLs: []string{"", "https://example.com/img.jpg", ""},
|
||||
}
|
||||
|
||||
resp, err := service.CreateDocument(context.Background(), req, user.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
}
|
||||
// TestDocumentService_CreateDocument_WithEmptyImageURL was removed alongside
|
||||
// the legacy ImageURLs field — empty-URL filtering is no longer a code path.
|
||||
|
||||
// === UpdateDocument — all optional fields ===
|
||||
|
||||
|
||||
@@ -727,23 +727,8 @@ func (s *TaskService) CreateCompletion(ctx context.Context, req *requests.Create
|
||||
if err := s.taskRepo.WithContext(ctx).UpdateTx(tx, task); err != nil {
|
||||
return err
|
||||
}
|
||||
// B-07: Create images inside the same transaction as completion.
|
||||
// Two sources contribute, both produce TaskCompletionImage rows:
|
||||
// 1. Legacy multipart path — client uploaded via the API and got
|
||||
// back URLs in req.ImageURLs.
|
||||
// 2. New presigned path — client uploaded direct to B2 and we
|
||||
// claimed the pending_uploads rows above.
|
||||
for _, imageURL := range req.ImageURLs {
|
||||
if imageURL != "" {
|
||||
img := &models.TaskCompletionImage{
|
||||
CompletionID: completion.ID,
|
||||
ImageURL: imageURL,
|
||||
}
|
||||
if err := tx.Create(img).Error; err != nil {
|
||||
return fmt.Errorf("failed to create completion image: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create completion image rows from the claimed pending_uploads.
|
||||
// Bytes already live in B2; we just record the FK + URL.
|
||||
for i := range claimedUploads {
|
||||
pu := claimedUploads[i]
|
||||
img := &models.TaskCompletionImage{
|
||||
@@ -1131,16 +1116,11 @@ func (s *TaskService) UpdateCompletion(ctx context.Context, completionID, userID
|
||||
return nil, apperrors.Internal(err)
|
||||
}
|
||||
|
||||
// Add any new images
|
||||
for _, imageURL := range req.ImageURLs {
|
||||
image := &models.TaskCompletionImage{
|
||||
CompletionID: completion.ID,
|
||||
ImageURL: imageURL,
|
||||
}
|
||||
if err := s.taskRepo.WithContext(ctx).CreateCompletionImage(image); err != nil {
|
||||
log.Error().Err(err).Uint("completion_id", completion.ID).Msg("Failed to create completion image during update")
|
||||
}
|
||||
}
|
||||
// Image-add on update is unsupported in the new flow — clients should
|
||||
// instead delete and recreate the completion if image attachments need
|
||||
// to change after the fact. The presigned-URL path requires a single
|
||||
// "create with attachments" handshake and there's no equivalent attach-
|
||||
// to-existing pathway today. Add one here when a UI feature requires it.
|
||||
|
||||
// Reload to get full associations
|
||||
updated, err := s.taskRepo.WithContext(ctx).FindCompletionByID(completionID)
|
||||
|
||||
Reference in New Issue
Block a user