171 lines
5.0 KiB
Go
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",
|
||
|
|
})
|
||
|
|
}
|