Skip to content

Commit 77fc2d5

Browse files
committed
Phases 206-210: REST API Layer (v1) — 34 tests, 1806 total passing
Sanctum token auth (login/logout/me), abstract ApiController base with success/error/paginated helpers. 11 API controllers exposing: Products, Invoices, Customers, Inventory (stock/movements/adjust), CRM leads, Helpdesk tickets, HR (employees/departments/leave), Manufacturing orders+BOMs, POS sessions+orders, cross-module Dashboard summary. All routes under /api/v1/ with auth:sanctum middleware. 34/34 API tests, 1806 total passing. https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent 59f2b67 commit 77fc2d5

21 files changed

Lines changed: 1802 additions & 0 deletions
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\V1;
4+
5+
use App\Http\Controllers\Controller;
6+
use Illuminate\Http\JsonResponse;
7+
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
8+
9+
abstract class ApiController extends Controller
10+
{
11+
protected function success(mixed $data, int $status = 200): JsonResponse
12+
{
13+
return response()->json(['success' => true, 'data' => $data], $status);
14+
}
15+
16+
protected function error(string $message, int $status = 400): JsonResponse
17+
{
18+
return response()->json(['success' => false, 'message' => $message], $status);
19+
}
20+
21+
protected function paginated(LengthAwarePaginator $paginator): JsonResponse
22+
{
23+
return response()->json([
24+
'success' => true,
25+
'data' => $paginator->items(),
26+
'meta' => [
27+
'total' => $paginator->total(),
28+
'per_page' => $paginator->perPage(),
29+
'current_page' => $paginator->currentPage(),
30+
'last_page' => $paginator->lastPage(),
31+
],
32+
]);
33+
}
34+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\V1;
4+
5+
use Illuminate\Http\JsonResponse;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Support\Facades\Auth;
8+
9+
class AuthController extends ApiController
10+
{
11+
/**
12+
* POST /api/v1/auth/login
13+
* Validate credentials, return Sanctum token.
14+
*/
15+
public function login(Request $request): JsonResponse
16+
{
17+
$request->validate([
18+
'email' => 'required|email',
19+
'password' => 'required',
20+
]);
21+
22+
if (! Auth::attempt($request->only('email', 'password'))) {
23+
return $this->error('Invalid credentials', 401);
24+
}
25+
26+
/** @var \App\Models\User $user */
27+
$user = Auth::user();
28+
$token = $user->createToken('api-token')->plainTextToken;
29+
30+
return $this->success([
31+
'token' => $token,
32+
'user' => [
33+
'id' => $user->id,
34+
'name' => $user->name,
35+
'email' => $user->email,
36+
'tenant_id' => $user->tenant_id,
37+
],
38+
]);
39+
}
40+
41+
/**
42+
* POST /api/v1/auth/logout
43+
* Revoke the current access token.
44+
*/
45+
public function logout(Request $request): JsonResponse
46+
{
47+
$request->user()->currentAccessToken()->delete();
48+
49+
return $this->success(['message' => 'Logged out successfully']);
50+
}
51+
52+
/**
53+
* GET /api/v1/auth/me
54+
* Return authenticated user with tenant info.
55+
*/
56+
public function me(Request $request): JsonResponse
57+
{
58+
$user = $request->user()->load('tenant');
59+
60+
return $this->success([
61+
'id' => $user->id,
62+
'name' => $user->name,
63+
'email' => $user->email,
64+
'tenant_id' => $user->tenant_id,
65+
'tenant' => $user->tenant ? [
66+
'id' => $user->tenant->id,
67+
'name' => $user->tenant->name,
68+
'slug' => $user->tenant->slug,
69+
] : null,
70+
]);
71+
}
72+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\V1;
4+
5+
use App\Modules\CRM\Models\CrmLead;
6+
use Illuminate\Http\JsonResponse;
7+
use Illuminate\Http\Request;
8+
9+
class CrmApiController extends ApiController
10+
{
11+
/**
12+
* GET /api/v1/crm/leads
13+
*/
14+
public function index(Request $request): JsonResponse
15+
{
16+
$query = CrmLead::with(['stage:id,name', 'assignee:id,name']);
17+
18+
if ($type = $request->query('type')) {
19+
$query->where('type', $type);
20+
}
21+
22+
if ($status = $request->query('status')) {
23+
$query->where('status', $status);
24+
}
25+
26+
if ($assignedTo = $request->query('assigned_to')) {
27+
$query->where('assigned_to', $assignedTo);
28+
}
29+
30+
$paginator = $query->latest()->paginate(20);
31+
32+
return $this->paginated($paginator);
33+
}
34+
35+
/**
36+
* GET /api/v1/crm/leads/{id}
37+
*/
38+
public function show(int $id): JsonResponse
39+
{
40+
$lead = CrmLead::with(['stage', 'activities', 'assignee:id,name'])->findOrFail($id);
41+
42+
return $this->success($lead);
43+
}
44+
45+
/**
46+
* POST /api/v1/crm/leads
47+
*/
48+
public function store(Request $request): JsonResponse
49+
{
50+
$validated = $request->validate([
51+
'title' => 'required|string|max:255',
52+
'type' => 'nullable|string|in:lead,opportunity',
53+
'contact_name' => 'nullable|string|max:255',
54+
'company_name' => 'nullable|string|max:255',
55+
'email' => 'nullable|email|max:255',
56+
'phone' => 'nullable|string|max:50',
57+
'source' => 'nullable|string|max:100',
58+
'expected_revenue' => 'nullable|numeric|min:0',
59+
'probability' => 'nullable|numeric|min:0|max:100',
60+
'priority' => 'nullable|string',
61+
'stage_id' => 'nullable|integer|exists:crm_stages,id',
62+
'assigned_to' => 'nullable|integer|exists:users,id',
63+
'description' => 'nullable|string',
64+
]);
65+
66+
$tenantId = app()->has('tenant') ? app('tenant')->id : $request->user()->tenant_id;
67+
68+
$validated['tenant_id'] = $tenantId;
69+
$validated['created_by'] = $request->user()->id;
70+
71+
$lead = CrmLead::create($validated);
72+
73+
return $this->success($lead, 201);
74+
}
75+
76+
/**
77+
* PUT /api/v1/crm/leads/{id}
78+
*/
79+
public function update(Request $request, int $id): JsonResponse
80+
{
81+
$lead = CrmLead::findOrFail($id);
82+
83+
$validated = $request->validate([
84+
'title' => 'sometimes|string|max:255',
85+
'type' => 'nullable|string|in:lead,opportunity',
86+
'contact_name' => 'nullable|string|max:255',
87+
'company_name' => 'nullable|string|max:255',
88+
'email' => 'nullable|email|max:255',
89+
'phone' => 'nullable|string|max:50',
90+
'source' => 'nullable|string|max:100',
91+
'expected_revenue' => 'nullable|numeric|min:0',
92+
'probability' => 'nullable|numeric|min:0|max:100',
93+
'priority' => 'nullable|string',
94+
'stage_id' => 'nullable|integer|exists:crm_stages,id',
95+
'assigned_to' => 'nullable|integer|exists:users,id',
96+
'description' => 'nullable|string',
97+
]);
98+
99+
$lead->update($validated);
100+
101+
return $this->success($lead);
102+
}
103+
104+
/**
105+
* DELETE /api/v1/crm/leads/{id}
106+
*/
107+
public function destroy(int $id): JsonResponse
108+
{
109+
$lead = CrmLead::findOrFail($id);
110+
$lead->delete();
111+
112+
return $this->success(['message' => 'Lead deleted']);
113+
}
114+
115+
/**
116+
* POST /api/v1/crm/leads/{lead}/won
117+
*/
118+
public function markWon(int $lead): JsonResponse
119+
{
120+
$crmLead = CrmLead::findOrFail($lead);
121+
$crmLead->markWon();
122+
123+
return $this->success($crmLead);
124+
}
125+
126+
/**
127+
* POST /api/v1/crm/leads/{lead}/lost
128+
*/
129+
public function markLost(Request $request, int $lead): JsonResponse
130+
{
131+
$crmLead = CrmLead::findOrFail($lead);
132+
133+
$validated = $request->validate([
134+
'reason' => 'nullable|string',
135+
]);
136+
137+
$crmLead->markLost($validated['reason'] ?? '');
138+
139+
return $this->success($crmLead);
140+
}
141+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Api\V1;
4+
5+
use App\Modules\Finance\Models\Contact;
6+
use Illuminate\Http\JsonResponse;
7+
use Illuminate\Http\Request;
8+
9+
class CustomerApiController extends ApiController
10+
{
11+
/**
12+
* GET /api/v1/customers
13+
*/
14+
public function index(Request $request): JsonResponse
15+
{
16+
$query = Contact::customers();
17+
18+
if ($search = $request->query('search')) {
19+
$query->where(function ($q) use ($search) {
20+
$q->where('name', 'like', "%{$search}%")
21+
->orWhere('email', 'like', "%{$search}%");
22+
});
23+
}
24+
25+
$paginator = $query->latest()->paginate(20);
26+
27+
return $this->paginated($paginator);
28+
}
29+
30+
/**
31+
* GET /api/v1/customers/{id}
32+
*/
33+
public function show(int $id): JsonResponse
34+
{
35+
$customer = Contact::customers()->with([
36+
'invoices' => fn ($q) => $q->select('id', 'contact_id', 'number', 'status', 'issue_date', 'due_date')->latest()->limit(10),
37+
])->findOrFail($id);
38+
39+
return $this->success($customer);
40+
}
41+
42+
/**
43+
* POST /api/v1/customers
44+
*/
45+
public function store(Request $request): JsonResponse
46+
{
47+
$validated = $request->validate([
48+
'name' => 'required|string|max:255',
49+
'email' => 'nullable|email|max:255',
50+
'phone' => 'nullable|string|max:50',
51+
'address' => 'nullable|string',
52+
'notes' => 'nullable|string',
53+
]);
54+
55+
$tenantId = app()->has('tenant') ? app('tenant')->id : $request->user()->tenant_id;
56+
57+
$validated['tenant_id'] = $tenantId;
58+
$validated['type'] = 'customer';
59+
$validated['is_active'] = true;
60+
61+
$customer = Contact::create($validated);
62+
63+
return $this->success($customer, 201);
64+
}
65+
66+
/**
67+
* PUT /api/v1/customers/{id}
68+
*/
69+
public function update(Request $request, int $id): JsonResponse
70+
{
71+
$customer = Contact::customers()->findOrFail($id);
72+
73+
$validated = $request->validate([
74+
'name' => 'sometimes|string|max:255',
75+
'email' => 'nullable|email|max:255',
76+
'phone' => 'nullable|string|max:50',
77+
'address' => 'nullable|string',
78+
'notes' => 'nullable|string',
79+
'is_active' => 'nullable|boolean',
80+
]);
81+
82+
$customer->update($validated);
83+
84+
return $this->success($customer);
85+
}
86+
87+
/**
88+
* DELETE /api/v1/customers/{id}
89+
*/
90+
public function destroy(int $id): JsonResponse
91+
{
92+
$customer = Contact::customers()->findOrFail($id);
93+
$customer->delete();
94+
95+
return $this->success(['message' => 'Customer deleted']);
96+
}
97+
}

0 commit comments

Comments
 (0)