Repository: saleor/saleor
Pull request: Add mutation for deleting account
I want to merge this change because resolve #4286  

<!-- Please mention all relevant issue numbers. -->

### Screenshots

<!-- If your changes affect the UI, providing "before" and "after" screenshots will
greatly reduce the amount of work needed to review your work. -->

### Pull Request Checklist

<!-- Please keep this section. It will make maintainer's life easier. -->

1. [ ] Privileged views and APIs are guarded by proper permission checks.
1. [ ] All visible strings are translated with proper context.
1. [ ] All data-formatting is locale-aware (dates, numbers, and so on).
1. [ ] Database queries are optimized and the number of queries is constant.
1. [ ] Database migration files are up to date.
1. [x] The changes are tested.
1. [ ] The code is documented (docstrings, project documentation).
1. [x] GraphQL schema and type definitions are up to date.
1. [x] Changes are mentioned in the changelog.


Problem statement:
Add mutation for removing user account
### What I'm trying to achieve
As a user of storefront I would like to remove my account. Right now it is only possible for staff/admin user accounts

### Describe a proposed solution
Add mutation for removing account by regular user

Hints from issue discussion:

Commit message: Add graphql schema and changelog

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 72693a82f3a..42f775ec61c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ All notable, unreleased changes to this project will be documented in this file.
 - Fix voucher limit value when checkbox unchecked - #4456 by @benekex2
 - New menu design - #4476 by @benekex2
 - Mutation for changing logged user first and last name - #4489 by @fowczarek
+- Add mutation for deleting account - #4494 by @fowczarek
 - New translations:
   - Greek
 - Fix searches and pickers - #4487 by @dominik-zeglen
diff --git a/saleor/graphql/account/mutations.py b/saleor/graphql/account/mutations.py
index f86954f3107..06a373ef4ea 100644
--- a/saleor/graphql/account/mutations.py
+++ b/saleor/graphql/account/mutations.py
@@ -297,6 +297,21 @@ def perform_mutation(cls, root, info, **data):
         return super().perform_mutation(root, info, **data)
 
 
+class AccountDelete(BaseMutation):
+    class Meta:
+        description = "Remove user account after email confirmation"
+
+    @classmethod
+    def check_permissions(cls, user):
+        return user.is_authenticated
+
+    @classmethod
+    def perform_mutation(cls, root, info, **data):
+        user = info.context.user
+        emails.send_account_delete_confirmation_email.delay(str(user.token), user.email)
+        return AccountDelete()
+
+
 class UserDelete(UserDeleteMixin, ModelDeleteMutation):
     class Meta:
         abstract = True
