'2026-10-10', 'dingtalk.worklog.penalty_excluded_user_ids' => [], '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' => '秋香'], ]); $attendance = Mockery::mock(DingAttendanceMirrorRepository::class); $attendance->shouldReceive('ready')->once()->andReturnTrue(); $attendance->shouldReceive('exemptDatesForUsers')->once()->andReturn([]); $result = (new DingWorklogPenaltyCalculator($repository, $roster, $attendance))->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', 'dingtalk.worklog.penalty_excluded_user_ids' => [], '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' => '江陵'], ]); $attendance = Mockery::mock(DingAttendanceMirrorRepository::class); $attendance->shouldReceive('ready')->once()->andReturnTrue(); $attendance->shouldReceive('exemptDatesForUsers')->once()->andReturn([]); $result = (new DingWorklogPenaltyCalculator($repository, $roster, $attendance))->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']); } public function test_excluded_members_do_not_receive_penalty_points(): void { config([ 'dingtalk.worklog.penalty_start_date' => '2026-11-04', 'dingtalk.worklog.penalty_excluded_user_ids' => [ '02591267217229', '17097754648162977', ], 'app.timezone' => 'Asia/Shanghai', ]); $repository = Mockery::mock(DingWorklogMirrorRepository::class); $repository->shouldReceive('earliestStatisticsDate')->once()->andReturn('2026-11-04'); $repository->shouldReceive('submittedDatesByUser') ->once()->with('2026-11-04', '2026-11-04')->andReturn([]); $roster = Mockery::mock(FinanceRoster::class); $roster->shouldReceive('cachedFinanceMembers')->once()->andReturn([ ['userid' => '02591267217229', 'name' => '江陵'], ['userid' => '17097754648162977', 'name' => '周生辰'], ['userid' => 'qiuxiang', 'name' => '秋香'], ]); $attendance = Mockery::mock(DingAttendanceMirrorRepository::class); $attendance->shouldReceive('ready')->once()->andReturnTrue(); $attendance->shouldReceive('exemptDatesForUsers')->once()->andReturn([]); $result = (new DingWorklogPenaltyCalculator($repository, $roster, $attendance))->summary( '2026-11-04', '2026-11-04', Carbon::parse('2026-11-05 13:00:00', 'Asia/Shanghai'), ); $this->assertSame(['秋香'], array_column($result['members'], 'name')); $this->assertSame('10', $result['total_points']); } public function test_approved_leave_or_rest_days_are_not_charged(): void { config([ 'dingtalk.worklog.penalty_start_date' => '2026-11-04', 'dingtalk.worklog.penalty_excluded_user_ids' => [], 'app.timezone' => 'Asia/Shanghai', ]); $repository = Mockery::mock(DingWorklogMirrorRepository::class); $repository->shouldReceive('earliestStatisticsDate')->once()->andReturn('2026-11-04'); $repository->shouldReceive('submittedDatesByUser') ->once()->with('2026-11-04', '2026-11-04')->andReturn([]); $roster = Mockery::mock(FinanceRoster::class); $roster->shouldReceive('cachedFinanceMembers')->once()->andReturn([ ['userid' => 'changyi', 'name' => '常仪'], ['userid' => 'yaoyao', 'name' => '杳杳'], ]); $attendance = Mockery::mock(DingAttendanceMirrorRepository::class); $attendance->shouldReceive('ready')->once()->andReturnTrue(); $attendance->shouldReceive('exemptDatesForUsers') ->once() ->with(['changyi', 'yaoyao'], '2026-11-04', '2026-11-04') ->andReturn([ 'changyi' => ['2026-11-04' => true], 'yaoyao' => ['2026-11-04' => true], ]); $result = (new DingWorklogPenaltyCalculator($repository, $roster, $attendance))->summary( '2026-11-04', '2026-11-04', Carbon::parse('2026-11-05 13:00:00', 'Asia/Shanghai'), ); $this->assertSame([], $result['members']); $this->assertSame('0', $result['total_points']); } }