seed([RoleSeeder::class, SamplePsgcSeeder::class, SampleDropOffPointsSeeder::class]); $this->scanner = User::factory()->create(['role' => User::ROLE_SCANNER, 'status' => 'active']); $this->dop = DropOffPoint::where('code', 'DOP-QC-DLM-01')->firstOrFail(); $this->household = Household::factory()->create(['barangay_id' => $this->dop->barangay_id]); $batch = app(BatchGenerator::class)->generate(3, QrCodeBatch::PURPOSE_FREE); $this->activeCode = $batch->codes()->first(); $this->activeCode->forceFill([ 'assigned_to_household_id' => $this->household->id, 'status' => 'active', 'allocated_at' => now(), 'activated_at' => now(), ])->save(); } public function test_valid_scan_at_dop_marks_code_used(): void { Sanctum::actingAs($this->scanner); $response = $this->postJson('/api/v1/scanner/scan', [ 'serial' => $this->activeCode->serial, 'drop_off_point_id' => $this->dop->id, 'lat' => 14.6539, 'lng' => 121.0685, 'weight_kg' => 5, 'waste_type' => 'general', ]); $response->assertOk() ->assertJsonPath('data.status', 'used'); $this->assertDatabaseHas('collection_logs', [ 'qr_code_id' => $this->activeCode->id, 'verification_status' => 'valid', ]); } public function test_scan_far_from_dop_rejected(): void { Sanctum::actingAs($this->scanner); $response = $this->postJson('/api/v1/scanner/scan', [ 'serial' => $this->activeCode->serial, 'drop_off_point_id' => $this->dop->id, 'lat' => 8.0, 'lng' => 124.0, ]); $response->assertStatus(422); $this->assertStringStartsWith('out_of_range', $response->json('errors.reason.0')); } public function test_unknown_serial_rejected(): void { Sanctum::actingAs($this->scanner); $response = $this->postJson('/api/v1/scanner/scan', [ 'serial' => 'PH-XX-9999-9999-999999-XX', 'drop_off_point_id' => $this->dop->id, 'lat' => 14.6539, 'lng' => 121.0685, ]); $response->assertStatus(422) ->assertJsonPath('errors.reason.0', 'not_found'); } public function test_already_used_serial_rejected_as_duplicate(): void { $this->activeCode->forceFill(['status' => 'used', 'used_at' => now()])->save(); Sanctum::actingAs($this->scanner); $response = $this->postJson('/api/v1/scanner/scan', [ 'serial' => $this->activeCode->serial, 'drop_off_point_id' => $this->dop->id, 'lat' => 14.6539, 'lng' => 121.0685, ]); $response->assertStatus(422) ->assertJsonPath('errors.reason.0', 'duplicate'); } public function test_unassigned_code_rejected(): void { $this->activeCode->forceFill(['status' => 'unassigned'])->save(); Sanctum::actingAs($this->scanner); $response = $this->postJson('/api/v1/scanner/scan', [ 'serial' => $this->activeCode->serial, 'drop_off_point_id' => $this->dop->id, 'lat' => 14.6539, 'lng' => 121.0685, ]); $response->assertStatus(422); } public function test_bulk_scan_processes_each_independently(): void { $batch = app(BatchGenerator::class)->generate(3, QrCodeBatch::PURPOSE_FREE); $batch->codes()->update([ 'assigned_to_household_id' => $this->household->id, 'status' => 'active', ]); $codes = $batch->codes()->limit(2)->get(); Sanctum::actingAs($this->scanner); $response = $this->postJson('/api/v1/scanner/scan/bulk', [ 'scans' => [ ['serial' => $codes[0]->serial, 'drop_off_point_id' => $this->dop->id, 'lat' => 14.6539, 'lng' => 121.0685], ['serial' => $codes[1]->serial, 'drop_off_point_id' => $this->dop->id, 'lat' => 14.6539, 'lng' => 121.0685], ['serial' => 'PH-bogus', 'drop_off_point_id' => $this->dop->id, 'lat' => 14.6539, 'lng' => 121.0685], ], ]); $response->assertOk() ->assertJsonPath('data.accepted_count', 2) ->assertJsonPath('data.rejected_count', 1); } public function test_resident_blocked_from_scanning(): void { $resident = User::factory()->create(['role' => User::ROLE_RESIDENT, 'status' => 'active']); Sanctum::actingAs($resident); $this->postJson('/api/v1/scanner/scan', [ 'serial' => $this->activeCode->serial, 'drop_off_point_id' => $this->dop->id, 'lat' => 14.6539, 'lng' => 121.0685, ])->assertStatus(403); } }