isoWeekday() - 1) * isoWeekday(): Monday=1, Tuesday=2, ..., Sunday=7 * → Monday = 1<<0 = 1 * → Tuesday = 1<<1 = 2 * → ... * → Sunday = 1<<6 = 64 * → Full week mask = 127 (bits 0-6 all set) * * SnapshotForge::activeDate() mirrors LeadRouter::activeSnapshotDate(): * - Before 21:00 MSK → today's date * - From 21:00 MSK → tomorrow's date * snapshot:rebuild filters by that date's isoWeekday bit. * * Setup: * - One shared SupplierProject (B1 site signal). * - Two tenants / projects on that supplier. * - ACTIVE client: delivery_days_mask = 127 (all days — includes any day). * - INACTIVE client: delivery_days_mask = full-week MINUS today's bit (excludes active date). * - Both tenants have ample balance and no freeze. * - SnapshotForge::rebuild() called AFTER masks are set. * - One lead injected. * * Expected (per plan §6.2 D): * - ACTIVE client receives the deal. * - INACTIVE client receives ZERO deals (not in snapshot → invisible to LeadRouter). * * Subject code: Москва = 82 (порядковый, НЕ ГИБДД). * * Task 8 — Phase 1 Portal Client Imitation, Scenario D. * Spec: docs/superpowers/specs/2026-06-03-portal-client-imitation-phase1-design.md §6.2 D * Plan: docs/superpowers/plans/2026-06-03-portal-client-imitation-phase1.md §Task 8 */ // ── SUBJECT CODE ────────────────────────────────────────────────────────────── /** App\Support\RussianRegions::CODE_TO_NAME[82] = 'Москва' */ const D_MOSCOW_CODE = 82; // ── SUPPLIER SIGNAL ─────────────────────────────────────────────────────────── const D_SUPPLIER_DOMAIN = 'scenario-d-delivery-days.ru'; const D_SUPPLIER_PLATFORM = 'B1'; // ── DETERMINISTIC SEED ──────────────────────────────────────────────────────── const D_SEED = 19; // ── PHONE NUMBERS ───────────────────────────────────────────────────────────── /** Phone resolving to Москва via FakeDaDataPhoneClient. */ const D_PHONE_1 = '79270000099'; beforeEach(function (): void { // Pricing tiers — required by LedgerService::chargeForDelivery. $this->seed(PricingTierSeeder::class); // Global RLS bypass for seeding phase (tenant context = 0). DB::statement("SELECT set_config('app.current_tenant_id', '0', true)"); // DaData config — FakeDaDataPhoneClient bypasses HTTP entirely. config([ 'services.dadata.enabled' => true, 'services.dadata.api_key' => 'fake-key', 'services.dadata.secret' => 'fake-secret', 'services.dadata.daily_cap_rub' => 1_000_000, ]); // Bind deterministic LeadRouter (small cap — only 2 projects, so no real lottery needed, // but we seed for reproducibility). app()->instance(LeadRouter::class, new LeadRouter(new Randomizer(new Mt19937(D_SEED)))); }); it('only delivers to the active-today client; the excluded-day client gets zero deals', function (): void { // ── DETERMINE ACTIVE DATE AND ITS WEEKDAY BIT ──────────────────────────── // // SnapshotForge::activeDate() mirrors LeadRouter::activeSnapshotDate(). // SnapshotRebuildCommand: weekdayBit = 1 << (date->isoWeekday() - 1). // isoWeekday(): Monday=1 .. Sunday=7. // // We MUST compute the bit from the active-snapshot date (not wall-clock today), // because after 21:00 MSK the active date flips to tomorrow. $activeDate = SnapshotForge::activeDate(); // 'YYYY-MM-DD' $activeDateObj = Carbon::parse($activeDate, 'Europe/Moscow'); $todayBit = 1 << ($activeDateObj->isoWeekday() - 1); // e.g. Wednesday=4 // ACTIVE mask: full week (includes every day, including today). $activeMask = 127; // 0b1111111 // INACTIVE mask: full week MINUS today's bit → excludes the active snapshot date. // snapshot:rebuild WHERE (delivery_days_mask & weekdayBit) <> 0 will skip this project. $inactiveMask = 127 & ~$todayBit; // clears the bit for the active date's weekday // Sanity: active mask passes the filter, inactive mask does not. expect(($activeMask & $todayBit) !== 0)->toBeTrue(); expect(($inactiveMask & $todayBit))->toBe(0); // ── ARRANGE: SHARED SUPPLIER PROJECT ───────────────────────────────────── $supplier = SupplierProject::factory()->create([ 'platform' => D_SUPPLIER_PLATFORM, 'signal_type' => 'site', 'unique_key' => D_SUPPLIER_DOMAIN, ]); // ── ARRANGE: ACTIVE TENANT / PROJECT ───────────────────────────────────── $tenantActive = Tenant::factory()->create([ 'balance_rub' => '9999.00', 'frozen_by_balance_at' => null, ]); $projectActive = Project::factory()->create([ 'tenant_id' => $tenantActive->id, 'is_active' => true, 'signal_type' => 'site', 'signal_identifier' => D_SUPPLIER_DOMAIN, 'daily_limit_target' => 10, 'effective_daily_limit_today' => null, 'delivered_today' => 0, 'delivered_in_month' => 0, 'delivery_days_mask' => $activeMask, // all days — included today 'preflight_blocked_at' => null, 'regions' => [D_MOSCOW_CODE], ]); linkProjectToSupplier($projectActive, $supplier); // ── ARRANGE: INACTIVE TENANT / PROJECT ─────────────────────────────────── $tenantInactive = Tenant::factory()->create([ 'balance_rub' => '9999.00', 'frozen_by_balance_at' => null, ]); $projectInactive = Project::factory()->create([ 'tenant_id' => $tenantInactive->id, 'is_active' => true, 'signal_type' => 'site', 'signal_identifier' => D_SUPPLIER_DOMAIN, 'daily_limit_target' => 10, 'effective_daily_limit_today' => null, 'delivered_today' => 0, 'delivered_in_month' => 0, 'delivery_days_mask' => $inactiveMask, // today's bit cleared — excluded from snapshot 'preflight_blocked_at' => null, 'regions' => [D_MOSCOW_CODE], ]); linkProjectToSupplier($projectInactive, $supplier); // ── REBUILD SNAPSHOT AFTER MASKS ARE SET ───────────────────────────────── // SnapshotRebuildCommand: WHERE (delivery_days_mask & weekdayBit) <> 0 // → projectActive IS inserted (bit set) // → projectInactive NOT inserted (bit cleared) SnapshotForge::rebuild(); // Verify snapshot state directly: active project is in snapshot, inactive is not. $snapshotActiveExists = DB::connection('pgsql_supplier') ->table('project_routing_snapshots') ->where('snapshot_date', $activeDate) ->where('project_id', $projectActive->id) ->exists(); $snapshotInactiveExists = DB::connection('pgsql_supplier') ->table('project_routing_snapshots') ->where('snapshot_date', $activeDate) ->where('project_id', $projectInactive->id) ->exists(); expect($snapshotActiveExists)->toBeTrue( "FINDING: projectActive (mask={$activeMask}, bit={$todayBit}) ". "was expected in the snapshot for {$activeDate} but is absent. ". 'SnapshotRebuildCommand may have a bug in delivery_days_mask filtering.' ); expect($snapshotInactiveExists)->toBeFalse( "FINDING: projectInactive (mask={$inactiveMask}, bit={$todayBit}) ". "was expected to be ABSENT from the snapshot for {$activeDate} but was INSERTED. ". 'This means the delivery_days_mask filter is not working — the inactive project '. 'will receive leads it should not receive.' ); // ── ARRANGE: FAKE DADATA CLIENT ────────────────────────────────────────── $fake = (new FakeDaDataPhoneClient)->stub(D_PHONE_1, qc: 0, region: 'Москва', provider: 'МТС'); app()->instance(DaDataPhoneClient::class, $fake); // ── ACT: INJECT ONE LEAD ───────────────────────────────────────────────── (new LeadInjector)->site( domain: D_SUPPLIER_DOMAIN, phone: D_PHONE_1, tag: 'Москва', platform: D_SUPPLIER_PLATFORM, vid: 88_000_000_001, ); // ── ASSERT: DEALS DISTRIBUTION ─────────────────────────────────────────── // Active client MUST receive exactly 1 deal. $activeDeals = DB::connection('pgsql_supplier') ->table('deals') ->where('tenant_id', $tenantActive->id) ->count(); // Inactive client MUST receive 0 deals (not in snapshot). $inactiveDeals = DB::connection('pgsql_supplier') ->table('deals') ->where('tenant_id', $tenantInactive->id) ->count(); // Diagnostic output. fwrite(STDOUT, PHP_EOL.'=== SCENARIO D: DELIVERY DAYS FILTER REPORT ==='.PHP_EOL); fwrite(STDOUT, sprintf('Active date: %s (isoWeekday=%d)%s', $activeDate, $activeDateObj->isoWeekday(), PHP_EOL)); fwrite(STDOUT, sprintf('Today bit: %d (0b%s)%s', $todayBit, str_pad(decbin($todayBit), 7, '0', STR_PAD_LEFT), PHP_EOL)); fwrite(STDOUT, sprintf('Active mask: %d (0b%s) → snapshot: %s%s', $activeMask, str_pad(decbin($activeMask), 7, '0', STR_PAD_LEFT), $snapshotActiveExists ? 'INCLUDED' : 'MISSING', PHP_EOL)); fwrite(STDOUT, sprintf('Inactive mask: %d (0b%s) → snapshot: %s%s', $inactiveMask, str_pad(decbin($inactiveMask), 7, '0', STR_PAD_LEFT), $snapshotInactiveExists ? 'PRESENT (BUG!)' : 'ABSENT (correct)', PHP_EOL)); fwrite(STDOUT, sprintf('Active client deals: %d (expected: 1)%s', $activeDeals, PHP_EOL)); fwrite(STDOUT, sprintf('Inactive client deals: %d (expected: 0)%s', $inactiveDeals, PHP_EOL)); fwrite(STDOUT, '=== END SCENARIO D ==='.PHP_EOL.PHP_EOL); expect($activeDeals)->toBe(1, "FINDING: Active client (mask={$activeMask}, project_id={$projectActive->id}) ". "expected 1 deal but got {$activeDeals}. ". 'Check LeadRouter eligibility query or LedgerService::chargeForDelivery.' ); expect($inactiveDeals)->toBe(0, "FINDING: Inactive client (mask={$inactiveMask}, today_bit={$todayBit}, ". "project_id={$projectInactive->id}) expected 0 deals but got {$inactiveDeals}. ". 'The delivery_days_mask filter in SnapshotRebuildCommand is NOT excluding this project. '. "This is a correctness bug: clients with today's bit cleared MUST be absent from the snapshot." ); })->group('imitation');