16bfedb85f
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
181 lines
5.5 KiB
Go
181 lines
5.5 KiB
Go
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
|
|
}
|
|
|
|
// ErrNotDraft — удалить можно только черновик (заявку, которая ещё не уходила в НРД).
|
|
var ErrNotDraft = errors.New("m2mcore: удалить можно только черновик")
|
|
|
|
// DraftDeleter — хранилище умеет удалять черновики (сделка в состоянии draft вместе с событиями).
|
|
type DraftDeleter interface {
|
|
DeleteDraft(ctx context.Context, id string) error
|
|
}
|
|
|
|
// DeleteDraft удаляет сделку, если она в состоянии draft.
|
|
func (r *MemoryRepository) DeleteDraft(_ context.Context, id string) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
d, ok := r.byID[id]
|
|
if !ok {
|
|
return ErrNotFound
|
|
}
|
|
if d.State != StateDraft {
|
|
return ErrNotDraft
|
|
}
|
|
delete(r.byID, id)
|
|
delete(r.byGUID, d.GUID)
|
|
delete(r.events, id)
|
|
return nil
|
|
}
|