Repository: shlinkio/shlink (PHP)

Issue:
click delete visits in short URLS - stats - option, the visits were clear to zero, but it still shows number of stats on the overview panel and visits on the list of short URLS
### Shlink version

4.2.5

### PHP version

8.2.25

### How do you serve Shlink

Self-hosted nginx

### Database engine

MariaDB

### Database version

10.11.6

### Current behavior

I deleted all the access data in the leftmost option in the short link data panel. When I clicked on the short link data panel alone, it showed 0. However, when I went back to the overview or list of short URLs to check the access data, I found that there was no change. It still showed the original access data.

### Expected behavior

The access data of the short link seen from all locations should be cleared

### Minimum steps to reproduce

already made it clear 


Discussion: Yep, I can reproduce. I'll take a look.
I think it's very likely related with the changes introduced here https://github.com/shlinkio/shlink/pull/2074

That PR introduced a way to track visits counts separately than the actual visits table, to improve performance when only checking how many visits exist.

When deleting visits, it is apparently not cleaning the relevant entries there. I'll check why.



Fix (patch):
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e9555e54..a810fdc17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,7 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
 * *Nothing*
 
 ### Fixed
-* *Nothing*
+* [#2264](https://github.com/shlinkio/shlink/issues/2264) Fix visits counts not being deleted when deleting short URL or orphan visits.
 
 
 ## [4.2.5] - 2024-11-03
diff --git a/module/Core/src/Visit/Repository/VisitDeleterRepository.php b/module/Core/src/Visit/Repository/VisitDeleterRepository.php
index 94c570910..425dd88a7 100644
--- a/module/Core/src/Visit/Repository/VisitDeleterRepository.php
+++ b/module/Core/src/Visit/Repository/VisitDeleterRepository.php
@@ -6,15 +6,28 @@
 
 use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
 use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
+use Shlinkio\Shlink\Core\Visit\Entity\OrphanVisitsCount;
+use Shlinkio\Shlink\Core\Visit\Entity\ShortUrlVisitsCount;
 use Shlinkio\Shlink\Core\Visit\Entity\Visit;
 
 /** @extends EntitySpecificationRepository<Visit> */
 class VisitDeleterRepository extends EntitySpecificationRepository implements VisitDeleterRepositoryInterface
 {
     public function deleteShortUrlVisits(ShortUrl $shortUrl): int
+    {
+        return $this->getEntityManager()->wrapInTransaction(function () use ($shortUrl): int {
+            $this->deleteByShortUrl(ShortUrlVisitsCount::class, $shortUrl);
+            return $this->deleteByShortUrl(Visit::class, $shortUrl);
+        });
+    }
+
+    /**
+     * @param class-string<Visit | ShortUrlVisitsCount> $entityName
+     */
+    private function deleteByShortUrl(string $entityName, ShortUrl $shortUrl): int
     {
         $qb = $this->getEntityManager()->createQueryBuilder();
-        $qb->delete(Visit::class, 'v')
+        $qb->delete($entityName, 'v')
            ->where($qb->expr()->eq('v.shortUrl', ':shortUrl'))
            ->setParameter('shortUrl', $shortUrl);
 
@@ -23,10 +36,15 @@ public function deleteShortUrlVisits(ShortUrl $shortUrl): int
 
     public function deleteOrphanVisits(): int
     {
-        $qb = $this->getEntityManager()->createQueryBuilder();
-        $qb->delete(Visit::class, 'v')
-           ->where($qb->expr()->isNull('v.shortUrl'));
+        $em = $this->getEntityManager();
+        return $em->wrapInTransaction(function () use ($em): int {
+            $em->createQueryBuilder()->delete(OrphanVisitsCount::class, 'v')->getQuery()->execute();
 
-        return $qb->getQuery()->execute();
+            $qb = $em->createQueryBuilder();
+            $qb->delete(Visit::class, 'v')
+               ->where($qb->expr()->isNull('v.shortUrl'));
+
+            return $qb->getQuery()->execute();
+        });
     }
 }