78b57273e2
- Add password hashing with bcrypt - Add AuthService with proper login - Add password strength validation - Add RBAC middleware (AdminOnly, ManagerOrAdmin) - Add tenant isolation middleware - Update CRM handler with tenant filtering - Add JWT fallback for development mode - Add user context helpers - Build successful
191 lines
5.6 KiB
Go
191 lines
5.6 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// AIAccountingHandler hanterar AI-driven bokföringsförslag
|
|
type AIAccountingHandler struct{}
|
|
|
|
func NewAIAccountingHandler() *AIAccountingHandler {
|
|
return &AIAccountingHandler{}
|
|
}
|
|
|
|
// AISuggestion representerar ett AI-förslag
|
|
type AISuggestion struct {
|
|
ID string `json:"id"`
|
|
Description string `json:"description"`
|
|
SuggestedAccount string `json:"suggested_account"`
|
|
AccountName string `json:"account_name"`
|
|
Confidence float64 `json:"confidence"`
|
|
Debit float64 `json:"debit"`
|
|
Credit float64 `json:"credit"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
// GetSuggestions returnerar AI-förslag för en transaktion
|
|
func (h *AIAccountingHandler) GetSuggestions(w http.ResponseWriter, r *http.Request) {
|
|
var req struct {
|
|
Description string `json:"description"`
|
|
Amount float64 `json:"amount"`
|
|
Counterparty string `json:"counterparty"`
|
|
}
|
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
|
|
"ok": false,
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
suggestions := h.analyzeTransaction(req.Description, req.Amount, req.Counterparty)
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"ok": true,
|
|
"suggestions": suggestions,
|
|
"input": req,
|
|
})
|
|
}
|
|
|
|
// analyzeTransaction analyserar en transaktion och ger förslag
|
|
func (h *AIAccountingHandler) analyzeTransaction(description string, amount float64, counterparty string) []AISuggestion {
|
|
desc := strings.ToLower(description)
|
|
counter := strings.ToLower(counterparty)
|
|
var suggestions []AISuggestion
|
|
|
|
// Regelbaserad AI (kan bytas mot ML-modell)
|
|
switch {
|
|
case containsAny(desc, []string{"faktura", "inbetalning", "betalning"}) && amount > 0:
|
|
suggestions = append(suggestions, AISuggestion{
|
|
ID: "ai-001",
|
|
Description: description,
|
|
SuggestedAccount: "1510",
|
|
AccountName: "Kundfordringar",
|
|
Confidence: 0.92,
|
|
Debit: amount,
|
|
Credit: 0,
|
|
Reason: "Positivt belopp med fakturareferens = kundfordran",
|
|
})
|
|
suggestions = append(suggestions, AISuggestion{
|
|
ID: "ai-002",
|
|
Description: description,
|
|
SuggestedAccount: "3010",
|
|
AccountName: "Försäljning tjänster",
|
|
Confidence: 0.88,
|
|
Debit: 0,
|
|
Credit: amount,
|
|
Reason: "Motkonto till kundfordran = försäljning",
|
|
})
|
|
|
|
case containsAny(desc, []string{"lön", "salary", "löneutbetalning"}):
|
|
suggestions = append(suggestions, AISuggestion{
|
|
ID: "ai-003",
|
|
Description: description,
|
|
SuggestedAccount: "7210",
|
|
AccountName: "Löner",
|
|
Confidence: 0.95,
|
|
Debit: amount,
|
|
Credit: 0,
|
|
Reason: "Löneutbetalning = lönekonto",
|
|
})
|
|
suggestions = append(suggestions, AISuggestion{
|
|
ID: "ai-004",
|
|
Description: description,
|
|
SuggestedAccount: "1930",
|
|
AccountName: "Företagskonto",
|
|
Confidence: 0.95,
|
|
Debit: 0,
|
|
Credit: amount,
|
|
Reason: "Lön betalas från företagskonto",
|
|
})
|
|
|
|
case containsAny(desc, []string{"aws", "hosting", "server", "cloud"}):
|
|
suggestions = append(suggestions, AISuggestion{
|
|
ID: "ai-005",
|
|
Description: description,
|
|
SuggestedAccount: "6540",
|
|
AccountName: "IT-kostnader",
|
|
Confidence: 0.89,
|
|
Debit: amount,
|
|
Credit: 0,
|
|
Reason: "AWS/hosting = IT-kostnader",
|
|
})
|
|
|
|
case containsAny(desc, []string{"zoomer", "quixzoom", "fältarbetare"}):
|
|
suggestions = append(suggestions, AISuggestion{
|
|
ID: "ai-006",
|
|
Description: description,
|
|
SuggestedAccount: "7690",
|
|
AccountName: "Övriga personalkostnader",
|
|
Confidence: 0.87,
|
|
Debit: amount,
|
|
Credit: 0,
|
|
Reason: "Zoomer-utbetalning = personalkostnad",
|
|
})
|
|
|
|
case containsAny(desc, []string{"försäkring", "insurance"}):
|
|
suggestions = append(suggestions, AISuggestion{
|
|
ID: "ai-007",
|
|
Description: description,
|
|
SuggestedAccount: "6310",
|
|
AccountName: "Försäkringspremier",
|
|
Confidence: 0.91,
|
|
Debit: amount,
|
|
Credit: 0,
|
|
Reason: "Försäkringsbetalning = försäkringspremie",
|
|
})
|
|
|
|
case containsAny(counter, []string{"skatteverket", "skatt"}):
|
|
suggestions = append(suggestions, AISuggestion{
|
|
ID: "ai-008",
|
|
Description: description,
|
|
SuggestedAccount: "2012",
|
|
AccountName: "Skatter",
|
|
Confidence: 0.94,
|
|
Debit: amount,
|
|
Credit: 0,
|
|
Reason: "Skatteverket = skattebetalning",
|
|
})
|
|
|
|
default:
|
|
// Generiskt förslag baserat på belopp
|
|
if amount > 0 {
|
|
suggestions = append(suggestions, AISuggestion{
|
|
ID: "ai-099",
|
|
Description: description,
|
|
SuggestedAccount: "1930",
|
|
AccountName: "Företagskonto",
|
|
Confidence: 0.45,
|
|
Debit: 0,
|
|
Credit: amount,
|
|
Reason: "Kunde inte identifiera — granska manuellt",
|
|
})
|
|
} else {
|
|
suggestions = append(suggestions, AISuggestion{
|
|
ID: "ai-099",
|
|
Description: description,
|
|
SuggestedAccount: "6991",
|
|
AccountName: "Övriga externa kostnader",
|
|
Confidence: 0.45,
|
|
Debit: -amount,
|
|
Credit: 0,
|
|
Reason: "Kunde inte identifiera — granska manuellt",
|
|
})
|
|
}
|
|
}
|
|
|
|
return suggestions
|
|
}
|
|
|
|
func containsAny(s string, substrs []string) bool {
|
|
for _, substr := range substrs {
|
|
if strings.Contains(s, substr) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|