mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-06 08:01:24 +00:00
123 lines
4.3 KiB
Go
123 lines
4.3 KiB
Go
package nativeactions
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/warmbly/warmbly/internal/app/advanced"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
"github.com/warmbly/warmbly/internal/repository"
|
|
)
|
|
|
|
// Adapter satisfies integration.NativeActions, bridging the integration
|
|
// package's automation executor to the advanced/contact/org services. It
|
|
// converts *errx.Error to error (so a nil error stays nil) and resolves the
|
|
// contact + org owner the native CRM/contact actions need. Shared by the
|
|
// backend and consumer binaries, since automation actions run in BOTH (the
|
|
// consumer dispatches reply/bounce/warmup events, the backend the rest).
|
|
type Adapter struct {
|
|
Adv advanced.Service
|
|
Contacts repository.ContactRepository
|
|
Orgs repository.OrganizationRepository
|
|
}
|
|
|
|
func (a Adapter) ResolveContact(ctx context.Context, orgID uuid.UUID, contactID, email string) (*models.Contact, error) {
|
|
// Both lookups are ORG-SCOPED — never resolve a contact id from another org,
|
|
// even if a stale/crafted id reaches the event data.
|
|
if contactID != "" {
|
|
if id, perr := uuid.Parse(contactID); perr == nil {
|
|
if cs, e := a.Contacts.GetByIDsAndOrganization(ctx, orgID, []uuid.UUID{id}); e == nil && len(cs) > 0 {
|
|
return &cs[0], nil
|
|
}
|
|
}
|
|
}
|
|
if email != "" {
|
|
if c, e := a.Contacts.GetByEmailAndOrganization(ctx, orgID, email); e == nil && c != nil {
|
|
return c, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (a Adapter) OrgOwner(ctx context.Context, orgID uuid.UUID) (uuid.UUID, error) {
|
|
org, err := a.Orgs.GetByID(ctx, orgID)
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
if org == nil {
|
|
return uuid.Nil, fmt.Errorf("organization not found")
|
|
}
|
|
return org.OwnerUserID, nil
|
|
}
|
|
|
|
func (a Adapter) AddTag(ctx context.Context, orgID, actorID, contactID, categoryID uuid.UUID) error {
|
|
if _, e := a.Contacts.Update(ctx, actorID.String(), contactID.String(), orgID, &models.UpdateContact{
|
|
AddCategories: []string{categoryID.String()},
|
|
}); e != nil {
|
|
return e
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a Adapter) RemoveTag(ctx context.Context, orgID, actorID, contactID, categoryID uuid.UUID) error {
|
|
if _, e := a.Contacts.Update(ctx, actorID.String(), contactID.String(), orgID, &models.UpdateContact{
|
|
RemoveCategories: []string{categoryID.String()},
|
|
}); e != nil {
|
|
return e
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a Adapter) CreateTask(ctx context.Context, orgID, createdBy uuid.UUID, data *models.CreateCRMTask) error {
|
|
if _, e := a.Adv.CreateContactTask(ctx, orgID, createdBy, data); e != nil {
|
|
return e
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a Adapter) CreateDeal(ctx context.Context, orgID, createdBy uuid.UUID, data *models.CreateDeal) error {
|
|
if _, e := a.Adv.CreateContactDeal(ctx, orgID, createdBy, data); e != nil {
|
|
return e
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a Adapter) MoveDealStage(ctx context.Context, orgID, contactID, pipelineID, stageID uuid.UUID) error {
|
|
if _, e := a.Adv.MoveContactDealStage(ctx, orgID, contactID, pipelineID, stageID); e != nil {
|
|
return e
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a Adapter) Unsubscribe(ctx context.Context, campaignID, contactID uuid.UUID) error {
|
|
if e := a.Adv.Unsubscribe(ctx, campaignID, contactID); e != nil {
|
|
return e
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// LabelThread applies unibox conversation labels to a thread on behalf of the
|
|
// mailbox owner (the advanced service guards category ownership). The error is
|
|
// already a plain error, so it passes straight through.
|
|
func (a Adapter) LabelThread(ctx context.Context, userID uuid.UUID, threadID string, categoryIDs []uuid.UUID) error {
|
|
return a.Adv.LabelThread(ctx, userID, threadID, categoryIDs)
|
|
}
|
|
|
|
// ListCategories / CreateCategory / ListPipelines back the AI agent step's
|
|
// argument-based tag/label/deal tools (the model picks a name, resolved live).
|
|
// Categories are owner-scoped (tags == unibox labels); pipelines are org-scoped
|
|
// with stages hydrated. All three delegate straight to the advanced service.
|
|
func (a Adapter) ListCategories(ctx context.Context, ownerID uuid.UUID) ([]models.MiniCategory, error) {
|
|
return a.Adv.ListCategories(ctx, ownerID)
|
|
}
|
|
|
|
func (a Adapter) CreateCategory(ctx context.Context, ownerID uuid.UUID, title, color string) (models.MiniCategory, error) {
|
|
return a.Adv.CreateCategory(ctx, ownerID, title, color)
|
|
}
|
|
|
|
func (a Adapter) ListPipelines(ctx context.Context, orgID uuid.UUID) ([]models.Pipeline, error) {
|
|
return a.Adv.ListPipelines(ctx, orgID)
|
|
}
|