FIX EV3-466 hétvégi szállítási napok engedélyezése a heti sablonból

A DeliveryCalendarService 4. prioritása a WorkCalendarService::isWorkDay()-t
hívta, ami a naptárban nem szereplő napoknál a hétvégére esett vissza. Így egy
hétvégét is tartalmazó sablon szombat/vasárnap mezőit sosem érte el a
kiértékelés (5. prioritás), és csak egyedi felülbírálással lehetett hétvégi
szállítást engedni.

- WorkCalendarService: új isHoliday(), ami csak a work_calendars ünnepnap
  rekordjaira ad true-t, a naptári hétvégére nem. Az isWorkDay() változatlan.
- DeliveryCalendarService: a 4. prioritás mostantól isHoliday()-t használ, így
  a hétvégéről a heti sablon dönt. Ünnepnap továbbra is tilt.
- NextDeliveryDateCalculatorService: a szállítási nap keresése kikerült az
  isWorkDay() blokkból, különben a hétvége a következő szállítási nap
  számításánál továbbra is kimaradt volna. Az átfutási időbe az új
  countsTowardLeadTime() szerint a sablon hétvégi napjai is beleszámítanak,
  a H-P sablonok viselkedése változatlan.
- DeliveryCalendarWidget: a szürke "inaktív" festés csak a nem szállítási
  hétvégékre megy rá. Mellékesen a szállítási napok ciklusa $date->addDay()-jel
  mutálta a kollekció Carbon objektumait, ez copy()-ra javítva.

7 új teszt, köztük a H-P sablon regresszió-őre.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
E98Developer 2026-08-24 15:59:59 +02:00
parent 3cfaa6c9d5
commit d26ba5e5e6
6 changed files with 225 additions and 16 deletions

View File

