152 lines
3.7 KiB
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
|
|
}
|