> */ private array $receipts = []; /** @var array */ private array $live = []; /** @var array> */ private array $spools = []; private User $admin; private User $member; protected function setUp(): void { parent::setUp(); $this->admin = User::factory()->create(['role' => 'admin']); $this->member = User::factory()->create(['role' => 'user']); config()->set('services.etsy', ['keystring' => 'key', 'shared_secret' => 'secret']); config()->set('services.bambuddy', ['url' => 'https://3d.example.test', 'api_key' => 'key']); EtsyConnection::create(['shop_id' => 42, 'shop_name' => 'HickLab3D', 'access_token' => 'token', 'refresh_token' => 'refresh', 'expires_at' => now()->addHour()]); Http::preventStrayRequests(); Http::fake(fn (Request $request) => match (true) { str_contains($request->url(), '/receipts') => Http::response(['results' => $this->receipts]), str_ends_with($request->url(), '/api/v1/printers/') => Http::response([['id' => 1, 'name' => 'Printy', 'model' => 'X1C']]), str_ends_with($request->url(), '/printers/1/status') => Http::response($this->live), str_contains($request->url(), '/inventory/spools') => Http::response($this->spools), default => Http::response([], 404), }); } private function scan(): int { Cache::flush(); return app(AlertScanner::class)->scan(); } /** @return array */ private function receipt(int $id, ?int $shipBy = null): array { return [ 'receipt_id' => $id, 'name' => 'Buyer '.$id, 'grandtotal' => ['amount' => 1250, 'divisor' => 100, 'currency_code' => 'GBP'], 'transactions' => $shipBy ? [['expected_ship_date' => $shipBy]] : [], ]; } public function test_new_orders_notify_admins_only_and_the_first_scan_is_silent(): void { $this->receipts = [$this->receipt(1)]; $this->assertSame(0, $this->scan()); $this->receipts = [$this->receipt(2), $this->receipt(1)]; $this->assertSame(1, $this->scan()); $this->assertSame(0, $this->scan()); $notification = $this->admin->notifications()->first(); $this->assertSame('New Etsy order #2', $notification->data['title']); $this->assertSame('Buyer 2 · GBP 12.50', $notification->data['body']); $this->assertCount(0, $this->member->notifications); } public function test_orders_due_within_a_day_and_overdue_each_notify_once(): void { $this->receipts = [$this->receipt(7, time() + 3600 * 12)]; $this->assertSame(1, $this->scan()); $this->assertSame(0, $this->scan()); $this->assertSame('Order #7 is due to ship within 24 hours', $this->admin->notifications()->first()->data['title']); $this->receipts = [$this->receipt(7, time() - 60)]; $this->assertSame(1, $this->scan()); $this->assertSame(0, $this->scan()); $this->assertContains('Order #7 is overdue to ship', $this->admin->notifications->pluck('data.title')->all()); $this->receipts = [$this->receipt(8, time() + 3600 * 72)]; $this->scan(); $this->assertSame(3, $this->admin->notifications()->count()); $this->assertContains('New Etsy order #8', $this->admin->fresh()->notifications->pluck('data.title')->all()); } public function test_print_milestones_completion_and_errors_notify_everyone_once(): void { $this->live = ['connected' => true, 'state' => 'RUNNING', 'subtask_name' => 'Vase', 'progress' => 30.0, 'remaining_time' => 90, 'hms_errors' => []]; $this->assertSame(0, $this->scan()); $this->live['progress'] = 55.0; $this->assertSame(1, $this->scan()); $this->assertSame(0, $this->scan()); $this->assertSame('Printy · Vase is 50% complete', $this->member->notifications()->first()->data['title']); $this->live['progress'] = 80.0; $this->assertSame(1, $this->scan()); $this->live['hms_errors'] = [['description' => 'Filament tangled']]; $this->assertSame(1, $this->scan()); $this->assertSame(0, $this->scan()); $this->live = ['connected' => true, 'state' => 'FINISH', 'subtask_name' => 'Vase', 'progress' => 100.0, 'hms_errors' => []]; $this->assertSame(1, $this->scan()); $this->assertSame(0, $this->scan()); $this->assertContains('Printy · Vase is 100% complete', $this->admin->notifications->pluck('data.title')->all()); } public function test_failed_prints_notify_as_errors(): void { $this->live = ['connected' => true, 'state' => 'RUNNING', 'subtask_name' => 'Vase', 'progress' => 10.0, 'hms_errors' => []]; $this->scan(); $this->live['state'] = 'FAILED'; $this->assertSame(1, $this->scan()); $this->assertSame(0, $this->scan()); $notification = $this->admin->notifications()->first(); $this->assertSame('danger', $notification->data['level']); $this->assertSame('Printy · Vase failed', $notification->data['title']); } public function test_low_filament_notifies_once_until_the_spool_is_refilled(): void { $this->spools = [ ['id' => 1, 'color_name' => 'Azure', 'material' => 'ABS', 'brand' => 'Bambu Lab', 'label_weight' => 1000, 'weight_used' => 850, 'archived_at' => null], ['id' => 2, 'color_name' => 'Black', 'material' => 'PLA', 'label_weight' => 1000, 'weight_used' => 100, 'archived_at' => null], ['id' => 3, 'color_name' => 'Old', 'material' => 'PLA', 'label_weight' => 1000, 'weight_used' => 990, 'archived_at' => '2026-01-01T00:00:00'], ]; $this->assertSame(1, $this->scan()); $this->assertSame(0, $this->scan()); $notification = $this->admin->notifications()->first(); $this->assertSame('Low filament: Azure ABS', $notification->data['title']); $this->assertSame('Bambu Lab · 15% (about 150 g) left', $notification->data['body']); $this->spools[0]['weight_used'] = 100; $this->scan(); $this->spools[0]['weight_used'] = 900; $this->assertSame(1, $this->scan()); } }