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

92 lines
4.5 KiB
PHP

<?php
namespace App\Mcp\Tools;
use App\Enums\CategorySource;
use App\Enums\TransactionSource;
use App\Models\Transaction;
use App\Models\User;
use App\Services\ManualBalanceAdjuster;
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\IsDestructive;
#[IsDestructive]
#[Description('Create a manual transaction on a non-connected (manual) account. Amount is a signed integer in minor units (cents): negative for an expense, positive for income. Connected/bank accounts are read-only.')]
class CreateTransaction extends WriteTool
{
/**
* @return array<string, mixed>
*/
public function schema(JsonSchema $schema): array
{
return [
'account_id' => $schema->string()->description('Id of a non-connected (manual) account to add the transaction to.')->required(),
'description' => $schema->string()->description('Human-readable description.')->required(),
'amount' => $schema->integer()->description('Signed amount in minor units (cents). Negative = expense, positive = income.')->required(),
'transaction_date' => $schema->string()->description('Transaction date, YYYY-MM-DD.')->required(),
'currency_code' => $schema->string()->description('ISO 4217 currency code (3 letters). Defaults to the account currency.'),
'category_id' => $schema->string()->description('Optional category id to assign.'),
'creditor_name' => $schema->string()->description('Optional creditor (payee) name.'),
'debtor_name' => $schema->string()->description('Optional debtor (payer) name.'),
'notes' => $schema->string()->description('Optional free-text notes.'),
'label_ids' => $schema->array()->items($schema->string())->description('Optional label ids to attach.'),
'update_balance' => $schema->boolean()->description('When true, shift the manual account balance snapshots by this amount. Default false.'),
'space' => $schema->string()->description('Space id. Defaults to the personal space.'),
];
}
protected function write(Request $request, User $user): Response
{
$request->validate([
'account_id' => ['required', 'string'],
'description' => ['required', 'string'],
'amount' => ['required', 'integer'],
'transaction_date' => ['required', 'date'],
'currency_code' => ['sometimes', 'string', 'size:3'],
'notes' => ['sometimes', 'nullable', 'string'],
'creditor_name' => ['sometimes', 'nullable', 'string', 'max:255'],
'debtor_name' => ['sometimes', 'nullable', 'string', 'max:255'],
]);
$space = $this->resolveSpace($request, $user);
$account = $this->writableAccount($request, $space);
$labels = $this->labelsInSpace($request, $space, 'label_ids');
$categoryId = $request->filled('category_id')
? $this->categoryInSpace($request, $space)->id
: null;
$transaction = new Transaction([
'user_id' => $user->id,
'space_id' => $space->id,
'account_id' => $account->id,
'category_id' => $categoryId,
'category_source' => $categoryId === null ? null : CategorySource::Manual->value,
'description' => $request->string('description')->toString(),
'transaction_date' => $request->string('transaction_date')->toString(),
'amount' => $request->integer('amount'),
'currency_code' => $request->filled('currency_code')
? mb_strtoupper($request->string('currency_code')->toString())
: $account->currency_code,
'notes' => $request->filled('notes') ? $request->string('notes')->toString() : null,
'creditor_name' => $request->filled('creditor_name') ? $request->string('creditor_name')->toString() : null,
'debtor_name' => $request->filled('debtor_name') ? $request->string('debtor_name')->toString() : null,
'source' => TransactionSource::ManuallyCreated->value,
]);
$transaction->save();
if ($labels->isNotEmpty()) {
$transaction->labels()->sync($labels->pluck('id')->all());
}
if ($request->boolean('update_balance')) {
app(ManualBalanceAdjuster::class)->applyCreatedTransaction($transaction->load('account'));
}
return $this->json(['transaction' => $this->presentTransaction($transaction)]);
}
}