mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-19 08:01:16 +00:00
266 lines
7.7 KiB
Go
266 lines
7.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/warmbly/warmbly/internal/api/middleware"
|
|
"github.com/warmbly/warmbly/internal/errx"
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
)
|
|
|
|
// requireLeadSyncActor resolves the org + user for a lead-sync request the same
|
|
// way the contact and integration handlers do.
|
|
func (h *Handler) requireLeadSyncActor(c *gin.Context) (orgID, userID uuid.UUID, ok bool) {
|
|
orgID, ok = requireOrgID(c)
|
|
if !ok {
|
|
return uuid.Nil, uuid.Nil, false
|
|
}
|
|
uid, err := uuid.Parse(middleware.GetUserID(c))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.Unauthorized, "invalid user"))
|
|
return uuid.Nil, uuid.Nil, false
|
|
}
|
|
return orgID, uid, true
|
|
}
|
|
|
|
// leadSyncConnectionResponse is the minimal connection shape the lead-sync UI
|
|
// needs to decide whether to show "Connect Google" or the sheet picker.
|
|
type leadSyncConnectionResponse struct {
|
|
Connected bool `json:"connected"`
|
|
Connection *leadSyncConnectionSummary `json:"connection"`
|
|
}
|
|
|
|
type leadSyncConnectionSummary struct {
|
|
ID uuid.UUID `json:"id"`
|
|
ExternalAccountName string `json:"external_account_name"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// GetLeadSyncGoogleConnection reports whether the org has a connected Google
|
|
// account usable for lead-sync (the hidden google_sheets OAuth connection).
|
|
func (h *Handler) GetLeadSyncGoogleConnection(c *gin.Context) {
|
|
orgID, ok := requireOrgID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
conn, err := h.LeadSyncService.Connection(c.Request.Context(), orgID)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.Internal, "failed to resolve google connection"))
|
|
return
|
|
}
|
|
if conn == nil {
|
|
c.JSON(http.StatusOK, leadSyncConnectionResponse{Connected: false, Connection: nil})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, leadSyncConnectionResponse{
|
|
Connected: true,
|
|
Connection: &leadSyncConnectionSummary{
|
|
ID: conn.ID,
|
|
ExternalAccountName: conn.ExternalAccountName,
|
|
Status: string(conn.Status),
|
|
},
|
|
})
|
|
}
|
|
|
|
type leadSyncSpreadsheetPayload struct {
|
|
ConnectionID string `json:"connection_id"`
|
|
SheetID string `json:"sheet_id"`
|
|
}
|
|
|
|
// GetLeadSyncSpreadsheet returns a sheet's title + tabs so the UI can render a
|
|
// tab picker before mapping columns.
|
|
func (h *Handler) GetLeadSyncSpreadsheet(c *gin.Context) {
|
|
orgID, ok := requireOrgID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var p leadSyncSpreadsheetPayload
|
|
if err := c.ShouldBindJSON(&p); err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid payload"))
|
|
return
|
|
}
|
|
connID, err := uuid.Parse(strings.TrimSpace(p.ConnectionID))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid connection_id"))
|
|
return
|
|
}
|
|
if strings.TrimSpace(p.SheetID) == "" {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "sheet_id is required"))
|
|
return
|
|
}
|
|
meta, merr := h.LeadSyncService.SpreadsheetMeta(c.Request.Context(), orgID, connID, strings.TrimSpace(p.SheetID))
|
|
if merr != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "failed to read spreadsheet: "+merr.Error()))
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, meta)
|
|
}
|
|
|
|
type leadSyncPreviewPayload struct {
|
|
ConnectionID string `json:"connection_id"`
|
|
SheetID string `json:"sheet_id"`
|
|
TabTitle string `json:"tab_title"`
|
|
}
|
|
|
|
// PreviewLeadSync returns an ImportPreview-shaped payload (columns, sample_rows,
|
|
// total_rows, has_header, suggested_mapping) so the frontend reuses its
|
|
// contact-import column mapper verbatim.
|
|
func (h *Handler) PreviewLeadSync(c *gin.Context) {
|
|
orgID, ok := requireOrgID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var p leadSyncPreviewPayload
|
|
if err := c.ShouldBindJSON(&p); err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid payload"))
|
|
return
|
|
}
|
|
connID, err := uuid.Parse(strings.TrimSpace(p.ConnectionID))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid connection_id"))
|
|
return
|
|
}
|
|
preview, xerr := h.LeadSyncService.Preview(c.Request.Context(), orgID, connID, strings.TrimSpace(p.SheetID), strings.TrimSpace(p.TabTitle))
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, preview)
|
|
}
|
|
|
|
// ListLeadSyncSources lists this org's saved sources, optionally filtered to a
|
|
// campaign via ?campaign_id=.
|
|
func (h *Handler) ListLeadSyncSources(c *gin.Context) {
|
|
orgID, ok := requireOrgID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var campaignID *uuid.UUID
|
|
if raw := strings.TrimSpace(c.Query("campaign_id")); raw != "" {
|
|
id, err := uuid.Parse(raw)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid campaign_id"))
|
|
return
|
|
}
|
|
campaignID = &id
|
|
}
|
|
sources, err := h.LeadSyncService.List(c.Request.Context(), orgID, campaignID)
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.Internal, "failed to list sync sources"))
|
|
return
|
|
}
|
|
if sources == nil {
|
|
sources = []models.LeadSyncSource{}
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"data": sources})
|
|
}
|
|
|
|
// CreateLeadSyncSource saves a new on-demand sync source.
|
|
func (h *Handler) CreateLeadSyncSource(c *gin.Context) {
|
|
orgID, userID, ok := h.requireLeadSyncActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
var in models.CreateLeadSyncSource
|
|
if err := c.ShouldBindJSON(&in); err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid payload"))
|
|
return
|
|
}
|
|
src, xerr := h.LeadSyncService.Create(c.Request.Context(), orgID, userID, &in)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
h.auditOrg(c, models.AuditActionCreate, models.AuditEntityLeadSyncSource, &src.ID, nil, map[string]string{"sheet": src.SheetTitle})
|
|
c.JSON(http.StatusCreated, src)
|
|
}
|
|
|
|
// GetLeadSyncSource returns one saved source.
|
|
func (h *Handler) GetLeadSyncSource(c *gin.Context) {
|
|
orgID, ok := requireOrgID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid id"))
|
|
return
|
|
}
|
|
src, xerr := h.LeadSyncService.Get(c.Request.Context(), orgID, id)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, src)
|
|
}
|
|
|
|
// UpdateLeadSyncSource edits a saved source.
|
|
func (h *Handler) UpdateLeadSyncSource(c *gin.Context) {
|
|
orgID, ok := requireOrgID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid id"))
|
|
return
|
|
}
|
|
var in models.UpdateLeadSyncSource
|
|
if berr := c.ShouldBindJSON(&in); berr != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid payload"))
|
|
return
|
|
}
|
|
src, xerr := h.LeadSyncService.Update(c.Request.Context(), orgID, id, &in)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
h.auditOrg(c, models.AuditActionUpdate, models.AuditEntityLeadSyncSource, &id, nil, nil)
|
|
c.JSON(http.StatusOK, src)
|
|
}
|
|
|
|
// DeleteLeadSyncSource removes a saved source.
|
|
func (h *Handler) DeleteLeadSyncSource(c *gin.Context) {
|
|
orgID, ok := requireOrgID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid id"))
|
|
return
|
|
}
|
|
if xerr := h.LeadSyncService.Delete(c.Request.Context(), orgID, id); xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
h.auditOrg(c, models.AuditActionDelete, models.AuditEntityLeadSyncSource, &id, nil, nil)
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// SyncLeadSyncSourceNow runs the source on-demand: read the sheet -> upsert
|
|
// contacts via ImportCommit. Naturally idempotent (email upsert), so retries
|
|
// are safe without an Idempotency-Key.
|
|
func (h *Handler) SyncLeadSyncSourceNow(c *gin.Context) {
|
|
orgID, userID, ok := h.requireLeadSyncActor(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
id, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
errx.JSON(c, errx.New(errx.BadRequest, "invalid id"))
|
|
return
|
|
}
|
|
result, xerr := h.LeadSyncService.SyncNow(c.Request.Context(), userID, orgID, id)
|
|
if xerr != nil {
|
|
errx.JSON(c, xerr)
|
|
return
|
|
}
|
|
h.auditOrg(c, models.AuditActionImport, models.AuditEntityLeadSyncSource, &id, nil, map[string]string{"trigger": "manual_sync"})
|
|
c.JSON(http.StatusOK, result)
|
|
}
|