mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-26 08:00:42 +00:00
203 lines
6.5 KiB
Go
203 lines
6.5 KiB
Go
package integration
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/warmbly/warmbly/internal/models"
|
|
)
|
|
|
|
// project.go turns a Warmbly source record into provider destination properties
|
|
// using the connection's configured field map — this is the pivot that makes the
|
|
// CRM handlers config-driven instead of hardcoding props["firstname"]. When no
|
|
// map is configured the provider's default identity map is used, so an
|
|
// unconfigured connection behaves exactly as it did before the framework.
|
|
|
|
// projectFields applies a resolved field map to a source payload, producing
|
|
// {externalField: value}. Empty values are dropped (matching provider upsert
|
|
// semantics: don't overwrite with blanks).
|
|
func projectFields(entries []models.FieldMapEntry, source map[string]any) map[string]any {
|
|
out := map[string]any{}
|
|
for _, e := range entries {
|
|
var val string
|
|
if e.Transform == models.FieldTransformStatic {
|
|
// Static values support {{.key}} templating against the event data.
|
|
val = renderTemplate(e.StaticValue, source)
|
|
} else {
|
|
val = applyTransform(e.Transform, warmblyValue(source, e.WarmblyField))
|
|
}
|
|
if strings.TrimSpace(val) == "" {
|
|
continue
|
|
}
|
|
out[e.ExternalField] = val
|
|
}
|
|
return out
|
|
}
|
|
|
|
// warmblyValue reads a source field. "custom:<key>" reads from the contact's
|
|
// custom_fields map; everything else is a top-level normalized field.
|
|
func warmblyValue(source map[string]any, field string) string {
|
|
if strings.HasPrefix(field, "custom:") {
|
|
key := strings.TrimPrefix(field, "custom:")
|
|
switch cf := source["custom_fields"].(type) {
|
|
case map[string]string:
|
|
return strings.TrimSpace(cf[key])
|
|
case map[string]any:
|
|
if v, ok := cf[key].(string); ok {
|
|
return strings.TrimSpace(v)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
return stringFromMap(source, field)
|
|
}
|
|
|
|
func applyTransform(t models.FieldTransform, v string) string {
|
|
switch t {
|
|
case models.FieldTransformUppercase:
|
|
return strings.ToUpper(v)
|
|
case models.FieldTransformLowercase:
|
|
return strings.ToLower(v)
|
|
case models.FieldTransformTrim:
|
|
return strings.TrimSpace(v)
|
|
default:
|
|
return v
|
|
}
|
|
}
|
|
|
|
// defaultObject is the CRM object an upsert action writes for a provider.
|
|
func defaultObject(provider models.IntegrationProvider) string {
|
|
switch provider {
|
|
case models.IntegrationPipedrive:
|
|
return "person"
|
|
case models.IntegrationClose:
|
|
return "lead"
|
|
default:
|
|
return "contact"
|
|
}
|
|
}
|
|
|
|
// defaultFieldMap is the provider's identity projection — the standard fields the
|
|
// handlers used to hardcode. Configured mappings overlay this.
|
|
func defaultFieldMap(provider models.IntegrationProvider) []models.FieldMapEntry {
|
|
switch provider {
|
|
case models.IntegrationHubSpot:
|
|
return []models.FieldMapEntry{
|
|
{WarmblyField: "email", ExternalField: "email"},
|
|
{WarmblyField: "first_name", ExternalField: "firstname"},
|
|
{WarmblyField: "last_name", ExternalField: "lastname"},
|
|
{WarmblyField: "company", ExternalField: "company"},
|
|
{WarmblyField: "phone", ExternalField: "phone"},
|
|
}
|
|
case models.IntegrationSalesforce:
|
|
return []models.FieldMapEntry{
|
|
{WarmblyField: "email", ExternalField: "Email"},
|
|
{WarmblyField: "first_name", ExternalField: "FirstName"},
|
|
{WarmblyField: "last_name", ExternalField: "LastName"},
|
|
{WarmblyField: "phone", ExternalField: "Phone"},
|
|
}
|
|
case models.IntegrationPipedrive:
|
|
return []models.FieldMapEntry{
|
|
{WarmblyField: "name", ExternalField: "name"},
|
|
{WarmblyField: "email", ExternalField: "email"},
|
|
{WarmblyField: "phone", ExternalField: "phone"},
|
|
}
|
|
case models.IntegrationClose:
|
|
return []models.FieldMapEntry{
|
|
{WarmblyField: "name", ExternalField: "name"},
|
|
{WarmblyField: "email", ExternalField: "email"},
|
|
{WarmblyField: "phone", ExternalField: "phone"},
|
|
{WarmblyField: "company", ExternalField: "company"},
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// effectiveFieldMap resolves the final field map for an object with precedence:
|
|
// provider defaults < connection-default rows (subscription_id NULL) <
|
|
// subscription-scoped rows < inline subscription config. Later entries override
|
|
// earlier ones keyed by destination field; original order is preserved.
|
|
func effectiveFieldMap(provider models.IntegrationProvider, object string, rows []models.IntegrationFieldMapping, subID string, inline []models.FieldMapEntry) []models.FieldMapEntry {
|
|
byExt := map[string]models.FieldMapEntry{}
|
|
order := []string{}
|
|
add := func(e models.FieldMapEntry) {
|
|
if e.ExternalField == "" {
|
|
return
|
|
}
|
|
if _, seen := byExt[e.ExternalField]; !seen {
|
|
order = append(order, e.ExternalField)
|
|
}
|
|
byExt[e.ExternalField] = e
|
|
}
|
|
|
|
for _, e := range defaultFieldMap(provider) {
|
|
add(e)
|
|
}
|
|
rowToEntry := func(r models.IntegrationFieldMapping) models.FieldMapEntry {
|
|
return models.FieldMapEntry{
|
|
WarmblyField: r.WarmblyField,
|
|
ExternalField: r.ExternalField,
|
|
Transform: models.FieldTransform(r.Transform),
|
|
StaticValue: r.StaticValue,
|
|
}
|
|
}
|
|
for _, r := range rows { // connection defaults
|
|
if r.ObjectName == object && r.SubscriptionID == nil {
|
|
add(rowToEntry(r))
|
|
}
|
|
}
|
|
if subID != "" {
|
|
for _, r := range rows { // subscription-scoped
|
|
if r.ObjectName == object && r.SubscriptionID != nil && r.SubscriptionID.String() == subID {
|
|
add(rowToEntry(r))
|
|
}
|
|
}
|
|
}
|
|
for _, e := range inline {
|
|
add(e)
|
|
}
|
|
|
|
out := make([]models.FieldMapEntry, 0, len(order))
|
|
for _, k := range order {
|
|
out = append(out, byExt[k])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// eventSource normalizes an event/dispatch payload into the Warmbly source field
|
|
// vocabulary the field map reads from.
|
|
func eventSource(data map[string]any) map[string]any {
|
|
src := map[string]any{
|
|
"email": stringFromMap(data, "contact_email", "invitee_email", "email", "recipient"),
|
|
"first_name": stringFromMap(data, "first_name", "contact_first_name"),
|
|
"last_name": stringFromMap(data, "last_name", "contact_last_name"),
|
|
"name": stringFromMap(data, "contact_name", "invitee_name", "name"),
|
|
"company": stringFromMap(data, "company", "contact_company"),
|
|
"phone": stringFromMap(data, "phone", "contact_phone"),
|
|
}
|
|
if cf, ok := data["custom_fields"]; ok {
|
|
src["custom_fields"] = cf
|
|
}
|
|
return src
|
|
}
|
|
|
|
// contactEmail extracts the dedupe email from a dispatch payload.
|
|
func contactEmail(data map[string]any) string {
|
|
return stringFromMap(data, "contact_email", "invitee_email", "email", "recipient")
|
|
}
|
|
|
|
// strProp reads a string value from a projected props map.
|
|
func strProp(props map[string]any, key string) string {
|
|
if v, ok := props[key].(string); ok {
|
|
return strings.TrimSpace(v)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// toStr coerces a projected value (always a string today) to string.
|
|
func toStr(v any) string {
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
return ""
|
|
}
|