Files
finance_app/app/Http/Controllers/Api/InvestmentHoldingController.php
henry yo 85972f950c feat: 前端分離+功能api化
1. 新增資產管理CRUD、googleOAuth登入
2. 更改原記帳controller
2026-06-25 14:19:07 +08:00

124 lines
4.1 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Jobs\UpdateAssetPriceJob;
use App\Models\Account;
use App\Models\InvestmentHolding;
use App\Models\Security;
use Illuminate\Http\Request;
class InvestmentHoldingController extends Controller
{
public function index(Request $request, string $accountId)
{
$account = $request->user()->accounts()->findOrFail($accountId);
// 🟢 1. 預先載入最新價格字典表關聯,避免 N+1 問題
$holdings = $account->holdings()->with('security')->get();
// 🟢 2. 透過 map 幫每檔持倉加工算出市值與報酬率
$processedHoldings = $holdings->map(function ($holding) {
$data = $holding->toArray();
$remainingShares = floatval($holding->shares);
$avgCost = floatval($holding->avg_cost);
// 預設給 0 或拿平均成本防呆
$currentPrice = floatval($holding->security?->latest_price ?? 0);
if ($currentPrice == 0) {
$currentPrice = $avgCost;
}
if ($remainingShares > 0) {
// 單一標的總成本
$totalCost = $remainingShares * $avgCost;
// 單一標目前市值
$currentValue = $remainingShares * $currentPrice;
// 單一標的未實現損益
$profit = $currentValue - $totalCost;
// 單一標的報酬率
$profitRate = $totalCost > 0 ? ($profit / $totalCost) * 100 : 0;
// 🟢 3. 塞入擴充數據欄位
$data['latest_price'] = $currentPrice;
$data['current_value'] = round($currentValue);
$data['profit'] = round($profit);
$data['profit_rate'] = round($profitRate, 2);
} else {
// 庫存為 0 的防呆
$data['latest_price'] = $currentPrice;
$data['current_value'] = 0;
$data['profit'] = 0;
$data['profit_rate'] = 0;
}
return $data;
});
// 傳回包含豐富績效數據的持倉清單
return response()->json($processedHoldings);
}
public function store(Request $request, string $accountId)
{
$account = $request->user()->accounts()->findOrFail($accountId);
$validated = $request->validate([
'type' => 'required|in:stock,fund',
'symbol' => 'required|string|max:20',
'name' => 'required|string|max:255',
]);
$holding = $account->holdings()->create($validated);
// 檢查security: 如果之前有資料不create, 如果之前沒有資料create
$security = Security::firstOrCreate(
['code' => $validated['symbol']], // 尋找條件
[
'type' => $validated['type'],
'name' => $validated['name'],
'source' => $request->input('source') ?? 'twse',
]
);
// 立即派發更新價格的任務
UpdateAssetPriceJob::dispatch($security->code, $security->type, $security->name);
return response()->json($holding, 201);
}
public function show(Request $request, string $accountId, string $id)
{
$account = $request->user()->accounts()->findOrFail($accountId);
$holding = $account->holdings()->findOrFail($id);
return response()->json($holding);
}
public function update(Request $request, string $accountId, string $id)
{
$account = $request->user()->accounts()->findOrFail($accountId);
$holding = $account->holdings()->findOrFail($id);
$validated = $request->validate([
'name' => 'sometimes|string|max:255',
]);
$holding->update($validated);
return response()->json($holding);
}
public function destroy(Request $request, string $accountId, string $id)
{
$account = $request->user()->accounts()->findOrFail($accountId);
$holding = $account->holdings()->findOrFail($id);
$holding->delete();
return response()->json(null, 204);
}
}