package playback import ( "context" "time" "github.com/google/uuid" "pipu/api/internal/api/gen" "pipu/api/internal/auth" ) // EventService owns event acceptance and its transaction; HTTP only authenticates and maps DTOs. type EventService struct { store *Store now func() time.Time } func (s EventService) Accept(ctx context.Context, p auth.Principal, events []gen.PlayEvent) (gen.PostEvents200JSONResponse, error) { now := s.now() ids := make([]uuid.UUID, 0, len(events)) for _, e := range events { ids = append(ids, e.TrackId) } durations, err := s.store.TrackDurations(ctx, ids) if err != nil { return gen.PostEvents200JSONResponse{}, err } clientType := s.store.SessionClientType(ctx, p.SessionID) out := gen.PostEvents200JSONResponse{} tx, err := s.store.Begin(ctx) if err != nil { return gen.PostEvents200JSONResponse{}, err } defer func() { _ = tx.Rollback(ctx) }() for _, e := range events { dur, known := durations[e.TrackId] at := e.OccurredAt.UTC() if !known || at.Before(now.Add(-maxEventAge)) { out.Rejected++ continue } if at.After(now.Add(maxEventClockSkew)) { at = now // часы клиента спешат } ev := Event{ ID: e.Id, Type: string(e.Type), TrackID: e.TrackId, OccurredAt: at, PositionMs: e.PositionMs, PlayedMs: e.PlayedMs, ContextID: e.ContextId, WaveSessionID: e.WaveSessionId, Offline: e.Offline != nil && *e.Offline, } if e.ContextType != nil { ct := string(*e.ContextType) ev.ContextType = &ct } inserted, err := s.store.InsertEvent(ctx, tx, p.UserID, clientType, ev) if err != nil { return gen.PostEvents200JSONResponse{}, err } if !inserted { out.Duplicates++ continue } out.Accepted++ played := 0 if e.PlayedMs != nil { played = *e.PlayedMs } o := Judge(ev.Type, played, dur) if o != (Outcome{}) { if err := s.store.ApplyOutcome(ctx, tx, p.UserID, ev, o); err != nil { return gen.PostEvents200JSONResponse{}, err } } if outcome := impressionOutcome(ev.Type, o); outcome != "" && ev.ContextType != nil && *ev.ContextType == "wave" && ev.ContextID != nil { if sid, err := uuid.Parse(*ev.ContextID); err == nil { if err := s.store.MarkImpression(ctx, tx, sid, ev.TrackID, outcome); err != nil { return gen.PostEvents200JSONResponse{}, err } } } } if err := tx.Commit(ctx); err != nil { return gen.PostEvents200JSONResponse{}, err } return out, nil }