php-commerce/Controllers/Api/EntitlementApiController.php

382 lines
13 KiB
PHP
Raw Permalink Normal View History

<?php
declare(strict_types=1);
namespace Core\Mod\Commerce\Controllers\Api;
use Core\Front\Controller;
use Core\Mod\Commerce\Models\Subscription;
use Core\Mod\Commerce\Services\SubscriptionService;
use Core\Tenant\Models\Workspace;
use Core\Tenant\Services\EntitlementService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Provisioning API controller for entitlement lifecycle management.
*
* Provides endpoints for creating, inspecting, suspending, unsuspending,
* cancelling, and renewing entitlements (workspace package subscriptions).
* Authenticated via Bearer token through the CommerceApiAuth middleware.
*
* Entitlements represent the link between a workspace, a package, and
* its active subscription. Each entitlement is identified by its
* subscription ID.
*/
class EntitlementApiController extends Controller
{
public function __construct(
protected EntitlementService $entitlements,
protected SubscriptionService $subscriptionService,
) {}
/**
* Provision a new entitlement for a workspace.
*
* POST /api/v1/provisioning/entitlements
*
* Request body:
* - workspace_id (required): The workspace to provision
* - package_code (required): The entitlement package code
* - billing_cycle (optional): "monthly" or "yearly" (default: "monthly")
* - gateway (optional): Payment gateway identifier
* - gateway_subscription_id (optional): External subscription reference
*/
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'workspace_id' => 'required|integer|exists:workspaces,id',
'package_code' => 'required|string',
'billing_cycle' => 'in:monthly,yearly',
'gateway' => 'nullable|string',
'gateway_subscription_id' => 'nullable|string',
]);
$workspace = Workspace::findOrFail($validated['workspace_id']);
$billingCycle = $validated['billing_cycle'] ?? 'monthly';
try {
$result = DB::transaction(function () use ($workspace, $validated, $billingCycle) {
// Provision the package entitlements
$workspacePackage = $this->entitlements->provisionPackage(
$workspace,
$validated['package_code'],
[
'source' => 'provisioning_api',
'gateway' => $validated['gateway'] ?? null,
]
);
// Create the subscription record
$subscription = $this->subscriptionService->create(
$workspacePackage,
$billingCycle,
$validated['gateway'] ?? null,
$validated['gateway_subscription_id'] ?? null
);
return [
'workspace_package' => $workspacePackage,
'subscription' => $subscription,
];
});
Log::info('Entitlement provisioned via API', [
'workspace_id' => $workspace->id,
'package_code' => $validated['package_code'],
'subscription_id' => $result['subscription']->id,
]);
return response()->json([
'data' => [
'id' => $result['subscription']->id,
'workspace_id' => $workspace->id,
'package_code' => $validated['package_code'],
'billing_cycle' => $billingCycle,
'status' => $result['subscription']->status,
'current_period_start' => $result['subscription']->current_period_start?->toIso8601String(),
'current_period_end' => $result['subscription']->current_period_end?->toIso8601String(),
],
'message' => 'Entitlement provisioned successfully',
], 201);
} catch (\Exception $e) {
Log::error('Entitlement provisioning failed', [
'workspace_id' => $workspace->id,
'package_code' => $validated['package_code'],
'error' => $e->getMessage(),
]);
return response()->json([
'error' => 'provisioning_failed',
'message' => $e->getMessage(),
], 422);
}
}
/**
* Show entitlement details by subscription ID.
*
* GET /api/v1/provisioning/entitlements/{id}
*/
public function show(int $id): JsonResponse
{
$subscription = Subscription::with(['workspace', 'workspacePackage.package'])
->find($id);
if (! $subscription) {
return response()->json([
'error' => 'not_found',
'message' => 'Entitlement not found',
], 404);
}
return response()->json([
'data' => [
'id' => $subscription->id,
'workspace_id' => $subscription->workspace_id,
'package_code' => $subscription->workspacePackage?->package?->code,
'package_name' => $subscription->workspacePackage?->package?->name,
'status' => $subscription->status,
'billing_cycle' => $subscription->billing_cycle,
'gateway' => $subscription->gateway,
'current_period_start' => $subscription->current_period_start?->toIso8601String(),
'current_period_end' => $subscription->current_period_end?->toIso8601String(),
'cancelled_at' => $subscription->cancelled_at?->toIso8601String(),
'cancellation_reason' => $subscription->cancellation_reason,
'paused_at' => $subscription->paused_at?->toIso8601String(),
'ended_at' => $subscription->ended_at?->toIso8601String(),
],
]);
}
/**
* Suspend an entitlement (pause the subscription and restrict access).
*
* POST /api/v1/provisioning/entitlements/{id}/suspend
*/
public function suspend(Request $request, int $id): JsonResponse
{
$subscription = Subscription::find($id);
if (! $subscription) {
return response()->json([
'error' => 'not_found',
'message' => 'Entitlement not found',
], 404);
}
if (! $subscription->isActive()) {
return response()->json([
'error' => 'invalid_state',
'message' => "Cannot suspend entitlement in '{$subscription->status}' state",
], 422);
}
try {
$this->subscriptionService->pause($subscription, force: true);
if ($subscription->workspace) {
$reason = $request->get('reason', 'api_suspension');
$this->entitlements->suspendWorkspace($subscription->workspace, $reason);
}
Log::info('Entitlement suspended via API', [
'subscription_id' => $subscription->id,
'workspace_id' => $subscription->workspace_id,
]);
return response()->json([
'message' => 'Entitlement suspended successfully',
'data' => [
'id' => $subscription->id,
'status' => $subscription->fresh()->status,
],
]);
} catch (\Exception $e) {
return response()->json([
'error' => 'suspension_failed',
'message' => $e->getMessage(),
], 422);
}
}
/**
* Unsuspend an entitlement (unpause and restore access).
*
* POST /api/v1/provisioning/entitlements/{id}/unsuspend
*/
public function unsuspend(int $id): JsonResponse
{
$subscription = Subscription::find($id);
if (! $subscription) {
return response()->json([
'error' => 'not_found',
'message' => 'Entitlement not found',
], 404);
}
if (! $subscription->isPaused()) {
return response()->json([
'error' => 'invalid_state',
'message' => "Cannot unsuspend entitlement in '{$subscription->status}' state",
], 422);
}
try {
$this->subscriptionService->unpause($subscription);
if ($subscription->workspace) {
$this->entitlements->reactivateWorkspace($subscription->workspace, 'api_unsuspension');
}
Log::info('Entitlement unsuspended via API', [
'subscription_id' => $subscription->id,
'workspace_id' => $subscription->workspace_id,
]);
return response()->json([
'message' => 'Entitlement unsuspended successfully',
'data' => [
'id' => $subscription->id,
'status' => $subscription->fresh()->status,
],
]);
} catch (\Exception $e) {
return response()->json([
'error' => 'unsuspend_failed',
'message' => $e->getMessage(),
], 422);
}
}
/**
* Cancel an entitlement.
*
* POST /api/v1/provisioning/entitlements/{id}/cancel
*
* Request body:
* - reason (optional): Cancellation reason
* - immediately (optional): Whether to cancel immediately (default: false)
*/
public function cancel(Request $request, int $id): JsonResponse
{
$validated = $request->validate([
'reason' => 'nullable|string|max:500',
'immediately' => 'boolean',
]);
$subscription = Subscription::find($id);
if (! $subscription) {
return response()->json([
'error' => 'not_found',
'message' => 'Entitlement not found',
], 404);
}
if ($subscription->isCancelled() || $subscription->status === 'expired') {
return response()->json([
'error' => 'invalid_state',
'message' => 'Entitlement is already cancelled or expired',
], 422);
}
$immediately = $validated['immediately'] ?? false;
try {
$this->subscriptionService->cancel(
$subscription,
$validated['reason'] ?? null
);
if ($immediately) {
$this->subscriptionService->expire($subscription);
if ($subscription->workspace && $subscription->workspacePackage?->package) {
$this->entitlements->revokePackage(
$subscription->workspace,
$subscription->workspacePackage->package->code
);
}
}
Log::info('Entitlement cancelled via API', [
'subscription_id' => $subscription->id,
'workspace_id' => $subscription->workspace_id,
'immediately' => $immediately,
]);
$fresh = $subscription->fresh();
return response()->json([
'message' => $immediately
? 'Entitlement cancelled immediately'
: 'Entitlement will be cancelled at end of billing period',
'data' => [
'id' => $fresh->id,
'status' => $fresh->status,
'cancelled_at' => $fresh->cancelled_at?->toIso8601String(),
'ended_at' => $fresh->ended_at?->toIso8601String(),
],
]);
} catch (\Exception $e) {
return response()->json([
'error' => 'cancellation_failed',
'message' => $e->getMessage(),
], 422);
}
}
/**
* Renew an entitlement for another billing period.
*
* POST /api/v1/provisioning/entitlements/{id}/renew
*/
public function renew(int $id): JsonResponse
{
$subscription = Subscription::find($id);
if (! $subscription) {
return response()->json([
'error' => 'not_found',
'message' => 'Entitlement not found',
], 404);
}
if (! in_array($subscription->status, ['active', 'past_due'])) {
return response()->json([
'error' => 'invalid_state',
'message' => "Cannot renew entitlement in '{$subscription->status}' state",
], 422);
}
try {
$renewed = $this->subscriptionService->renew($subscription);
Log::info('Entitlement renewed via API', [
'subscription_id' => $renewed->id,
'workspace_id' => $renewed->workspace_id,
'new_period_end' => $renewed->current_period_end,
]);
return response()->json([
'message' => 'Entitlement renewed successfully',
'data' => [
'id' => $renewed->id,
'status' => $renewed->status,
'current_period_start' => $renewed->current_period_start?->toIso8601String(),
'current_period_end' => $renewed->current_period_end?->toIso8601String(),
],
]);
} catch (\Exception $e) {
return response()->json([
'error' => 'renewal_failed',
'message' => $e->getMessage(),
], 422);
}
}
}