Last active
March 25, 2026 14:39
-
-
Save colinux/a2ea5808fd9fe57578c192abe2731261 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Import a procedure structure from production via GraphQL API | |
| # Works for public procedures (opendata: true) or with API_PRODUCTION_TOKEN for private procedures | |
| # | |
| # Usage: | |
| # procedure = import_procedure_from_production(12345) # Uses ENV['DS_EMAIL'] | |
| # procedure = import_procedure_from_production(12345) | |
| # | |
| def import_procedure_from_production(procedure_number) | |
| # Find local administrator using the same method as other helpers | |
| admin = Administrateur.by_email(ENV.fetch("DS_EMAIL")) | |
| # Fetch procedure data from production GraphQL API | |
| graphql_data = fetch_procedure_from_graphql(procedure_number) | |
| # Create procedure locally | |
| procedure = nil | |
| ActiveRecord::Base.transaction do | |
| procedure = create_procedure_from_graphql_data(graphql_data, admin) | |
| end | |
| procedure | |
| rescue => e | |
| Rails.logger.error("Failed to import procedure #{procedure_number}: #{e.message}") | |
| Rails.logger.error(e.backtrace.join("\n")) | |
| raise | |
| end | |
| private | |
| def fetch_procedure_from_graphql(procedure_number) | |
| query_id = 'ds-query-v2' | |
| operation_name = 'getDemarcheDescriptor' | |
| url = 'https://demarche.numerique.gouv.fr/api/v2/graphql' # rubocop:disable DS/ApplicationName | |
| headers = { | |
| 'Content-Type' => 'application/json', | |
| } | |
| body = { | |
| queryId: query_id, | |
| operationName: operation_name, | |
| variables: { | |
| demarche: { number: procedure_number }, | |
| includeRevision: true, | |
| }, | |
| }.to_json | |
| response = Typhoeus.post(url, body: body, headers: headers, timeout: 30) | |
| unless response.success? | |
| raise "GraphQL request failed: #{response.code} - #{response.body}" | |
| end | |
| result = JSON.parse(response.body) | |
| if result['errors'] | |
| raise "GraphQL errors: #{result['errors'].map { _1['message'] }.join(', ')}" | |
| end | |
| result.dig('data', 'demarcheDescriptor') | |
| end | |
| def create_procedure_from_graphql_data(graphql_data, admin) | |
| # Find or create a service | |
| service = Service.first | |
| # Extract path from demarcheURL | |
| # URL format: https://demarche.numerique.gouv.fr/commencer/nom-de-la-demarche | |
| path = URI.parse(graphql_data['demarcheURL']).path.split('/').last | |
| # Create procedure | |
| procedure = Procedure.new( | |
| libelle: graphql_data['title'], | |
| description: graphql_data['description'], | |
| organisation: 'Imported from production', | |
| cadre_juridique: 'Imported from production', | |
| # limitation: demarcheDescription does not expose for_individual | |
| for_individual: true, | |
| lien_site_web: 'https://example.com', | |
| duree_conservation_dossiers_dans_ds: 3, | |
| max_duree_conservation_dossiers_dans_ds: Expired::DEFAULT_DOSSIER_RENTENTION_IN_MONTH, | |
| service:, | |
| administrateurs: [admin] | |
| ) | |
| procedure.draft_revision = procedure.revisions.build | |
| procedure.save! | |
| # Add TypeDeChamps from revision | |
| revision = graphql_data['revision'] | |
| if revision && revision['champDescriptors'] | |
| champ_descriptors = revision['champDescriptors'] | |
| champ_descriptors.each do |descriptor| | |
| create_type_de_champ_from_descriptor(procedure.draft_revision, descriptor) | |
| end | |
| end | |
| # Publish procedure with path | |
| procedure.publish_or_reopen!(admin, path) | |
| procedure | |
| end | |
| def create_type_de_champ_from_descriptor(revision, descriptor, parent_stable_id: nil) | |
| type_champ = map_graphql_type_to_rails_type_champ(descriptor['__typename']) | |
| params = { | |
| type_champ:, | |
| libelle: descriptor['label'] || 'Champ', | |
| description: descriptor['description'], | |
| mandatory: descriptor['required'] || false, | |
| private: false, | |
| parent_stable_id:, | |
| } | |
| # Handle dropdown options | |
| if descriptor['options'].present? | |
| params[:drop_down_list_value] = descriptor['options'].join("\n") | |
| end | |
| tdc = revision.add_type_de_champ(params) | |
| # Handle repetition children recursively | |
| if type_champ == :repetition && descriptor['champDescriptors'].present? | |
| descriptor['champDescriptors'].each do |child_descriptor| | |
| create_type_de_champ_from_descriptor(revision, child_descriptor, parent_stable_id: tdc.stable_id) | |
| end | |
| end | |
| tdc | |
| rescue => e | |
| puts "Failed to create TypeDeChamp #{descriptor['__typename']}: #{e.message}" | |
| raise | |
| end | |
| def map_graphql_type_to_rails_type_champ(graphql_typename) | |
| # Remove 'ChampDescriptor' suffix and convert to snake_case symbol | |
| # Example: TextChampDescriptor -> text, DropDownListChampDescriptor -> drop_down_list | |
| graphql_typename.sub('ChampDescriptor', '').underscore.to_sym | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment