infoalloggi-monorepo/apps/backend/miogest_handler.go

515 lines
15 KiB
Go
Raw Normal View History

2025-08-04 17:45:44 +02:00
package main
import (
"backend/config"
"backend/parser"
"backend/typesdefs"
"backend/utils"
2025-08-04 17:45:44 +02:00
"fmt"
"log"
"reflect"
"regexp"
"slices"
"strconv"
2025-08-04 17:45:44 +02:00
"strings"
"time"
)
type MiogestHandler struct {
Caratterisiche typesdefs.ListaCaratteristiche
Categorie []typesdefs.Categoria
AnnunciXML typesdefs.AnnunciXML
AnnunciParsed typesdefs.AnnunciParsed
2025-08-04 17:45:44 +02:00
annunci_cache_expires time.Time
vars_cache_expires time.Time
}
func NewMiogestHandler() *MiogestHandler {
return &MiogestHandler{
Caratterisiche: typesdefs.ListaCaratteristiche{},
2025-08-04 17:45:44 +02:00
Categorie: nil,
AnnunciXML: typesdefs.AnnunciXML{},
AnnunciParsed: typesdefs.AnnunciParsed{},
2025-08-04 17:45:44 +02:00
annunci_cache_expires: time.Now(),
vars_cache_expires: time.Now(),
}
}
// ANNUNCI FROM MIOGEST
func (m *MiogestHandler) GetAnnunci() (typesdefs.AnnunciXML, error) {
2025-08-04 17:45:44 +02:00
if len(m.AnnunciXML.Annuncio) == 0 || time.Now().After(m.annunci_cache_expires) {
err := m.GetAnnunciFromMiogest()
if err != nil {
return typesdefs.AnnunciXML{}, err
}
2025-08-04 17:45:44 +02:00
}
return m.AnnunciXML, nil
2025-08-04 17:45:44 +02:00
}
func (m *MiogestHandler) GetAnnunciFromMiogest() error {
maxRetries := 3
baseDelay := 10 * time.Second
for attempt := range maxRetries {
var err error
m.AnnunciXML, err = utils.GetDataXML[typesdefs.AnnunciXML]("http://partner.miogest.com/agenzie/infoalloggi.xml")
if err == nil {
// Success - validate we got data
if len(m.AnnunciXML.Annuncio) > 0 {
m.annunci_cache_expires = time.Now().Add(1 * time.Hour)
return nil
}
log.Printf("Warning: Successfully fetched XML but got 0 annunci")
}
// Log the specific error type
if err != nil {
log.Printf("Failed to get annunci (attempt %d/%d): %v", attempt+1, maxRetries, err)
}
// Failed - check if we should retry
if attempt < maxRetries-1 {
delay := baseDelay * time.Duration(1<<attempt) // Exponential backoff
log.Printf("Failed to get annunci (attempt %d/%d): %v. Retrying in %v...",
attempt+1, maxRetries, err, delay)
time.Sleep(delay)
} else {
log.Printf("Failed to get annunci after %d attempts: %v", maxRetries, err)
return fmt.Errorf("failed to get annunci after %d attempts: %w", maxRetries, err)
}
}
return nil
2025-08-04 17:45:44 +02:00
}
// CATEGORIE AND CARATTERISTICHE FROM MIOGEST
func (m *MiogestHandler) InitDefaults() {
m.GenerateCaratteristiche()
m.GenerateCategorie()
}
func (m *MiogestHandler) GetCaratteristiche() typesdefs.ListaCaratteristiche {
2025-08-04 17:45:44 +02:00
if len(m.Caratterisiche.Caratteristiche) == 0 || time.Now().After(m.vars_cache_expires) {
m.GenerateCaratteristiche()
}
return m.Caratterisiche
}
func (m *MiogestHandler) GetCategorie() []typesdefs.Categoria {
2025-08-04 17:45:44 +02:00
if len(m.Categorie) == 0 || time.Now().After(m.vars_cache_expires) {
m.GenerateCategorie()
}
return m.Categorie
}
func (m *MiogestHandler) GenerateCaratteristiche() {
//https://www.miogest.com/apps/revo.aspx?tipo=schede
var result, err = utils.GetXMLFromFile[typesdefs.DefaultCaratteristiche]("./caratteristiche.xml")
if err != nil {
log.Printf("Failed to get caratteristiche: %v", err)
return
}
var array typesdefs.ListaCaratteristiche
2025-08-04 17:45:44 +02:00
for _, tag := range result.Scheda {
var c = typesdefs.Caratteristica{Id: tag.Id, Tagxml: tag.Tagxml}
2025-08-04 17:45:44 +02:00
array.Caratteristiche = append(array.Caratteristiche, c)
}
m.Caratterisiche = array
m.vars_cache_expires = time.Now().Add(1 * time.Hour)
}
func (m *MiogestHandler) GenerateCategorie() {
//https://www.miogest.com/apps/revo.aspx?tipo=categorie
var result, err = utils.GetXMLFromFile[typesdefs.DefaultCategories]("./categorie.xml")
if err != nil {
log.Printf("Failed to get categorie: %v", err)
return
}
var listaCategorie []typesdefs.Categoria
2025-08-04 17:45:44 +02:00
for _, elem := range result.Cat {
var c = typesdefs.Categoria{Id: elem.ID, Nome: elem.Nome}
2025-08-04 17:45:44 +02:00
listaCategorie = append(listaCategorie, c)
}
m.Categorie = listaCategorie
m.vars_cache_expires = time.Now().Add(1 * time.Hour)
}
// ANNUNCI PARSED
func (m *MiogestHandler) GetAnnunciParsed(imgPool *[]typesdefs.MediaToProcess, videoPool *[]typesdefs.MediaToProcess, forceMediaStale bool) (typesdefs.AnnunciParsed, error) {
// salvo il parsing nello struct nel caso voglio implementare un caching e non fare il parsing ad ogni chiamata
var err error
m.AnnunciParsed, err = m.ParseAnnunci("", imgPool, videoPool, forceMediaStale)
return m.AnnunciParsed, err
2025-08-04 17:45:44 +02:00
}
// versione per singolo annuncio
func (m *MiogestHandler) GetAnnuncioParsed(codFilter string, imgPool *[]typesdefs.MediaToProcess, videoPool *[]typesdefs.MediaToProcess) (typesdefs.AnnunciParsed, error) {
data, err := m.ParseAnnunci(codFilter, imgPool, videoPool, false)
if err != nil {
return typesdefs.AnnunciParsed{}, err
}
if len(data.Annuncio) == 0 {
return typesdefs.AnnunciParsed{}, fmt.Errorf("No Annuncio found for codice %s", codFilter)
}
return data, nil
}
func (m *MiogestHandler) ParseAnnunci(codFilter string, imgPool *[]typesdefs.MediaToProcess, videoPool *[]typesdefs.MediaToProcess, forceMediaStale bool) (typesdefs.AnnunciParsed, error) {
annunci, err := m.GetAnnunci()
if err != nil {
log.Fatalf("Failed to get annunci for parsing: %v", err.Error())
return typesdefs.AnnunciParsed{}, err
}
if len(annunci.Annuncio) == 0 {
return typesdefs.AnnunciParsed{}, fmt.Errorf("No Annunci in update")
}
if codFilter != "" {
idx := slices.IndexFunc(annunci.Annuncio, func(a typesdefs.AnnuncioXML) bool {
return *a.Codice == codFilter
})
if idx == -1 {
return typesdefs.AnnunciParsed{}, fmt.Errorf("Codice %s not found", codFilter)
}
annunci.Annuncio = []typesdefs.AnnuncioXML{annunci.Annuncio[idx]}
}
image_codes_to_process := []string{}
if config.Cfg.Images.Enabled {
2025-08-04 17:45:44 +02:00
var err error
if forceMediaStale {
// process all images
image_codes_to_process = make([]string, 0, len(annunci.Annuncio))
for _, annuncio := range annunci.Annuncio {
if annuncio.Codice != nil {
image_codes_to_process = append(image_codes_to_process, *annuncio.Codice)
}
}
} else {
// process only new or updated images
image_codes_to_process, err = CodiciToProcessImages(&annunci)
if err != nil {
log.Fatalf("Failed to find images to update: %v", err.Error())
}
2025-08-04 17:45:44 +02:00
}
if codFilter != "" {
// if processing single codice, ensure it's in the list
if !slices.Contains(image_codes_to_process, codFilter) {
image_codes_to_process = append(image_codes_to_process, codFilter)
}
}
err = ReconcileImages()
err = ForceClearImages(image_codes_to_process)
if err != nil {
log.Fatalf("Failed to clear images for codice %s: %v", codFilter, err.Error())
2025-08-04 17:45:44 +02:00
}
}
video_codes_to_process := []string{}
if config.Cfg.Videos.Enabled {
var err error
if forceMediaStale {
// process all videos
video_codes_to_process = make([]string, 0, len(annunci.Annuncio))
for _, annuncio := range annunci.Annuncio {
if annuncio.Codice != nil {
video_codes_to_process = append(video_codes_to_process, *annuncio.Codice)
}
}
} else {
// process only new or updated videos
video_codes_to_process, err = CodiciToProcessVideos(&annunci)
if err != nil {
log.Fatalf("Failed to find videos to update: %v", err.Error())
}
}
if codFilter != "" {
// if processing single codice, ensure it's in the list
if !slices.Contains(video_codes_to_process, codFilter) {
video_codes_to_process = append(video_codes_to_process, codFilter)
}
}
err = ReconcileVideos()
err = ForceClearVideos(video_codes_to_process)
if err != nil {
log.Fatalf("Failed to clear videos for codice %s: %v", codFilter, err.Error())
}
2025-08-04 17:45:44 +02:00
}
totalAnnunci := len(annunci.Annuncio)
log.Printf("Processing %d annunci (images to update: %d, videos to update: %d)", totalAnnunci, len(image_codes_to_process), len(video_codes_to_process))
var AnnunciArray typesdefs.AnnunciParsed
AnnunciArray.Annuncio = make([]typesdefs.AnnuncioParsed, 0, len(annunci.Annuncio))
m.InitDefaults()
2025-08-04 17:45:44 +02:00
for i := range annunci.Annuncio {
imgToUpdate := slices.Contains(image_codes_to_process, *annunci.Annuncio[i].Codice)
updateVideos := slices.Contains(video_codes_to_process, *annunci.Annuncio[i].Codice)
result := extractData_update(&annunci.Annuncio[i], imgToUpdate, updateVideos, m, imgPool, videoPool)
AnnunciArray.Annuncio = append(AnnunciArray.Annuncio, result)
2025-08-04 17:45:44 +02:00
}
log.Printf("Processed %d annunci", len(AnnunciArray.Annuncio))
fmt.Println()
return AnnunciArray, nil
2025-08-04 17:45:44 +02:00
}
var re = regexp.MustCompile(`[^+\d]`)
func boolUpd(b bool) *bool {
return &b
}
func strUpd(s string) *string {
return &s
}
func extractData_update(annuncio *typesdefs.AnnuncioXML, updateImages bool, updateVideos bool, m *MiogestHandler, imgPool *[]typesdefs.MediaToProcess, videoPool *[]typesdefs.MediaToProcess) typesdefs.AnnuncioParsed {
var tmp typesdefs.AnnuncioParsed = typesdefs.AnnuncioParsed{}
tmp.Codice = utils.RemoveWhitespace(utils.GetStringValue(annuncio.Codice))
2025-08-04 17:45:44 +02:00
processLocatore(&tmp, annuncio)
processIndirizzo(&tmp, annuncio)
tmp.Tipo = parser.ParseTipologia(annuncio.Tipologia, annuncio.Tipologia2)
tmp.Consegna = parser.ParseConsegna(annuncio.Consegna)
processCategorie(&tmp, annuncio, m)
2025-08-04 17:45:44 +02:00
processDatiImmobile(&tmp, annuncio)
tmp.Stato = annuncio.Stato
tmp.Homepage = boolUpd(false)
if annuncio.HomePage != nil {
if *annuncio.HomePage == "si" {
tmp.Homepage = boolUpd(true)
}
}
layout := "20060102150405"
if annuncio.Creato != nil {
t, err := time.Parse(layout, *annuncio.Creato)
if err != nil {
tmp.Creato_il = nil
} else {
tmp.Creato_il = &t
}
}
if annuncio.Modifica != nil {
t, err := time.Parse(layout, *annuncio.Modifica)
if err != nil {
tmp.Modificato_il = nil
} else {
tmp.Modificato_il = &t
}
}
tmp.Titolo_it = annuncio.Titolo
tmp.Titolo_en = annuncio.TitoloEn
tmp.Desc_it = annuncio.Descrizione
tmp.Desc_en = annuncio.DescrizioneEn
if annuncio.Dettaglio != nil {
disp, parm, pers, err := parser.ParseDettaglio(*annuncio.Dettaglio, tmp.Consegna)
if err == nil {
tmp.Disponibile_da = disp
tmp.Permanenza = parm
tmp.Persone = pers
}
}
2025-08-04 17:45:44 +02:00
tmp.Web = boolUpd(true)
if updateVideos {
AddToVideoProcessing(&tmp, annuncio, videoPool)
2025-08-04 17:45:44 +02:00
}
if annuncio.Video != nil {
ext_videos := []string{}
for _, v := range *annuncio.Video {
if strings.Contains(v, "youtu") {
ext_videos = append(ext_videos, v)
2025-08-04 17:45:44 +02:00
}
tmp.External_videos = &ext_videos
2025-08-04 17:45:44 +02:00
}
} else {
tmp.External_videos = &[]string{}
2025-08-04 17:45:44 +02:00
}
if annuncio.Accessori != nil {
tmp.Accessori = annuncio.Accessori
}
tmpcaratt := parseCaratteristiche(annuncio, m)
2025-08-04 17:45:44 +02:00
tmp.Caratteristiche = &tmpcaratt
2025-08-04 17:45:44 +02:00
if updateImages {
AddToImageProcessing(&tmp, annuncio, imgPool)
2025-08-04 17:45:44 +02:00
}
2025-08-04 17:45:44 +02:00
return tmp
}
func flatClienteTipologia(t []string) string {
if len(t) == 0 {
return ""
}
if slices.Contains(t, "Proprietario") {
return "proprietario"
} else if slices.Contains(t, "Delegato") {
return "delegato"
} else if slices.Contains(t, "Cliente") {
return "inquilino"
}
return ""
}
func tipologiaPriority(tipologie []string) int {
if slices.Contains(tipologie, "Delegato") {
return 3
}
if slices.Contains(tipologie, "Cliente") {
return 2
}
if slices.Contains(tipologie, "Proprietario") {
return 1
}
return 0
}
func selectTargetCliente(annuncio *typesdefs.AnnuncioXML) (int, bool) {
if len(annuncio.Clienti) == 0 {
return 0, false
}
best := 0
bestPriority := tipologiaPriority(annuncio.Clienti[0].Tipologie)
for i := 1; i < len(annuncio.Clienti); i++ {
if p := tipologiaPriority(annuncio.Clienti[i].Tipologie); p > bestPriority {
bestPriority = p
best = i
}
}
return best, true
}
func processLocatore(tmp *typesdefs.AnnuncioParsed, annuncio *typesdefs.AnnuncioXML) {
idx, ok := selectTargetCliente(annuncio)
if !ok {
log.Printf("Annuncio %s has no client information", utils.GetStringValue(annuncio.Codice))
2025-08-04 17:45:44 +02:00
return
}
trg := annuncio.Clienti[idx]
tmp.Email = parser.ParseEmail(trg.Email1)
2025-08-04 17:45:44 +02:00
var number string
for _, tel := range []*string{trg.Tel1, trg.Tel2, trg.Tel3} {
2025-08-04 17:45:44 +02:00
if tel != nil {
cleanedTel := re.ReplaceAllString(*tel, "")
if cleanedTel != "" {
number = cleanedTel
break
}
}
}
tmp.Numero = &number
if trg.Cognome != nil && trg.Nome != nil {
rawLoc := fmt.Sprintf("%s %s", utils.MakeUppercase(*trg.Cognome), utils.MakeUppercase(*trg.Nome))
rawLoc = strings.TrimSpace(rawLoc)
rawLoc = regexp.MustCompile(`\s+`).ReplaceAllString(rawLoc, " ") // Replace multiple spaces with a single space
tmp.Locatore = strUpd(rawLoc)
2025-08-04 17:45:44 +02:00
}
tmp.Idlocatore = trg.Id
tmp.Tipo_locatore = strUpd(flatClienteTipologia(trg.Tipologie))
2025-08-04 17:45:44 +02:00
}
func processIndirizzo(tmp *typesdefs.AnnuncioParsed, annuncio *typesdefs.AnnuncioXML) {
tmp.Indirizzo = strUpd(utils.MakeUppercase(utils.GetStringValue(annuncio.Indirizzo)))
2025-08-04 17:45:44 +02:00
tmp.Civico = annuncio.Civico
tmp.Comune = strUpd(utils.MakeUppercase(utils.GetStringValue(annuncio.Comune)))
2025-08-04 17:45:44 +02:00
tmp.Cap = annuncio.Cap
tmp.Provincia = annuncio.Provincia
tmp.Regione = annuncio.Regione
tmp.Lat = annuncio.Latitudine
tmp.Lon = annuncio.Longitudine
tmp.Indirizzo_secondario = strUpd(utils.MakeUppercase(utils.GetStringValue(annuncio.IndirizzoSecondario)))
2025-08-04 17:45:44 +02:00
tmp.Civico_secondario = annuncio.CivicoSecondario
tmp.Lat_secondario = annuncio.LatitudineSecondario
tmp.Lon_secondario = annuncio.LongitudineSecondario
}
func processCategorie(tmp *typesdefs.AnnuncioParsed, annuncio *typesdefs.AnnuncioXML, m *MiogestHandler) {
2025-08-04 17:45:44 +02:00
if annuncio.Categoria == nil {
return
}
resultArray := make([]string, 0)
cats := *annuncio.Categoria
defaultCategorie := m.GetCategorie()
2025-08-04 17:45:44 +02:00
for _, cat := range cats {
for _, catdef := range defaultCategorie {
if cat == catdef.Id {
resultArray = append(resultArray, catdef.Nome)
}
}
}
if tmp.Categorie == nil {
tmp.Categorie = &[]string{}
}
*tmp.Categorie = resultArray
}
func processDatiImmobile(tmp *typesdefs.AnnuncioParsed, annuncio *typesdefs.AnnuncioXML) {
tmpanno := utils.GetStringValue(annuncio.Anno)
2025-08-04 17:45:44 +02:00
if tmpanno != "0" && tmpanno != "" {
tmp.Anno = &tmpanno
}
tmp.Prezzo = parser.ParsePrezzo(annuncio.Prezzo)
2025-08-04 17:45:44 +02:00
tmp.Classe = annuncio.Classe
if annuncio.Mq != nil {
replaced := strings.ReplaceAll(*annuncio.Mq, ",", ".")
if s, err := strconv.ParseFloat(replaced, 64); err == nil {
tmp.Mq = &s
}
2025-08-04 17:45:44 +02:00
}
tmp.Piano = annuncio.Piano
tmp.Piano_palazzo = annuncio.PianiCondominio
tmp.Unita_condominio = annuncio.UnitaCondominio
tmp.Numero_vani = annuncio.Vani
tmp.Numero_camere = annuncio.Camere
tmp.Numero_bagni = annuncio.Bagni
tmp.Numero_balconi = annuncio.Balconi
tmp.Numero_terrazzi = annuncio.Terrazzi
tmp.Numero_box = annuncio.Box
tmp.Numero_postiauto = annuncio.PostiAuto
}
func parseCaratteristiche(wrkrecord *typesdefs.AnnuncioXML, m *MiogestHandler) map[string]interface{} {
2025-08-04 17:45:44 +02:00
if reflect.TypeOf(wrkrecord).Kind() != reflect.Ptr || reflect.ValueOf(wrkrecord).Elem().Kind() != reflect.Struct {
return nil
}
tmpCaratteristiche := make(map[string]interface{})
val := reflect.ValueOf(wrkrecord).Elem()
numField := val.NumField()
for i := 0; i < numField; i++ {
if strings.HasPrefix(val.Type().Field(i).Name, "Scheda_") {
nameField := val.Type().Field(i).Name
valueField := val.Field(i)
if valueField.IsValid() && !valueField.IsNil() {
tmpCaratteristiche[nameField] = valueField.Interface()
} else {
tmpCaratteristiche[nameField] = nil
}
}
}
for _, scheda := range m.GetCaratteristiche().Caratteristiche {
2025-08-04 17:45:44 +02:00
if _, ok := tmpCaratteristiche[scheda.Tagxml]; !ok {
tmpCaratteristiche[scheda.Tagxml] = nil
}
}
return tmpCaratteristiche
}