Deleting a Store can make its Orders unloadable, undeletable, and crash the site

Created on 14 April 2023, about 1 year ago
Updated 13 January 2024, 5 months ago

Describe your bug or feature request.

After deleting a store, loading an order throws. As deleting an entity needs the loaded entity, this can not be remedied.
(Am i misssing sth? Hard to imagine i'm the first one.)
While the order and cart list seem to go fine with that, i bumped on this crashing the group "all entities" tab.

Proposed resolution

As Order::getStore return type must not be null, so:
- Prevent store delete when orders exist.
- We can do this on the form level first, as described in #9, and this should prevent in most cases.
- And we must do it on the API level (eg via Store::access()), maybe in a followup.

If a bug, provide steps to reproduce it from a clean install.

- Add a store and an order (or cart)
- Delete the store
- Load the order

Exception1

TypeError Drupal\commerce\Context::__construct(): Argument #2 ($store) must be of type Drupal\commerce_store\Entity\StoreInterface, null given, called in modules/contrib/commerce/modules/order/src/OrderRefresh.php on line 171.

Exception2

Error: Call to a member function getTimezone() on null in Drupal\commerce_order\Entity\Order->getCalculationDate() (line 669 of modules/contrib/commerce/modules/order/src/Entity/Order.php).
Drupal\commerce_order\Entity\Order->getCalculationDate() (Line: 170)
Drupal\commerce_order\OrderRefresh->refresh() (Line: 84)
Drupal\commerce_order\OrderStorage->doOrderPreSave() (Line: 61)
Drupal\commerce_order\OrderStorage->invokeHook() (Line: 563)
Drupal\Core\Entity\EntityStorageBase->doPreSave() (Line: 756)
Drupal\Core\Entity\ContentEntityStorageBase->doPreSave() (Line: 517)
Drupal\Core\Entity\EntityStorageBase->save() (Line: 804)
Drupal\Core\Entity\Sql\SqlContentEntityStorage->save() (Line: 339)
Drupal\Core\Entity\EntityBase->save() (Line: 117)
Drupal\commerce_order\OrderStorage->postLoad() (Line: 353)
Drupal\Core\Entity\EntityStorageBase->loadMultiple() (Line: 296)
Drupal\Core\Entity\EntityStorageBase->load() (Line: 72)
Drupal\Core\Entity\Plugin\DataType\EntityReference->getTarget() (Line: 37)
Drupal\Core\TypedData\DataReferenceBase->getValue() (Line: 140)
Drupal\Core\Field\FieldItemBase->__get() (Line: 116)
Drupal\Core\Field\FieldItemList->__get() (Line: 100)
Drupal\group\Entity\GroupContent->getEntity() (Line: 163)
Drupal\group\Plugin\GroupContentEnablerBase->getContentLabel() (Line: 132)
Drupal\group\Entity\GroupContent->label() (Line: 242)
Drupal\Core\Entity\EntityBase->toLink() (Line: 113)
Drupal\group\Entity\Controller\GroupContentListBuilder->buildRow() (Line: 219)
Drupal\Core\Entity\EntityListBuilder->render() (Line: 125)
Drupal\group\Entity\Controller\GroupContentListBuilder->render() (Line: 23)
Drupal\Core\Entity\Controller\EntityListController->listing()
call_user_func_array() (Line: 123)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 580)
Drupal\Core\Render\Renderer->executeInRenderContext() (Line: 124)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext() (Line: 97)
Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}() (Line: 169)
Symfony\Component\HttpKernel\HttpKernel->handleRaw() (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle() (Line: 58)
Drupal\Core\StackMiddleware\Session->handle() (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle() (Line: 106)
Drupal\page_cache\StackMiddleware\PageCache->pass() (Line: 85)
Drupal\page_cache\StackMiddleware\PageCache->handle() (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle() (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle() (Line: 23)
Stack\StackedHttpKernel->handle() (Line: 718)
Drupal\Core\DrupalKernel->handle() (Line: 19)
require('/home/merlin/Code-Incubator/site-c4c-dev/web/index.php') (Line: 71)

🐛 Bug report
Status

Fixed

Version

2.0

Component

Store

Created by

🇩🇪Germany geek-merlin Freiburg, Germany

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @geek-merlin
  • 🇮🇱Israel jsacksick

    One problem with disallowing store deletion is that we'll get support requests / bug reports from people that do not understand why they can't properly uninstall Commerce, or delete a store they don't need, because of test orders.

    Perhaps we need code in the order refresh that just reset the store to the default store in case the referenced store no longer exist to prevent a crash? That could be a good first step I guess?

  • 🇩🇪Germany geek-merlin Freiburg, Germany

    > Perhaps we need code in the order refresh that just reset the store to the default store in case the referenced store no longer exist to prevent a crash? That could be a good first step I guess?

    Hmm. Deleting a store assigns its orders to a completely different store? In GDPR age this may get someone handcuffed ;-/.

  • 🇩🇪Germany geek-merlin Freiburg, Germany

    OK then a store delete must delete its orders.
    Looked a bit around. If we want to delete its orders on store deletion, we need a batch.
    Fortunately \Drupal\system\Form\PrepareModulesEntityUninstallForm does all that, we just need to override the redirect.

  • 🇺🇸United States lisastreeter

    I don't think we should automatically delete orders on store deletion. A better approach might be similar to Content Type deletion:

    Store name is used by 1,579 orders. You may not remove Store name until you have removed all of the Store name content.

    And then perhaps set up your Orders view so that you can select by Store and use a "Delete orders" bulk action. I think most sites in production would want to strictly block any order bulk-deletion functionality. Deleting draft orders is generally fine but unless you're exporting all your orders to an external management system, deleting Completed orders (with payments!) could be very bad.

  • 🇩🇪Germany geek-merlin Freiburg, Germany

    #5: Yes, this also makes sense.

    In this case we inspire from \Drupal\node\Form\NodeTypeDeleteConfirm and friends.
    10 lines, easy to adapt.
    Create \Drupal\commerce_store\Form\StoreDeleteForm as subclass of \Drupal\Core\Entity\EntityDeleteForm
    Copy adapted code from \Drupal\node\Form\NodeTypeDeleteConfirm
    Adjust handlers.form.delete in \Drupal\commerce_store\Entity\Store

    If maintainer signs this off, we can tag this novice.

  • 🇮🇱Israel jsacksick

    I completely agree with #5, you almost neve really want to delete orders... (unless these are test orders, or exported to a 3rd party system).

    I approve copying the node approach. but yeah it should I guess be more than just displaying a message, we should forbid the deletion of the store too.

  • 🇺🇸United States lisastreeter

    Yes, forbidding deletion with message indicating reason sounds good.

    May also want to look at implications for references from products, promotions, shipping methods (commerce shipping module, but also references more general.) Make sure that references to missing store entities don't break anything else.

  • 🇩🇪Germany geek-merlin Freiburg, Germany

    > I approve copying the node approach. but yeah it should I guess be more than just displaying a message, we should forbid the deletion of the store too.

    Yes this is that the form then does, by not calling parent::buildForm (so it does it on the form, not on the access level, which is good enough as a first step).

    Tagging novice for the approach described in #6.

  • 🇮🇱Israel jsacksick

    @geek-merlin: Sorry I meant forbidding the access at the API level as well, not at the form level only. Forbidding the deletion at the form level would be a nice first step I guess, at least this should solve 80% of the cases where an average user performs a store deletion without thinking it through.

  • 🇩🇪Germany geek-merlin Freiburg, Germany

    Update IS with agreed solution.

  • 🇪🇸Spain henkpotman

    I encountered this same error after deleting all old orders that were imported from an imported Drupal 7 Ubercart store. After I deleted the old orders, when visiting the Order page I encountered this same error message.

    In PhpMyadmin I found the old orders where still there in the commerce_order table. Next I emptied (truncated) the commerce_order table and this solved the problem. I can view the Order page again and all old orders were deleted.

  • Pipeline finished with Skipped
    6 months ago
    #66543
  • 🇩🇪Germany geek-merlin Freiburg, Germany

    Crosslinking an older dup.

  • 🇩🇪Germany geek-merlin Freiburg, Germany

    Untagging as of #10/#11

  • 🇩🇪Germany geek-merlin Freiburg, Germany

    Note: I ran again into this on a dev site.

    How to reproduce that even loading an order throws:
    vendor/bin/drush ev 'foreach (\Drupal\commerce_order\Entity\Order::loadMultiple() as $order) print $order->id();'

  • Open in Jenkins → Open on Drupal.org →
    Core: 9.5.x + Environment: PHP 8.0 & MySQL 5.7
    last update 6 months ago
    780 pass, 1 fail
  • Status changed to Needs review 6 months ago
  • 🇩🇪Germany geek-merlin Freiburg, Germany

    As @jsacksick said in #2:
    > Perhaps we need code in the order refresh that just reset the store to the default store in case the referenced store no longer exist to prevent a crash? That could be a good first step I guess?

    Rolled a trivial MR that does this.

    Patching my commerce with this (need to do a manual rebase), i can
    - access the product and cart page
    - run this cleanup script:

    vendor/bin/drush ev 'foreach (\Drupal\commerce_order\Entity\Order::loadMultiple(
    ) as $order) { if ($order->getStore()) {print "Checked ";} else {$order->delete(); print "DELETED ";} print $order->id() . "\n"; }'
    
  • Open in Jenkins → Open on Drupal.org →
    Core: 9.5.x + Environment: PHP 8.0 & MySQL 5.7
    last update 6 months ago
    789 pass
  • Open in Jenkins → Open on Drupal.org →
    Core: 9.5.x + Environment: PHP 8.0 & MySQL 5.7
    last update 6 months ago
    789 pass
  • 🇮🇱Israel jsacksick

    Merged your MR, thanks! Let's just do this for now...

  • Status changed to Fixed 6 months ago
  • Automatically closed - issue fixed for 2 weeks with no activity.

Production build 0.69.0 2024