resolveUser((string) $this->argument('user'));
if ($user === null) {
$this->error('User not found.');
return self::FAILURE;
}
$this->line("User: {$user->email} ({$user->id})");
$this->line(sprintf(
'Transactions: %d · eligible: %s · throttled: %s',
$availability->transactionCount($user),
$availability->isEligible($user) ? 'yes' : 'no',
$availability->isThrottled($user) ? 'yes' : 'no',
));
$this->newLine();
if ($this->option('persist')) {
return $this->runPersisted($user);
}
return $this->runDryRun($user, $aggregator, $generator, $guard);
}
private function runDryRun(
User $user,
RuleSuggestionAggregator $aggregator,
RuleSuggestionGenerator $generator,
RuleSuggestionGuard $guard,
): int {
$groups = $aggregator->groupsFor($user);
if ($groups === []) {
$this->warn('No transaction groups to suggest from (need uncategorized, server-readable transactions).');
return self::SUCCESS;
}
$this->components->info(count($groups).' transaction group(s) sent to the model');
$this->table(
['Key', 'Field', 'Count', 'Direction', 'Avg', 'Samples'],
array_map(fn (array $group): array => [
$group['key'],
$group['field'],
$group['count'],
$group['direction'],
$group['avg_amount'],
Str::limit(implode(' | ', $group['samples']), 50),
], $groups),
);
$categories = $aggregator->categoryOptions($user);
try {
$raw = $generator->generate($groups, $categories);
} catch (Throwable $exception) {
$this->error('Model call failed: '.$exception->getMessage());
return self::FAILURE;
}
$this->components->info(count($raw).' raw suggestion(s) returned by the model');
$this->table(
['Group', 'Field', 'Op', 'Token', 'Category', 'Confidence'],
array_map(fn (array $suggestion): array => [
$suggestion['group_key'] ?? '',
$suggestion['match_field'] ?? '',
$suggestion['match_operator'] ?? '',
$suggestion['match_token'] ?? '',
$this->describeRawCategory($suggestion, $categories),
$suggestion['confidence'] ?? '',
], $raw),
);
$validated = $guard->validate($user, $raw, $categories);
$this->components->info(count($validated).' suggestion(s) survived the guards');
$this->table(
['Field', 'Op', 'Token', 'Category', 'Confidence', 'Matches'],
array_map(fn (array $suggestion): array => [
$suggestion['match_field'],
$suggestion['match_operator'],
$suggestion['match_token'],
$this->describeValidatedCategory($suggestion, $categories),
$suggestion['confidence'],
$suggestion['group_size'],
], $validated),
);
$this->newLine();
$this->line('Dry run — nothing was saved. Use --persist to store a SuggestionRun.');
return self::SUCCESS;
}
private function runPersisted(User $user): int
{
$run = $user->suggestionRuns()->create(['status' => SuggestionRunStatus::Pending]);
app(GenerateRuleSuggestions::class)->run($run);
$run->refresh()->load('suggestions.proposedCategory');
$this->components->info("Run {$run->id} finished with status: {$run->status->value}");
if ($run->error) {
$this->error($run->error);
return self::FAILURE;
}
if ($run->suggestions->isEmpty()) {
$this->warn('No suggestions were produced.');
return self::SUCCESS;
}
$this->table(
['Field', 'Op', 'Token', 'Category', 'Confidence', 'Matches'],
$run->suggestions->map(fn (RuleSuggestion $suggestion): array => [
$suggestion->match_field,
$suggestion->match_operator,
$suggestion->match_token,
$suggestion->proposed_category_id !== null
? $suggestion->proposedCategory->name
: ($suggestion->new_category_name !== null
? "NEW: {$suggestion->new_category_name}"
: '—'),
$suggestion->confidence,
$suggestion->group_size,
])->all(),
);
return self::SUCCESS;
}
private function resolveUser(string $identifier): ?User
{
return User::query()->where('email', $identifier)->first()
?? User::query()->find($identifier);
}
/**
* @param array $suggestion
* @param list> $categories
*/
private function describeRawCategory(array $suggestion, array $categories): string
{
$id = $suggestion['category_id'] ?? null;
if (filled($id)) {
foreach ($categories as $category) {
if ($category['id'] === $id) {
return (string) $category['path'];
}
}
return (string) $id;
}
$name = $suggestion['new_category_name'] ?? null;
return filled($name) ? "NEW: {$name}" : '—';
}
/**
* @param array $suggestion
* @param list> $categories
*/
private function describeValidatedCategory(array $suggestion, array $categories): string
{
if (filled($suggestion['proposed_category_id'])) {
foreach ($categories as $category) {
if ($category['id'] === $suggestion['proposed_category_id']) {
return (string) $category['path'];
}
}
return (string) $suggestion['proposed_category_id'];
}
return filled($suggestion['new_category_name'])
? "NEW: {$suggestion['new_category_name']} ({$suggestion['new_category_direction']})"
: '—';
}
}