Skip to content

Commit 4a4a440

Browse files
committed
Add APIs for common application routes
- Implement tests for listing, filtering, searching, creating, showing, updating, and deleting categories in CategoryControllerTest. - Add tests for team management including listing, filtering, searching, creating, showing, updating, and deleting teams in TeamControllerTest. - Create tests for transaction management covering listing, filtering, searching, creating, showing, updating, and deleting transactions in TransactionControllerTest. - Ensure proper authentication and authorization checks in all tests. - Validate required fields for creating categories, teams, and transactions. - Include tests for handling attachments in transactions. - Update Pest configuration to use RefreshDatabase trait.
1 parent fe8a135 commit 4a4a440

31 files changed

+5416
-10
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\V1;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Http\Resources\Api\V1\UserResource;
7+
use App\Models\User;
8+
use Illuminate\Http\Request;
9+
use Illuminate\Support\Facades\Hash;
10+
use Illuminate\Validation\ValidationException;
11+
12+
final class AuthController extends Controller
13+
{
14+
/**
15+
* Handle user login and token generation.
16+
*/
17+
public function login(Request $request)
18+
{
19+
$request->validate([
20+
'email' => ['required', 'email'],
21+
'password' => ['required', 'string'],
22+
'device_name' => ['nullable', 'string', 'max:255'],
23+
]);
24+
25+
$user = User::where('email', $request->email)->first();
26+
27+
if (! $user || ! Hash::check($request->password, $user->password)) {
28+
throw ValidationException::withMessages([
29+
'email' => ['The provided credentials are incorrect.'],
30+
]);
31+
}
32+
33+
$deviceName = $request->device_name ?? $request->userAgent() ?? 'api-token';
34+
$token = $user->createToken($deviceName)->plainTextToken;
35+
36+
return response()->json([
37+
'success' => true,
38+
'message' => 'Login successful',
39+
'data' => [
40+
'user' => new UserResource($user->load('activeTeam')),
41+
'token' => $token,
42+
'token_type' => 'Bearer',
43+
],
44+
], 200);
45+
}
46+
47+
/**
48+
* Handle user registration.
49+
*/
50+
public function register(Request $request)
51+
{
52+
$validated = $request->validate([
53+
'name' => ['required', 'string', 'max:255'],
54+
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
55+
'password' => ['required', 'string', 'min:8', 'confirmed'],
56+
'device_name' => ['nullable', 'string', 'max:255'],
57+
]);
58+
59+
$user = User::create([
60+
'name' => $validated['name'],
61+
'email' => $validated['email'],
62+
'password' => Hash::make($validated['password']),
63+
]);
64+
65+
$deviceName = $request->device_name ?? $request->userAgent() ?? 'api-token';
66+
$token = $user->createToken($deviceName)->plainTextToken;
67+
68+
return response()->json([
69+
'success' => true,
70+
'message' => 'Registration successful',
71+
'data' => [
72+
'user' => new UserResource($user),
73+
'token' => $token,
74+
'token_type' => 'Bearer',
75+
],
76+
], 201);
77+
}
78+
79+
/**
80+
* Handle user logout (revoke token).
81+
*/
82+
public function logout(Request $request)
83+
{
84+
$request->user()->currentAccessToken()->delete();
85+
86+
return response()->json([
87+
'success' => true,
88+
'message' => 'Logout successful',
89+
], 200);
90+
}
91+
92+
/**
93+
* Get the authenticated user.
94+
*/
95+
public function user(Request $request)
96+
{
97+
return response()->json([
98+
'success' => true,
99+
'data' => new UserResource($request->user()->load('activeTeam', 'teams')),
100+
], 200);
101+
}
102+
103+
/**
104+
* Update the authenticated user's profile.
105+
*/
106+
public function updateProfile(Request $request)
107+
{
108+
$user = $request->user();
109+
110+
$validated = $request->validate([
111+
'name' => ['sometimes', 'string', 'max:255'],
112+
'email' => ['sometimes', 'string', 'email', 'max:255', 'unique:users,email,' . $user->id],
113+
'password' => ['sometimes', 'string', 'min:8', 'confirmed'],
114+
]);
115+
116+
if (isset($validated['password'])) {
117+
$validated['password'] = Hash::make($validated['password']);
118+
}
119+
120+
$user->update($validated);
121+
122+
return response()->json([
123+
'success' => true,
124+
'message' => 'Profile updated successfully',
125+
'data' => new UserResource($user->fresh()->load('activeTeam')),
126+
], 200);
127+
}
128+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\V1;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Http\Resources\Api\V1\BillResource;
7+
use App\Models\Bill;
8+
use Illuminate\Http\Request;
9+
10+
final class BillController extends Controller
11+
{
12+
/**
13+
* Display a listing of the bills.
14+
*/
15+
public function index(Request $request)
16+
{
17+
$query = Bill::with(['category', 'user', 'team'])
18+
->when($request->search, function ($q, $search) {
19+
if (str_contains($search, ':')) {
20+
[$column, $value] = explode(':', $search);
21+
if ($column && $value && in_fillable($column, Bill::class)) {
22+
return $q->where($column, 'like', '%'.$value.'%');
23+
}
24+
}
25+
26+
$q->where('title', 'like', '%'.$search.'%')
27+
->orWhere('description', 'like', '%'.$search.'%');
28+
})
29+
->when($request->status, function ($q, $status) {
30+
if ($status === 'upcoming') {
31+
$q->upcoming(7);
32+
} else {
33+
$q->where('status', $status);
34+
}
35+
})
36+
->when($request->category_id, function ($q, $categoryId) {
37+
$q->where('category_id', $categoryId);
38+
})
39+
->when($request->is_recurring !== null, function ($q) use ($request) {
40+
$q->where('is_recurring', $request->boolean('is_recurring'));
41+
})
42+
->when($request->has('tags'), function ($q) use ($request) {
43+
$tags = is_array($request->tags) ? $request->tags : [$request->tags];
44+
foreach ($tags as $tag) {
45+
$q->whereJsonContains('tags', $tag);
46+
}
47+
});
48+
49+
// Sorting
50+
$sortBy = $request->input('sort_by', 'due_date');
51+
$sortDirection = $request->input('sort_direction', 'asc');
52+
53+
if (in_fillable($sortBy, Bill::class)) {
54+
$query->orderBy($sortBy, $sortDirection === 'desc' ? 'desc' : 'asc');
55+
}
56+
57+
// Pagination
58+
$perPage = min($request->input('per_page', 15), 100);
59+
$bills = $query->paginate($perPage);
60+
61+
return BillResource::collection($bills);
62+
}
63+
64+
/**
65+
* Store a newly created bill.
66+
*/
67+
public function store(Request $request)
68+
{
69+
$validated = $request->validate([
70+
'title' => ['required', 'string', 'max:255'],
71+
'amount' => ['required', 'numeric', 'min:0'],
72+
'due_date' => ['required', 'date'],
73+
'trial_start_date' => ['nullable', 'date'],
74+
'trial_end_date' => ['nullable', 'date', 'after:trial_start_date'],
75+
'has_trial' => ['nullable', 'boolean'],
76+
'category_id' => ['nullable', 'integer', 'exists:categories,id'],
77+
'description' => ['nullable', 'string'],
78+
'is_recurring' => ['nullable', 'boolean'],
79+
'recurrence_period' => ['nullable', 'string', 'in:daily,weekly,monthly,yearly'],
80+
'payment_url' => ['nullable', 'string', 'url'],
81+
'tags' => ['nullable', 'array'],
82+
]);
83+
84+
$validated['user_id'] = $request->user()->id;
85+
$validated['team_id'] = $request->user()->active_team_id;
86+
87+
$bill = Bill::create($validated);
88+
89+
return response()->json([
90+
'success' => true,
91+
'message' => 'Bill created successfully',
92+
'data' => new BillResource($bill->load(['category', 'user', 'team'])),
93+
], 201);
94+
}
95+
96+
/**
97+
* Display the specified bill.
98+
*/
99+
public function show(Bill $bill)
100+
{
101+
return response()->json([
102+
'success' => true,
103+
'data' => new BillResource($bill->load(['category', 'user', 'team', 'transactions', 'notes'])),
104+
]);
105+
}
106+
107+
/**
108+
* Update the specified bill.
109+
*/
110+
public function update(Request $request, Bill $bill)
111+
{
112+
$validated = $request->validate([
113+
'title' => ['sometimes', 'string', 'max:255'],
114+
'amount' => ['sometimes', 'numeric', 'min:0'],
115+
'due_date' => ['sometimes', 'date'],
116+
'trial_start_date' => ['nullable', 'date'],
117+
'trial_end_date' => ['nullable', 'date', 'after:trial_start_date'],
118+
'has_trial' => ['nullable', 'boolean'],
119+
'category_id' => ['nullable', 'integer', 'exists:categories,id'],
120+
'description' => ['nullable', 'string'],
121+
'is_recurring' => ['nullable', 'boolean'],
122+
'recurrence_period' => ['nullable', 'string', 'in:daily,weekly,monthly,yearly'],
123+
'payment_url' => ['nullable', 'string', 'url'],
124+
'tags' => ['nullable', 'array'],
125+
'status' => ['sometimes', 'string', 'in:paid,unpaid,pending,cancelled'],
126+
]);
127+
128+
$bill->update($validated);
129+
130+
return response()->json([
131+
'success' => true,
132+
'message' => 'Bill updated successfully',
133+
'data' => new BillResource($bill->fresh()->load(['category', 'user', 'team'])),
134+
]);
135+
}
136+
137+
/**
138+
* Remove the specified bill.
139+
*/
140+
public function destroy(Bill $bill)
141+
{
142+
$bill->delete();
143+
144+
return response()->json([
145+
'success' => true,
146+
'message' => 'Bill deleted successfully',
147+
]);
148+
}
149+
150+
/**
151+
* Mark bill as paid.
152+
*/
153+
public function markAsPaid(Request $request, Bill $bill)
154+
{
155+
$bill->update(['status' => 'paid']);
156+
157+
return response()->json([
158+
'success' => true,
159+
'message' => 'Bill marked as paid',
160+
'data' => new BillResource($bill->fresh()->load(['category', 'user', 'team'])),
161+
]);
162+
}
163+
164+
/**
165+
* Get upcoming bills.
166+
*/
167+
public function upcoming(Request $request)
168+
{
169+
$days = $request->input('days', 7);
170+
$bills = Bill::with(['category', 'user', 'team'])
171+
->upcoming($days)
172+
->get();
173+
174+
return response()->json([
175+
'success' => true,
176+
'data' => BillResource::collection($bills),
177+
]);
178+
}
179+
}

0 commit comments

Comments
 (0)