Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.50% covered (warning)
87.50%
21 / 24
87.50% covered (warning)
87.50%
7 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
PlanController
87.50% covered (warning)
87.50%
21 / 24
87.50% covered (warning)
87.50%
7 / 8
8.12
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 index
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 show
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 store
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 update
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 destroy
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 assignFeatures
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 syncStripe
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace App\Http\Controllers\v2\Admin;
4
5use App\Http\Controllers\Controller;
6use App\Http\Models\Plans;
7use App\Http\Requests\v2\Plan\Admin\AssignFeaturesRequest;
8use App\Http\Requests\v2\Plan\Admin\DestroyPlanRequest;
9use App\Http\Requests\v2\Plan\Admin\IndexPlanRequest;
10use App\Http\Requests\v2\Plan\Admin\ShowPlanRequest;
11use App\Http\Requests\v2\Plan\Admin\StorePlanRequest;
12use App\Http\Requests\v2\Plan\Admin\SyncStripeRequest;
13use App\Http\Requests\v2\Plan\Admin\UpdatePlanRequest;
14use App\Http\Resources\v2\PlanWithFeaturesResource;
15use App\Http\Services\PlanService;
16use Illuminate\Http\JsonResponse;
17use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
18
19/**
20 * Controller for managing plans (admin).
21 *
22 * Only accessible by users with the VENGRESO_ADMIN role.
23 * Provides CRUD operations for plans, feature assignment, and Stripe sync.
24 */
25class PlanController extends Controller
26{
27    public function __construct(
28        private PlanService $planService
29    ) {}
30
31    /**
32     * Get all plans with optional filtering and pagination.
33     *
34     * @param  IndexPlanRequest  $request  Validated request with filter, per_page, page
35     *
36     * @response 200 {
37     *   "result": {
38     *     "data": [
39     *       {
40     *         "id": "...",
41     *         "title": "Freemium",
42     *         "identifier": "freemium",
43     *         "stripe_id": "price_xxx",
44     *         "stripe_product_id": "prod_xxx",
45     *         "stripe_sync_enabled": true,
46     *         "created_at": 1642521600,
47     *         "updated_at": 1642521600
48     *       }
49     *     ],
50     *     "meta": {
51     *       "current_page": 1,
52     *       "per_page": 15,
53     *       "total": 100,
54     *       "last_page": 7
55     *     }
56     *   }
57     * }
58     */
59    public function index(IndexPlanRequest $request): AnonymousResourceCollection
60    {
61        $plans = $this->planService->getAll(
62            filter: $request->input('filter'),
63            perPage: $request->input('per_page', 15),
64            includeSubscriptionCounts: $request->boolean('include_subscription_counts', false)
65        );
66
67        return PlanWithFeaturesResource::collection($plans);
68    }
69
70    /**
71     * Get a single plan by ID with its features.
72     *
73     * @param  ShowPlanRequest  $request  Validated request (authorization only)
74     * @param  Plans  $plan  The plan to retrieve (route model binding)
75     *
76     * @response 200 {
77     *   "result": {
78     *     "data": {
79     *       "id": "507f1f77bcf86cd799439011",
80     *       "title": "Freemium",
81     *       "identifier": "freemium",
82     *       "stripe_id": "price_xxx",
83     *       "stripe_product_id": "prod_xxx",
84     *       "stripe_sync_enabled": true,
85     *       "plan_features": [...],
86     *       "features_map": {...},
87     *       "created_at": 1642521600,
88     *       "updated_at": 1642521600
89     *     }
90     *   }
91     * }
92     */
93    public function show(ShowPlanRequest $request, Plans $plan): PlanWithFeaturesResource
94    {
95        // Load the plan with features
96        $planWithFeatures = $this->planService->getWithFeatures((string) $plan->_id);
97
98        return new PlanWithFeaturesResource($planWithFeatures);
99    }
100
101    /**
102     * Create a new plan.
103     *
104     * If stripe_sync_enabled is true, also creates a Stripe product.
105     *
106     * @param  StorePlanRequest  $request  Validated request with plan data
107     *
108     * @response 201 {
109     *   "result": {
110     *     "data": {
111     *       "id": "507f1f77bcf86cd799439011",
112     *       "title": "New Plan",
113     *       "identifier": "new-plan",
114     *       "stripe_product_id": "prod_xxx",
115     *       "stripe_sync_enabled": true,
116     *       "created_at": 1642521600,
117     *       "updated_at": 1642521600
118     *     }
119     *   }
120     * }
121     */
122    public function store(StorePlanRequest $request): JsonResponse
123    {
124        $plan = $this->planService->create($request->validated());
125
126        return (new PlanWithFeaturesResource($plan))
127            ->response()
128            ->setStatusCode(201);
129    }
130
131    /**
132     * Update an existing plan.
133     *
134     * If stripe_sync_enabled is true, also updates the Stripe product.
135     *
136     * @param  UpdatePlanRequest  $request  Validated request with update data
137     * @param  Plans  $plan  The plan to update (route model binding)
138     *
139     * @response 200 {
140     *   "result": {
141     *     "data": {
142     *       "id": "507f1f77bcf86cd799439011",
143     *       "title": "Updated Plan",
144     *       "identifier": "updated-plan",
145     *       "stripe_product_id": "prod_xxx",
146     *       "stripe_sync_enabled": true,
147     *       "created_at": 1642521600,
148     *       "updated_at": 1642525200
149     *     }
150     *   }
151     * }
152     */
153    public function update(UpdatePlanRequest $request, Plans $plan): PlanWithFeaturesResource
154    {
155        $plan = $this->planService->update($plan, $request->validated());
156
157        // Reload with features
158        $planWithFeatures = $this->planService->getWithFeatures((string) $plan->_id);
159
160        return new PlanWithFeaturesResource($planWithFeatures);
161    }
162
163    /**
164     * Delete a plan.
165     *
166     * If archive_stripe is true (default) and plan has a Stripe product, archives it.
167     *
168     * @param  DestroyPlanRequest  $request  Validated request with archive_stripe option
169     * @param  Plans  $plan  The plan to delete (route model binding)
170     *
171     * @response 204 No content
172     */
173    public function destroy(DestroyPlanRequest $request, Plans $plan): JsonResponse
174    {
175        $archiveStripe = $request->input('archive_stripe', true);
176
177        $this->planService->delete($plan, $archiveStripe);
178
179        return response()->json(null, 204);
180    }
181
182    /**
183     * Assign features to a plan.
184     *
185     * This replaces all existing feature assignments for the plan.
186     *
187     * @param  AssignFeaturesRequest  $request  Validated request with features array
188     * @param  Plans  $plan  The plan to assign features to (route model binding)
189     *
190     * @response 200 {
191     *   "result": {
192     *     "data": {
193     *       "id": "507f1f77bcf86cd799439011",
194     *       "title": "Freemium",
195     *       "identifier": "freemium",
196     *       "plan_features": [
197     *         {
198     *           "id": "...",
199     *           "feature_id": "...",
200     *           "value": true,
201     *           "is_enabled": true,
202     *           "feature": {...}
203     *         }
204     *       ],
205     *       "features_map": {
206     *         "bold": {"value": true, "description": "Enable bold text"}
207     *       }
208     *     }
209     *   }
210     * }
211     */
212    public function assignFeatures(AssignFeaturesRequest $request, Plans $plan): PlanWithFeaturesResource
213    {
214        $plan = $this->planService->assignFeatures($plan, $request->input('features'));
215
216        return new PlanWithFeaturesResource($plan);
217    }
218
219    /**
220     * Sync a plan to Stripe.
221     *
222     * Creates a new Stripe product if one doesn't exist, or updates the existing one.
223     *
224     * @param  SyncStripeRequest  $request  Validated request (authorization only)
225     * @param  Plans  $plan  The plan to sync (route model binding)
226     *
227     * @response 200 {
228     *   "result": {
229     *     "data": {
230     *       "id": "507f1f77bcf86cd799439011",
231     *       "title": "Freemium",
232     *       "identifier": "freemium",
233     *       "stripe_product_id": "prod_xxx",
234     *       "last_stripe_sync_at": 1642525200
235     *     }
236     *   }
237     * }
238     */
239    public function syncStripe(SyncStripeRequest $request, Plans $plan): PlanWithFeaturesResource
240    {
241        $plan = $this->planService->syncToStripe($plan);
242
243        // Reload with features
244        $planWithFeatures = $this->planService->getWithFeatures((string) $plan->_id);
245
246        return new PlanWithFeaturesResource($planWithFeatures);
247    }
248}