init push

This commit is contained in:
2026-07-16 22:16:45 +07:00
commit 8b068bdb10
1021 changed files with 332816 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
package apperrorsx
import "wucher/internal/transport/http/jsonapi"
// AppError is the canonical backend error contract used by translators and writers.
type AppError struct {
Status int
Code string
ErrorCode string
Title string
Message string
Detail any
Source *jsonapi.ErrorSource
Cause error
}
func (e *AppError) Error() string {
if e == nil {
return ""
}
if e.Message != "" {
return e.Message
}
if e.Cause != nil {
return e.Cause.Error()
}
return e.Code
}
func (e *AppError) WithCause(err error) *AppError {
if e == nil {
return nil
}
e.Cause = err
return e
}

View File

@@ -0,0 +1,183 @@
package apperrorsx
import (
"fmt"
"net/http"
)
const (
ModuleGeneral = "00"
ModuleAuth = "01"
ModulePIN = "02"
ModuleHelicopter = "03"
ModuleHelicopterUsage = "35"
ModuleUser = "04"
ModuleMasterData = "05"
ModuleDutyRoster = "06"
ModuleFlight = "07"
ModuleContact = "08"
ModuleMissionOps = "09"
ModuleFacility = "10"
ModuleHospital = "11"
ModuleFederalState = "12"
ModuleNationality = "13"
ModuleICAO = "14"
ModuleLand = "15"
ModuleBase = "16"
ModuleHealthInsuranceCompanies = "17"
ModuleVocation = "18"
ModuleMedicine = "19"
ModuleMasterSettings = "20"
ModuleFileManager = "21"
ModuleOPCData = "22"
ModulePatientData = "23"
ModuleInsurancePatientData = "24"
ModuleRole = "25"
ModuleFlightData = "26"
ModuleAirRescuerChecklist = "27"
ModuleBranding = "28"
ModuleDUL = "29"
ModuleTakeover = "30"
ModuleFleetStatus = "31"
ModuleComplaint = "32"
ModuleEASARelease = "33"
ModuleMCF = "34"
ModuleHelicopterUsageSync = "35"
ModuleFMReport = "36"
ModuleAfterFlightInspection = "37"
ModuleActionSignoff = "38"
)
// ContextKeyHTTPErrorDetail stores the original, non-sanitized error detail for
// request logging. HTTP response writers should sanitize the public payload but
// keep this local available for access logs.
const ContextKeyHTTPErrorDetail = "http_error_detail"
// ContextKeyHTTPErrorInternalDetail stores an internal-only, verbose error
// summary for request logging. Unlike ContextKeyHTTPErrorDetail, this is not
// meant for public response sanitizers.
const ContextKeyHTTPErrorInternalDetail = "http_error_internal_detail"
type Def struct {
Status int
Code string
ErrorCode string
Title string
Message string
}
func Build(status int, moduleCode, caseCode, code, message string) Def {
return BuildWithTitle(
status,
moduleCode,
caseCode,
code,
titleForStatus(status),
message,
)
}
func BuildWithTitle(status int, moduleCode, caseCode, code, title, message string) Def {
return Def{
Status: status,
Code: code,
ErrorCode: fmt.Sprintf("%03d%s%s", status, moduleCode, caseCode),
Title: title,
Message: message,
}
}
func titleForStatus(status int) string {
switch status {
case http.StatusAccepted:
return "Accepted"
case http.StatusBadRequest:
return "Bad Request"
case http.StatusUnauthorized:
return "Unauthorized"
case http.StatusLocked:
return "Locked"
case http.StatusForbidden:
return "Forbidden"
case http.StatusNotFound:
return "Not found"
case http.StatusConflict:
return "Conflict"
case http.StatusUnprocessableEntity:
return "Validation error"
case http.StatusTooManyRequests:
return "Too many requests"
default:
return "Internal server error"
}
}
func New(def Def) *AppError {
return &AppError{
Status: def.Status,
Code: def.Code,
ErrorCode: def.ErrorCode,
Title: def.Title,
Message: def.Message,
}
}
func DefGeneral(status int, caseCode, code, message string) Def {
return Build(status, ModuleGeneral, caseCode, code, message)
}
func DefGeneralWithTitle(status int, caseCode, code, title, message string) Def {
return BuildWithTitle(status, ModuleGeneral, caseCode, code, title, message)
}
func DefAuth(status int, caseCode, code, message string) Def {
return Build(status, ModuleAuth, caseCode, code, message)
}
func DefPIN(status int, caseCode, code, message string) Def {
return Build(status, ModulePIN, caseCode, code, message)
}
func DefPINWithTitle(status int, caseCode, code, title, message string) Def {
return BuildWithTitle(status, ModulePIN, caseCode, code, title, message)
}
func DefHelicopter(status int, caseCode, code, message string) Def {
return Build(status, ModuleHelicopter, caseCode, code, message)
}
func DefHelicopterWithTitle(status int, caseCode, code, title, message string) Def {
return BuildWithTitle(status, ModuleHelicopter, caseCode, code, title, message)
}
func DefHelicopterUsage(status int, caseCode, code, message string) Def {
return Build(status, ModuleHelicopterUsage, caseCode, code, message)
}
func DefHelicopterUsageWithTitle(status int, caseCode, code, title, message string) Def {
return BuildWithTitle(status, ModuleHelicopterUsage, caseCode, code, title, message)
}
func DefHelicopterUsageSync(status int, caseCode, code, message string) Def {
return Build(status, ModuleHelicopterUsageSync, caseCode, code, message)
}
func DefHelicopterUsageSyncWithTitle(status int, caseCode, code, title, message string) Def {
return BuildWithTitle(status, ModuleHelicopterUsageSync, caseCode, code, title, message)
}
func DefFMReport(status int, caseCode, code, message string) Def {
return Build(status, ModuleFMReport, caseCode, code, message)
}
func DefFMReportWithTitle(status int, caseCode, code, title, message string) Def {
return BuildWithTitle(status, ModuleFMReport, caseCode, code, title, message)
}
func DefByModule(moduleCode string, status int, caseCode, code, message string) Def {
return Build(status, moduleCode, caseCode, code, message)
}
func DefByModuleWithTitle(moduleCode string, status int, caseCode, code, title, message string) Def {
return BuildWithTitle(status, moduleCode, caseCode, code, title, message)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,106 @@
package apperrorsx
import (
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
"wucher/internal/transport/http/jsonapi"
)
var (
registryByCode map[string]Def
registryByErrorCode map[string]Def
)
func init() {
registryByCode = make(map[string]Def, len(Registry))
registryByErrorCode = make(map[string]Def, len(Registry))
for _, def := range Registry {
registryByCode[def.Code] = def
registryByErrorCode[def.ErrorCode] = def
}
}
func EnrichErrorObjects(status int, errs []jsonapi.ErrorObject) []jsonapi.ErrorObject {
out := make([]jsonapi.ErrorObject, 0, len(errs))
for i := range errs {
obj := errs[i]
obj.Status = strconv.Itoa(status)
def := inferDef(status, obj)
if obj.Code == "" && def.Code != "" {
obj.Code = def.Code
}
if obj.ErrorCode == "" && def.ErrorCode != "" {
obj.ErrorCode = def.ErrorCode
}
if obj.Title == "" && def.Title != "" {
obj.Title = def.Title
}
out = append(out, obj)
}
return out
}
func inferDef(status int, obj jsonapi.ErrorObject) Def {
if obj.Code != "" {
if def, ok := registryByCode[obj.Code]; ok {
def.Status = status
def.ErrorCode = Build(status, moduleFromErrorCode(def.ErrorCode), caseFromErrorCode(def.ErrorCode), def.Code, def.Message).ErrorCode
return def
}
}
if obj.ErrorCode != "" {
if def, ok := registryByErrorCode[obj.ErrorCode]; ok {
return def
}
}
switch status {
case fiber.StatusBadRequest:
return ErrInvalidRequest
case fiber.StatusUnauthorized:
return ErrUserUnauthorized
case fiber.StatusForbidden:
return ErrForbiddenAccess
case fiber.StatusNotFound:
return ErrDataNotFound
case fiber.StatusConflict:
return ErrBusinessRuleFailed
case fiber.StatusUnprocessableEntity:
return ErrValidation
case fiber.StatusTooManyRequests:
return ErrPINAttemptLimitReached
default:
msg := strings.ToLower(strings.TrimSpace(obj.Detail))
if strings.Contains(msg, "not found") {
return ErrDataNotFound
}
if strings.Contains(msg, "unauthorized") {
return ErrUserUnauthorized
}
if strings.Contains(msg, "forbidden") {
return ErrForbiddenAccess
}
if strings.Contains(msg, "validation") || strings.Contains(msg, "invalid") {
return ErrValidation
}
return ErrInternal
}
}
func moduleFromErrorCode(errorCode string) string {
if len(errorCode) >= 5 {
return errorCode[3:5]
}
return ModuleGeneral
}
func caseFromErrorCode(errorCode string) string {
if len(errorCode) >= 7 {
return errorCode[5:7]
}
return CaseGeneralInternalError
}

View File

@@ -0,0 +1,141 @@
package apperrorsx
import (
"errors"
mysqldriver "github.com/go-sql-driver/mysql"
)
const (
mysqlErrAccessDenied uint16 = 1045
mysqlErrDuplicateEntry uint16 = 1062
mysqlErrCommandDenied uint16 = 1142
mysqlErrLockWaitTimeout uint16 = 1205
mysqlErrDeadlockFound uint16 = 1213
mysqlErrSpecificAccessDenied uint16 = 1227
mysqlErrColumnCannotBeNull uint16 = 1048
mysqlErrTruncatedWrongValue uint16 = 1292
mysqlErrIncorrectValue uint16 = 1366
mysqlErrServerGone uint16 = 2006
mysqlErrFkParentStillReferenced uint16 = 1451
mysqlErrFkChildNotFound uint16 = 1452
mysqlErrCheckConstraintViolated uint16 = 3819
mysqlErrConstraintFailed uint16 = 4025
)
func mapMySQLError(module string, err error) *AppError {
var mysqlErr *mysqldriver.MySQLError
if !errors.As(err, &mysqlErr) {
return nil
}
switch module {
case ModuleHelicopter:
return mapMySQLErrorHelicopter(mysqlErr, err)
case ModuleHospital:
return mapMySQLErrorHospital(mysqlErr, err)
case ModuleFleetStatus:
return mapMySQLErrorFleetStatus(mysqlErr, err)
case ModuleComplaint:
return mapMySQLErrorComplaint(mysqlErr, err)
default:
return mapMySQLErrorGeneral(mysqlErr, err)
}
}
func mapMySQLErrorFleetStatus(mysqlErr *mysqldriver.MySQLError, cause error) *AppError {
switch mysqlErr.Number {
case mysqlErrDuplicateEntry:
return New(ErrFleetStatusDuplicate).WithCause(cause)
case mysqlErrFkParentStillReferenced:
return New(ErrFleetStatusRelationDelete).WithCause(cause)
case mysqlErrFkChildNotFound:
// fleet_status.helicopter_id references a non-existent helicopter.
return New(ErrFleetStatusHelicopterInvalid).WithCause(cause)
case mysqlErrColumnCannotBeNull,
mysqlErrTruncatedWrongValue,
mysqlErrIncorrectValue,
mysqlErrCheckConstraintViolated,
mysqlErrConstraintFailed:
return New(ErrFleetStatusInvalidPayload).WithCause(cause)
default:
return nil
}
}
func mapMySQLErrorGeneral(mysqlErr *mysqldriver.MySQLError, cause error) *AppError {
switch mysqlErr.Number {
case mysqlErrAccessDenied:
return New(ErrUserUnauthorized).WithCause(cause)
case mysqlErrCommandDenied, mysqlErrSpecificAccessDenied:
return New(ErrForbiddenAccess).WithCause(cause)
case mysqlErrLockWaitTimeout, mysqlErrDeadlockFound:
return New(ErrBusinessRuleFailed).WithCause(cause)
case mysqlErrServerGone:
return New(ErrInternal).WithCause(cause)
case mysqlErrDuplicateEntry, mysqlErrFkParentStillReferenced:
return New(ErrBusinessRuleFailed).WithCause(cause)
case mysqlErrColumnCannotBeNull,
mysqlErrTruncatedWrongValue,
mysqlErrIncorrectValue,
mysqlErrFkChildNotFound,
mysqlErrCheckConstraintViolated,
mysqlErrConstraintFailed:
return New(ErrValidation).WithCause(cause)
default:
return nil
}
}
func mapMySQLErrorHelicopter(mysqlErr *mysqldriver.MySQLError, cause error) *AppError {
switch mysqlErr.Number {
case mysqlErrDuplicateEntry:
return New(ErrHelicopterDuplicate).WithCause(cause)
case mysqlErrFkParentStillReferenced:
return New(ErrHelicopterRelationDelete).WithCause(cause)
case mysqlErrColumnCannotBeNull,
mysqlErrTruncatedWrongValue,
mysqlErrIncorrectValue,
mysqlErrFkChildNotFound,
mysqlErrCheckConstraintViolated,
mysqlErrConstraintFailed:
return New(ErrHelicopterInvalidPayload).WithCause(cause)
default:
return nil
}
}
func mapMySQLErrorHospital(mysqlErr *mysqldriver.MySQLError, cause error) *AppError {
switch mysqlErr.Number {
case mysqlErrFkParentStillReferenced:
return New(ErrHospitalRelationDelete).WithCause(cause)
case mysqlErrColumnCannotBeNull,
mysqlErrTruncatedWrongValue,
mysqlErrIncorrectValue,
mysqlErrFkChildNotFound,
mysqlErrCheckConstraintViolated,
mysqlErrConstraintFailed:
return New(ErrHospitalInvalidPayload).WithCause(cause)
default:
return nil
}
}
func mapMySQLErrorComplaint(mysqlErr *mysqldriver.MySQLError, cause error) *AppError {
switch mysqlErr.Number {
case mysqlErrDuplicateEntry:
return New(ErrComplaintInvalidPayload).WithCause(cause)
case mysqlErrFkParentStillReferenced:
return New(ErrComplaintRelationDelete).WithCause(cause)
case mysqlErrFkChildNotFound:
return New(ErrComplaintInvalidHelicopterID).WithCause(cause)
case mysqlErrColumnCannotBeNull,
mysqlErrTruncatedWrongValue,
mysqlErrIncorrectValue,
mysqlErrCheckConstraintViolated,
mysqlErrConstraintFailed:
return New(ErrComplaintInvalidPayload).WithCause(cause)
default:
return nil
}
}

View File

@@ -0,0 +1,33 @@
package apperrorsx
import "testing"
func TestRegistry_UniqueCodeAndErrorCode(t *testing.T) {
seenCode := map[string]struct{}{}
seenErrorCode := map[string]struct{}{}
seenModuleCase := map[string]struct{}{}
for _, d := range Registry {
if d.Code == "" {
t.Fatalf("empty code in registry: %#v", d)
}
if d.ErrorCode == "" {
t.Fatalf("empty error_code in registry: %#v", d)
}
if _, ok := seenCode[d.Code]; ok {
t.Fatalf("duplicate code: %s", d.Code)
}
if _, ok := seenErrorCode[d.ErrorCode]; ok {
t.Fatalf("duplicate error_code: %s", d.ErrorCode)
}
if len(d.ErrorCode) != 7 {
t.Fatalf("invalid error_code length for %s: %s", d.Code, d.ErrorCode)
}
moduleCase := d.ErrorCode[3:]
if _, ok := seenModuleCase[moduleCase]; ok {
t.Fatalf("duplicate module+case detected: %s (error_code=%s)", moduleCase, d.ErrorCode)
}
seenCode[d.Code] = struct{}{}
seenErrorCode[d.ErrorCode] = struct{}{}
seenModuleCase[moduleCase] = struct{}{}
}
}

View File

@@ -0,0 +1,68 @@
package apperrorsx
import (
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
"wucher/internal/transport/http/jsonapi"
)
func Write(c *fiber.Ctx, e *AppError) error {
if e == nil {
e = New(ErrInternal)
}
if e.Status == 0 {
e.Status = fiber.StatusInternalServerError
}
if e.Title == "" {
e.Title = defaultTitle(e.Status)
}
if detail := publicLogDetail(e.Message, e.Cause, e.Title); detail != "" {
c.Locals(ContextKeyHTTPErrorDetail, detail)
}
obj := jsonapi.ErrorObject{
Status: strconv.Itoa(e.Status),
Code: e.Code,
ErrorCode: e.ErrorCode,
Title: e.Title,
Detail: PublicDetail(e.Status),
Source: e.Source,
}
return jsonapi.WriteErrors(c, e.Status, []jsonapi.ErrorObject{obj})
}
func publicLogDetail(message string, cause error, fallback string) string {
if cause != nil {
if trimmed := strings.TrimSpace(cause.Error()); trimmed != "" {
return trimmed
}
}
if trimmed := strings.TrimSpace(message); trimmed != "" {
return trimmed
}
return strings.TrimSpace(fallback)
}
func defaultTitle(status int) string {
switch status {
case fiber.StatusBadRequest:
return "Bad Request"
case fiber.StatusUnauthorized:
return "Unauthorized"
case fiber.StatusForbidden:
return "Forbidden"
case fiber.StatusNotFound:
return "Not found"
case fiber.StatusConflict:
return "Conflict"
case fiber.StatusUnprocessableEntity:
return "Validation error"
case fiber.StatusTooManyRequests:
return "Too many requests"
default:
return "Internal server error"
}
}

View File

@@ -0,0 +1,54 @@
package apperrorsx
import (
"bytes"
"io"
"net/http"
"testing"
"github.com/gofiber/fiber/v2"
)
func TestPublicDetail(t *testing.T) {
tests := []struct {
status int
want string
}{
{http.StatusBadRequest, "invalid request"},
{http.StatusUnauthorized, "unauthorized"},
{http.StatusForbidden, "forbidden"},
{http.StatusNotFound, "not found"},
{http.StatusConflict, "conflict"},
{http.StatusUnprocessableEntity, "validation failed"},
{http.StatusTooManyRequests, "too many requests"},
{http.StatusInternalServerError, "an internal error occurred"},
}
for _, tc := range tests {
if got := PublicDetail(tc.status); got != tc.want {
t.Fatalf("status %d: expected %q, got %q", tc.status, tc.want, got)
}
}
}
func TestWriteSanitizesDetail(t *testing.T) {
app := fiber.New()
app.Get("/", func(c *fiber.Ctx) error {
err := New(ErrInternal)
err.Message = "raw database message"
return Write(c, err)
})
req, _ := http.NewRequest(http.MethodGet, "/", nil)
resp, err := app.Test(req)
if err != nil {
t.Fatalf("app.Test: %v", err)
}
body, _ := io.ReadAll(resp.Body)
if bytes.Contains(body, []byte("raw database message")) {
t.Fatalf("expected sanitized detail, got %s", string(body))
}
if !bytes.Contains(body, []byte(`"detail":"an internal error occurred"`)) {
t.Fatalf("expected generic detail, got %s", string(body))
}
}

View File

@@ -0,0 +1,221 @@
package apperrorsx
import (
"context"
"errors"
"gorm.io/gorm"
afterflightinspection "wucher/internal/domain/after_flight_inspection"
"wucher/internal/service"
"wucher/internal/shared/pkg/apperrors"
)
// Translate is the single entrypoint for mapping domain/service errors to AppError.
func Translate(module string, err error) *AppError {
if err == nil {
return nil
}
switch module {
case ModuleAuth, ModulePIN:
if mapped := mapAuthPIN(err); mapped != nil {
return mapped
}
case ModuleHelicopter:
if mapped := mapHelicopter(err); mapped != nil {
return mapped
}
case ModuleHospital:
if mapped := mapHospital(err); mapped != nil {
return mapped
}
case ModuleTakeover:
if mapped := mapTakeover(err); mapped != nil {
return mapped
}
case ModuleFleetStatus:
if mapped := mapFleetStatus(err); mapped != nil {
return mapped
}
case ModuleComplaint:
if mapped := mapComplaint(err); mapped != nil {
return mapped
}
case ModuleAfterFlightInspection:
if mapped := mapAfterFlightInspection(err); mapped != nil {
return mapped
}
}
return mapGeneral(err)
}
func mapAfterFlightInspection(err error) *AppError {
switch {
case errors.Is(err, afterflightinspection.ErrInvalidFlightInspectionID):
return New(ErrAfterFlightInvalidID).WithCause(err)
case errors.Is(err, afterflightinspection.ErrFlightInspectionNotFound):
return New(ErrAfterFlightFlightInspectionNotFound).WithCause(err)
case errors.Is(err, afterflightinspection.ErrCapabilitiesUnavailable):
return New(ErrAfterFlightCapabilitiesUnavailable).WithCause(err)
case errors.Is(err, afterflightinspection.ErrNR1Required):
return New(ErrAfterFlightNR1Required).WithCause(err)
case errors.Is(err, afterflightinspection.ErrNR1NotAvailable):
return New(ErrAfterFlightNR1NotAvailable).WithCause(err)
case errors.Is(err, afterflightinspection.ErrNR2Required):
return New(ErrAfterFlightNR2Required).WithCause(err)
case errors.Is(err, afterflightinspection.ErrNR2NotAvailable):
return New(ErrAfterFlightNR2NotAvailable).WithCause(err)
case errors.Is(err, afterflightinspection.ErrLHRequired):
return New(ErrAfterFlightLHRequired).WithCause(err)
case errors.Is(err, afterflightinspection.ErrLHNotAvailable):
return New(ErrAfterFlightLHNotAvailable).WithCause(err)
case errors.Is(err, afterflightinspection.ErrRHRequired):
return New(ErrAfterFlightRHRequired).WithCause(err)
case errors.Is(err, afterflightinspection.ErrRHNotAvailable):
return New(ErrAfterFlightRHNotAvailable).WithCause(err)
case errors.Is(err, afterflightinspection.ErrMGBRequired):
return New(ErrAfterFlightMGBRequired).WithCause(err)
case errors.Is(err, afterflightinspection.ErrMGBNotAvailable):
return New(ErrAfterFlightMGBNotAvailable).WithCause(err)
case errors.Is(err, afterflightinspection.ErrIGBRequired):
return New(ErrAfterFlightIGBRequired).WithCause(err)
case errors.Is(err, afterflightinspection.ErrIGBNotAvailable):
return New(ErrAfterFlightIGBNotAvailable).WithCause(err)
case errors.Is(err, afterflightinspection.ErrTGBRequired):
return New(ErrAfterFlightTGBRequired).WithCause(err)
case errors.Is(err, afterflightinspection.ErrTGBNotAvailable):
return New(ErrAfterFlightTGBNotAvailable).WithCause(err)
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return New(ErrAfterFlightInspectionNotFound).WithCause(err)
}
if mapped := mapMySQLError(ModuleAfterFlightInspection, err); mapped != nil {
return mapped
}
return nil
}
func mapFleetStatus(err error) *AppError {
if errors.Is(err, gorm.ErrRecordNotFound) {
return New(ErrFleetStatusNotFound).WithCause(err)
}
if _, ok := apperrors.AsDeleteConflict(err); ok {
return New(ErrFleetStatusRelationDelete).WithCause(err)
}
if mapped := mapMySQLError(ModuleFleetStatus, err); mapped != nil {
return mapped
}
return nil
}
func mapComplaint(err error) *AppError {
if errors.Is(err, gorm.ErrRecordNotFound) {
return New(ErrComplaintNotFound).WithCause(err)
}
if mapped := mapMySQLError(ModuleComplaint, err); mapped != nil {
return mapped
}
return nil
}
func mapGeneral(err error) *AppError {
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
return New(ErrDataNotFound).WithCause(err)
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
return New(ErrInvalidRequest).WithCause(err)
}
if mapped := mapMySQLError(ModuleGeneral, err); mapped != nil {
return mapped
}
switch {
case errors.Is(err, service.ErrInvalidReauthMethod):
return New(ErrValidation).WithCause(err)
default:
return New(ErrInternal).WithCause(err)
}
}
func mapAuthPIN(err error) *AppError {
switch {
case errors.Is(err, service.ErrInvalidToken):
return New(ErrTokenInvalid).WithCause(err)
case errors.Is(err, service.ErrInvalidCredentials):
return New(ErrUserUnauthorized).WithCause(err)
case errors.Is(err, service.ErrSecurityPINNotSet):
return New(ErrPINNotSet).WithCause(err)
case errors.Is(err, service.ErrSecurityPINLocked), errors.Is(err, service.ErrSecurityPINResetRequired):
return New(ErrPINBlocked).WithCause(err)
case errors.Is(err, service.ErrSecurityPINAttemptLimit):
return New(ErrPINBlocked).WithCause(err)
case errors.Is(err, service.ErrInvalidSecurityPIN):
return New(ErrPINIncorrect).WithCause(err)
case errors.Is(err, service.ErrInvalidPINAction):
return New(ErrPINVerificationRequired202).WithCause(err)
case errors.Is(err, service.ErrInvalidReauthMethod):
return New(ErrValidation).WithCause(err)
default:
return nil
}
}
func mapHelicopter(err error) *AppError {
if errors.Is(err, gorm.ErrRecordNotFound) {
return New(ErrHelicopterNotFound).WithCause(err)
}
if _, ok := apperrors.AsDeleteConflict(err); ok {
return New(ErrHelicopterRelationDelete).WithCause(err)
}
if mapped := mapMySQLError(ModuleHelicopter, err); mapped != nil {
return mapped
}
switch {
case errors.Is(err, service.ErrInvalidReauthMethod):
return New(ErrHelicopterInvalidPayload).WithCause(err)
default:
return nil
}
}
func mapHospital(err error) *AppError {
if errors.Is(err, gorm.ErrRecordNotFound) {
return New(ErrHospitalNotFound).WithCause(err)
}
if _, ok := apperrors.AsDeleteConflict(err); ok {
return New(ErrHospitalRelationDelete).WithCause(err)
}
if mapped := mapMySQLError(ModuleHospital, err); mapped != nil {
return mapped
}
return nil
}
func mapTakeover(err error) *AppError {
if errors.Is(err, gorm.ErrRecordNotFound) {
return New(ErrTakeoverNotFound).WithCause(err)
}
if mapped := mapMySQLError(ModuleTakeover, err); mapped != nil {
return mapped
}
return nil
}

