Repository: thegreenwebfoundation/admin-portal
Pull request #655: Add 'basis for verification' question to provider request form
Author: login: timcowlishaw
Created: 2025-05-12T03:46:06Z | Merged: 2025-05-20T15:01:19Z

This PR adds a new, compulsory "what basis are you claiming for green web verification?" question to the first page of the provider request wizard, as a form of triage - in order to reduce the number of unsuccessful requests that @fershad has to deal with (see [This trello ticket](https://trello.com/c/WZgVt1At/438-add-a-what-is-your-basis-for-verification-question-to-the-verification-flow)).

The possible responses here are stored in a new Taggit model in the database, so are editable through the admin portal, and the given responses on a given providerrequest are also visible in the admin. The question shows on the first page of the wizard, and the responses are shown in the final confirmation / preview step.

Question in wizard:
![image](https://github.com/user-attachments/assets/ec10d202-553c-427c-ac92-70aa6a2bee84)
Show in preview / confirmation page at end of wizard:
![image](https://github.com/user-attachments/assets/a019200d-df63-41e1-affa-af5c04013047)
Accessible from admin:
![image](https://github.com/user-attachments/assets/245b67f8-5b3e-42cb-8473-e2fb97fe603a)



Diff:
diff --git a/.gitignore b/.gitignore
index 175d7c8b..de27ee7a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -142,6 +142,7 @@ deploy.retry
 data
 
 .env.staging
+.env.local
 *.prod
 *.prod.json
 .DS_Store
diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py
index e656acf0..5fcd663b 100644
--- a/apps/accounts/admin.py
+++ b/apps/accounts/admin.py
@@ -72,6 +72,7 @@
     Service,
     SupportMessage,
     User,
+    VerificationBasis,
 )
 from .permissions import manage_datacenter, manage_provider
 from .utils import get_admin_name, reverse_admin_name, send_email
@@ -266,6 +267,13 @@ class ServiceAdmin(admin.ModelAdmin):
     class Meta:
         verbose_name = "Services Offered"
 
+@admin.register(VerificationBasis, site=greenweb_admin)
+class VerificationBasisAdmin(admin.ModelAdmin):
+    model = VerificationBasis
+
+    class Meta:
+        verbose_name = "Bases for Verification"
+
 
 @admin.register(Label, site=greenweb_admin)
 class LabelAdmin(admin.ModelAdmin):
@@ -805,6 +813,7 @@ def get_queryset(self, request, *args, **kwargs):
             "datacenter",
             "greencheckip_set",
             "services",
+            "verification_bases",
         ).annotate(models.Count("greencheckip"))
 
         if not request.user.is_admin:
@@ -826,6 +835,7 @@ def get_fieldsets(self, request, obj=None):
                         "country",
                         "city",
                         "services",
+                        "verification_bases",
                         "created_by",
                     )
                 },
@@ -1410,7 +1420,6 @@ class ProviderRequest(ActionInChangeFormMixin, admin.ModelAdmin):
         ProviderRequestASNInline,
     ]
     search_fields = ("name", "website")
