diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index 1273f4cb..12e3ebe6 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -29,6 +29,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @property ?string $categorized_by_rule_id * @property ?string $ai_suggested_category_id * @property ?Carbon $ai_suggested_category_at + * @property ?string $ai_model */ class Transaction extends Model { @@ -51,6 +52,7 @@ class Transaction extends Model 'categorized_by_rule_id', 'ai_suggested_category_id', 'ai_suggested_category_at', + 'ai_model', 'description', 'description_iv', 'original_description', @@ -79,6 +81,7 @@ class Transaction extends Model 'dedup_fingerprint', 'raw_data', 'categorized_by_rule_id', + 'ai_model', 'deleted_at', ]; diff --git a/app/Services/Ai/CategorizeTransactions.php b/app/Services/Ai/CategorizeTransactions.php index b36c2137..fbae5fe8 100644 --- a/app/Services/Ai/CategorizeTransactions.php +++ b/app/Services/Ai/CategorizeTransactions.php @@ -42,6 +42,7 @@ class CategorizeTransactions $results = $this->resolve($transactions, $catalog); $labelBar = (float) config('ai_categorization.label_confidence'); + $model = (string) config('ai_categorization.model'); $outcomes = []; foreach ($results as $result) { @@ -62,7 +63,7 @@ class CategorizeTransactions $confidence = (float) ($result['confidence'] ?? 0.0); $applied = $confidence >= $labelBar; - $this->recordOutcome($transaction, $categoryId, $confidence, $applied); + $this->recordOutcome($transaction, $categoryId, $confidence, $applied, $model); $outcomes[] = new CategorizationOutcome( transaction: $transaction, @@ -82,11 +83,12 @@ class CategorizeTransactions * suggestion is kept (for confidence-bar tuning and a future confirm UI); * at or above it the category is also auto-applied. */ - private function recordOutcome(Transaction $transaction, string $categoryId, float $confidence, bool $applied): void + private function recordOutcome(Transaction $transaction, string $categoryId, float $confidence, bool $applied, string $model): void { $transaction->ai_suggested_category_id = $categoryId; $transaction->ai_confidence = $confidence; $transaction->ai_suggested_category_at = now(); + $transaction->ai_model = $model; if ($applied) { $transaction->category_id = $categoryId; diff --git a/database/migrations/2026_06_26_124609_add_ai_model_to_transactions_table.php b/database/migrations/2026_06_26_124609_add_ai_model_to_transactions_table.php new file mode 100644 index 00000000..625a6c98 --- /dev/null +++ b/database/migrations/2026_06_26_124609_add_ai_model_to_transactions_table.php @@ -0,0 +1,38 @@ +string('ai_model')->nullable()->after('ai_suggested_category_at'); + }); + + // Existing AI suggestions predate this column. They were produced by the + // floating `gemini-flash-latest` alias, which by then resolved to + // gemini-3.5-flash, so stamp that as the model used. + // ponytail: raw update so the backfill doesn't bump every row's updated_at. + DB::table('transactions') + ->whereNotNull('ai_suggested_category_at') + ->whereNull('ai_model') + ->update(['ai_model' => 'gemini-3.5-flash']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('transactions', function (Blueprint $table) { + $table->dropColumn('ai_model'); + }); + } +}; diff --git a/tests/Feature/Ai/CategorizeTransactionsTest.php b/tests/Feature/Ai/CategorizeTransactionsTest.php index 08951f3c..bb4c9bf4 100644 --- a/tests/Feature/Ai/CategorizeTransactionsTest.php +++ b/tests/Feature/Ai/CategorizeTransactionsTest.php @@ -69,6 +69,7 @@ it('auto-applies the category when confidence clears the label bar', function () ->and($transaction->ai_confidence)->toEqual(0.95) ->and($transaction->ai_suggested_category_id)->toBe($category->id) ->and($transaction->ai_suggested_category_at)->not->toBeNull() + ->and($transaction->ai_model)->toBe((string) config('ai_categorization.model')) ->and($outcomes)->toHaveCount(1) ->and($outcomes[0]->applied)->toBeTrue() ->and($outcomes[0]->merchantUnambiguous)->toBeTrue(); @@ -99,6 +100,7 @@ it('leaves the transaction blank but records the suggestion when confidence is b ->and($transaction->ai_suggested_category_id)->toBe($category->id) ->and($transaction->ai_confidence)->toEqual(0.5) ->and($transaction->ai_suggested_category_at)->not->toBeNull() + ->and($transaction->ai_model)->toBe((string) config('ai_categorization.model')) ->and($outcomes)->toHaveCount(1) ->and($outcomes[0]->applied)->toBeFalse(); });