package ledger import ( "context" "strings" domain "gl/domain/ledger" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" ) type AuthorityRepository interface { OperatingMode(context.Context) (domain.OperatingState, error) SetOperatingMode(context.Context, domain.OperatingMode, int64, string, string, bool) (domain.OperatingState, error) UserBalances(context.Context, string, string, *int64) ([]domain.UserAssetBalance, error) } func (s *Service) OperatingMode(ctx context.Context) (domain.OperatingState, error) { repository, ok := s.repository.(AuthorityRepository) if !ok { return domain.OperatingState{}, domain.ErrModeRequired } return repository.OperatingMode(ctx) } func (s *Service) SetOperatingMode(ctx context.Context, mode domain.OperatingMode, revision int64, actor, reason string, reconciled bool) (domain.OperatingState, error) { if (mode != domain.ModeNormal && mode != domain.ModeGLOnly) || revision < 0 || strings.TrimSpace(actor) == "" || strings.TrimSpace(reason) == "" { return domain.OperatingState{}, invalid("mode, nonnegative revision, actor, and reason are required") } repository, ok := s.repository.(AuthorityRepository) if !ok { return domain.OperatingState{}, domain.ErrModeRequired } ctx, span := otel.Tracer("gl/ledger").Start(ctx, "ledger.set_operating_mode") defer span.End() span.SetAttributes(attribute.String("ledger.mode", string(mode)), attribute.Int64("ledger.expected_revision", revision), attribute.String("ledger.actor_id", actor)) state, err := repository.SetOperatingMode(ctx, mode, revision, actor, reason, reconciled) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "mode change failed") return domain.OperatingState{}, err } if state.Revision != revision { span.AddEvent("ledger.operating_mode_changed") } span.SetAttributes(attribute.Int64("ledger.revision", state.Revision)) return state, nil } func (s *Service) UserBalances(ctx context.Context, ownerType, ownerID string, assetID *int64) ([]domain.UserAssetBalance, error) { if strings.TrimSpace(ownerType) == "" || strings.TrimSpace(ownerID) == "" || (assetID != nil && *assetID <= 0) { return nil, invalid("owner type, owner id, and a positive optional asset id are required") } repository, ok := s.repository.(AuthorityRepository) if !ok { return nil, domain.ErrNotFound } return repository.UserBalances(ctx, ownerType, ownerID, assetID) } // PostInternalJournal executes exact balanced entries without network settlement. // PostgreSQL rechecks mode and funds under locks in the same journal transaction. func (s *Service) PostInternalJournal(ctx context.Context, command AppendJournalCommand) (AppendJournalResult, error) { if strings.TrimSpace(command.ActorID) == "" { return AppendJournalResult{}, invalid("actor id is required for internal execution") } if command.Blockchain != (domain.BlockchainReference{}) { return AppendJournalResult{}, invalid("internal execution cannot claim blockchain settlement") } for _, entry := range command.Entries { if entry.Account.Class == domain.AccountClassExternalBlockchain { return AppendJournalResult{}, invalid("external blockchain movements cannot execute internally") } } command.Metadata = cloneMetadata(command.Metadata) command.Metadata[domain.InternalExecutionMetadata] = string(domain.ModeGLOnly) return s.appendJournal(ctx, command) }