whisper-money/app/Mcp/Tools/SearchTransactions.php

95 lines
5.1 KiB
PHP

<?php
namespace App\Mcp\Tools;
use App\Models\Transaction;
use App\Models\User;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tools\Annotations\IsReadOnly;
#[IsReadOnly]
#[Description('Search and filter the user\'s transactions by text, category, account, label, date range and amount. Use it to analyse spending, or to find recurring charges by grouping results by merchant.')]
class SearchTransactions extends McpTool
{
/**
* @return array<string, mixed>
*/
public function schema(JsonSchema $schema): array
{
return [
'query' => $schema->string()->description('Free text matched against description, creditor and debtor names.'),
'account_id' => $schema->string()->description('Restrict to a single account id.'),
'category_id' => $schema->string()->description('Restrict to a single category id.'),
'label_ids' => $schema->array()->items($schema->string())->description('Restrict to transactions carrying any of these label ids. Call list_labels to see valid ids.'),
'from' => $schema->string()->description('Earliest transaction date, YYYY-MM-DD.'),
'to' => $schema->string()->description('Latest transaction date, YYYY-MM-DD.'),
'min_amount' => $schema->integer()->description('Minimum signed amount in minor units (cents).'),
'max_amount' => $schema->integer()->description('Maximum signed amount in minor units (cents).'),
'limit' => $schema->integer()->min(1)->max(200)->description('Max rows to return (default 50).'),
'space' => $schema->string()->description('Space id to query. Defaults to the personal space.'),
];
}
protected function respond(Request $request, User $user): Response
{
$request->validate([
'from' => ['sometimes', 'date'],
'to' => ['sometimes', 'date'],
'limit' => ['sometimes', 'integer', 'min:1', 'max:200'],
]);
$space = $this->resolveSpace($request, $user);
$labels = $this->labelsInSpace($request, $space, 'label_ids');
$transactions = Transaction::query()
->forSpace($space)
->with(['account:id,name', 'category:id,name,type', 'labels:id,name'])
->when($request->string('query')->toString() !== '', function ($query) use ($request): void {
$term = '%'.$request->string('query')->toString().'%';
$query->where(function ($q) use ($term): void {
$q->where('description', 'like', $term)
->orWhere('creditor_name', 'like', $term)
->orWhere('debtor_name', 'like', $term);
});
})
->when($request->string('account_id')->toString() !== '', fn ($query) => $query->where('account_id', $request->string('account_id')->toString()))
->when($request->string('category_id')->toString() !== '', fn ($query) => $query->where('category_id', $request->string('category_id')->toString()))
->when($labels->isNotEmpty(), fn ($query) => $query->whereHas('labels', fn ($q) => $q->whereIn('labels.id', $labels->pluck('id')->all())))
->when($request->string('from')->toString() !== '', fn ($query) => $query->whereDate('transaction_date', '>=', $request->string('from')->toString()))
->when($request->string('to')->toString() !== '', fn ($query) => $query->whereDate('transaction_date', '<=', $request->string('to')->toString()))
->when($request->has('min_amount'), fn ($query) => $query->where('amount', '>=', $request->integer('min_amount')))
->when($request->has('max_amount'), fn ($query) => $query->where('amount', '<=', $request->integer('max_amount')))
->orderByDesc('transaction_date')
->limit($request->integer('limit', 50))
->get()
->map(fn (Transaction $transaction): array => [
'id' => $transaction->id,
'date' => $transaction->transaction_date->toDateString(),
'description' => $transaction->description,
'amount' => $transaction->amount,
'currency' => $transaction->currency_code,
'category' => $transaction->category?->name,
'category_id' => $transaction->category_id,
'account' => $transaction->account?->name,
'account_id' => $transaction->account_id,
'source' => $transaction->source->value,
'creditor_name' => $transaction->creditor_name,
'debtor_name' => $transaction->debtor_name,
'labels' => $transaction->labels
->map(fn ($label): array => ['id' => $label->id, 'name' => $label->name])
->values()
->all(),
]);
return $this->json([
'space_id' => $space->id,
'count' => $transactions->count(),
'transactions' => $transactions,
]);
}
}