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 DeletedAt gorm.DeletedAt `gorm:"index"` 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 } 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 }