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 }