Files
food-ai/backend/internal/domain/recognition/product_job.go
dbastrikin c7317c4335 feat: async product/receipt recognition via Kafka
Backend:
- Migration 002: product_recognition_jobs table with JSONB images column
  and job_type CHECK ('receipt' | 'products')
- New Kafka topics: ai.products.paid / ai.products.free
- ProductJob model, ProductJobRepository (mirrors dish job pattern)
- itemEnricher extracted from Handler — shared by HTTP handler and worker
- ProductSSEBroker: PG LISTEN on product_job_update channel
- ProductWorkerPool: 5 workers, branches on job_type to call
  RecognizeReceipt or RecognizeProducts per image in parallel
- Handler: RecognizeReceipt and RecognizeProducts now return 202 Accepted
  instead of blocking; 4 new endpoints: GET /ai/product-jobs,
  /product-jobs/history, /product-jobs/{id}, /product-jobs/{id}/stream
- cmd/worker: extended to run ProductWorkerPool alongside dish WorkerPool
- cmd/server: wires productJobRepository + productSSEBroker; both SSE
  brokers started in App.Start()

Flutter client:
- ProductJobCreated, ProductJobResult, ProductJobSummary, ProductJobEvent
  models + submitReceiptRecognition/submitProductsRecognition/stream methods
- Shared _openSseStream helper eliminates duplicate SSE parsing loop
- ScanScreen: replace blocking AI calls with async submit + navigate to
  ProductJobWatchScreen
- ProductJobWatchScreen: watches SSE stream, navigates to /scan/confirm
  when done, shows error on failure
- ProductsScreen: prepends _RecentScansSection (hidden when empty); compact
  horizontal list of recent scans with "See all" → history
- ProductJobHistoryScreen: full list of all product recognition jobs
- New routes: /scan/product-job-watch, /products/job-history
- L10n: 7 new keys in all 12 ARB files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 23:01:30 +02:00

64 lines
1.9 KiB
Go

package recognition
import (
"time"
"github.com/food-ai/backend/internal/adapters/ai"
)
// Kafka topic names for product recognition.
const (
ProductTopicPaid = "ai.products.paid"
ProductTopicFree = "ai.products.free"
)
// ProductImagePayload is a single image stored in the product_recognition_jobs.images JSONB column.
type ProductImagePayload struct {
ImageBase64 string `json:"image_base64"`
MimeType string `json:"mime_type"`
}
// ProductJobResultItem is an enriched product item stored in the result JSONB.
type ProductJobResultItem struct {
Name string `json:"name"`
Quantity float64 `json:"quantity"`
Unit string `json:"unit"`
Category string `json:"category"`
Confidence float64 `json:"confidence"`
MappingID *string `json:"mapping_id,omitempty"`
StorageDays int `json:"storage_days"`
}
// ProductJobResult is the JSONB payload stored in product_recognition_jobs.result.
type ProductJobResult struct {
JobType string `json:"job_type"`
Items []ProductJobResultItem `json:"items"`
Unrecognized []ai.UnrecognizedItem `json:"unrecognized,omitempty"`
}
// ProductJob represents an async product/receipt recognition task.
type ProductJob struct {
ID string
UserID string
UserPlan string
JobType string // "receipt" | "products"
Images []ProductImagePayload
Lang string
Status string
Result *ProductJobResult
Error *string
CreatedAt time.Time
StartedAt *time.Time
CompletedAt *time.Time
}
// ProductJobSummary is a lightweight record for list endpoints (omits image payloads).
type ProductJobSummary struct {
ID string `json:"id"`
JobType string `json:"job_type"`
Status string `json:"status"`
Result *ProductJobResult `json:"result,omitempty"`
Error *string `json:"error,omitempty"`
CreatedAt time.Time `json:"created_at"`
}