github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/server/inscriptions/controller.go (about) 1 // Expose les fonctionnalités du formulaire d'inscription 2 // aux séjours ACVE. 3 package inscriptions 4 5 import ( 6 "errors" 7 "fmt" 8 "net/url" 9 "path" 10 "sort" 11 "strings" 12 "time" 13 14 rd "github.com/benoitkugler/goACVE/server/core/rawdata" 15 "github.com/benoitkugler/goACVE/server/core/rawdata/composites" 16 "github.com/benoitkugler/goACVE/server/core/rawdata/matching" 17 "github.com/benoitkugler/goACVE/server/core/utils/mails" 18 "github.com/benoitkugler/goACVE/server/shared" 19 ) 20 21 // adresse de confirmation du mail 22 var PathValidMail = path.Join(UrlInscriptions, EndPointValidMail) 23 24 type Controller struct { 25 shared.Controller 26 } 27 28 // Preinscription code le choix d'un responsable et des participants associés. 29 type Preinscription struct { 30 IdResponsable int64 31 IdsEnfants rd.Ids 32 } 33 34 type CampSimple struct { 35 Id int64 `json:"id"` 36 IdGroupe int64 `json:"id_groupe"` 37 Label rd.String `json:"label"` 38 Prix rd.Euros `json:"prix"` 39 DateDebut rd.Date `json:"date_debut"` 40 DateFin rd.Date `json:"date_fin"` 41 AgeMin rd.Int `json:"age_min"` 42 AgeMax rd.Int `json:"age_max"` 43 } 44 45 func (c *CampSimple) From(camp rd.Camp) { 46 c.Id = camp.Id 47 c.Label = camp.Label() 48 c.Prix = camp.Prix 49 c.AgeMax = camp.AgeMax 50 c.AgeMin = camp.AgeMin 51 c.DateDebut = camp.DateDebut 52 c.DateFin = camp.DateFin 53 } 54 55 type dataCamps struct { 56 camps rd.Camps 57 groupes rd.Groupes 58 } 59 60 // triCamps différencie les camps simple ou non 61 func (d dataCamps) triCamps() ([]shared.Camp, map[int64]CampSimple) { 62 out1 := make([]shared.Camp, 0, len(d.camps)) 63 out2 := make(map[int64]CampSimple) 64 for _, camp := range d.camps { 65 if camp.InscriptionSimple { 66 var pCamp CampSimple 67 pCamp.From(camp) 68 out2[camp.Id] = pCamp 69 } else { 70 var pCamp shared.Camp 71 pCamp.From(camp) 72 out1 = append(out1, pCamp) 73 } 74 } 75 sort.Slice(out1, func(i, j int) bool { 76 return out1[i].Label < out1[j].Label 77 }) 78 return out1, out2 79 } 80 81 // loadCamps renvoie les camps ouverts aux inscriptions et dont la date de fin n'est pas passée 82 func (ct Controller) loadCamps() (dataCamps, error) { 83 rows, err := ct.DB.Query("SELECT * FROM camps WHERE ouvert = true") 84 if err != nil { 85 return dataCamps{}, err 86 } 87 camps, err := rd.ScanCamps(rows) 88 if err != nil { 89 return dataCamps{}, err 90 } 91 for id, camp := range camps { 92 if camp.DateFin.Time().Before(time.Now()) { 93 delete(camps, id) 94 } 95 } 96 rows, err = ct.DB.Query("SELECT * FROM groupes WHERE id_camp = ANY($1)", camps.Ids().AsSQL()) 97 if err != nil { 98 return dataCamps{}, err 99 } 100 groupes, err := rd.ScanGroupes(rows) 101 if err != nil { 102 return dataCamps{}, err 103 } 104 return dataCamps{camps: camps, groupes: groupes}, nil 105 } 106 107 // vérifie si le mail est le même 108 func (ct Controller) hasMailChanged(mail rd.String, idPersonne rd.IdentificationId) (bool, error) { 109 personne, err := rd.SelectPersonne(ct.DB, idPersonne.Id) 110 if err != nil { 111 return false, shared.FormatErr("Impossible de consulter le profil du responsable.", err) 112 } 113 return personne.Mail.ToLower() != mail.ToLower(), nil 114 } 115 116 type candidatsPreinscription struct { 117 responsables []rd.Personne 118 idsParticipantPersonnes rd.Ids // participants cumulés 119 } 120 121 // chercheMail renvoie les personnes ayant le mail fourni 122 func (ct Controller) chercheMail(mail string) (candidatsPreinscription, error) { 123 mail = strings.TrimSpace(mail) 124 if len(mail) <= 1 { 125 return candidatsPreinscription{}, nil 126 } 127 rows, err := ct.DB.Query("SELECT * FROM personnes WHERE mail = $1", mail) 128 if err != nil { 129 return candidatsPreinscription{}, err 130 } 131 pers, err := rd.ScanPersonnes(rows) 132 if err != nil { 133 return candidatsPreinscription{}, err 134 } 135 var ( 136 ids rd.Ids 137 out candidatsPreinscription 138 ) 139 for id, pers := range pers { 140 ids = append(ids, id) 141 out.responsables = append(out.responsables, pers) 142 } 143 sort.Slice(out.responsables, func(i int, j int) bool { 144 return out.responsables[i].NomPrenom() < out.responsables[j].NomPrenom() 145 }) 146 rows, err = ct.DB.Query(`SELECT participants.id_personne FROM participants 147 JOIN factures ON participants.id_facture = factures.id 148 WHERE factures.id_personne = ANY($1)`, ids.AsSQL()) 149 if err != nil { 150 return candidatsPreinscription{}, err 151 } 152 idsEnfants, err := rd.ScanIds(rows) 153 if err != nil { 154 return candidatsPreinscription{}, err 155 } 156 out.idsParticipantPersonnes = idsEnfants.AsSet().Keys() // unicité 157 return out, nil 158 } 159 160 func (ct Controller) buildLiensPreinscription(cd candidatsPreinscription, origin string) ([]mails.TargetRespo, error) { 161 baseUrl, err := url.Parse(origin) 162 if err != nil { 163 return nil, err 164 } 165 var out []mails.TargetRespo 166 for _, resp := range cd.responsables { 167 t := Preinscription{IdResponsable: resp.Id, IdsEnfants: cd.idsParticipantPersonnes} 168 crypted, err := shared.Encode(ct.Signing, t) 169 if err != nil { 170 return nil, err 171 } 172 lien := shared.BuildUrl(baseUrl.Host, baseUrl.Path, map[string]string{"preinscription": crypted}) 173 out = append(out, mails.TargetRespo{Lien: lien, NomPrenom: resp.NomPrenom().String()}) 174 } 175 return out, nil 176 } 177 178 // decodePreinscription parse le lien du mail et forme une inscription pré-remplie. 179 func (ct Controller) decodePreinscription(crypted string) (insc rd.Inscription, err error) { 180 var pre Preinscription 181 if err := shared.Decode(ct.Signing, crypted, &pre); err != nil { 182 return insc, shared.FormatErr("Le lien d'inscription rapide que vous utilisez semble invalide.", err) 183 } 184 respo, err := rd.SelectPersonne(ct.DB, pre.IdResponsable) 185 if err != nil { 186 return insc, shared.FormatErr("Le responsable n'a pu être retrouvé.", err) 187 } 188 rows, err := ct.DB.Query("SELECT * FROM personnes WHERE id = ANY($1)", pre.IdsEnfants.AsSQL()) 189 if err != nil { 190 return insc, shared.FormatErr("Les participants n'ont pu être retrouvés.", err) 191 } 192 parts, err := rd.ScanPersonnes(rows) 193 if err != nil { 194 return insc, shared.FormatErr("Les participants n'ont pu être retrouvés.", err) 195 } 196 197 insc.Responsable = respo.ToInscription() 198 insc.Responsable.Lienid.Crypted, err = shared.EncodeID(ct.Signing, shared.OrPreIdentification, respo.Id) 199 if err != nil { 200 return insc, shared.FormatErr("Le cryptage des identifiants a échoué.", err) 201 } 202 203 for _, part := range parts { 204 partInsc := part.ToParticipantInscription() 205 partInsc.Lienid.Crypted, err = shared.EncodeID(ct.Signing, shared.OrPreIdentification, part.Id) 206 if err != nil { 207 return insc, shared.FormatErr("Le cryptage des identifiants a échoué.", err) 208 } 209 insc.Participants = append(insc.Participants, partInsc) 210 } 211 return insc, nil 212 } 213 214 func (ct Controller) decodeLienId(lienid *rd.IdentificationId) error { 215 if lienid.Crypted != "" { 216 lienId, err := shared.DecodeID(ct.Signing, lienid.Crypted, shared.OrPreIdentification) 217 if err != nil { 218 return shared.FormatErr("Le cryptage de la pré-identification semble incorrect.", err) 219 } 220 *lienid = rd.IdentificationId{Id: lienId, Valid: true} 221 } 222 return nil 223 } 224 225 func valideInscription(insc rd.Inscription) error { 226 if insc.Responsable.Nom.TrimSpace() == "" { 227 return errors.New("Merci de préciser votre nom.") 228 } 229 if insc.Responsable.Prenom.TrimSpace() == "" { 230 return errors.New("Merci de préciser votre prénom.") 231 } 232 if insc.Responsable.DateNaissance.Time().IsZero() { 233 return errors.New("Merci de fournir votre date de naissance.") 234 } 235 if len(insc.Participants) == 0 { 236 return errors.New("Aucun participant n'a été choisi !") 237 } 238 age := rd.CalculeAge(insc.Responsable.DateNaissance, time.Time{}).Age() 239 if age < 18 { 240 return errors.New("Le responsable légal doit être majeur !") 241 } 242 return nil 243 } 244 245 // transforme l'inscription en dossier, l'enregistre et garde un log 246 // renvoie le dossier créé et son responsable 247 // `camps` et `groupes` doivent contenir les camps et groupes concernés par l'inscription 248 func (ct Controller) enregistreInscription(insc rd.Inscription, confirmed bool, data dataCamps) (rd.Facture, rd.Personne, error) { 249 insc.DateHeure = rd.Time(time.Now()) 250 251 type identifiedPersonne struct { 252 personne rd.Personne 253 lienid rd.IdentificationId 254 } 255 256 // on regroupe les personnes à identifier 257 var allPers []identifiedPersonne 258 ids := rd.NewSet() 259 260 allPers = append(allPers, identifiedPersonne{personne: insc.Responsable.ToPersonne(), lienid: insc.Responsable.Lienid}) 261 ids.Add(insc.Responsable.Lienid.Id) 262 for _, part := range insc.Participants { 263 allPers = append(allPers, identifiedPersonne{personne: part.ToPersonne(), lienid: part.Lienid}) 264 ids.Add(part.Lienid.Id) 265 } 266 267 tx, err := ct.DB.Begin() 268 if err != nil { 269 return rd.Facture{}, rd.Personne{}, err 270 } 271 272 // on charge les personnes pour la comparaison 273 rows, err := tx.Query("SELECT * FROM personnes WHERE id = ANY($1)", rd.Ids(ids.Keys()).AsSQL()) 274 if err != nil { 275 return rd.Facture{}, rd.Personne{}, shared.Rollback(tx, err) 276 } 277 personnes, err := rd.ScanPersonnes(rows) 278 if err != nil { 279 return rd.Facture{}, rd.Personne{}, shared.Rollback(tx, err) 280 } 281 282 for i, personneAndId := range allPers { 283 var personne rd.Personne 284 if existante, isInBase := personnes[personneAndId.lienid.Id]; personneAndId.lienid.Valid && isInBase { 285 // si l'inscription est préidentifiée, on fusionne automatiquement 286 // l'inscription avec le profil 287 existante.BasePersonne, _ = matching.Merge(personneAndId.personne.BasePersonne, existante.BasePersonne) 288 personne, err = existante.Update(tx) 289 } else { 290 // sinon, on crée une nouvelle personne temporaire 291 personneAndId.personne.IsTemporaire = true 292 personne, err = personneAndId.personne.Insert(tx) 293 } 294 if err != nil { 295 return rd.Facture{}, rd.Personne{}, shared.Rollback(tx, err) 296 } 297 allPers[i].personne = personne // on a besoin de l'id plus tard 298 } 299 responsablePersonne := allPers[0].personne // le responsable est en premier 300 301 key, err := shared.GetNewKey(tx) 302 if err != nil { 303 return rd.Facture{}, rd.Personne{}, shared.Rollback(tx, err) 304 } 305 fac := rd.Facture{ 306 IdPersonne: responsablePersonne.Id, 307 Key: rd.String(key), 308 CopiesMails: insc.CopiesMails, 309 IsConfirmed: confirmed, 310 IsValidated: false, 311 PartageAdressesOK: insc.PartageAdressesOK, 312 } 313 fac, err = fac.Insert(tx) 314 if err != nil { 315 return rd.Facture{}, rd.Personne{}, shared.Rollback(tx, err) 316 } 317 318 // on garde une trace du moment d'inscription 319 messageTime := rd.Message{ 320 IdFacture: fac.Id, 321 Kind: rd.MInscription, 322 Created: rd.Time(time.Now()), 323 } 324 _, err = messageTime.Insert(tx) 325 if err != nil { 326 return rd.Facture{}, rd.Personne{}, shared.Rollback(tx, err) 327 } 328 329 // on insert le message du formulaire 330 if me := insc.Info.TrimSpace(); me != "" { 331 message := rd.Message{ 332 IdFacture: fac.Id, 333 Kind: rd.MResponsable, 334 Created: rd.Time(time.Now().Add(time.Second)), // on s'assure que le message vient après le moment d'inscription 335 } 336 message, err = message.Insert(tx) 337 if err != nil { 338 return rd.Facture{}, rd.Personne{}, shared.Rollback(tx, err) 339 } 340 mMessage := rd.MessageMessage{IdMessage: message.Id, Contenu: me, GuardKind: rd.MResponsable} 341 err = rd.InsertManyMessageMessages(tx, mMessage) 342 if err != nil { 343 return rd.Facture{}, rd.Personne{}, shared.Rollback(tx, err) 344 } 345 } 346 347 // le choix attente/inscrit nécessite les participants 348 participants, err := rd.SelectParticipantsByIdCamps(tx, data.camps.Ids()...) 349 if err != nil { 350 return rd.Facture{}, rd.Personne{}, shared.Rollback(tx, err) 351 } 352 353 // on ajoute maintenant les participants 354 for i, inscPart := range insc.Participants { 355 personne := allPers[i+1].personne // personne créée ou mise à jour 356 camp := data.camps[inscPart.IdCamp] 357 358 campP := composites.NewCampParticipants(camp, participants) 359 if campP.IsParticipantAlreadyHere(personne.Id) { 360 _ = shared.Rollback(tx, nil) 361 return rd.Facture{}, rd.Personne{}, fmt.Errorf("%s est déjà inscrit sur le séjour %s", 362 personne.NomPrenom(), campP.Label()) 363 } 364 365 campG := composites.NewCampGroupes(camp, data.groupes) 366 367 statutAttente := campP.HintsAttente(personne.BasePersonne, false, personnes).Hint() 368 participant := rd.Participant{ 369 IdCamp: inscPart.IdCamp, 370 IdPersonne: personne.Id, 371 IdFacture: rd.NewOptionnalId(fac.Id), 372 ListeAttente: rd.ListeAttente{Statut: statutAttente}, 373 Options: inscPart.Options, 374 OptionPrix: inscPart.OptionsPrix, 375 DateHeure: rd.Time(time.Now()), 376 } 377 participant, err = participant.Insert(tx) 378 if err != nil { 379 return rd.Facture{}, rd.Personne{}, shared.Rollback(tx, err) 380 } 381 382 groupe, hasFound := campG.TrouveGroupe(inscPart.DateNaissance) 383 if hasFound { 384 // on ajoute automatiquement le nouveau participant au groupe 385 lien := rd.GroupeParticipant{IdGroupe: groupe.Id, IdCamp: groupe.IdCamp, IdParticipant: participant.Id} 386 err = rd.InsertManyGroupeParticipants(tx, lien) 387 if err != nil { 388 return rd.Facture{}, rd.Personne{}, shared.Rollback(tx, err) 389 } 390 } 391 } 392 393 // on garde un log de l'inscription brute 394 if _, err = insc.Insert(tx); err != nil { 395 return rd.Facture{}, rd.Personne{}, shared.Rollback(tx, err) 396 } 397 err = tx.Commit() 398 return fac, responsablePersonne, err 399 } 400 401 // envoie un mail de demande de confirmation 402 func (ct Controller) verifieMail(host string, facture rd.Facture, responsable rd.Personne) error { 403 cryptedId, err := shared.EncodeID(ct.Signing, shared.OrValidationMail, facture.Id) 404 if err != nil { 405 return shared.FormatErr("Le cryptage des identifiants a échoué.", err) 406 } 407 408 urlValide := shared.BuildUrl(host, PathValidMail, map[string]string{ 409 "crypted-id": cryptedId, 410 }) 411 412 html, err := mails.NewValideMail(urlValide, mails.Contact{Prenom: responsable.FPrenom(), Sexe: responsable.Sexe}) 413 if err != nil { 414 return shared.FormatErr("La création du mail a échoué.", err) 415 } 416 if err = mails.NewMailer(ct.SMTP).SendMail(string(responsable.Mail), "[ACVE] Vérification de l'adresse mail", html, nil, nil); err != nil { 417 return shared.FormatErr("L'envoi du mail de vérification a échoué.", err) 418 } 419 return nil 420 } 421 422 func (ct Controller) confirmeInscription(cryptedId string) (rd.Facture, error) { 423 idFacture, err := shared.DecodeID(ct.Signing, cryptedId, shared.OrValidationMail) 424 if err != nil { 425 return rd.Facture{}, err 426 } 427 row := ct.DB.QueryRow("UPDATE factures SET is_confirmed = true WHERE id = $1 RETURNING *", idFacture) 428 facture, err := rd.ScanFacture(row) 429 if err != nil { 430 return rd.Facture{}, shared.FormatErr("La validation de l'adresse a échoué.", err) 431 } 432 return facture, nil 433 } 434 435 func (ct Controller) enregistreInscriptionSimple(insc InscriptionSimple, camp CampSimple) error { 436 tx, err := ct.DB.Begin() 437 if err != nil { 438 return shared.FormatErr("Base de données injoinable.", err) 439 } 440 441 // on enregistre directement le participant (lié à une personne temporaire) 442 personne := rd.Personne{ 443 BasePersonne: rd.BasePersonne{ 444 Nom: insc.Nom, 445 Prenom: insc.Prenom, 446 DateNaissance: insc.DateNaissance, 447 Sexe: insc.Sexe, 448 Mail: insc.Mail, 449 Tels: rd.Tels{insc.Tel}, 450 }, 451 IsTemporaire: true, 452 } 453 personne, err = personne.Insert(tx) 454 if err != nil { 455 return shared.Rollback(tx, err) 456 } 457 participant := rd.Participantsimple{ 458 IdCamp: camp.Id, 459 IdPersonne: personne.Id, 460 Info: insc.Info, 461 DateHeure: rd.Time(time.Now()), 462 } 463 participant, err = participant.Insert(tx) 464 if err != nil { 465 return shared.Rollback(tx, err) 466 } 467 468 if err = tx.Commit(); err != nil { 469 return shared.FormatErr("L'enregistrement de l'inscription a échoué.", err) 470 } 471 return nil 472 }