Skip to content

Commit 9e8b727

Browse files
committed
feat: agent app endpoint to fetch unassign appliances
1 parent 8cca39e commit 9e8b727

5 files changed

Lines changed: 227 additions & 0 deletions

File tree

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;
4+
5+
use App\Http\Requests\ListAgentUnassignedDevicesRequest;
6+
use App\Http\Resources\ApiResource;
7+
use App\Services\AgentService;
8+
use App\Services\DeviceService;
9+
use Illuminate\Validation\ValidationException;
10+
11+
class AgentAvailableDeviceController extends Controller {
12+
public function __construct(
13+
private DeviceService $deviceService,
14+
private AgentService $agentService,
15+
) {}
16+
17+
public function index(ListAgentUnassignedDevicesRequest $request): ApiResource {
18+
$agent = $this->agentService->getByAuthenticatedUser();
19+
$applianceId = $request->integer('appliance_id');
20+
21+
$isAssignedToAgent = $agent->assignedAppliance()
22+
->where('appliance_id', $applianceId)
23+
->exists();
24+
25+
if (!$isAssignedToAgent) {
26+
throw ValidationException::withMessages(['appliance_id' => 'You are not assigned this appliance.']);
27+
}
28+
29+
$deviceClass = ListAgentUnassignedDevicesRequest::SUPPORTED_TYPES[$request->string('type')->toString()];
30+
$devices = $this->deviceService->getUnassignedByAppliance($applianceId, $deviceClass);
31+
32+
return ApiResource::make($devices);
33+
}
34+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Http\Requests;
4+
5+
use App\Models\EBike;
6+
use App\Models\SolarHomeSystem;
7+
use Illuminate\Foundation\Http\FormRequest;
8+
use Illuminate\Validation\Rule;
9+
10+
class ListAgentUnassignedDevicesRequest extends FormRequest {
11+
public const SUPPORTED_TYPES = [
12+
SolarHomeSystem::RELATION_NAME => SolarHomeSystem::class,
13+
EBike::RELATION_NAME => EBike::class,
14+
];
15+
16+
public function authorize(): bool {
17+
return true;
18+
}
19+
20+
/**
21+
* @return array<string, mixed>
22+
*/
23+
public function rules(): array {
24+
return [
25+
'appliance_id' => ['required', 'integer', 'exists:tenant.appliances,id'],
26+
'type' => ['required', 'string', Rule::in(array_keys(self::SUPPORTED_TYPES))],
27+
];
28+
}
29+
}

src/backend/app/Services/DeviceService.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,34 @@ public function getAll(?int $limit = null, array $filters = []): Collection|Leng
8585
return $limit ? $query->paginate($limit) : $query->get();
8686
}
8787

88+
/**
89+
* Unassigned devices (no owner yet) of the given morph class whose
90+
* underlying unit belongs to the given appliance.
91+
*
92+
* @param class-string $deviceClass one of SolarHomeSystem::class or EBike::class
93+
*
94+
* @return Collection<int, Device>
95+
*/
96+
public function getUnassignedByAppliance(int $applianceId, string $deviceClass): Collection {
97+
/** @var array<string, \Closure|string> $relations */
98+
$relations = [
99+
'device' => function (MorphTo $morphTo) use ($deviceClass): void {
100+
$morphTo->morphWith([$deviceClass => ['manufacturer', 'appliance']]);
101+
},
102+
];
103+
104+
return $this->device->newQuery()
105+
->with($relations)
106+
->whereNull('person_id')
107+
->whereHasMorph(
108+
'device',
109+
[$deviceClass],
110+
fn ($morph) => $morph->where('appliance_id', $applianceId),
111+
)
112+
->latest()
113+
->get();
114+
}
115+
88116
/**
89117
* @return Collection<int, Device>
90118
*/

