227 lines
5.9 KiB
Go
227 lines
5.9 KiB
Go
|
|
package repository
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
|
||
|
|
libmongo "apps/backend/internal/lib/mongo"
|
||
|
|
"apps/backend/internal/module/crm/domain"
|
||
|
|
|
||
|
|
"github.com/zeromicro/go-zero/core/stores/mon"
|
||
|
|
"go.mongodb.org/mongo-driver/bson"
|
||
|
|
"go.mongodb.org/mongo-driver/mongo"
|
||
|
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||
|
|
)
|
||
|
|
|
||
|
|
type MonStore struct {
|
||
|
|
contacts *mon.Model
|
||
|
|
touches *mon.Model
|
||
|
|
followups *mon.Model
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewMonStore(uri, database string) *MonStore {
|
||
|
|
uri = libmongo.MustMongoURI(uri)
|
||
|
|
return &MonStore{
|
||
|
|
contacts: mon.MustNewModel(uri, database, "crm_contacts"),
|
||
|
|
touches: mon.MustNewModel(uri, database, "crm_touches"),
|
||
|
|
followups: mon.MustNewModel(uri, database, "crm_followups"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MonStore) UpsertContactByIdentity(ctx context.Context, c *domain.Contact) (*domain.Contact, error) {
|
||
|
|
if err := c.Normalize(); err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
filter := bson.M{
|
||
|
|
"owner_uid": c.OwnerUID,
|
||
|
|
"source_platform": c.SourcePlatform,
|
||
|
|
"author_handle": c.AuthorHandle,
|
||
|
|
}
|
||
|
|
var existing domain.Contact
|
||
|
|
err := s.contacts.FindOne(ctx, &existing, filter)
|
||
|
|
if err == nil {
|
||
|
|
// merge
|
||
|
|
seen := map[string]bool{}
|
||
|
|
for _, id := range existing.OpportunityIDs {
|
||
|
|
seen[id] = true
|
||
|
|
}
|
||
|
|
for _, id := range c.OpportunityIDs {
|
||
|
|
if id != "" && !seen[id] {
|
||
|
|
existing.OpportunityIDs = append(existing.OpportunityIDs, id)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if c.TopIntentScore > existing.TopIntentScore {
|
||
|
|
existing.TopIntentScore = c.TopIntentScore
|
||
|
|
existing.TopIntentBand = c.TopIntentBand
|
||
|
|
}
|
||
|
|
existing.UpdatedAt = domain.NowNano()
|
||
|
|
_, err = s.contacts.ReplaceOne(ctx, bson.M{"_id": existing.ID}, &existing)
|
||
|
|
return &existing, err
|
||
|
|
}
|
||
|
|
if err != mon.ErrNotFound {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if c.ID == "" {
|
||
|
|
c.ID = domain.NewID()
|
||
|
|
}
|
||
|
|
now := domain.NowNano()
|
||
|
|
if c.CreatedAt == 0 {
|
||
|
|
c.CreatedAt = now
|
||
|
|
}
|
||
|
|
c.UpdatedAt = now
|
||
|
|
_, err = s.contacts.InsertOne(ctx, c)
|
||
|
|
if err != nil && mongo.IsDuplicateKeyError(err) {
|
||
|
|
return s.UpsertContactByIdentity(ctx, c)
|
||
|
|
}
|
||
|
|
return c, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MonStore) GetContact(ctx context.Context, id string) (*domain.Contact, error) {
|
||
|
|
var c domain.Contact
|
||
|
|
err := s.contacts.FindOne(ctx, &c, bson.M{"_id": id})
|
||
|
|
if err == mon.ErrNotFound {
|
||
|
|
return nil, domain.ErrNotFound
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return &c, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MonStore) SaveContact(ctx context.Context, c *domain.Contact) error {
|
||
|
|
_, err := s.contacts.ReplaceOne(ctx, bson.M{"_id": c.ID}, c, options.Replace().SetUpsert(true))
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MonStore) ListContacts(ctx context.Context, ownerUID int64, f domain.ContactListFilter) ([]*domain.Contact, int64, error) {
|
||
|
|
q := bson.M{"owner_uid": ownerUID}
|
||
|
|
if f.Stage != "" {
|
||
|
|
q["stage"] = f.Stage
|
||
|
|
}
|
||
|
|
if f.FollowUp != nil {
|
||
|
|
q["needs_follow_up"] = *f.FollowUp
|
||
|
|
}
|
||
|
|
if f.Band != "" {
|
||
|
|
q["top_intent_band"] = f.Band
|
||
|
|
}
|
||
|
|
total, err := s.contacts.CountDocuments(ctx, q)
|
||
|
|
if err != nil {
|
||
|
|
return nil, 0, err
|
||
|
|
}
|
||
|
|
page, ps := f.Page, f.PageSize
|
||
|
|
if page < 1 {
|
||
|
|
page = 1
|
||
|
|
}
|
||
|
|
if ps < 1 {
|
||
|
|
ps = 20
|
||
|
|
}
|
||
|
|
sortKey := "last_touch_at"
|
||
|
|
if f.Sort == "intent_score" {
|
||
|
|
sortKey = "top_intent_score"
|
||
|
|
}
|
||
|
|
var list []*domain.Contact
|
||
|
|
err = s.contacts.Find(ctx, &list, q, options.Find().
|
||
|
|
SetSort(bson.D{{Key: sortKey, Value: -1}}).
|
||
|
|
SetSkip(int64((page-1)*ps)).
|
||
|
|
SetLimit(int64(ps)))
|
||
|
|
return list, total, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MonStore) CountByStage(ctx context.Context, ownerUID int64) (map[string]int, error) {
|
||
|
|
// simple: list all and count (contacts per owner are bounded)
|
||
|
|
list, _, err := s.ListContacts(ctx, ownerUID, domain.ContactListFilter{Page: 1, PageSize: 500})
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
out := map[string]int{}
|
||
|
|
follow := 0
|
||
|
|
for _, c := range list {
|
||
|
|
out[c.Stage]++
|
||
|
|
if c.NeedsFollowUp {
|
||
|
|
follow++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
out["needs_follow_up"] = follow
|
||
|
|
return out, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MonStore) InsertTouch(ctx context.Context, t *domain.ContactTouch) error {
|
||
|
|
if t.ID == "" {
|
||
|
|
t.ID = domain.NewID()
|
||
|
|
}
|
||
|
|
_, err := s.touches.InsertOne(ctx, t)
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MonStore) ListTouches(ctx context.Context, ownerUID int64, contactID string, page, pageSize int) ([]*domain.ContactTouch, int64, error) {
|
||
|
|
q := bson.M{"owner_uid": ownerUID, "contact_id": contactID}
|
||
|
|
total, err := s.touches.CountDocuments(ctx, q)
|
||
|
|
if err != nil {
|
||
|
|
return nil, 0, err
|
||
|
|
}
|
||
|
|
if page < 1 {
|
||
|
|
page = 1
|
||
|
|
}
|
||
|
|
if pageSize < 1 {
|
||
|
|
pageSize = 20
|
||
|
|
}
|
||
|
|
var list []*domain.ContactTouch
|
||
|
|
err = s.touches.Find(ctx, &list, q, options.Find().
|
||
|
|
SetSort(bson.D{{Key: "created_at", Value: -1}}).
|
||
|
|
SetSkip(int64((page-1)*pageSize)).
|
||
|
|
SetLimit(int64(pageSize)))
|
||
|
|
return list, total, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MonStore) SaveFollowUp(ctx context.Context, f *domain.FollowUp) error {
|
||
|
|
_, err := s.followups.ReplaceOne(ctx, bson.M{"_id": f.ID}, f, options.Replace().SetUpsert(true))
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MonStore) GetFollowUp(ctx context.Context, id string) (*domain.FollowUp, error) {
|
||
|
|
var f domain.FollowUp
|
||
|
|
err := s.followups.FindOne(ctx, &f, bson.M{"_id": id})
|
||
|
|
if err == mon.ErrNotFound {
|
||
|
|
return nil, domain.ErrNotFound
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return &f, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MonStore) ListFollowUps(ctx context.Context, ownerUID int64, f domain.FollowUpListFilter) ([]*domain.FollowUp, int64, error) {
|
||
|
|
q := bson.M{"owner_uid": ownerUID}
|
||
|
|
if f.Status != "" {
|
||
|
|
q["status"] = f.Status
|
||
|
|
}
|
||
|
|
total, err := s.followups.CountDocuments(ctx, q)
|
||
|
|
if err != nil {
|
||
|
|
return nil, 0, err
|
||
|
|
}
|
||
|
|
page, ps := f.Page, f.PageSize
|
||
|
|
if page < 1 {
|
||
|
|
page = 1
|
||
|
|
}
|
||
|
|
if ps < 1 {
|
||
|
|
ps = 20
|
||
|
|
}
|
||
|
|
var list []*domain.FollowUp
|
||
|
|
err = s.followups.Find(ctx, &list, q, options.Find().
|
||
|
|
SetSort(bson.D{{Key: "due_at", Value: 1}}).
|
||
|
|
SetSkip(int64((page-1)*ps)).
|
||
|
|
SetLimit(int64(ps)))
|
||
|
|
return list, total, err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s *MonStore) ListDueFollowUps(ctx context.Context, now int64, limit int) ([]*domain.FollowUp, error) {
|
||
|
|
if limit <= 0 {
|
||
|
|
limit = 50
|
||
|
|
}
|
||
|
|
var list []*domain.FollowUp
|
||
|
|
err := s.followups.Find(ctx, &list, bson.M{
|
||
|
|
"status": bson.M{"$in": []string{domain.FollowUpScheduled, domain.FollowUpSnoozed}},
|
||
|
|
"due_at": bson.M{"$lte": now},
|
||
|
|
}, options.Find().SetLimit(int64(limit)))
|
||
|
|
return list, err
|
||
|
|
}
|