You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Migration Plan: DataProvider from YAML to Database
Context
The DataProvider model is currently a static model (inheriting from StaticApplicationRecord) that loads its data from config/data_providers.yml. The goal is to migrate it to a database-backed model with the following changes:
Convert from StaticApplicationRecord to ApplicationRecord (database-backed)
Use friendly_id gem for slug-based lookups (slug derived from current YAML keys for import, auto-generated from name for future records)
Use ActiveStorage for logo file management (same service as other uploads)
Add validations for name, logo, and link (URL validation using same regex as cadre_juridique)
Migrate existing YAML data to database records
Update all code references from .find(id) to .friendly.find(slug)
Update view helpers to use ActiveStorage URLs instead of asset pipeline
Keep YAML file for reference/backup (no deletion)
Important: This is a backend-only migration. No controllers or admin UI will be created. Future management will be via Rails console.
Current State Analysis
Existing Data (12 providers in YAML)
dgs - Direction Générale de la Santé
dila - Direction de l'Information Légale et Administrative
dinum - DINUM
dgfip - Direction Générale des Finances Publiques
ministere_des_armees - Ministère Des Armées
aife - Agence pour l'Information Financière de l'État
ans - Agence du Numérique en Santé
urssaf - URSSAF
menj - Ministère de l'Éducation Nationale et de la Jeunesse
cnam - CNAM
cisirh - Centre Interministériel des Systèmes d'Information
mtes - Ministère de la Transition écologique
Code Locations Using DataProvider
app/models/authorization_definition.rb:47 - Loads provider from YAML hash
Update authorization_definitions to compare with id.to_s (database id vs static string)
Notes:
URL regex matches pattern from app/models/concerns/authorization_extensions/cadre_juridique.rb
ActiveStorage validations use active_storage_validations gem
Logo content types match existing files (png, jpg, jpeg)
Test: Model tests (see Step 1.5)
Step 1.5: Write Model Tests
File: spec/models/data_provider_spec.rb
Update/add tests:
RSpec.describeDataProviderdodescribe'validations'dosubject{build(:data_provider)}it{is_expected.tovalidate_presence_of(:slug)}it{is_expected.tovalidate_presence_of(:name)}it{is_expected.tovalidate_presence_of(:link)}it{is_expected.tovalidate_uniqueness_of(:slug)}describe'link format validation'doit'accepts valid URLs'dovalid_urls=['https://www.example.gouv.fr','http://example.gouv.fr','https://example.gouv.fr/path/to/page',]valid_urls.eachdo |url|
subject.link=urlexpect(subject).tobe_valid,"Expected #{url} to be valid"endendit'rejects invalid URLs'doinvalid_urls=['not a url','ftp://example.com','example','']invalid_urls.eachdo |url|
subject.link=urlexpect(subject).not_tobe_valid,"Expected #{url} to be invalid"endendenddescribe'logo attachment'doit'validates logo is attached'doprovider=build(:data_provider)provider.logo.purgeexpect(provider).not_tobe_validexpect(provider.errors[:logo]).tobe_presentendit'validates logo content type'doprovider=build(:data_provider)provider.logo.attach(io: StringIO.new('content'),filename: 'test.txt',content_type: 'text/plain')expect(provider).not_tobe_validendit'accepts valid image types'do%w[image/pngimage/jpgimage/jpeg].eachdo |content_type|
provider=build(:data_provider)provider.logo.attach(io: StringIO.new('content'),filename: 'test.png',content_type:)expect(provider).tobe_valid,"Expected #{content_type} to be valid"endendendenddescribe'friendly_id'doit'finds by slug using friendly.find'doprovider=create(:data_provider,slug: 'test-provider')expect(DataProvider.friendly.find('test-provider')).toeq(provider)endit'raises error when slug not found'doexpect{DataProvider.friendly.find('nonexistent')}.toraise_error(ActiveRecord::RecordNotFound)endenddescribe'#authorization_definitions'doit'returns authorization definitions for this provider'doprovider=create(:data_provider,slug: 'dgfip')definitions=provider.authorization_definitionsexpect(definitions).tobe_all{ |d| d.provider.id == 'dgfip'}endenddescribe'#reporters'dosubject{create(:data_provider,:dgfip).reporters}let!(:valid_users)do[create(:user,:reporter,authorization_request_types: %w[api_impot_particulierapi_hermes]),create(:user,:instructor,authorization_request_types: %w[api_hermes]),]endlet!(:invalid_users)do[create(:user,:reporter,authorization_request_types: %w[api_entreprise]),]endit{is_expected.tomatch_array(valid_users)}enddescribe'#instructors'dosubject{create(:data_provider,:dgfip).instructors}let!(:valid_users)do[create(:user,:instructor,authorization_request_types: %w[api_impot_particulierapi_hermes]),create(:user,:instructor,authorization_request_types: %w[api_hermes]),]endlet!(:invalid_users)do[create(:user,:reporter,authorization_request_types: %w[api_hermes]),create(:user,:reporter,authorization_request_types: %w[api_entreprise]),]endit{is_expected.tomatch_array(valid_users)}endend
Notes:
Reuse existing test patterns from current spec/models/data_provider_spec.rb
Add validation tests for all new validations
Test friendly_id functionality
Keep existing reporters and instructors tests (should still work)
Migration to import YAML data with strict validation:
classMigrateDataProvidersFromYaml < ActiveRecord::Migration[8.0]defupproviders_data=Rails.application.config_for(:data_providers)providers_data.eachdo |slug,attributes|
provider=DataProvider.create!(slug:,name: attributes['name'],link: attributes['link'])logo_filename=attributes['logo']logo_path=Rails.root.join('app','assets','images','data_providers',logo_filename)extension=File.extname(logo_filename).downcaseunlessFile.exist?(logo_path)raise"Logo file not found for provider '#{slug}': #{logo_path}"endunless['.png','.jpg','.jpeg'].include?(extension)raise"Invalid logo format for provider '#{slug}': #{logo_filename}. Only PNG and JPG/JPEG are allowed."endcontent_type=caseextensionwhen'.png'then'image/png'when'.jpg','.jpeg'then'image/jpeg'endprovider.logo.attach!(io: File.open(logo_path),filename: logo_filename,content_type:
)Rails.logger.info"Migrated provider '#{slug}' with logo '#{logo_filename}'"endRails.logger.info"Successfully migrated #{providers_data.count} data providers"enddefdownDataProvider.destroy_allendend
Key changes from previous version:
Use attach! instead of attach (raises error on failure)
Raise explicit error if logo file doesn't exist (no silent warnings)
Validate file extension before attempting attach (only .png, .jpg, .jpeg)
Raise error for invalid file formats
Add info logging for successful migrations
Migration will fail fast on any error
Notes:
Migration will halt deployment if any logo is missing or invalid
All validations happen before database write
Reversible migration (down method destroys all records)
Test: Run migration in test environment
RAILS_ENV=test bundle exec rails db:migrate
Verify:
RAILS_ENV=test bundle exec rails runner "puts DataProvider.count"# Should be 12
RAILS_ENV=test bundle exec rails runner "puts DataProvider.all.map(&:slug).join(', ')"
Step 3.3: Update Application Helper for Logo Display
File: app/helpers/application_helper.rb:17-20
Change (simplified - logo is always present):
# Beforedefprovider_logo_image_tag(authorization_definition,options={})options=options.merge(alt: "Logo du fournisseur de données \"#{authorization_definition.provider.name}\"")image_tag("data_providers/#{authorization_definition.provider.logo}",options)end# Afterdefprovider_logo_image_tag(authorization_definition,options={})options=options.merge(alt: "Logo du fournisseur de données \"#{authorization_definition.provider.name}\"")image_tag(authorization_definition.provider.logo,options)end
Notes:
Use ActiveStorage's image_tag helper (works directly with attachments)
No fallback needed - logo is always present after migration (validated)
Simpler implementation since validation ensures logo exists
Test: Manual testing in views that use provider logos
Phase 4: Testing and Validation
Step 4.1: Run All RSpec Tests
Command:
bundle exec rspec
Verify:
DataProvider model tests pass
All related model tests pass (AuthorizationDefinition, User)
Controller tests pass (if any for dgfip export)
Helper tests pass (if any)
No regressions in other tests
Fix failures as they occur
Step 4.2: Run Cucumber Features
Command:
bundle exec cucumber
Verify:
All E2E tests pass
Features using DataProvider work correctly
Logo display works in views
Fix failures as they occur
Step 4.3: Manual Testing in Development
Start server:
bin/local_run.sh
Test:
Check DataProvider records exist:
DataProvider.count# Should be 12DataProvider.all.map(&:slug)
bundle exec rubocop
bundle exec rubocop -A # Auto-fix issues
Fix any style violations:
Maximum method length: 15 lines
Class length: max 150 lines
Use single quotes for strings
2 spaces indentation
Phase 5: Documentation
Step 5.1: Add Comment to YAML File
File: config/data_providers.yml
Add comment at the top:
---
# NOTE: This file is kept for reference purposes only.# Data providers are now stored in the database (data_providers table).# To add new providers, use the Rails console:# provider = DataProvider.create!(# name: 'Provider Name',# link: 'https://provider.gouv.fr'# )# provider.logo.attach(io: File.open('path/to/logo.png'), filename: 'logo.png', content_type: 'image/png')## This file was used for the initial data migration.# Migration date: [INSERT DATE]shared:
dgs:
name: Organisation de la direction générale de la santé (DGS)logo: ministere_sante.jpglink: https://www.sante.gouv.fr/# ... rest of file
Notes:
Keep YAML file as backup/reference (per user decision)
Add clear instructions for future additions via console
Migration Plan: DataProvider from YAML to Database
Context
The DataProvider model is currently a static model (inheriting from StaticApplicationRecord) that loads its data from config/data_providers.yml. The goal is to migrate it to a database-backed model with the following changes:
Convert from StaticApplicationRecord to ApplicationRecord (database-backed)
Use friendly_id gem for slug-based lookups (slug derived from current YAML keys)
Use ActiveStorage for logo file management
Add validations for name, logo, and link (URL validation)
Migrate existing YAML data to database records
Update all code references from .find(id) to .friendly.find(slug)
Current State Analysis
Existing Data (12 providers in YAML)
dgs - Direction Générale de la Santé
dila - Direction de l'Information Légale et Administrative
dinum - DINUM
dgfip - Direction Générale des Finances Publiques
ministere_des_armees - Ministère Des Armées
aife - Agence pour l'Information Financière de l'État
ans - Agence du Numérique en Santé
urssaf - URSSAF
menj - Ministère de l'Éducation Nationale et de la Jeunesse
cnam - CNAM
cisirh - Centre Interministériel des Systèmes d'Information
mtes - Ministère de la Transition écologique
Code Locations Using DataProvider.find
app/models/authorization_definition.rb:47 - Loads provider from YAML hash
Update other static models as well (ServiceProvider, IdentityProvider, etc.)
Notes:
This is out of scope for current task
Document as future improvement
Phase 6: Documentation
Step 6.1: Update Technical Documentation
Check if any docs need updating:
docs/ folder files
README (if mentions DataProvider)
Architecture documentation
Step 6.2: Add Migration Notes
Document:
Why migration was done
How to add new data providers (now via admin interface or console)
Logo upload process
Testing Strategy
Following TDD approach:
Step 1: Models
Write failing tests for new DataProvider model
Implement model to make tests pass
Refactor and ensure rubocop passes
Step 2: Data Migration
Test migration in test environment
Verify data integrity
Ensure idempotency (can run multiple times safely)
Step 3: Code Updates
Write/update tests for each changed file
Implement changes
Verify tests pass
Step 4: Integration
Run full test suite
Run cucumber features
Manual testing in development environment
Questions for Clarification
Slug Generation: Should the slug be the same as the current YAML keys (dgs, dila, dgfip, etc.), or should it be auto-generated from the name?
Recommendation: Use current YAML keys as slugs for backward compatibility
Oui pour l'import, et pour les créations futures on se base sur le nom.
Logo Management: After migration, how should new logos be added?
Option A: Via admin interface (requires building admin UI)
Option B: Via Rails console
Option C: Keep ability to seed from assets folder
On verra plus tard.
YAML File: Should we keep the YAML file after migration?
Option A: Keep for reference/rollback
Option B: Archive in config/archived/
Option C: Delete entirely
Option A.
Authorization: Who should be able to create/update/delete DataProviders?
Currently no authorization mentioned
Should only admins manage this?
Should we add a basic admin interface?
Purement une migration de backend, aucun controller/vues prévues.
Rollback Strategy: If migration fails in production, what's the rollback plan?
Keep YAML file as backup?
Migration down method to restore StaticApplicationRecord pattern?
On fail pour annuler le déploiement.
Other Static Models: Are we planning to migrate other static models (ServiceProvider, IdentityProvider, Subdomain, CodeNAF)?
Should we establish a pattern for all static models?
Or is this a one-off migration?
Non on verra plus tard.
Logo Display: How are logos currently displayed in views?
Are they using asset pipeline (image_tag)?
Will need to update to use ActiveStorage URLs
Should we add an image variant/processing?
Oui il faut utiliser les méthodes classiques d'ActiveStorage pour l'affichage.
Friendly ID Finders: The codebase currently uses .friendly.find() for Authorization model, but the friendly_id config has config.use :finders disabled. Do we need to enable it or stick with explicit .friendly.find()?
Recommendation: Use explicit .friendly.find() to match existing pattern
Oui garde friendly
URL Validation: Should we use the same URL regex pattern as in cadre_juridique.rb, or is there a preferred URL validation gem/method?
Current pattern: %r{\A((http|https)://)?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/[\w\-._~:/?#\[\]@!$&'()*+,;%=]*)?\z}
Oui très bien
ActiveStorage Service: Should logos use the same ActiveStorage service as other uploads (likely S3 in production)?
Or should they use local storage since they're relatively static?
On garde le même service que le reste.
Success Criteria
DataProvider table created with all required columns and indexes
All 12 existing providers migrated to database with correct data
All logos attached via ActiveStorage and display correctly
All validations working (name, logo, link presence and format)
friendly_id working for slug-based lookups
All code references updated to use .friendly.find()
All existing functionality preserved (authorization_definitions, reporters, instructors)
All RSpec tests passing
All Cucumber features passing
Rubocop passing with no violations
No regressions in application behavior
Estimated Implementation Order
✅ Create table migration (30 min)
✅ Update DataProvider model (1 hour)
✅ Write model tests (1 hour)
✅ Run tests and fix issues (30 min)
✅ Create data migration script (1 hour)
✅ Test data migration (30 min)
✅ Update code references (30 min)
✅ Run full test suite (30 min)
✅ Fix any failing tests (1 hour buffer)
✅ Run rubocop and fix style issues (30 min)
✅ Manual testing (1 hour)
✅ Documentation updates (30 min)
Total estimated time: ~9 hours
Risk Assessment
High Risk
Data migration integrity: Losing or corrupting provider data
Mitigation: Test thoroughly in test environment first, keep YAML backup
Breaking AuthorizationDefinition loading: This is a critical part of the app
Mitigation: Extensive testing of authorization definition loading
Medium Risk
Missing logo files: Some logos might not exist or have wrong filenames
Mitigation: Verify all logo files exist before migration, add error handling
URL validation too strict: Existing URLs might not match regex
Mitigation: Test regex against all existing URLs first
Low Risk
Performance: Database queries instead of in-memory lookups
Mitigation: Add database indexes, use caching if needed
Slug conflicts: Unlikely with current data, but possible
Mitigation: friendly_id handles this automatically with candidate generation
Notes
This migration follows the same pattern as the Authorization model's use of friendly_id
The codebase uses interactor gem for services, but this migration might not need a complex service
Consider if DataProvider needs a decorator (Draper) for view-related logic
Current logo path pattern: app/assets/images/data_providers/#{filename}
All logos are in different formats: jpg, jpeg, png
Migration Plan: DataProvider from YAML to Database
Context
The DataProvider model is currently a static model (inheriting from StaticApplicationRecord) that loads its data from config/data_providers.yml. The goal is to migrate it to a database-backed model with the following changes:
Convert from StaticApplicationRecord to ApplicationRecord (database-backed)
Use friendly_id gem for slug-based lookups (slug derived from current YAML keys for import, auto-generated from name for future records)
Use ActiveStorage for logo file management (same service as other uploads)
Add validations for name, logo, and link (URL validation using same regex as cadre_juridique)
Migrate existing YAML data to database records
Update all code references from .find(id) to .friendly.find(slug)
Update view helpers to use ActiveStorage URLs instead of asset pipeline
Keep YAML file for reference/backup (no deletion)
Important: This is a backend-only migration. No controllers or admin UI will be created. Future management will be via Rails console.
Current State Analysis
Existing Data (12 providers in YAML)
dgs - Direction Générale de la Santé
dila - Direction de l'Information Légale et Administrative
dinum - DINUM
dgfip - Direction Générale des Finances Publiques
ministere_des_armees - Ministère Des Armées
aife - Agence pour l'Information Financière de l'État
ans - Agence du Numérique en Santé
urssaf - URSSAF
menj - Ministère de l'Éducation Nationale et de la Jeunesse
cnam - CNAM
cisirh - Centre Interministériel des Systèmes d'Information
mtes - Ministère de la Transition écologique
Code Locations Using DataProvider
app/models/authorization_definition.rb:47 - Loads provider from YAML hash
Update authorization_definitions to compare with id.to_s (database id vs static string)
Notes:
URL regex matches pattern from app/models/concerns/authorization_extensions/cadre_juridique.rb
ActiveStorage validations use active_storage_validations gem
Logo content types match existing files (png, jpg, jpeg)
Test: Model tests (see Step 1.4)
Step 1.4: Write Model Tests
File: spec/models/data_provider_spec.rb
Update/add tests:
RSpec.describeDataProviderdodescribe'validations'dosubject{build(:data_provider)}it{is_expected.tovalidate_presence_of(:slug)}it{is_expected.tovalidate_presence_of(:name)}it{is_expected.tovalidate_presence_of(:link)}it{is_expected.tovalidate_uniqueness_of(:slug)}describe'link format validation'doit'accepts valid URLs'dovalid_urls=['https://www.example.gouv.fr','http://example.gouv.fr','https://example.gouv.fr/path/to/page',]valid_urls.eachdo |url|
subject.link=urlexpect(subject).tobe_valid,"Expected #{url} to be valid"endendit'rejects invalid URLs'doinvalid_urls=['not a url','ftp://example.com','example','']invalid_urls.eachdo |url|
subject.link=urlexpect(subject).not_tobe_valid,"Expected #{url} to be invalid"endendenddescribe'logo attachment'doit'validates logo is attached'doprovider=build(:data_provider)provider.logo.purgeexpect(provider).not_tobe_validexpect(provider.errors[:logo]).tobe_presentendit'validates logo content type'doprovider=build(:data_provider)provider.logo.attach(io: StringIO.new('content'),filename: 'test.txt',content_type: 'text/plain')expect(provider).not_tobe_validendit'accepts valid image types'do%w[image/pngimage/jpgimage/jpeg].eachdo |content_type|
provider=build(:data_provider)provider.logo.attach(io: StringIO.new('content'),filename: 'test.png',content_type:)expect(provider).tobe_valid,"Expected #{content_type} to be valid"endendendenddescribe'friendly_id'doit'finds by slug using friendly.find'doprovider=create(:data_provider,slug: 'test-provider')expect(DataProvider.friendly.find('test-provider')).toeq(provider)endit'raises error when slug not found'doexpect{DataProvider.friendly.find('nonexistent')}.toraise_error(ActiveRecord::RecordNotFound)endenddescribe'#authorization_definitions'doit'returns authorization definitions for this provider'doprovider=create(:data_provider,slug: 'dgfip')definitions=provider.authorization_definitionsexpect(definitions).tobe_all{ |d| d.provider.id == 'dgfip'}endenddescribe'#reporters'dosubject{create(:data_provider,:dgfip).reporters}let!(:valid_users)do[create(:user,:reporter,authorization_request_types: %w[api_impot_particulierapi_hermes]),create(:user,:instructor,authorization_request_types: %w[api_hermes]),]endlet!(:invalid_users)do[create(:user,:reporter,authorization_request_types: %w[api_entreprise]),]endit{is_expected.tomatch_array(valid_users)}enddescribe'#instructors'dosubject{create(:data_provider,:dgfip).instructors}let!(:valid_users)do[create(:user,:instructor,authorization_request_types: %w[api_impot_particulierapi_hermes]),create(:user,:instructor,authorization_request_types: %w[api_hermes]),]endlet!(:invalid_users)do[create(:user,:reporter,authorization_request_types: %w[api_hermes]),create(:user,:reporter,authorization_request_types: %w[api_entreprise]),]endit{is_expected.tomatch_array(valid_users)}endend
Notes:
Reuse existing test patterns from current spec/models/data_provider_spec.rb
Add validation tests for all new validations
Test friendly_id functionality
Keep existing reporters and instructors tests (should still work)
Step 3.3: Update Application Helper for Logo Display
File: app/helpers/application_helper.rb:17-20
FIXME le logo est forcément présent
Change:
# Beforedefprovider_logo_image_tag(authorization_definition,options={})options=options.merge(alt: "Logo du fournisseur de données \"#{authorization_definition.provider.name}\"")image_tag("data_providers/#{authorization_definition.provider.logo}",options)end# Afterdefprovider_logo_image_tag(authorization_definition,options={})options=options.merge(alt: "Logo du fournisseur de données \"#{authorization_definition.provider.name}\"")ifauthorization_definition.provider.logo.attached?image_tag(authorization_definition.provider.logo,options)else# Fallback if logo not attached (should not happen after migration)Rails.logger.warn"Logo not attached for provider: #{authorization_definition.provider.slug}"''endend
Notes:
Use ActiveStorage's image_tag helper (works directly with attachments)
Add safety check for attached logo
Log warning if logo missing (shouldn't happen)
Test: Manual testing in views that use provider logos
Phase 4: Testing and Validation
Step 4.1: Run All RSpec Tests
Command:
bundle exec rspec
Verify:
DataProvider model tests pass
All related model tests pass (AuthorizationDefinition, User)
Controller tests pass (if any for dgfip export)
Helper tests pass (if any)
No regressions in other tests
Fix failures as they occur
Step 4.2: Run Cucumber Features
Command:
bundle exec cucumber
Verify:
All E2E tests pass
Features using DataProvider work correctly
Logo display works in views
Fix failures as they occur
Step 4.3: Manual Testing in Development
Start server:
bin/local_run.sh
Test:
Check DataProvider records exist:
DataProvider.count# Should be 12DataProvider.all.map(&:slug)
bundle exec rubocop
bundle exec rubocop -A # Auto-fix issues
Fix any style violations:
Maximum method length: 15 lines
Class length: max 150 lines
Use single quotes for strings
2 spaces indentation
Phase 5: Documentation
Step 5.1: Add Comment to YAML File
File: config/data_providers.yml
Add comment at the top:
---
# NOTE: This file is kept for reference purposes only.# Data providers are now stored in the database (data_providers table).# To add new providers, use the Rails console:# provider = DataProvider.create!(# name: 'Provider Name',# link: 'https://provider.gouv.fr'# )# provider.logo.attach(io: File.open('path/to/logo.png'), filename: 'logo.png', content_type: 'image/png')## This file was used for the initial data migration.# Migration date: [INSERT DATE]shared:
dgs:
name: Organisation de la direction générale de la santé (DGS)logo: ministere_sante.jpglink: https://www.sante.gouv.fr/# ... rest of file
Notes:
Keep YAML file as backup/reference (per user decision)
Add clear instructions for future additions via console