294 lines
7.4 KiB
Go
294 lines
7.4 KiB
Go
|
|
package handlers
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"os"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// VismaHandler hanterar riktig Visma-integration
|
||
|
|
type VismaHandler struct {
|
||
|
|
clientID string
|
||
|
|
clientSecret string
|
||
|
|
redirectURI string
|
||
|
|
accessToken string
|
||
|
|
refreshToken string
|
||
|
|
tokenExpiry time.Time
|
||
|
|
client *http.Client
|
||
|
|
}
|
||
|
|
|
||
|
|
// NewVismaHandler skapar en ny handler
|
||
|
|
func NewVismaHandler() *VismaHandler {
|
||
|
|
return &VismaHandler{
|
||
|
|
clientID: os.Getenv("VISMA_CLIENT_ID"),
|
||
|
|
clientSecret: os.Getenv("VISMA_CLIENT_SECRET"),
|
||
|
|
redirectURI: os.Getenv("VISMA_REDIRECT_URI"),
|
||
|
|
client: &http.Client{Timeout: 30 * time.Second},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// IsConfigured returnerar true om Visma är konfigurerat
|
||
|
|
func (h *VismaHandler) IsConfigured() bool {
|
||
|
|
return h.clientID != "" && h.clientSecret != ""
|
||
|
|
}
|
||
|
|
|
||
|
|
// VismaCompany representerar ett Visma-företag
|
||
|
|
type VismaCompany struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
Name string `json:"name"`
|
||
|
|
OrgNumber string `json:"organisationNumber"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// VismaVoucher representerar ett Visma-verifikat
|
||
|
|
type VismaVoucher struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
VoucherDate string `json:"voucherDate"`
|
||
|
|
Text string `json:"text"`
|
||
|
|
Rows []VismaRow `json:"rows"`
|
||
|
|
Modified time.Time `json:"modifiedUtc"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// VismaRow representerar en verifikatrad
|
||
|
|
type VismaRow struct {
|
||
|
|
AccountID string `json:"accountId"`
|
||
|
|
AccountName string `json:"accountName"`
|
||
|
|
DebitAmount float64 `json:"debitAmount"`
|
||
|
|
CreditAmount float64 `json:"creditAmount"`
|
||
|
|
Description string `json:"description"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetAuthURL returnerar Visma OAuth URL
|
||
|
|
func (h *VismaHandler) GetAuthURL(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if !h.IsConfigured() {
|
||
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": "Visma not configured",
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
authURL := fmt.Sprintf(
|
||
|
|
"https://eaccountingapi.vismaonline.com/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&scope=ea:api",
|
||
|
|
h.clientID,
|
||
|
|
h.redirectURI,
|
||
|
|
)
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||
|
|
"ok": true,
|
||
|
|
"auth_url": authURL,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// HandleCallback hanterar Visma OAuth callback
|
||
|
|
func (h *VismaHandler) HandleCallback(w http.ResponseWriter, r *http.Request) {
|
||
|
|
code := r.URL.Query().Get("code")
|
||
|
|
if code == "" {
|
||
|
|
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": "missing code",
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Byt kod mot token
|
||
|
|
tokenURL := "https://eaccountingapi.vismaonline.com/oauth/token"
|
||
|
|
reqBody := fmt.Sprintf("grant_type=authorization_code&code=%s&redirect_uri=%s&client_id=%s&client_secret=%s",
|
||
|
|
code, h.redirectURI, h.clientID, h.clientSecret)
|
||
|
|
|
||
|
|
req, err := http.NewRequest("POST", tokenURL, strings.NewReader(reqBody))
|
||
|
|
if err != nil {
|
||
|
|
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": err.Error(),
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||
|
|
|
||
|
|
resp, err := h.client.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": err.Error(),
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
body, err := io.ReadAll(resp.Body)
|
||
|
|
if err != nil {
|
||
|
|
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": err.Error(),
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
var tokenResp struct {
|
||
|
|
AccessToken string `json:"access_token"`
|
||
|
|
RefreshToken string `json:"refresh_token"`
|
||
|
|
ExpiresIn int `json:"expires_in"`
|
||
|
|
}
|
||
|
|
if err := json.Unmarshal(body, &tokenResp); err != nil {
|
||
|
|
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": err.Error(),
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
h.accessToken = tokenResp.AccessToken
|
||
|
|
h.refreshToken = tokenResp.RefreshToken
|
||
|
|
h.tokenExpiry = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||
|
|
"ok": true,
|
||
|
|
"authenticated": true,
|
||
|
|
"expires": h.tokenExpiry,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetCompanies hämtar företag från Visma
|
||
|
|
func (h *VismaHandler) GetCompanies(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if h.accessToken == "" {
|
||
|
|
writeJSON(w, http.StatusUnauthorized, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": "not authenticated",
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
req, err := http.NewRequest("GET", "https://eaccountingapi.vismaonline.com/v2/companysettings", nil)
|
||
|
|
if err != nil {
|
||
|
|
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": err.Error(),
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
req.Header.Set("Authorization", "Bearer "+h.accessToken)
|
||
|
|
req.Header.Set("Accept", "application/json")
|
||
|
|
|
||
|
|
resp, err := h.client.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": err.Error(),
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
body, err := io.ReadAll(resp.Body)
|
||
|
|
if err != nil {
|
||
|
|
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": err.Error(),
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if resp.StatusCode != http.StatusOK {
|
||
|
|
writeJSON(w, resp.StatusCode, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": fmt.Sprintf("Visma API error: %s", string(body)),
|
||
|
|
"status": resp.StatusCode,
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
var companies []VismaCompany
|
||
|
|
if err := json.Unmarshal(body, &companies); err != nil {
|
||
|
|
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": err.Error(),
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||
|
|
"ok": true,
|
||
|
|
"companies": companies,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetVouchers hämtar verifikat från Visma
|
||
|
|
func (h *VismaHandler) GetVouchers(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if h.accessToken == "" {
|
||
|
|
writeJSON(w, http.StatusUnauthorized, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": "not authenticated",
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Hämta vouchers från Visma API
|
||
|
|
req, err := http.NewRequest("GET", "https://eaccountingapi.vismaonline.com/v2/vouchers", nil)
|
||
|
|
if err != nil {
|
||
|
|
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": err.Error(),
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
req.Header.Set("Authorization", "Bearer "+h.accessToken)
|
||
|
|
req.Header.Set("Accept", "application/json")
|
||
|
|
|
||
|
|
resp, err := h.client.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": fmt.Sprintf("Visma API error: %v", err),
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
if resp.StatusCode != http.StatusOK {
|
||
|
|
body, _ := io.ReadAll(resp.Body)
|
||
|
|
writeJSON(w, resp.StatusCode, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": fmt.Sprintf("Visma API returned %d: %s", resp.StatusCode, string(body)),
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
var vouchers []VismaVoucher
|
||
|
|
if err := json.NewDecoder(resp.Body).Decode(&vouchers); err != nil {
|
||
|
|
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
|
||
|
|
"ok": false,
|
||
|
|
"error": "Failed to decode Visma response",
|
||
|
|
})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||
|
|
"ok": true,
|
||
|
|
"vouchers": vouchers,
|
||
|
|
"source": "visma",
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// GetStatus returnerar Visma-kopplingsstatus
|
||
|
|
func (h *VismaHandler) GetStatus(w http.ResponseWriter, r *http.Request) {
|
||
|
|
status := map[string]interface{}{
|
||
|
|
"configured": h.IsConfigured(),
|
||
|
|
"authenticated": h.accessToken != "",
|
||
|
|
"client_id": h.clientID,
|
||
|
|
"token_expiry": h.tokenExpiry,
|
||
|
|
}
|
||
|
|
|
||
|
|
if !h.IsConfigured() {
|
||
|
|
status["setup_url"] = "/api/v1/visma/auth"
|
||
|
|
}
|
||
|
|
|
||
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||
|
|
"ok": true,
|
||
|
|
"visma": status,
|
||
|
|
})
|
||
|
|
}
|