From ab784fb4ece290b2f0958ffe965f6dc91813ff2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 9 Jul 2026 20:04:02 +0300 Subject: [PATCH] =?UTF-8?q?feat(supplier):=20SupplierOrderPlan::build=20?= =?UTF-8?q?=E2=80=94=20=D0=B7=D0=B0=D0=B4=D1=83=D0=BC=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D1=84=D0=BE=D1=80=D0=BC=D1=83=D0=BB=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Services/Supplier/SupplierOrderPlan.php | 74 +++++++++++++++++++ .../Unit/Supplier/SupplierOrderPlanTest.php | 45 +++++++++++ 2 files changed, 119 insertions(+) create mode 100644 app/app/Services/Supplier/SupplierOrderPlan.php create mode 100644 app/tests/Unit/Supplier/SupplierOrderPlanTest.php diff --git a/app/app/Services/Supplier/SupplierOrderPlan.php b/app/app/Services/Supplier/SupplierOrderPlan.php new file mode 100644 index 00000000..6cc341df --- /dev/null +++ b/app/app/Services/Supplier/SupplierOrderPlan.php @@ -0,0 +1,74 @@ + list], где row = ['signal_type','identifier','platform','limit'], + * только доли ≥1 (как distributeForPlatform). + * + * Spec: docs/superpowers/specs/2026-07-09-supplier-order-verification-and-deadline-watch-design.md §3, §4.2 + */ +final class SupplierOrderPlan +{ + /** + * @param Collection $eligibleProjects проекты со slepok-полями (daily_limit_target/delivery_days_mask/regions) + * @return array{intended: list} + */ + public static function build(Collection $eligibleProjects, Carbon $targetDate): array + { + $targetWeekday = $targetDate->copy()->timezone('Europe/Moscow')->isoWeekday(); + + // Группировка (signal_type|identifier) — как в SyncSupplierProjectsJob. + /** @var array, limits:list}> $groups */ + $groups = []; + + foreach ($eligibleProjects as $project) { + $platforms = SupplierProjectGrouping::resolvePlatforms($project); + if ($platforms === []) { + continue; + } + // eligible-today (маска дня на targetDate). + if (((int) $project->delivery_days_mask & (1 << ($targetWeekday - 1))) === 0) { + continue; + } + + $identifier = SupplierProjectGrouping::buildUniqueKeyAgnostic($project); + $key = $project->signal_type.'|'.$identifier; + + if (! isset($groups[$key])) { + $groups[$key] = [ + 'signal_type' => (string) $project->signal_type, + 'identifier' => $identifier, + 'platforms' => $platforms, + 'limits' => [], + ]; + } + $groups[$key]['limits'][] = (int) $project->daily_limit_target; + } + + $intended = []; + foreach ($groups as $group) { + $order = SupplierQuotaAllocator::computeOrder($group['limits']); + $shares = SupplierQuotaAllocator::distributeForPlatform($order, $group['platforms']); + foreach ($shares as $platform => $limit) { + $intended[] = [ + 'signal_type' => $group['signal_type'], + 'identifier' => $group['identifier'], + 'platform' => $platform, + 'limit' => $limit, + ]; + } + } + + return ['intended' => $intended]; + } +} diff --git a/app/tests/Unit/Supplier/SupplierOrderPlanTest.php b/app/tests/Unit/Supplier/SupplierOrderPlanTest.php new file mode 100644 index 00000000..54f4d35c --- /dev/null +++ b/app/tests/Unit/Supplier/SupplierOrderPlanTest.php @@ -0,0 +1,45 @@ +isoWeekday() - 1); // бит пн + + $p1 = new Project(['signal_type' => 'call', 'signal_identifier' => '79135397707']); + $p1->daily_limit_target = 3; + $p1->delivery_days_mask = $mask; + $p1->regions = []; + + $p2 = new Project(['signal_type' => 'call', 'signal_identifier' => '79135397707']); + $p2->daily_limit_target = 2; + $p2->delivery_days_mask = $mask; + $p2->regions = []; + + $plan = SupplierOrderPlan::build(collect([$p1, $p2]), $tomorrow); + + // order = max(3, ceil(5/3)=2) = 3; split B1/B2/B3 = 1/1/1 + expect($plan['intended'])->toEqualCanonicalizing([ + ['signal_type' => 'call', 'identifier' => '79135397707', 'platform' => 'B1', 'limit' => 1], + ['signal_type' => 'call', 'identifier' => '79135397707', 'platform' => 'B2', 'limit' => 1], + ['signal_type' => 'call', 'identifier' => '79135397707', 'platform' => 'B3', 'limit' => 1], + ]); +}); + +it('проект не eligible на завтра (нет бита дня) → не попадает в intended', function (): void { + $tomorrow = Carbon::parse('2026-07-13'); // пн + $notMondayMask = 1 << (Carbon::parse('2026-07-14')->isoWeekday() - 1); // вт + + $p = new Project(['signal_type' => 'call', 'signal_identifier' => '7911']); + $p->daily_limit_target = 5; + $p->delivery_days_mask = $notMondayMask; + $p->regions = []; + + $plan = SupplierOrderPlan::build(collect([$p]), $tomorrow); + + expect($plan['intended'])->toBe([]); +});