CaiWuXiTong/tests/Unit/EcommerceFinance/DingWorklogPenaltyCalculatorTest.php
renshen c47af23709 fix: 日志漏写按公司排班判断工作日 (#1030)
合并 PR #1030:日志漏写扣分改用钉钉公司排班判断工作日。
2026-11-10 10:57:47 +08:00

243 lines
11 KiB
PHP

<?php
namespace Tests\Unit\EcommerceFinance;
use App\Services\Auth\FinanceRoster;
use Illuminate\Support\Carbon;
use Mockery;
use Modules\EcommerceFinance\Repositories\DingAttendanceMirrorRepository;
use Modules\EcommerceFinance\Repositories\DingWorklogMirrorRepository;
use Modules\EcommerceFinance\Services\DingWorklogPenaltyCalculator;
use Tests\TestCase;
class DingWorklogPenaltyCalculatorTest extends TestCase
{
public function test_penalties_double_for_each_person_and_accumulate(): void
{
config([
'dingtalk.worklog.penalty_start_date' => '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('scheduleReady')->once()->andReturnTrue();
$attendance->shouldReceive('exemptDatesForUsers')->once()->andReturn([]);
$attendance->shouldReceive('scheduledWorkDatesForUsers')
->once()
->with(['jiangling', 'qiuxiang'], '2026-10-10', '2026-10-18')
->andReturn($this->scheduledWorkDates(
['jiangling', 'qiuxiang'],
'2026-10-10',
'2026-10-18',
));
$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('scheduleReady')->once()->andReturnTrue();
$attendance->shouldReceive('exemptDatesForUsers')->once()->andReturn([]);
$attendance->shouldReceive('scheduledWorkDatesForUsers')
->once()
->with(['jiangling'], '2026-11-04', '2026-11-04')
->andReturn($this->scheduledWorkDates(['jiangling'], '2026-11-04', '2026-11-04'));
$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('scheduleReady')->once()->andReturnTrue();
$attendance->shouldReceive('exemptDatesForUsers')->once()->andReturn([]);
$attendance->shouldReceive('scheduledWorkDatesForUsers')
->once()
->with(['qiuxiang'], '2026-11-04', '2026-11-04')
->andReturn($this->scheduledWorkDates(['qiuxiang'], '2026-11-04', '2026-11-04'));
$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('scheduleReady')->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],
]);
$attendance->shouldReceive('scheduledWorkDatesForUsers')
->once()
->with(['changyi', 'yaoyao'], '2026-11-04', '2026-11-04')
->andReturn($this->scheduledWorkDates(['changyi', 'yaoyao'], '2026-11-04', '2026-11-04'));
$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']);
}
public function test_company_rest_day_is_not_charged_without_log_or_leave(): 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-08', '2026-11-08')->andReturn([]);
$roster = Mockery::mock(FinanceRoster::class);
$roster->shouldReceive('cachedFinanceMembers')->once()->andReturn([
['userid' => 'renshen', 'name' => '人参'],
['userid' => 'linchong', 'name' => '林冲'],
]);
$attendance = Mockery::mock(DingAttendanceMirrorRepository::class);
$attendance->shouldReceive('ready')->once()->andReturnTrue();
$attendance->shouldReceive('scheduleReady')->once()->andReturnTrue();
$attendance->shouldReceive('exemptDatesForUsers')->once()->andReturn([]);
$attendance->shouldReceive('scheduledWorkDatesForUsers')->once()->andReturn([]);
$result = (new DingWorklogPenaltyCalculator($repository, $roster, $attendance))->summary(
'2026-11-08',
'2026-11-08',
Carbon::parse('2026-11-10 10:00:00', 'Asia/Shanghai'),
);
$this->assertSame([], $result['members']);
$this->assertSame('0', $result['total_points']);
}
/** @param list<string> $userIds */
private function scheduledWorkDates(array $userIds, string $from, string $to): array
{
$dates = [];
for ($date = Carbon::parse($from); $date->lessThanOrEqualTo(Carbon::parse($to)); $date->addDay()) {
foreach ($userIds as $userId) {
$dates[$userId][$date->toDateString()] = true;
}
}
return $dates;
}
}