278 lines
6.5 KiB
Go
278 lines
6.5 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
|
|
"apps/backend/internal/module/crm/domain"
|
|
)
|
|
|
|
type Memory struct {
|
|
mu sync.Mutex
|
|
contacts map[string]*domain.Contact
|
|
identity map[string]string // owner|platform|handle → id
|
|
touches map[string]*domain.ContactTouch
|
|
followups map[string]*domain.FollowUp
|
|
}
|
|
|
|
func NewMemory() *Memory {
|
|
return &Memory{
|
|
contacts: map[string]*domain.Contact{},
|
|
identity: map[string]string{},
|
|
touches: map[string]*domain.ContactTouch{},
|
|
followups: map[string]*domain.FollowUp{},
|
|
}
|
|
}
|
|
|
|
func idKey(owner int64, platform, handle string) string {
|
|
return fmt.Sprintf("%d|%s|%s", owner, platform, handle)
|
|
}
|
|
|
|
func (m *Memory) UpsertContactByIdentity(_ context.Context, c *domain.Contact) (*domain.Contact, error) {
|
|
if err := c.Normalize(); err != nil {
|
|
return nil, err
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
key := idKey(c.OwnerUID, c.SourcePlatform, c.AuthorHandle)
|
|
if id, ok := m.identity[key]; ok {
|
|
ex := m.contacts[id]
|
|
// merge opportunity ids
|
|
seen := map[string]bool{}
|
|
for _, x := range ex.OpportunityIDs {
|
|
seen[x] = true
|
|
}
|
|
for _, x := range c.OpportunityIDs {
|
|
if x != "" && !seen[x] {
|
|
ex.OpportunityIDs = append(ex.OpportunityIDs, x)
|
|
}
|
|
}
|
|
if c.TopIntentScore > ex.TopIntentScore {
|
|
ex.TopIntentScore = c.TopIntentScore
|
|
ex.TopIntentBand = c.TopIntentBand
|
|
}
|
|
ex.UpdatedAt = domain.NowNano()
|
|
cp := *ex
|
|
return &cp, nil
|
|
}
|
|
if c.ID == "" {
|
|
c.ID = domain.NewID()
|
|
}
|
|
now := domain.NowNano()
|
|
if c.CreatedAt == 0 {
|
|
c.CreatedAt = now
|
|
}
|
|
c.UpdatedAt = now
|
|
cp := *c
|
|
cp.OpportunityIDs = append([]string(nil), c.OpportunityIDs...)
|
|
m.contacts[cp.ID] = &cp
|
|
m.identity[key] = cp.ID
|
|
out := cp
|
|
out.OpportunityIDs = append([]string(nil), cp.OpportunityIDs...)
|
|
return &out, nil
|
|
}
|
|
|
|
func (m *Memory) GetContact(_ context.Context, id string) (*domain.Contact, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
c, ok := m.contacts[id]
|
|
if !ok {
|
|
return nil, domain.ErrNotFound
|
|
}
|
|
cp := *c
|
|
cp.OpportunityIDs = append([]string(nil), c.OpportunityIDs...)
|
|
cp.MergedFrom = append([]string(nil), c.MergedFrom...)
|
|
return &cp, nil
|
|
}
|
|
|
|
func (m *Memory) SaveContact(_ context.Context, c *domain.Contact) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
cp := *c
|
|
cp.OpportunityIDs = append([]string(nil), c.OpportunityIDs...)
|
|
cp.MergedFrom = append([]string(nil), c.MergedFrom...)
|
|
m.contacts[c.ID] = &cp
|
|
m.identity[idKey(c.OwnerUID, c.SourcePlatform, c.AuthorHandle)] = c.ID
|
|
return nil
|
|
}
|
|
|
|
func (m *Memory) ListContacts(_ context.Context, ownerUID int64, f domain.ContactListFilter) ([]*domain.Contact, int64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
matched := make([]*domain.Contact, 0)
|
|
for _, c := range m.contacts {
|
|
if c.OwnerUID != ownerUID {
|
|
continue
|
|
}
|
|
if f.Stage != "" && c.Stage != f.Stage {
|
|
continue
|
|
}
|
|
if f.FollowUp != nil && c.NeedsFollowUp != *f.FollowUp {
|
|
continue
|
|
}
|
|
if f.Band != "" && c.TopIntentBand != f.Band {
|
|
continue
|
|
}
|
|
cp := *c
|
|
cp.OpportunityIDs = append([]string(nil), c.OpportunityIDs...)
|
|
matched = append(matched, &cp)
|
|
}
|
|
sort.Slice(matched, func(i, j int) bool {
|
|
if f.Sort == "intent_score" {
|
|
if matched[i].TopIntentScore != matched[j].TopIntentScore {
|
|
return matched[i].TopIntentScore > matched[j].TopIntentScore
|
|
}
|
|
}
|
|
if matched[i].LastTouchAt != matched[j].LastTouchAt {
|
|
return matched[i].LastTouchAt > matched[j].LastTouchAt
|
|
}
|
|
return matched[i].CreatedAt > matched[j].CreatedAt
|
|
})
|
|
total := int64(len(matched))
|
|
page, ps := f.Page, f.PageSize
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if ps < 1 {
|
|
ps = 20
|
|
}
|
|
start := (page - 1) * ps
|
|
if start >= len(matched) {
|
|
return nil, total, nil
|
|
}
|
|
end := start + ps
|
|
if end > len(matched) {
|
|
end = len(matched)
|
|
}
|
|
return matched[start:end], total, nil
|
|
}
|
|
|
|
func (m *Memory) CountByStage(_ context.Context, ownerUID int64) (map[string]int, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
out := map[string]int{}
|
|
follow := 0
|
|
for _, c := range m.contacts {
|
|
if c.OwnerUID != ownerUID {
|
|
continue
|
|
}
|
|
out[c.Stage]++
|
|
if c.NeedsFollowUp {
|
|
follow++
|
|
}
|
|
}
|
|
out["needs_follow_up"] = follow
|
|
return out, nil
|
|
}
|
|
|
|
func (m *Memory) InsertTouch(_ context.Context, t *domain.ContactTouch) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if t.ID == "" {
|
|
t.ID = domain.NewID()
|
|
}
|
|
cp := *t
|
|
m.touches[t.ID] = &cp
|
|
return nil
|
|
}
|
|
|
|
func (m *Memory) ListTouches(_ context.Context, ownerUID int64, contactID string, page, pageSize int) ([]*domain.ContactTouch, int64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
matched := make([]*domain.ContactTouch, 0)
|
|
for _, t := range m.touches {
|
|
if t.OwnerUID == ownerUID && t.ContactID == contactID {
|
|
cp := *t
|
|
matched = append(matched, &cp)
|
|
}
|
|
}
|
|
sort.Slice(matched, func(i, j int) bool { return matched[i].CreatedAt > matched[j].CreatedAt })
|
|
total := int64(len(matched))
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if pageSize < 1 {
|
|
pageSize = 20
|
|
}
|
|
start := (page - 1) * pageSize
|
|
if start >= len(matched) {
|
|
return nil, total, nil
|
|
}
|
|
end := start + pageSize
|
|
if end > len(matched) {
|
|
end = len(matched)
|
|
}
|
|
return matched[start:end], total, nil
|
|
}
|
|
|
|
func (m *Memory) SaveFollowUp(_ context.Context, f *domain.FollowUp) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
cp := *f
|
|
m.followups[f.ID] = &cp
|
|
return nil
|
|
}
|
|
|
|
func (m *Memory) GetFollowUp(_ context.Context, id string) (*domain.FollowUp, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
f, ok := m.followups[id]
|
|
if !ok {
|
|
return nil, domain.ErrNotFound
|
|
}
|
|
cp := *f
|
|
return &cp, nil
|
|
}
|
|
|
|
func (m *Memory) ListFollowUps(_ context.Context, ownerUID int64, f domain.FollowUpListFilter) ([]*domain.FollowUp, int64, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
matched := make([]*domain.FollowUp, 0)
|
|
for _, x := range m.followups {
|
|
if x.OwnerUID != ownerUID {
|
|
continue
|
|
}
|
|
if f.Status != "" && x.Status != f.Status {
|
|
continue
|
|
}
|
|
cp := *x
|
|
matched = append(matched, &cp)
|
|
}
|
|
sort.Slice(matched, func(i, j int) bool { return matched[i].DueAt < matched[j].DueAt })
|
|
total := int64(len(matched))
|
|
page, ps := f.Page, f.PageSize
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if ps < 1 {
|
|
ps = 20
|
|
}
|
|
start := (page - 1) * ps
|
|
if start >= len(matched) {
|
|
return nil, total, nil
|
|
}
|
|
end := start + ps
|
|
if end > len(matched) {
|
|
end = len(matched)
|
|
}
|
|
return matched[start:end], total, nil
|
|
}
|
|
|
|
func (m *Memory) ListDueFollowUps(_ context.Context, now int64, limit int) ([]*domain.FollowUp, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
out := make([]*domain.FollowUp, 0)
|
|
for _, x := range m.followups {
|
|
if (x.Status == domain.FollowUpScheduled || x.Status == domain.FollowUpSnoozed) && x.DueAt <= now {
|
|
cp := *x
|
|
out = append(out, &cp)
|
|
}
|
|
}
|
|
if limit > 0 && len(out) > limit {
|
|
out = out[:limit]
|
|
}
|
|
return out, nil
|
|
}
|