mirror of
https://github.com/henry4682/finance_app.git
synced 2026-07-16 00:10:00 +00:00
124 lines
4.1 KiB
PHP
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);
|
|
}
|
|
|
|
|
|
}
|