refactor(billing-v2): remove DuplicateDetector — trust supplier dedup (Spec B)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -275,112 +275,37 @@ test('SupplierLeadCost НЕ создаётся если у проекта нет
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Биз-19: антифрод-дедуп по phone в окне 24 ч (DuplicateDetector, §10.8.1)
|
||||
// Spec B: no phone dedup — supplier owns dedup, Лидерра charges everything delivered
|
||||
// =============================================================================
|
||||
|
||||
test('Биз-19: master в окне 24ч → дубль, баланс НЕ списывается', function () {
|
||||
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
|
||||
$phone = '79007770001';
|
||||
test('charges both leads with same phone but different vid (no phone dedup, Spec B)', function () {
|
||||
$tenant = Tenant::factory()->create(['balance_leads' => 5]);
|
||||
$phone = '79007770010';
|
||||
|
||||
// Master: пришёл вчера в 12:00.
|
||||
$masterPayload = makePayload(vid: 901, time: now()->subHours(12)->timestamp);
|
||||
$masterPayload['phone'] = $phone;
|
||||
$masterPayload['phones'] = [$phone];
|
||||
(new ProcessWebhookJob($tenant->id, $masterPayload))->handle();
|
||||
// First webhook — distinct vid
|
||||
$payload1 = makePayload(vid: 951);
|
||||
$payload1['phone'] = $phone;
|
||||
$payload1['phones'] = [$phone];
|
||||
(new ProcessWebhookJob($tenant->id, $payload1))->handle();
|
||||
|
||||
// Second webhook — same phone, different vid
|
||||
$payload2 = makePayload(vid: 952);
|
||||
$payload2['phone'] = $phone;
|
||||
$payload2['phones'] = [$phone];
|
||||
(new ProcessWebhookJob($tenant->id, $payload2))->handle();
|
||||
|
||||
$tenant->refresh();
|
||||
expect($tenant->balance_leads)->toBe(9);
|
||||
// Both charged — balance_leads decremented twice.
|
||||
expect($tenant->balance_leads)->toBe(3);
|
||||
|
||||
// Дубль: пришёл сейчас, в окне 24 ч.
|
||||
$dupPayload = makePayload(vid: 902, time: now()->timestamp);
|
||||
$dupPayload['phone'] = $phone;
|
||||
$dupPayload['phones'] = [$phone];
|
||||
(new ProcessWebhookJob($tenant->id, $dupPayload))->handle();
|
||||
// Two distinct deals exist for this tenant.
|
||||
$deals = Deal::query()->where('tenant_id', $tenant->id)->get();
|
||||
expect($deals)->toHaveCount(2);
|
||||
|
||||
$master = Deal::query()->where('tenant_id', $tenant->id)->where('source_crm_id', 901)->first();
|
||||
$dup = Deal::query()->where('tenant_id', $tenant->id)->where('source_crm_id', 902)->first();
|
||||
|
||||
expect($master->duplicate_of_id)->toBeNull();
|
||||
expect($dup->duplicate_of_id)->toBe($master->id);
|
||||
|
||||
$tenant->refresh();
|
||||
expect($tenant->balance_leads)->toBe(9); // только master списан, дубль — нет
|
||||
expect(BalanceTransaction::query()->where('tenant_id', $tenant->id)->count())->toBe(1);
|
||||
expect(SupplierLeadCost::query()->where('deal_id', $dup->id)->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('Биз-19: master старше 24ч → НЕ дубль, баланс списывается дважды', function () {
|
||||
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
|
||||
$phone = '79007770002';
|
||||
|
||||
// Master: пришёл 25 часов назад — за окном.
|
||||
$masterPayload = makePayload(vid: 911, time: now()->subHours(25)->timestamp);
|
||||
$masterPayload['phone'] = $phone;
|
||||
$masterPayload['phones'] = [$phone];
|
||||
(new ProcessWebhookJob($tenant->id, $masterPayload))->handle();
|
||||
|
||||
// Новая сделка с тем же phone — master уже за окном.
|
||||
$newPayload = makePayload(vid: 912, time: now()->timestamp);
|
||||
$newPayload['phone'] = $phone;
|
||||
$newPayload['phones'] = [$phone];
|
||||
(new ProcessWebhookJob($tenant->id, $newPayload))->handle();
|
||||
|
||||
$deal911 = Deal::query()->where('tenant_id', $tenant->id)->where('source_crm_id', 911)->first();
|
||||
$deal912 = Deal::query()->where('tenant_id', $tenant->id)->where('source_crm_id', 912)->first();
|
||||
|
||||
expect($deal911->duplicate_of_id)->toBeNull();
|
||||
expect($deal912->duplicate_of_id)->toBeNull();
|
||||
|
||||
$tenant->refresh();
|
||||
expect($tenant->balance_leads)->toBe(8); // оба списаны
|
||||
expect(BalanceTransaction::query()->where('tenant_id', $tenant->id)->count())->toBe(2);
|
||||
});
|
||||
|
||||
test('Биз-19: дубли изолированы по tenant_id', function () {
|
||||
$tenantA = Tenant::factory()->create(['balance_leads' => 10]);
|
||||
$tenantB = Tenant::factory()->create(['balance_leads' => 10]);
|
||||
$phone = '79007770003';
|
||||
|
||||
$payloadA = makePayload(vid: 921);
|
||||
$payloadA['phone'] = $phone;
|
||||
$payloadA['phones'] = [$phone];
|
||||
(new ProcessWebhookJob($tenantA->id, $payloadA))->handle();
|
||||
|
||||
// Тот же phone у tenantB — НЕ должен считаться дублем.
|
||||
$payloadB = makePayload(vid: 922);
|
||||
$payloadB['phone'] = $phone;
|
||||
$payloadB['phones'] = [$phone];
|
||||
(new ProcessWebhookJob($tenantB->id, $payloadB))->handle();
|
||||
|
||||
$dealA = Deal::query()->where('tenant_id', $tenantA->id)->first();
|
||||
$dealB = Deal::query()->where('tenant_id', $tenantB->id)->first();
|
||||
|
||||
expect($dealA->duplicate_of_id)->toBeNull();
|
||||
expect($dealB->duplicate_of_id)->toBeNull();
|
||||
});
|
||||
|
||||
test('Биз-19: ActivityLog для дубля содержит context.duplicate_of', function () {
|
||||
$tenant = Tenant::factory()->create(['balance_leads' => 10]);
|
||||
$phone = '79007770004';
|
||||
|
||||
$masterPayload = makePayload(vid: 931, time: now()->subHours(2)->timestamp);
|
||||
$masterPayload['phone'] = $phone;
|
||||
$masterPayload['phones'] = [$phone];
|
||||
(new ProcessWebhookJob($tenant->id, $masterPayload))->handle();
|
||||
|
||||
$dupPayload = makePayload(vid: 932, time: now()->timestamp);
|
||||
$dupPayload['phone'] = $phone;
|
||||
$dupPayload['phones'] = [$phone];
|
||||
(new ProcessWebhookJob($tenant->id, $dupPayload))->handle();
|
||||
|
||||
$master = Deal::query()->where('source_crm_id', 931)->first();
|
||||
$dup = Deal::query()->where('source_crm_id', 932)->first();
|
||||
|
||||
$masterLog = ActivityLog::query()->where('deal_id', $master->id)->first();
|
||||
$dupLog = ActivityLog::query()->where('deal_id', $dup->id)->first();
|
||||
|
||||
expect($masterLog->context)->toBe(['source' => 'webhook']);
|
||||
expect($dupLog->context)->toBe(['source' => 'webhook', 'duplicate_of' => $master->id]);
|
||||
// Neither deal has duplicate_of_id set.
|
||||
foreach ($deals as $deal) {
|
||||
expect($deal->duplicate_of_id)->toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
|
||||
Reference in New Issue
Block a user