github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/server/acvegestion/controller.go (about)

     1  // Expose les fonctionnalités requises par le client 'lourd' Controller
     2  // Le client implémente sa propre logique en terme de lecture des données,
     3  // mais délègue les modifications au serveur.
     4  package acvegestion
     5  
     6  import (
     7  	"database/sql"
     8  	"encoding/json"
     9  	"fmt"
    10  	"net/http"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/benoitkugler/goACVE/server/logiciel"
    15  	"github.com/benoitkugler/goACVE/server/shared"
    16  
    17  	"github.com/benoitkugler/goACVE/logs"
    18  	"github.com/benoitkugler/goACVE/server/core/apiserver"
    19  	rd "github.com/benoitkugler/goACVE/server/core/rawdata"
    20  	"github.com/benoitkugler/goACVE/server/core/rawdata/composites"
    21  	"github.com/benoitkugler/goACVE/server/core/utils/mails"
    22  	"github.com/labstack/echo"
    23  )
    24  
    25  type FicheSanitaireNotifier interface {
    26  	SendMailPartageFicheSanitaire(host, respoMail string, participant rd.Personne) ([]string, error)
    27  }
    28  
    29  type Controller struct {
    30  	shared.Controller
    31  
    32  	FicheSanitaireNotifier FicheSanitaireNotifier
    33  	ContraintesEquipiers   rd.Contraintes
    34  }
    35  
    36  func authentifie(context echo.Context) error {
    37  	username, password, ok := context.Request().BasicAuth()
    38  	if ok && username == apiserver.BasicAuthUsername && password == logs.APIKEY {
    39  		return nil
    40  	}
    41  	return echo.ErrUnauthorized
    42  }
    43  
    44  // SertUpdateInfos renvoie les versions disponibles
    45  func SertUpdateInfos(c echo.Context) error {
    46  	infos, err := logiciel.GetUpdateInfos(c.Param("platform"))
    47  	if err != nil {
    48  		return err
    49  	}
    50  	return c.JSONPretty(200, infos, "  ")
    51  }
    52  
    53  // SertUpdate renvoie la mise à jour compressée correspondant à la demande
    54  func SertUpdate(c echo.Context) error {
    55  	if err := authentifie(c); err != nil {
    56  		return err
    57  	}
    58  	path, err := logiciel.GetUpdate(c.Param("platform"))
    59  	if err != nil {
    60  		return err
    61  	}
    62  	return c.File(path)
    63  }
    64  
    65  // db est utile pour trouver le contact
    66  func (ct Controller) mailFromMessage(host string, db rd.DB, message rd.Message) error {
    67  	facture, err := rd.SelectFacture(db, message.IdFacture)
    68  	if err != nil {
    69  		return err
    70  	}
    71  	personne, err := rd.SelectPersonne(db, facture.IdPersonne)
    72  	if err != nil {
    73  		return err
    74  	}
    75  	subject := fmt.Sprintf("[ACVE] - %s", message.Kind.MailTitle())
    76  	contact := mails.Contact{
    77  		Prenom: personne.FPrenom(),
    78  		Sexe:   personne.Sexe,
    79  	}
    80  	contenu := contenus[message.Kind]
    81  	url := shared.BuildUrl(host, facture.UrlEspacePerso("espace_perso"), nil)
    82  	body, err := mails.NewNotifieMessage(contact, message.Kind.MailTitle(), contenu, url)
    83  	if err != nil {
    84  		return err
    85  	}
    86  	to := personne.Mail.String()
    87  	err = mails.NewMailer(ct.SMTP).SendMail(to, subject, body, facture.CopiesMails, nil)
    88  	return err
    89  }
    90  
    91  // ajoute un message et envoie un mail
    92  func (ct Controller) creeMessageCentre(host string, idFacture int64, contenu rd.String) (apiserver.NotifieMessageOut, error) {
    93  	tx, err := ct.DB.Begin()
    94  	if err != nil {
    95  		return apiserver.NotifieMessageOut{}, err
    96  	}
    97  	var out apiserver.NotifieMessageOut
    98  	out.Message = rd.Message{IdFacture: idFacture, Kind: rd.MCentre, Created: rd.Time(time.Now())}
    99  	out.Message, err = out.Message.Insert(tx)
   100  	if err != nil {
   101  		return out, shared.Rollback(tx, err)
   102  	}
   103  	out.MessageMessage = rd.MessageMessage{IdMessage: out.Message.Id, Contenu: contenu, GuardKind: rd.MCentre}
   104  	err = rd.InsertManyMessageMessages(tx, out.MessageMessage)
   105  	if err != nil {
   106  		return out, shared.Rollback(tx, err)
   107  	}
   108  	err = ct.mailFromMessage(host, tx, out.Message)
   109  	if err != nil {
   110  		return out, shared.Rollback(tx, err)
   111  	}
   112  	err = tx.Commit()
   113  	return out, err
   114  }
   115  
   116  func (ct Controller) editMessageCentre(params apiserver.EditMessageIn) (apiserver.EditMessageOut, error) {
   117  	var out apiserver.EditMessageOut
   118  	tx, err := ct.DB.Begin()
   119  	if err != nil {
   120  		return out, err
   121  	}
   122  	rows, err := tx.Query("UPDATE message_messages SET contenu = $1 WHERE id_message = ANY($2) RETURNING *", params.Contenu, params.IdMessages.AsSQL())
   123  	if err != nil {
   124  		return out, shared.Rollback(tx, err)
   125  	}
   126  	out.MessageMessages, err = rd.ScanMessageMessages(rows)
   127  	if err != nil {
   128  		return out, shared.Rollback(tx, err)
   129  	}
   130  	rows, err = tx.Query("UPDATE messages SET modified = now() WHERE id = ANY($1) RETURNING *", params.IdMessages.AsSQL())
   131  	if err != nil {
   132  		return out, shared.Rollback(tx, err)
   133  	}
   134  	out.Messages, err = rd.ScanMessages(rows)
   135  	if err != nil {
   136  		return out, shared.Rollback(tx, err)
   137  	}
   138  	err = tx.Commit()
   139  	return out, err
   140  }
   141  
   142  func (ct Controller) createMessageMessage(params apiserver.CreateMessageMessage) (apiserver.CreateMessageMessage, error) {
   143  	tx, err := ct.DB.Begin()
   144  	if err != nil {
   145  		return params, err
   146  	}
   147  	params.Message, err = params.Message.Insert(tx)
   148  	if err != nil {
   149  		return params, shared.Rollback(tx, err)
   150  	}
   151  	params.MessageMessage.IdMessage = params.Message.Id
   152  	err = rd.InsertManyMessageMessages(tx, params.MessageMessage)
   153  	if err != nil {
   154  		return params, shared.Rollback(tx, err)
   155  	}
   156  	err = tx.Commit()
   157  	return params, err
   158  }
   159  
   160  func (ct Controller) notifieSimple(host string, params apiserver.NotifieSimple) (rd.Message, error) {
   161  	tx, err := ct.DB.Begin()
   162  	if err != nil {
   163  		return rd.Message{}, err
   164  	}
   165  	message := rd.Message{IdFacture: params.IdFacture, Kind: params.Kind, Created: rd.Time(time.Now())}
   166  	message, err = message.Insert(tx)
   167  	if err != nil {
   168  		return rd.Message{}, shared.Rollback(tx, err)
   169  	}
   170  	err = ct.mailFromMessage(host, tx, message)
   171  	if err != nil {
   172  		return rd.Message{}, shared.Rollback(tx, err)
   173  	}
   174  	err = tx.Commit()
   175  	return message, err
   176  }
   177  
   178  func (ct Controller) notifiePlaceLiberee(host string, params apiserver.NotifiePlaceLibereeIn) (apiserver.NotifiePlaceLibereeOut, error) {
   179  	var out apiserver.NotifiePlaceLibereeOut
   180  
   181  	tx, err := ct.DB.Begin()
   182  	if err != nil {
   183  		return out, err
   184  	}
   185  
   186  	// on met à jour le participant
   187  	participant, err := rd.SelectParticipant(tx, params.IdParticipant)
   188  	if err != nil {
   189  		return out, shared.Rollback(tx, err)
   190  	}
   191  	if participant.IdFacture.IsNil() {
   192  		err = fmt.Errorf("Le participant (%d) n'est pas lié à un dossier !", participant.Id)
   193  		return out, shared.Rollback(tx, err)
   194  	}
   195  
   196  	participant.ListeAttente.Statut = params.NewStatut
   197  	out.Participant, err = participant.Update(tx)
   198  	if err != nil {
   199  		return out, shared.Rollback(tx, err)
   200  	}
   201  
   202  	// on insert le message et ses détails
   203  	out.Message = rd.Message{
   204  		IdFacture: participant.IdFacture.Int64,
   205  		Kind:      rd.MPlaceLiberee,
   206  		Created:   rd.Time(time.Now()),
   207  	}
   208  	out.Message, err = out.Message.Insert(tx)
   209  	if err != nil {
   210  		return out, shared.Rollback(tx, err)
   211  	}
   212  	out.Details = rd.MessagePlacelibere{
   213  		IdMessage:     out.Message.Id,
   214  		IdParticipant: participant.Id,
   215  	}
   216  	err = rd.InsertManyMessagePlaceliberes(tx, out.Details)
   217  	if err != nil {
   218  		return out, shared.Rollback(tx, err)
   219  	}
   220  
   221  	// finalement on envoie le mail
   222  	err = ct.mailFromMessage(host, tx, out.Message)
   223  	if err != nil {
   224  		return out, shared.Rollback(tx, err)
   225  	}
   226  
   227  	err = tx.Commit()
   228  	return out, err
   229  }
   230  
   231  func (ct Controller) notifieAttestation(host string, params apiserver.NotifieAttestationIn) (apiserver.NotifieAttestationOut, error) {
   232  	var out apiserver.NotifieAttestationOut
   233  	if !(params.Kind == rd.MFactureAcquittee || params.Kind == rd.MAttestationPresence) {
   234  		return out, fmt.Errorf("Un message d'attestation est attendu : reçu %s", params.Kind)
   235  	}
   236  
   237  	tx, err := ct.DB.Begin()
   238  	if err != nil {
   239  		return out, err
   240  	}
   241  	out.Message = rd.Message{IdFacture: params.IdFacture, Kind: params.Kind, Created: rd.Time(time.Now())}
   242  	out.Message, err = out.Message.Insert(tx)
   243  	if err != nil {
   244  		return out, shared.Rollback(tx, err)
   245  	}
   246  	out.MessageAttestation = rd.MessageAttestation{IdMessage: out.Message.Id, Distribution: rd.DMail, GuardKind: out.Message.Kind}
   247  	err = rd.InsertManyMessageAttestations(tx, out.MessageAttestation)
   248  	if err != nil {
   249  		return out, shared.Rollback(tx, err)
   250  	}
   251  	err = ct.mailFromMessage(host, tx, out.Message)
   252  	if err != nil {
   253  		return out, shared.Rollback(tx, err)
   254  	}
   255  	err = tx.Commit()
   256  	return out, err
   257  }
   258  
   259  type streamMailer interface {
   260  	// streamOut must never be nil
   261  	insertMessageComplement(message rd.Message, tx *sql.Tx) (interface{}, error)
   262  }
   263  
   264  type streamMessage struct {
   265  	contenu rd.String
   266  }
   267  
   268  func (b streamMessage) insertMessageComplement(message rd.Message, tx *sql.Tx) (interface{}, error) {
   269  	var (
   270  		out apiserver.NotifieManyOut
   271  		err error
   272  	)
   273  	out.Message = message
   274  	out.MessageMessage = rd.MessageMessage{IdMessage: out.Message.Id, Contenu: b.contenu, GuardKind: rd.MCentre}
   275  	err = rd.InsertManyMessageMessages(tx, out.MessageMessage)
   276  	return out, err
   277  }
   278  
   279  // checkVerrouDocuments renvoie une erreur si l'envoi est encore vérouillé
   280  func (ct Controller) checkVerrouDocuments(idCamp int64) error {
   281  	camp, err := rd.SelectCamp(ct.DB, idCamp)
   282  	if err != nil {
   283  		return err
   284  	}
   285  	return camp.CheckEnvoisLock()
   286  }
   287  
   288  type streamDocument struct {
   289  	idCamp int64
   290  }
   291  
   292  // vérifie que tous les dossiers ont au moins un participant
   293  // au séjour (pas en liste d'attente)
   294  func (b streamDocument) checkListeAttente(factures rd.Ids, db rd.DB) error {
   295  	participants, err := rd.SelectParticipantsByIdFactures(db, factures...)
   296  	if err != nil {
   297  		return err
   298  	}
   299  	liens, _ := participants.Resoud() // par facture
   300  	for _, idFacture := range factures {
   301  		isOK := false
   302  		for _, idParticipant := range liens[idFacture] {
   303  			part := participants[idParticipant]
   304  			isInscritCamp := part.IdCamp == b.idCamp && part.ListeAttente.IsInscrit()
   305  			if isInscritCamp {
   306  				isOK = true
   307  				break
   308  			}
   309  		}
   310  		if !isOK {
   311  			return fmt.Errorf("Un dossier (id %d) n'a aucun participant en liste principale.", idFacture)
   312  		}
   313  	}
   314  	return nil
   315  }
   316  
   317  func (b streamDocument) insertMessageComplement(message rd.Message, tx *sql.Tx) (interface{}, error) {
   318  	var (
   319  		out apiserver.NotifieDocumentsOut
   320  		err error
   321  	)
   322  	out.Message = message
   323  	out.MessageCamp = rd.MessageDocument{IdMessage: out.Message.Id, IdCamp: b.idCamp}
   324  	err = rd.InsertManyMessageDocuments(tx, out.MessageCamp)
   325  	return out, err
   326  }
   327  
   328  type streamSondage struct {
   329  	idCamp int64
   330  }
   331  
   332  func (b streamSondage) insertMessageComplement(message rd.Message, tx *sql.Tx) (interface{}, error) {
   333  	var (
   334  		out apiserver.NotifieSondagesOut
   335  		err error
   336  	)
   337  	out.Message = message
   338  	out.MessageSondage = rd.MessageSondage{IdMessage: out.Message.Id, IdCamp: b.idCamp}
   339  	err = rd.InsertManyMessageSondages(tx, out.MessageSondage)
   340  	return out, err
   341  }
   342  
   343  func (ct Controller) notifieManyMessages(host string, mailer streamMailer, message rd.Message, dossiers rd.Ids, resp *echo.Response) error {
   344  	pool, err := mails.NewPool(ct.SMTP, nil)
   345  	if err != nil {
   346  		return err
   347  	}
   348  	defer pool.Close()
   349  
   350  	// Streaming status between each mails
   351  	resp.Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
   352  	resp.WriteHeader(http.StatusOK)
   353  
   354  	oneMail := func(message rd.Message) (interface{}, error) {
   355  		tx, err := ct.DB.Begin()
   356  		if err != nil {
   357  			return nil, err
   358  		}
   359  
   360  		message, err = message.Insert(tx)
   361  		if err != nil {
   362  			return nil, shared.Rollback(tx, err)
   363  		}
   364  		out, err := mailer.insertMessageComplement(message, tx)
   365  		if err != nil {
   366  			return out, shared.Rollback(tx, err)
   367  		}
   368  
   369  		err = ct.mailFromMessage(host, tx, message)
   370  		if err != nil {
   371  			return out, shared.Rollback(tx, err)
   372  		}
   373  		err = tx.Commit()
   374  		return out, err
   375  	}
   376  
   377  	message.Created = rd.Time(time.Now())
   378  	for _, dossier := range dossiers {
   379  		// on duplique le message
   380  		message.IdFacture = dossier
   381  
   382  		data, err := oneMail(message)
   383  		out := apiserver.StreamOut{Data: data}
   384  		if err != nil {
   385  			out.Error = err.Error()
   386  		}
   387  
   388  		if err := json.NewEncoder(resp).Encode(out); err != nil {
   389  			// impossible d'écrire le résultat, on interrompt complètement
   390  			return err
   391  		}
   392  		resp.Flush()
   393  	}
   394  	return nil
   395  }
   396  
   397  // supprime aussi les aides (et leurs justificatifs)
   398  // si le participant est temporaire, supprime aussi la personne associée
   399  // ne commit pas, ne rollback pas
   400  func deleteOneParticipant(tx rd.DB, id int64) (apiserver.DeleteParticipantOut, error) {
   401  	out := apiserver.DeleteParticipantOut{IdParticipant: id}
   402  
   403  	// liens documents
   404  	rows, err := tx.Query(`DELETE FROM document_aides 
   405  	USING aides WHERE aides.id = document_aides.id_aide AND aides.id_participant = $1
   406  	RETURNING document_aides.id_document`, id)
   407  	if err != nil {
   408  		return out, err
   409  	}
   410  	out.IdsDocuments, err = rd.ScanIds(rows)
   411  	if err != nil {
   412  		return out, err
   413  	}
   414  
   415  	_, err = rd.DeleteDocumentsByIds(tx, out.IdsDocuments...)
   416  	if err != nil {
   417  		return out, err
   418  	}
   419  
   420  	out.IdsAides, err = rd.DeleteAidesByIdParticipants(tx, id)
   421  	if err != nil {
   422  		return out, err
   423  	}
   424  
   425  	// on supprime aussi les messages Place libérée
   426  	rows, err = tx.Query("DELETE FROM message_placeliberes WHERE id_participant = $1 RETURNING id_message", id)
   427  	if err != nil {
   428  		return out, err
   429  	}
   430  	idMessages, err := rd.ScanIds(rows)
   431  	if err != nil {
   432  		return out, err
   433  	}
   434  	out.IdsMessages, err = rd.DeleteMessagesByIds(tx, idMessages...)
   435  	if err != nil {
   436  		return out, err
   437  	}
   438  
   439  	part, err := rd.DeleteParticipantById(tx, id)
   440  	if err != nil {
   441  		return out, err
   442  	}
   443  
   444  	isTmp, docs, err := shared.DeletePersonne(tx, part.IdPersonne)
   445  	if err != nil {
   446  		return out, err
   447  	}
   448  	out.IdsDocuments = append(out.IdsDocuments, docs...)
   449  
   450  	// si la personne était temporaire
   451  	out.IdPersonne = -1
   452  	if isTmp {
   453  		out.IdPersonne = part.IdPersonne
   454  	}
   455  
   456  	return out, nil
   457  }
   458  
   459  // supprime le dossier, les paiments, les participants, les aides
   460  // et les personnes temporaires associés
   461  func (ct Controller) deleteInscription(idFacture int64) (apiserver.DeleteInscriptionOut, error) {
   462  	var out apiserver.DeleteInscriptionOut
   463  	tx, err := ct.DB.Begin()
   464  	if err != nil {
   465  		return out, err
   466  	}
   467  
   468  	out.IdsPaiements, err = rd.DeletePaiementsByIdFactures(tx, idFacture)
   469  	if err != nil {
   470  		return out, shared.Rollback(tx, err)
   471  	}
   472  
   473  	rows, err := tx.Query("SELECT id FROM participants WHERE id_facture = $1", idFacture)
   474  	if err != nil {
   475  		return out, shared.Rollback(tx, err)
   476  	}
   477  	idParts, err := rd.ScanIds(rows)
   478  	if err != nil {
   479  		return out, shared.Rollback(tx, err)
   480  	}
   481  	out.Participants = make([]apiserver.DeleteParticipantOut, len(idParts))
   482  	for index, idPart := range idParts {
   483  		out.Participants[index], err = deleteOneParticipant(tx, idPart)
   484  		if err != nil {
   485  			return out, shared.Rollback(tx, err)
   486  		}
   487  	}
   488  
   489  	facture, err := rd.DeleteFactureById(tx, idFacture)
   490  	if err != nil {
   491  		return out, shared.Rollback(tx, err)
   492  	}
   493  
   494  	// responsable
   495  	isTmp, docs, err := shared.DeletePersonne(tx, facture.IdPersonne)
   496  	if err != nil {
   497  		return out, shared.Rollback(tx, err)
   498  	}
   499  	out.IdsDocuments = docs
   500  	out.IdResponsable = facture.IdPersonne
   501  	if isTmp {
   502  		out.IdResponsable = -1
   503  	}
   504  	err = tx.Commit()
   505  	return out, err
   506  }
   507  
   508  // envoi un mail de confirmation après identification d'une inscription simple
   509  func (ct Controller) notifieInscriptionSimpleValide(part rd.Participantsimple) error {
   510  	personne, err := rd.SelectPersonne(ct.DB, part.IdPersonne)
   511  	if err != nil {
   512  		return err
   513  	}
   514  	camp, err := rd.SelectCamp(ct.DB, part.IdCamp)
   515  	if err != nil {
   516  		return err
   517  	}
   518  
   519  	mailHtml, err := mails.NewAccuseReceptionSimple(camp, mails.Contact{
   520  		Prenom: personne.FPrenom(),
   521  		Sexe:   personne.Sexe,
   522  	})
   523  	if err != nil {
   524  		return err
   525  	}
   526  	err = mails.NewMailer(ct.SMTP).SendMail(personne.Mail.String(), "[ACVE] Inscription validée", mailHtml,
   527  		nil, mails.CustomReplyTo(rd.CoordonnesCentre.Mail))
   528  	return err
   529  }
   530  
   531  // vérifie si les participants vont avoir besoin de débloquer la fiche sanitaire
   532  // Si oui, envoi un mail aux propriétaires
   533  func (ct Controller) checkNotifiePartageFicheSanitaire(host string, idFacture int64) error {
   534  	row := ct.DB.QueryRow(`SELECT factures.*, personnes.* FROM factures 
   535  		JOIN personnes ON factures.id_personne = personnes.id
   536  		WHERE factures.id = $1`, idFacture)
   537  	responsable, err := composites.ScanFacturePersonne(row)
   538  	if err != nil {
   539  		return err
   540  	}
   541  
   542  	rows, err := ct.DB.Query(`SELECT participants.*, personnes.* FROM participants 
   543  	JOIN personnes ON participants.id_personne = personnes.id
   544  	WHERE participants.id_facture = $1`, idFacture)
   545  	if err != nil {
   546  		return err
   547  	}
   548  	participants, err := composites.ScanParticipantPersonnes(rows)
   549  	if err != nil {
   550  		return err
   551  	}
   552  
   553  	withCamps, err := participants.LoadCamps(ct.DB)
   554  	if err != nil {
   555  		return err
   556  	}
   557  	lookup := participants.LookupToPersonnes()
   558  
   559  	// on se ramène aux personnes sous-jacentes et on s'assure de l'unicité
   560  	personnes := rd.Personnes{}
   561  	for _, part := range withCamps {
   562  		// on ignore les participants en liste d'attente
   563  		if !part.ListeAttente.IsInscrit() {
   564  			continue
   565  		}
   566  
   567  		personne := lookup[part.Participant.Id]
   568  		age := part.Camp.AgeDebutCamp(personne.BasePersonne)
   569  		if age.Age() >= 18 { // on ignore les participants majeurs
   570  			continue
   571  		}
   572  		personnes[part.IdPersonne] = personne
   573  	}
   574  
   575  	mailRespo := responsable.Personne.Mail.ToLower()
   576  	for _, personne := range personnes {
   577  		mailsKnown := personne.FicheSanitaire.Mails
   578  		isUnlocked := len(mailsKnown) == 0 // si aucun mail, on autorise, sinon on check
   579  		for _, mailOk := range mailsKnown {
   580  			if strings.ToLower(mailOk) == mailRespo {
   581  				// Ok, le responsable est autorisé
   582  				isUnlocked = true
   583  				break
   584  			}
   585  		}
   586  
   587  		if !isUnlocked { // on envoie un mail de déverouillage
   588  			mailErrors, err := ct.FicheSanitaireNotifier.SendMailPartageFicheSanitaire(host, mailRespo, personne)
   589  			if err != nil {
   590  				return err
   591  			}
   592  			// on résume l'erreur
   593  			if len(mailErrors) > 0 {
   594  				return fmt.Errorf("Erreur pendant l'envoi des mails de partage d'une fiche sanitaire : <br/> %s",
   595  					strings.Join(mailErrors, "<br/>"))
   596  			}
   597  		}
   598  	}
   599  	return nil
   600  }
   601  
   602  // rattache les paiements/participants/messages/sondages à l'autre dossier, et notifie (en option)
   603  func (ct Controller) fusionneFactures(host string, params apiserver.FusionneFacturesIn) (out apiserver.FusionneFacturesOut, err error) {
   604  	tx, err := ct.DB.Begin()
   605  	if err != nil {
   606  		return out, err
   607  	}
   608  
   609  	// participants
   610  	rows, err := tx.Query("UPDATE participants SET id_facture = $1 WHERE id_facture = $2 RETURNING *", params.To, params.From)
   611  	if err != nil {
   612  		return out, shared.Rollback(tx, err)
   613  	}
   614  	out.Participants, err = rd.ScanParticipants(rows)
   615  	if err != nil {
   616  		return out, shared.Rollback(tx, err)
   617  	}
   618  
   619  	// paiements
   620  	rows, err = tx.Query("UPDATE paiements SET id_facture = $1 WHERE id_facture = $2 RETURNING *", params.To, params.From)
   621  	if err != nil {
   622  		return out, shared.Rollback(tx, err)
   623  	}
   624  	out.Paiements, err = rd.ScanPaiements(rows)
   625  	if err != nil {
   626  		return out, shared.Rollback(tx, err)
   627  	}
   628  
   629  	// messages
   630  	rows, err = tx.Query("UPDATE messages SET id_facture = $1 WHERE id_facture = $2 RETURNING *", params.To, params.From)
   631  	if err != nil {
   632  		return out, shared.Rollback(tx, err)
   633  	}
   634  	out.Messages, err = rd.ScanMessages(rows)
   635  	if err != nil {
   636  		return out, shared.Rollback(tx, err)
   637  	}
   638  
   639  	// sondages
   640  	out.Sondages, err = moveSondages(params, tx)
   641  	if err != nil {
   642  		return out, shared.Rollback(tx, err)
   643  	}
   644  
   645  	// notification par mail (nécessite les 2 dossiers)
   646  	if params.Notifie {
   647  		err = ct.notifieFusion(host, tx, params)
   648  		if err != nil {
   649  			return out, shared.Rollback(tx, err)
   650  		}
   651  	}
   652  
   653  	// suppression du dossier
   654  	_, err = rd.DeleteFactureById(tx, params.From)
   655  	if err != nil {
   656  		return out, shared.Rollback(tx, err)
   657  	}
   658  	out.OldId = params.From
   659  
   660  	err = tx.Commit()
   661  	return out, err
   662  }
   663  
   664  // on envoie un mail au respo de l'ancien dossier, en donnant le lien du nouveau
   665  func (ct *Controller) notifieFusion(host string, db rd.DB, params apiserver.FusionneFacturesIn) error {
   666  	oldFacture, err := rd.SelectFacture(db, params.From)
   667  	if err != nil {
   668  		return err
   669  	}
   670  	personne, err := rd.SelectPersonne(db, oldFacture.IdPersonne)
   671  	if err != nil {
   672  		return err
   673  	}
   674  	subject := "[ACVE] - Fusion de dossier"
   675  	contact := mails.Contact{
   676  		Prenom: personne.FPrenom(),
   677  		Sexe:   personne.Sexe,
   678  	}
   679  	to := personne.Mail.String()
   680  
   681  	newFacture, err := rd.SelectFacture(db, params.To)
   682  	if err != nil {
   683  		return err
   684  	}
   685  	url := shared.BuildUrl(host, newFacture.UrlEspacePerso("espace_perso"), nil)
   686  
   687  	body, err := mails.NewNotifFusion(contact, url)
   688  	if err != nil {
   689  		return err
   690  	}
   691  	err = mails.NewMailer(ct.SMTP).SendMail(to, subject, body, oldFacture.CopiesMails, nil)
   692  	return err
   693  }
   694  
   695  // une seule réponse est autorisée par sondage et dossier:
   696  // si une réponse existe déjà, on la garde et on ignore la réponse
   697  // du dossier à fusionner
   698  func moveSondages(params apiserver.FusionneFacturesIn, tx rd.DB) (rd.Sondages, error) {
   699  	rows, err := tx.Query(`UPDATE sondages AS s1 SET id_facture = $1
   700  	WHERE s1.id_facture = $2 AND
   701  		(SELECT count(*) FROM sondages AS s2
   702  			WHERE s2.id_camp = s1.id_camp AND s2.id_facture = $1) = 0
   703  	RETURNING *`,
   704  		params.To, params.From)
   705  	if err != nil {
   706  		return nil, err
   707  	}
   708  	return rd.ScanSondages(rows)
   709  }