# frozen_string_literal: true require 'csv' describe ProcedureExportService do let(:instructeur) { create(:instructeur) } let(:service) { ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) } let(:export_template) { nil } describe 'to_xlsx' do subject do service .to_xlsx .open { |f| SimpleXlsxReader.open(f.path) } end let(:dossiers_sheet) { subject.sheets.first } let(:etablissements_sheet) { subject.sheets.second } let(:avis_sheet) { subject.sheets.third } let(:repetition_sheet) { subject.sheets.fourth } describe 'sheets' do let(:procedure) { create(:procedure) } it 'should have a sheet for each record type' do expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis']) end end describe 'Dossiers sheet' do context 'with all data for individual' do let(:procedure) { create(:procedure, :published, :for_individual, :with_all_champs) } let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) } # before do # # change one tdc place to check if the header is ordered # tdc_first = procedure.active_revision.revision_types_de_champ_public.first # tdc_last = procedure.active_revision.revision_types_de_champ_public.last # tdc_first.update(position: tdc_last.position + 1) # procedure.reload # end let(:nominal_headers) do [ "ID", "Email", "FranceConnect ?", "Civilité", "Nom", "Prénom", "Dépôt pour un tiers", "Nom du mandataire", "Prénom du mandataire", "Archivé", "État du dossier", "Dernière mise à jour le", "Dernière mise à jour du dossier le", "Déposé le", "Passé en instruction le", "Traité le", "Motivation de la décision", "Instructeurs", "textarea", "date", "datetime", "number", "decimal_number", "integer_number", "checkbox", "civilite", "email", "phone", "address", "simple_drop_down_list", "multiple_drop_down_list", "linked_drop_down_list", "communes", "communes (Code INSEE)", "communes (Département)", "departements", "departements (Code)", "regions", "regions (Code)", "pays", "pays (Code)", "dossier_link", "piece_justificative", "rna", "carte", "titre_identite", "iban", "siret", "annuaire_education", "cnaf", "dgfip", "pole_emploi", "mesri", "text", "epci", "epci (Code)", "epci (Département)", "cojo", "expression_reguliere", "rnf", "rnf (Nom)", "rnf (Adresse)", "rnf (Code INSEE Ville)", "rnf (Département)", "engagement_juridique", "yes_no" ] end it 'should have data' do expect(dossiers_sheet.headers).to match_array(nominal_headers) expect(dossiers_sheet.data.size).to eq(1) expect(etablissements_sheet.data.size).to eq(1) # SimpleXlsxReader is transforming datetimes in utc... It is only used in test so we just hack around. offset = dossier.depose_at.utc_offset depose_at = Time.zone.at(dossiers_sheet.data[0][13] - offset.seconds) en_instruction_at = Time.zone.at(dossiers_sheet.data[0][14] - offset.seconds) expect(dossiers_sheet.data.first.size).to eq(nominal_headers.size) expect(depose_at).to eq(dossier.depose_at.round) expect(en_instruction_at).to eq(dossier.en_instruction_at.round) end end context 'with a birthdate' do let(:procedure) { create(:procedure, :published, :for_individual, ask_birthday: true) } let!(:dossier) { create(:dossier, :en_instruction, :with_individual, procedure:) } it 'find date de naissance' do expect(dossiers_sheet.headers).to include('Date de naissance') expect(dossiers_sheet.data[0][dossiers_sheet.headers.index('Date de naissance')]).to be_a(Date) end end context 'with a procedure routee' do let(:procedure) { create(:procedure, :published, :for_individual) } let!(:dossier) { create(:dossier, :en_instruction, :with_individual, procedure:) } before { create(:groupe_instructeur, label: '2', procedure:) } it 'find groupe instructeur data' do expect(dossiers_sheet.headers).to include('Groupe instructeur') expect(dossiers_sheet.data[0][dossiers_sheet.headers.index('Groupe instructeur')]).to eq('défaut') end end context 'with a dossier having multiple pjs' do let(:procedure) { create(:procedure, :published, :for_individual, types_de_champ_public:) } let(:types_de_champ_public) { [{ type: :piece_justificative }] } let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure:) } let!(:dossier_2) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure:) } before do dossier_2.champs_public .find { _1.is_a? Champs::PieceJustificativeChamp } .piece_justificative_file .attach(io: StringIO.new("toto"), filename: "toto.txt", content_type: "text/plain") end it { expect(dossiers_sheet.data.first.size).to eq(19) } # default number of header when procedure has only one champ end context 'with procedure chorus' do before { expect_any_instance_of(Procedure).to receive(:chorusable?).and_return(true) } let(:procedure) { create(:procedure, :published, :for_individual, :filled_chorus) } let!(:dossier) { create(:dossier, :en_instruction, procedure: procedure) } it 'includes chorus headers' do expect(dossiers_sheet.headers).to include('Domaine Fonctionnel') expect(dossiers_sheet.headers).to include('Référentiel De Programmation') expect(dossiers_sheet.headers).to include('Centre De Coût') end end end describe 'Etablissement sheet' do let(:procedure) { create(:procedure, :published, types_de_champ_public:) } let(:types_de_champ_public) { [{ type: :siret, libelle: 'siret' }] } let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_entreprise, procedure: procedure) } let(:dossier_etablissement) { etablissements_sheet.data[1] } let(:champ_etablissement) { etablissements_sheet.data[0] } let(:nominal_headers) do [ "ID", "Email", "FranceConnect ?", "Entreprise raison sociale", "Archivé", "État du dossier", "Dernière mise à jour le", "Dernière mise à jour du dossier le", "Déposé le", "Passé en instruction le", "Traité le", "Motivation de la décision", "Instructeurs", 'siret' ] end context 'as csv' do subject do ProcedureExportService.new(procedure, procedure.dossiers, instructeur, export_template) .to_csv .open { |f| CSV.read(f.path) } end let(:nominal_headers) do [ "ID", "Email", "FranceConnect ?", "Établissement SIRET", "Établissement siège social", "Établissement NAF", "Établissement libellé NAF", "Établissement Adresse", "Établissement numero voie", "Établissement type voie", "Établissement nom voie", "Établissement complément adresse", "Établissement code postal", "Établissement localité", "Établissement code INSEE localité", "Entreprise SIREN", "Entreprise capital social", "Entreprise numero TVA intracommunautaire", "Entreprise forme juridique", "Entreprise forme juridique code", "Entreprise nom commercial", "Entreprise raison sociale", "Entreprise SIRET siège social", "Entreprise code effectif entreprise", "Entreprise date de création", "Entreprise état administratif", "Entreprise nom", "Entreprise prénom", "Association RNA", "Association titre", "Association objet", "Association date de création", "Association date de déclaration", "Association date de publication", "Archivé", "État du dossier", "Dernière mise à jour le", "Dernière mise à jour du dossier le", "Déposé le", "Passé en instruction le", "Traité le", "Motivation de la décision", "Instructeurs", 'siret' ] end let(:dossiers_sheet_headers) { subject.first } it 'should have headers' do expect(dossiers_sheet_headers).to match_array(nominal_headers) end end it 'should have headers and data' do expect(dossiers_sheet.headers).to match_array(nominal_headers) expect(etablissements_sheet.headers).to eq([ "Dossier ID", "Champ", "Établissement SIRET", "Etablissement enseigne", "Établissement siège social", "Établissement NAF", "Établissement libellé NAF", "Établissement Adresse", "Établissement numero voie", "Établissement type voie", "Établissement nom voie", "Établissement complément adresse", "Établissement code postal", "Établissement localité", "Établissement code INSEE localité", "Entreprise SIREN", "Entreprise capital social", "Entreprise numero TVA intracommunautaire", "Entreprise forme juridique", "Entreprise forme juridique code", "Entreprise nom commercial", "Entreprise raison sociale", "Entreprise SIRET siège social", "Entreprise code effectif entreprise", "Entreprise date de création", "Entreprise état administratif", "Entreprise nom", "Entreprise prénom", "Association RNA", "Association titre", "Association objet", "Association date de création", "Association date de déclaration", "Association date de publication" ]) expect(etablissements_sheet.data.size).to eq(2) expect(dossier_etablissement[1]).to eq("Dossier") expect(champ_etablissement[1]).to eq("siret") end end describe 'Avis sheet' do let(:procedure) { create(:procedure, :published, :for_individual) } let!(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) } let!(:avis) { create(:avis, :with_answer, dossier: dossier) } it 'should have headers and data' do expect(avis_sheet.headers).to eq([ "Dossier ID", "Introduction", "Réponse", "Question", "Réponse oui/non", "Créé le", "Répondu le", "Instructeur", "Expert" ]) expect(avis_sheet.data.size).to eq(1) end end describe 'Repetitions sheet' do before do # change one tdc place to check if the header is ordered tdc_first = procedure.active_revision.revision_types_de_champ_public.first tdc_last = procedure.active_revision.revision_types_de_champ_public.last tdc_first.update(position: tdc_last.position + 1) procedure.reload end let(:types_de_champ_public) { [{ type: :repetition, children: [{ libelle: 'Nom' }, { libelle: 'Age' }] }] } let(:procedure) { create(:procedure, :published, :for_individual, types_de_champ_public:) } let!(:dossiers) do [ create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure), create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) ] end let(:champ_repetition) { dossiers.first.champs_public.find { |champ| champ.type_champ == 'repetition' } } it 'should have sheets' do expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', champ_repetition.type_de_champ.libelle_for_export]) end context 'with cloned procedure' do let(:other_parent) { create(:type_de_champ_repetition, stable_id: champ_repetition.stable_id) } before do create(:type_de_champ, parent: create(:procedure_revision_type_de_champ, type_de_champ: other_parent, revision: create(:procedure).active_revision, position: 0)) end it 'should have headers' do expect(repetition_sheet.headers).to eq([ "Dossier ID", "Ligne", "Nom", "Age" ]) end end it 'should have data' do expect(repetition_sheet.data.size).to eq(4) end context 'with invalid characters' do before do champ_repetition.type_de_champ.update(libelle: 'A / B \ C *[]?') end it 'should have valid sheet name' do expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', "(#{champ_repetition.type_de_champ.stable_id}) A - B - C"]) end end context 'with long libelle composed of utf8 characteres' do before do procedure.active_revision.types_de_champ_public.each do |c| c.update!(libelle: "#{c.id} - ?/[] ééé ééé ééééééé ééééééé éééééééé. ééé éé éééééééé éé ééé. ééééé éééééééé ééé ééé.") end champ_repetition.champs.each do |c| c.type_de_champ.update!(libelle: "#{c.id} - Quam rem nam maiores numquam dolorem nesciunt. Cum et possimus et aut. Fugit voluptas qui qui.") end end it 'should have valid sheet name' do expect { subject }.not_to raise_error end end context 'with non unique labels' do let(:types_de_champ_public) { [{ type: :repetition, libelle: 'Une repetition', children: [{}] }, { type: :repetition, libelle: 'Une repetition', children: [{}] }] } let(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure:) } let(:type_de_champ_repetition) { dossier.revision.types_de_champ_public.first } let(:another_type_de_champ_repetition) { dossier.revision.types_de_champ_public.second } it 'should have sheets' do expect(subject.sheets.map(&:name)).to eq(['Dossiers', 'Etablissements', 'Avis', type_de_champ_repetition.libelle_for_export, another_type_de_champ_repetition.libelle_for_export]) end end context 'with empty repetition' do before do dossiers.flat_map { |dossier| dossier.champs_public.filter(&:repetition?) }.each do |champ| champ.champs.destroy_all end end it 'should not have data' do expect(repetition_sheet).to be_nil end end end end describe 'to_zip' do let(:procedure) { create(:procedure, :published, :for_individual, types_de_champ_public:) } let(:types_de_champ_public) { [{ type: :piece_justificative, libelle: 'piece_justificative' }] } subject { service.to_zip } context 'without files' do it 'does not raises in_batches' do expect { subject }.not_to raise_error end it 'returns an empty blob' do expect(subject).to be_an_instance_of(ActiveStorage::Blob) end end describe 'generate_dossiers_export' do it 'include_infos_administration (so it includes avis, champs privés)' do expect(ActiveStorage::DownloadableFile).to receive(:create_list_from_dossiers).with(dossiers: anything, user_profile: instructeur, export_template:).and_return([]) subject end context 'with export_template' do let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) } let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur, export_template:).generate_dossiers_export(Dossier.where(id: dossier)) } let(:export_template) { create(:export_template, :enabled_pjs, groupe_instructeur: procedure.defaut_groupe_instructeur) } before do allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io") end it 'returns a blob with custom filenames' do VCR.use_cassette('archive/new_file_to_get_200') do subject File.write('tmp.zip', subject.download, mode: 'wb') File.open('tmp.zip') do |fd| files = ZipTricks::FileReader.read_zip_structure(io: fd) base_fn = "export" structure = [ "#{base_fn}/", "#{base_fn}/dossier-#{dossier.id}/", "#{base_fn}/dossier-#{dossier.id}/piece_justificative-#{dossier.id}-01.txt", "#{base_fn}/dossier-#{dossier.id}/export-#{dossier.id}.pdf" ] expect(files.map(&:filename)).to match_array(structure) end FileUtils.remove_entry_secure('tmp.zip') end end end context 'with files (and http calls)' do let!(:dossier) { create(:dossier, :accepte, :with_populated_champs, :with_individual, procedure: procedure) } let(:dossier_exports) { PiecesJustificativesService.new(user_profile: instructeur, export_template: nil).generate_dossiers_export(Dossier.where(id: dossier)) } before do allow_any_instance_of(ActiveStorage::Attachment).to receive(:url).and_return("https://opengraph.githubassets.com/d0e7862b24d8026a3c03516d865b28151eb3859029c6c6c2e86605891fbdcd7a/socketry/async-io") end it 'returns a blob with valid files' do VCR.use_cassette('archive/new_file_to_get_200') do subject File.write('tmp.zip', subject.download, mode: 'wb') File.open('tmp.zip') do |fd| files = ZipTricks::FileReader.read_zip_structure(io: fd) base_fn = 'export' structure = [ "#{base_fn}/", "#{base_fn}/dossier-#{dossier.id}/", "#{base_fn}/dossier-#{dossier.id}/pieces_justificatives/", "#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(ActiveStorage::Attachment.where(record_type: "Champ").first)}", "#{base_fn}/dossier-#{dossier.id}/#{ActiveStorage::DownloadableFile.timestamped_filename(dossier_exports.first.first)}" ] expect(files.size).to eq(structure.size) expect(files.map(&:filename)).to match_array(structure) end FileUtils.remove_entry_secure('tmp.zip') end end end end end describe 'to_geo_json' do let(:procedure) { create(:procedure, :published, :for_individual, types_de_champ_public:) } let(:types_de_champ_public) { [{ type: :carte }] } subject do service .to_geo_json .open { |f| JSON.parse(f.read) } end let(:dossier) { create(:dossier, :en_instruction, :with_populated_champs, :with_individual, procedure: procedure) } let(:champ_carte) { dossier.champs_public.find(&:carte?) } let(:properties) { subject['features'].first['properties'] } before do create(:geo_area, :polygon, champ: champ_carte) end it 'should have features' do expect(subject['features'].size).to eq(3) expect(properties['dossier_id']).to eq(dossier.id) expect(properties['champ_id']).to eq(champ_carte.stable_id) expect(properties['champ_label']).to eq(champ_carte.libelle) end end end