-    formfield_overrides = {TaggableManager: {"widget": LabelWidget(model=Service)}}
     empty_value_display = "(empty)"
     list_filter = ("status",)
     readonly_fields = (
@@ -1428,6 +1437,13 @@ class ProviderRequest(ActionInChangeFormMixin, admin.ModelAdmin):
     actions = ["mark_approved", "mark_open", "mark_rejected", "mark_removed"]
     change_form_template = "admin/provider_request/change_form.html"
 
+    def formfield_for_dbfield(self, db_field, **kwargs):
+        if db_field.name == 'services':
+            kwargs['widget'] = LabelWidget(model=Service)
+        elif db_field.name == "verification_bases":
+            kwargs['widget'] = LabelWidget(model=VerificationBasis)
+        return super(ProviderRequest, self).formfield_for_dbfield(db_field,**kwargs)
+
     def send_approval_email(
         self,
         provider_request: ProviderRequest,
diff --git a/apps/accounts/factories.py b/apps/accounts/factories.py
index a2d7cf8a..3040338d 100644
--- a/apps/accounts/factories.py
+++ b/apps/accounts/factories.py
@@ -12,6 +12,15 @@
 factory.random.reseed_random("venture not into the land of flaky tests")
 
 
+class VerificationBasisFactory(dj_factory.DjangoModelFactory):
+    name = factory.Faker("word")
+
+    class Meta:
+        model = ac_models.VerificationBasis
+        # avoid creating duplicate entries
+        django_get_or_create = ("name",)
+
+
 class SupportingEvidenceFactory(dj_factory.DjangoModelFactory):
     """
     A piece of supporting evidence, at a remote url, rather than uploaded
@@ -62,6 +71,20 @@ def services(self, create, extracted, **kwargs):
         for tag in extracted:
             self.services.add(tag)
 
+    @factory.post_generation
+    def verification_bases(self, create, extracted, **kwargs):
+        """
+        This handles many-to-many relationship between ProviderRequest and VerificationBasis.
+
+        More details: https://factoryboy.readthedocs.io/en/latest/recipes.html#simple-many-to-many-relationship
+        """  # noqa
+        # nothing passed as an argument
+        if not create or not extracted:
+            return
+        # set tags
+        for tag in extracted:
+            self.verification_bases.add(tag)
+
     class Meta:
         model = ac_models.ProviderRequest
 
diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py
index 3ad34196..a17c1196 100644
--- a/apps/accounts/forms.py
+++ b/apps/accounts/forms.py
@@ -17,7 +17,7 @@
 from file_resubmit.widgets import ResubmitFileWidget
 from taggit_labels.widgets import LabelWidget
 
-from apps.accounts.models.provider_request import ProviderRequest, ProviderRequestStatus
+from apps.accounts.models.provider_request import ProviderRequest, ProviderRequestStatus, VerificationBasis
 
 from . import models as ac_models
 from django.utils.safestring import mark_safe
@@ -29,7 +29,7 @@ class AlwaysChangedModelFormMixin:
     """
     A mixin for ModelForms that makes sure that a form with this Mixin
     is always marked as changed in checks like `has_changed()`.
-    Used in form wizards containing formsets, to return true for all forms 
+    Used in form wizards containing formsets, to return true for all forms
     in a formset, so that all data is saved at the end of the wizard.
     """
 
@@ -111,6 +111,7 @@ class Meta:
         fields = "__all__"
         widgets = {
             "services": LabelWidget(model=ac_models.Service),
+            "verification_bases": LabelWidget(model=ac_models.VerificationBasis),
             "staff_labels": dal_widgets.TaggitSelect2("label-autocomplete"),
         }
 
@@ -209,7 +210,6 @@ class Meta:
         model = ac_models.HostingProviderSupportingDocument
         fields = "__all__"
 
-
 class OrgDetailsForm(forms.ModelForm):
     """
     Part of multi-step registration form (screen 1)
@@ -297,6 +297,41 @@ class Meta:
         fields = ["services"]
 
 
+class BasisForVerificationForm(forms.ModelForm):
+    """
+    Part of multi-step registration form (screen 3)
+    """
+
+    verification_bases = forms.MultipleChoiceField(
+        choices=ProviderRequest.get_verification_bases_choices,
+        widget=forms.CheckboxSelectMultiple,
+        label="On what basis are you seeking verification?",
+        help_text=mark_safe(
+          "The Green Web Dataset lists providers taking actions to <b>avoid</b>, <b>reduce</b>, or <b>offset</b> "
+          "the greenhouse gas emissions caused by using electricity to provide their services. "
+          "<p>On the next page, we will ask you to provide evidence that allows us to verify the steps you are taking. "
+          "So before continuing, we strongly recommend you understand the kinds of evidence required based on your organisations circumstances. "
+          "We've written about this on the <i><b><a target=\"_blank\" href=\"https://www.thegreenwebfoundation.org/what-we-accept-as-evidence-of-green-power/\">What we accept as evidence of green power?</a></i></b> page on our website.</p>"
+          "<p>When you are ready to continue, please select one or more of the statements below that apply to your organisation.</p>"
+        ),
+    )
+
+    def __init__(self, *args, **kwargs):
+        """
+        Implement injecting initial values for bases_for_verification field (for editing existing objects).
+        By default the initial value is passed as a queryset,
+        but TaggableManager does not handle that well - we pass a list instead.
+        """
+        super().__init__(*args, **kwargs)
+        instance = kwargs.get("instance")
+        if instance:
+            self.initial = {"verification_bases": [b for b in instance.verification_bases.slugs()]}
+
+    class Meta:
+        model = ac_models.ProviderRequest
+        fields = ["verification_bases"]
+
+
 class CredentialForm(AlwaysChangedModelFormMixin, forms.ModelForm):
     class Meta:
         model = ac_models.ProviderRequestEvidence
@@ -570,7 +605,7 @@ def clean(self):
 
         return self.cleaned_data
 
-    
+
 class ConsentForm(forms.ModelForm):
     """
     Part of multi-step registration form (screen 5).
diff --git a/apps/accounts/group_permissions.py b/apps/accounts/group_permissions.py
index 7676e907..b3ea3b9f 100644
--- a/apps/accounts/group_permissions.py
+++ b/apps/accounts/group_permissions.py
@@ -127,6 +127,11 @@
     "change_tag",
     "delete_tag",
     "view_tag",
+    ## Can add and update the bases for verification listed in the onboarding wizard
+    "add_verificationbasis",
+    "change_verificationbasis",
+    "view_verificationbasis",
+    "delete_verificationbasis",
 ]
 
 
@@ -270,3 +275,20 @@ def group_permissions_2023_04_26_revert_disallow_adding_hp_and_dc(apps, schema_e
 
     hp.permissions.add(*pr_perms)
     dc.permissions.add(*pr_perms)
+
+def group_permissions_2025_05_12_add_verification_basis_permissions_for_admins(apps, schema_editor):
+    """
+    Gives admins permission to modify bases for verification.
+    """
+    pr_perms_codenames = [
+        "add_verificationbasis",
+        "change_verificationbasis",
+        "view_verificationbasis",
+        "delete_verificationbasis"
+    ]
+
+    pr_perms = Permission.objects.filter(codename__in=pr_perms_codenames)
+
+    admin, _ = Group.objects.get_or_create(name="admin")
+
+    admin.permissions.add(*pr_perms)
diff --git a/apps/accounts/migrations/0067_verificationbasis_and_more.py b/apps/accounts/migrations/0067_verificationbasis_and_more.py
new file mode 100644
index 00000000..66cd3f1b
--- /dev/null
+++ b/apps/accounts/migrations/0067_verificationbasis_and_more.py
@@ -0,0 +1,103 @@
+# Generated by Django 5.0.9 on 2025-05-09 15:55
+
+import django.db.models.deletion
+import taggit.managers
+from django.db import migrations, models
+from django.utils.text import slugify
+
+
+class Migration(migrations.Migration):
+
+    def populate_verification_bases(apps, schema_editor):
+        VerificationBasis = apps.get_model("accounts", "VerificationBasis")
+        bases = [
+                "We use 100% green energy from our own infrastructure.",
+                "We operate in a region that has a grid intensity of less than 20g CO2e/kWh or uses over 99% renewable power.",
+                "We directly pay for green energy to cover the non-green energy we use.",
+                "We purchase quality carbon offsets to cover the non-green energy we use.",
+                "We resell or actively use a provider that is already in the Green Web Dataset.",
+        ]
+        for base in bases:
+            VerificationBasis.objects.create(name=base, slug=slugify(base))
+
+    dependencies = [
+        ("accounts", "0066_hostingprovider_carbon_txt_url"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="VerificationBasis",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "name",
+                    models.CharField(max_length=255, unique=True, verbose_name="name"),
+                ),
+                (
+                    "slug",
+                    models.SlugField(
+                        allow_unicode=True,
+                        max_length=255,
+                        unique=True,
+                        verbose_name="slug",
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "Basis for verification",
+                "verbose_name_plural": "Bases for verification",
+            },
+        ),
+        migrations.RunPython(populate_verification_bases),
+        migrations.CreateModel(
+            name="ProviderRequestVerificationBasis",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "content_object",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to="accounts.providerrequest",
+                    ),
+                ),
+                (
+                    "tag",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="%(app_label)s_%(class)s_items",
+                        to="accounts.verificationbasis",
+                    ),
+                ),
+            ],
+            options={
+                "abstract": False,
+            },
+        ),
+        migrations.AddField(
+            model_name="providerrequest",
+            name="verification_bases",
+            field=taggit.managers.TaggableManager(
+                blank=True,
+                help_text="A comma-separated list of tags.",
+                through="accounts.ProviderRequestVerificationBasis",
+                to="accounts.VerificationBasis",
+                verbose_name="Basis for verification",
+            ),
+        ),
+    ]
diff --git a/apps/accounts/migrations/0068_verification_basis_permissions.py b/apps/accounts/migrations/0068_verification_basis_permissions.py
new file mode 100644
index 00000000..2d162294
--- /dev/null
+++ b/apps/accounts/migrations/0068_verification_basis_permissions.py
@@ -0,0 +1,16 @@
+# Generated by Django 5.0.9 on 2025-05-12 04:13
+
+from django.db import migrations
+from ..group_permissions import group_permissions_2025_05_12_add_verification_basis_permissions_for_admins
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("accounts", "0067_verificationbasis_and_more"),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            group_permissions_2025_05_12_add_verification_basis_permissions_for_admins
+        )
+    ]
diff --git a/apps/accounts/migrations/0069_verificationbasis_required_evidence_link.py b/apps/accounts/migrations/0069_verificationbasis_required_evidence_link.py
new file mode 100644
index 00000000..cd3c0180
--- /dev/null
+++ b/apps/accounts/migrations/0069_verificationbasis_required_evidence_link.py
@@ -0,0 +1,24 @@
+# Generated by Django 5.0.9 on 2025-05-12 12:13
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("accounts", "0068_verification_basis_permissions"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="verificationbasis",
+            name="required_evidence_link",
+            field=models.URLField(
+                blank=True,
+                max_length=255,
+                null=True,
+                verbose_name="Required evidence link",
+            ),
+        ),
+    ]
diff --git a/apps/accounts/migrations/0070_create_providerverificationbasis.py b/apps/accounts/migrations/0070_create_providerverificationbasis.py
new file mode 100644
index 00000000..d4d127e3
--- /dev/null
+++ b/apps/accounts/migrations/0070_create_providerverificationbasis.py
@@ -0,0 +1,46 @@
+# Generated by Django 5.0.9 on 2025-05-12 15:06
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("accounts", "0069_verificationbasis_required_evidence_link"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="ProviderVerificationBasis",
+            fields=[
+                (
+                    "id",
+                    models.AutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "content_object",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to="accounts.hostingprovider",
+                    ),
+                ),
+                (
+                    "tag",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="%(app_label)s_%(class)s_items",
+                        to="accounts.verificationbasis",
+                    ),
+                ),
+            ],
+            options={
+                "abstract": False,
+            },
+        ),
+    ]
diff --git a/apps/accounts/models/hosting/__init__.py b/apps/accounts/models/hosting/__init__.py
index 054445b4..0fb205a1 100644
--- a/apps/accounts/models/hosting/__init__.py
+++ b/apps/accounts/models/hosting/__init__.py
@@ -21,6 +21,7 @@
     Service,
     PartnerChoice,
     ProviderSharedSecret,
+    VerificationBasis,
     DOMAIN_HASH_ISSUER_ID,
     GREEN_VIA_CARBON_TXT,
 )
diff --git a/apps/accounts/models/hosting/provider.py b/apps/accounts/models/hosting/provider.py
index 619820f2..d840f603 100644
--- a/apps/accounts/models/hosting/provider.py
+++ b/apps/accounts/models/hosting/provider.py
@@ -12,6 +12,8 @@
 from django.template.loader import render_to_string
 from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
+from django.utils.translation import pgettext_lazy
+from django.utils.safestring import mark_safe
 from django_countries.fields import CountryField
 from django_mysql.models import EnumField
 from guardian.shortcuts import get_users_with_perms
@@ -73,6 +75,54 @@ class Meta:
         verbose_name_plural = _("Services")
 
 
+class VerificationBasis(tag_models.TagBase):
+    """
+    A model representing reasons why a provider would be verified.
+    This includes things like
+
+        "We use 100% green energy from our own infrastructure.",
+        "We operate in a region that has a grid intensity of less than 20g CO2e/kWh or uses over 99% renewable power.",
+        "We directly pay for green energy to cover the non-green energy we use.",
+        "We purchase quality carbon offsets to cover the non-green energy we use.",
+        "We resell or actively use a provider that is already in the Green Web Dataset.",
+
+    A subclass of Taggit's 'TagBase' model.
+
+    """
+
+
+    # Annoyingly, the only way to override the max_length in taggit appears to be to copy and adjust
+    # these two field definitions wholesale: https://github.com/jazzband/django-taggit/issues/510
+    name = models.CharField(
+        verbose_name=pgettext_lazy("A tag name", "name"), unique=True, max_length=255
+    )
+
+
+    slug = models.SlugField(
+        verbose_name=pgettext_lazy("A tag slug", "slug"),
+        unique=True,
+        max_length=255,
+        allow_unicode=True,
+    )
+
+    required_evidence_link = models.URLField(
+        max_length=255, null=True, blank=True,
+        verbose_name="Required evidence link"
+    )
+
+    class Meta:
+        verbose_name = _("Basis for verification")
+        verbose_name_plural = _("Bases for verification")
+
+    @property
+    def label(self):
+        label = self.name
+        if self.required_evidence_link is not None and self.required_evidence_link.strip() != "":
+            label += f" (<a href='{self.required_evidence_link}' target='_blank'>see required evidence</a>)"
+        return mark_safe(label)
+
+
+
 class ProviderService(tag_models.TaggedItemBase):
     """
     The corresponding through model for linking a Provider to
@@ -89,6 +139,21 @@ class ProviderService(tag_models.TaggedItemBase):
         on_delete=models.CASCADE,
     )
 
+class ProviderVerificationBasis(tag_models.TaggedItemBase):
+    """
+    The corresponding through model for linking a Provider to
+    a VerificationBasis as outlined above.
+    """
+
+    content_object = models.ForeignKey(
+        "Hostingprovider",
+        on_delete=models.CASCADE,
+    )
+    tag = models.ForeignKey(
+        VerificationBasis,
+        related_name="%(app_label)s_%(class)s_items",
+        on_delete=models.CASCADE,
+    )
 
 class Hostingprovider(models.Model):
     archived = models.BooleanField(default=False)
@@ -131,7 +196,15 @@ class Hostingprovider(models.Model):
         through=ProviderService,
         # related_name="services",
     )
