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,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)
}