@ -139,12 +139,17 @@ public function fetchEvents(array $info): array
$assignment
);
// A szállítási napok halmaza a hétvége-festéshez (4. pont) is kell.
$deliveryDayLookup = array_flip(
$deliveryDates->map(fn (Carbon $date) => $date->format('Y-m-d'))->all()
);
foreach ($deliveryDates as $date) {
$events[] = [
'id' => 'delivery-' . $date->format('Y-m-d'),
'title' => '',
'start' => $date->format('Y-m-d'),
'end' => $date->addDay()->format('Y-m-d'),
'end' => $date->copy()->addDay()->format('Y-m-d'),
'allDay' => true,
'display' => 'background',
'backgroundColor' => '#4CAF50',
@ -182,9 +187,11 @@ public function fetchEvents(array $info): array
}
// 4. Hétvégék szürke háttér (inaktív napok)
// Csak azok a hétvégi napok, amelyek nem szállítási napok: a heti sablon
// tartalmazhatja a szombatot/vasárnapot is (EV3-466).
$current = $start->copy();
while ($current->lte($end)) {
if ($current->isWeekend()) {
if ($current->isWeekend() && ! isset($deliveryDayLookup[$current->format('Y-m-d')])) {
$events[] = [
'id' => 'weekend-' . $current->format('Y-m-d'),
'title' => '',

View File

@ -72,9 +72,11 @@ public function isDeliveryDay(ProfitCenter $pc, Supplier $supplier, Carbon $date
return $override->is_delivery_day;
}
// 4. Prioritás: Hivatalos ünnepnap (WorkCalendar)
// Ha aznap nem hivatalos munkanap van, akkor nincs szállítás (kivéve ha az 1-3. pont felülbírálta).
if (! $this->workCalendarService->isWorkDay($date)) {
// 4. Prioritás: Hivatalos munkaszüneti nap (WorkCalendar)
// Csak a naptárban ünnepnapként rögzített dátum tilt. A naptári hétvége önmagában
// NEM: azt az 5. pont heti sablonja dönti el, különben egy hétvégét is tartalmazó
// sablon napjai elérhetetlenek maradnának (EV3-466).
if ($this->workCalendarService->isHoliday($date)) {
return false;
}

View File

@ -62,36 +62,65 @@ public function calculate(
}
}
return $this->findNextDeliveryDay($startDay, $supplier, $pc, $leadDays);
return $this->findNextDeliveryDay($startDay, $supplier, $pc, $leadDays, $assignment);
}
/**
* Munkanapokban számol előre ($leadDays darabot), majd az első olyan napot adja vissza,
* Előreszámol $leadDays darab átfutási napot, majd az első olyan napot adja vissza,
* amelyik szállítási nap is.
*
* A szállítási nap keresése NINCS munkanaphoz kötve: hétvégi szállítást a heti sablon
* engedhet, ezt az isDeliveryDay() dönti el (EV3-466).
*/
private function findNextDeliveryDay(
Carbon $candidate,
Supplier $supplier,
ProfitCenter $pc,
int $leadDays,
ProfitCenterSupplierSchedule $assignment,
): ?Carbon {
$workingDaysCounted = 0;
$leadDaysCounted = 0;
// Max. 365 nap iteráció (végtelen ciklus-védelem)
for ($i = 0; $i < 365; $i++) {
if ($this->workCalendarService->isWorkDay($candidate)) {
if ($workingDaysCounted >= $leadDays) {
// Átfutási idő letelt az első szállítási napot keressük
if ($this->deliveryCalendarService->isDeliveryDay($pc, $supplier, $candidate)) {
return $candidate->copy();
}
} else {
$workingDaysCounted++;
if ($leadDaysCounted >= $leadDays) {
// Átfutási idő letelt az első szállítási napot keressük
if ($this->deliveryCalendarService->isDeliveryDay($pc, $supplier, $candidate, $assignment)) {
return $candidate->copy();
}
} elseif ($this->countsTowardLeadTime($candidate, $assignment)) {
$leadDaysCounted++;
}
$candidate->addDay();
}
return null;
}
/**
* Eldönti, hogy az adott nap beleszámít-e az átfutási idő visszaszámolásába.
*
* A hivatalos munkanapokon túl beleszámítanak azok a hétvégi napok is, amelyeket a
* PC-hez rendelt heti sablon szállítási napként jelöl egy 7 napos sablonnál tehát
* minden nap telik, egy HP sablonnál viszont a hétvége változatlanul nem (EV3-466).
* Munkaszüneti napon sosem telik az átfutás.
*/
private function countsTowardLeadTime(Carbon $date, ProfitCenterSupplierSchedule $assignment): bool
{
if ($this->workCalendarService->isHoliday($date)) {
return false;
}
if ($this->workCalendarService->isWorkDay($date)) {
return true;
}
$schedule = $assignment->deliverySchedule;
if (! $schedule || ! $schedule->is_active) {
return false;
}
return (bool) $schedule->{strtolower($date->format('l'))};
}
}

View File

@ -110,6 +110,21 @@ public function isWorkDay(Carbon $date): bool
return !$date->isWeekend();
}
/**
* Megállapítja, hogy az adott nap hivatalos munkaszüneti nap-e.
*
* Az isWorkDay()-jel ellentétben a puszta naptári hétvégét NEM tekinti annak:
* csak a work_calendars táblában ünnepnapként rögzített dátumra ad true-t.
* A szállítási naptár ezt használja, hogy a heti sablon engedhessen hétvégi
* szállítást is (EV3-466).
*/
public function isHoliday(Carbon $date): bool
{
$record = WorkCalendar::where('date', $date->toDateString())->first();
return $record?->type === WorkDayType::Holiday;
}
/**
* Kiszámítja a következő érvényes munkanapot.
*/

View File

@ -264,3 +264,78 @@
expect($dateStrings)->toContain('2026-04-15');
expect($dateStrings)->toContain('2026-04-17');
});
it('allows weekend delivery when the schedule includes it', function () {
$pc = ProfitCenter::factory()->create();
$supplier = Supplier::factory()->create();
$schedule = DeliverySchedule::factory()->create(['saturday' => true]);
ProfitCenterSupplierSchedule::factory()->create([
'profit_center_id' => $pc->id,
'supplier_id' => $supplier->id,
'delivery_schedule_id' => $schedule->id,
]);
$service = app(DeliveryCalendarService::class);
$saturday = Carbon::parse('2026-04-18'); // Saturday
expect($service->isDeliveryDay($pc, $supplier, $saturday))->toBeTrue();
});
it('still blocks weekends the schedule does not include', function () {
$pc = ProfitCenter::factory()->create();
$supplier = Supplier::factory()->create();
$schedule = DeliverySchedule::factory()->create(['monday' => true, 'saturday' => false]);
ProfitCenterSupplierSchedule::factory()->create([
'profit_center_id' => $pc->id,
'supplier_id' => $supplier->id,
'delivery_schedule_id' => $schedule->id,
]);
$service = app(DeliveryCalendarService::class);
$saturday = Carbon::parse('2026-04-18'); // Saturday
expect($service->isDeliveryDay($pc, $supplier, $saturday))->toBeFalse();
});
it('blocks official holidays even on a scheduled weekend day', function () {
$pc = ProfitCenter::factory()->create();
$supplier = Supplier::factory()->create();
$schedule = DeliverySchedule::factory()->create(['saturday' => true]);
ProfitCenterSupplierSchedule::factory()->create([
'profit_center_id' => $pc->id,
'supplier_id' => $supplier->id,
'delivery_schedule_id' => $schedule->id,
]);
WorkCalendar::create([
'date' => '2026-04-18',
'type' => WorkDayType::Holiday,
'data_source' => DataSource::Manual,
]);
$service = app(DeliveryCalendarService::class);
$saturday = Carbon::parse('2026-04-18'); // Saturday
expect($service->isDeliveryDay($pc, $supplier, $saturday))->toBeFalse();
});
it('includes scheduled weekend days in an available date range', function () {
$pc = ProfitCenter::factory()->create();
$supplier = Supplier::factory()->create();
$schedule = DeliverySchedule::factory()->create([
'monday' => true,
'tuesday' => true,
'wednesday' => true,
'thursday' => true,
'friday' => true,
'saturday' => true,
'sunday' => true,
]);
ProfitCenterSupplierSchedule::factory()->create([
'profit_center_id' => $pc->id,
'supplier_id' => $supplier->id,
'delivery_schedule_id' => $schedule->id,
]);
$service = app(DeliveryCalendarService::class);
$dates = $service->getAvailableDeliveryDates(
$pc,
$supplier,
Carbon::parse('2026-04-13'), // Monday
Carbon::parse('2026-04-19'), // Sunday
);
expect($dates)->toHaveCount(7);
});

View File

@ -0,0 +1,81 @@
<?php
use App\Models\DeliverySchedule;
use App\Models\ProfitCenter;
use App\Models\ProfitCenterSupplierSchedule;
use App\Models\Supplier;
use App\Services\NextDeliveryDateCalculatorService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
/**
* @param array<string, bool> $days
*/
function makeAssignment(array $days, bool $hasConstraint = true): array
{
$pc = ProfitCenter::factory()->create();
$supplier = Supplier::factory()->create([
'hasDeliveryConstraint' => $hasConstraint,
'orderCutOffTime' => 12,
'deliveryLeadTime' => 48, // 2 nap
]);
$schedule = DeliverySchedule::factory()->create($days);
ProfitCenterSupplierSchedule::factory()->create([
'profit_center_id' => $pc->id,
'supplier_id' => $supplier->id,
'delivery_schedule_id' => $schedule->id,
]);
return [$pc, $supplier];
}
const EVERY_DAY = [
'monday' => true,
'tuesday' => true,
'wednesday' => true,
'thursday' => true,
'friday' => true,
'saturday' => true,
'sunday' => true,
];
const WEEKDAYS_ONLY = [
'monday' => true,
'tuesday' => true,
'wednesday' => true,
'thursday' => true,
'friday' => true,
];
it('counts scheduled weekend days towards the lead time', function () {
[$pc, $supplier] = makeAssignment(EVERY_DAY);
$result = app(NextDeliveryDateCalculatorService::class)
->calculate($supplier->id, $pc->id, Carbon::parse('2026-04-17 08:00')); // Friday, cut-off előtt
// Péntek (1.) + szombat (2.) telik el az átfutásból, így vasárnap már szállítható.
expect($result?->toDateString())->toBe('2026-04-19'); // Sunday
});
it('does not count weekends for a weekday-only schedule', function () {
[$pc, $supplier] = makeAssignment(WEEKDAYS_ONLY);
$result = app(NextDeliveryDateCalculatorService::class)
->calculate($supplier->id, $pc->id, Carbon::parse('2026-04-17 08:00')); // Friday, cut-off előtt
// Péntek (1.) + hétfő (2.) telik el a hétvége kimarad , így kedd az első szállítási nap.
expect($result?->toDateString())->toBe('2026-04-21'); // Tuesday
});
it('returns a weekend day when there is no delivery constraint', function () {
[$pc, $supplier] = makeAssignment(EVERY_DAY, hasConstraint: false);
$result = app(NextDeliveryDateCalculatorService::class)
->calculate($supplier->id, $pc->id, Carbon::parse('2026-04-18 08:00')); // Saturday
// Átfutási idő nélkül a szombat maga az első szállítási nap.
expect($result?->toDateString())->toBe('2026-04-18'); // Saturday
});