// Package app is the service layer. It holds every business operation the // management interface offers, so the HTML handlers and the REST API share one // implementation of validation, auditing and cache invalidation rather than // each growing their own. package app import ( "errors" "fmt" "net/http" ) // Error is a user-facing failure with an HTTP status attached. // // Messages are written for an administrator reading them in a toast or a JSON // response: they say what went wrong and, where useful, what to do about it. // Internal detail goes in the wrapped error, which is logged but never shown. type Error struct { Status int Message string Err error } func (e *Error) Error() string { if e.Err != nil { return fmt.Sprintf("%s: %v", e.Message, e.Err) } return e.Message } func (e *Error) Unwrap() error { return e.Err } // Invalid reports a client mistake such as a malformed record. func Invalid(format string, args ...any) *Error { return &Error{Status: http.StatusBadRequest, Message: fmt.Sprintf(format, args...)} } // NotFound reports a missing object. func NotFound(format string, args ...any) *Error { return &Error{Status: http.StatusNotFound, Message: fmt.Sprintf(format, args...)} } // Conflict reports a uniqueness violation. func Conflict(format string, args ...any) *Error { return &Error{Status: http.StatusConflict, Message: fmt.Sprintf(format, args...)} } // Forbidden reports an operation the caller may not perform. func Forbidden(format string, args ...any) *Error { return &Error{Status: http.StatusForbidden, Message: fmt.Sprintf(format, args...)} } // Internal wraps an unexpected failure. The message is safe to show; err is // logged server-side. func Internal(err error, format string, args ...any) *Error { return &Error{Status: http.StatusInternalServerError, Message: fmt.Sprintf(format, args...), Err: err} } // StatusOf maps any error to an HTTP status code. func StatusOf(err error) int { var e *Error if errors.As(err, &e) { return e.Status } return http.StatusInternalServerError } // MessageOf returns the user-facing message for an error, falling back to a // generic sentence so internal detail never leaks into a response. func MessageOf(err error) string { var e *Error if errors.As(err, &e) { return e.Message } return "An unexpected error occurred. Check the server log for details." } // IsInternal reports whether an error should be logged with its full detail. func IsInternal(err error) bool { return StatusOf(err) >= 500 }