feat(subscription): allow admin to delete a granted subscription

Granting stacks a new row and extends the existing expiry, so a mistaken
grant had no way back: the report tab was read-only and no endpoint deleted
a subscription (only DELETE .../subscription/period/{uuid}, a different
resource).

Adds DELETE /api/v1/admin/subscription/{uuid}. Payment-backed subscriptions
are refused with 409 — deleting one would leave the sales report with a
payment that owns nothing; refunds are the correct path there.

The report tab gets a per-row delete with a confirm dialog, plus a button
that jumps to the grant tab so add and remove live in the same place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-19 21:59:12 +03:30
co-authored by Claude Opus 5
parent 5eb3bd586f
commit a1584c3296
6 changed files with 239 additions and 3 deletions
@@ -330,6 +330,25 @@ class SubscriptionController extends BaseController
return $this->success($subscription->toArray(), 201);
}
/**
* حذف اشتراک — برگرداندنِ اعطای اشتباه.
*
* `grant` هر بار ردیف تازه می‌سازد و روی انقضای قبلی سوار می‌شود، پس بدون این
* مسیر، یک کلیک اضافی در تب اعطا راه برگشت نداشت.
*/
#[Route('/api/v1/admin/subscription/{uuid}', methods: ['DELETE'])]
#[IsGranted('ROLE_ADMIN')]
public function adminRevoke(string $uuid): JsonResponse
{
try {
$this->subscriptionService->revoke($uuid);
} catch (AppException $e) {
return $this->error($e->getErrorCode(), $e->getMessage(), $e->getHttpStatus());
}
return $this->success(null);
}
/**
* اشتراک فعالِ یک مقصد — پیش از اعطا، تا ادمین downgrade را ناخواسته انجام ندهد.
*/
@@ -170,6 +170,31 @@ class SubscriptionService
return $subscription;
}
/**
* حذف اشتراک توسط ادمین.
*
* فقط اشتراکِ اعطایی یا تریال حذف می‌شود: اشتراکِ متصل به پرداخت سند مالی دارد و
* پاک شدنش گزارش فروش را با ردیف پرداختِ بی‌اشتراک ناسازگار می‌کند. مسیر درستِ
* آن استرداد است (`deleteByPayment`).
*/
public function revoke(string $uuid): void
{
$subscription = $this->subscriptionRepo->findByUuid($uuid);
if ($subscription === null) {
throw new AppException(ErrorCodes::ERR_SUBSCRIPTION_NOT_FOUND, null, 404);
}
if ($subscription->getPayment() !== null) {
throw new AppException(
ErrorCodes::ERR_CONFLICT_001,
'اشتراک پرداخت‌شده حذف نمی‌شود؛ از مسیر استرداد وجه اقدام کنید',
409,
);
}
$this->subscriptionRepo->remove($subscription);
}
/** حذف اشتراکِ ساخته‌شده از یک پرداخت (هنگام استرداد/برگشت وجه). */
public function deleteByPayment(\App\Payment\Entity\Payment $payment): void
{