Files
core/internal/api/tickets.go

152 lines
3.7 KiB
Go

package api
import (
"net/http"
"time"
"reanimator/internal/domain"
"reanimator/internal/repository/registry"
"reanimator/internal/repository/tickets"
)
type TicketDependencies struct {
Tickets *tickets.TicketRepository
Assets *registry.AssetRepository
}
type ticketHandlers struct {
deps TicketDependencies
}
func RegisterTicketRoutes(mux *http.ServeMux, deps TicketDependencies) {
h := ticketHandlers{deps: deps}
mux.HandleFunc("/connectors/tickets/sync", h.handleTicketSync)
}
type ticketSyncRequest struct {
Source string `json:"source"`
Tickets []ticketSyncEntry `json:"tickets"`
}
type ticketSyncEntry struct {
ExternalID string `json:"external_id"`
Title string `json:"title"`
Status string `json:"status"`
OpenedAt *string `json:"opened_at"`
ClosedAt *string `json:"closed_at"`
URL *string `json:"url"`
AssetID int64 `json:"asset_id"`
}
func (h ticketHandlers) handleTicketSync(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if h.deps.Tickets == nil {
writeError(w, http.StatusInternalServerError, "tickets unavailable")
return
}
var req ticketSyncRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid json")
return
}
if req.Source == "" {
writeError(w, http.StatusBadRequest, "source is required")
return
}
if len(req.Tickets) == 0 {
writeError(w, http.StatusBadRequest, "tickets is required")
return
}
tx, err := h.deps.Tickets.BeginTx(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, "ticket sync failed")
return
}
defer func() {
_ = tx.Rollback()
}()
for _, entry := range req.Tickets {
if entry.ExternalID == "" {
writeError(w, http.StatusBadRequest, "tickets.external_id is required")
return
}
if entry.Title == "" {
writeError(w, http.StatusBadRequest, "tickets.title is required")
return
}
if entry.Status == "" {
writeError(w, http.StatusBadRequest, "tickets.status is required")
return
}
if entry.AssetID <= 0 {
writeError(w, http.StatusBadRequest, "tickets.asset_id is required")
return
}
if h.deps.Assets != nil {
if _, err := h.deps.Assets.Get(r.Context(), entry.AssetID); err != nil {
if err == registry.ErrNotFound {
writeError(w, http.StatusBadRequest, "asset_id not found")
return
}
writeError(w, http.StatusInternalServerError, "asset lookup failed")
return
}
}
openedAt, err := parseRFC3339(entry.OpenedAt)
if err != nil {
writeError(w, http.StatusBadRequest, "tickets.opened_at must be RFC3339")
return
}
closedAt, err := parseRFC3339(entry.ClosedAt)
if err != nil {
writeError(w, http.StatusBadRequest, "tickets.closed_at must be RFC3339")
return
}
id, err := h.deps.Tickets.Upsert(r.Context(), tx, domain.Ticket{
Source: req.Source,
ExternalID: entry.ExternalID,
Title: entry.Title,
Status: entry.Status,
OpenedAt: openedAt,
ClosedAt: closedAt,
URL: entry.URL,
})
if err != nil {
writeError(w, http.StatusInternalServerError, "ticket sync failed")
return
}
if err := h.deps.Tickets.LinkToAsset(r.Context(), tx, id, entry.AssetID); err != nil {
writeError(w, http.StatusInternalServerError, "ticket link failed")
return
}
}
if err := tx.Commit(); err != nil {
writeError(w, http.StatusInternalServerError, "ticket sync failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"synced": len(req.Tickets),
})
}
func parseRFC3339(value *string) (*time.Time, error) {
if value == nil || *value == "" {
return nil, nil
}
parsed, err := time.Parse(time.RFC3339, *value)
if err != nil {
return nil, err
}
return &parsed, nil
}