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
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SupplierHandler struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
func NewSupplierHandler(db *sql.DB) *SupplierHandler {
|
||||
return &SupplierHandler{DB: db}
|
||||
}
|
||||
|
||||
type Supplier struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Phone string `json:"phone"`
|
||||
OrgNumber string `json:"org_number"`
|
||||
Address map[string]interface{} `json:"address"`
|
||||
PaymentTerms string `json:"payment_terms"`
|
||||
BankAccount string `json:"bank_account"`
|
||||
Bankgiro string `json:"bankgiro"`
|
||||
Postgiro string `json:"postgiro"`
|
||||
Currency string `json:"currency"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type PurchaseOrder struct {
|
||||
ID string `json:"id"`
|
||||
SupplierID string `json:"supplier_id"`
|
||||
PONumber string `json:"po_number"`
|
||||
Status string `json:"status"`
|
||||
Amount float64 `json:"amount"`
|
||||
TaxAmount float64 `json:"tax_amount"`
|
||||
Currency string `json:"currency"`
|
||||
ExpectedDelivery *time.Time `json:"expected_delivery"`
|
||||
ReceivedAt *time.Time `json:"received_at"`
|
||||
Notes string `json:"notes"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type SupplierInvoice struct {
|
||||
ID string `json:"id"`
|
||||
SupplierID string `json:"supplier_id"`
|
||||
POID *string `json:"po_id"`
|
||||
InvoiceNumber string `json:"invoice_number"`
|
||||
Amount float64 `json:"amount"`
|
||||
TaxAmount float64 `json:"tax_amount"`
|
||||
Currency string `json:"currency"`
|
||||
Status string `json:"status"`
|
||||
DueDate *time.Time `json:"due_date"`
|
||||
PaidAt *time.Time `json:"paid_at"`
|
||||
OCRNumber string `json:"ocr_number"`
|
||||
Notes string `json:"notes"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (h *SupplierHandler) ListSuppliers(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := h.DB.Query(`
|
||||
SELECT id, name, email, phone, org_number, address, payment_terms, bank_account, bankgiro, postgiro, currency, status, created_at
|
||||
FROM boc_suppliers WHERE status = 'active' ORDER BY name
|
||||
`)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
suppliers := []Supplier{}
|
||||
for rows.Next() {
|
||||
var s Supplier
|
||||
var addr []byte
|
||||
if err := rows.Scan(&s.ID, &s.Name, &s.Email, &s.Phone, &s.OrgNumber, &addr, &s.PaymentTerms, &s.BankAccount, &s.Bankgiro, &s.Postgiro, &s.Currency, &s.Status, &s.CreatedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
json.Unmarshal(addr, &s.Address)
|
||||
suppliers = append(suppliers, s)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"suppliers": suppliers,
|
||||
"total": len(suppliers),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *SupplierHandler) CreateSupplier(w http.ResponseWriter, r *http.Request) {
|
||||
var req Supplier
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
addr, _ := json.Marshal(req.Address)
|
||||
|
||||
var id string
|
||||
err := h.DB.QueryRow(`
|
||||
INSERT INTO boc_suppliers (name, email, phone, org_number, address, payment_terms, bank_account, bankgiro, postgiro, currency)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING id
|
||||
`, req.Name, req.Email, req.Phone, req.OrgNumber, addr, req.PaymentTerms, req.BankAccount, req.Bankgiro, req.Postgiro, req.Currency).Scan(&id)
|
||||
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to create supplier")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"id": id,
|
||||
"message": "Supplier created",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *SupplierHandler) ListPurchaseOrders(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, supplier_id, po_number, status, amount, currency, expected_delivery, received_at, created_at FROM boc_purchase_orders ORDER BY created_at DESC LIMIT 100`
|
||||
} else {
|
||||
query = `SELECT id, supplier_id, po_number, status, amount, currency, expected_delivery, received_at, created_at FROM boc_purchase_orders 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()
|
||||
|
||||
pos := []PurchaseOrder{}
|
||||
for rows.Next() {
|
||||
var p PurchaseOrder
|
||||
if err := rows.Scan(&p.ID, &p.SupplierID, &p.PONumber, &p.Status, &p.Amount, &p.Currency, &p.ExpectedDelivery, &p.ReceivedAt, &p.CreatedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
pos = append(pos, p)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"purchase_orders": pos,
|
||||
"total": len(pos),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *SupplierHandler) CreatePurchaseOrder(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
SupplierID string `json:"supplier_id"`
|
||||
ExpectedDelivery *time.Time `json:"expected_delivery"`
|
||||
Notes string `json:"notes"`
|
||||
Items []struct {
|
||||
ProductID string `json:"product_id"`
|
||||
Description string `json:"description"`
|
||||
Quantity float64 `json:"quantity"`
|
||||
UnitPrice float64 `json:"unit_price"`
|
||||
TaxRate float64 `json:"tax_rate"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
poNumber := "PO-" + time.Now().Format("20060102-150405")
|
||||
|
||||
var totalAmount, totalTax float64
|
||||
for _, item := range req.Items {
|
||||
itemTotal := item.Quantity * item.UnitPrice
|
||||
itemTax := itemTotal * (item.TaxRate / 100)
|
||||
totalAmount += itemTotal
|
||||
totalTax += itemTax
|
||||
}
|
||||
|
||||
tx, err := h.DB.Begin()
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "transaction error")
|
||||
return
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
var id string
|
||||
err = tx.QueryRow(`
|
||||
INSERT INTO boc_purchase_orders (supplier_id, po_number, amount, tax_amount, currency, expected_delivery, notes)
|
||||
VALUES ($1, $2, $3, $4, 'USD', $5, $6)
|
||||
RETURNING id
|
||||
`, req.SupplierID, poNumber, totalAmount, totalTax, req.ExpectedDelivery, req.Notes).Scan(&id)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to create PO")
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range req.Items {
|
||||
itemTotal := item.Quantity * item.UnitPrice
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO boc_purchase_order_items (po_id, product_id, description, quantity, unit_price, tax_rate, total)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`, id, item.ProductID, item.Description, item.Quantity, item.UnitPrice, item.TaxRate, itemTotal)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to create PO items")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "commit failed")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"id": id,
|
||||
"number": poNumber,
|
||||
"message": "Purchase order created",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *SupplierHandler) ListSupplierInvoices(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, supplier_id, po_id, invoice_number, amount, currency, status, due_date, paid_at, ocr_number, created_at FROM boc_supplier_invoices ORDER BY created_at DESC LIMIT 100`
|
||||
} else {
|
||||
query = `SELECT id, supplier_id, po_id, invoice_number, amount, currency, status, due_date, paid_at, ocr_number, created_at FROM boc_supplier_invoices 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()
|
||||
|
||||
invoices := []SupplierInvoice{}
|
||||
for rows.Next() {
|
||||
var i SupplierInvoice
|
||||
if err := rows.Scan(&i.ID, &i.SupplierID, &i.POID, &i.InvoiceNumber, &i.Amount, &i.Currency, &i.Status, &i.DueDate, &i.PaidAt, &i.OCRNumber, &i.CreatedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
invoices = append(invoices, i)
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"invoices": invoices,
|
||||
"total": len(invoices),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *SupplierHandler) CreateSupplierInvoice(w http.ResponseWriter, r *http.Request) {
|
||||
var req SupplierInvoice
|
||||
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_supplier_invoices (supplier_id, po_id, invoice_number, amount, tax_amount, currency, due_date, ocr_number, notes)
|
||||
VALUES ($1, $2, $3, $4, $5, 'USD', $6, $7, $8)
|
||||
RETURNING id
|
||||
`, req.SupplierID, req.POID, req.InvoiceNumber, req.Amount, req.TaxAmount, req.DueDate, req.OCRNumber, req.Notes).Scan(&id)
|
||||
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "failed to create supplier invoice")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"id": id,
|
||||
"message": "Supplier invoice created",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user