443 lines
14 KiB
Go
443 lines
14 KiB
Go
package models
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type UserRole string
|
|
|
|
const (
|
|
ROLE_ADMIN UserRole = "ADMIN"
|
|
ROLE_CLIENT UserRole = "CLIENT"
|
|
)
|
|
|
|
type User struct {
|
|
ID uint `gorm:"primaryKey"`
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
|
|
NameClient string `gorm:"not null"`
|
|
Password string `gorm:"not null"` // hashé avec bcrypt
|
|
Email string `gorm:"uniqueIndex;not null"`
|
|
SSOID string `gorm:"uniqueIndex;not null"`
|
|
Role UserRole `gorm:"type:enum('ADMIN','CLIENT');default:'CLIENT';not null"`
|
|
IsActive bool `gorm:"default:true"`
|
|
|
|
WhatsappToken string `gorm:"type:text"` // token Meta spécifique au client
|
|
WhatsappPhoneNumberID string `gorm:"type:varchar(50)"` // ID du numéro WhatsApp Business
|
|
|
|
|
|
MonthlyCredits uint `gorm:"default:100"`
|
|
CurrentMonthCredits uint `gorm:"default:100"`
|
|
LastRecharge *time.Time `gorm:"default:null"`
|
|
|
|
Consumption []Consumption
|
|
MonthlyConsumptions []MonthlyConsumption
|
|
}
|
|
|
|
|
|
type Consumption struct {
|
|
ID uint `gorm:"primaryKey"`
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
|
|
UserID uint `gorm:"index;not null"`
|
|
MessageType string `gorm:"type:varchar(20);not null"` // ex: text, image, button
|
|
Description string `gorm:"type:text"`
|
|
CreditsUsed uint `gorm:"not null"`
|
|
}
|
|
|
|
type MonthlyConsumption struct {
|
|
ID uint `gorm:"primaryKey"`
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
|
|
UserID uint `gorm:"index;not null"`
|
|
Month string `gorm:"type:char(7);index;not null"` // Format YYYY-MM
|
|
TotalUsed uint `gorm:"not null"` // Crédits utilisés ce mois
|
|
}
|
|
type Template struct {
|
|
Name string `json:"name"`
|
|
Language TemplateLanguage `json:"language"`
|
|
Components []TemplateComponent `json:"components"`
|
|
}
|
|
|
|
type TemplateLanguage struct {
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
type TemplateComponent struct {
|
|
Type string `json:"type"`
|
|
Parameters []TemplateParameter `json:"parameters"`
|
|
}
|
|
|
|
type TemplateParameter struct {
|
|
Type string `json:"type"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
type WhatsappMessage struct {
|
|
MessagingProduct string `json:"messaging_product"` // always "whatsapp"
|
|
To string `json:"to"` // recipient phone number
|
|
Type string `json:"type"` // message type (text, image, video, etc.)
|
|
Context *Context `json:"context,omitempty"` // for replies
|
|
RecipientType string `json:"recipient_type,omitempty"` // optional
|
|
Text *Text `json:"text,omitempty"`
|
|
Image *Media `json:"image,omitempty"`
|
|
Video *Media `json:"video,omitempty"`
|
|
Document *Media `json:"document,omitempty"`
|
|
Audio *Media `json:"audio,omitempty"`
|
|
Sticker *Media `json:"sticker,omitempty"`
|
|
Location *Location `json:"location,omitempty"`
|
|
Contacts []Contact `json:"contacts,omitempty"`
|
|
Interactive *Interactive `json:"interactive,omitempty"`
|
|
Reaction *Reaction `json:"reaction,omitempty"`
|
|
Template *Template `json:"template,omitempty"` // ✅ Ajoute ce champ
|
|
}
|
|
|
|
type Address struct {
|
|
Street string `json:"street,omitempty"`
|
|
City string `json:"city,omitempty"`
|
|
State string `json:"state,omitempty"`
|
|
Zip string `json:"zip,omitempty"`
|
|
Country string `json:"country,omitempty"`
|
|
CountryCode string `json:"country_code,omitempty"`
|
|
Type string `json:"type,omitempty"` // home, work
|
|
}
|
|
type Contact struct {
|
|
Addresses []Address `json:"addresses,omitempty"`
|
|
Name *Name `json:"name,omitempty"`
|
|
Emails []Email `json:"emails,omitempty"`
|
|
Phones []Phone `json:"phones,omitempty"`
|
|
}
|
|
|
|
type Name struct {
|
|
FormattedName string `json:"formatted_name"`
|
|
FirstName string `json:"first_name,omitempty"`
|
|
LastName string `json:"last_name,omitempty"`
|
|
}
|
|
|
|
type Email struct {
|
|
Email string `json:"email"`
|
|
Type string `json:"type,omitempty"` // work, home
|
|
}
|
|
|
|
type Phone struct {
|
|
Phone string `json:"phone"`
|
|
Type string `json:"type,omitempty"`
|
|
}
|
|
type Media struct {
|
|
ID string `json:"id,omitempty"` // media ID from upload
|
|
Link string `json:"link,omitempty"` // direct URL
|
|
Caption string `json:"caption,omitempty"` // for image/video
|
|
Filename string `json:"filename,omitempty"` // for documents
|
|
MimeType string `json:"mime_type,omitempty"`
|
|
}
|
|
type Location struct {
|
|
Latitude float64 `json:"latitude"`
|
|
Longitude float64 `json:"longitude"`
|
|
Name string `json:"name,omitempty"`
|
|
Address string `json:"address,omitempty"`
|
|
Url string `json:"url,omitempty"`
|
|
IsLive bool `json:"is_live,omitempty"` // si demande de localisation en direct
|
|
}
|
|
type Text struct {
|
|
Body string `json:"body"`
|
|
PreviewURL bool `json:"preview_url,omitempty"`
|
|
}
|
|
type Context struct {
|
|
MessageID string `json:"message_id"`
|
|
}
|
|
type Reaction struct {
|
|
MessageID string `json:"message_id"`
|
|
Emoji string `json:"emoji"`
|
|
}
|
|
type StatusIndicator struct {
|
|
Status string `json:"status"` // "read" or "typing"
|
|
To string `json:"to"`
|
|
}
|
|
type Interactive struct {
|
|
Type string `json:"type"` // button | list | flow
|
|
Body *BodyContent `json:"body"`
|
|
Action *Action `json:"action"`
|
|
}
|
|
|
|
type BodyContent struct {
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
type Action struct {
|
|
Button string `json:"button,omitempty"`
|
|
Buttons []ReplyButton `json:"buttons,omitempty"`
|
|
Sections []Section `json:"sections,omitempty"` // for list
|
|
CatalogID string `json:"catalog_id,omitempty"` // for flow
|
|
ProductID string `json:"product_retailer_id,omitempty"` // for flow
|
|
}
|
|
|
|
type ReplyButton struct {
|
|
Type string `json:"type"` // reply
|
|
Reply ButtonReply `json:"reply"`
|
|
}
|
|
|
|
type ButtonReply struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
}
|
|
|
|
type Section struct {
|
|
Title string `json:"title,omitempty"`
|
|
Rows []ListItem `json:"rows"`
|
|
}
|
|
|
|
type ListItem struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description,omitempty"`
|
|
}
|
|
type StatusMessage struct {
|
|
MessagingProduct string `json:"messaging_product"`
|
|
To string `json:"to"`
|
|
Type string `json:"type"` // "typing_on", "typing_off", "read"
|
|
}
|
|
type Conversation struct {
|
|
ID uint `gorm:"primaryKey"`
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
UserID uint `gorm:"index;not null"`
|
|
From string `gorm:"not null"`
|
|
To string `gorm:"not null"`
|
|
MessageID string `gorm:"uniqueIndex;not null"`
|
|
Type string `gorm:"not null"`
|
|
Content string `gorm:"type:text"`
|
|
Direction string `gorm:"not null"` // inbound / outbound
|
|
Status string `gorm:"type:varchar(20)"` // sent, delivered, read, failed
|
|
ParentID *uint `gorm:"index"` // lien vers le message parent (nullable)
|
|
}
|
|
|
|
|
|
func NewTextMessage(to string, body string) WhatsappMessage {
|
|
return WhatsappMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "text",
|
|
Text: &Text{Body: body},
|
|
}
|
|
}
|
|
|
|
func NewImageMessage(to, mediaID, caption string) WhatsappMessage {
|
|
return WhatsappMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "image",
|
|
Image: &Media{ID: mediaID, Caption: caption},
|
|
}
|
|
}
|
|
|
|
func NewVideoMessage(to, mediaID, caption string) WhatsappMessage {
|
|
return WhatsappMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "video",
|
|
Video: &Media{ID: mediaID, Caption: caption},
|
|
}
|
|
}
|
|
|
|
func NewAudioMessage(to, mediaID string) WhatsappMessage {
|
|
return WhatsappMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "audio",
|
|
Audio: &Media{ID: mediaID},
|
|
}
|
|
}
|
|
|
|
func NewDocumentMessage(to, mediaID, filename string) WhatsappMessage {
|
|
return WhatsappMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "document",
|
|
Document: &Media{ID: mediaID, Filename: filename},
|
|
}
|
|
}
|
|
|
|
func NewStickerMessage(to, mediaID string) WhatsappMessage {
|
|
return WhatsappMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "sticker",
|
|
Sticker: &Media{ID: mediaID},
|
|
}
|
|
}
|
|
|
|
func NewContactMessage(to string, contact Contact) WhatsappMessage {
|
|
return WhatsappMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "contacts",
|
|
Contacts: []Contact{contact},
|
|
}
|
|
}
|
|
|
|
func NewLocationMessage(to string, lat, lng float64, name, address string) WhatsappMessage {
|
|
return WhatsappMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "location",
|
|
Location: &Location{Latitude: lat, Longitude: lng, Name: name, Address: address},
|
|
}
|
|
}
|
|
|
|
func NewReactionMessage(to, msgID, emoji string) WhatsappMessage {
|
|
return WhatsappMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "reaction",
|
|
Reaction: &Reaction{MessageID: msgID, Emoji: emoji},
|
|
}
|
|
}
|
|
|
|
func NewReplyMessage(to, msgID, body string) WhatsappMessage {
|
|
return WhatsappMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "text",
|
|
Context: &Context{MessageID: msgID},
|
|
Text: &Text{Body: body},
|
|
}
|
|
}
|
|
|
|
func NewInteractiveButtons(to string, text string, buttons []ReplyButton) WhatsappMessage {
|
|
return WhatsappMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "interactive",
|
|
Interactive: &Interactive{Type: "button", Body: &BodyContent{Text: text}, Action: &Action{Buttons: buttons}},
|
|
}
|
|
}
|
|
|
|
func NewInteractiveList(to string, text string, sections []Section) WhatsappMessage {
|
|
return WhatsappMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "interactive",
|
|
Interactive: &Interactive{Type: "list", Body: &BodyContent{Text: text}, Action: &Action{Sections: sections}},
|
|
}
|
|
}
|
|
|
|
func NewFlowMessage(to, text, catalogID, productID string) WhatsappMessage {
|
|
return WhatsappMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "interactive",
|
|
Interactive: &Interactive{Type: "flow", Body: &BodyContent{Text: text}, Action: &Action{CatalogID: catalogID, ProductID: productID}},
|
|
}
|
|
}
|
|
|
|
func NewTypingIndicator(to string) StatusMessage {
|
|
return StatusMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "typing_on",
|
|
}
|
|
}
|
|
|
|
func NewReadReceipt(to string) StatusMessage {
|
|
return StatusMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "read",
|
|
}
|
|
}
|
|
func BuildMessageFromPayload(payload map[string]interface{}) (interface{}, error) {
|
|
to := fmt.Sprintf("%v", payload["to"])
|
|
typeStr := fmt.Sprintf("%v", payload["type"])
|
|
|
|
switch typeStr {
|
|
case "text":
|
|
if text, ok := payload["text"].(map[string]interface{}); ok {
|
|
return NewTextMessage(to, fmt.Sprintf("%v", text["body"])), nil
|
|
}
|
|
case "image":
|
|
if image, ok := payload["image"].(map[string]interface{}); ok {
|
|
return NewImageMessage(to, fmt.Sprintf("%v", image["id"]), fmt.Sprintf("%v", image["caption"])), nil
|
|
}
|
|
case "video":
|
|
if video, ok := payload["video"].(map[string]interface{}); ok {
|
|
return NewVideoMessage(to, fmt.Sprintf("%v", video["id"]), fmt.Sprintf("%v", video["caption"])), nil
|
|
}
|
|
case "audio":
|
|
if audio, ok := payload["audio"].(map[string]interface{}); ok {
|
|
return NewAudioMessage(to, fmt.Sprintf("%v", audio["id"])), nil
|
|
}
|
|
case "document":
|
|
if doc, ok := payload["document"].(map[string]interface{}); ok {
|
|
return NewDocumentMessage(to, fmt.Sprintf("%v", doc["id"]), fmt.Sprintf("%v", doc["filename"])), nil
|
|
}
|
|
case "sticker":
|
|
if st, ok := payload["sticker"].(map[string]interface{}); ok {
|
|
return NewStickerMessage(to, fmt.Sprintf("%v", st["id"])), nil
|
|
}
|
|
case "location":
|
|
if loc, ok := payload["location"].(map[string]interface{}); ok {
|
|
lat := loc["latitude"].(float64)
|
|
lng := loc["longitude"].(float64)
|
|
return NewLocationMessage(to, lat, lng, fmt.Sprintf("%v", loc["name"]), fmt.Sprintf("%v", loc["address"])), nil
|
|
}
|
|
case "reaction":
|
|
if react, ok := payload["reaction"].(map[string]interface{}); ok {
|
|
return NewReactionMessage(to, fmt.Sprintf("%v", react["message_id"]), fmt.Sprintf("%v", react["emoji"])), nil
|
|
}
|
|
case "typing_on", "typing_off", "read":
|
|
return StatusMessage{MessagingProduct: "whatsapp", To: to, Type: typeStr}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported message type: %s", typeStr)
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to build message")
|
|
}
|
|
func NewTemplateMessage(to, name, lang string, params []string) WhatsappMessage {
|
|
paramList := make([]TemplateParameter, len(params))
|
|
for i, p := range params {
|
|
paramList[i] = TemplateParameter{
|
|
Type: "text",
|
|
Text: p,
|
|
}
|
|
}
|
|
|
|
return WhatsappMessage{
|
|
MessagingProduct: "whatsapp",
|
|
To: to,
|
|
Type: "template",
|
|
Template: &Template{
|
|
Name: name,
|
|
Language: TemplateLanguage{Code: lang},
|
|
Components: []TemplateComponent{
|
|
{
|
|
Type: "body",
|
|
Parameters: paramList,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Enregistre une erreur de statut dans la table Conversation
|
|
func SaveMessageStatusError(db *gorm.DB, userID uint, recipient, msgID, status, errorMsg string) error {
|
|
conv := Conversation{
|
|
UserID: userID,
|
|
From: "system",
|
|
To: recipient,
|
|
MessageID: msgID,
|
|
Type: "status",
|
|
Content: errorMsg,
|
|
Direction: "outbound",
|
|
Status: status,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
return db.Create(&conv).Error
|
|
}
|
|
|