github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/server/espaceperso/espace_perso.go (about) 1 // Expose les fonctionnalités de la page de suivi personnelle 2 package espaceperso 3 4 import ( 5 "errors" 6 "fmt" 7 "sort" 8 "strings" 9 "time" 10 11 "github.com/benoitkugler/goACVE/logs" 12 dm "github.com/benoitkugler/goACVE/server/core/datamodel" 13 cd "github.com/benoitkugler/goACVE/server/core/documents" 14 rd "github.com/benoitkugler/goACVE/server/core/rawdata" 15 "github.com/benoitkugler/goACVE/server/core/utils/joomeo" 16 "github.com/benoitkugler/goACVE/server/core/utils/mails" 17 18 "github.com/benoitkugler/goACVE/server/documents" 19 "github.com/benoitkugler/goACVE/server/shared" 20 "github.com/lib/pq" 21 ) 22 23 const ( 24 EndPointPartageFicheSanitaire = "/partage_fiche_sanitaire" 25 26 // Pas de modification directe après J-7 (début du camp). 27 UpdateLimitation = 7 * 24 * time.Hour 28 ) 29 30 type Controller struct { 31 shared.Controller 32 33 joomeo logs.Joomeo 34 contrainteVaccin rd.Contrainte 35 36 sondageNotifier SondageNotifier 37 } 38 39 type SondageNotifier interface { 40 Notifie(host string, sondage rd.Sondage) error 41 } 42 43 // NewController charge les données initiales 44 func NewController(ct shared.Controller, joomeo logs.Joomeo, sondageNotifier SondageNotifier) (Controller, error) { 45 out := Controller{Controller: ct, joomeo: joomeo, sondageNotifier: sondageNotifier} 46 47 // on préselectionne la contrainte 'vaccins' 48 row := ct.DB.QueryRow("SELECT * FROM contraintes WHERE builtin = 'vaccin'") 49 var err error 50 out.contrainteVaccin, err = rd.ScanContrainte(row) 51 return out, err 52 } 53 54 type Participant struct { 55 Id int64 `json:"id,omitempty"` // en lecture seulement, les modifications sont sécurisées avec IdCrypted 56 IdCrypted string `json:"id_crypted,omitempty"` 57 IdPersonneCrypted string `json:"id_personne_crypted,omitempty"` 58 IdCamp int64 `json:"id_camp,omitempty"` 59 ListeAttente rd.ListeAttente `json:"liste_attente,omitempty"` 60 HintsAttente rd.HintsAttente `json:"hints_attente,omitempty"` 61 IsFicheSanitaireUpToDate bool `json:"is_fiche_sanitaire_up_to_date,omitempty"` 62 Options rd.OptionsParticipant `json:"options,omitempty"` 63 } 64 65 type partageFicheSanitaire struct { 66 Mail string 67 IdPersonne int64 68 } 69 70 func (ct Controller) decrypteKey(key string) (idFacture int64, err error) { 71 key = strings.TrimSpace(key) 72 if key == "" { 73 return 0, fmt.Errorf("Merci de fournir une clé de dossier !") 74 } 75 row := ct.DB.QueryRow("SELECT id FROM factures WHERE key = $1", key) 76 var idFac int64 77 if err = row.Scan(&idFac); err != nil { 78 return 0, shared.FormatErr(fmt.Sprintf("Votre dossier (n° %s) est introuvable : êtes-vous sûr du lien utilisé ?", key), err) 79 } 80 return idFac, nil 81 } 82 83 // loadData charge depuis la base toutes les données nécessaires 84 // à l'affichage de l'espace personnel de suivi. 85 // La base renvoyée est PARTIELLE. 86 func (ct Controller) loadData(idFacture int64) (*dm.BaseLocale, error) { 87 fac, err := rd.SelectFacture(ct.DB, idFacture) 88 if err != nil { 89 return nil, err 90 } 91 idsPersonnes := rd.Ids{fac.IdPersonne} 92 93 participants, err := rd.SelectParticipantsByIdFactures(ct.DB, idFacture) 94 if err != nil { 95 return nil, err 96 } 97 98 rows, err := ct.DB.Query(`SELECT groupes.* FROM groupes 99 JOIN groupe_participants ON groupe_participants.id_groupe = groupes.id 100 WHERE groupe_participants.id_participant = ANY($1)`, participants.Ids().AsSQL()) 101 if err != nil { 102 return nil, err 103 } 104 groupes, err := rd.ScanGroupes(rows) 105 if err != nil { 106 return nil, err 107 } 108 groupeParticipants, err := rd.SelectGroupeParticipantsByIdGroupes(ct.DB, groupes.Ids()...) 109 if err != nil { 110 return nil, err 111 } 112 113 var idsParticipants, idsCamps rd.Ids 114 for _, part := range participants { 115 idsParticipants = append(idsParticipants, part.Id) 116 idsCamps = append(idsCamps, part.IdCamp) 117 idsPersonnes = append(idsPersonnes, part.IdPersonne) 118 } 119 120 campContraintes, err := rd.SelectCampContraintesByIdCamps(ct.DB, idsCamps...) 121 if err != nil { 122 return nil, err 123 } 124 125 groupeContraintes, err := rd.SelectGroupeContraintesByIdGroupes(ct.DB, groupes.Ids()...) 126 if err != nil { 127 return nil, err 128 } 129 130 aides, err := rd.SelectAidesByIdParticipants(ct.DB, idsParticipants...) 131 if err != nil { 132 return nil, err 133 } 134 135 documentAides, err := rd.SelectDocumentAidesByIdAides(ct.DB, aides.Ids()...) 136 if err != nil { 137 return nil, err 138 } 139 140 documentPersonnes, err := rd.SelectDocumentPersonnesByIdPersonnes(ct.DB, idsPersonnes...) 141 if err != nil { 142 return nil, err 143 } 144 145 documentCamps, err := rd.SelectDocumentCampsByIdCamps(ct.DB, idsCamps...) 146 if err != nil { 147 return nil, err 148 } 149 150 rows, err = ct.DB.Query(`SELECT documents.* FROM documents 151 JOIN document_aides ON document_aides.id_document = documents.id 152 WHERE document_aides.id_aide = ANY($1) 153 UNION 154 SELECT documents.* FROM documents 155 JOIN document_camps ON document_camps.id_document = documents.id 156 WHERE document_camps.id_camp = ANY($2) 157 UNION 158 SELECT documents.* FROM documents 159 JOIN document_personnes ON document_personnes.id_document = documents.id 160 WHERE document_personnes.id_personne = ANY($3) 161 `, aides.Ids().AsSQL(), idsCamps.AsSQL(), idsPersonnes.AsSQL()) 162 if err != nil { 163 return nil, err 164 } 165 docs, err := rd.ScanDocuments(rows) 166 if err != nil { 167 return nil, err 168 } 169 170 // les contraintes sont celles des groupes, des séjours et celles des documents des personnes 171 rows, err = ct.DB.Query(`SELECT contraintes.* FROM contraintes 172 JOIN groupe_contraintes ON groupe_contraintes.id_contrainte = contraintes.id 173 WHERE groupe_contraintes.id_groupe = ANY($1) 174 UNION 175 SELECT contraintes.* FROM contraintes 176 JOIN camp_contraintes ON camp_contraintes.id_contrainte = contraintes.id 177 WHERE camp_contraintes.id_camp = ANY($2) 178 UNION 179 SELECT contraintes.* FROM contraintes 180 JOIN document_personnes ON document_personnes.id_contrainte = contraintes.id 181 WHERE document_personnes.id_document = ANY($3)`, 182 groupes.Ids().AsSQL(), idsCamps.AsSQL(), docs.Ids().AsSQL()) 183 if err != nil { 184 return nil, err 185 } 186 contraintes, err := rd.ScanContraintes(rows) 187 if err != nil { 188 return nil, err 189 } 190 191 rows, err = ct.DB.Query("SELECT * FROM camps WHERE id = ANY($1)", idsCamps.AsSQL()) 192 if err != nil { 193 return nil, err 194 } 195 camps, err := rd.ScanCamps(rows) 196 if err != nil { 197 return nil, err 198 } 199 200 // récupération des directeurs (equipiers et personnes) 201 rows, err = ct.DB.Query("SELECT * FROM equipiers WHERE id_camp = ANY($1) AND $2 = ANY(roles)", idsCamps.AsSQL(), rd.RDirecteur) 202 if err != nil { 203 return nil, err 204 } 205 equipiers, err := rd.ScanEquipiers(rows) 206 if err != nil { 207 return nil, err 208 } 209 for _, directeur := range equipiers { 210 idsPersonnes = append(idsPersonnes, directeur.IdPersonne) 211 } 212 213 rows, err = ct.DB.Query("SELECT * FROM personnes WHERE id = ANY ($1)", idsPersonnes.AsSQL()) 214 if err != nil { 215 return nil, err 216 } 217 personnes, err := rd.ScanPersonnes(rows) 218 if err != nil { 219 return nil, err 220 } 221 222 structures, err := rd.SelectAllStructureaides(ct.DB) 223 if err != nil { 224 return nil, err 225 } 226 227 paiements, err := rd.SelectPaiementsByIdFactures(ct.DB, idFacture) 228 if err != nil { 229 return nil, err 230 } 231 232 // messages 233 messages, err := rd.SelectMessagesByIdFactures(ct.DB, idFacture) 234 if err != nil { 235 return nil, err 236 } 237 // compléments 238 mA, err := rd.SelectMessageAttestationsByIdMessages(ct.DB, messages.Ids()...) 239 if err != nil { 240 return nil, err 241 } 242 mC, err := rd.SelectMessageDocumentsByIdMessages(ct.DB, messages.Ids()...) 243 if err != nil { 244 return nil, err 245 } 246 mS, err := rd.SelectMessageSondagesByIdMessages(ct.DB, messages.Ids()...) 247 if err != nil { 248 return nil, err 249 } 250 mP, err := rd.SelectMessagePlaceliberesByIdMessages(ct.DB, messages.Ids()...) 251 if err != nil { 252 return nil, err 253 } 254 mM, err := rd.SelectMessageMessagesByIdMessages(ct.DB, messages.Ids()...) 255 if err != nil { 256 return nil, err 257 } 258 259 sondages, err := rd.SelectSondagesByIdFactures(ct.DB, idFacture) 260 if err != nil { 261 return nil, err 262 } 263 264 out := dm.BaseLocale{ 265 Personnes: personnes, 266 Paiements: paiements, 267 Participants: participants, 268 Groupes: groupes, 269 Equipiers: equipiers, 270 Factures: rd.Factures{fac.Id: fac}, 271 Aides: aides, 272 Structureaides: structures, 273 Camps: camps, 274 Contraintes: contraintes, 275 Documents: docs, 276 Messages: messages, 277 Sondages: sondages, 278 } 279 out.ProcessRawLinks(dm.RawLinks{ 280 DocumentAides: documentAides, 281 DocumentCamps: documentCamps, 282 DocumentPersonnes: documentPersonnes, 283 GroupeContraintes: groupeContraintes, 284 GroupeParticipants: groupeParticipants, 285 CampContraintes: campContraintes, 286 MessageDocuments: mC, 287 MessageSondages: mS, 288 MessageAttestations: mA, 289 MessagePlaceliberes: mP, 290 MessageMessages: mM, 291 }) 292 293 return &out, nil 294 } 295 296 func (ct Controller) crypteIds(dossiers []dm.AccesParticipant) (personnes, participants map[int64]string, err error) { 297 personnes, participants = map[int64]string{}, map[int64]string{} 298 var idC, idPC string 299 for _, part := range dossiers { 300 rawPart := part.RawData() 301 idC, err = shared.EncodeID(ct.Signing, shared.OrParticipant, rawPart.Id) 302 if err != nil { 303 return 304 } 305 participants[part.Id] = idC 306 idPC, err = shared.EncodeID(ct.Signing, shared.OrPersonne, rawPart.IdPersonne) 307 if err != nil { 308 return 309 } 310 personnes[rawPart.IdPersonne] = idPC 311 } 312 return 313 } 314 315 func (ct Controller) compileData(host string, idFacture int64, base *dm.BaseLocale) (out ContentEspacePerso, err error) { 316 fac := base.NewFacture(idFacture) 317 respPers := fac.GetPersonne().RawData() 318 319 out.Responsable = Responsable{ 320 Prenom: respPers.FPrenom(), 321 Sexe: respPers.Sexe, 322 NomPrenom: respPers.NomPrenom().String(), 323 CopiesMails: fac.RawData().CopiesMails, 324 SecuriteSociale: respPers.SecuriteSociale, 325 Coordonnees: Coordonnees{ 326 Mail: respPers.Mail, 327 Tels: respPers.Tels, 328 Adresse: respPers.Adresse, 329 CodePostal: respPers.CodePostal, 330 Ville: respPers.Ville, 331 Pays: respPers.Pays, 332 }, 333 DestinatairesOptionnels: fac.ChoixDestinataires(), 334 } 335 336 dossiers := fac.GetDossiers(nil) 337 338 persCrypted, partsCrypted, err := ct.crypteIds(dossiers) 339 if err != nil { 340 return out, err 341 } 342 343 idsCamps := rd.NewSet() 344 personneContraintes := map[int64]map[int64]rd.Ids{} // id personne -> id contrainte -> ids camps (demandant la contrainte) 345 for _, part := range dossiers { 346 rawPart := part.RawData() 347 idP, idCamp := rawPart.IdPersonne, part.GetCamp().Id 348 349 // on ajoute à la personne sous jacente les contraintes, en gardant le séjour 350 cont := personneContraintes[idP] 351 if cont == nil { 352 cont = make(map[int64]rd.Ids) 353 } 354 // contraintes communes à tout le séjour 355 for _, campContrainte := range part.Base.CampContraintes[idCamp] { 356 cont[campContrainte.IdContrainte] = append(cont[campContrainte.IdContrainte], idCamp) 357 } 358 // contraintes spécifiques au groupe 359 groupe, hasGroupe := part.GetGroupe() 360 if hasGroupe { 361 for _, groupeContrainte := range part.Base.GroupeContraintes[groupe.Id] { 362 cont[groupeContrainte.IdContrainte] = append(cont[groupeContrainte.IdContrainte], idCamp) 363 } 364 } 365 366 personneContraintes[idP] = cont 367 368 // on ajoute le camp 369 idsCamps.Add(idCamp) 370 371 out.Participants = append(out.Participants, Participant{ 372 Id: part.Id, 373 IdCrypted: partsCrypted[rawPart.Id], 374 IdPersonneCrypted: persCrypted[idP], 375 IdCamp: idCamp, 376 ListeAttente: rawPart.ListeAttente, 377 HintsAttente: part.HintsAttente(nil), 378 IsFicheSanitaireUpToDate: part.IsFicheSanitaireUpToDate().Bool(), 379 Options: rawPart.Options, 380 }) 381 382 } 383 384 out.Personnes, err = ct.compileParticipantsPersonnes(host, respPers.Mail, personneContraintes, persCrypted, base) 385 if err != nil { 386 return 387 } 388 389 out.Camps, err = ct.compileCamps(host, idsCamps, base) 390 if err != nil { 391 return 392 } 393 out.Sondages, err = ct.compileSondages(base.Sondages) 394 if err != nil { 395 return 396 } 397 398 out.Messages = compileMessages(fac) 399 return 400 } 401 402 // marque les messages comme vu/non vu, basé sur la dernière connection 403 func compileMessages(fac dm.AccesFacture) []dm.PseudoMessage { 404 messages := fac.GetEtat(nil, nil).PseudoMessages(dm.POVResponsable) 405 // on présente le dernier message en haut 406 sort.Slice(messages, func(i, j int) bool { 407 return messages[i].Created.Time().After(messages[j].Created.Time()) 408 }) 409 lastCo := fac.RawData().LastConnection 410 for i, message := range messages { 411 messages[i].Vu = isOld(message, lastCo) 412 } 413 return messages 414 } 415 416 func (ct Controller) getMeta(host string) (out MetaEspacePerso, err error) { 417 out.UpdateLimitation = int(UpdateLimitation.Hours()) 418 out.MailCentreInscription = rd.CoordonnesCentre.Mail 419 out.ContrainteVaccin, err = documents.PublieContrainte(ct.Signing, ct.DB, host, ct.contrainteVaccin) 420 return 421 } 422 423 func (ct Controller) loadFinances(host string, idFacture int64, base *dm.BaseLocale) (Finances, error) { 424 var ( 425 out Finances 426 err error 427 aides []Aide 428 structuresCrypted map[int64]string 429 ) 430 431 out.Structureaides, structuresCrypted, err = ct.compileStructureaides(base) 432 if err != nil { 433 return out, err 434 } 435 436 fac := base.NewFacture(idFacture) 437 dossiers := fac.GetDossiers(nil) 438 _, partsCrypted, err := ct.crypteIds(dossiers) 439 if err != nil { 440 return out, err 441 } 442 443 for _, part := range dossiers { 444 aides, err = ct.compileAides(host, partsCrypted[part.Id], structuresCrypted, part.GetAides(nil)) 445 if err != nil { 446 return out, err 447 } 448 out.Aides = append(out.Aides, aides...) 449 } 450 451 for _, paie := range fac.GetPaiements(nil, true) { 452 r := paie.RawData() 453 out.Paiements = append(out.Paiements, Paiement{ 454 LabelPayeur: r.LabelPayeur, 455 DateReglement: r.DateReglement, 456 IsInvalide: r.IsInvalide, 457 IsRemboursement: r.IsRemboursement, 458 Valeur: r.Valeur, 459 ModePaiement: r.ModePaiement, 460 }) 461 } 462 463 // bilan financier 464 bilan := fac.EtatFinancier(dm.CacheEtatFinancier{}, false) 465 apresPaiement := bilan.ApresPaiement() 466 var totalSejours, totalAides rd.Euros 467 for _, part := range bilan.Participants { 468 d := part.EtatFinancier(nil, false) 469 totalAides += d.TotalAides().Remise(d.Remises.Pourcent()) 470 totalSejours += d.PrixSansAide() 471 } 472 out.EtatFinancier = etatFinancier{ 473 TotalSejours: totalSejours, 474 TotalAides: totalAides, 475 TotalPaiements: bilan.Recu, 476 TotalRestant: apresPaiement, 477 } 478 out.LabelVirement = fac.LabelVirement() 479 return out, nil 480 } 481 482 func (ct Controller) compileParticipantsPersonnes(host string, mailRespo rd.String, personnesContraintes map[int64]map[int64]rd.Ids, 483 crypted map[int64]string, base *dm.BaseLocale) (map[string]Personne, error) { 484 personnes := make(map[string]Personne, len(personnesContraintes)) 485 for idPers, contrainteCamps := range personnesContraintes { 486 rawPers := base.Personnes[idPers] 487 var vaccins []documents.PublicDocument 488 489 docs := map[int64]ContrainteWithOrigine{} 490 // on commence par copier les contraintes demandées 491 for idContrainte, idsCamps := range contrainteCamps { 492 pubCt, err := documents.PublieContrainte(ct.Signing, ct.DB, host, base.Contraintes[idContrainte]) 493 if err != nil { 494 return nil, err 495 } 496 docs[idContrainte] = ContrainteWithOrigine{ 497 ContrainteDocuments: documents.ContrainteDocuments{ 498 Contrainte: pubCt, 499 }, 500 Origine: idsCamps.AsSet().Keys(), // unicité 501 } 502 } 503 504 for _, doc := range base.NewPersonne(idPers).GetDocuments(nil) { 505 contrainteDoc := doc.GetContrainte() 506 isVaccin := contrainteDoc.Builtin == rd.CVaccin 507 _, isNeeded := docs[contrainteDoc.Id] 508 if !(isVaccin || isNeeded) { 509 continue 510 } 511 512 docPub, err := documents.PublieDocument(ct.Signing, host, doc.RawData()) 513 if err != nil { 514 return nil, err 515 } 516 517 if isVaccin { 518 // cas particulier pour les vaccins 519 vaccins = append(vaccins, docPub) 520 } else if isNeeded { 521 // on aggrege les documents présents par contrainte 522 cwo := docs[contrainteDoc.Id] // nécessaire si réallocation 523 cwo.Docs = append(cwo.Docs, docPub) 524 docs[contrainteDoc.Id] = cwo 525 } 526 } 527 528 isLocked := isFicheSanitaireLocked(mailRespo.String(), rawPers.FicheSanitaire.Mails) 529 fsl := LockableFicheSanitaire{Locked: isLocked} 530 if isLocked { // seulement les meta données 531 fsl.FicheSanitaire.Mails = rawPers.FicheSanitaire.Mails 532 fsl.FicheSanitaire.LastModif = rawPers.FicheSanitaire.LastModif 533 } else { 534 fsl.FicheSanitaire = rawPers.FicheSanitaire 535 } 536 out := Personne{ 537 IdCrypted: crypted[idPers], 538 Prenom: rawPers.FPrenom(), 539 NomPrenom: rawPers.NomPrenom().String(), 540 Sexe: rawPers.Sexe, 541 DateNaissance: rawPers.DateNaissance, 542 FicheSanitaire: fsl, 543 Vaccins: vaccins, 544 IsTemporaire: rawPers.IsTemporaire, 545 } 546 547 sortByTime := func(l []documents.PublicDocument) { 548 sort.Slice(l, func(i int, j int) bool { 549 return l[i].DateHeureModif.Time().After(l[j].DateHeureModif.Time()) 550 }) 551 } 552 sortByTime(out.Vaccins) 553 for _, l := range docs { 554 sortByTime(l.Docs) // sort in place backing array 555 out.Documents = append(out.Documents, l) 556 } 557 sort.Slice(out.Documents, func(i, j int) bool { // déterministe 558 return out.Documents[i].Contrainte.Nom < out.Documents[j].Contrainte.Nom 559 }) 560 561 personnes[out.IdCrypted] = out 562 } 563 return personnes, nil 564 } 565 566 func (ct Controller) compileCamps(host string, idsCamps map[int64]bool, base *dm.BaseLocale) (map[int64]CampPlus, error) { 567 out := make(map[int64]CampPlus, len(idsCamps)) 568 for idCamp := range idsCamps { 569 rawCamp := base.Camps[idCamp] 570 var docs []documents.PublicDocument 571 envois := rawCamp.Envois 572 acCamp := base.NewCamp(idCamp) 573 if !envois.Locked { 574 for _, m := range acCamp.GetRegisteredDocuments(envois.LettreDirecteur, true) { 575 up, err := documents.PublieDocument(ct.Signing, host, m) 576 if err != nil { 577 return nil, err 578 } 579 docs = append(docs, up) 580 } 581 if envois.ListeParticipants { 582 doc, err := documents.MetaDoc{IdCamp: idCamp, Categorie: documents.ListeParticipants}.Share(ct.Signing, host) 583 if err != nil { 584 return nil, err 585 } 586 docs = append(docs, doc) 587 } 588 if envois.ListeVetements { 589 doc, err := documents.MetaDoc{IdCamp: idCamp, Categorie: documents.ListeVetements}.Share(ct.Signing, host) 590 if err != nil { 591 return nil, err 592 } 593 docs = append(docs, doc) 594 } 595 } 596 mailDirecteur := "" 597 if directeur, hasDirecteur := acCamp.GetDirecteur(); hasDirecteur { 598 mailDirecteur = directeur.RawData().Mail.String() 599 } 600 pCamp := CampPlus{ 601 Documents: docs, 602 MailDirecteur: mailDirecteur, 603 } 604 pCamp.Camp.From(rawCamp) 605 out[rawCamp.Id] = pCamp 606 } 607 return out, nil 608 } 609 610 func (ct Controller) compileStructureaides(base *dm.BaseLocale) (map[string]Structureaide, map[int64]string, error) { 611 out := make(map[string]Structureaide, len(base.Structureaides)) 612 structures := make(map[int64]string, len(base.Structureaides)) 613 614 for _, sa := range base.Structureaides { 615 publicSa, err := ct.newStructureaide(sa) 616 if err != nil { 617 return nil, nil, err 618 } 619 structures[sa.Id] = publicSa.IdCrypted 620 out[publicSa.IdCrypted] = publicSa 621 } 622 return out, structures, nil 623 } 624 625 // ne remplit les champs IdParticipant et IdStructure 626 func (ct Controller) shareAide(host string, accesAide dm.AccesAide) (Aide, error) { 627 rawAide := accesAide.RawData() 628 idC, err := shared.EncodeID(ct.Signing, shared.OrAide, rawAide.Id) 629 if err != nil { 630 return Aide{}, err 631 } 632 docs := accesAide.GetDocuments() 633 var doc documents.PublicDocument 634 if len(docs) > 0 { 635 doc, err = documents.PublieDocument(ct.Signing, host, accesAide.Base.Documents[docs[0].Id.Int64()]) 636 if err != nil { 637 return Aide{}, err 638 } 639 } 640 return Aide{ 641 ChampsAideEditables: ChampsAideEditables{ 642 IdCrypted: idC, 643 NbJoursMax: rawAide.NbJoursMax, 644 Valeur: rd.Euros(rawAide.Valeur.Round()), 645 ParJour: rawAide.ParJour, 646 }, 647 Valid: rawAide.Valide, 648 ValeurComputed: rd.Euros(accesAide.ValeurEffective(true).Round()), 649 Document: doc, 650 }, nil 651 } 652 653 func (ct Controller) compileAides(host, idParticipantCrypted string, structures map[int64]string, aides []dm.AccesAide) ([]Aide, error) { 654 out := make([]Aide, len(aides)) 655 for index, accesAide := range aides { 656 aide, err := ct.shareAide(host, accesAide) 657 if err != nil { 658 return nil, err 659 } 660 aide.IdParticipantCrypted = idParticipantCrypted 661 aide.IdStructureCrypted = structures[accesAide.RawData().IdStructureaide] 662 out[index] = aide 663 } 664 return out, nil 665 } 666 667 func (ct Controller) compileSondages(sondages rd.Sondages) (map[int64]PublicSondage, error) { 668 out := make(map[int64]PublicSondage) 669 for _, sondage := range sondages { 670 publicSd, err := ct.newPublicSondage(sondage) 671 if err != nil { 672 return nil, err 673 } 674 out[sondage.IdCamp] = publicSd 675 } 676 return out, nil 677 } 678 679 func (ct Controller) loadDataFicheSanitaire(idFacture int64, idCrypted string) (respo, pers rd.Personne, err error) { 680 id, err := shared.DecodeID(ct.Signing, idCrypted, shared.OrPersonne) 681 if err != nil { 682 err = shared.FormatErr("Le participant n'a pu être identifié.", err) 683 return 684 } 685 686 row := ct.DB.QueryRow(`SELECT personnes.* FROM personnes 687 JOIN factures ON factures.id_personne = personnes.id 688 WHERE factures.id = $1`, idFacture) 689 respo, err = rd.ScanPersonne(row) 690 if err != nil { 691 err = shared.FormatErr("Le responsable légal est introuvable.", err) 692 return 693 } 694 pers, err = rd.SelectPersonne(ct.DB, id) 695 if err != nil { 696 err = shared.FormatErr("Le participant est introuvable.", err) 697 return 698 } 699 return respo, pers, nil 700 } 701 702 func (ct Controller) lienPartageFicheSanitaire(host, mail string, idPersonne int64) (string, error) { 703 m := partageFicheSanitaire{Mail: mail, IdPersonne: idPersonne} 704 s, err := shared.Encode(ct.Signing, m) 705 if err != nil { 706 return "", shared.FormatErr("Erreur pendant le cryptage des données.", err) 707 } 708 lien := shared.BuildUrl(host, EndPointPartageFicheSanitaire, map[string]string{ 709 "target": s, 710 }) 711 return lien, nil 712 } 713 714 // SendMailPartageFicheSanitaire envoie un mail aux adresses autorisées par la fiche sanitaire, 715 // demandant d'inclure `respoMail`. Le mail contient une url relative à `host`. 716 func (ct Controller) SendMailPartageFicheSanitaire(host, respoMail string, participant rd.Personne) ([]string, error) { 717 urlDebloque, err := ct.lienPartageFicheSanitaire(host, respoMail, participant.Id) 718 if err != nil { 719 return nil, err 720 } 721 html, err := mails.NewDebloqueFicheSanitaire(urlDebloque, respoMail, participant.NomPrenom().String()) 722 if err != nil { 723 return nil, shared.FormatErr("La création du mail a échoué", err) 724 } 725 pool, err := mails.NewPool(ct.SMTP, nil) 726 if err != nil { 727 return nil, shared.FormatErr("Le serveur de mail est indisponible", err) 728 } 729 defer pool.Close() 730 var errs []string 731 for _, mail := range participant.FicheSanitaire.Mails { 732 if err = pool.SendMail(mail, "[ACVE] - Partage d'une fiche sanitaire", html, nil, nil); err != nil { 733 errs = append(errs, err.Error()) 734 } 735 } 736 return errs, nil 737 } 738 739 func (ct Controller) validePartageFicheSanitaire(target string) error { 740 var args partageFicheSanitaire 741 if err := shared.Decode(ct.Signing, target, &args); err != nil { 742 return shared.FormatErr("Le lien semble invalide.", err) 743 } 744 pers, err := rd.SelectPersonne(ct.DB, args.IdPersonne) 745 if err != nil { 746 return shared.FormatErr("Le participant est introuvable.", err) 747 } 748 uniq := rd.StringSet{} 749 for _, mail := range pers.FicheSanitaire.Mails { 750 uniq[mail] = true 751 } 752 uniq[args.Mail] = true 753 newMails := pq.StringArray(uniq.ToList()) 754 _, err = ct.DB.Query("UPDATE personnes SET fiche_sanitaire = jsonb_set(fiche_sanitaire, '{mails}', array_to_json($1::varchar[])::jsonb)"+ 755 "WHERE id = $2", newMails, args.IdPersonne) 756 if err != nil { 757 return shared.FormatErr("La mise à jour des propriétaires de la fiche a échoué.", err) 758 } 759 return nil 760 } 761 762 func (ct Controller) updateFicheSanitaire(idFacture int64, params InFicheSanitaire) (OutFicheSanitaire, error) { 763 var out OutFicheSanitaire 764 respo, pers, err := ct.loadDataFicheSanitaire(idFacture, params.IdCrypted) 765 if err != nil { 766 return out, err 767 } 768 if isFicheSanitaireLocked(respo.Mail.String(), pers.FicheSanitaire.Mails) { // impossible normalement 769 return out, errors.New("La fiche sanitaire est verrouillée !") 770 } 771 if pers.IsTemporaire { // impossible normalement 772 return out, errors.New("La modification de la fiche sanitaire est désactivée pour les profils temporaires.") 773 } 774 if len(pers.FicheSanitaire.Mails) == 0 { 775 // le premier responsable à modifier devient le proprio de la fiche 776 params.FicheSanitaire.Mails = []string{respo.Mail.String()} 777 } 778 params.FicheSanitaire.LastModif = rd.Time(time.Now()) 779 tx, err := ct.DB.Begin() 780 if err != nil { 781 return out, shared.FormatErr("La base de données est injoinable.", err) 782 } 783 row := tx.QueryRow("UPDATE personnes SET securite_sociale = $1 WHERE id = $2 RETURNING *", 784 params.SecuriteSociale, respo.Id) 785 respo, err = rd.ScanPersonne(row) 786 if err != nil { 787 err = shared.FormatErr("Erreur pendant la mise à jour du numéro de sécurité sociale.", err) 788 return out, shared.Rollback(tx, err) 789 } 790 row = tx.QueryRow("UPDATE personnes SET fiche_sanitaire = $1 WHERE id = $2 RETURNING *", params.FicheSanitaire, pers.Id) 791 pers, err = rd.ScanPersonne(row) 792 if err != nil { 793 err = shared.FormatErr("Erreur pendant l'enregistrement de la fiche sanitaire.", err) 794 return out, shared.Rollback(tx, err) 795 } 796 if err = tx.Commit(); err != nil { 797 return out, shared.FormatErr("Erreur pendant l'enregistrement de la fiche sanitaire.", err) 798 } 799 out.SecuriteSociale = respo.SecuriteSociale 800 out.FicheSanitaire.FicheSanitaire = pers.FicheSanitaire 801 return out, nil 802 } 803 804 func (ct Controller) updateOptionsParticipants(participants []Participant) error { 805 tx, err := ct.DB.Begin() 806 if err != nil { 807 return err 808 } 809 for _, part := range participants { 810 id, err := shared.DecodeID(ct.Signing, part.IdCrypted, shared.OrParticipant) 811 if err != nil { 812 err = shared.FormatErr("Les identifiants sont corrompus.", err) 813 return shared.Rollback(tx, err) 814 } 815 row := tx.QueryRow(`SELECT * FROM camps 816 JOIN participants ON participants.id_camp = camps.id 817 WHERE participants.id = $1`, id) 818 camp, err := rd.ScanCamp(row) 819 if err != nil { 820 err = shared.FormatErr("Le camp est introuvable.", err) 821 return shared.Rollback(tx, err) 822 } 823 if time.Until(camp.DateDebut.Time()) < UpdateLimitation { 824 err = fmt.Errorf("Les modifications sur le séjour %s sont maintenant désactivées.", camp.Label()) 825 return shared.Rollback(tx, err) 826 } 827 _, err = tx.Exec("UPDATE participants SET options = $1 WHERE id = $2", part.Options, id) 828 if err != nil { 829 err = shared.FormatErr("La modification des options a échoué.", err) 830 return shared.Rollback(tx, err) 831 } 832 } 833 834 if err = tx.Commit(); err != nil { 835 return shared.FormatErr("Les modifications n'ont pas pu être enregistrées.", err) 836 } 837 return nil 838 } 839 840 func (ct Controller) loadJoomeo(idFacture int64) (JoomeoOutput, error) { 841 var out JoomeoOutput 842 row := ct.DB.QueryRow(`SELECT mail FROM personnes 843 JOIN factures ON factures.id_personne = personnes.id 844 WHERE factures.id = $1`, idFacture) 845 var mail string 846 if err := row.Scan(&mail); err != nil { 847 return out, shared.FormatErr("L'adresse mail liée à votre dossier n'a pu être retrouvée.", err) 848 } 849 850 api, err := joomeo.InitApi(ct.joomeo) 851 if err != nil { 852 return out, shared.FormatErr("Le serveur Joomeo est indisponible.", err) 853 } 854 defer api.Kill() 855 out.UrlSpace = api.SpaceURL 856 857 contact, albums, err := api.GetLoginFromMail(mail) 858 if err != nil { 859 return out, shared.FormatErr("L'accès à vos informations Joomeo a échoué.", err) 860 } 861 out.Loggin = contact.Login 862 out.Password = contact.Password 863 out.Albums = albums 864 return out, nil 865 } 866 867 // dispatch entre acquittée ou non 868 func (ct Controller) downloadFacture(idFacture int64, indexDestinataire int) ([]byte, string, error) { 869 // on rassemble les données 870 base, err := ct.loadData(idFacture) 871 if err != nil { 872 return nil, "", err 873 } 874 ac := base.NewFacture(idFacture) 875 // dans tous les cas, on crée le pdf en mémoire vive 876 destinataire, err := ac.ChoixDestinataires().Index(indexDestinataire) 877 if err != nil { 878 return nil, "", err 879 } 880 meta := cd.Facture{ 881 Destinataire: destinataire, 882 Bilan: ac.EtatFinancier(dm.CacheEtatFinancier{}, false), 883 } 884 file, err := meta.Generate() 885 if err != nil { 886 return nil, "", err 887 } 888 if ac.EtatFinancier(dm.CacheEtatFinancier{}, false).IsAcquitte() { 889 err = ct.markFactureAcquittee(idFacture, base.Messages, base.MessageAttestations) 890 } else { 891 err = ct.markFactureCourante(base.Messages) 892 } 893 return file, meta.FileName(), err 894 } 895 896 // met à jour les message concernés : messages "Facture" 897 // n'ayant pas encore été modifiés 898 func (ct Controller) markFactureCourante(messages rd.Messages) error { 899 // logique côté serveur 900 var ids rd.Ids 901 for _, message := range messages { 902 isNotModified := message.Modified.Time().IsZero() 903 isFacture := message.Kind == rd.MFacture 904 if isFacture && isNotModified { 905 ids = append(ids, message.Id) 906 } 907 } 908 _, err := ct.DB.Exec("UPDATE messages SET modified = now() WHERE id = ANY($1)", ids.AsSQL()) 909 return err 910 } 911 912 // met à jour les messages concernés 913 func (ct Controller) markFactureAcquittee(idFacture int64, messages rd.Messages, messageAttestations map[int64]rd.MessageAttestation) error { 914 return ct.markAttestation(rd.MFactureAcquittee, idFacture, messages, messageAttestations) 915 } 916 917 // indexDestinataire fait référence à ChoixDestinataire 918 func (ct Controller) downloadAttestationPresence(idFacture int64, indexDestinataire int) ([]byte, error) { 919 // on rassemble les données 920 base, err := ct.loadData(idFacture) 921 if err != nil { 922 return nil, err 923 } 924 ac := base.NewFacture(idFacture) 925 // on vérifie que le séjour est terminé pour tout les participants inscrits 926 now := time.Now() 927 for _, part := range ac.GetDossiers(nil) { 928 if part.RawData().ListeAttente.IsInscrit() && part.GetCamp().RawData().DateFin.Time().After(now) { 929 return nil, fmt.Errorf("Le séjour %s n'est pas encore terminé.", part.GetCamp().RawData().Label()) 930 } 931 } 932 destinataire, err := ac.ChoixDestinataires().Index(indexDestinataire) 933 if err != nil { 934 return nil, err 935 } 936 meta := cd.Presence{ 937 Destinataire: destinataire, 938 // uniquement inscrits 939 Participants: ac.EtatFinancier(dm.CacheEtatFinancier{}, false).Participants, 940 } 941 doc, err := meta.Generate() 942 if err != nil { 943 return nil, err 944 } 945 err = ct.markAttestation(rd.MAttestationPresence, idFacture, base.Messages, base.MessageAttestations) 946 return doc, err 947 } 948 949 // kind est FactureAc ou AttestationPres 950 func (ct Controller) markAttestation(targetKind rd.MessageKind, idFacture int64, messages rd.Messages, messageAttestations map[int64]rd.MessageAttestation) error { 951 // on cherche les nouveaux mails annoncant une attestation 952 var ids rd.Ids 953 for _, message := range messages { 954 isAttest := message.Kind == targetKind 955 isMail := messageAttestations[message.Id].Distribution == rd.DMail 956 if isAttest && isMail { 957 ids = append(ids, message.Id) 958 } 959 } 960 961 tx, err := ct.DB.Begin() 962 if err != nil { 963 return err 964 } 965 966 if len(ids) == 0 { 967 // il n'y a pas de nouveau mail, on crée un message 968 // pour garder trace du téléchargement 969 message := rd.Message{ 970 IdFacture: idFacture, 971 Created: rd.Time(time.Now()), 972 Kind: targetKind, 973 } 974 message, err = message.Insert(tx) 975 if err != nil { 976 return shared.Rollback(tx, err) 977 } 978 dist := rd.MessageAttestation{IdMessage: message.Id, Distribution: rd.DEspacePerso, GuardKind: message.Kind} 979 err = rd.InsertManyMessageAttestations(tx, dist) 980 } else { 981 // on met à jour les messages courants ... 982 _, err = tx.Exec("UPDATE messages SET modified = now() WHERE id = ANY($1)", ids.AsSQL()) 983 if err != nil { 984 return shared.Rollback(tx, err) 985 } 986 // ... et les compléments 987 _, err = tx.Exec("UPDATE message_attestations SET distribution = $1 WHERE id_message = ANY($2)", rd.DMailAndDownload, ids.AsSQL()) 988 } 989 if err != nil { 990 return shared.Rollback(tx, err) 991 } 992 err = tx.Commit() 993 return err 994 } 995 996 func (ct Controller) markConnection(idFacture int64) error { 997 _, err := ct.DB.Exec("UPDATE factures SET last_connection = now() WHERE id = $1", idFacture) 998 return err 999 } 1000 1001 // vérifie que le camp est "ouvert au sondage" pour le dossier 1002 func (ct Controller) isSondageOpen(idFacture, idCamp int64) (bool, error) { 1003 rows, err := ct.DB.Query(`SELECT message_sondages.* FROM message_sondages 1004 JOIN messages ON message_sondages.id_message = messages.id 1005 WHERE messages.id_facture = $1 AND messages.kind = $2`, idFacture, rd.MSondage) 1006 if err != nil { 1007 return false, err 1008 } 1009 messages, err := rd.ScanMessageSondages(rows) 1010 if err != nil { 1011 return false, err 1012 } 1013 for _, message := range messages { 1014 if message.IdCamp == idCamp { 1015 return true, nil 1016 } 1017 } 1018 return false, nil 1019 } 1020 1021 // update or create 1022 func (ct Controller) saveSondage(host string, idFacture int64, sondage PublicSondage) (PublicSondage, error) { 1023 // on vérifie que le camp est "ouvert au sondage" pour le dossier 1024 isOpen, err := ct.isSondageOpen(idFacture, sondage.IdCamp) 1025 if err != nil { 1026 return PublicSondage{}, fmt.Errorf("Impossible de vérifier l'état du séjour : %s", err) 1027 } 1028 if !isOpen { 1029 return PublicSondage{}, fmt.Errorf("Le séjour (%d) n'enregistre pas de retours.", sondage.IdCamp) 1030 } 1031 1032 var sd rd.Sondage 1033 tx, err := ct.DB.Begin() 1034 if err != nil { 1035 return PublicSondage{}, err 1036 } 1037 1038 if sondage.IdCrypted == "" { // créé un retour 1039 sd = rd.Sondage{ 1040 IdCamp: sondage.IdCamp, 1041 IdFacture: idFacture, 1042 Modified: time.Now(), 1043 RepSondage: sondage.RepSondage, 1044 } 1045 sd, err = sd.Insert(tx) 1046 if err != nil { 1047 return PublicSondage{}, shared.Rollback(tx, err) 1048 } 1049 } else { // on décrypte l'id et on modifie 1050 var id int64 1051 id, err = shared.DecodeID(ct.Signing, sondage.IdCrypted, shared.OrSondage) 1052 if err != nil { 1053 return PublicSondage{}, shared.Rollback(tx, err) 1054 } 1055 sd, err = rd.SelectSondage(tx, id) 1056 if err != nil { 1057 return PublicSondage{}, shared.Rollback(tx, err) 1058 } 1059 // sécurité : on modifie juste le contenu 1060 sd.RepSondage = sondage.RepSondage 1061 sd.Modified = time.Now() 1062 sd, err = sd.Update(tx) 1063 if err != nil { 1064 return PublicSondage{}, shared.Rollback(tx, err) 1065 } 1066 } 1067 // Si le mail de notification échoue, on préfère annuler 1068 // le sondage, pour être certain que les sondages enregistrés 1069 // sont bien traités 1070 if err := ct.sondageNotifier.Notifie(host, sd); err != nil { 1071 return PublicSondage{}, shared.Rollback(tx, err) 1072 } 1073 out, err := ct.newPublicSondage(sd) 1074 if err != nil { 1075 return PublicSondage{}, shared.Rollback(tx, err) 1076 } 1077 err = tx.Commit() 1078 return out, err 1079 }