feat(supplier): LedgerService + CsvReconcileJob recognise DIRECT platform
LedgerService::resolveSupplierId returns suppliers.code='direct' row for DIRECT-platform supplier_projects (and for parsed-from-payload non-B projects). CsvReconcileJob::extractPlatform now classifies most non-empty, non-junk project strings as DIRECT (instead of dumping them into unparseable_count) — this allows CSV recovery to also create DIRECT supplier_leads, mirroring the webhook path. CsvReconcileJobTest junk-rows fixtures updated: previously used callback phone-number-as-project (79135551234) and URL-like strings as 'junk', but those are now valid DIRECT identifiers. Replaced with truly junk strings matching only outside-whitelist symbols (e.g. '???', '!@#'). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -231,14 +231,23 @@ final class CsvReconcileJob implements ShouldQueue
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлекает platform (B1/B2/B3) из имени проекта формата `B[123]_<rest>`.
|
||||
* Возвращает null если не парсится — caller пропустит строку с warning.
|
||||
* Извлекает platform из имени проекта:
|
||||
* - `B[123]_<rest>` → 'B1' / 'B2' / 'B3';
|
||||
* - Phase 3: иначе, если строка непустая и состоит из identifier-символов
|
||||
* (домены / телефоны / SMS-отправители) → 'DIRECT';
|
||||
* - откровенный мусор (только спец-символы, пусто) → null (unparseable).
|
||||
*/
|
||||
private function extractPlatform(string $project): ?string
|
||||
{
|
||||
if (preg_match('/^(B[123])_/', $project, $m) === 1) {
|
||||
return $m[1];
|
||||
}
|
||||
// Phase 3: всё что выглядит как разумный identifier (домен / телефон / SMS-sender) → DIRECT.
|
||||
// unparseable_count теперь только для откровенного мусора (пустые / только спец-символы).
|
||||
$trimmed = trim($project);
|
||||
if ($trimmed !== '' && preg_match('/^[\w\-.а-яА-Я0-9\/() +]+$/u', $trimmed) === 1) {
|
||||
return 'DIRECT';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -128,10 +128,17 @@ final class LedgerService
|
||||
{
|
||||
if ($lead->supplier_project_id !== null) {
|
||||
$sp = DB::table('supplier_projects')->where('id', $lead->supplier_project_id)->first();
|
||||
if ($sp !== null && in_array($sp->platform, ['B1', 'B2', 'B3'], true)) {
|
||||
$supplier = Supplier::where('code', strtolower($sp->platform))->first();
|
||||
if ($supplier !== null) {
|
||||
return (int) $supplier->id;
|
||||
if ($sp !== null) {
|
||||
if (in_array($sp->platform, ['B1', 'B2', 'B3'], true)) {
|
||||
$supplier = Supplier::where('code', strtolower($sp->platform))->first();
|
||||
if ($supplier !== null) {
|
||||
return (int) $supplier->id;
|
||||
}
|
||||
}
|
||||
if ($sp->platform === 'DIRECT') {
|
||||
$supplier = Supplier::where('code', 'direct')->first();
|
||||
|
||||
return $supplier?->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,6 +150,12 @@ final class LedgerService
|
||||
|
||||
return $supplier?->id;
|
||||
}
|
||||
// Phase 3: project без B-префикса (и не пустой) → DIRECT.
|
||||
if ($project !== '') {
|
||||
$supplier = Supplier::where('code', 'direct')->first();
|
||||
|
||||
return $supplier?->id;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -272,14 +272,16 @@ it('unparseable CSV rows excluded from drift: 100 matched + 10 junk-project rows
|
||||
]);
|
||||
}
|
||||
|
||||
// CSV: те же 100 (matched) + 10 строк с мусорным project (extractPlatform = null).
|
||||
// Это реальный паттерн поставщика — телефон в поле «Name» вместо проекта (см. 22.05 в ПИЛОТ).
|
||||
// CSV: те же 100 (matched) + 10 строк с настоящим мусорным project (extractPlatform = null).
|
||||
// Phase 3 (2026-05-25): расширили DIRECT-распознавание — теперь цифровые callback-проекты
|
||||
// (79135551234) — валидный DIRECT, не junk. Реальный junk — это символы вне whitelist regex.
|
||||
$rows = [];
|
||||
for ($i = 0; $i < 100; $i++) {
|
||||
$rows[] = ['project' => 'B1_a.com', 'phone' => '79993'.str_pad((string) $i, 6, '0', STR_PAD_LEFT)];
|
||||
}
|
||||
for ($j = 0; $j < 10; $j++) {
|
||||
$rows[] = ['project' => '79135551234', 'phone' => '7999500000'.$j];
|
||||
$junkProjects = ['???', '!@#', '%%%', '$$$', '???!!!', '~~~', '***', '|||', '^^^', '&&&'];
|
||||
foreach ($junkProjects as $j => $junk) {
|
||||
$rows[] = ['project' => $junk, 'phone' => '7999500000'.$j];
|
||||
}
|
||||
fakeReportFlow(csvBody($rows));
|
||||
|
||||
@@ -314,8 +316,10 @@ it('mixed: 95 matched + 5 junk + 3 real-missing → unparseable_count=5, recover
|
||||
for ($i = 0; $i < 95; $i++) {
|
||||
$rows[] = ['project' => 'B1_a.com', 'phone' => '79994'.str_pad((string) $i, 6, '0', STR_PAD_LEFT)];
|
||||
}
|
||||
for ($j = 0; $j < 5; $j++) {
|
||||
$rows[] = ['project' => 'https://junk.example/'.$j, 'phone' => '7999600000'.$j];
|
||||
// Phase 3: реальный junk — символы вне whitelist (не \w/.-/cyrillic/digits/slash/parens/space/plus).
|
||||
$junkProjects = ['???', '!!!@@@', '%%%', '****', '???!!!'];
|
||||
foreach ($junkProjects as $j => $junk) {
|
||||
$rows[] = ['project' => $junk, 'phone' => '7999600000'.$j];
|
||||
}
|
||||
for ($k = 0; $k < 3; $k++) {
|
||||
$rows[] = ['project' => 'B1_a.com', 'phone' => '7999700000'.$k];
|
||||
|
||||
Reference in New Issue
Block a user