+
+    verification_bases = TaggableManager(
+        verbose_name="Basis for verification",
+        blank=True,
+        through=ProviderVerificationBasis,
+    )
+
     # this should not be exposed publicly to end users.
+
     # It's for internal use
     staff_labels = TaggableManager(
         verbose_name="Staff labels",
diff --git a/apps/accounts/models/provider_request.py b/apps/accounts/models/provider_request.py
index ba8295f9..3d902524 100644
--- a/apps/accounts/models/provider_request.py
+++ b/apps/accounts/models/provider_request.py
@@ -6,6 +6,7 @@
 from django.core.exceptions import ValidationError
 from django.db import IntegrityError, models, transaction
 from django.urls import reverse
+from django.utils.translation import gettext_lazy as _
 from django_countries.fields import CountryField
 from guardian.shortcuts import assign_perm
 from model_utils.models import TimeStampedModel
@@ -21,6 +22,7 @@
     Hostingprovider,
     HostingProviderSupportingDocument,
     Service,
+    VerificationBasis,
 )
 
 import ipaddress
@@ -66,6 +68,23 @@ class ProviderRequestService(tag_models.TaggedItemBase):
     )
 
 
+class ProviderRequestVerificationBasis(tag_models.TaggedItemBase):
+    """
+    The corresponding through model for linking a Provider to
+    a VerificationBasis as outlined above.
+    """
+
+    content_object = models.ForeignKey(
+        "ProviderRequest",
+        on_delete=models.CASCADE,
+    )
+    tag = models.ForeignKey(
+        VerificationBasis,
+        related_name="%(app_label)s_%(class)s_items",
+        on_delete=models.CASCADE,
+    )
+
+
 class ProviderRequest(TimeStampedModel):
     """
     Model representing the input data
@@ -96,6 +115,11 @@ class ProviderRequest(TimeStampedModel):
         blank=True,
         through=ProviderRequestService,
     )
+    verification_bases = TaggableManager(
+        verbose_name="Basis for verification",
+        blank=True,
+        through=ProviderRequestVerificationBasis,
+    )
     missing_network_explanation = models.TextField(
         verbose_name="Reason for no IP / AS data",
         help_text=(
@@ -149,6 +173,14 @@ def set_services_from_slugs(self, service_slugs: Iterable[str]) -> None:
         services = Service.objects.filter(slug__in=service_slugs)
         self.services.set(services)
 
+    def set_verification_bases_from_slugs(self, verification_basis_slugs: Iterable[str]) -> None:
+        """
+        Given list of verification_basis slugs (corresponding to Tag slugs)
+        apply matching verification_bases to the ProviderRequest object
+        """
+        verification_bases = VerificationBasis.objects.filter(slug__in=verification_basis_slugs).distinct()
+        self.verification_bases.set(verification_bases)
+
     @classmethod
     def get_service_choices(cls) -> List[Tuple[int, str]]:
         """
@@ -157,6 +189,14 @@ def get_service_choices(cls) -> List[Tuple[int, str]]:
         """
         return [(tag.slug, tag.name) for tag in Service.objects.all()]
 
+    @classmethod
+    def get_verification_bases_choices(cls) -> List[Tuple[int, str]]:
+        """
+        Returns a list of available verification bases (implemented in the Tag model)
+        in a format expected by ChoiceField
+        """
+        return [(tag.slug, tag.label) for tag in VerificationBasis.objects.all()]
+
     @transaction.atomic
     def approve(self) -> Hostingprovider:
         """
@@ -248,6 +288,8 @@ def approve(self) -> Hostingprovider:
         else:
             hp.showonwebsite = True
 
+        hp.verification_bases.set(self.verification_bases.all())
+
         hp.save()
 
         # set permissions
diff --git a/apps/accounts/templates/provider_portal/request_detail.html b/apps/accounts/templates/provider_portal/request_detail.html
index 0b0bb45c..e972e3c4 100644
--- a/apps/accounts/templates/provider_portal/request_detail.html
+++ b/apps/accounts/templates/provider_portal/request_detail.html
@@ -34,7 +34,7 @@ <h1 class="text-6xl mb-0"><span class="uppercase">Summary of request</h1>
           If you want to update the information relating to this provider, please get in touch using <a target="_blank" rel="noopener noreferrer" href="https://www.thegreenwebfoundation.org/support-form/">our support form</a>.
         {% endif %}
 
-          
+
       </header>
 
       <article class="pb-12">
@@ -71,6 +71,14 @@ <h3 class="text-2xl">About this provider</h3>
               {% endfor %}
             </td>
           </tr>
+          <tr>
+            <td>Basis for verification</td>
+            <td>
+              {% for basis in object.verification_bases.all %}
+                {{ basis.name }}<br>
+              {% endfor %}
+            </td>
+          </tr>
         </table>
 
         <h3 class="text-2xl">Supporting evidence</h3>
diff --git a/apps/accounts/templates/provider_registration/basis_for_verification.html b/apps/accounts/templates/provider_registration/basis_for_verification.html
new file mode 100644
index 00000000..d7cd3214
--- /dev/null
+++ b/apps/accounts/templates/provider_registration/basis_for_verification.html
@@ -0,0 +1,6 @@
+{% extends "./base_form.html" %}
+{% block header_section %}
+<section class="prose mt-10">
+	<h1 class="mb-0">Basis for verification</h1>
+</section>
+{% endblock %}
diff --git a/apps/accounts/templates/provider_registration/partials/_preview.html b/apps/accounts/templates/provider_registration/partials/_preview.html
index 133a86f2..a14f88e8 100644
--- a/apps/accounts/templates/provider_registration/partials/_preview.html
+++ b/apps/accounts/templates/provider_registration/partials/_preview.html
@@ -11,6 +11,11 @@
 				using custom template tag
 			{% endcomment %}
 			<p class="m-0">{{ field.value|render_as_services|conditional_yesno:"Yes,No,-" }}</p>
+		{% elif field.label|lower == "on what basis are you seeking verification?" %}
+			{% comment %}
+				Expand verification slugs to their full names using custom template tag
+			{% endcomment %}
+			<p class="m-0">{{ field.value|render_as_verification_bases|conditional_yesno:"Yes,No,-" }}</p>
 		{% elif field.label|lower == "country" %}
 			{% comment %}
 				Expand country code to country name (e.g. "DE" -> "Germany")
