From 31b4db16b8b5162aa40e022ddfb7c61298963b61 Mon Sep 17 00:00:00 2001 From: ssorin Date: Mon, 20 Jan 2025 18:10:39 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F(dashboard)=20add=20admini?= =?UTF-8?q?stration=20and=20company=20fields=20to=20consent=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced new fields in the Consent model to handle company and administration information, including validation for SIRET, NAF code, and zip code. Updated settings with configurable default values for these fields. Added core validators for ensuring data integrity in relevant fields. --- .github/workflows/dashboard.yml | 2 + env.d/dashboard | 3 + src/dashboard/CHANGELOG.md | 1 + src/dashboard/Pipfile | 1 + src/dashboard/Pipfile.lock | 144 +++++++++++++++++- .../0004_consent_contractual_fields.py | 107 +++++++++++++ src/dashboard/apps/consent/models.py | 60 +++++++- src/dashboard/apps/consent/schemas.py | 88 +++++++++++ src/dashboard/apps/consent/settings.py | 16 ++ .../apps/consent/tests/test_validators.py | 119 +++++++++++++++ src/dashboard/apps/consent/validators.py | 42 +++++ .../apps/core/tests/test_validators.py | 58 +++++++ src/dashboard/apps/core/validators.py | 66 ++++++++ src/dashboard/dashboard/settings.py | 5 + 14 files changed, 710 insertions(+), 2 deletions(-) create mode 100644 src/dashboard/apps/consent/migrations/0004_consent_contractual_fields.py create mode 100644 src/dashboard/apps/consent/schemas.py create mode 100644 src/dashboard/apps/consent/tests/test_validators.py create mode 100644 src/dashboard/apps/consent/validators.py create mode 100644 src/dashboard/apps/core/tests/test_validators.py create mode 100644 src/dashboard/apps/core/validators.py diff --git a/.github/workflows/dashboard.yml b/.github/workflows/dashboard.yml index 2a68721a..0cd842a9 100644 --- a/.github/workflows/dashboard.yml +++ b/.github/workflows/dashboard.yml @@ -95,3 +95,5 @@ jobs: DASHBOARD_DB_NAME: test-qualicharge-dashboard DASHBOARD_DATABASE_URL: psql://qualicharge:pass@localhost:5432/test-qualicharge-dashboard DASHBOARD_SECRET_KEY: the_secret_key + DASHBOARD_CONTROL_AUTHORITY: null + DASHBOARD_CONSENT_DONE_AT: Paris diff --git a/env.d/dashboard b/env.d/dashboard index 3bed1aa7..6e7ab66d 100644 --- a/env.d/dashboard +++ b/env.d/dashboard @@ -19,3 +19,6 @@ DJANGO_SUPERUSER_PASSWORD=admin DJANGO_SUPERUSER_USERNAME=admin DJANGO_SUPERUSER_EMAIL=admin@example.com +# Control authority contact +DASHBOARD_CONTROL_AUTHORITY={"name": "QualiCharge", "address_1": "1 rue de test", "address_2": "", "zip_code": "75000", "city": "Paris", "represented_by": "John Doe", "email": "jdoe@exemple.com"} +DASHBOARD_CONSENT_DONE_AT=Paris diff --git a/src/dashboard/CHANGELOG.md b/src/dashboard/CHANGELOG.md index c8452f08..09aefc0e 100644 --- a/src/dashboard/CHANGELOG.md +++ b/src/dashboard/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to - add consent form to manage consents of one or many entities - add admin integration for Entity, DeliveryPoint and Consent - add mass admin action (make revoked) for consents +- add validators for SIRET, NAF code and Zip code - disallow mass action "delete" for consents in admin - block the updates of all new data if a consent has the status `REVOKED` - block the updates of all new data if a consent has the status `VALIDATED` diff --git a/src/dashboard/Pipfile b/src/dashboard/Pipfile index ec6f4ab1..adc390cb 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.2"} gunicorn = "==23.0.0" +jsonschema = "==4.23.0" psycopg = {extras = ["pool", "binary"], version = "==3.2.4"} 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 ed434bfc..1d9eb1e0 100644 --- a/src/dashboard/Pipfile.lock +++ b/src/dashboard/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "496b40a735303e7249a068161816995cef1e281c1594706b9c2c0294f436f8b2" + "sha256": "7358d3f0e8f050ead73c57f2c8ad599b4ba947fe0467f16732d7ccf87ad62bfe" }, "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:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", @@ -369,6 +394,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", @@ -377,6 +410,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_contractual_fields.py b/src/dashboard/apps/consent/migrations/0004_consent_contractual_fields.py new file mode 100644 index 00000000..94c6af70 --- /dev/null +++ b/src/dashboard/apps/consent/migrations/0004_consent_contractual_fields.py @@ -0,0 +1,107 @@ +# 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, + default=None, + 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, + default=None, + null=True, + validators=[apps.consent.validators.validate_representative_schema], + verbose_name="company representative informations", + ), + ), + migrations.AddField( + model_name="consent", + name="control_authority", + field=models.JSONField( + blank=True, + default=None, + null=True, + validators=[apps.consent.validators.validate_control_authority_schema], + verbose_name="control authority informations", + ), + ), + 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 d011999c..23432917 100644 --- a/src/dashboard/apps/consent/models.py +++ b/src/dashboard/apps/consent/models.py @@ -10,11 +10,19 @@ 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`. + Attributes: - AWAITING: Status indicating that the consent is awaiting validation. - VALIDATED: Status indicating that the consent has been validated. @@ -48,6 +56,56 @@ class Consent(DashboardBase): end = models.DateTimeField(_("end date"), default=consent_end_date) revoked_at = models.DateTimeField(_("revoked at"), null=True, blank=True) + # Contractual field of the company + company = models.JSONField( + _("company informations"), + blank=True, + null=True, + default=None, + validators=[validate_company_schema], + ) + + # Contractual field of the company representative + company_representative = models.JSONField( + _("company representative informations"), + blank=True, + null=True, + default=None, + validators=[validate_representative_schema], + ) + + # Contractual field of the control authority + control_authority = models.JSONField( + _("control authority informations"), + blank=True, + null=True, + default=None, + validators=[validate_control_authority_schema], + ) + + # specific authorization fields + is_authorized_signatory = models.BooleanField( + _("the signatory is authorized"), default=False + ) + allows_measurements = models.BooleanField( + _("allows historical measurements in kWh"), default=False + ) + allows_daily_index_readings = models.BooleanField( + _("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 "), default=False + ) + allows_load_curve = models.BooleanField( + _("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"), default=False + ) + + 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() active_objects = ConsentManager() @@ -109,7 +167,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..2c2e0a79 --- /dev/null +++ b/src/dashboard/apps/consent/schemas.py @@ -0,0 +1,88 @@ +"""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": [ + "company_type", + "name", + "legal_form", + "trade_name", + "siret", + "naf", + "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", "email", "phone"], + "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/settings.py b/src/dashboard/apps/consent/settings.py index 2dd4e33e..ef0542a5 100644 --- a/src/dashboard/apps/consent/settings.py +++ b/src/dashboard/apps/consent/settings.py @@ -12,3 +12,19 @@ # CONSENT_NUMBER_DAYS_END_DATE = None will return 2024-12-31 23:59:59 (if calculated # during the year 2024). CONSENT_NUMBER_DAYS_END_DATE = getattr(settings, "CONSENT_NUMBER_DAYS_END_DATE", None) + +# Control authority contact for consent validation. +CONSENT_CONTROL_AUTHORITY = getattr( + settings, + "CONSENT_CONTROL_AUTHORITY", + { + "name": "", + "address_1": "", + "address_2": "", + "zip_code": "92000", + "city": "", + "represented_by": "", + "email": "", + }, +) +CONSENT_DONE_AT = getattr(settings, "CONSENT_DONE_AT", "") 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..25a6c5f4 --- /dev/null +++ b/src/dashboard/apps/consent/tests/test_validators.py @@ -0,0 +1,119 @@ +"""Dashboard consent validators tests.""" + +import pytest +from django.core.exceptions import ValidationError + +from apps.consent.validators import ( + validate_company_schema, + validate_control_authority_schema, + validate_representative_schema, +) + +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_validate_company_schema_valid(): + """Test the json schema validator with a valid company data.""" + validate_company_schema(VALID_COMPANY_DATA) + + +def test_validate_company_schema_invalid(): + """Test the json schema validator with a valid company data.""" + # test without properties + invalid_value = {} + with pytest.raises(ValidationError): + validate_company_schema(invalid_value) + + # test with invalid value (invalid siret) + invalid_value = VALID_COMPANY_DATA + invalid_value["siret"] = "1234" + with pytest.raises(ValidationError): + validate_company_schema(invalid_value) + + # test with additional properties + invalid_value = VALID_COMPANY_DATA + invalid_value["additional_property"] = "" + with pytest.raises(ValidationError): + validate_company_schema(invalid_value) + + +def test_validate_representative_schema_valid(): + """Test the json schema validator with a valid representative company data.""" + validate_representative_schema(VALID_REPRESENTATIVE_DATA) + + +def test_validate_representative_schema_invalid(): + """Test the json schema validator with a valid representative company data.""" + # test without properties + invalid_value = {} + with pytest.raises(ValidationError): + validate_representative_schema(invalid_value) + + # test with invalid value + invalid_value = VALID_REPRESENTATIVE_DATA + invalid_value["firstname"] = 1234 + with pytest.raises(ValidationError): + validate_representative_schema(invalid_value) + + # test with additional properties + invalid_value = VALID_REPRESENTATIVE_DATA + invalid_value["additional_property"] = "" + with pytest.raises(ValidationError): + validate_representative_schema(invalid_value) + + +def test_validate_control_authority_schema_valid(): + """Test the json schema validator with a valid control authority data.""" + validate_control_authority_schema(VALID_CONTROL_AUTHORITY_DATA) + + +def test_validate_control_authority_schema_invalid(): + """Test the json schema validator with a valid control authority data.""" + # test without properties + invalid_value = {} + with pytest.raises(ValidationError): + validate_control_authority_schema(invalid_value) + + # test with invalid value + invalid_value = VALID_CONTROL_AUTHORITY_DATA + invalid_value["represented_by"] = 1234 + with pytest.raises(ValidationError): + validate_control_authority_schema(invalid_value) + + # test with additional properties + invalid_value = VALID_CONTROL_AUTHORITY_DATA + invalid_value["additional_property"] = "" + with pytest.raises(ValidationError): + validate_control_authority_schema(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 new file mode 100644 index 00000000..18562acc --- /dev/null +++ b/src/dashboard/apps/core/tests/test_validators.py @@ -0,0 +1,58 @@ +"""Dashboard core validators tests.""" + +import pytest +from django.core.exceptions import ValidationError + +from apps.core.validators import validate_naf_code, validate_siret, validate_zip_code + + +@pytest.mark.parametrize("value", ["12345678901234", "00000000000000", None]) +def test_validate_siret_valid(value): + """Tests that a valid SIRET does not raise an exception.""" + assert validate_siret(value) is None + + +@pytest.mark.parametrize( + "value", + [ + "1234567890123", # Too short + "123456789012345", # Too long + "1234ABC8901234", # Contains non-numeric characters + 1234, # Number + "", # Empty string + " " * 14, # Only spaces + ], +) +def test_validate_siret_invalid(value): + """Tests that an invalid SIRET raises a ValidationError.""" + with pytest.raises(ValidationError): + validate_siret(value) + + +@pytest.mark.parametrize("value", ["1234A", "0001Z", "9876B", "0000Z", None]) +def test_validate_naf_code_valid(value): + """Test that valid NAF codes does not raise an exception.""" + assert validate_naf_code(value) is None + + +@pytest.mark.parametrize( + "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.""" + with pytest.raises(ValidationError): + validate_naf_code(value) + + +@pytest.mark.parametrize("value", ["12345", "98765", "00000", None]) +def test_validate_zip_code_valid(value): + """Tests validation of valid zip codes does not raise an exception.""" + assert validate_zip_code(value) is None + + +@pytest.mark.parametrize("value", ["1234", "123456", "12a45", "abcde", "", " ", 12345]) +def test_validate_zip_code_invalid(value): + """Tests validation of invalid zip codes raise a ValidationError.""" + with pytest.raises(ValidationError): + validate_zip_code(value) diff --git a/src/dashboard/apps/core/validators.py b/src/dashboard/apps/core/validators.py new file mode 100644 index 00000000..8cde34c9 --- /dev/null +++ b/src/dashboard/apps/core/validators.py @@ -0,0 +1,66 @@ +"""Dashboard core app validators.""" + +import re + +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + + +def validate_siret(value: str | None) -> None: + """Validate a SIRET number. + + SIRET must be a string that contains only numbers and have a fixed length of 14 + characters. + """ + 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) + + if not re.match(r"^\d{14}$", value): + raise ValidationError(error_message) + + +def validate_naf_code(value: str | None) -> None: + """Validate a NAF code. + + 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(error_message) + + +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. + """ + error_message = _( + "Zip code must be composed of number and a fixed length of 5 characters." + ) + + if value is None: + return + + if not isinstance(value, str): + raise ValidationError(error_message) + + if not re.match(r"^[0-9]{5}$", value): + raise ValidationError(error_message) diff --git a/src/dashboard/dashboard/settings.py b/src/dashboard/dashboard/settings.py index 5f2f21da..25bc738b 100644 --- a/src/dashboard/dashboard/settings.py +++ b/src/dashboard/dashboard/settings.py @@ -194,6 +194,11 @@ # during the year 2024). CONSENT_NUMBER_DAYS_END_DATE = None +# `Control authority` contact for consent validation. +CONSENT_CONTROL_AUTHORITY = env.json("CONTROL_AUTHORITY") + +CONSENT_DONE_AT = env.str("CONSENT_DONE_AT") + ## Debug-toolbar