Repository: saleor/saleor
Pull request: Add support for metadata for Address model 
- Add metadata to `Address` model and type
- Allow updating metadata with use of update and delete metadata mutations

Resolves https://github.com/saleor/saleor/issues/11678

<!-- Please mention all relevant issue numbers. -->

# Impact

* [ ] New migrations
* [ ] New/Updated API fields or mutations
* [ ] Deprecated API fields or mutations
* [ ] Removed API types, fields, or mutations
* [ ] Documentation needs to be updated

# Pull Request Checklist

<!-- Please keep this section. It will make the maintainer's life easier. -->

* [ ] Privileged queries and mutations are guarded by proper permission checks
* [ ] Database queries are optimized and the number of queries is constant
* [ ] Database migration files are up to date
* [ ] The changes are tested
* [ ] GraphQL schema and type definitions are up to date
* [ ] Changes are mentioned in the changelog


Problem statement:
Add support for metadata for Address model
### What I'm trying to achieve
Set metadata for an address object.

### Describe a proposed solution
Re-use the current solution that we have for other models and API. In the API updateMetadata for the Address type should be protected with MANAGE_USERS permission, plus owners of the address should be able to change the metadata as well.

### Other solutions I've tried and won't work
…

### Screenshots or mockups
<!-- Please provide any illustrations that could help others understand the problem or the proposed solution. -->

Hints from issue discussion:

Commit message: Set correct version in Address metadata fields descriptions

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78e33ff7f4f..8a43853b5a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@ All notable, unreleased changes to this project will be documented in this file.
 - Extend invoice object types with `Order` references - #11505 by @przlada
   - Add `Invoice.order` field
   - Add `InvoiceRequested.order`, `InvoiceDeleted.order` and `InvoiceSent.order` fields
+- Add support for metadata for Address model - #11701 by @IKarbowiakg
 
 ### Other changes
 - Fix fetching the `checkout.availableCollectionPoints` - #11489 by @IKarbowiak
diff --git a/saleor/account/migrations/0075_add_address_metadata.py b/saleor/account/migrations/0075_add_address_metadata.py
new file mode 100644
index 00000000000..6e57e5a792d
--- /dev/null
+++ b/saleor/account/migrations/0075_add_address_metadata.py
@@ -0,0 +1,47 @@
+# Generated by Django 3.2.16 on 2023-01-10 08:39
+
+import django.contrib.postgres.indexes
+from django.db import migrations, models
+import saleor.core.utils.json_serializer
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("account", "0074_merge_20230102_0914"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="address",
+            name="metadata",
+            field=models.JSONField(
+                blank=True,
+                default=dict,
+                encoder=saleor.core.utils.json_serializer.CustomJsonEncoder,
+                null=True,
+            ),
+        ),
+        migrations.AddField(
+            model_name="address",
+            name="private_metadata",
+            field=models.JSONField(
+                blank=True,
+                default=dict,
+                encoder=saleor.core.utils.json_serializer.CustomJsonEncoder,
+                null=True,
+            ),
+        ),
+        migrations.AddIndex(
+            model_name="address",
+            index=django.contrib.postgres.indexes.GinIndex(
+                fields=["private_metadata"], name="address_p_meta_idx"
+            ),
+        ),
+        migrations.AddIndex(
+            model_name="address",
+            index=django.contrib.postgres.indexes.GinIndex(
+                fields=["metadata"], name="address_meta_idx"
+            ),
+        ),
+    ]
diff --git a/saleor/account/models.py b/saleor/account/models.py
index 226143c9775..841b73be8c9 100644
--- a/saleor/account/models.py
+++ b/saleor/account/models.py
@@ -53,7 +53,7 @@ def annotate_default(self, user):
 AddressManager = models.Manager.from_queryset(AddressQueryset)
 
 
-class Address(models.Model):
+class Address(ModelWithMetadata):
     first_name = models.CharField(max_length=256, blank=True)
     last_name = models.CharField(max_length=256, blank=True)
     company_name = models.CharField(max_length=256, blank=True)
