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  }