diff --git a/src/dashboard/Pipfile b/src/dashboard/Pipfile index cb7eddad..25977d58 100644 --- a/src/dashboard/Pipfile +++ b/src/dashboard/Pipfile @@ -10,6 +10,7 @@ django-environ = "==0.12.0" django-extensions = "==3.2.3" django-stubs = {extras = ["compatible-mypy"], version = "==5.1.1"} gunicorn = "==23.0.0" +jsonschema = "==4.23.0" psycopg = {extras = ["pool", "binary"], version = "==3.2.3"} sentry-sdk = {extras = ["django"], version = "==2.20.0"} whitenoise = "==6.8.2" diff --git a/src/dashboard/Pipfile.lock b/src/dashboard/Pipfile.lock index dd26d999..85e8b979 100644 --- a/src/dashboard/Pipfile.lock +++ b/src/dashboard/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d9c2919121acd17c2e73c77004ecf48566e097ae7294ad7da7d03d7df9ef3013" + "sha256": "abd1de7b273b496e48fca82fee5c385ba876719e9c303045680e368dda8fa461" }, "pipfile-spec": 6, "requires": { @@ -24,6 +24,14 @@ "markers": "python_version >= '3.8'", "version": "==3.8.1" }, + "attrs": { + "hashes": [ + "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", + "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308" + ], + "markers": "python_version >= '3.8'", + "version": "==24.3.0" + }, "certifi": { "hashes": [ "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", @@ -218,6 +226,23 @@ "markers": "python_version >= '3.6'", "version": "==3.10" }, + "jsonschema": { + "hashes": [ + "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", + "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==4.23.0" + }, + "jsonschema-specifications": { + "hashes": [ + "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", + "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf" + ], + "markers": "python_version >= '3.9'", + "version": "==2024.10.1" + }, "mypy": { "hashes": [ "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", @@ -362,6 +387,14 @@ "markers": "python_version >= '3.8'", "version": "==3.2.4" }, + "referencing": { + "hashes": [ + "sha256:363d9c65f080d0d70bc41c721dce3c7f3e77fc09f269cd5c8813da18069a6794", + "sha256:ca2e6492769e3602957e9b831b94211599d2aade9477f5d44110d2530cf9aade" + ], + "markers": "python_version >= '3.9'", + "version": "==0.36.1" + }, "requests": { "hashes": [ "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", @@ -370,6 +403,115 @@ "markers": "python_version >= '3.8'", "version": "==2.32.3" }, + "rpds-py": { + "hashes": [ + "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518", + "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059", + "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61", + "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5", + "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9", + "sha256:0c150c7a61ed4a4f4955a96626574e9baf1adf772c2fb61ef6a5027e52803543", + "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2", + "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a", + "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d", + "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56", + "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d", + "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd", + "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b", + "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4", + "sha256:214b7a953d73b5e87f0ebece4a32a5bd83c60a3ecc9d4ec8f1dca968a2d91e99", + "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d", + "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd", + "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe", + "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1", + "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e", + "sha256:2b8f60e1b739a74bab7e01fcbe3dddd4657ec685caa04681df9d562ef15b625f", + "sha256:2de29005e11637e7a2361fa151f780ff8eb2543a0da1413bb951e9f14b699ef3", + "sha256:2e8b55d8517a2fda8d95cb45d62a5a8bbf9dd0ad39c5b25c8833efea07b880ca", + "sha256:2fa4331c200c2521512595253f5bb70858b90f750d39b8cbfd67465f8d1b596d", + "sha256:3445e07bf2e8ecfeef6ef67ac83de670358abf2996916039b16a218e3d95e97e", + "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc", + "sha256:378753b4a4de2a7b34063d6f95ae81bfa7b15f2c1a04a9518e8644e81807ebea", + "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38", + "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b", + "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c", + "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff", + "sha256:44d61b4b7d0c2c9ac019c314e52d7cbda0ae31078aabd0f22e583af3e0d79723", + "sha256:4617e1915a539a0d9a9567795023de41a87106522ff83fbfaf1f6baf8e85437e", + "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493", + "sha256:5246b14ca64a8675e0a7161f7af68fe3e910e6b90542b4bfb5439ba752191df6", + "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83", + "sha256:583f6a1993ca3369e0f80ba99d796d8e6b1a3a2a442dd4e1a79e652116413091", + "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1", + "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627", + "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1", + "sha256:5f0e260eaf54380380ac3808aa4ebe2d8ca28b9087cf411649f96bad6900c728", + "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16", + "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c", + "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45", + "sha256:666ecce376999bf619756a24ce15bb14c5bfaf04bf00abc7e663ce17c3f34fe7", + "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a", + "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730", + "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967", + "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25", + "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24", + "sha256:70fb28128acbfd264eda9bf47015537ba3fe86e40d046eb2963d75024be4d055", + "sha256:7b2513ba235829860b13faa931f3b6846548021846ac808455301c23a101689d", + "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0", + "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e", + "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7", + "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c", + "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f", + "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd", + "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652", + "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8", + "sha256:a63cbdd98acef6570c62b92a1e43266f9e8b21e699c363c0fef13bd530799c11", + "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333", + "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96", + "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64", + "sha256:b25bc607423935079e05619d7de556c91fb6adeae9d5f80868dde3468657994b", + "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e", + "sha256:bb47271f60660803ad11f4c61b42242b8c1312a31c98c578f79ef9387bbde21c", + "sha256:bbb232860e3d03d544bc03ac57855cd82ddf19c7a07651a7c0fdb95e9efea8b9", + "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec", + "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb", + "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37", + "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad", + "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9", + "sha256:cfbc454a2880389dbb9b5b398e50d439e2e58669160f27b60e5eca11f68ae17c", + "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf", + "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4", + "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f", + "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d", + "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09", + "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d", + "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566", + "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74", + "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338", + "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15", + "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c", + "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648", + "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84", + "sha256:eaf16ae9ae519a0e237a0f528fd9f0197b9bb70f40263ee57ae53c2b8d48aeb3", + "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123", + "sha256:f276b245347e6e36526cbd4a266a417796fc531ddf391e43574cf6466c492520", + "sha256:f47ad3d5f3258bd7058d2d506852217865afefe6153a36eb4b6928758041d831", + "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e", + "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf", + "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b", + "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2", + "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3", + "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130", + "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b", + "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de", + "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5", + "sha256:fb6116dfb8d1925cbdb52595560584db42a7f664617a1f7d7f6e32f138cdf37d", + "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00", + "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e" + ], + "markers": "python_version >= '3.9'", + "version": "==0.22.3" + }, "sentry-sdk": { "extras": [ "django" diff --git a/src/dashboard/apps/consent/migrations/0004_consent_administration_address_1_and_more.py b/src/dashboard/apps/consent/migrations/0004_consent_administration_address_1_and_more.py deleted file mode 100644 index b4dacfdc..00000000 --- a/src/dashboard/apps/consent/migrations/0004_consent_administration_address_1_and_more.py +++ /dev/null @@ -1,284 +0,0 @@ -# Generated by Django 5.1.5 on 2025-01-21 14:17 - -import apps.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("qcd_consent", "0003_alter_consent_managers_alter_consent_end_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="consent", - name="administration_address_1", - field=models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="administration address", - ), - ), - migrations.AddField( - model_name="consent", - name="administration_address_2", - field=models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="administration address complement", - ), - ), - migrations.AddField( - model_name="consent", - name="administration_email", - field=models.EmailField( - blank=True, - max_length=254, - null=True, - verbose_name="administration email", - ), - ), - migrations.AddField( - model_name="consent", - name="administration_name", - field=models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="administration name", - ), - ), - migrations.AddField( - model_name="consent", - name="administration_represented_by", - field=models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="administration represented by", - ), - ), - migrations.AddField( - model_name="consent", - name="administration_town", - field=models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="administration town", - ), - ), - migrations.AddField( - model_name="consent", - name="administration_zip_code", - field=models.CharField( - blank=True, - max_length=5, - null=True, - validators=[apps.core.validators.validate_zip_code], - verbose_name="administration zip code", - ), - ), - migrations.AddField( - model_name="consent", - name="allows_daily_index_readings", - field=models.BooleanField( - blank=True, - null=True, - verbose_name="allow history of daily index readings in kWh", - ), - ), - migrations.AddField( - model_name="consent", - name="allows_load_curve", - field=models.BooleanField( - blank=True, - null=True, - verbose_name="allows history of load curve, at steps returned by Enedis", - ), - ), - migrations.AddField( - model_name="consent", - name="allows_max_daily_power", - field=models.BooleanField( - blank=True, - null=True, - verbose_name="allows historical maximum daily power in kVa or kWh ", - ), - ), - migrations.AddField( - model_name="consent", - name="allows_measurements", - field=models.BooleanField( - blank=True, - null=True, - verbose_name="allows historical measurements in kWh", - ), - ), - migrations.AddField( - model_name="consent", - name="allows_technical_contractual_data", - field=models.BooleanField( - blank=True, - null=True, - verbose_name="allows the technical and contractual data available", - ), - ), - migrations.AddField( - model_name="consent", - name="company_address_1", - field=models.CharField( - blank=True, max_length=255, null=True, verbose_name="company address" - ), - ), - migrations.AddField( - model_name="consent", - name="company_address_2", - field=models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="company address complement", - ), - ), - migrations.AddField( - model_name="consent", - name="company_legal_form", - field=models.CharField( - blank=True, - help_text="SA, SARL …", - max_length=50, - null=True, - verbose_name="company legal form", - ), - ), - migrations.AddField( - model_name="consent", - name="company_naf", - field=models.CharField( - blank=True, - max_length=5, - null=True, - validators=[apps.core.validators.validate_naf_code], - verbose_name="company NAF code", - ), - ), - migrations.AddField( - model_name="consent", - name="company_name", - field=models.CharField( - blank=True, max_length=255, null=True, verbose_name="company name" - ), - ), - migrations.AddField( - model_name="consent", - name="company_siret", - field=models.CharField( - blank=True, - max_length=14, - null=True, - validators=[apps.core.validators.validate_siret], - verbose_name="company SIRET", - ), - ), - migrations.AddField( - model_name="consent", - name="company_town", - field=models.CharField( - blank=True, max_length=255, null=True, verbose_name="company town" - ), - ), - migrations.AddField( - model_name="consent", - name="company_trade_name", - field=models.CharField( - blank=True, max_length=255, null=True, verbose_name="company trade name" - ), - ), - migrations.AddField( - model_name="consent", - name="company_type", - field=models.CharField( - blank=True, - help_text="entreprise/ collectivité locale, ECPI, Association, copropriété, ...", - max_length=255, - null=True, - verbose_name="company type", - ), - ), - migrations.AddField( - model_name="consent", - name="company_zip_code", - field=models.CharField( - blank=True, - max_length=5, - null=True, - validators=[apps.core.validators.validate_zip_code], - verbose_name="company zip code", - ), - ), - migrations.AddField( - model_name="consent", - name="done_at", - field=models.CharField( - blank=True, max_length=255, null=True, verbose_name="done_at" - ), - ), - migrations.AddField( - model_name="consent", - name="is_authorized_signatory", - field=models.BooleanField( - blank=True, null=True, verbose_name="the signatory is authorized" - ), - ), - migrations.AddField( - model_name="consent", - name="representative_email", - field=models.EmailField( - blank=True, - max_length=254, - null=True, - verbose_name="representative email", - ), - ), - migrations.AddField( - model_name="consent", - name="representative_firstname", - field=models.CharField( - blank=True, - max_length=150, - null=True, - verbose_name="representative firstname", - ), - ), - migrations.AddField( - model_name="consent", - name="representative_lastname", - field=models.CharField( - blank=True, - max_length=150, - null=True, - verbose_name="representative lastname", - ), - ), - migrations.AddField( - model_name="consent", - name="representative_phone", - field=models.CharField( - blank=True, - max_length=20, - null=True, - verbose_name="representative phone", - ), - ), - migrations.AddField( - model_name="consent", - name="signature_date", - field=models.DateTimeField( - blank=True, null=True, verbose_name="signature date" - ), - ), - ] diff --git a/src/dashboard/apps/consent/migrations/0004_consent_contractual_fields.py b/src/dashboard/apps/consent/migrations/0004_consent_contractual_fields.py new file mode 100644 index 00000000..d94bbf8f --- /dev/null +++ b/src/dashboard/apps/consent/migrations/0004_consent_contractual_fields.py @@ -0,0 +1,104 @@ +# Generated by Django 5.1.5 on 2025-01-22 15:24 + +import apps.consent.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("qcd_consent", "0003_alter_consent_managers_alter_consent_end_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="consent", + name="allows_daily_index_readings", + field=models.BooleanField( + default=False, + verbose_name="allow history of daily index readings in kWh", + ), + ), + migrations.AddField( + model_name="consent", + name="allows_load_curve", + field=models.BooleanField( + default=False, + verbose_name="allows history of load curve, at steps returned by Enedis", + ), + ), + migrations.AddField( + model_name="consent", + name="allows_max_daily_power", + field=models.BooleanField( + default=False, + verbose_name="allows historical maximum daily power in kVa or kWh ", + ), + ), + migrations.AddField( + model_name="consent", + name="allows_measurements", + field=models.BooleanField( + default=False, verbose_name="allows historical measurements in kWh" + ), + ), + migrations.AddField( + model_name="consent", + name="allows_technical_contractual_data", + field=models.BooleanField( + default=False, + verbose_name="allows the technical and contractual data available", + ), + ), + migrations.AddField( + model_name="consent", + name="company", + field=models.JSONField( + blank=True, + null=True, + validators=[apps.consent.validators.validate_company_schema], + verbose_name="company informations", + ), + ), + migrations.AddField( + model_name="consent", + name="company_representative", + field=models.JSONField( + blank=True, + null=True, + validators=[apps.consent.validators.validate_representative_schema], + verbose_name="company representative", + ), + ), + migrations.AddField( + model_name="consent", + name="control_authority", + field=models.JSONField( + blank=True, + null=True, + validators=[apps.consent.validators.validate_control_authority_schema], + verbose_name="company representative", + ), + ), + migrations.AddField( + model_name="consent", + name="done_at", + field=models.CharField( + blank=True, max_length=255, null=True, verbose_name="done at" + ), + ), + migrations.AddField( + model_name="consent", + name="is_authorized_signatory", + field=models.BooleanField( + default=False, verbose_name="the signatory is authorized" + ), + ), + migrations.AddField( + model_name="consent", + name="signed_at", + field=models.DateTimeField( + blank=True, null=True, verbose_name="signature date" + ), + ), + ] diff --git a/src/dashboard/apps/consent/models.py b/src/dashboard/apps/consent/models.py index 1407bb65..e876358b 100644 --- a/src/dashboard/apps/consent/models.py +++ b/src/dashboard/apps/consent/models.py @@ -5,19 +5,23 @@ from django.utils.translation import gettext_lazy as _ from apps.core.abstract_models import DashboardBase -from apps.core.validators import validate_naf_code, validate_siret, validate_zip_code from . import AWAITING, CONSENT_STATUS_CHOICE, REVOKED, VALIDATED from .exceptions import ConsentWorkflowError from .managers import ConsentManager from .utils import consent_end_date +from .validators import ( + validate_company_schema, + validate_control_authority_schema, + validate_representative_schema, +) class Consent(DashboardBase): """Represents the consent status for a given delivery point and user. For contractual reason, a consent cannot be modified after it has the status - "VALIDATED" or "REVOKED". + `VALIDATED` or `REVOKED`. Attributes: - AWAITING: Status indicating that the consent is awaiting validation. @@ -52,134 +56,54 @@ class Consent(DashboardBase): end = models.DateTimeField(_("end date"), default=consent_end_date) revoked_at = models.DateTimeField(_("revoked at"), null=True, blank=True) - # Contractual fields of the company + # Contractual field of the company # These fields are populated with data from the linked entity. - company_type = models.CharField( - _("company type"), - max_length=255, - help_text=_( - "entreprise/ collectivité locale, ECPI, Association, copropriété, ..." - ), + company = models.JSONField( + _("company informations"), blank=True, null=True, - ) - company_name = models.CharField( - _("company name"), max_length=255, blank=True, null=True - ) - company_legal_form = models.CharField( - _("company legal form"), - max_length=50, - help_text=_("SA, SARL …"), - blank=True, - null=True, - ) - company_trade_name = models.CharField( - _("company trade name"), max_length=255, blank=True, null=True - ) - company_siret = models.CharField( - _("company SIRET"), - max_length=14, - validators=[validate_siret], - blank=True, - null=True, - ) - company_naf = models.CharField( - _("company NAF code"), - validators=[validate_naf_code], - max_length=5, - blank=True, - null=True, - ) - company_address_1 = models.CharField( - _("company address"), max_length=255, blank=True, null=True - ) - company_address_2 = models.CharField( - _("company address complement"), max_length=255, blank=True, null=True - ) - company_zip_code = models.CharField( - _("company zip code"), - max_length=5, - validators=[validate_zip_code], - blank=True, - null=True, - ) - company_town = models.CharField( - _("company town"), max_length=255, blank=True, null=True + validators=[validate_company_schema], ) - # Contractual fields of the company representative + # Contractual field of the company representative # These fields are populated with the current user data. - representative_firstname = models.CharField( - _("representative firstname"), max_length=150, blank=True, null=True - ) - representative_lastname = models.CharField( - _("representative lastname"), max_length=150, blank=True, null=True - ) - representative_email = models.EmailField( - _("representative email"), blank=True, null=True - ) - representative_phone = models.CharField( - _("representative phone"), max_length=20, blank=True, null=True + company_representative = models.JSONField( + _("company representative"), + blank=True, + null=True, + validators=[validate_representative_schema], ) - # Contractual fields of the Administration - # These fields are populated via `settings.CONSENT_ADMINISTRATION_FIELDS` - administration_name = models.CharField( - _("administration name"), max_length=255, blank=True, null=True - ) - administration_address_1 = models.CharField( - _("administration address"), max_length=255, blank=True, null=True - ) - administration_address_2 = models.CharField( - _("administration address complement"), max_length=255, blank=True, null=True - ) - administration_zip_code = models.CharField( - _("administration zip code"), - max_length=5, - validators=[validate_zip_code], + # Contractual field of the control authority + # These fields are populated via `settings.CONSENT_ADMINISTRATION_FIELDS`. + control_authority = models.JSONField( + _("company representative"), blank=True, null=True, - ) - administration_town = models.CharField( - _("administration town"), max_length=255, blank=True, null=True - ) - administration_represented_by = models.CharField( - _("administration represented by"), max_length=255, blank=True, null=True - ) - administration_email = models.EmailField( - _("administration email"), blank=True, null=True + validators=[validate_control_authority_schema], ) # Fields populated via the consent form is_authorized_signatory = models.BooleanField( - _("the signatory is authorized"), blank=True, null=True + _("the signatory is authorized"), default=False ) - allows_measurements = models.BooleanField( - _("allows historical measurements in kWh"), blank=True, null=True + _("allows historical measurements in kWh"), default=False ) allows_daily_index_readings = models.BooleanField( - _("allow history of daily index readings in kWh"), - blank=True, - null=True, + _("allow history of daily index readings in kWh"), default=False ) allows_max_daily_power = models.BooleanField( - _("allows historical maximum daily power in kVa or kWh "), - blank=True, - null=True, + _("allows historical maximum daily power in kVa or kWh "), default=False ) allows_load_curve = models.BooleanField( - _("allows history of load curve, at steps returned by Enedis"), - blank=True, - null=True, + _("allows history of load curve, at steps returned by Enedis"), default=False ) allows_technical_contractual_data = models.BooleanField( - _("allows the technical and contractual data available"), - blank=True, - null=True, + _("allows the technical and contractual data available"), default=False ) - signature_date = models.DateTimeField(_("signature date"), blank=True, null=True) - done_at = models.CharField(_("done_at"), max_length=255, blank=True, null=True) + signed_at = models.DateTimeField(_("signature date"), blank=True, null=True) + done_at = models.CharField(_("done at"), max_length=255, blank=True, null=True) # models.Manager() must be in first place to ensure django admin expectations. objects = models.Manager() @@ -242,7 +166,7 @@ def _is_update_allowed(self) -> bool: Workflow according to consent status: - AWAITING: - - can be updated without restriction + - can be updated without restriction. - VALIDATED - if the status is updated to something other than REVOKED, an exception is diff --git a/src/dashboard/apps/consent/schemas.py b/src/dashboard/apps/consent/schemas.py new file mode 100644 index 00000000..80531c82 --- /dev/null +++ b/src/dashboard/apps/consent/schemas.py @@ -0,0 +1,80 @@ +"""Dashboard consent app JSON schemas. + +JSON schemas: +- company_schema +- representative_schema +- control_authority_schema +""" + +company_schema = { + "type": "object", + "properties": { + "company_type": {"type": ["string", "null"], "maxLength": 255}, + "name": {"type": ["string", "null"], "maxLength": 255}, + "legal_form": {"type": ["string", "null"], "maxLength": 50}, + "trade_name": {"type": ["string", "null"], "maxLength": 255}, + "siret": { + "type": ["string", "null"], + "maxLength": 14, + "pattern": "^[0-9]{14}$", + }, + "naf": { + "type": ["string", "null"], + "maxLength": 5, + "pattern": "^[0-9]{4}[A-Za-z]$", + }, + "address": { + "type": "object", + "properties": { + "line_1": {"type": ["string", "null"], "maxLength": 255}, + "line_2": {"type": ["string", "null"], "maxLength": 255}, + "zip_code": { + "type": ["string", "null"], + "maxLength": 5, + "pattern": "^[0-9]{5}$", + }, + "city": {"type": ["string", "null"], "maxLength": 255}, + }, + "required": ["line_1", "zip_code", "city"], + }, + }, + "required": ["name", "siret", "address"], + "additionalProperties": False, +} + +representative_schema = { + "type": "object", + "properties": { + "firstname": {"type": ["string", "null"], "maxLength": 150}, + "lastname": {"type": ["string", "null"], "maxLength": 150}, + "email": {"type": ["string", "null"], "format": "email"}, + "phone": {"type": ["string", "null"], "maxLength": 20}, + }, + "required": ["firstname", "lastname"], + "additionalProperties": False, +} + +control_authority_schema = { + "type": "object", + "properties": { + "name": {"type": ["string", "null"], "maxLength": 255}, + "represented_by": {"type": ["string", "null"], "maxLength": 255}, + "email": {"type": ["string", "null"], "format": "email"}, + "address": { + "type": "object", + "properties": { + "line_1": {"type": ["string", "null"], "maxLength": 255}, + "line_2": {"type": ["string", "null"], "maxLength": 255}, + "zip_code": { + "type": ["string", "null"], + "maxLength": 5, + "pattern": "^[0-9]{5}$", + }, + "city": {"type": ["string", "null"], "maxLength": 255}, + }, + "required": ["line_1", "zip_code", "city"], + }, + }, + "required": ["name", "represented_by", "email", "address"], + "additionalProperties": False, +} diff --git a/src/dashboard/apps/consent/tests/test_validators.py b/src/dashboard/apps/consent/tests/test_validators.py new file mode 100644 index 00000000..6f01b838 --- /dev/null +++ b/src/dashboard/apps/consent/tests/test_validators.py @@ -0,0 +1,129 @@ +"""Dashboard consent validators tests.""" + +import pytest +from django.core.exceptions import ValidationError + +from apps.consent.schemas import ( + company_schema, + control_authority_schema, + representative_schema, +) +from apps.consent.validators import json_schema_validator + +VALID_COMPANY_DATA = { + "company_type": "SARL", + "name": "My Company", + "legal_form": "SARL", + "trade_name": "The test company", + "siret": "12345678901234", + "naf": "1234A", + "address": { + "line_1": "1 rue Exemple", + "line_2": None, + "zip_code": "75000", + "city": "Paris", + }, +} + +VALID_REPRESENTATIVE_DATA = { + "firstname": "Alice", + "lastname": "Brown", + "email": "alice.brown@example.com", + "phone": "9876543210", +} + +VALID_CONTROL_AUTHORITY_DATA = { + "name": "QualiCharge", + "represented_by": "John Doe", + "email": "mail@test.com", + "address": { + "line_1": "1 Rue Exemple", + "line_2": None, + "zip_code": "75000", + "city": "Paris", + }, +} + + +def test_company_json_schema_validator_valid(): + """Test the json schema validator with a valid company data.""" + validator = json_schema_validator(company_schema) + validator(VALID_COMPANY_DATA) + + +def test_company_json_schema_validator_invalid(): + """Test the json schema validator with a valid company data.""" + validator = json_schema_validator(company_schema) + + # test without properties + invalid_value = {} + with pytest.raises(ValidationError): + validator(invalid_value) + + # test with invalid value (invalid siret) + invalid_value = VALID_COMPANY_DATA + invalid_value["siret"] = "1234" + with pytest.raises(ValidationError): + validator(invalid_value) + + # test with additional properties + invalid_value = VALID_COMPANY_DATA + invalid_value["additional_property"] = "" + with pytest.raises(ValidationError): + validator(invalid_value) + + +def test_representative_json_schema_validator_valid(): + """Test the json schema validator with a valid representative company data.""" + validator = json_schema_validator(representative_schema) + validator(VALID_REPRESENTATIVE_DATA) + + +def test_representative_json_schema_validator_invalid(): + """Test the json schema validator with a valid representative company data.""" + validator = json_schema_validator(representative_schema) + + # test without properties + invalid_value = {} + with pytest.raises(ValidationError): + validator(invalid_value) + + # test with invalid value (invalid email) + invalid_value = VALID_REPRESENTATIVE_DATA + invalid_value["firstname"] = 1234 + with pytest.raises(ValidationError): + validator(invalid_value) + + # test with additional properties + invalid_value = VALID_REPRESENTATIVE_DATA + invalid_value["additional_property"] = "" + with pytest.raises(ValidationError): + validator(invalid_value) + + +def test_control_authority_json_schema_validator_valid(): + """Test the json schema validator with a valid control authority data.""" + validator = json_schema_validator(control_authority_schema) + validator(VALID_CONTROL_AUTHORITY_DATA) + + +def test_control_authority_json_schema_validator_invalid(): + """Test the json schema validator with a valid control authority data.""" + validator = json_schema_validator(control_authority_schema) + + # test without properties + invalid_value = {} + with pytest.raises(ValidationError): + validator(invalid_value) + + # test with invalid value (invalid email) + invalid_value = VALID_CONTROL_AUTHORITY_DATA + invalid_value["represented_by"] = 1234 + with pytest.raises(ValidationError): + validator(invalid_value) + + # test with additional properties + invalid_value = VALID_CONTROL_AUTHORITY_DATA + invalid_value["additional_property"] = "" + with pytest.raises(ValidationError): + validator(invalid_value) diff --git a/src/dashboard/apps/consent/validators.py b/src/dashboard/apps/consent/validators.py new file mode 100644 index 00000000..e1444086 --- /dev/null +++ b/src/dashboard/apps/consent/validators.py @@ -0,0 +1,42 @@ +"""Dashboard consent app validators.""" + +from django.core.exceptions import ValidationError +from jsonschema import ValidationError as JSONSchemaValidationError +from jsonschema import validate + +from apps.consent.schemas import ( + company_schema, + control_authority_schema, + representative_schema, +) + + +def json_schema_validator(schema): + """JSON schema validator.""" + + def validator(value): + """Validate a JSON object against a schema.""" + try: + validate(instance=value, schema=schema) + except JSONSchemaValidationError as e: + raise ValidationError(f"Invalid JSON: {e.message}") from e + + return validator + + +def validate_company_schema(value): + """Validate a company JSON object against the company schema.""" + validator = json_schema_validator(company_schema) + return validator(value) + + +def validate_representative_schema(value): + """Validate a representative JSON object against the representative schema.""" + validator = json_schema_validator(representative_schema) + return validator(value) + + +def validate_control_authority_schema(value): + """Validate a control authority JSON object against the control authority schema.""" + validator = json_schema_validator(control_authority_schema) + return validator(value) diff --git a/src/dashboard/apps/core/tests/test_validators.py b/src/dashboard/apps/core/tests/test_validators.py index ccc52e58..18562acc 100644 --- a/src/dashboard/apps/core/tests/test_validators.py +++ b/src/dashboard/apps/core/tests/test_validators.py @@ -36,7 +36,8 @@ def test_validate_naf_code_valid(value): @pytest.mark.parametrize( - "value", ["12345", "12345Z", "123A", "12A45", "ABC1Z", "1234!", "abcdA", "1234", ""] + "value", + ["12345", "12345Z", "123A", "12A45", "ABC1Z", "1234!", "abcdA", "1234", "", 12345], ) def test_validate_naf_code_invalid(value): """Test that invalid NAF codes raise a ValidationError.""" diff --git a/src/dashboard/apps/core/validators.py b/src/dashboard/apps/core/validators.py index e0f884e7..8cde34c9 100644 --- a/src/dashboard/apps/core/validators.py +++ b/src/dashboard/apps/core/validators.py @@ -5,8 +5,6 @@ from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -DIGITS_NUMBER = 14 - def validate_siret(value: str | None) -> None: """Validate a SIRET number. @@ -14,20 +12,19 @@ def validate_siret(value: str | None) -> None: SIRET must be a string that contains only numbers and have a fixed length of 14 characters. """ - error_message_numeric = _("The SIRET must be composed only of numbers.") - error_message_length = _("The SIRET must contain exactly 14 digits.") + error_message = _( + "The SIRET must be composed only of numbers and must " + "contain exactly 14 digits." + ) if value is None: return if not isinstance(value, str): - raise ValidationError(error_message_numeric) - - if not value.isdigit(): - raise ValidationError(error_message_numeric) + raise ValidationError(error_message) - if len(value) != DIGITS_NUMBER: - raise ValidationError(error_message_length) + if not re.match(r"^\d{14}$", value): + raise ValidationError(error_message) def validate_naf_code(value: str | None) -> None: @@ -35,19 +32,22 @@ def validate_naf_code(value: str | None) -> None: NAF code must respect the format "####A" (4 digits + 1 letter). """ + error_message = _( + "The NAF code must be in the format of 4 digits " + "followed by a letter (e.g.: 6820A)." + ) + if value is None: return + if not isinstance(value, str): + raise ValidationError(error_message) + if not re.match(r"^\d{4}[A-Za-z]$", value): - raise ValidationError( - _( - "The NAF code must be in the format of 4 digits " - "followed by a letter (e.g.: 6820A)." - ) - ) + raise ValidationError(error_message) -def validate_zip_code(value: int | None) -> None: +def validate_zip_code(value: str | None) -> None: """Validate a zip code. Zip code must have only digits and a fixed length of 5 characters.