View File

@@ -0,0 +1,40 @@
package apperrorsx
import (
"errors"
"testing"
mysqldriver "github.com/go-sql-driver/mysql"
"gorm.io/gorm"
"wucher/internal/service"
)
func TestTranslate_AuthPIN(t *testing.T) {
got := Translate(ModulePIN, service.ErrInvalidSecurityPIN)
if got == nil || got.Code != CodePINIncorrect {
t.Fatalf("expected %s, got %#v", CodePINIncorrect, got)
}
got = Translate(ModuleAuth, service.ErrInvalidToken)
if got == nil || got.Code != CodeTokenInvalid {
t.Fatalf("expected %s, got %#v", CodeTokenInvalid, got)
}
}
func TestTranslate_HelicopterAndFallback(t *testing.T) {
got := Translate(ModuleHelicopter, gorm.ErrRecordNotFound)
if got == nil || got.Code != CodeHelicopterNotFound {
t.Fatalf("expected %s, got %#v", CodeHelicopterNotFound, got)
}
got = Translate(ModuleHelicopter, &mysqldriver.MySQLError{Number: 1062, Message: "duplicate"})
if got == nil || got.Code != CodeHelicopterDuplicate {
t.Fatalf("expected %s, got %#v", CodeHelicopterDuplicate, got)
}
got = Translate("99", errors.New("unknown issue"))
if got == nil || got.Code != CodeInternalServerError {
t.Fatalf("expected %s, got %#v", CodeInternalServerError, got)
}
}