package m2mcore import ( "context" "errors" "sync" "time" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/m2m" ) // ErrNotFound возвращается, когда сделка не найдена. var ErrNotFound = errors.New("m2mcore: сделка не найдена") // Filter описывает фильтры выборки сделок. type Filter struct { State *State InvestorID string CreatedFrom *time.Time CreatedTo *time.Time Limit int Offset int } // Repository — порт хранилища сделок. type Repository interface { // Create вставляет сделку с идемпотентностью по GUID: повторный вызов // для существующего GUID возвращает уже существующую сделку (без // модификации). Create(ctx context.Context, deal *Deal) (*Deal, error) // GetByGUID находит сделку по M2M GUID. GetByGUID(ctx context.Context, guid m2m.UUID) (*Deal, error) // GetByID находит сделку по внутреннему UUID. GetByID(ctx context.Context, id string) (*Deal, error) // Update сохраняет изменения сделки. Update(ctx context.Context, deal *Deal) error // List возвращает сделки по фильтру. List(ctx context.Context, f Filter) ([]*Deal, error) // AppendEvent добавляет аудит-событие к сделке. AppendEvent(ctx context.Context, dealID string, ev Event) error } // MemoryRepository — in-memory реализация Repository для тестов и // dev-стенда без PostgreSQL. type MemoryRepository struct { mu sync.RWMutex byID map[string]*Deal byGUID map[m2m.UUID]string events map[string][]Event } // NewMemoryRepository создаёт пустое in-memory хранилище. func NewMemoryRepository() *MemoryRepository { return &MemoryRepository{ byID: make(map[string]*Deal), byGUID: make(map[m2m.UUID]string), events: make(map[string][]Event), } } // Create вставляет сделку или возвращает существующую по GUID. func (r *MemoryRepository) Create(_ context.Context, deal *Deal) (*Deal, error) { r.mu.Lock() defer r.mu.Unlock() if id, ok := r.byGUID[deal.GUID]; ok { return r.byID[id], nil } r.byID[deal.ID] = deal r.byGUID[deal.GUID] = deal.ID return deal, nil } // GetByGUID возвращает сделку по GUID или ErrNotFound. func (r *MemoryRepository) GetByGUID(_ context.Context, guid m2m.UUID) (*Deal, error) { r.mu.RLock() defer r.mu.RUnlock() id, ok := r.byGUID[guid] if !ok { return nil, ErrNotFound } return r.byID[id], nil } // GetByID возвращает сделку по внутреннему ID или ErrNotFound. func (r *MemoryRepository) GetByID(_ context.Context, id string) (*Deal, error) { r.mu.RLock() defer r.mu.RUnlock() d, ok := r.byID[id] if !ok { return nil, ErrNotFound } return d, nil } // Update в in-memory импликации no-op: указатель уже хранится. func (r *MemoryRepository) Update(_ context.Context, deal *Deal) error { r.mu.Lock() defer r.mu.Unlock() if _, ok := r.byID[deal.ID]; !ok { return ErrNotFound } return nil } // List перебирает сделки и фильтрует на лету. func (r *MemoryRepository) List(_ context.Context, f Filter) ([]*Deal, error) { r.mu.RLock() defer r.mu.RUnlock() out := make([]*Deal, 0) for _, d := range r.byID { if f.State != nil && d.State != *f.State { continue } if f.InvestorID != "" && d.InvestorID != f.InvestorID { continue } if f.CreatedFrom != nil && d.CreatedAt.Before(*f.CreatedFrom) { continue } if f.CreatedTo != nil && d.CreatedAt.After(*f.CreatedTo) { continue } out = append(out, d) } if f.Offset > 0 && f.Offset < len(out) { out = out[f.Offset:] } else if f.Offset >= len(out) { return nil, nil } if f.Limit > 0 && f.Limit < len(out) { out = out[:f.Limit] } return out, nil } // AppendEvent добавляет событие в журнал сделки. func (r *MemoryRepository) AppendEvent(_ context.Context, dealID string, ev Event) error { r.mu.Lock() defer r.mu.Unlock() if _, ok := r.byID[dealID]; !ok { return ErrNotFound } r.events[dealID] = append(r.events[dealID], ev) return nil } // EventsOf возвращает все события сделки (только для тестов и дев-логов). func (r *MemoryRepository) EventsOf(dealID string) []Event { r.mu.RLock() defer r.mu.RUnlock() src := r.events[dealID] out := make([]Event, len(src)) copy(out, src) return out }