diff --git a/saleor/graphql/account/schema.py b/saleor/graphql/account/schema.py
index 0a1a812d15d..fa50eddb34d 100644
--- a/saleor/graphql/account/schema.py
+++ b/saleor/graphql/account/schema.py
@@ -8,6 +8,7 @@
 from .enums import CountryCodeEnum
 from .filters import CustomerFilter, StaffUserFilter
 from .mutations import (
+    AccountDelete,
     AccountUpdate,
     AddressCreate,
     AddressDelete,
@@ -111,6 +112,7 @@ class AccountMutations(graphene.ObjectType):
 
     logged_user_update = LoggedUserUpdate.Field()
     account_update = AccountUpdate.Field()
+    account_delete = AccountDelete.Field()
 
     staff_create = StaffCreate.Field()
     staff_delete = StaffDelete.Field()
diff --git a/saleor/graphql/schema.graphql b/saleor/graphql/schema.graphql
index c4aa7d09a5e..b301ca9d2bf 100644
--- a/saleor/graphql/schema.graphql
+++ b/saleor/graphql/schema.graphql
@@ -3,6 +3,10 @@ schema {
   mutation: Mutations
 }
 
+type AccountDelete {
+  errors: [Error!]
+}
+
 input AccountInput {
   firstName: String
   lastName: String
@@ -1667,6 +1671,7 @@ type Mutations {
   customerSetDefaultAddress(id: ID!, type: AddressTypeEnum!): CustomerSetDefaultAddress
   loggedUserUpdate(input: UserAddressInput!): LoggedUserUpdate
   accountUpdate(input: AccountInput!): AccountUpdate
+  accountDelete: AccountDelete
   staffCreate(input: StaffCreateInput!): StaffCreate
   staffDelete(id: ID!): StaffDelete
   staffBulkDelete(ids: [ID]!): StaffBulkDelete
diff --git a/tests/api/test_account.py b/tests/api/test_account.py
index 13bd3be104a..b39a14e7d94 100644
--- a/tests/api/test_account.py
+++ b/tests/api/test_account.py
@@ -938,6 +938,41 @@ def test_logged_customer_update_anonymous_user(api_client, query):
     assert_no_permission(response)
 
 
+ACCOUNT_DELETE_QUERY = """
+    mutation accountDelete {
+        accountDelete {
+            errors {
+                field
+                message
+            }
+        }
+    }
+"""
+
+
+@patch("saleor.account.emails.send_account_delete_confirmation_email.delay")
+def test_account_delete(send_account_delete_confirmation_email_mock, user_api_client):
+    user = user_api_client.user
+
+    response = user_api_client.post_graphql(ACCOUNT_DELETE_QUERY)
+    content = get_graphql_content(response)
+    data = content["data"]["accountDelete"]
+
+    assert not data["errors"]
+    send_account_delete_confirmation_email_mock.assert_called_once_with(
+        str(user.token), user.email
+    )
+
+
+@patch("saleor.account.emails.send_account_delete_confirmation_email.delay")
+def test_account_delete_anonymous_user(
+    send_account_delete_confirmation_email_mock, api_client
+):
+    response = api_client.post_graphql(ACCOUNT_DELETE_QUERY, {})
+    assert_no_permission(response)
+    send_account_delete_confirmation_email_mock.assert_not_called()
+
+
 @patch(
     "saleor.graphql.account.utils.account_events.staff_user_deleted_a_customer_event"
 )


Code review comments:
diff_hunk: @@ -938,6 +938,41 @@ def test_logged_customer_update_anonymous_user(api_client, query):
     assert_no_permission(response)
 
 
+ACCOUNT_DELETE_QUERY = """
+    mutation accountDelete {
+        accountDelete {
+            errors {
+                field
+                message
+            }
+        }
+    }
+"""
+
+
+@patch("saleor.account.emails.send_account_delete_confirmation_email.delay")
+def test_account_delete(send_account_delete_confirmation_email_mock, user_api_client):
+    user = user_api_client.user
+
+    response = user_api_client.post_graphql(ACCOUNT_DELETE_QUERY)
+    content = get_graphql_content(response)
+    data = content["data"]["accountDelete"]
+
+    assert not data["errors"]
+    send_account_delete_confirmation_email_mock.assert_called_once_with(
+        str(user.token), user.email
+    )
+
+
+@patch("saleor.account.emails.send_account_delete_confirmation_email.delay")
+def test_account_delete_anonymous_user(
path: tests/api/test_account.py
text: @user1:
```suggestion
def test_request_account_deletion_anonymous_user(
```

diff_hunk: @@ -297,6 +297,21 @@ def perform_mutation(cls, root, info, **data):
         return super().perform_mutation(root, info, **data)
 
 
+class AccountDelete(BaseMutation):
path: saleor/graphql/account/mutations.py
text: @user1:
This name can be confusing since this mutation doesn't delete the account yet. I would rename it to `AccountDeleteEmail`.

For every operation that requires an email confirmation, we'll need two mutations: one to send an email and the other actually perform the action once users click the link. But I think we'll do the refactor of that operation in a separate PR.

@user2:
`AccountDeleteEmail` is confusing me as well. What about `AccountRequestDelete`/`AccountRequestDeletion`?

@user1:
Right, `AccountRequestDeletion` sound good.

diff_hunk: @@ -938,6 +938,41 @@ def test_logged_customer_update_anonymous_user(api_client, query):
     assert_no_permission(response)
 
 
+ACCOUNT_DELETE_QUERY = """
+    mutation accountDelete {
+        accountDelete {
+            errors {
+                field
+                message
+            }
+        }
+    }
+"""
+
+
+@patch("saleor.account.emails.send_account_delete_confirmation_email.delay")
+def test_account_delete(send_account_delete_confirmation_email_mock, user_api_client):
path: tests/api/test_account.py
text: @user1:
```suggestion
def test_request_account_deletion(send_account_delete_confirmation_email_mock, user_api_client):
```