src/backend/routes/resources/AgentApp.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
use App\Http\Controllers\AgentAssignedAppliancesController;
44
use App\Http\Controllers\AgentAuthController;
5+
use App\Http\Controllers\AgentAvailableDeviceController;
56
use App\Http\Controllers\AgentBalanceController;
67
use App\Http\Controllers\AgentCustomerController;
78
use App\Http\Controllers\AgentCustomersPaymentHistoryController;
@@ -69,6 +70,9 @@
6970
Route::group(['prefix' => 'appliance_types'], function () {
7071
Route::get('/', [AgentAssignedAppliancesController::class, 'index']);
7172
});
73+
Route::group(['prefix' => 'devices'], function () {
74+
Route::get('/unassigned', [AgentAvailableDeviceController::class, 'index']);
75+
});
7276
Route::group(['prefix' => 'meters'], function () {
7377
Route::get('/', [AgentMeterController::class, 'index']);
7478
});
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use App\Models\AgentAssignedAppliances;
6+
use App\Models\Device;
7+
use App\Models\SolarHomeSystem;
8+
use Database\Factories\ApplianceFactory;
9+
use Database\Factories\ManufacturerFactory;
10+
use Tests\CreateEnvironments;
11+
use Tests\TestCase;
12+
13+
class AgentUnassignedDevicesTest extends TestCase {
14+
use CreateEnvironments;
15+
16+
public function testAgentListsOnlyUnassignedDevicesForTheirAssignedAppliance(): void {
17+
$this->seedAgentAndAssignedAppliance();
18+
$applianceId = $this->assignedAppliance->appliance_id;
19+
20+
$unassigned = $this->createShsDevice('SHS-FREE-0001', $applianceId, null);
21+
$assignedToPerson = $this->createShsDevice('SHS-SOLD-0001', $applianceId, $this->people[0]->id);
22+
23+
$response = $this->actingAs($this->agent)
24+
->get(sprintf('/api/app/agents/devices/unassigned?appliance_id=%d&type=solar_home_system', $applianceId));
25+
26+
$response->assertStatus(200);
27+
$serials = collect($response->json('data'))->pluck('device_serial')->all();
28+
$this->assertContains($unassigned->device_serial, $serials);
29+
$this->assertNotContains($assignedToPerson->device_serial, $serials);
30+
}
31+
32+
public function testListExcludesUnitsOfOtherAppliances(): void {
33+
$this->seedAgentAndAssignedAppliance();
34+
$applianceId = $this->assignedAppliance->appliance_id;
35+
36+
$siblingAppliance = ApplianceFactory::new()->create([
37+
'appliance_type_id' => $this->applianceType->id,
38+
]);
39+
$siblingDevice = $this->createShsDevice('SHS-SIBLING-0001', $siblingAppliance->id, null);
40+
41+
$response = $this->actingAs($this->agent)
42+
->get(sprintf('/api/app/agents/devices/unassigned?appliance_id=%d&type=solar_home_system', $applianceId));
43+
44+
$response->assertStatus(200);
45+
$serials = collect($response->json('data'))->pluck('device_serial')->all();
46+
$this->assertNotContains($siblingDevice->device_serial, $serials);
47+
}
48+
49+
public function testAgentCannotListDevicesForApplianceNotAssignedToThem(): void {
50+
$this->seedAgentAndAssignedAppliance();
51+
52+
$foreignAppliance = ApplianceFactory::new()->create([
53+
'appliance_type_id' => $this->applianceType->id,
54+
]);
55+
$this->createShsDevice('SHS-FOREIGN-0001', $foreignAppliance->id, null);
56+
57+
$response = $this->actingAs($this->agent)
58+
->get(sprintf(
59+
'/api/app/agents/devices/unassigned?appliance_id=%d&type=solar_home_system',
60+
$foreignAppliance->id
61+
));
62+
63+
$response->assertStatus(422);
64+
$response->assertJsonValidationErrors(['appliance_id']);
65+
}
66+
67+
public function testApplianceIdAndTypeAreRequired(): void {
68+
$this->seedAgentAndAssignedAppliance();
69+
70+
$response = $this->actingAs($this->agent)
71+
->get('/api/app/agents/devices/unassigned');
72+
73+
$response->assertStatus(422);
74+
$response->assertJsonValidationErrors(['appliance_id', 'type']);
75+
}
76+
77+
public function testTypeMustBeSupported(): void {
78+
$this->seedAgentAndAssignedAppliance();
79+
80+
$response = $this->actingAs($this->agent)
81+
->get(sprintf(
82+
'/api/app/agents/devices/unassigned?appliance_id=%d&type=bogus',
83+
$this->assignedAppliance->appliance_id
84+
));
85+
86+
$response->assertStatus(422);
87+
$response->assertJsonValidationErrors(['type']);
88+
}
89+
90+
public function testUnauthenticatedRequestIsRejected(): void {
91+
$response = $this->get('/api/app/agents/devices/unassigned?appliance_id=1&type=solar_home_system');
92+
93+
$response->assertStatus(401);
94+
}
95+
96+
private function seedAgentAndAssignedAppliance(): void {
97+
$this->createTestData();
98+
$this->createCluster();
99+
$this->createMiniGrid();
100+
$this->createCity();
101+
$this->createAgentCommission();
102+
$this->createAgent();
103+
$this->createPerson(1, 1);
104+
$this->createApplianceType();
105+
106+
$appliance = ApplianceFactory::new()->create([
107+
'appliance_type_id' => $this->applianceType->id,
108+
]);
109+
$this->assignedAppliance = AgentAssignedAppliances::query()->create([
110+
'agent_id' => $this->agent->id,
111+
'user_id' => $this->user->id,
112+
'appliance_id' => $appliance->id,
113+
'cost' => 100,
114+
]);
115+
}
116+
117+
private function createShsDevice(string $serial, int $applianceId, ?int $personId): Device {
118+
$manufacturer = ManufacturerFactory::new()->isShsManufacturer()->create();
119+
$shs = SolarHomeSystem::query()->create([
120+
'serial_number' => $serial,
121+
'manufacturer_id' => $manufacturer->id,
122+
'appliance_id' => $applianceId,
123+
]);
124+
125+
return Device::query()->create([
126+
'person_id' => $personId,
127+
'device_id' => $shs->id,
128+
'device_type' => SolarHomeSystem::RELATION_NAME,
129+
'device_serial' => $serial,
130+
]);
131+
}
132+
}

0 commit comments

Comments
 (0)