@@ -71,6 +71,7 @@ class Address(models.Model):
     class Meta:
         ordering = ("pk",)
         indexes = [
+            *ModelWithMetadata.Meta.indexes,
             GinIndex(
                 name="address_search_gin",
                 # `opclasses` and `fields` should be the same length
diff --git a/saleor/account/tests/test_account.py b/saleor/account/tests/test_account.py
index c990107e5f2..635330588c6 100644
--- a/saleor/account/tests/test_account.py
+++ b/saleor/account/tests/test_account.py
@@ -172,6 +172,8 @@ def test_address_as_data(address):
         "country": "PL",
         "country_area": "",
         "phone": "+48713988102",
+        "metadata": {},
+        "private_metadata": {},
     }
 
 
diff --git a/saleor/graphql/account/i18n.py b/saleor/graphql/account/i18n.py
index 77f4609ddf5..cf41684c18f 100644
--- a/saleor/graphql/account/i18n.py
+++ b/saleor/graphql/account/i18n.py
@@ -61,6 +61,12 @@ def validate_address_form(
             )
             if errors:
                 raise ValidationError(errors)
+
+        if address_form.cleaned_data["metadata"] is None:
+            address_form.cleaned_data["metadata"] = {}
+        if address_form.cleaned_data["private_metadata"] is None:
+            address_form.cleaned_data["private_metadata"] = {}
+
         return address_form
 
     @classmethod
diff --git a/saleor/graphql/account/tests/test_account.py b/saleor/graphql/account/tests/test_account.py
index b8f9228a464..d09ec8d6318 100644
--- a/saleor/graphql/account/tests/test_account.py
+++ b/saleor/graphql/account/tests/test_account.py
@@ -1590,6 +1590,9 @@ def test_customer_create(
     last_name = "api_last_name"
     note = "Test user"
     address_data = convert_dict_keys_to_camel_case(address.as_data())
+    address_data.pop("metadata")
+    address_data.pop("privateMetadata")
+
     redirect_url = "https://www.example.com"
     external_reference = "test-ext-ref"
     variables = {
@@ -1854,6 +1857,8 @@ def test_customer_update(
     note = "Test update note"
     external_reference = "test-ext-ref"
     address_data = convert_dict_keys_to_camel_case(address.as_data())
+    address_data.pop("metadata")
+    address_data.pop("privateMetadata")
 
     new_street_address = "Updated street address"
     address_data["streetAddress1"] = new_street_address
diff --git a/saleor/graphql/account/types.py b/saleor/graphql/account/types.py
index 27fb1e8ff38..c92e43a6353 100644
--- a/saleor/graphql/account/types.py
+++ b/saleor/graphql/account/types.py
@@ -24,7 +24,12 @@
 from ..checkout.types import Checkout, CheckoutCountableConnection
 from ..core import ResolveInfo
 from ..core.connection import CountableConnection, create_connection_slice
-from ..core.descriptions import ADDED_IN_38, ADDED_IN_310, DEPRECATED_IN_3X_FIELD
+from ..core.descriptions import (
+    ADDED_IN_38,
+    ADDED_IN_310,
+    DEPRECATED_IN_3X_FIELD,
+    PREVIEW_FEATURE,
+)
 from ..core.enums import LanguageCodeEnum
 from ..core.federation import federated_entity, resolve_federation_references
 from ..core.fields import ConnectionField, PermissionsField
@@ -40,7 +45,7 @@
 )
 from ..core.utils import from_global_id_or_error, str_to_enum, to_global_id_or_none
 from ..giftcard.dataloaders import GiftCardsByUserLoader
-from ..meta.types import ObjectWithMetadata
+from ..meta.types import Metadata, MetadataDescription, MetadataItem, ObjectWithMetadata
 from ..order.dataloaders import OrderLineByIdLoader, OrdersByUserLoader
 from ..plugins.dataloaders import get_plugin_manager_promise
 from ..utils import format_permissions_for_display, get_user_or_app_from_context
@@ -89,9 +94,44 @@ class Address(ModelObjectType[models.Address]):
         required=False, description="Address is user's default billing address."
     )
 
+    # temporary copy of meta fields to allow specifying the correct version when
+    # fields were introduced
+    private_metadata = NonNullList(
+        MetadataItem,
+        required=True,
+        description=MetadataDescription.PRIVATE_METADATA
+        + ADDED_IN_310
+        + PREVIEW_FEATURE,
+    )
+    private_metafield = graphene.String(
+        args={"key": graphene.NonNull(graphene.String)},
+        description=(
+            MetadataDescription.PRIVATE_METAFIELD + ADDED_IN_310 + PREVIEW_FEATURE
+        ),
+    )
+    private_metafields = Metadata(
+        args={"keys": NonNullList(graphene.String)},
+        description=(
+            MetadataDescription.PRIVATE_METAFIELDS + ADDED_IN_310 + PREVIEW_FEATURE
+        ),
+    )
+    metadata = NonNullList(
+        MetadataItem,
+        required=True,
+        description=(MetadataDescription.METADATA + ADDED_IN_310 + PREVIEW_FEATURE),
+    )
+    metafield = graphene.String(
+        args={"key": graphene.NonNull(graphene.String)},
+        description=(MetadataDescription.METAFIELD + ADDED_IN_310 + PREVIEW_FEATURE),
+    )
+    metafields = Metadata(
+        args={"keys": NonNullList(graphene.String)},
+        description=(MetadataDescription.METAFIELDS + ADDED_IN_310 + PREVIEW_FEATURE),
+    )
+
     class Meta:
         description = "Represents user address data."
-        interfaces = [relay.Node]
+        interfaces = [relay.Node, ObjectWithMetadata]
         model = models.Address
 
     @staticmethod
diff --git a/saleor/graphql/checkout/mutations/checkout_complete.py b/saleor/graphql/checkout/mutations/checkout_complete.py
index e118d2e8857..bd5e58e3b07 100644
--- a/saleor/graphql/checkout/mutations/checkout_complete.py
+++ b/saleor/graphql/checkout/mutations/checkout_complete.py
@@ -215,7 +215,6 @@ def perform_mutation(  # type: ignore[override]
                     order=order, confirmation_needed=False, confirmation_data={}
                 )
             raise e
-
         if metadata is not None:
             cls.check_metadata_permissions(
                 info,
diff --git a/saleor/graphql/meta/permissions.py b/saleor/graphql/meta/permissions.py
index ac50ed5e1fd..e82d8b26521 100644
--- a/saleor/graphql/meta/permissions.py
+++ b/saleor/graphql/meta/permissions.py
@@ -1,6 +1,7 @@
 from typing import Any, Callable, Dict, List, Union
 
 from django.core.exceptions import ValidationError
+from django.db.models import Exists, OuterRef
 
 from ...account import models as account_models
 from ...account.error_codes import AccountErrorCode
@@ -25,6 +26,8 @@
     ShippingPermissions,
 )
 from ...payment.utils import payment_owned_by_user
+from ...site import models as site_models
+from ...warehouse import models as warehouse_models
 from ..app.dataloaders import get_app_promise
 from ..core import ResolveInfo
 from ..core.utils import from_global_id_or_error
@@ -70,6 +73,80 @@ def private_user_permissions(
     return [AccountPermissions.MANAGE_USERS]
 
 
+def public_address_permissions(
+    info: ResolveInfo, address_pk: int
+) -> List[BasePermissionEnum]:
+    """Resolve permission for access to public metadata for user addresses.
+
+    Customer have access to the public metadata of their own addresses.
+    Staff user with `MANAGE_USERS` have access to public metadata of customer
+    addresses.
+    Staff user with `MANAGE_STAFF` have access to public metadata of staff user
+    addresses.
+    For now, updating warehouse and shop addresses is forbidden.
+    """
+    address = (
+        account_models.Address.objects.filter(pk=address_pk)
+        .prefetch_related("user_addresses")
+        .first()
+    )
+    if not address:
+        raise ValidationError(
+            {
+                "id": ValidationError(
+                    "Couldn't resolve address.", code=AccountErrorCode.NOT_FOUND.value
+                )
+            }
+        )
+    user = info.context.user
+    # no permission is required when the requestor is the owner of the address
+    if user and address.user_addresses.filter(id=user.id):
+        return []
+    staff_users = account_models.User.objects.filter(is_staff=True)
+
+    if address.user_addresses.filter(Exists(staff_users.filter(id=OuterRef("id")))):
+        return [AccountPermissions.MANAGE_STAFF]
+
+    if (
+        warehouse_models.Warehouse.objects.filter(address_id=address.id).exists()
+        or site_models.SiteSettings.objects.filter(
+            company_address_id=address.id
+        ).exists()
+    ):
+        raise PermissionDenied()
+
+    return [AccountPermissions.MANAGE_USERS]
+
+
+def private_address_permissions(
+    _info: ResolveInfo, address_pk: int
+) -> List[BasePermissionEnum]:
+    address = (
+        account_models.Address.objects.filter(pk=address_pk)
+        .prefetch_related("user_addresses")
+        .first()
+    )
+    if not address:
+        raise ValidationError(
+            {
+                "id": ValidationError(
+                    "Couldn't resolve address.", code=AccountErrorCode.NOT_FOUND.value
+                )
+            }
+        )
+    staff_users = account_models.User.objects.filter(is_staff=True)
+    if address.user_addresses.filter(Exists(staff_users.filter(id=OuterRef("id")))):
+        return [AccountPermissions.MANAGE_STAFF]
+    if (
+        warehouse_models.Warehouse.objects.filter(address_id=address.id).exists()
+        or site_models.SiteSettings.objects.filter(
+            company_address_id=address.id
+        ).exists()
+    ):
+        raise PermissionDenied()
+    return [AccountPermissions.MANAGE_USERS]
+
+
 def product_permissions(
     _info: ResolveInfo, _object_pk: Any
 ) -> List[BasePermissionEnum]:
@@ -191,6 +268,7 @@ def tax_permissions(_info: ResolveInfo, _object_pk: int) -> List[BasePermissionE
 PUBLIC_META_PERMISSION_MAP: Dict[
     str, Callable[[ResolveInfo, Any], List[BasePermissionEnum]]
 ] = {
+    "Address": public_address_permissions,
     "App": app_permissions,
     "Attribute": attribute_permissions,
     "Category": product_permissions,
@@ -226,6 +304,7 @@ def tax_permissions(_info: ResolveInfo, _object_pk: int) -> List[BasePermissionE
 PRIVATE_META_PERMISSION_MAP: Dict[
     str, Callable[[ResolveInfo, Any], List[BasePermissionEnum]]
 ] = {
+    "Address": private_address_permissions,
     "App": private_app_permssions,
     "Attribute": attribute_permissions,
     "Category": product_permissions,
diff --git a/saleor/graphql/meta/resolvers.py b/saleor/graphql/meta/resolvers.py
index 01ccd7cdc5c..c05fc41d957 100644
--- a/saleor/graphql/meta/resolvers.py
+++ b/saleor/graphql/meta/resolvers.py
@@ -45,6 +45,7 @@ def resolve_object_with_metadata_type(instance):
 
     if isinstance(instance, ModelWithMetadata):
         MODEL_TO_TYPE_MAP = {
+            account_models.Address: account_types.Address,
             account_models.User: account_types.User,
             app_models.App: app_types.App,
             attribute_models.Attribute: attribute_types.Attribute,
diff --git a/saleor/graphql/meta/tests/test_meta_mutations.py b/saleor/graphql/meta/tests/test_meta_mutations.py
index f9b0654e9a3..fc7442ae538 100644
--- a/saleor/graphql/meta/tests/test_meta_mutations.py
+++ b/saleor/graphql/meta/tests/test_meta_mutations.py
@@ -1019,16 +1019,19 @@ def test_update_public_metadata_for_non_exist_item(
     assert errors[0]["code"] == MetadataErrorCode.NOT_FOUND.name
 
 
-def test_update_public_metadata_for_item_without_meta(api_client, address):
+def test_update_public_metadata_for_item_without_meta(
+    api_client, permission_group_manage_users
+):
     # given
-    assert not issubclass(type(address), ModelWithMetadata)
-    address_id = graphene.Node.to_global_id("Address", address.pk)
+    group = permission_group_manage_users
+    assert not issubclass(type(group), ModelWithMetadata)
+    group_id = graphene.Node.to_global_id("Group", group.pk)
 
     # when
     # We use "User" type inside mutation for valid graphql query with fragment
     # without this we are not able to reuse UPDATE_PUBLIC_METADATA_MUTATION
     response = execute_update_public_metadata_for_item(
-        api_client, None, address_id, "User"
+        api_client, None, group_id, "User"
     )
 
     # then
@@ -1134,6 +1137,281 @@ def test_add_public_metadata_for_gift_card(
     )
 
 
+def test_update_public_metadata_for_customer_address_by_logged_user(
+    user_api_client, address
+):
+    # given
+    user = user_api_client.user
+    user.addresses.add(address)
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    # when
+    response = execute_update_public_metadata_for_item(
+        user_api_client, None, address_id, "Address", value="NewMetaValue"
+    )
+
+    # then
+    assert item_contains_proper_public_metadata(
+        response["data"]["updateMetadata"]["item"],
+        address,
+        address_id,
+        value="NewMetaValue",
+    )
+
+
+def test_update_public_metadata_for_customer_address_by_different_logged_user(
+    user2_api_client, customer_user
+):
+    # given
+    address = customer_user.addresses.first()
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    variables = {
+        "id": address_id,
+        "input": [{"key": PUBLIC_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = user2_api_client.post_graphql(
+        UPDATE_PUBLIC_METADATA_MUTATION % "Address", variables, permissions=None
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_update_public_metadata_for_customer_address_by_staff_with_perm(
+    staff_api_client, customer_user, permission_manage_users
+):
+    # given
+    address = customer_user.addresses.first()
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    # when
+    response = execute_update_public_metadata_for_item(
+        staff_api_client,
+        permission_manage_users,
+        address_id,
+        "Address",
+        value="NewMetaValue",
+    )
+
+    # then
+    assert item_contains_proper_public_metadata(
+        response["data"]["updateMetadata"]["item"],
+        address,
+        address_id,
+        value="NewMetaValue",
+    )
+
+
+def test_update_public_metadata_for_customer_address_by_app_with_perm(
+    app_api_client, customer_user, permission_manage_users
+):
+    # given
+    address = customer_user.addresses.first()
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    # when
+    response = execute_update_public_metadata_for_item(
+        app_api_client,
+        permission_manage_users,
+        address_id,
+        "Address",
+        value="NewMetaValue",
+    )
+
+    # then
+    assert item_contains_proper_public_metadata(
+        response["data"]["updateMetadata"]["item"],
+        address,
+        address_id,
+        value="NewMetaValue",
+    )
+
+
+def test_update_public_metadata_for_address_by_non_logged_user(
+    api_client, customer_user
+):
+    # given
+    address = customer_user.addresses.first()
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+    variables = {
+        "id": address_id,
+        "input": [{"key": PUBLIC_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = api_client.post_graphql(
+        UPDATE_PUBLIC_METADATA_MUTATION % "Address", variables, permissions=None
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_update_public_metadata_for_staff_address_by_staff_with_perm(
+    staff_api_client, address, staff_users, permission_manage_staff
+):
+    # given
+    user = staff_users[-1]
+    user.addresses.add(address)
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    # when
+    response = execute_update_public_metadata_for_item(
+        staff_api_client,
+        permission_manage_staff,
+        address_id,
+        "Address",
+        value="NewMetaValue",
+    )
+
+    # then
+    assert item_contains_proper_public_metadata(
+        response["data"]["updateMetadata"]["item"],
+        address,
+        address_id,
+        value="NewMetaValue",
+    )
+
+
+def test_update_public_metadata_for_staff_address_by_staff_without_perm(
+    staff_api_client, address, staff_users
+):
+    # given
+    user = staff_users[-1]
+    user.addresses.add(address)
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    variables = {
+        "id": address_id,
+        "input": [{"key": PUBLIC_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = staff_api_client.post_graphql(
+        UPDATE_PUBLIC_METADATA_MUTATION % "Address", variables, permissions=None
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_update_public_metadata_for_staff_address_by_customer(
+    user_api_client, address, staff_users
+):
+    # given
+    user = staff_users[-1]
+    user.addresses.add(address)
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    variables = {
+        "id": address_id,
+        "input": [{"key": PUBLIC_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = user_api_client.post_graphql(
+        UPDATE_PUBLIC_METADATA_MUTATION % "Address", variables, permissions=None
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_update_public_metadata_for_staff_address_by_app_with_perm(
+    app_api_client, staff_user, address, permission_manage_staff
+):
+    # given
+    staff_user.addresses.add(address)
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    variables = {
+        "id": address_id,
+        "input": [{"key": PUBLIC_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = app_api_client.post_graphql(
+        UPDATE_PUBLIC_METADATA_MUTATION % "Address",
+        variables,
+        permissions=[permission_manage_staff],
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_update_public_metadata_for_staff_address_by_app_without_perm(
+    app_api_client, staff_user, address
+):
+    # given
+    staff_user.addresses.add(address)
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    variables = {
+        "id": address_id,
+        "input": [{"key": PUBLIC_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = app_api_client.post_graphql(
+        UPDATE_PUBLIC_METADATA_MUTATION % "Address", variables
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_update_public_metadata_for_warehouse_address_by_staff(
+    staff_api_client, warehouse, permission_manage_staff
+):
+    # given
+    address = warehouse.address
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    variables = {
+        "id": address_id,
+        "input": [{"key": PUBLIC_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = staff_api_client.post_graphql(
+        UPDATE_PUBLIC_METADATA_MUTATION % "Address",
+        variables,
+        permissions=[permission_manage_staff],
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_update_public_metadata_for_site_settings_address_by_staff(
+    staff_api_client, site_settings, address, permission_manage_staff
+):
+    # given
+    site_settings.company_address = address
+    site_settings.save(update_fields=["company_address"])
+
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    variables = {
+        "id": address_id,
+        "input": [{"key": PUBLIC_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = staff_api_client.post_graphql(
+        UPDATE_PUBLIC_METADATA_MUTATION % "Address",
+        variables,
+        permissions=[permission_manage_staff],
+    )
+
+    # then
+    assert_no_permission(response)
+
+
 DELETE_PUBLIC_METADATA_MUTATION = """
 mutation DeletePublicMetadata($id: ID!, $keys: [String!]!) {
     deleteMetadata(
@@ -1852,16 +2130,19 @@ def test_delete_public_metadata_for_non_exist_item(
     assert errors[0]["code"] == MetadataErrorCode.NOT_FOUND.name
 
 
-def test_delete_public_metadata_for_item_without_meta(api_client, address):
+def test_delete_public_metadata_for_item_without_meta(
+    api_client, permission_group_manage_users
+):
     # given
-    assert not issubclass(type(address), ModelWithMetadata)
-    address_id = graphene.Node.to_global_id("Address", address.pk)
+    group = permission_group_manage_users
+    assert not issubclass(type(group), ModelWithMetadata)
+    group_id = graphene.Node.to_global_id("Group", group.pk)
 
     # when
     # We use "User" type inside mutation for valid graphql query with fragment
     # without this we are not able to reuse DELETE_PUBLIC_METADATA_MUTATION
     response = execute_clear_public_metadata_for_item(
-        api_client, None, address_id, "User"
+        api_client, None, group_id, "User"
     )
 
     # then
@@ -2777,16 +3058,19 @@ def test_update_private_metadata_for_non_exist_item(
     assert errors[0]["code"] == MetadataErrorCode.NOT_FOUND.name
 
 
-def test_update_private_metadata_for_item_without_meta(api_client, address):
+def test_update_private_metadata_for_item_without_meta(
+    api_client, permission_group_manage_users
+):
     # given
-    assert not issubclass(type(address), ModelWithMetadata)
-    address_id = graphene.Node.to_global_id("Address", address.pk)
+    group = permission_group_manage_users
+    assert not issubclass(type(group), ModelWithMetadata)
+    group_id = graphene.Node.to_global_id("Group", group.pk)
 
     # when
     # We use "User" type inside mutation for valid graphql query with fragment
     # without this we are not able to reuse UPDATE_PRIVATE_METADATA_MUTATION
     response = execute_update_private_metadata_for_item(
-        api_client, None, address_id, "User"
+        api_client, None, group_id, "User"
     )
 
     # then
@@ -2984,53 +3268,328 @@ def test_add_private_metadata_for_gift_card(
     )
 
 
-DELETE_PRIVATE_METADATA_MUTATION = """
-mutation DeletePrivateMetadata($id: ID!, $keys: [String!]!) {
-    deletePrivateMetadata(
-        id: $id
-        keys: $keys
-    ) {
-        errors{
-            field
-            code
-        }
-        item {
-            privateMetadata{
-                key
-                value
-            }
-            ...on %s{
-                id
-            }
-        }
-    }
-}
-"""
-
-
-def execute_clear_private_metadata_for_item(
-    client,
-    permissions,
-    item_id,
-    item_type,
-    key=PRIVATE_KEY,
+def test_update_private_metadata_for_customer_address_by_logged_user(
+    user_api_client, address
 ):
+    # given
+    user = user_api_client.user
+    user.addresses.add(address)
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
     variables = {
-        "id": item_id,
-        "keys": [key],
+        "id": address_id,
+        "input": [{"key": PRIVATE_KEY, "value": "NewMetaValue"}],
     }
 
-    response = client.post_graphql(
-        DELETE_PRIVATE_METADATA_MUTATION % item_type,
-        variables,
-        permissions=[permissions] if permissions else None,
+    # when
+    response = user_api_client.post_graphql(
+        UPDATE_PRIVATE_METADATA_MUTATION % "Address", variables, permissions=None
     )
-    response = get_graphql_content(response)
-    return response
+
+    # then
+    assert_no_permission(response)
 
 
-def execute_clear_private_metadata_for_multiple_items(
-    client, permissions, item_id, item_type, key=PUBLIC_KEY, key2=PUBLIC_KEY2
+def test_update_private_metadata_for_customer_address_by_different_logged_user(
+    user2_api_client, customer_user
+):
+    # given
+    address = customer_user.addresses.first()
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    variables = {
+        "id": address_id,
+        "input": [{"key": PRIVATE_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = user2_api_client.post_graphql(
+        UPDATE_PRIVATE_METADATA_MUTATION % "Address", variables, permissions=None
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_update_private_metadata_for_customer_address_by_staff_with_perm(
+    staff_api_client, customer_user, permission_manage_users
+):
+    # given
+    address = customer_user.addresses.first()
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    # when
+    response = execute_update_private_metadata_for_item(
+        staff_api_client,
+        permission_manage_users,
+        address_id,
+        "Address",
+        value="NewMetaValue",
+    )
+
+    # then
+    assert item_contains_proper_private_metadata(
+        response["data"]["updatePrivateMetadata"]["item"],
+        address,
+        address_id,
+        value="NewMetaValue",
+    )
+
+
+def test_update_private_metadata_for_customer_address_by_app_with_perm(
+    app_api_client, customer_user, permission_manage_users
+):
+    # given
+    address = customer_user.addresses.first()
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    # when
+    response = execute_update_private_metadata_for_item(
+        app_api_client,
+        permission_manage_users,
+        address_id,
+        "Address",
+        value="NewMetaValue",
+    )
+
+    # then
+    assert item_contains_proper_private_metadata(
+        response["data"]["updatePrivateMetadata"]["item"],
+        address,
+        address_id,
+        value="NewMetaValue",
+    )
+
+
+def test_update_private_metadata_for_address_by_non_logged_user(
+    api_client, customer_user
+):
+    # given
+    address = customer_user.addresses.first()
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+    variables = {
+        "id": address_id,
+        "input": [{"key": PRIVATE_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = api_client.post_graphql(
+        UPDATE_PRIVATE_METADATA_MUTATION % "Address", variables, permissions=None
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_update_private_metadata_for_staff_address_by_staff_with_perm(
+    staff_api_client, address, staff_users, permission_manage_staff
+):
+    # given
+    user = staff_users[-1]
+    user.addresses.add(address)
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    # when
+    response = execute_update_private_metadata_for_item(
+        staff_api_client,
+        permission_manage_staff,
+        address_id,
+        "Address",
+        value="NewMetaValue",
+    )
+
+    # then
+    assert item_contains_proper_private_metadata(
+        response["data"]["updatePrivateMetadata"]["item"],
+        address,
+        address_id,
+        value="NewMetaValue",
+    )
+
+
+def test_update_private_metadata_for_staff_address_by_staff_without_perm(
+    staff_api_client, address, staff_users
+):
+    # given
+    user = staff_users[-1]
+    user.addresses.add(address)
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    variables = {
+        "id": address_id,
+        "input": [{"key": PRIVATE_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = staff_api_client.post_graphql(
+        UPDATE_PRIVATE_METADATA_MUTATION % "Address", variables, permissions=None
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_update_private_metadata_for_staff_address_by_customer(
+    user_api_client, address, staff_users
+):
+    # given
+    user = staff_users[-1]
+    user.addresses.add(address)
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    variables = {
+        "id": address_id,
+        "input": [{"key": PRIVATE_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = user_api_client.post_graphql(
+        UPDATE_PRIVATE_METADATA_MUTATION % "Address", variables, permissions=None
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_update_private_metadata_for_staff_address_by_app_with_perm(
+    app_api_client, staff_user, address, permission_manage_staff
+):
+    # given
+    staff_user.addresses.add(address)
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    variables = {
+        "id": address_id,
+        "input": [{"key": PRIVATE_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = app_api_client.post_graphql(
+        UPDATE_PRIVATE_METADATA_MUTATION % "Address",
+        variables,
+        permissions=[permission_manage_staff],
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_update_private_metadata_for_staff_address_by_app_without_perm(
+    app_api_client, staff_user, address
+):
+    # given
+    staff_user.addresses.add(address)
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    variables = {
+        "id": address_id,
+        "input": [{"key": PRIVATE_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = app_api_client.post_graphql(
+        UPDATE_PRIVATE_METADATA_MUTATION % "Address", variables
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_update_private_metadata_for_warehouse_address_by_staff(
+    staff_api_client, warehouse, permission_manage_staff
+):
+    # given
+    address = warehouse.address
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    variables = {
+        "id": address_id,
+        "input": [{"key": PRIVATE_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = staff_api_client.post_graphql(
+        UPDATE_PRIVATE_METADATA_MUTATION % "Address",
+        variables,
+        permissions=[permission_manage_staff],
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_update_private_metadata_for_site_settings_address_by_staff(
+    staff_api_client, site_settings, address, permission_manage_staff
+):
+    # given
+    site_settings.company_address = address
+    site_settings.save(update_fields=["company_address"])
+
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    variables = {
+        "id": address_id,
+        "input": [{"key": PRIVATE_KEY, "value": "NewMetaValue"}],
+    }
+
+    # when
+    response = staff_api_client.post_graphql(
+        UPDATE_PRIVATE_METADATA_MUTATION % "Address",
+        variables,
+        permissions=[permission_manage_staff],
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+DELETE_PRIVATE_METADATA_MUTATION = """
+mutation DeletePrivateMetadata($id: ID!, $keys: [String!]!) {
+    deletePrivateMetadata(
+        id: $id
+        keys: $keys
+    ) {
+        errors{
+            field
+            code
+        }
+        item {
+            privateMetadata{
+                key
+                value
+            }
+            ...on %s{
+                id
+            }
+        }
+    }
+}
+"""
+
+
+def execute_clear_private_metadata_for_item(
+    client,
+    permissions,
+    item_id,
+    item_type,
+    key=PRIVATE_KEY,
+):
+    variables = {
+        "id": item_id,
+        "keys": [key],
+    }
+
+    response = client.post_graphql(
+        DELETE_PRIVATE_METADATA_MUTATION % item_type,
+        variables,
+        permissions=[permissions] if permissions else None,
+    )
+    response = get_graphql_content(response)
+    return response
+
+
+def execute_clear_private_metadata_for_multiple_items(
+    client, permissions, item_id, item_type, key=PUBLIC_KEY, key2=PUBLIC_KEY2
 ):
     variables = {
         "id": item_id,
@@ -3739,16 +4298,19 @@ def test_delete_private_metadata_for_non_exist_item(
     assert errors[0]["code"] == MetadataErrorCode.NOT_FOUND.name
 
 
-def test_delete_private_metadata_for_item_without_meta(api_client, address):
+def test_delete_private_metadata_for_item_without_meta(
+    api_client, permission_group_manage_users
+):
     # given
-    assert not issubclass(type(address), ModelWithMetadata)
-    address_id = graphene.Node.to_global_id("Address", address.pk)
+    group = permission_group_manage_users
+    assert not issubclass(type(group), ModelWithMetadata)
+    group_id = graphene.Node.to_global_id("Group", group.pk)
 
     # when
     # We use "User" type inside mutation for valid graphql query with fragment
     # without this we are not able to reuse DELETE_PRIVATE_METADATA_MUTATION
     response = execute_clear_private_metadata_for_item(
-        api_client, None, address_id, "User"
+        api_client, None, group_id, "User"
     )
 
     # then
@@ -3927,6 +4489,236 @@ def test_delete_private_metadata_for_sale(
     )
 
 
+def test_delete_private_metadata_for_customer_address_as_staff(
+    staff_api_client, permission_manage_users, customer_user
+):
+    # given
+    address = customer_user.addresses.first()
+    address.store_value_in_private_metadata({PRIVATE_KEY: PRIVATE_VALUE})
+    address.save(update_fields=["private_metadata"])
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    # when
+    response = execute_clear_private_metadata_for_item(
+        staff_api_client, permission_manage_users, address_id, "Address"
+    )
+
+    # then
+    assert item_without_private_metadata(
+        response["data"]["deletePrivateMetadata"]["item"], address, address_id
+    )
+
+
+def test_delete_private_metadata_for_customer_address_as_app(
+    app_api_client, permission_manage_users, customer_user
+):
+    # given
+    address = customer_user.addresses.first()
+    address.store_value_in_private_metadata({PRIVATE_KEY: PRIVATE_VALUE})
+    address.save(update_fields=["private_metadata"])
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    # when
+    response = execute_clear_private_metadata_for_item(
+        app_api_client, permission_manage_users, address_id, "Address"
+    )
+
+    # then
+    assert item_without_private_metadata(
+        response["data"]["deletePrivateMetadata"]["item"], address, address_id
+    )
+
+
+def test_delete_private_metadata_for_staff_address_as_staff(
+    staff_api_client, address, permission_manage_staff
+):
+    # given
+    staff_user = staff_api_client.user
+    staff_user.addresses.add(address)
+    address.store_value_in_private_metadata({PRIVATE_KEY: PRIVATE_VALUE})
+    address.save(update_fields=["private_metadata"])
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    # when
+    response = execute_clear_private_metadata_for_item(
+        staff_api_client, permission_manage_staff, address_id, "Address"
+    )
+
+    # then
+    assert item_without_private_metadata(
+        response["data"]["deletePrivateMetadata"]["item"], address, address_id
+    )
+
+
+def test_delete_private_metadata_for_staff_address_as_app(
+    app_api_client, staff_user, address, permission_manage_staff
+):
+    # given
+    staff_user.addresses.add(address)
+    address.store_value_in_private_metadata({PRIVATE_KEY: PRIVATE_VALUE})
+    address.save(update_fields=["private_metadata"])
+    variables = {
+        "id": graphene.Node.to_global_id("Address", address.pk),
+        "keys": [PRIVATE_KEY],
+    }
+
+    # when
+    response = app_api_client.post_graphql(
+        DELETE_PRIVATE_METADATA_MUTATION % "Address",
+        variables,
+        permissions=[permission_manage_staff],
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_delete_private_metadata_for_myself_address_as_staff_no_permission(
+    staff_api_client, address, permission_manage_users
+):
+    # given
+    staff = staff_api_client.user
+    staff.addresses.add(address)
+    staff.store_value_in_private_metadata({PRIVATE_KEY: PRIVATE_VALUE})
+    staff.save(update_fields=["private_metadata"])
+    variables = {
+        "id": graphene.Node.to_global_id("Address", address.pk),
+        "keys": [PRIVATE_KEY],
+    }
+
+    # when
+    response = staff_api_client.post_graphql(
+        DELETE_PRIVATE_METADATA_MUTATION % "Address",
+        variables,
+        permissions=[permission_manage_users],
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_delete_public_metadata_for_customer_address_as_staff(
+    staff_api_client, permission_manage_users, customer_user
+):
+    # given
+    address = customer_user.addresses.first()
+    address.store_value_in_metadata({PUBLIC_KEY: PUBLIC_VALUE})
+    address.save(update_fields=["metadata"])
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    # when
+    response = execute_clear_public_metadata_for_item(
+        staff_api_client, permission_manage_users, address_id, "Address"
+    )
+
+    # then
+    assert item_without_public_metadata(
+        response["data"]["deleteMetadata"]["item"], address, address_id
+    )
+
+
+def test_delete_public_metadata_for_customer_address_as_app(
+    app_api_client, permission_manage_users, customer_user
+):
+    # given
+    address = customer_user.addresses.first()
+    address.store_value_in_metadata({PUBLIC_KEY: PUBLIC_VALUE})
+    address.save(update_fields=["metadata"])
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    # when
+    response = execute_clear_public_metadata_for_item(
+        app_api_client, permission_manage_users, address_id, "Address"
+    )
+
+    # then
+    assert item_without_public_metadata(
+        response["data"]["deleteMetadata"]["item"], address, address_id
+    )
+
+
+def test_delete_public_metadata_for_staff_address_as_another_staff(
+    staff_api_client, staff_users, address, permission_manage_staff
+):
+    # given
+    staff_user = staff_users[-1]
+    staff_user.addresses.add(address)
+    address.store_value_in_metadata({PUBLIC_KEY: PUBLIC_VALUE})
+    address.save(update_fields=["metadata"])
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    # when
+    response = execute_clear_public_metadata_for_item(
+        staff_api_client, permission_manage_staff, address_id, "Address"
+    )
+
+    # then
+    assert item_without_public_metadata(
+        response["data"]["deleteMetadata"]["item"], address, address_id
+    )
+
+
+def test_delete_public_metadata_for_staff_address_as_staff(staff_api_client, address):
+    # given
+    staff_user = staff_api_client.user
+    staff_user.addresses.add(address)
+    address.store_value_in_metadata({PUBLIC_KEY: PUBLIC_VALUE})
+    address.save(update_fields=["metadata"])
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    # when
+    response = execute_clear_public_metadata_for_item(
+        staff_api_client, None, address_id, "Address"
+    )
+
+    # then
+    assert item_without_public_metadata(
+        response["data"]["deleteMetadata"]["item"], address, address_id
+    )
+
+
+def test_delete_public_metadata_for_staff_address_as_app(
+    app_api_client, staff_user, address, permission_manage_staff
+):
+    # given
+    staff_user.addresses.add(address)
+    address.store_value_in_metadata({PUBLIC_KEY: PUBLIC_VALUE})
+    address.save(update_fields=["metadata"])
+    variables = {
+        "id": graphene.Node.to_global_id("Address", address.pk),
+        "keys": [PUBLIC_KEY],
+    }
+
+    # when
+    response = app_api_client.post_graphql(
+        DELETE_PUBLIC_METADATA_MUTATION % "Address",
+        variables,
+        permissions=[permission_manage_staff],
+    )
+
+    # then
+    assert_no_permission(response)
+
+
+def test_delete_public_metadata_for_myself_address(staff_api_client, address):
+    # given
+    staff = staff_api_client.user
+    staff.addresses.add(address)
+    address.store_value_in_metadata({PUBLIC_KEY: PUBLIC_VALUE})
+    staff.save(update_fields=["metadata"])
+    address_id = graphene.Node.to_global_id("Address", address.pk)
+
+    # when
+    response = execute_clear_public_metadata_for_item(
+        staff_api_client, None, address_id, "Address"
+    )
+
+    # then
+    assert item_without_public_metadata(
+        response["data"]["deleteMetadata"]["item"], address, address_id
+    )
+
+
 def test_add_public_metadata_for_voucher(
     staff_api_client, permission_manage_discounts, voucher
 ):
diff --git a/saleor/graphql/meta/types.py b/saleor/graphql/meta/types.py
index 2c3014ee30c..fbc54da1f9f 100644
--- a/saleor/graphql/meta/types.py
+++ b/saleor/graphql/meta/types.py
@@ -43,54 +43,61 @@ def _filter_metadata(metadata, keys):
     return {key: value for key, value in metadata.items() if key in keys}
 
 
+class MetadataDescription:
+    PRIVATE_METADATA = (
+        "List of private metadata items. Requires staff permissions to access."
+    )
+    PRIVATE_METAFIELD = (
+        "A single key from private metadata. "
+        "Requires staff permissions to access.\n\n"
+        "Tip: Use GraphQL aliases to fetch multiple keys."
+    )
+    PRIVATE_METAFIELDS = (
+        "Private metadata. Requires staff permissions to access. "
+        "Use `keys` to control which fields you want to include. "
+        "The default is to include everything."
+    )
+    METADATA = "List of public metadata items. Can be accessed without permissions."
+    METAFIELD = (
+        "A single key from public metadata.\n\n"
+        "Tip: Use GraphQL aliases to fetch multiple keys."
+    )
+    METAFIELDS = (
+        "Public metadata. Use `keys` to control which fields you want to include. "
+        "The default is to include everything."
+    )
+
+
 class ObjectWithMetadata(graphene.Interface):
     private_metadata = NonNullList(
         MetadataItem,
         required=True,
-        description=(
-            "List of private metadata items. Requires staff permissions to access."
-        ),
+        description=MetadataDescription.PRIVATE_METADATA,
     )
     private_metafield = graphene.String(
         args={"key": graphene.NonNull(graphene.String)},
         description=(
-            "A single key from private metadata. "
-            "Requires staff permissions to access.\n\n"
-            "Tip: Use GraphQL aliases to fetch multiple keys."
-            + ADDED_IN_33
-            + PREVIEW_FEATURE
+            MetadataDescription.PRIVATE_METAFIELD + ADDED_IN_33 + PREVIEW_FEATURE
         ),
     )
     private_metafields = Metadata(
         args={"keys": NonNullList(graphene.String)},
         description=(
-            "Private metadata. Requires staff permissions to access. "
-            "Use `keys` to control which fields you want to include. "
-            "The default is to include everything." + ADDED_IN_33 + PREVIEW_FEATURE
+            MetadataDescription.PRIVATE_METAFIELDS + ADDED_IN_33 + PREVIEW_FEATURE
         ),
     )
     metadata = NonNullList(
         MetadataItem,
         required=True,
-        description=(
-            "List of public metadata items. Can be accessed without permissions."
-        ),
+        description=(MetadataDescription.METADATA),
     )
     metafield = graphene.String(
         args={"key": graphene.NonNull(graphene.String)},
-        description=(
-            "A single key from public metadata.\n\n"
-            "Tip: Use GraphQL aliases to fetch multiple keys."
-            + ADDED_IN_33
-            + PREVIEW_FEATURE
-        ),
+        description=(MetadataDescription.METAFIELD + ADDED_IN_33 + PREVIEW_FEATURE),
     )
     metafields = Metadata(
         args={"keys": NonNullList(graphene.String)},
-        description=(
-            "Public metadata. Use `keys` to control which fields you want to include. "
-            "The default is to include everything." + ADDED_IN_33 + PREVIEW_FEATURE
-        ),
+        description=(MetadataDescription.METAFIELDS + ADDED_IN_33 + PREVIEW_FEATURE),
     )
 
     @staticmethod
diff --git a/saleor/graphql/schema.graphql b/saleor/graphql/schema.graphql
index 3582f7b31d0..77c1754f012 100644
--- a/saleor/graphql/schema.graphql
+++ b/saleor/graphql/schema.graphql
@@ -3076,8 +3076,66 @@ type Warehouse implements Node & ObjectWithMetadata {
 }
 
 """Represents user address data."""
-type Address implements Node {
+type Address implements Node & ObjectWithMetadata {
   id: ID!
+
+  """
+  List of private metadata items. Requires staff permissions to access.
+  
+  Added in Saleor 3.10.
+  
+  Note: this API is currently in Feature Preview and can be subject to changes at later point.
+  """
+  privateMetadata: [MetadataItem!]!
+
+  """
+  A single key from private metadata. Requires staff permissions to access.
+  
+  Tip: Use GraphQL aliases to fetch multiple keys.
+  
+  Added in Saleor 3.10.
+  
+  Note: this API is currently in Feature Preview and can be subject to changes at later point.
+  """
+  privateMetafield(key: String!): String
+
+  """
+  Private metadata. Requires staff permissions to access. Use `keys` to control which fields you want to include. The default is to include everything.
+  
+  Added in Saleor 3.10.
+  
+  Note: this API is currently in Feature Preview and can be subject to changes at later point.
+  """
+  privateMetafields(keys: [String!]): Metadata
+
+  """
+  List of public metadata items. Can be accessed without permissions.
+  
+  Added in Saleor 3.10.
+  
+  Note: this API is currently in Feature Preview and can be subject to changes at later point.
+  """
+  metadata: [MetadataItem!]!
+
+  """
+  A single key from public metadata.
+  
+  Tip: Use GraphQL aliases to fetch multiple keys.
+  
+  Added in Saleor 3.10.
+  
+  Note: this API is currently in Feature Preview and can be subject to changes at later point.
+  """
+  metafield(key: String!): String
+
+  """
+  Public metadata. Use `keys` to control which fields you want to include. The default is to include everything.
+  
+  Added in Saleor 3.10.
+  
+  Note: this API is currently in Feature Preview and can be subject to changes at later point.
+  """
+  metafields(keys: [String!]): Metadata
   firstName: String!
   lastName: String!
   companyName: String!
diff --git a/saleor/payment/gateways/np_atobarai/tests/conftest.py b/saleor/payment/gateways/np_atobarai/tests/conftest.py
index ee22b649022..6cdbaf0eb76 100644
--- a/saleor/payment/gateways/np_atobarai/tests/conftest.py
+++ b/saleor/payment/gateways/np_atobarai/tests/conftest.py
@@ -97,6 +97,8 @@ def np_address_data():
         city_area="本宿",
         street_address_1="2-16-3",
         street_address_2="",
+        metadata={},
+        private_metadata={},
     )
 
 
diff --git a/saleor/payment/interface.py b/saleor/payment/interface.py
index 806cba34297..1e8dc25b68d 100644
--- a/saleor/payment/interface.py
+++ b/saleor/payment/interface.py
@@ -79,6 +79,8 @@ class AddressData:
     country: str
     country_area: str
     phone: str
+    metadata: Optional[dict]
+    private_metadata: Optional[dict]
 
 
 class StorePaymentMethodEnum(str, Enum):
diff --git a/saleor/tests/fixtures.py b/saleor/tests/fixtures.py
index 31be5af8e90..cc42e8bb516 100644
--- a/saleor/tests/fixtures.py
+++ b/saleor/tests/fixtures.py
@@ -4723,6 +4723,8 @@ def dummy_address_data(address):
         country=address.country,
         country_area=address.country_area,
         phone=address.phone,
+        metadata=address.metadata,
+        private_metadata=address.private_metadata,
     )
 
 


Code review comments:
diff_hunk: @@ -22,6 +22,7 @@ All notable, unreleased changes to this project will be documented in this file.
 - Extend invoice object types with `Order` references - #11505 by @przlada
   - Add `Invoice.order` field
   - Add `InvoiceRequested.order`, `InvoiceDeleted.order` and `InvoiceSent.order` fields
+- Add support for metadata for Address model - #11701 by @IKarbowiakg
path: CHANGELOG.md
text: @user1:
typo :p 
```suggestion
- Add support for metadata for Address model - #11701 by @author
```