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,289 @@
package service
import (
"context"
"errors"
"fmt"
"strings"
"time"
"wucher/internal/domain/flight"
fmreport "wucher/internal/domain/fm_report"
"wucher/internal/shared/pkg/appctx"
"wucher/internal/shared/pkg/uuidv7"
)
const reportCodeMaxAttempts = 5
type FMReportService struct {
repo fmreport.Repository
historyBuilder fmreport.FleetHistoryBuilder
codeResolver fmreport.ReportCodeResolver
usage interface {
RefreshAll(context.Context) error
}
}
func NewFMReportService(repo fmreport.Repository) *FMReportService {
return &FMReportService{repo: repo}
}
func (s *FMReportService) WithFleetHistoryBuilder(builder fmreport.FleetHistoryBuilder) *FMReportService {
s.historyBuilder = builder
return s
}
func (s *FMReportService) WithReportCodeResolver(resolver fmreport.ReportCodeResolver) *FMReportService {
s.codeResolver = resolver
return s
}
func (s *FMReportService) WithUsageRefresher(refresher interface {
RefreshAll(context.Context) error
}) *FMReportService {
s.usage = refresher
return s
}
func (s *FMReportService) resolveHelicopterForReportCode(ctx context.Context, takeoverID []byte) ([]byte, string) {
if s.codeResolver == nil || len(takeoverID) != 16 {
return nil, ""
}
helicopterID, reportPrefix := s.codeResolver.ResolveHelicopterForReportCode(ctx, takeoverID)
reportPrefix = strings.ToUpper(strings.TrimSpace(reportPrefix))
if len(helicopterID) != 16 || reportPrefix == "" {
return nil, ""
}
return helicopterID, reportPrefix
}
func (s *FMReportService) assignReportCode(ctx context.Context, row *fmreport.Report, helicopterID []byte, reportPrefix string) {
if len(helicopterID) != 16 || reportPrefix == "" || len(row.ID) != 16 {
return
}
for attempt := 0; attempt < reportCodeMaxAttempts; attempt++ {
maxSeq, err := s.repo.MaxReportSeqByHelicopter(ctx, helicopterID)
if err != nil {
return
}
code := reportPrefix + "-" + fmt.Sprintf("%04d", maxSeq+1)
if err := s.repo.SetReportCode(ctx, row.ID, code); err != nil {
if errors.Is(err, fmreport.ErrDuplicateReportCode) {
continue
}
return
}
row.ReportCode = &code
return
}
}
func (s *FMReportService) GetFleetHistory(ctx context.Context, reportID []byte) (*fmreport.FleetHistory, error) {
if len(reportID) != 16 {
return nil, fmt.Errorf("report id must be a valid UUID")
}
return s.repo.GetFleetHistoryByReportID(ctx, reportID)
}
func (s *FMReportService) UpsertForFlight(ctx context.Context, flightRow *flight.Flight, flightInspectionID []byte) (*fmreport.Report, error) {
if flightRow == nil || len(flightRow.ID) != 16 {
return nil, fmt.Errorf("flight is required")
}
if len(flightRow.TakeoverAcID) != 16 {
return nil, fmt.Errorf("takeover id is required")
}
if len(flightInspectionID) != 16 {
return nil, fmt.Errorf("flight inspection id is required")
}
row := &fmreport.Report{
FlightID: flightRow.ID,
TakeoverID: flightRow.TakeoverAcID,
FlightInspectionID: flightInspectionID,
}
if userID := appctx.GetUserID(ctx); len(userID) == 16 {
row.CreatedBy = userID
row.UpdatedBy = userID
}
isNew := true
if existing, err := s.repo.GetByFlightID(ctx, flightRow.ID); err == nil && existing != nil && len(existing.ID) == 16 {
isNew = false
if existing.CompletedAt != nil {
return existing, nil
}
row.ID = existing.ID
row.CreatedAt = existing.CreatedAt
row.CreatedBy = existing.CreatedBy
row.HelicopterID = existing.HelicopterID
row.ReportCode = existing.ReportCode
row.Engine1GpcN1 = existing.Engine1GpcN1
row.Engine1PtcN2 = existing.Engine1PtcN2
row.Engine2GpcN1 = existing.Engine2GpcN1
row.Engine2PtcN2 = existing.Engine2PtcN2
}
var helicopterID []byte
var reportPrefix string
if isNew {
helicopterID, reportPrefix = s.resolveHelicopterForReportCode(ctx, flightRow.TakeoverAcID)
if len(helicopterID) == 16 {
row.HelicopterID = helicopterID
}
}
if err := s.repo.Upsert(ctx, row); err != nil {
return nil, fmt.Errorf("failed to upsert fm report: %w", err)
}
if isNew {
s.assignReportCode(ctx, row, helicopterID, reportPrefix)
}
// The fleet-status snapshot is frozen at COMPLETION (see Complete), not here — while the
// report is open its fleet_status is shown live so fleet updates reflect.
return row, nil
}
// snapshotFleetStatus freezes the current fleet status for a report (idempotent: no-op if a
// snapshot already exists for the report).
func (s *FMReportService) snapshotFleetStatus(ctx context.Context, row *fmreport.Report) {
if s.historyBuilder == nil || len(row.ID) != 16 {
return
}
if existing, _ := s.repo.GetFleetHistoryByReportID(ctx, row.ID); existing != nil {
return
}
if history := s.historyBuilder.BuildFleetHistory(ctx, row.TakeoverID); history != nil {
history.FMReportID = row.ID
_ = s.repo.CreateFleetHistory(ctx, history)
}
}
func (s *FMReportService) UpdateForFlight(ctx context.Context, flightID []byte, engine1GpcN1, engine1PtcN2, engine2GpcN1, engine2PtcN2 *string) (*fmreport.Report, error) {
if len(flightID) != 16 {
return nil, fmt.Errorf("flight id must be a valid UUID")
}
row, err := s.repo.GetByFlightID(ctx, flightID)
if err != nil {
return nil, err
}
if row == nil || len(row.ID) != 16 {
return nil, fmreport.ErrNotFound
}
if row.CompletedAt != nil {
return nil, fmreport.ErrCompleted
}
if engine1GpcN1 != nil {
v := strings.TrimSpace(*engine1GpcN1)
row.Engine1GpcN1 = &v
}
if engine1PtcN2 != nil {
v := strings.TrimSpace(*engine1PtcN2)
row.Engine1PtcN2 = &v
}
if engine2GpcN1 != nil {
v := strings.TrimSpace(*engine2GpcN1)
row.Engine2GpcN1 = &v
}
if engine2PtcN2 != nil {
v := strings.TrimSpace(*engine2PtcN2)
row.Engine2PtcN2 = &v
}
if userID := appctx.GetUserID(ctx); len(userID) == 16 {
row.UpdatedBy = userID
}
if err := s.repo.Upsert(ctx, row); err != nil {
return nil, fmt.Errorf("failed to update fm report: %w", err)
}
return row, nil
}
func (s *FMReportService) Complete(ctx context.Context, flightID []byte) (*fmreport.Report, error) {
if len(flightID) != 16 {
return nil, fmt.Errorf("flight id must be a valid UUID")
}
row, err := s.repo.GetByFlightID(ctx, flightID)
if err != nil {
return nil, err
}
if row == nil || len(row.ID) != 16 {
return nil, fmreport.ErrNotFound
}
if row.CompletedAt != nil {
if s.usage != nil {
if err := s.usage.RefreshAll(ctx); err != nil {
return nil, err
}
}
return row, nil
}
now := time.Now().UTC()
row.CompletedAt = &now
if userID := appctx.GetUserID(ctx); len(userID) == 16 {
row.CompletedBy = userID
row.UpdatedBy = userID
}
if err := s.repo.Upsert(ctx, row); err != nil {
return nil, fmt.Errorf("failed to complete fm report: %w", err)
}
// Freeze the fleet-status snapshot at the moment of completion.
s.snapshotFleetStatus(ctx, row)
if s.usage != nil {
if err := s.usage.RefreshAll(ctx); err != nil {
return nil, err
}
}
return row, nil
}
func (s *FMReportService) GetByID(ctx context.Context, id []byte) (*fmreport.Report, error) {
if len(id) != 16 {
return nil, fmt.Errorf("id must be a valid UUID")
}
return s.repo.GetByID(ctx, id)
}
func (s *FMReportService) GetByFlightID(ctx context.Context, flightID []byte) (*fmreport.Report, error) {
if len(flightID) != 16 {
return nil, fmt.Errorf("flight id must be a valid UUID")
}
row, err := s.repo.GetByFlightID(ctx, flightID)
if err != nil {
return nil, err
}
return row, nil
}
func (s *FMReportService) List(ctx context.Context, filter fmreport.ListFilter, limit, offset int) ([]fmreport.Report, int64, error) {
return s.repo.List(ctx, filter, limit, offset)
}
func (s *FMReportService) Delete(ctx context.Context, id []byte) error {
if len(id) != 16 {
return fmt.Errorf("id must be a valid UUID")
}
row, err := s.repo.GetByID(ctx, id)
if err != nil {
return err
}
if row == nil || len(row.ID) != 16 {
return fmreport.ErrNotFound
}
var deletedBy []byte
if userID := appctx.GetUserID(ctx); len(userID) == 16 {
deletedBy = userID
}
return s.repo.Delete(ctx, id, deletedBy)
}
func (s *FMReportService) AttachmentReferenceType() string { return "fm_report" }
func (s *FMReportService) AttachmentReferenceID(row *fmreport.Report) string {
if row == nil {
return ""
}
id, err := uuidv7.BytesToString(row.ID)
if err != nil {
return ""
}
return strings.TrimSpace(id)
}