- Add auth handlers (signup, login, logout, account management) with bcrypt - Add client, customer, service, scheduling, payment, question, answer handlers - Add dashboard, monthly report, and lead pipeline pages - Add UTF-8 middleware to force charset on HTML responses - Add config package with env-based overrides for DB path, secrets, endpoints - Add parser package for WhatsApp message ingestion - Add clean-arch layers: pkg/domain, pkg/repo, pkg/usecase for leads - Add cmd/migrate utility for DB migrations - Add Makefile, README, run-tests.sh, and dev scripts - Update docker-compose.yml with memory limits - Update .air.toml to exclude DB files and stop on errors - Update whatsapp-sync dependencies and add src/index.js entrypoint - Add whatsme standalone WhatsApp reader app (source only) - Untrack .opencode-sandbox/data/go-crm.db from git history - Expand root .gitignore: ngrok, tmp dirs, sandbox DBs, compiled binaries
74 lines
2.0 KiB
Go
74 lines
2.0 KiB
Go
// Package parser provides message text parsing utilities for the CRM.
|
|
// It extracts structured lead information from raw WhatsApp message text.
|
|
package parser
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"unicode"
|
|
|
|
"golang.org/x/text/unicode/norm"
|
|
)
|
|
|
|
// ExtractService scans msg for any keyword in the mapping (case-insensitive,
|
|
// accent-insensitive) and returns the corresponding service name.
|
|
// If no keyword matches, it returns "Não especificou".
|
|
//
|
|
// mapping is a map[keyword]serviceName loaded from the service_keywords table.
|
|
func ExtractService(msg string, mapping map[string]string) string {
|
|
normalized := normalizeText(msg)
|
|
|
|
// Sort by keyword length descending so longer phrases match before shorter ones.
|
|
// We iterate the map which has no order, so we do two passes:
|
|
// first collect all matches, then pick the longest keyword match.
|
|
type match struct {
|
|
keyword string
|
|
service string
|
|
}
|
|
var best match
|
|
|
|
for kw, svc := range mapping {
|
|
normKW := normalizeText(kw)
|
|
if strings.Contains(normalized, normKW) {
|
|
if len(kw) > len(best.keyword) {
|
|
best = match{keyword: kw, service: svc}
|
|
}
|
|
}
|
|
}
|
|
|
|
if best.service != "" {
|
|
return best.service
|
|
}
|
|
return "Não especificou"
|
|
}
|
|
|
|
// NormalizePhone converts a raw WhatsApp phone string to E.164 format (+countrycode...).
|
|
// It strips the @s.whatsapp.net suffix, removes non-digit characters, and prepends "+".
|
|
func NormalizePhone(raw string) string {
|
|
// Strip WhatsApp JID suffix.
|
|
if idx := strings.Index(raw, "@"); idx != -1 {
|
|
raw = raw[:idx]
|
|
}
|
|
// Keep only digits.
|
|
digits := regexp.MustCompile(`\D`).ReplaceAllString(raw, "")
|
|
if digits == "" {
|
|
return raw
|
|
}
|
|
return "+" + digits
|
|
}
|
|
|
|
// normalizeText lowercases and removes accents from s for fuzzy comparison.
|
|
func normalizeText(s string) string {
|
|
// NFD decomposition splits accented characters into base + combining marks.
|
|
t := norm.NFD.String(strings.ToLower(s))
|
|
// Remove combining marks (Unicode category Mn).
|
|
var b strings.Builder
|
|
for _, r := range t {
|
|
if unicode.Is(unicode.Mn, r) {
|
|
continue
|
|
}
|
|
b.WriteRune(r)
|
|
}
|
|
return b.String()
|
|
}
|