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); } } }