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:
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Subscription;
|
||||
|
||||
use App\Config\Repository\SiteConfigRepository;
|
||||
use App\Payment\Entity\Payment;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Exception\AppException;
|
||||
use App\Subscription\Entity\ClinicSubscription;
|
||||
use App\Subscription\Repository\ClinicSubscriptionRepository;
|
||||
use App\Subscription\Repository\SubscriptionPeriodRepository;
|
||||
use App\Subscription\Repository\SubscriptionPlanRepository;
|
||||
use App\Subscription\Service\SubscriptionService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Revoking undoes an admin grant. A payment-backed subscription is out of scope:
|
||||
* deleting it would leave the sales report with a payment that owns nothing.
|
||||
*/
|
||||
class RevokeSubscriptionTest extends TestCase
|
||||
{
|
||||
private ?ClinicSubscription $removed = null;
|
||||
|
||||
private function service(?ClinicSubscription $found): SubscriptionService
|
||||
{
|
||||
$subscriptionRepo = $this->createStub(ClinicSubscriptionRepository::class);
|
||||
$subscriptionRepo->method('findByUuid')->willReturn($found);
|
||||
$subscriptionRepo->method('remove')->willReturnCallback(function (ClinicSubscription $s): void {
|
||||
$this->removed = $s;
|
||||
});
|
||||
|
||||
return new SubscriptionService(
|
||||
$subscriptionRepo,
|
||||
$this->createStub(SubscriptionPlanRepository::class),
|
||||
$this->createStub(SubscriptionPeriodRepository::class),
|
||||
$this->createStub(SiteConfigRepository::class),
|
||||
);
|
||||
}
|
||||
|
||||
public function testGrantedSubscriptionIsRemoved(): void
|
||||
{
|
||||
$subscription = $this->createStub(ClinicSubscription::class);
|
||||
$subscription->method('getPayment')->willReturn(null);
|
||||
|
||||
$this->service($subscription)->revoke('sub-uuid');
|
||||
|
||||
$this->assertSame($subscription, $this->removed);
|
||||
}
|
||||
|
||||
public function testUnknownUuidIsRejected(): void
|
||||
{
|
||||
try {
|
||||
$this->service(null)->revoke('missing-uuid');
|
||||
$this->fail('Expected AppException');
|
||||
} catch (AppException $e) {
|
||||
$this->assertSame(ErrorCodes::ERR_SUBSCRIPTION_NOT_FOUND, $e->getErrorCode());
|
||||
$this->assertSame(404, $e->getHttpStatus());
|
||||
}
|
||||
|
||||
$this->assertNull($this->removed);
|
||||
}
|
||||
|
||||
public function testPaidSubscriptionIsNotRemoved(): void
|
||||
{
|
||||
$subscription = $this->createStub(ClinicSubscription::class);
|
||||
$subscription->method('getPayment')->willReturn($this->createStub(Payment::class));
|
||||
|
||||
try {
|
||||
$this->service($subscription)->revoke('sub-uuid');
|
||||
$this->fail('Expected AppException');
|
||||
} catch (AppException $e) {
|
||||
$this->assertSame(ErrorCodes::ERR_CONFLICT_001, $e->getErrorCode());
|
||||
$this->assertSame(409, $e->getHttpStatus());
|
||||
}
|
||||
|
||||
$this->assertNull($this->removed);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user