init push
This commit is contained in:
443
internal/service/helicopter_write_service.go
Normal file
443
internal/service/helicopter_write_service.go
Normal file
@@ -0,0 +1,443 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"wucher/internal/domain/helicopter"
|
||||
sharedconst "wucher/internal/shared/const"
|
||||
"wucher/internal/shared/pkg/uuidv7"
|
||||
)
|
||||
|
||||
type helicopterPINVerifier interface {
|
||||
ConsumeSecurityPINActionToken(ctx context.Context, userID []byte, action, token string) error
|
||||
}
|
||||
|
||||
type helicopterPINSessionVerifier interface {
|
||||
AccessCookieName() string
|
||||
ParseAccessTokenSessionID(ctx context.Context, accessToken string) (string, error)
|
||||
ConsumeSecurityPINActionTokenInSession(ctx context.Context, userID []byte, action, token, sessionID string) error
|
||||
}
|
||||
|
||||
type HelicopterWriteError struct {
|
||||
Status int
|
||||
Title string
|
||||
Detail string
|
||||
Pointer string
|
||||
}
|
||||
|
||||
func (e *HelicopterWriteError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return e.Detail
|
||||
}
|
||||
|
||||
type HelicopterWriteResult struct {
|
||||
Row *helicopter.Helicopter
|
||||
IsInUse bool
|
||||
PreviousFotoAttachmentID []byte
|
||||
}
|
||||
|
||||
type HelicopterCreateInput struct {
|
||||
ID []byte
|
||||
Designation string
|
||||
Identifier string
|
||||
Type string
|
||||
SortKey *int
|
||||
Engine1 string
|
||||
Engine2 string
|
||||
Consumption float64
|
||||
NR1 bool
|
||||
NR2 bool
|
||||
LH bool
|
||||
RH bool
|
||||
MGB bool
|
||||
IGB bool
|
||||
TGB bool
|
||||
UTCOffset int
|
||||
FotoAttachmentID []byte
|
||||
Dry bool
|
||||
AirOnGround bool
|
||||
AOGReason string
|
||||
AOGSource string
|
||||
Note string
|
||||
IsActive bool
|
||||
ActorUserID []byte
|
||||
PINVerifier helicopterPINVerifier
|
||||
PINToken string
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
type HelicopterUpdateInput struct {
|
||||
ID []byte
|
||||
Designation *string
|
||||
Identifier *string
|
||||
Type *string
|
||||
SortKey helicopter.SortKeyPatch
|
||||
Engine1 *string
|
||||
Engine2 *string
|
||||
Consumption *float64
|
||||
NR1 *bool
|
||||
NR2 *bool
|
||||
LH *bool
|
||||
RH *bool
|
||||
MGB *bool
|
||||
IGB *bool
|
||||
TGB *bool
|
||||
UTCOffset *int
|
||||
FotoAttachmentID []byte
|
||||
FotoAttachmentSet bool
|
||||
Dry *bool
|
||||
AirOnGround *bool
|
||||
AOGReason *string
|
||||
AOGSource *string
|
||||
Note *string
|
||||
IsActive *bool
|
||||
ActorUserID []byte
|
||||
PINVerifier helicopterPINVerifier
|
||||
PINToken string
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
func (s *HelicopterService) CreateDetailed(ctx context.Context, in HelicopterCreateInput) (*HelicopterWriteResult, error) {
|
||||
if s == nil || s.repo == nil {
|
||||
return nil, &HelicopterWriteError{Status: http.StatusInternalServerError, Title: "Create failed", Detail: "helicopter service is not configured"}
|
||||
}
|
||||
|
||||
designation := strings.TrimSpace(in.Designation)
|
||||
identifier := normalizeHelicopterIdentifier(in.Identifier)
|
||||
heliType := strings.TrimSpace(in.Type)
|
||||
if designation == "" || identifier == "" || heliType == "" {
|
||||
return nil, &HelicopterWriteError{
|
||||
Status: http.StatusUnprocessableEntity,
|
||||
Title: "Validation error",
|
||||
Detail: "designation, identifier, and type are required",
|
||||
Pointer: "/data/attributes",
|
||||
}
|
||||
}
|
||||
exists, err := s.repo.ExistsByIdentifier(ctx, identifier, nil)
|
||||
if err != nil {
|
||||
return nil, &HelicopterWriteError{Status: http.StatusBadRequest, Title: "Create failed", Detail: err.Error()}
|
||||
}
|
||||
if exists {
|
||||
return nil, &HelicopterWriteError{
|
||||
Status: http.StatusConflict,
|
||||
Title: "Conflict",
|
||||
Detail: "Serial number already exists",
|
||||
Pointer: "/data/attributes/identifier",
|
||||
}
|
||||
}
|
||||
|
||||
aogReason := strings.TrimSpace(in.AOGReason)
|
||||
if in.AirOnGround && aogReason == "" {
|
||||
return nil, &HelicopterWriteError{
|
||||
Status: http.StatusUnprocessableEntity,
|
||||
Title: "AOG reason required",
|
||||
Detail: "AOG reason must be provided when AOG is active (`aog=true`).",
|
||||
Pointer: "/data/attributes/aog_reason",
|
||||
}
|
||||
}
|
||||
if in.AirOnGround {
|
||||
if err := verifyHelicopterAOGPIN(ctx, in.PINVerifier, in.AccessToken, in.ActorUserID, in.PINToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
row := &helicopter.Helicopter{
|
||||
ID: append([]byte(nil), in.ID...),
|
||||
Designation: designation,
|
||||
Identifier: identifier,
|
||||
ReportSequence: 0,
|
||||
Type: heliType,
|
||||
SortKey: cloneIntPtr(in.SortKey),
|
||||
Engine1: strings.TrimSpace(in.Engine1),
|
||||
Engine2: strings.TrimSpace(in.Engine2),
|
||||
Consumption: in.Consumption,
|
||||
NR1: in.NR1,
|
||||
NR2: in.NR2,
|
||||
LH: in.LH,
|
||||
RH: in.RH,
|
||||
MGB: in.MGB,
|
||||
IGB: in.IGB,
|
||||
TGB: in.TGB,
|
||||
UTCOffset: in.UTCOffset,
|
||||
FotoAttachmentID: append([]byte(nil), in.FotoAttachmentID...),
|
||||
Dry: in.Dry,
|
||||
AirOnGround: in.AirOnGround,
|
||||
AOGReason: aogReason,
|
||||
AOGSource: func() string {
|
||||
if in.AirOnGround {
|
||||
return helicopter.AOGSourceManual
|
||||
}
|
||||
return helicopter.AOGSourceUnknown
|
||||
}(),
|
||||
Note: strings.TrimSpace(in.Note),
|
||||
IsActive: in.IsActive,
|
||||
CreatedBy: append([]byte(nil), in.ActorUserID...),
|
||||
UpdatedBy: append([]byte(nil), in.ActorUserID...),
|
||||
}
|
||||
if len(row.ID) == 0 {
|
||||
row.ID = uuidv7.MustBytes()
|
||||
}
|
||||
if err := s.Create(ctx, row); err != nil {
|
||||
return nil, &HelicopterWriteError{Status: http.StatusBadRequest, Title: "Create failed", Detail: err.Error()}
|
||||
}
|
||||
|
||||
created, err := s.GetByID(ctx, row.ID)
|
||||
if err != nil {
|
||||
return nil, &HelicopterWriteError{Status: http.StatusBadRequest, Title: "Create failed", Detail: err.Error()}
|
||||
}
|
||||
if created == nil {
|
||||
created = row
|
||||
}
|
||||
isInUse, err := s.IsInUse(ctx, row.ID)
|
||||
if err != nil {
|
||||
return nil, &HelicopterWriteError{Status: http.StatusBadRequest, Title: "Create failed", Detail: err.Error()}
|
||||
}
|
||||
return &HelicopterWriteResult{Row: created, IsInUse: isInUse}, nil
|
||||
}
|
||||
|
||||
func (s *HelicopterService) UpdateDetailed(ctx context.Context, in HelicopterUpdateInput) (*HelicopterWriteResult, error) {
|
||||
if s == nil || s.repo == nil {
|
||||
return nil, &HelicopterWriteError{Status: http.StatusInternalServerError, Title: "Update failed", Detail: "helicopter service is not configured"}
|
||||
}
|
||||
if len(in.ID) != 16 {
|
||||
return nil, &HelicopterWriteError{
|
||||
Status: http.StatusUnprocessableEntity,
|
||||
Title: "Validation error",
|
||||
Detail: "id is invalid UUID",
|
||||
Pointer: "/path/id",
|
||||
}
|
||||
}
|
||||
|
||||
existing, err := s.GetByID(ctx, in.ID)
|
||||
if err != nil {
|
||||
return nil, &HelicopterWriteError{Status: http.StatusBadRequest, Title: "Update failed", Detail: err.Error()}
|
||||
}
|
||||
if existing == nil {
|
||||
return nil, &HelicopterWriteError{Status: http.StatusNotFound, Title: "Not found", Detail: "helicopter not found"}
|
||||
}
|
||||
|
||||
prevAOG := existing.AirOnGround
|
||||
prevFotoAttachmentID := append([]byte(nil), existing.FotoAttachmentID...)
|
||||
|
||||
if in.Designation != nil {
|
||||
v := strings.TrimSpace(*in.Designation)
|
||||
if v == "" {
|
||||
return nil, &HelicopterWriteError{
|
||||
Status: http.StatusUnprocessableEntity,
|
||||
Title: "Validation error",
|
||||
Detail: "designation cannot be empty",
|
||||
Pointer: "/data/attributes/designation",
|
||||
}
|
||||
}
|
||||
existing.Designation = v
|
||||
}
|
||||
if in.Identifier != nil {
|
||||
raw := normalizeHelicopterIdentifier(*in.Identifier)
|
||||
if raw == "" {
|
||||
return nil, &HelicopterWriteError{
|
||||
Status: http.StatusUnprocessableEntity,
|
||||
Title: "Validation error",
|
||||
Detail: "identifier cannot be empty",
|
||||
Pointer: "/data/attributes/identifier",
|
||||
}
|
||||
}
|
||||
exists, existsErr := s.repo.ExistsByIdentifier(ctx, raw, existing.ID)
|
||||
if existsErr != nil {
|
||||
return nil, &HelicopterWriteError{Status: http.StatusBadRequest, Title: "Update failed", Detail: existsErr.Error()}
|
||||
}
|
||||
if exists {
|
||||
return nil, &HelicopterWriteError{
|
||||
Status: http.StatusConflict,
|
||||
Title: "Conflict",
|
||||
Detail: "Serial number already exists",
|
||||
Pointer: "/data/attributes/identifier",
|
||||
}
|
||||
}
|
||||
existing.Identifier = raw
|
||||
}
|
||||
if in.Type != nil {
|
||||
v := strings.TrimSpace(*in.Type)
|
||||
if v == "" {
|
||||
return nil, &HelicopterWriteError{
|
||||
Status: http.StatusUnprocessableEntity,
|
||||
Title: "Validation error",
|
||||
Detail: "type cannot be empty",
|
||||
Pointer: "/data/attributes/type",
|
||||
}
|
||||
}
|
||||
existing.Type = v
|
||||
}
|
||||
s.ApplySortKeyPatch(existing, in.SortKey)
|
||||
if in.Engine1 != nil {
|
||||
existing.Engine1 = strings.TrimSpace(*in.Engine1)
|
||||
}
|
||||
if in.Engine2 != nil {
|
||||
existing.Engine2 = strings.TrimSpace(*in.Engine2)
|
||||
}
|
||||
if in.Consumption != nil {
|
||||
existing.Consumption = *in.Consumption
|
||||
}
|
||||
if in.NR1 != nil {
|
||||
existing.NR1 = *in.NR1
|
||||
}
|
||||
if in.NR2 != nil {
|
||||
existing.NR2 = *in.NR2
|
||||
}
|
||||
if in.LH != nil {
|
||||
existing.LH = *in.LH
|
||||
}
|
||||
if in.RH != nil {
|
||||
existing.RH = *in.RH
|
||||
}
|
||||
if in.MGB != nil {
|
||||
existing.MGB = *in.MGB
|
||||
}
|
||||
if in.IGB != nil {
|
||||
existing.IGB = *in.IGB
|
||||
}
|
||||
if in.TGB != nil {
|
||||
existing.TGB = *in.TGB
|
||||
}
|
||||
if in.UTCOffset != nil {
|
||||
existing.UTCOffset = *in.UTCOffset
|
||||
}
|
||||
if in.FotoAttachmentSet {
|
||||
if len(in.FotoAttachmentID) == 0 {
|
||||
existing.FotoAttachmentID = nil
|
||||
} else {
|
||||
existing.FotoAttachmentID = append([]byte(nil), in.FotoAttachmentID...)
|
||||
}
|
||||
}
|
||||
if in.Dry != nil {
|
||||
existing.Dry = *in.Dry
|
||||
}
|
||||
if in.AirOnGround != nil {
|
||||
existing.AirOnGround = *in.AirOnGround
|
||||
if *in.AirOnGround {
|
||||
existing.AOGSource = helicopter.AOGSourceManual
|
||||
}
|
||||
}
|
||||
if in.AOGReason != nil {
|
||||
existing.AOGReason = strings.TrimSpace(*in.AOGReason)
|
||||
}
|
||||
if in.AOGSource != nil {
|
||||
existing.AOGSource = strings.TrimSpace(*in.AOGSource)
|
||||
}
|
||||
if existing.AirOnGround && strings.TrimSpace(existing.AOGReason) == "" {
|
||||
return nil, &HelicopterWriteError{
|
||||
Status: http.StatusUnprocessableEntity,
|
||||
Title: "AOG reason required",
|
||||
Detail: "AOG reason must be provided when AOG is active (`aog=true`).",
|
||||
Pointer: "/data/attributes/aog_reason",
|
||||
}
|
||||
}
|
||||
if in.AirOnGround != nil && *in.AirOnGround != prevAOG {
|
||||
if err := verifyHelicopterAOGPIN(ctx, in.PINVerifier, in.AccessToken, in.ActorUserID, in.PINToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if in.Note != nil {
|
||||
existing.Note = strings.TrimSpace(*in.Note)
|
||||
}
|
||||
if in.IsActive != nil {
|
||||
existing.IsActive = *in.IsActive
|
||||
}
|
||||
existing.UpdatedBy = append([]byte(nil), in.ActorUserID...)
|
||||
|
||||
if err := s.Update(ctx, existing); err != nil {
|
||||
return nil, &HelicopterWriteError{Status: http.StatusBadRequest, Title: "Update failed", Detail: err.Error()}
|
||||
}
|
||||
updated, err := s.GetByID(ctx, existing.ID)
|
||||
if err != nil {
|
||||
return nil, &HelicopterWriteError{Status: http.StatusBadRequest, Title: "Update failed", Detail: err.Error()}
|
||||
}
|
||||
if updated == nil {
|
||||
updated = existing
|
||||
}
|
||||
isInUse, err := s.IsInUse(ctx, existing.ID)
|
||||
if err != nil {
|
||||
return nil, &HelicopterWriteError{Status: http.StatusBadRequest, Title: "Update failed", Detail: err.Error()}
|
||||
}
|
||||
return &HelicopterWriteResult{Row: updated, IsInUse: isInUse, PreviousFotoAttachmentID: prevFotoAttachmentID}, nil
|
||||
}
|
||||
|
||||
func verifyHelicopterAOGPIN(
|
||||
ctx context.Context,
|
||||
verifier helicopterPINVerifier,
|
||||
accessToken string,
|
||||
userID []byte,
|
||||
token string,
|
||||
) error {
|
||||
if verifier == nil {
|
||||
return &HelicopterWriteError{
|
||||
Status: http.StatusUnauthorized,
|
||||
Title: "Unauthorized",
|
||||
Detail: "auth service not configured",
|
||||
}
|
||||
}
|
||||
if len(userID) == 0 {
|
||||
return &HelicopterWriteError{
|
||||
Status: http.StatusUnauthorized,
|
||||
Title: "Unauthorized",
|
||||
Detail: "missing auth context",
|
||||
}
|
||||
}
|
||||
|
||||
if sessionVerifier, ok := verifier.(helicopterPINSessionVerifier); ok {
|
||||
sessionID := ""
|
||||
if accessToken = strings.TrimSpace(accessToken); accessToken != "" {
|
||||
sid, err := sessionVerifier.ParseAccessTokenSessionID(ctx, accessToken)
|
||||
if err != nil {
|
||||
return &HelicopterWriteError{
|
||||
Status: http.StatusUnauthorized,
|
||||
Title: "Unauthorized",
|
||||
Detail: "invalid or expired token",
|
||||
}
|
||||
}
|
||||
sessionID = sid
|
||||
}
|
||||
|
||||
if err := sessionVerifier.ConsumeSecurityPINActionTokenInSession(ctx, userID, sharedconst.PinActionHelicopterAOGChange, token, sessionID); err != nil {
|
||||
return &HelicopterWriteError{
|
||||
Status: http.StatusAccepted,
|
||||
Title: "PIN verification required",
|
||||
Detail: "valid PIN action token is required",
|
||||
Pointer: "/headers/X-PIN-Verification-Token",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := verifier.ConsumeSecurityPINActionToken(ctx, userID, sharedconst.PinActionHelicopterAOGChange, token); err != nil {
|
||||
return &HelicopterWriteError{
|
||||
Status: http.StatusAccepted,
|
||||
Title: "PIN verification required",
|
||||
Detail: "valid PIN action token is required",
|
||||
Pointer: "/headers/X-PIN-Verification-Token",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeHelicopterIdentifier(raw string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(raw))
|
||||
}
|
||||
|
||||
func cloneIntPtr(v *int) *int {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
out := *v
|
||||
return &out
|
||||
}
|
||||
|
||||
func isHelicopterNotFound(err error) bool {
|
||||
return errors.Is(err, gorm.ErrRecordNotFound)
|
||||
}
|
||||
Reference in New Issue
Block a user