@@ -28,4 +33,4 @@
 		{% endif %}
 	</div>
 	{% endfor %}
-</div>
\ No newline at end of file
+</div>
diff --git a/apps/accounts/templates/provider_registration/preview.html b/apps/accounts/templates/provider_registration/preview.html
index 23b40e15..68dcf02c 100644
--- a/apps/accounts/templates/provider_registration/preview.html
+++ b/apps/accounts/templates/provider_registration/preview.html
@@ -26,13 +26,13 @@ <h1>Final check of your data</h1>
 
           <div class="alert__warning">
             <p>Please check your data is correct before you submit your request.</p>
-            <p>On completion of the form, your submission will be reviewed by our staff. 
-              If we have all the evidence we need, we’ll notify you to let you know that checks 
-              against your site or services show as green. If we can't find the evidence we need, 
+            <p>On completion of the form, your submission will be reviewed by our staff.
+              If we have all the evidence we need, we’ll notify you to let you know that checks
+              against your site or services show as green. If we can't find the evidence we need,
               we’ll reply letting you know what else is required.</p>
           </div>
         </section>
-        
+
         <section>
           <h3>About your organisation</h3>
           {% include "provider_registration/partials/_preview.html" with form=preview_forms.0 %}
@@ -53,9 +53,15 @@ <h3>Services provided by your organisation</h3>
           {% include "provider_registration/partials/_preview.html" with form=preview_forms.2 %}
         </section>
 
+        <section class="my-10">
+          <h3>Basis for verification</h3>
+          {% include "provider_registration/partials/_preview.html" with form=preview_forms.3 %}
+        </section>
+
+
         <section class="my-10">
           <h3>Green evidence</h3>
-          {% for evidence_form in preview_forms.3.forms %}
+          {% for evidence_form in preview_forms.4.forms %}
             {% if not evidence_form.DELETE.value %}
               {% include "provider_registration/partials/_preview.html" with form=evidence_form %}
             {% endif %}
@@ -65,7 +71,7 @@ <h3>Green evidence</h3>
         <section class="my-10">
           <h3>Network footprint</h3>
 
-          {% for ip_form in preview_forms.4.ips %}
+          {% for ip_form in preview_forms.5.ips %}
             {% if not ip_form.DELETE.value %}
               <div class="bg-neutral-50 rounded-xl my-6 p-4">
                 <div class="mb-4 last-of-type:mb-0">
@@ -76,20 +82,20 @@ <h3>Network footprint</h3>
             {% endif %}
           {% endfor %}
 
-          {% for asn_form in preview_forms.4.asns %}
+          {% for asn_form in preview_forms.5.asns %}
             {% if not asn_form.DELETE.value %}
               {% include "provider_registration/partials/_preview.html" with form=asn_form %}
             {% endif %}
           {% endfor %}
 
-          {% include "provider_registration/partials/_preview.html" with form=preview_forms.4.extra %}
+          {% include "provider_registration/partials/_preview.html" with form=preview_forms.5.extra %}
 
 
         </section>
 
         <section class="my-10">
           <h3>Consent</h3>
-          {% include "provider_registration/partials/_preview.html" with form=preview_forms.5 %}
+          {% include "provider_registration/partials/_preview.html" with form=preview_forms.6 %}
         </section>
       </div>
 
diff --git a/apps/accounts/templatetags/preview_extras.py b/apps/accounts/templatetags/preview_extras.py
index f1ec080b..b13ae989 100644
--- a/apps/accounts/templatetags/preview_extras.py
+++ b/apps/accounts/templatetags/preview_extras.py
@@ -1,6 +1,7 @@
 from django.template.defaultfilters import yesno
 from django import template
-from apps.accounts.models import Service
+from django.utils.safestring import mark_safe
+from apps.accounts.models import Service, VerificationBasis
 
 
 register = template.Library()
@@ -30,6 +31,17 @@ def render_as_services(value):
         return ", ".join([tag.name for tag in tags])
     return None
 
+@register.filter
+def render_as_verification_bases(value):
+    """
+    Attempts to map slugs in to verification basis names
+    based on a database query.
+    """
+    tags = VerificationBasis.objects.filter(slug__in=value).distinct()
+    if tags:
+        list_items =  "\n".join([f"<li>{tag.name}</li>" for tag in tags])
+        return mark_safe(f"<ul>{list_items}</ul>")
+    return None
 
 @register.filter
 def exclude_preview_fields(form):
diff --git a/apps/accounts/tests/test_provider_request.py b/apps/accounts/tests/test_provider_request.py
index d88aaa06..b058449d 100644
--- a/apps/accounts/tests/test_provider_request.py
+++ b/apps/accounts/tests/test_provider_request.py
@@ -30,6 +30,7 @@
     ProviderRequestIPRangeFactory,
     ProviderRequestLocationFactory,
     SupportingEvidenceFactory,
+    VerificationBasisFactory,
 )
 from apps.greencheck.factories import (
     GreenASNFactory,
@@ -99,6 +100,22 @@ def wizard_form_services_data():
     }
 
 
+@pytest.fixture()
+def wizard_form_verification_bases_data():
+    """
+    Returns valid data for step BASIS FOR VERIFICATION of the wizard as expected by the POST request.
+    """
+    for _ in range(5):
+        VerificationBasisFactory.create()
+
+    tags_choices = models.VerificationBasis.objects.all()
+    bases_sample = random.sample([tag.slug for tag in tags_choices], 3)
+
+    return {
+        "provider_request_wizard_view-current_step": "3",
+        "3-verification_bases": bases_sample,
+    }
+
 @pytest.fixture()
 def wizard_form_evidence_data(fake_evidence):
     """
@@ -106,19 +123,19 @@ def wizard_form_evidence_data(fake_evidence):
     as expected by the POST request.
     """
     return {
-        "provider_request_wizard_view-current_step": "3",
-        "3-TOTAL_FORMS": 2,
-        "3-INITIAL_FORMS": 0,
-        "3-0-title": " ".join(faker.words(3)),
-        "3-0-link": faker.url(),
-        "3-0-file": "",
-        "3-0-type": models.EvidenceType.WEB_PAGE.value,
-        "3-0-public": "on",
-        "3-1-title": " ".join(faker.words(3)),
-        "3-1-link": "",
-        "3-1-file": fake_evidence,
-        "3-1-type": models.EvidenceType.ANNUAL_REPORT.value,
-        "3-1-public": "on",
+        "provider_request_wizard_view-current_step": "4",
+        "4-TOTAL_FORMS": 2,
+        "4-INITIAL_FORMS": 0,
+        "4-0-title": " ".join(faker.words(3)),
+        "4-0-link": faker.url(),
+        "4-0-file": "",
+        "4-0-type": models.EvidenceType.WEB_PAGE.value,
+        "4-0-public": "on",
+        "4-1-title": " ".join(faker.words(3)),
+        "4-1-link": "",
+        "4-1-file": fake_evidence,
+        "4-1-type": models.EvidenceType.ANNUAL_REPORT.value,
+        "4-1-public": "on",
     }
 
 
@@ -137,16 +154,16 @@ def wizard_form_network_data(sorted_ips):
     as expected by the POST request.
     """
     return {
-        "provider_request_wizard_view-current_step": "4",
-        "ips__4-TOTAL_FORMS": "2",
-        "ips__4-INITIAL_FORMS": "0",
-        "ips__4-0-start": sorted_ips[0],
-        "ips__4-0-end": sorted_ips[1],
-        "ips__4-1-start": sorted_ips[2],
-        "ips__4-1-end": sorted_ips[3],
-        "asns__4-TOTAL_FORMS": "1",
-        "asns__4-INITIAL_FORMS": "0",
-        "asns__4-0-asn": faker.random_int(min=100, max=999),
+        "provider_request_wizard_view-current_step": "5",
+        "ips__5-TOTAL_FORMS": "2",
+        "ips__5-INITIAL_FORMS": "0",
+        "ips__5-0-start": sorted_ips[0],
+        "ips__5-0-end": sorted_ips[1],
+        "ips__5-1-start": sorted_ips[2],
+        "ips__5-1-end": sorted_ips[3],
+        "asns__5-TOTAL_FORMS": "1",
+        "asns__5-INITIAL_FORMS": "0",
+        "asns__5-0-asn": faker.random_int(min=100, max=999),
     }
 
 
@@ -157,12 +174,12 @@ def wizard_form_network_explanation_only():
     form wizard, without any IP or AS information.
     """
     return {
-        "provider_request_wizard_view-current_step": "4",
-        "ips__4-TOTAL_FORMS": "0",
-        "ips__4-INITIAL_FORMS": "0",
-        "asns__4-TOTAL_FORMS": "0",
-        "asns__4-INITIAL_FORMS": "0",
-        "extra__4-missing_network_explanation": faker.sentence(10),
+        "provider_request_wizard_view-current_step": "5",
+        "ips__5-TOTAL_FORMS": "0",
+        "ips__5-INITIAL_FORMS": "0",
+        "asns__5-TOTAL_FORMS": "0",
+        "asns__5-INITIAL_FORMS": "0",
+        "extra__5-missing_network_explanation": faker.sentence(10),
     }
 
 
