'2026-10-10', 'app.timezone' => 'Asia/Shanghai', ]); $submitted = []; $misses = [ 'jiangling' => ['2026-10-10', '2026-10-12', '2026-10-18'], 'qiuxiang' => ['2026-10-12', '2026-10-18'], ]; foreach (['jiangling' => '江陵', 'qiuxiang' => '秋香'] as $userId => $name) { for ($day = 10; $day <= 18; $day++) { $date = sprintf('2026-10-%02d', $day); if (! in_array($date, $misses[$userId], true)) { $submitted[] = [ 'creator_userid' => $userId, 'creator_name' => $name, 'statistics_date' => $date, ]; } } } $repository = Mockery::mock(DingWorklogMirrorRepository::class); $repository->shouldReceive('earliestStatisticsDate')->once()->andReturn('2026-10-01'); $repository->shouldReceive('submittedDatesByUser') ->once()->with('2026-10-10', '2026-10-18')->andReturn($submitted); $roster = Mockery::mock(FinanceRoster::class); $roster->shouldReceive('cachedFinanceMembers')->once()->andReturn([ ['userid' => 'jiangling', 'name' => '江陵'], ['userid' => 'qiuxiang', 'name' => '秋香'], ]); $result = (new DingWorklogPenaltyCalculator($repository, $roster))->summary( '2026-10-10', '2026-10-18', Carbon::parse('2026-10-20 10:00:00', 'Asia/Shanghai'), ); $this->assertSame('2026-10-10', $result['start_date']); $this->assertSame('2026-10-18', $result['end_date']); $this->assertSame('江陵', $result['members'][0]['name']); $this->assertSame(['2026-10-10', '2026-10-12', '2026-10-18'], $result['members'][0]['missed_dates']); $this->assertSame(3, $result['members'][0]['missed_count']); $this->assertSame('10 + 20 + 40', $result['members'][0]['penalty_formula']); $this->assertSame('70', $result['members'][0]['penalty_points']); $this->assertSame('秋香', $result['members'][1]['name']); $this->assertSame('10 + 20', $result['members'][1]['penalty_formula']); $this->assertSame('30', $result['members'][1]['penalty_points']); $this->assertSame('100', $result['total_points']); $this->assertSame('100', $result['total_points_formatted']); } public function test_dates_before_november_fourth_are_not_charged(): void { config([ 'dingtalk.worklog.penalty_start_date' => '2026-11-04', 'app.timezone' => 'Asia/Shanghai', ]); $repository = Mockery::mock(DingWorklogMirrorRepository::class); $repository->shouldReceive('earliestStatisticsDate')->once()->andReturn('2026-10-21'); $repository->shouldReceive('submittedDatesByUser') ->once()->with('2026-11-04', '2026-11-04')->andReturn([]); $roster = Mockery::mock(FinanceRoster::class); $roster->shouldReceive('cachedFinanceMembers')->once()->andReturn([ ['userid' => 'jiangling', 'name' => '江陵'], ]); $result = (new DingWorklogPenaltyCalculator($repository, $roster))->summary( '2026-10-22', '2026-11-04', Carbon::parse('2026-11-05 13:00:00', 'Asia/Shanghai'), ); $this->assertSame('2026-11-04', $result['start_date']); $this->assertSame('2026-11-04', $result['end_date']); $this->assertSame(['2026-11-04'], $result['members'][0]['missed_dates']); $this->assertSame('10', $result['total_points']); } }