seedConfig(); DB::table('additional_charges')->insert([ 'parent_id' => 10, 'school_year' => '2025-2026', 'semester' => 'Fall', 'charge_type' => 'add', 'title' => 'Books', 'amount' => 25.00, 'status' => 'pending', ]); $service = $this->makeService(); $result = $service->createExtraCharge([ 'parent_id' => 10, 'title' => 'Books', 'amount' => 25, 'charge_type' => 'add', ]); $this->assertFalse($result['ok']); $this->assertSame('duplicate_charge', $result['error']); } public function test_create_event_charge_blocks_duplicate(): void { $this->seedConfig(); DB::table('event_charges')->insert([ 'parent_id' => 10, 'student_id' => 1, 'event_name' => 'Field Trip', 'participation' => 'yes', 'charged' => 25.00, 'amount' => 25.00, 'school_year' => '2025-2026', 'semester' => 'Fall', ]); $service = $this->makeService(); $result = $service->createEventCharge([ 'parent_id' => 10, 'student_id' => 1, 'event_name' => 'Field Trip', 'amount' => 25, ]); $this->assertFalse($result['ok']); $this->assertSame('duplicate_charge', $result['error']); } public function test_create_event_charge_succeeds(): void { $this->seedConfig(); $service = $this->makeService(); $result = $service->createEventCharge([ 'parent_id' => 10, 'student_id' => 1, 'event_name' => 'Camp', 'amount' => 40, 'created_by' => 1, ]); $this->assertTrue($result['ok']); $this->assertDatabaseHas('event_charges', [ 'parent_id' => 10, 'student_id' => 1, 'event_name' => 'Camp', 'participation' => 'yes', ]); } public function test_cancel_event_charge_zeros_charge_and_creates_credit(): void { $this->seedConfig(); $chargeId = DB::table('event_charges')->insertGetId([ 'parent_id' => 10, 'student_id' => 1, 'event_name' => 'Trip', 'participation' => 'yes', 'charged' => 30.00, 'amount' => 30.00, 'school_year' => '2025-2026', 'semester' => 'Fall', ]); $service = $this->makeService(); $result = $service->cancelEventCharge((int) $chargeId, true); $this->assertTrue($result['ok']); $this->assertSame('canceled', $result['cancellation_status']); $charge = EventCharges::query()->find($chargeId); $this->assertSame('no', $charge->participation); $this->assertSame(0.0, (float) $charge->charged); $this->assertDatabaseHas('additional_charges', [ 'parent_id' => 10, 'charge_type' => 'deduct', 'status' => 'pending', ]); } public function test_apply_extra_charge_marks_applied(): void { $this->seedConfig(); $invoiceId = DB::table('invoices')->insertGetId([ 'parent_id' => 10, 'invoice_number' => 'INV-10', 'total_amount' => 100.00, 'balance' => 100.00, 'paid_amount' => 0.00, 'issue_date' => '2025-01-01', 'school_year' => '2025-2026', 'semester' => 'Fall', ]); $chargeId = DB::table('additional_charges')->insertGetId([ 'parent_id' => 10, 'school_year' => '2025-2026', 'semester' => 'Fall', 'charge_type' => 'add', 'title' => 'Lab fee', 'amount' => 15.00, 'status' => 'pending', ]); $service = $this->makeService(); $result = $service->applyExtraCharge((int) $chargeId, (int) $invoiceId); $this->assertTrue($result['ok']); $charge = AdditionalCharge::query()->find($chargeId); $this->assertSame('applied', $charge->status); $this->assertSame($invoiceId, (int) $charge->invoice_id); } public function test_list_for_parent_returns_both_charge_types(): void { $this->seedConfig(); DB::table('additional_charges')->insert([ 'parent_id' => 10, 'school_year' => '2025-2026', 'semester' => 'Fall', 'charge_type' => 'add', 'title' => 'Uniform', 'amount' => 20.00, 'status' => 'pending', ]); DB::table('events')->insert([ 'id' => 1, 'event_name' => 'Trip', 'amount' => 25.00, 'expiration_date' => '2025-06-01', ]); DB::table('event_charges')->insert([ 'event_id' => 1, 'parent_id' => 10, 'student_id' => 1, 'participation' => 'yes', 'charged' => 25.00, 'school_year' => '2025-2026', 'semester' => 'Fall', ]); $service = $this->makeService(); $list = $service->listForParent(10, '2025-2026'); $this->assertCount(1, $list['extra_charges']); $this->assertCount(1, $list['event_charges']); $this->assertSame('extra', $list['extra_charges'][0]['charge_type']); $this->assertSame('event', $list['event_charges'][0]['charge_type']); } private function makeService(): ChargeService { $invoiceGen = Mockery::mock(InvoiceGenerationService::class); $invoiceGen->shouldReceive('generateInvoice')->andReturn(['ok' => true]); return new ChargeService( app(ExtraChargesChargeService::class), app(ExtraChargesInvoiceService::class), app(ExtraChargesContextService::class), $invoiceGen ); } private function seedConfig(): void { DB::table('configuration')->insert([ ['config_key' => 'school_year', 'config_value' => '2025-2026'], ['config_key' => 'semester', 'config_value' => 'Fall'], ]); } }