@@ -173,9 +190,9 @@ def wizard_form_consent():
     as expected by the POST request.
     """
     return {
-        "provider_request_wizard_view-current_step": "5",
-        "5-data_processing_opt_in": "on",
-        "5-newsletter_opt_in": "off",
+        "provider_request_wizard_view-current_step": "6",
+        "6-data_processing_opt_in": "on",
+        "6-newsletter_opt_in": "off",
     }
 
 
@@ -186,7 +203,7 @@ def wizard_form_preview():
     as expected by the POST request.
     """
     return {
-        "provider_request_wizard_view-current_step": "6",
+        "provider_request_wizard_view-current_step": "7",
     }
 
 
@@ -220,7 +237,7 @@ def fake_evidence():
 def test_evidence_validation_fails(evidence_data):
 
     evidence_data["request"] = ProviderRequestFactory.create()
-    
+
     evidence = models.ProviderRequestEvidence.objects.create(**evidence_data)
 
     with pytest.raises(ValidationError):
@@ -295,6 +312,7 @@ def test_wizard_view_happy_path(
     wizard_form_org_details_data,
     wizard_form_org_location_data,
     wizard_form_services_data,
+    wizard_form_verification_bases_data,
     wizard_form_evidence_data,
     wizard_form_network_data,
     wizard_form_consent,
@@ -305,6 +323,7 @@ def test_wizard_view_happy_path(
         wizard_form_org_details_data,
         wizard_form_org_location_data,
         wizard_form_services_data,
+        wizard_form_verification_bases_data,
         wizard_form_evidence_data,
         wizard_form_network_data,
         wizard_form_consent,
@@ -360,6 +379,7 @@ def test_wizard_sends_email_on_submission(
     wizard_form_org_details_data,
     wizard_form_org_location_data,
     wizard_form_services_data,
+    wizard_form_verification_bases_data,
     wizard_form_evidence_data,
     wizard_form_network_data,
     wizard_form_consent,
@@ -377,6 +397,7 @@ def test_wizard_sends_email_on_submission(
         wizard_form_org_details_data,
         wizard_form_org_location_data,
         wizard_form_services_data,
+        wizard_form_verification_bases_data,
         wizard_form_evidence_data,
         wizard_form_network_data,
         wizard_form_consent,
@@ -519,7 +540,7 @@ def test_approve_changes_status_to_approved():
 @pytest.mark.django_db
 def test_approve_creates_hosting_provider():
     # given: a provider request with services is created
-    pr = ProviderRequestFactory.create(services=faker.words(nb=4))
+    pr = ProviderRequestFactory.create(services=faker.words(nb=4), verification_bases=faker.words(nb=4))
     ProviderRequestLocationFactory.create(request=pr)
     ProviderRequestEvidenceFactory.create(request=pr)
 
@@ -531,6 +552,7 @@ def test_approve_creates_hosting_provider():
     assert hp.name == pr.name
     assert hp.description == pr.description
     assert list(hp.services.all()) == list(pr.services.all())
+    assert list(hp.verification_bases.all()) == list(pr.verification_bases.all())
     assert hp.website == pr.website
     assert hp.request == pr
     assert hp.created_by == pr.created_by
@@ -935,6 +957,7 @@ def test_wizard_view_with_just_network_explanation(
     wizard_form_org_details_data,
     wizard_form_org_location_data,
     wizard_form_services_data,
+    wizard_form_verification_bases_data,
     wizard_form_evidence_data,
     wizard_form_network_explanation_only,
     wizard_form_consent,
@@ -946,6 +969,7 @@ def test_wizard_view_with_just_network_explanation(
         wizard_form_org_details_data,
         wizard_form_org_location_data,
         wizard_form_services_data,
+        wizard_form_verification_bases_data,
         wizard_form_evidence_data,
         wizard_form_network_explanation_only,
         wizard_form_consent,
@@ -963,7 +987,7 @@ def test_wizard_view_with_just_network_explanation(
 
     # then we have our explanation saved to the provider
     explanation = wizard_form_network_explanation_only.get(
-        "extra__4-missing_network_explanation"
+        "extra__5-missing_network_explanation"
     )
 
     assert pr.missing_network_explanation == explanation
@@ -977,6 +1001,7 @@ def test_wizard_records_if_location_import_needed(
     wizard_form_org_details_data,
     wizard_form_org_location_data,
     wizard_form_services_data,
+    wizard_form_verification_bases_data,
     wizard_form_evidence_data,
     wizard_form_network_data,
     wizard_form_consent,
@@ -993,6 +1018,7 @@ def test_wizard_records_if_location_import_needed(
         wizard_form_org_details_data,
         wizard_form_org_location_data,
         wizard_form_services_data,
+        wizard_form_verification_bases_data,
         wizard_form_evidence_data,
         wizard_form_network_data,
         wizard_form_consent,
@@ -1018,6 +1044,7 @@ def test_new_submission_doesnt_modify_available_services(
     wizard_form_org_details_data,
     wizard_form_org_location_data,
     wizard_form_services_data,
+    wizard_form_verification_bases_data,
     wizard_form_evidence_data,
     wizard_form_network_data,
     wizard_form_consent,
@@ -1031,6 +1058,7 @@ def test_new_submission_doesnt_modify_available_services(
         wizard_form_org_details_data,
         wizard_form_org_location_data,
         wizard_form_services_data,
+        wizard_form_verification_bases_data,
         wizard_form_evidence_data,
         wizard_form_network_data,
         wizard_form_consent,
@@ -1139,6 +1167,7 @@ def test_editing_pr_updates_original_submission(
     client,
     wizard_form_org_details_data,
     wizard_form_services_data,
+    wizard_form_verification_bases_data,
     wizard_form_evidence_data,
     wizard_form_network_data,
     wizard_form_consent,
@@ -1233,6 +1262,9 @@ def test_editing_pr_updates_original_submission(
     # when: submitting SERVICES form with overridden data
     response = client.post(edit_url, wizard_form_services_data, follow=True)
 
+    # then: wizard proceeds, VERIFICATION BASIS form is displayed
+    response = client.post(edit_url, wizard_form_verification_bases_data, follow=True)
+
     # then: wizards proceeds, EVIDENCE formset is displayed with bound queryset and initial data
     evidence_formset = response.context_data["form"]
     assert set(evidence_formset.queryset) == set([ev1, ev2])
@@ -1382,6 +1414,7 @@ def test_editing_hp_creates_new_verification_request(
     sorted_ips,
     wizard_form_org_details_data,
     wizard_form_services_data,
+    wizard_form_verification_bases_data,
     wizard_form_evidence_data,
     wizard_form_network_data,
     wizard_form_consent,
@@ -1459,6 +1492,9 @@ def test_editing_hp_creates_new_verification_request(
     # when: submitting SERVICES form with overridden data
     response = client.post(edit_url, wizard_form_services_data, follow=True)
 
+    # then: wizard proceeds, VERIFICATION BASIS form is displayed with initial data
+    response = client.post(edit_url, wizard_form_verification_bases_data, follow=True)
+
     # then: wizards proceeds, EVIDENCE formset is displayed with initial data
     evidence_formset = response.context_data["form"]
     # we strip expected initial data from "file" key for comparison purposes
@@ -1559,6 +1595,7 @@ def test_saving_changes_to_verification_request_from_hp_via_wizard(
     sorted_ips,
     wizard_form_org_details_data,
     wizard_form_services_data,
+    wizard_form_verification_bases_data,
     wizard_form_evidence_data,
     wizard_form_network_data,
     wizard_form_consent,
@@ -1643,6 +1680,13 @@ def test_saving_changes_to_verification_request_from_hp_via_wizard(
     # when: submitting SERVICES form with overridden data
     response = client.post(edit_url, wizard_form_services_data, follow=True)
 
+    # then: wizard proceeds, VERIFICATION BASES form is displayed with initial data
+    verification_bases_form = response.context_data["form"]
+    assert isinstance(verification_bases_form, account_forms.BasisForVerificationForm)
+
+    # when: submitting VERIFICATION BASES form with overridden data
+    response = client.post(edit_url, wizard_form_verification_bases_data, follow=True)
+
     # then: wizards proceeds,
     # when: EVIDENCE formset is displayed with initial data
     evidence_formset = response.context_data["form"]
@@ -1652,21 +1696,21 @@ def test_saving_changes_to_verification_request_from_hp_via_wizard(
     # when: submitting EVIDENCE step with one piece of overridden data
     # and one saved data
     updated_evidence_data = {
-        "provider_request_wizard_view-current_step": "3",
-        "3-TOTAL_FORMS": 2,
-        "3-INITIAL_FORMS": 0,
+        "provider_request_wizard_view-current_step": "4",
+        "4-TOTAL_FORMS": 2,
+        "4-INITIAL_FORMS": 0,
         # first an existing piece of evidence from ev1
-        "3-0-title": ev1.title,
-        "3-0-link": ev1.url,
-        "3-0-file": "",
-        "3-0-type": ev1.type,
-        "3-0-public": ev1.public,
+        "4-0-title": ev1.title,
+        "4-0-link": ev1.url,
+        "4-0-file": "",
+        "4-0-type": ev1.type,
+        "4-0-public": ev1.public,
         # then a new piece of evidence
-        "3-1-title": " ".join(faker.words(3)),
-        "3-1-link": "",
-        "3-1-file": fake_evidence,
-        "3-1-type": models.EvidenceType.ANNUAL_REPORT.value,
-        "3-1-public": "on",
+        "4-1-title": " ".join(faker.words(3)),
+        "4-1-link": "",
+        "4-1-file": fake_evidence,
+        "4-1-type": models.EvidenceType.ANNUAL_REPORT.value,
+        "4-1-public": "on",
     }
 
     response = client.post(edit_url, updated_evidence_data, follow=True)
@@ -1699,16 +1743,16 @@ def test_saving_changes_to_verification_request_from_hp_via_wizard(
     # when: submitting NETWORK step with overridden data, where we
     # have one less IP  range than before
     modified_network_data = {
-        "provider_request_wizard_view-current_step": "4",
-        "ips__4-TOTAL_FORMS": "2",
-        "ips__4-INITIAL_FORMS": "0",
-        "ips__4-0-start": ip1.ip_start,
-        "ips__4-0-end": ip1.ip_end,
-        "ips__4-1-start": ip2.ip_start,
-        "ips__4-1-end": ip2.ip_end,
-        "asns__4-TOTAL_FORMS": "1",
-        "asns__4-INITIAL_FORMS": "0",
-        "asns__4-0-asn": asn.asn,
+        "provider_request_wizard_view-current_step": "5",
+        "ips__5-TOTAL_FORMS": "2",
+        "ips__5-INITIAL_FORMS": "0",
+        "ips__5-0-start": ip1.ip_start,
+        "ips__5-0-end": ip1.ip_end,
+        "ips__5-1-start": ip2.ip_start,
+        "ips__5-1-end": ip2.ip_end,
+        "asns__5-TOTAL_FORMS": "1",
+        "asns__5-INITIAL_FORMS": "0",
+        "asns__5-0-asn": asn.asn,
     }
 
     response = client.post(edit_url, modified_network_data, follow=True)
@@ -1775,6 +1819,7 @@ def test_saving_changes_to_hp_with_new_verification_request(
     sorted_ips,
     wizard_form_org_details_data,
     wizard_form_services_data,
+    wizard_form_verification_bases_data,
     wizard_form_evidence_data,
     wizard_form_network_data,
     wizard_form_consent,
@@ -1844,6 +1889,9 @@ def test_saving_changes_to_hp_with_new_verification_request(
     # when: submitting SERVICES form with overridden data
     response = client.post(edit_url, wizard_form_services_data, follow=True)
 
+    # when: submitting VERIFICATION BASES form with overridden data
+    response = client.post(edit_url, wizard_form_verification_bases_data, follow=True)
+
     # then: wizards proceeds,
     # when: EVIDENCE formset is displayed with initial data
     evidence_formset = response.context_data["form"]
@@ -1853,21 +1901,21 @@ def test_saving_changes_to_hp_with_new_verification_request(
     # when: submitting EVIDENCE step with one piece of overridden data
     # and one saved data
     updated_evidence_data = {
-        "provider_request_wizard_view-current_step": "3",
-        "3-TOTAL_FORMS": 2,
-        "3-INITIAL_FORMS": 0,
+        "provider_request_wizard_view-current_step": "4",
+        "4-TOTAL_FORMS": 2,
+        "4-INITIAL_FORMS": 0,
         # first an existing piece of evidence from ev1
-        "3-0-title": ev1.title,
-        "3-0-link": ev1.url,
-        "3-0-file": "",
-        "3-0-type": ev1.type,
-        "3-0-public": ev1.public,
-        # then a new piece of evidence
-        "3-1-title": " ".join(faker.words(3)),
-        "3-1-link": "",
-        "3-1-file": fake_evidence,
-        "3-1-type": models.EvidenceType.ANNUAL_REPORT.value,
-        "3-1-public": "on",
+        "4-0-title": ev1.title,
+        "4-0-link": ev1.url,
+        "4-0-file": "",
+        "4-0-type": ev1.type,
+        "4-0-public": ev1.public,
+        #4then a new piece of evidence
+        "4-1-title": " ".join(faker.words(3)),
+        "4-1-link": "",
+        "4-1-file": fake_evidence,
+        "4-1-type": models.EvidenceType.ANNUAL_REPORT.value,
+        "4-1-public": "on",
     }
 
     response = client.post(edit_url, updated_evidence_data, follow=True)
@@ -1900,16 +1948,16 @@ def test_saving_changes_to_hp_with_new_verification_request(
     # when: submitting NETWORK step with overridden data, where we
     # have one less IP  range than before
     modified_network_data = {
-        "provider_request_wizard_view-current_step": "4",
-        "ips__4-TOTAL_FORMS": "2",
-        "ips__4-INITIAL_FORMS": "0",
-        "ips__4-0-start": ip1.ip_start,
-        "ips__4-0-end": ip1.ip_end,
-        "ips__4-1-start": ip2.ip_start,
-        "ips__4-1-end": ip2.ip_end,
-        "asns__4-TOTAL_FORMS": "1",
-        "asns__4-INITIAL_FORMS": "0",
-        "asns__4-0-asn": asn.asn,
+        "provider_request_wizard_view-current_step": "5",
+        "ips__5-TOTAL_FORMS": "2",
+        "ips__5-INITIAL_FORMS": "0",
+        "ips__5-0-start": ip1.ip_start,
+        "ips__5-0-end": ip1.ip_end,
+        "ips__5-1-start": ip2.ip_start,
+        "ips__5-1-end": ip2.ip_end,
+        "asns__5-TOTAL_FORMS": "1",
+        "asns__5-INITIAL_FORMS": "0",
+        "asns__5-0-asn": asn.asn,
     }
 
     response = client.post(edit_url, modified_network_data, follow=True)
diff --git a/apps/accounts/views.py b/apps/accounts/views.py
index 8552f5fd..05c573af 100644
--- a/apps/accounts/views.py
+++ b/apps/accounts/views.py
@@ -32,6 +32,7 @@
 from formtools.wizard.views import SessionWizardView
 
 from .forms import (
+    BasisForVerificationForm,
     ConsentForm,
     GreenEvidenceForm,
     LocationStepForm,
@@ -60,15 +61,15 @@
 class DashboardView(TemplateView):
     """
     This dashboard view was what people would see when signing into the admin.
-    We currently redirect to the provider portal home page as at present,we 
+    We currently redirect to the provider portal home page as at present,we
     only really logged in activity by users who work for the providers in our system.
     """
     template_name = "dashboard.html"
 
     def get(self, request, *args, **kwargs):
         return HttpResponseRedirect(reverse("provider_portal_home"))
-        
-    
+
+
 
 class ProviderAutocompleteView(autocomplete.Select2QuerySetView):
     def get_queryset(self):
@@ -240,15 +241,17 @@ class Steps(Enum):
         ORG_DETAILS = "0"
         LOCATIONS = "1"
         SERVICES = "2"
-        GREEN_EVIDENCE = "3"
-        NETWORK_FOOTPRINT = "4"
-        CONSENT = "5"
-        PREVIEW = "6"
+        BASIS_FOR_VERIFICATION = "3"
+        GREEN_EVIDENCE = "4"
+        NETWORK_FOOTPRINT = "5"
+        CONSENT = "6"
+        PREVIEW = "7"
 
     FORMS = [
         (Steps.ORG_DETAILS.value, OrgDetailsForm),
         (Steps.LOCATIONS.value, LocationStepForm),
         (Steps.SERVICES.value, ServicesForm),
+        (Steps.BASIS_FOR_VERIFICATION.value, BasisForVerificationForm),
         (Steps.GREEN_EVIDENCE.value, GreenEvidenceForm),
         (Steps.NETWORK_FOOTPRINT.value, NetworkFootprintForm),
         (Steps.CONSENT.value, ConsentForm),
@@ -259,6 +262,7 @@ class Steps(Enum):
         Steps.ORG_DETAILS.value: "provider_registration/about_org.html",
         Steps.LOCATIONS.value: "provider_registration/locations.html",
         Steps.SERVICES.value: "provider_registration/services.html",
+        Steps.BASIS_FOR_VERIFICATION.value: "provider_registration/basis_for_verification.html",
         Steps.GREEN_EVIDENCE.value: "provider_registration/evidence.html",
         Steps.NETWORK_FOOTPRINT.value: "provider_registration/network_footprint.html",
         Steps.CONSENT.value: "provider_registration/consent.html",
@@ -411,6 +415,11 @@ def _process_formset(formset, request):
         pr.created_by = self.request.user
         pr.save()
 
+        # process BASIS_FOR_VERIFICATION form: assign verification bases to ProviderRequest
+        verification_bases_form = form_dict[steps.BASIS_FOR_VERIFICATION.value]
+        verification_bases_slugs = verification_bases_form.cleaned_data["verification_bases"]
+        pr.set_verification_bases_from_slugs(verification_bases_slugs)
+
         # process GREEN_EVIDENCE form: link evidence to ProviderRequest
         evidence_formset = form_dict[steps.GREEN_EVIDENCE.value]
         _process_formset(evidence_formset, pr)
@@ -617,6 +626,7 @@ def get_instance_dict(cls, request_id):
                 "extra": pr_instance,
             },
             cls.Steps.SERVICES.value: pr_instance,
+            cls.Steps.BASIS_FOR_VERIFICATION.value: pr_instance,
             cls.Steps.GREEN_EVIDENCE.value: evidence_qs,
             cls.Steps.NETWORK_FOOTPRINT.value: {
                 "ips": ip_qs,
@@ -724,6 +734,9 @@ def _network_footprint_initial_data(hosting_provider: Hostingprovider):
             cls.Steps.SERVICES.value: {
                 "services": [s for s in hp_instance.services.slugs()]
             },
+            cls.Steps.BASIS_FOR_VERIFICATION.value: {
+                "verification_bases": [b for b in hp_instance.verification_bases.slugs()]
+            },
             cls.Steps.GREEN_EVIDENCE.value: [
                 _evidence_initial_data(ev)
                 for ev in hp_instance.supporting_documents.all()
diff --git a/apps/theme/static_src/src/css/custom/forms.css b/apps/theme/static_src/src/css/custom/forms.css
index 086e1b02..5920f9ce 100644
--- a/apps/theme/static_src/src/css/custom/forms.css
+++ b/apps/theme/static_src/src/css/custom/forms.css
@@ -7,7 +7,6 @@ legend {
 }
 fieldset label {
 	@apply text-base;
-	@apply uppercase;
 }
 
 [type="text"],
@@ -34,14 +33,14 @@ select {
 textarea {
 	@apply border-2;
 }
-[type="checkbox"], 
+[type="checkbox"],
 [type="radio"] {
 	@apply mb-2;
 	@apply mr-2;
 	@apply text-black;
 	@apply border-black;
 }
-[type="checkbox"]:checked, 
+[type="checkbox"]:checked,
 [type="radio"]:checked {
 	@apply bg-black;
 }


Reviews:
user: login: mrchrisadams
body: Hi @timcowlishaw  looks good! Only a few small niggles, and no show stoppers. Danke!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-13T12:43:36Z
user: login: timcowlishaw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-16T09:51:29Z

Review comments:
diff_hunk: @@ -0,0 +1,16 @@
+# Generated by Django 5.0.9 on 2025-05-12 04:13
+
+from django.db import migrations
+from ..group_permissions import group_permissions_2025_05_12_add_verification_basis_permissions_for_admins
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("accounts", "0067_verificationbasis_and_more"),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            group_permissions_2025_05_12_add_verification_basis_permissions_for_admins
path: apps/accounts/migrations/0068_verification_basis_permissions.py
user: login: mrchrisadams
body: One note - I started taking this approach a while back, and I'm not sure if it's the idiomatic way to handle migrations, but it seemed to work. I'm open to using a more django-y approach if it seems weird.
created_at: 2025-05-13T12:35:14Z
author_association: MEMBER
diff_hunk: @@ -149,6 +173,14 @@ def set_services_from_slugs(self, service_slugs: Iterable[str]) -> None:
         services = Service.objects.filter(slug__in=service_slugs)
         self.services.set(services)
 
+    def set_verification_bases_from_slugs(self, verification_basis_slugs: Iterable[str]) -> None:
+        """
+        Given list of verification_basis slugs (corresponding to Tag slugs)
+        apply matching verification_bases to the ProviderRequest object
+        """
+        verification_bases = VerificationBasis.objects.filter(slug__in=verification_basis_slugs)
path: apps/accounts/models/provider_request.py
user: login: mrchrisadams
body:  I think the django taggit API docs recommends using distinct to avoid having duplicate results come back whwn we use the `__in=` operator. Would you mind checking to see if this makes a difference to results, and adding the `distinct()` if so?

https://django-taggit.readthedocs.io/en/latest/api.html#filtering
created_at: 2025-05-13T12:41:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,16 @@
+# Generated by Django 5.0.9 on 2025-05-12 04:13
+
+from django.db import migrations
+from ..group_permissions import group_permissions_2025_05_12_add_verification_basis_permissions_for_admins
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("accounts", "0067_verificationbasis_and_more"),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            group_permissions_2025_05_12_add_verification_basis_permissions_for_admins
path: apps/accounts/migrations/0068_verification_basis_permissions.py
user: login: timcowlishaw
body: just commenting here for future reference: we had a chat about this yesterday, and have created a new card for a more general review of how we're handling permissions and data migrations! the current approach makes sense to me but doesn't quite seem to be doing what we expect.
created_at: 2025-05-16T09:51:29Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-12T03:48:23Z
body: <details><summary>Old Energy Estimation</summary>













































Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.21|427.773|4.11|104|
|checkout|10.29|3.64008|3.64|1|
|pip install uv wheel|21.47|7.98125|3.99|2|
|pip install requirements|70.786|33.4333|5.57|6|
|pytest|27.3243|382.719|4.12|93|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-12T03:48:30Z
body: <details><summary>Old Energy Estimation</summary>











































Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|28.625|436.652|4.04|108|
|checkout|3|2.77863|2.78|1|
|pip install uv wheel|16.635|7.05822|3.53|2|
|pip install requirements|65.052|31.2031|5.20|6|
|pytest|27.3479|395.612|4.12|96|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-12T04:18:01Z
body: <details><summary>Old Energy Estimation</summary>







































Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|28.2063|442.42|3.92|113|
|checkout|1.12|2.14354|1.07|2|
|pip install uv wheel|21.435|14.7024|2.94|5|
|pip install requirements|57.5917|33.8714|4.84|7|
|pytest|26.9757|391.703|4.08|96|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-12T04:18:02Z
body: <details><summary>Old Energy Estimation</summary>





































Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.0618|467.52|4.03|116|
|checkout|2.4|2.57658|1.29|2|
|pip install uv wheel|25.7686|28.1046|3.51|8|
|pip install requirements|62.3617|36.2967|5.19|7|
|pytest|27.6015|400.542|4.09|98|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: timcowlishaw
created_at: 2025-05-12T06:24:53Z
body: ![](https://github.trello.services/images/mini-trello-icon.png) [Add a "What is your basis for verification?" question to the verification flow.](https://trello.com/c/WZgVt1At/438-add-a-what-is-your-basis-for-verification-question-to-the-verification-flow)
author_association: COLLABORATOR
user: login: fershad
created_at: 2025-05-12T09:26:50Z
body: @timcowlishaw could we merge this into Staging first so that I can step through it from both the verification wizard & admin panel perspective
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-05-13T07:13:25Z
body: <details><summary>Old Energy Estimation</summary>































Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.4702|426.715|3.99|107|
|checkout|27.15|4.14941|2.07|2|
|pip install uv wheel|28.245|8.37055|2.79|3|
|pip install requirements|66.704|31.5719|5.26|6|
|pytest|27.304|382.623|4.07|94|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-13T07:13:35Z
body: <details><summary>Old Energy Estimation</summary>





























Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|28.7499|460.374|4.04|114|
|checkout|10.86|3.67022|3.67|1|
|pip install uv wheel|23.76|16.1925|4.05|4|
|pip install requirements|65.65|31.6368|6.33|5|
|pytest|27.3661|408.875|4.09|100|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-13T07:29:55Z
body: <details><summary>Old Energy Estimation</summary>























Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|28.8855|407.596|4.00|102|
|checkout|2.47|2.59914|2.60|1|
|pip install uv wheel|2.52|2.61544|1.31|2|
|pip install requirements|62.678|30.2593|6.05|5|
|pytest|27.7076|372.122|4.09|91|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-13T07:29:57Z
body: <details><summary>Old Energy Estimation</summary>





















Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.4651|433.099|4.09|106|
|checkout|11.99|3.69879|3.70|1|
|pip install uv wheel|12.13|3.70123|3.70|1|
|pip install requirements|65.36|31.4765|6.30|5|
|pytest|27.9262|394.223|4.15|95|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: timcowlishaw
created_at: 2025-05-13T07:44:26Z
body: Updated to add "more info" links and to copy verification basis to hosting provider on approval!
author_association: COLLABORATOR
user: login: timcowlishaw
created_at: 2025-05-13T07:45:09Z
body: @fershad  - I'll get this up onto staging today, just want to check something wiith @mrchrisadams first!
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-05-13T07:45:41Z
body: <details><summary>Old Energy Estimation</summary>



















Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.65|430.83|4.03|107|
|checkout|10.76|3.65659|3.66|1|
|pip install uv wheel|23.1567|12.1039|3.03|4|
|pip install requirements|58.965|23.7471|4.75|5|
|pytest|28.294|391.323|4.08|96|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-13T07:45:45Z
body: <details><summary>Old Energy Estimation</summary>



















Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.8564|450.845|4.10|110|
|checkout|25.01|4.07113|2.04|2|
|pip install uv wheel|30.936|21.5329|3.59|6|
|pip install requirements|66.18|31.2387|6.25|5|
|pytest|27.9602|394.003|4.10|96|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-13T10:38:30Z
body: <details><summary>Old Energy Estimation</summary>

















Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.0987|416.054|4.08|102|
|checkout|5.42|3.46825|3.47|1|
|pip install uv wheel|21.5233|11.932|3.98|3|
|pip install requirements|60.3875|24.3204|4.86|5|
|pytest|27.6252|376.333|4.09|92|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-13T10:38:37Z
body: <details><summary>Old Energy Estimation</summary>















Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|30.3301|439.104|4.07|108|
|checkout|25.93|4.10699|2.05|2|
|pip install uv wheel|25.55|4.08943|2.04|2|
|pip install requirements|68.458|32.13|6.43|5|
|pytest|28.078|398.778|4.15|96|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-13T14:22:15Z
body: <details><summary>Old Energy Estimation</summary>













Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.5681|400.182|4.04|99|
|checkout|7.51|3.5414|3.54|1|
|pip install uv wheel|7.65|3.5465|3.55|1|
|pip install requirements|58.0325|23.5753|4.72|5|
|pytest|28.0809|369.518|4.11|90|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-13T14:22:29Z
body: <details><summary>Old Energy Estimation</summary>











Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.3483|426.044|3.98|107|
|checkout|10.88|3.6604|3.66|1|
|pip install uv wheel|10.91|3.65931|1.83|2|
|pip install requirements|58.52|23.6078|4.72|5|
|pytest|28.1635|395.117|4.12|96|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-15T10:13:28Z
body: <details><summary>Old Energy Estimation</summary>







Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.8401|442.947|4.10|108|
|checkout|23.37|4.02783|2.01|2|
|pip install uv wheel|26.36|8.25367|4.13|2|
|pip install requirements|67.836|32.0953|5.35|6|
|pytest|28.0816|398.57|4.11|97|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-15T10:13:33Z
body: <details><summary>Old Energy Estimation</summary>







Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.9321|456.099|4.11|111|
|checkout|28.01|4.17937|2.09|2|
|pip install uv wheel|28.29|16.7457|3.35|5|
|pip install requirements|70.572|32.5844|5.43|6|
|pytest|28.0156|402.59|4.11|98|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-15T14:41:09Z
body: <details><summary>Old Energy Estimation</summary>





Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|30.5942|436.542|4.12|106|
|checkout|28.85|4.20865|2.10|2|
|pip install uv wheel|29.7|12.7105|3.18|4|
|pip install requirements|64.8467|36.9937|5.28|7|
|pytest|28.3452|382.629|4.11|93|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-15T14:41:20Z
body: <details><summary>Old Energy Estimation</summary>





Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.6552|463.516|4.10|113|
|checkout|27.45|4.15976|4.16|1|
|pip install uv wheel|27.8575|16.6857|4.17|4|
|pip install requirements|67.904|31.8838|6.38|5|
|pytest|27.88|410.787|4.15|99|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-16T09:50:28Z
body: <details><summary>Old Energy Estimation</summary>



Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|28.9763|421.876|4.02|105|
|checkout|22.68|4.00911|4.01|1|
|pip install uv wheel|22.46|4.0021|2.00|2|
|pip install requirements|67.186|31.8336|5.31|6|
|pytest|27.167|382.031|4.06|94|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-16T09:50:30Z
body: <details><summary>Old Energy Estimation</summary>



Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.6329|441.016|4.08|108|
|checkout|28.7|4.20409|4.20|1|
|pip install uv wheel|28.3|4.18721|4.19|1|
|pip install requirements|69.084|32.317|6.46|5|
|pytest|27.6167|400.308|4.08|98|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-19T10:18:42Z
body: <details><summary>Old Energy Estimation</summary>

Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.4038|440.799|4.01|110|
|checkout|1.34|2.22257|1.11|2|
|pip install uv wheel|20.2133|10.6956|3.57|3|
|pip install requirements|65.288|30.7445|5.12|6|
|pytest|27.8334|397.136|4.14|96|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-19T10:18:45Z
body: <details><summary>Old Energy Estimation</summary>

Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.2068|438.438|3.99|110|
|checkout|7.15|3.52919|1.76|2|
|pip install uv wheel|22.22|12.0072|4.00|3|
|pip install requirements|64.234|31.3552|5.23|6|
|pytest|27.426|391.546|4.12|95|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-20T10:02:13Z
body: Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|29.6841|433.398|4.01|108|
|checkout|26.34|4.12052|2.06|2|
|pip install uv wheel|28.7067|12.6041|4.20|3|
|pip install requirements|69.188|31.9954|6.40|5|
|pytest|27.7718|384.678|4.09|94|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-20T10:02:17Z
body: Eco-CI Output: 
|Label|🖥 avg. CPU utilization [%]|🔋 Total Energy [Joules]|🔌 avg. Power [Watts]|Duration [Seconds]|
|---|---|---|---|---|
|Total Run (incl. overhead)|28.7816|444.463|4.04|110|
|checkout|2.47|2.60061|1.30|2|
|pip install uv wheel|17.185|6.93086|3.47|2|
|pip install requirements|66.322|31.3186|6.26|5|
|pytest|27.401|403.613|4.08|99|

❌ CO2 Data:
Error in retrieving values. Please see the detailed logs for the exact error messages!

author_association: NONE