Files
Bernt (LandveX AI) 67a69ab073 feat(boc): v1.0 - Complete Business Operations Center
- Go backend API with full CRUD for all modules
- Rust analytics service with parallel processing
- C runtime with POSIX shared memory IPC
- PostgreSQL schema with 30+ tables
- Redis cache, Kafka event streaming
- WebSocket hub, automation engine
- PDF generation, Resend email integration
- JWT auth, multi-tenant
- Docker Compose deployment
- Nginx reverse proxy

Refs: BOC-001
2026-07-12 13:21:10 +00:00

171 lines
5.0 KiB
Go

package handlers
import (
"database/sql"
"encoding/json"
"net/http"
"time"
)
type ReceiptHandler struct {
DB *sql.DB
}
func NewReceiptHandler(db *sql.DB) *ReceiptHandler {
return &ReceiptHandler{DB: db}
}
type Receipt struct {
ID string `json:"id"`
EmployeeID string `json:"employee_id"`
ExpenseID *string `json:"expense_id"`
ImageURL string `json:"image_url"`
OCRText string `json:"ocr_text"`
OCRData map[string]interface{} `json:"ocr_data"`
OCRConfidence float64 `json:"ocr_confidence"`
Status string `json:"status"`
ProcessedAt *time.Time `json:"processed_at"`
CreatedAt time.Time `json:"created_at"`
}
func (h *ReceiptHandler) ListReceipts(w http.ResponseWriter, r *http.Request) {
status := r.URL.Query().Get("status")
if status == "" {
status = "all"
}
var query string
var args []interface{}
if status == "all" {
query = `SELECT id, employee_id, expense_id, image_url, ocr_text, ocr_data, ocr_confidence, status, processed_at, created_at FROM boc_receipts ORDER BY created_at DESC LIMIT 100`
} else {
query = `SELECT id, employee_id, expense_id, image_url, ocr_text, ocr_data, ocr_confidence, status, processed_at, created_at FROM boc_receipts WHERE status = $1 ORDER BY created_at DESC LIMIT 100`
args = append(args, status)
}
rows, err := h.DB.Query(query, args...)
if err != nil {
writeError(w, http.StatusInternalServerError, "database error")
return
}
defer rows.Close()
receipts := []Receipt{}
for rows.Next() {
var rc Receipt
var ocrData []byte
if err := rows.Scan(&rc.ID, &rc.EmployeeID, &rc.ExpenseID, &rc.ImageURL, &rc.OCRText, &ocrData, &rc.OCRConfidence, &rc.Status, &rc.ProcessedAt, &rc.CreatedAt); err != nil {
continue
}
json.Unmarshal(ocrData, &rc.OCRData)
receipts = append(receipts, rc)
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"receipts": receipts,
"total": len(receipts),
})
}
func (h *ReceiptHandler) UploadReceipt(w http.ResponseWriter, r *http.Request) {
var req struct {
EmployeeID string `json:"employee_id"`
ImageURL string `json:"image_url"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request")
return
}
var id string
err := h.DB.QueryRow(`
INSERT INTO boc_receipts (employee_id, image_url, status)
VALUES ($1, $2, 'pending')
RETURNING id
`, req.EmployeeID, req.ImageURL).Scan(&id)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to upload receipt")
return
}
// TODO: Trigger async OCR processing
go h.processOCR(id, req.ImageURL)
writeJSON(w, http.StatusCreated, map[string]interface{}{
"id": id,
"message": "Receipt uploaded, OCR processing started",
})
}
func (h *ReceiptHandler) processOCR(receiptID, imageURL string) {
// Placeholder for OCR processing
// In production, this would call an OCR service (AWS Textract, Google Vision, etc.)
// Simulate OCR processing
time.Sleep(2 * time.Second)
ocrData := map[string]interface{}{
"amount": 125.50,
"date": time.Now().Format("2006-01-02"),
"vendor": "Example Store",
"category": "Mat",
}
ocrJSON, _ := json.Marshal(ocrData)
h.DB.Exec(`
UPDATE boc_receipts
SET ocr_text = $1, ocr_data = $2, ocr_confidence = $3, status = 'processed', processed_at = NOW()
WHERE id = $4
`, "Example Store\nDate: 2026-07-12\nTotal: $125.50", ocrJSON, 0.95, receiptID)
}
func (h *ReceiptHandler) ApproveReceipt(w http.ResponseWriter, r *http.Request) {
var req struct {
ReceiptID string `json:"receipt_id"`
Amount float64 `json:"amount"`
Category string `json:"category"`
Description string `json:"description"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request")
return
}
tx, err := h.DB.Begin()
if err != nil {
writeError(w, http.StatusInternalServerError, "transaction error")
return
}
defer tx.Rollback()
// Create expense from receipt
var expenseID string
err = tx.QueryRow(`
INSERT INTO boc_expenses (category, description, amount, currency, status, receipt_url)
VALUES ($1, $2, $3, 'USD', 'pending', (SELECT image_url FROM boc_receipts WHERE id = $4))
RETURNING id
`, req.Category, req.Description, req.Amount, req.ReceiptID).Scan(&expenseID)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to create expense")
return
}
// Link receipt to expense
_, err = tx.Exec(`UPDATE boc_receipts SET expense_id = $1, status = 'approved' WHERE id = $2`, expenseID, req.ReceiptID)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to update receipt")
return
}
if err := tx.Commit(); err != nil {
writeError(w, http.StatusInternalServerError, "commit failed")
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"expense_id": expenseID,
"message": "Receipt approved and expense created",
})
}