bootTenant(); // 1. Inbound lead (anonymous, no client yet). $lead = Lead::create([ 'name' => 'Ion Pop', 'phone' => '+37369123456', 'email' => 'ion@example.com', 'car' => 'BMW', 'model' => 'X5', 'message' => 'Vreau diagnoză + schimb plăcuțe', 'source' => 'site', 'status' => 'new', ]); $this->assertEquals('new', $lead->status); $this->assertNull($lead->client_id); // 2. Convert lead → Client + Deal. (Mirrors what UI Lead-convert action does.) $client = Client::create([ 'name' => $lead->name, 'phone' => $lead->phone, 'email' => $lead->email, 'type' => 'individual', 'status' => 'active', ]); $vehicle = Vehicle::create([ 'client_id' => $client->id, 'make' => $lead->car, 'model' => $lead->model, 'plate' => 'TST-001', ]); $deal = Deal::create([ 'client_id' => $client->id, 'vehicle_id' => $vehicle->id, 'name' => 'BMW X5 — frâne + diag', 'price' => 0, 'stage' => 'new', ]); $lead->update([ 'client_id' => $client->id, 'vehicle_id' => $vehicle->id, 'deal_id' => $deal->id, 'status' => 'won', 'converted_at' => now(), ]); // Verify Lead now linked. $this->assertEquals($client->id, $lead->fresh()->client_id); $this->assertEquals($deal->id, $lead->fresh()->deal_id); $this->assertNotNull($lead->fresh()->converted_at); // 3. Schedule appointment from deal. $appt = Appointment::create([ 'client_id' => $client->id, 'vehicle_id' => $vehicle->id, 'deal_id' => $deal->id, 'date' => today()->addDays(2)->toDateString(), 'time_start' => '10:00:00', 'time_end' => '12:00:00', 'title' => 'Diagnoză BMW X5', 'status' => 'scheduled', ]); $this->assertEquals($deal->id, $appt->deal_id); // 4. Open WorkOrder from appointment. $wo = WorkOrder::create([ 'number' => WorkOrder::generateNumber($ctx['company']->id), 'client_id' => $client->id, 'vehicle_id' => $vehicle->id, 'deal_id' => $deal->id, 'appointment_id' => $appt->id, 'opened_at' => today(), 'status' => 'in_work', 'complaint' => $lead->message, ]); // 5. Add labor — WO total should update via saved hook. WorkOrderWork::create([ 'work_order_id' => $wo->id, 'name' => 'Schimb plăcuțe față', 'hours' => 1.5, 'price_per_hour' => 400, 'status' => 'todo', ]); $wo->refresh(); $this->assertEquals(600.0, (float) $wo->total, '1.5h × 400 = 600'); $this->assertEquals(600.0, $wo->balanceDue(), 'no payments yet, full due'); // 6. Customer pays 250 cash. Payment::create([ 'client_id' => $client->id, 'work_order_id' => $wo->id, 'paid_at' => today(), 'amount' => 250, 'method' => 'cash', ]); $wo->refresh(); $this->assertEquals(250.0, $wo->paidAmount()); $this->assertEquals(350.0, $wo->balanceDue(), '600 − 250 = 350'); // 7. Customer pays the rest. Payment::create([ 'client_id' => $client->id, 'work_order_id' => $wo->id, 'paid_at' => today(), 'amount' => 350, 'method' => 'card', ]); $wo->refresh(); $this->assertEquals(600.0, $wo->paidAmount()); $this->assertEquals(0.0, $wo->balanceDue()); // 8. Close the WO. master_id wasn't set, so no push attempt → no error. $wo->update(['status' => 'done', 'closed_at' => today(), 'pay_status' => 'paid']); $this->assertEquals('done', $wo->fresh()->status); // 9. The Deal can now be marked won. $deal->update(['stage' => 'done', 'price' => $wo->total, 'won_at' => now()]); $this->assertEquals('done', $deal->fresh()->stage); } public function test_lead_status_transitions_persist(): void { $this->bootTenant(); $lead = Lead::create(['name' => 'X', 'phone' => '+1', 'status' => 'new']); foreach (['contacted', 'won', 'lost'] as $st) { $lead->update(['status' => $st]); $this->assertEquals($st, $lead->fresh()->status); } } public function test_payment_negative_amount_not_silently_increases_balance(): void { $ctx = $this->bootTenant(); $client = Client::create(['name' => 'X', 'phone' => '+1', 'type' => 'individual', 'status' => 'active']); $vehicle = Vehicle::create(['client_id' => $client->id, 'make' => 'X', 'model' => 'Y', 'plate' => 'A1']); $wo = WorkOrder::create([ 'number' => WorkOrder::generateNumber($ctx['company']->id), 'client_id' => $client->id, 'vehicle_id' => $vehicle->id, 'opened_at' => today(), 'status' => 'in_work', ]); WorkOrderWork::create([ 'work_order_id' => $wo->id, 'name' => 'X', 'hours' => 1, 'price_per_hour' => 100, 'status' => 'todo', ]); $wo->refresh(); $this->assertEquals(100.0, $wo->balanceDue()); // A negative payment (refund) is supported and should INCREASE balance back. Payment::create([ 'client_id' => $client->id, 'work_order_id' => $wo->id, 'paid_at' => today(), 'amount' => -50, 'method' => 'cash', ]); $wo->refresh(); $this->assertEquals(-50.0, $wo->paidAmount(), 'negative paidAmount means refund'); $this->assertEquals(150.0, $wo->balanceDue(), 'refund of 50 adds to outstanding: 100 − (−50) = 150'); } private function bootTenant(): array { $plan = Plan::firstOrCreate(['slug' => 'test'], ['name' => 'T', 'price' => 0, 'features' => []]); $company = Company::create([ 'plan_id' => $plan->id, 'slug' => 'crm-' . uniqid(), 'name' => 'CRM Co', 'status' => 'active', ]); app(TenantManager::class)->setCurrent($company); return compact('company'); } }