chore: code style
This commit is contained in:
@@ -4,10 +4,10 @@ namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\WithTrimStrings;
|
||||
use Laravel\Jetstream\Jetstream;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Laravel\Fortify\Contracts\CreatesNewUsers;
|
||||
use Laravel\Jetstream\Jetstream;
|
||||
|
||||
class CreateNewUser implements CreatesNewUsers
|
||||
{
|
||||
|
||||
@@ -4,9 +4,9 @@ namespace App\Actions\Fortify;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Traits\WithTrimStrings;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||
|
||||
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
|
||||
@@ -58,7 +58,7 @@ class CaptureDailyChange extends Command
|
||||
'total_cost_basis' => $total_cost_basis,
|
||||
'total_gain' => $total_market_value - $total_cost_basis,
|
||||
'total_dividends_earned' => $total_dividends,
|
||||
'realized_gains' => $realized_gains
|
||||
'realized_gains' => $realized_gains,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Holding;
|
||||
use App\Models\Dividend;
|
||||
use App\Models\Holding;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RefreshDividendData extends Command
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Split;
|
||||
use App\Models\Holding;
|
||||
use App\Models\Split;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RefreshSplitData extends Command
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Console\Commands;
|
||||
use App\Models\Portfolio;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\PromptsForMissingInput;
|
||||
|
||||
use function Laravel\Prompts\search;
|
||||
|
||||
class SyncDailyChange extends Command implements PromptsForMissingInput
|
||||
|
||||
@@ -14,18 +14,14 @@ class BackupExport implements WithMultipleSheets
|
||||
|
||||
public function __construct(
|
||||
public bool $empty = false
|
||||
)
|
||||
{ }
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function sheets(): array
|
||||
{
|
||||
return [
|
||||
new PortfoliosSheet($this->empty),
|
||||
new TransactionsSheet($this->empty),
|
||||
new DailyChangesSheet($this->empty)
|
||||
new DailyChangesSheet($this->empty),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class DailyChangesSheet implements FromCollection, WithHeadings, WithTitle
|
||||
'Total Gain',
|
||||
'Total Dividends Earned',
|
||||
'Realized Gains',
|
||||
'Annotation'
|
||||
'Annotation',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -35,9 +35,6 @@ class DailyChangesSheet implements FromCollection, WithHeadings, WithTitle
|
||||
return $this->empty ? collect() : DailyChange::myDailyChanges()->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function title(): string
|
||||
{
|
||||
return 'Daily Changes';
|
||||
|
||||
@@ -21,7 +21,7 @@ class PortfoliosSheet implements FromCollection, WithHeadings, WithTitle
|
||||
'Notes',
|
||||
'Wishlist',
|
||||
'Created',
|
||||
'Updated'
|
||||
'Updated',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -33,9 +33,6 @@ class PortfoliosSheet implements FromCollection, WithHeadings, WithTitle
|
||||
return $this->empty ? collect() : Portfolio::myPortfolios()->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function title(): string
|
||||
{
|
||||
return 'Portfolios';
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
namespace App\Exports\Sheets;
|
||||
|
||||
use App\Models\Transaction;
|
||||
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\FromCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithTitle;
|
||||
|
||||
class TransactionsSheet implements FromCollection, WithHeadings, WithTitle
|
||||
{
|
||||
@@ -27,7 +27,7 @@ class TransactionsSheet implements FromCollection, WithHeadings, WithTitle
|
||||
'Reinvested Dividend',
|
||||
'Date',
|
||||
'Created',
|
||||
'Updated'
|
||||
'Updated',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -39,9 +39,6 @@ class TransactionsSheet implements FromCollection, WithHeadings, WithTitle
|
||||
return $this->empty ? collect() : Transaction::myTransactions()->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function title(): string
|
||||
{
|
||||
return 'Transactions';
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
namespace App\Http\ApiControllers;
|
||||
|
||||
use App\Models\Holding;
|
||||
use App\Models\Portfolio;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use App\Http\ApiControllers\Controller as ApiController;
|
||||
use App\Http\Requests\HoldingRequest;
|
||||
use App\Http\Resources\HoldingResource;
|
||||
use App\Models\Holding;
|
||||
use App\Models\Portfolio;
|
||||
use HackerEsq\FilterModels\FilterModels;
|
||||
use App\Http\ApiControllers\Controller as ApiController;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class HoldingController extends ApiController
|
||||
{
|
||||
|
||||
@@ -4,10 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\ApiControllers;
|
||||
|
||||
use App\Http\ApiControllers\Controller as ApiController;
|
||||
use App\Http\Resources\MarketDataResource;
|
||||
use App\Models\MarketData;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Resources\MarketDataResource;
|
||||
use App\Http\ApiControllers\Controller as ApiController;
|
||||
|
||||
class MarketDataController extends ApiController
|
||||
{
|
||||
|
||||
@@ -4,12 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\ApiControllers;
|
||||
|
||||
use App\Models\Portfolio;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use HackerEsq\FilterModels\FilterModels;
|
||||
use App\Http\Resources\PortfolioResource;
|
||||
use App\Http\Requests\PortfolioRequest;
|
||||
use App\Http\ApiControllers\Controller as ApiController;
|
||||
use App\Http\Requests\PortfolioRequest;
|
||||
use App\Http\Resources\PortfolioResource;
|
||||
use App\Models\Portfolio;
|
||||
use HackerEsq\FilterModels\FilterModels;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class PortfolioController extends ApiController
|
||||
{
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
namespace App\Http\ApiControllers;
|
||||
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use HackerEsq\FilterModels\FilterModels;
|
||||
use App\Http\ApiControllers\Controller as ApiController;
|
||||
use App\Http\Requests\TransactionRequest;
|
||||
use App\Http\Resources\TransactionResource;
|
||||
use App\Http\ApiControllers\Controller as ApiController;
|
||||
use App\Models\Transaction;
|
||||
use HackerEsq\FilterModels\FilterModels;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class TransactionController extends ApiController
|
||||
{
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace App\Http\ApiControllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Resources\UserResource;
|
||||
use App\Http\ApiControllers\Controller as ApiController;
|
||||
use App\Http\Resources\UserResource;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserController extends ApiController
|
||||
{
|
||||
|
||||
@@ -2,21 +2,19 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Exception;
|
||||
use App\Models\User;
|
||||
use App\Models\ConnectedAccount;
|
||||
use Illuminate\Support\MessageBag;
|
||||
use App\Models\User;
|
||||
use App\Notifications\VerifyConnectedAccountNotification;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\MessageBag;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use App\Notifications\VerifyConnectedAccountNotification;
|
||||
|
||||
class ConnectedAccountController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Redirect the user to the GitHub authentication page.
|
||||
*
|
||||
*/
|
||||
public function redirectToProvider(string $provider)
|
||||
{
|
||||
@@ -27,7 +25,6 @@ class ConnectedAccountController extends Controller
|
||||
|
||||
/**
|
||||
* Obtain the user information from GitHub.
|
||||
*
|
||||
*/
|
||||
public function handleProviderCallback(string $provider)
|
||||
{
|
||||
@@ -46,13 +43,13 @@ class ConnectedAccountController extends Controller
|
||||
// check if this account is already linked
|
||||
$connected_account = ConnectedAccount::firstOrNew([
|
||||
'provider' => $provider,
|
||||
'provider_id' => $providerUser->id
|
||||
'provider_id' => $providerUser->id,
|
||||
], [
|
||||
'token' => $providerUser->token,
|
||||
'secret' => $providerUser->tokenSecret,
|
||||
'refresh_token' => $providerUser->refreshToken,
|
||||
'expires_at' => $providerUser->expiresIn,
|
||||
'verified_at' => false
|
||||
'verified_at' => false,
|
||||
]);
|
||||
|
||||
// already linked and verified, let's go login!
|
||||
@@ -72,7 +69,7 @@ class ConnectedAccountController extends Controller
|
||||
$user = User::create([
|
||||
'name' => $providerUser->name,
|
||||
'email' => $providerUser->email,
|
||||
'email_verified_at' => now()
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
$connected_account->user_id = $user->id;
|
||||
@@ -127,8 +124,8 @@ class ConnectedAccountController extends Controller
|
||||
'css' => 'alert-success',
|
||||
'icon' => Blade::render("<x-mary-icon class='w-7 h-7' name='o-check-circle' />"),
|
||||
'position' => 'toast-top toast-end',
|
||||
'timeout' => '5000'
|
||||
]
|
||||
'timeout' => '5000',
|
||||
],
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,16 @@ use Illuminate\Http\Request;
|
||||
|
||||
class HoldingController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
public function show(Request $request, Portfolio $portfolio, String $symbol)
|
||||
public function show(Request $request, Portfolio $portfolio, string $symbol)
|
||||
{
|
||||
$holding = Holding::with([
|
||||
'market_data',
|
||||
'transactions' => function ($query) use ($symbol) {
|
||||
$query->where('transactions.symbol', $symbol);
|
||||
}
|
||||
},
|
||||
])
|
||||
->symbol($symbol)
|
||||
->portfolio($portfolio->id)
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class InvitedOnboardingController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Check if the invited user needs a password?
|
||||
*
|
||||
*/
|
||||
public function __invoke(Request $request, Portfolio $portfolio, User $user)
|
||||
{
|
||||
@@ -26,7 +24,7 @@ class InvitedOnboardingController extends Controller
|
||||
// route to create password form
|
||||
return view('auth.invited-onboarding', [
|
||||
'portfolio' => $portfolio,
|
||||
'user' => $user
|
||||
'user' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class PortfolioController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Http\Controllers;
|
||||
|
||||
class TransactionController extends Controller
|
||||
{
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*/
|
||||
|
||||
@@ -6,7 +6,6 @@ use Illuminate\Foundation\Http\FormRequest as BaseFormRequest;
|
||||
|
||||
class FormRequest extends BaseFormRequest
|
||||
{
|
||||
|
||||
public function requestOrModelValue($key, $model): mixed
|
||||
{
|
||||
return $this->request->get($key) ?? $this->{$model}?->{$key};
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Http\Requests\FormRequest;
|
||||
|
||||
class HoldingRequest extends FormRequest
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
@@ -16,7 +13,7 @@ class HoldingRequest extends FormRequest
|
||||
{
|
||||
|
||||
$rules = [
|
||||
'reinvest_dividends' => ['sometimes', 'boolean']
|
||||
'reinvest_dividends' => ['sometimes', 'boolean'],
|
||||
];
|
||||
|
||||
return $rules;
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Http\Requests\FormRequest;
|
||||
|
||||
class PortfolioRequest extends FormRequest
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
|
||||
@@ -3,18 +3,16 @@
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Portfolio;
|
||||
use App\Http\Requests\FormRequest;
|
||||
use App\Rules\SymbolValidationRule;
|
||||
use App\Rules\QuantityValidationRule;
|
||||
use App\Rules\SymbolValidationRule;
|
||||
|
||||
class TransactionRequest extends FormRequest
|
||||
{
|
||||
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
|
||||
$this->merge([
|
||||
'portfolio' => Portfolio::find($this->requestOrModelValue('portfolio_id', 'transaction'))
|
||||
'portfolio' => Portfolio::find($this->requestOrModelValue('portfolio_id', 'transaction')),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -40,7 +38,7 @@ class TransactionRequest extends FormRequest
|
||||
$this->requestOrModelValue('symbol', 'transaction'),
|
||||
$this->requestOrModelValue('transaction_type', 'transaction'),
|
||||
$this->requestOrModelValue('date', 'transaction')
|
||||
)
|
||||
),
|
||||
],
|
||||
'cost_basis' => ['exclude_if:transaction_type,SELL', 'min:0', 'numeric'],
|
||||
'sale_price' => ['exclude_if:transaction_type,BUY', 'min:0', 'numeric'],
|
||||
|
||||
@@ -31,7 +31,7 @@ class HoldingResource extends JsonResource
|
||||
'market_gain_dollars' => $this->market_gain_dollars,
|
||||
'market_gain_percent' => $this->market_gain_percent,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,35 +2,31 @@
|
||||
|
||||
namespace App\Imports;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Imports\Sheets\PortfoliosSheet;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use App\Console\Commands\RefreshDividendData;
|
||||
use App\Console\Commands\RefreshMarketData;
|
||||
use App\Console\Commands\SyncDailyChange;
|
||||
use App\Console\Commands\SyncHoldingData;
|
||||
use App\Imports\Sheets\DailyChangesSheet;
|
||||
use App\Imports\Sheets\PortfoliosSheet;
|
||||
use App\Imports\Sheets\TransactionsSheet;
|
||||
use Maatwebsite\Excel\Events\AfterImport;
|
||||
use App\Models\BackupImport as BackupImportModel;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Maatwebsite\Excel\Concerns\Importable;
|
||||
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
|
||||
use Maatwebsite\Excel\Events\AfterImport;
|
||||
use Maatwebsite\Excel\Events\BeforeImport;
|
||||
use Maatwebsite\Excel\Events\ImportFailed;
|
||||
use App\Console\Commands\RefreshMarketData;
|
||||
use App\Console\Commands\RefreshDividendData;
|
||||
use App\Models\BackupImport as BackupImportModel;
|
||||
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
|
||||
|
||||
class BackupImport implements WithMultipleSheets, WithEvents
|
||||
class BackupImport implements WithEvents, WithMultipleSheets
|
||||
{
|
||||
|
||||
use Importable;
|
||||
|
||||
public function __construct(
|
||||
public BackupImportModel $backupImportModel
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function registerEvents(): array
|
||||
{
|
||||
return [
|
||||
@@ -43,7 +39,7 @@ class BackupImport implements WithMultipleSheets, WithEvents
|
||||
$this->backupImportModel->update([
|
||||
'status' => 'success',
|
||||
'message' => 'Import completed successfully!',
|
||||
'completed_at' => now()
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
|
||||
Artisan::queue(RefreshMarketData::class, ['--user' => $this->backupImportModel->user_id, '--force' => true])
|
||||
@@ -53,14 +49,14 @@ class BackupImport implements WithMultipleSheets, WithEvents
|
||||
fn () => User::find($this->backupImportModel->user_id)->portfolios->each(function ($portfolio) {
|
||||
|
||||
Artisan::queue(SyncDailyChange::class, ['portfolio_id' => $portfolio->id]);
|
||||
})
|
||||
}),
|
||||
]);
|
||||
},
|
||||
ImportFailed::class => fn (ImportFailed $event) => $this->backupImportModel->update([
|
||||
'status' => 'failed',
|
||||
'message' => 'Error: '.substr($event->getException()->getMessage(), 0, 220),
|
||||
'has_errors' => true,
|
||||
'completed_at' => now()
|
||||
'completed_at' => now(),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
namespace App\Imports\Sheets;
|
||||
|
||||
use App\Imports\ValidatesPortfolioAccess;
|
||||
use App\Models\DailyChange;
|
||||
use App\Models\BackupImport;
|
||||
use App\Models\DailyChange;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Maatwebsite\Excel\Events\BeforeSheet;
|
||||
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Maatwebsite\Excel\Concerns\SkipsEmptyRows;
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||
use Maatwebsite\Excel\Concerns\WithValidation;
|
||||
use Maatwebsite\Excel\Events\BeforeSheet;
|
||||
|
||||
class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation, SkipsEmptyRows, WithEvents
|
||||
class DailyChangesSheet implements SkipsEmptyRows, ToCollection, WithEvents, WithHeadingRow, WithValidation
|
||||
{
|
||||
use ValidatesPortfolioAccess;
|
||||
|
||||
@@ -23,9 +23,6 @@ class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation,
|
||||
public BackupImport $backupImport
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function registerEvents(): array
|
||||
{
|
||||
return [
|
||||
@@ -35,7 +32,7 @@ class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation,
|
||||
'message' => __('Importing daily changes...'),
|
||||
]);
|
||||
DB::beginTransaction();
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -56,7 +53,7 @@ class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation,
|
||||
'realized_gains' => $dailyChange['realized_gains'],
|
||||
'annotation' => $dailyChange['annotation'],
|
||||
'portfolio_id' => $dailyChange['portfolio_id'],
|
||||
'date' => Carbon::parse($dailyChange['date'])->format('Y-m-d')
|
||||
'date' => Carbon::parse($dailyChange['date'])->format('Y-m-d'),
|
||||
];
|
||||
});
|
||||
|
||||
@@ -71,7 +68,7 @@ class DailyChangesSheet implements ToCollection, WithHeadingRow, WithValidation,
|
||||
'realized_gains',
|
||||
'annotation',
|
||||
'portfolio_id',
|
||||
'date'
|
||||
'date',
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,26 +2,23 @@
|
||||
|
||||
namespace App\Imports\Sheets;
|
||||
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\BackupImport;
|
||||
use App\Models\Portfolio;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Maatwebsite\Excel\Events\BeforeSheet;
|
||||
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Maatwebsite\Excel\Concerns\SkipsEmptyRows;
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||
use Maatwebsite\Excel\Concerns\WithValidation;
|
||||
use Maatwebsite\Excel\Events\BeforeSheet;
|
||||
|
||||
class PortfoliosSheet implements ToCollection, WithValidation, WithHeadingRow, SkipsEmptyRows, WithEvents
|
||||
class PortfoliosSheet implements SkipsEmptyRows, ToCollection, WithEvents, WithHeadingRow, WithValidation
|
||||
{
|
||||
public function __construct(
|
||||
public BackupImport $backupImport
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function registerEvents(): array
|
||||
{
|
||||
return [
|
||||
@@ -31,7 +28,7 @@ class PortfoliosSheet implements ToCollection, WithValidation, WithHeadingRow, S
|
||||
'message' => __('Importing portfolios...'),
|
||||
]);
|
||||
DB::beginTransaction();
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -42,7 +39,7 @@ class PortfoliosSheet implements ToCollection, WithValidation, WithHeadingRow, S
|
||||
Portfolio::unguard(); // ensures we can set an owner for the portfolio
|
||||
|
||||
$portfolio = Portfolio::fullAccess($this->backupImport->user_id)->updateOrCreate([
|
||||
'id' => $portfolio['portfolio_id']
|
||||
'id' => $portfolio['portfolio_id'],
|
||||
], [
|
||||
'id' => $portfolio['portfolio_id'] ?? null,
|
||||
'title' => $portfolio['title'],
|
||||
|
||||
@@ -3,32 +3,28 @@
|
||||
namespace App\Imports\Sheets;
|
||||
|
||||
use App\Imports\ValidatesPortfolioAccess;
|
||||
use App\Models\BackupImport;
|
||||
use App\Models\Holding;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\BackupImport;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Maatwebsite\Excel\Events\BeforeSheet;
|
||||
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Illuminate\Support\Str;
|
||||
use Maatwebsite\Excel\Concerns\SkipsEmptyRows;
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithEvents;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||
use Maatwebsite\Excel\Concerns\WithValidation;
|
||||
use Maatwebsite\Excel\Events\BeforeSheet;
|
||||
|
||||
class TransactionsSheet implements ToCollection, WithHeadingRow, WithValidation, SkipsEmptyRows, WithEvents
|
||||
class TransactionsSheet implements SkipsEmptyRows, ToCollection, WithEvents, WithHeadingRow, WithValidation
|
||||
{
|
||||
|
||||
use ValidatesPortfolioAccess;
|
||||
|
||||
public function __construct(
|
||||
public BackupImport $backupImport
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function registerEvents(): array
|
||||
{
|
||||
return [
|
||||
@@ -38,7 +34,7 @@ class TransactionsSheet implements ToCollection, WithHeadingRow, WithValidation,
|
||||
'message' => __('Importing transactions...'),
|
||||
]);
|
||||
DB::beginTransaction();
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -62,7 +58,7 @@ class TransactionsSheet implements ToCollection, WithHeadingRow, WithValidation,
|
||||
'sale_price' => $transaction['sale_price'],
|
||||
'split' => boolval($transaction['split']) ? 1 : 0,
|
||||
'reinvested_dividend' => boolval($transaction['reinvested_dividend']) ? 1 : 0,
|
||||
'date' => Carbon::parse($transaction['date'])->format('Y-m-d')
|
||||
'date' => Carbon::parse($transaction['date'])->format('Y-m-d'),
|
||||
];
|
||||
});
|
||||
|
||||
@@ -79,7 +75,7 @@ class TransactionsSheet implements ToCollection, WithHeadingRow, WithValidation,
|
||||
'sale_price',
|
||||
'split',
|
||||
'reinvested_dividend',
|
||||
'date'
|
||||
'date',
|
||||
]
|
||||
);
|
||||
|
||||
@@ -89,7 +85,7 @@ class TransactionsSheet implements ToCollection, WithHeadingRow, WithValidation,
|
||||
|
||||
Holding::firstOrCreate([
|
||||
'symbol' => $holding['symbol'],
|
||||
'portfolio_id' => $holding['portfolio_id']
|
||||
'portfolio_id' => $holding['portfolio_id'],
|
||||
], [
|
||||
'quantity' => 0,
|
||||
'average_cost_basis' => 0,
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Models\Portfolio;
|
||||
|
||||
trait ValidatesPortfolioAccess
|
||||
{
|
||||
|
||||
public function validatePortfolioAccess($collection)
|
||||
{
|
||||
|
||||
@@ -18,7 +17,7 @@ trait ValidatesPortfolioAccess
|
||||
if (
|
||||
$countPortfoliosWithAccess < $uniquePortfolios->count()
|
||||
) {
|
||||
throw new \Exception(__("You do not have access to that portfolio."));
|
||||
throw new \Exception(__('You do not have access to that portfolio.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,29 +2,31 @@
|
||||
|
||||
namespace App\Interfaces\MarketData;
|
||||
|
||||
use App\Interfaces\MarketData\Types\Dividend;
|
||||
use App\Interfaces\MarketData\Types\Ohlc;
|
||||
use App\Interfaces\MarketData\Types\Quote;
|
||||
use App\Interfaces\MarketData\Types\Split;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Interfaces\MarketData\Types\Quote;
|
||||
use App\Interfaces\MarketData\Types\Split;
|
||||
use App\Interfaces\MarketData\Types\Dividend;
|
||||
use App\Interfaces\MarketData\Types\Ohlc;
|
||||
use Tschucki\Alphavantage\Facades\Alphavantage;
|
||||
|
||||
class AlphaVantageMarketData implements MarketDataInterface
|
||||
{
|
||||
public function exists(String $symbol): Bool
|
||||
public function exists(string $symbol): bool
|
||||
{
|
||||
|
||||
return $this->quote($symbol)->isNotEmpty();
|
||||
}
|
||||
|
||||
public function quote(String $symbol): Quote
|
||||
public function quote(string $symbol): Quote
|
||||
{
|
||||
$quote = Alphavantage::core()->quoteEndpoint($symbol);
|
||||
$quote = Arr::get($quote, 'Global Quote', []);
|
||||
|
||||
if (empty($quote)) return new Quote();
|
||||
if (empty($quote)) {
|
||||
return new Quote;
|
||||
}
|
||||
|
||||
$fundamental = cache()->remember(
|
||||
'av-symbol-'.$symbol,
|
||||
@@ -49,11 +51,11 @@ class AlphaVantageMarketData implements MarketDataInterface
|
||||
: null,
|
||||
'dividend_yield' => Arr::get($fundamental, 'DividendYield') != 'None'
|
||||
? Arr::get($fundamental, 'DividendYield')
|
||||
: null
|
||||
: null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function dividends(String $symbol, $startDate, $endDate): Collection
|
||||
public function dividends(string $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
$dividends = Alphavantage::fundamentals()->dividends($symbol);
|
||||
$dividends = Arr::get($dividends, 'data', []);
|
||||
@@ -73,7 +75,7 @@ class AlphaVantageMarketData implements MarketDataInterface
|
||||
});
|
||||
}
|
||||
|
||||
public function splits(String $symbol, $startDate, $endDate): Collection
|
||||
public function splits(string $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
$splits = Alphavantage::fundamentals()->splits($symbol);
|
||||
$splits = Arr::get($splits, 'data', []);
|
||||
@@ -93,7 +95,7 @@ class AlphaVantageMarketData implements MarketDataInterface
|
||||
});
|
||||
}
|
||||
|
||||
public function history(String $symbol, $startDate, $endDate): Collection
|
||||
public function history(string $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
$history = Alphavantage::timeSeries()->daily($symbol, 'full');
|
||||
@@ -112,7 +114,7 @@ class AlphaVantageMarketData implements MarketDataInterface
|
||||
return [$date => new Ohlc([
|
||||
'symbol' => $symbol,
|
||||
'date' => $date,
|
||||
'close' => Arr::get($history, '4. close')
|
||||
'close' => Arr::get($history, '4. close'),
|
||||
])];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
|
||||
namespace App\Interfaces\MarketData;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Interfaces\MarketData\Types\Quote;
|
||||
use App\Interfaces\MarketData\Types\Dividend;
|
||||
use App\Interfaces\MarketData\Types\Ohlc;
|
||||
use App\Interfaces\MarketData\Types\Quote;
|
||||
use App\Interfaces\MarketData\Types\Split;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class FakeMarketData implements MarketDataInterface
|
||||
{
|
||||
public function exists(String $symbol): Bool
|
||||
public function exists(string $symbol): bool
|
||||
{
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function quote(String $symbol): Quote
|
||||
public function quote(string $symbol): Quote
|
||||
{
|
||||
|
||||
return new Quote([
|
||||
@@ -31,11 +31,11 @@ class FakeMarketData implements MarketDataInterface
|
||||
'market_cap' => 9800700600,
|
||||
'book_value' => 4.7,
|
||||
'last_dividend_date' => now()->subDays(45),
|
||||
'dividend_yield' => 0.033
|
||||
'dividend_yield' => 0.033,
|
||||
]);
|
||||
}
|
||||
|
||||
public function dividends(String $symbol, $startDate, $endDate): Collection
|
||||
public function dividends(string $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
return collect([
|
||||
@@ -57,7 +57,7 @@ class FakeMarketData implements MarketDataInterface
|
||||
]);
|
||||
}
|
||||
|
||||
public function splits(String $symbol, $startDate, $endDate): Collection
|
||||
public function splits(string $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
return collect([
|
||||
@@ -65,11 +65,11 @@ class FakeMarketData implements MarketDataInterface
|
||||
'symbol' => $symbol,
|
||||
'date' => now()->subMonths(36),
|
||||
'split_amount' => 10,
|
||||
])
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function history(String $symbol, $startDate, $endDate): Collection
|
||||
public function history(string $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
$numDays = Carbon::parse($startDate)->diffInDays($endDate, true);
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use Illuminate\Support\Facades\Log;
|
||||
|
||||
class FallbackInterface
|
||||
{
|
||||
|
||||
protected string $latest_error;
|
||||
|
||||
public function __call($method, $arguments)
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
namespace App\Interfaces\MarketData;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Interfaces\MarketData\Types\Dividend;
|
||||
use App\Interfaces\MarketData\Types\Ohlc;
|
||||
use App\Interfaces\MarketData\Types\Quote;
|
||||
use App\Interfaces\MarketData\Types\Split;
|
||||
use App\Interfaces\MarketData\Types\Dividend;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class FinnhubMarketData implements MarketDataInterface
|
||||
{
|
||||
@@ -18,11 +18,12 @@ class FinnhubMarketData implements MarketDataInterface
|
||||
{
|
||||
|
||||
$this->client = new \Finnhub\Api\DefaultApi(
|
||||
new \GuzzleHttp\Client(),
|
||||
new \GuzzleHttp\Client,
|
||||
\Finnhub\Configuration::getDefaultConfiguration()->setApiKey('token', config('finnhub.key'))
|
||||
);
|
||||
}
|
||||
public function exists(String $symbol): Bool
|
||||
|
||||
public function exists(string $symbol): bool
|
||||
{
|
||||
|
||||
return $this->quote($symbol)->isNotEmpty();
|
||||
@@ -32,13 +33,15 @@ class FinnhubMarketData implements MarketDataInterface
|
||||
{
|
||||
$quote = $this->client->quote($symbol);
|
||||
|
||||
if (empty($quote)) return new Quote();
|
||||
if (empty($quote)) {
|
||||
return new Quote;
|
||||
}
|
||||
|
||||
$fundamental = cache()->remember(
|
||||
'fh-symbol-'.$symbol,
|
||||
1440,
|
||||
function () use ($symbol) {
|
||||
return $this->client->companyBasicFinancials($symbol, "all");
|
||||
return $this->client->companyBasicFinancials($symbol, 'all');
|
||||
}
|
||||
);
|
||||
|
||||
@@ -89,13 +92,14 @@ class FinnhubMarketData implements MarketDataInterface
|
||||
public function history($symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
$history = $this->client->stockCandles($symbol, "D", $startDate->timestamp, $endDate->timestamp);
|
||||
$history = $this->client->stockCandles($symbol, 'D', $startDate->timestamp, $endDate->timestamp);
|
||||
|
||||
$timestamps = Arr::get($history, 't', []);
|
||||
$closes = Arr::get($history, 'c', []);
|
||||
|
||||
return collect($timestamps)->mapWithKeys(function ($timestamp, $index) use ($symbol, $closes) {
|
||||
$date = Carbon::createFromTimestamp($timestamp)->format('Y-m-d');
|
||||
|
||||
return [$date => new Ohlc([
|
||||
'symbol' => $symbol,
|
||||
'date' => $date,
|
||||
|
||||
@@ -2,59 +2,33 @@
|
||||
|
||||
namespace App\Interfaces\MarketData;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Interfaces\MarketData\Types\Quote;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
interface MarketDataInterface
|
||||
{
|
||||
/**
|
||||
* Does this symbol actually exist?
|
||||
*
|
||||
* @param String $symbol
|
||||
*
|
||||
* @return Bool
|
||||
*/
|
||||
public function exists(String $symbol): Bool;
|
||||
public function exists(string $symbol): bool;
|
||||
|
||||
/**
|
||||
* Get quote data
|
||||
*
|
||||
* @param String $symbol
|
||||
*
|
||||
* @return Quote
|
||||
*/
|
||||
public function quote(String $symbol): Quote;
|
||||
public function quote(string $symbol): Quote;
|
||||
|
||||
/**
|
||||
* Get dividend data
|
||||
*
|
||||
* @param String $symbol
|
||||
* @param \DateTimeInterface $startDate
|
||||
* @param \DateTimeInterface $endDate
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function dividends(String $symbol, \DateTimeInterface $startDate, \DateTimeInterface $endDate): Collection;
|
||||
public function dividends(string $symbol, \DateTimeInterface $startDate, \DateTimeInterface $endDate): Collection;
|
||||
|
||||
/**
|
||||
* Get split data
|
||||
*
|
||||
* @param String $symbol
|
||||
* @param \DateTimeInterface $startDate
|
||||
* @param \DateTimeInterface $endDate
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function splits(String $symbol, \DateTimeInterface $startDate, \DateTimeInterface $endDate): Collection;
|
||||
public function splits(string $symbol, \DateTimeInterface $startDate, \DateTimeInterface $endDate): Collection;
|
||||
|
||||
/**
|
||||
* Get historical close data
|
||||
*
|
||||
* @param String $symbol
|
||||
* @param \DateTimeInterface $startDate
|
||||
* @param \DateTimeInterface $endDate
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function history(String $symbol, \DateTimeInterface $startDate, \DateTimeInterface $endDate): Collection;
|
||||
public function history(string $symbol, \DateTimeInterface $startDate, \DateTimeInterface $endDate): Collection;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ namespace App\Interfaces\MarketData\Types;
|
||||
|
||||
use DateTime;
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\Interfaces\MarketData\Types\MarketDataType;
|
||||
|
||||
class Dividend extends MarketDataType
|
||||
{
|
||||
public function setSymbol(string $symbol): self
|
||||
{
|
||||
$this->items['symbol'] = $symbol;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class Dividend extends MarketDataType
|
||||
public function setDividendAmount($dividendAmount): self
|
||||
{
|
||||
$this->items['dividend_amount'] = (float) $dividendAmount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -30,9 +31,10 @@ class Dividend extends MarketDataType
|
||||
return $this->items['dividend_amount'] ?? 0.0;
|
||||
}
|
||||
|
||||
public function setDate(String|DateTime $date): self
|
||||
public function setDate(string|DateTime $date): self
|
||||
{
|
||||
$this->items['date'] = Carbon::parse($date)->format('Y-m-d H:i:s');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
|
||||
namespace App\Interfaces\MarketData\Types;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MarketDataType extends Collection
|
||||
{
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function __construct($items = [])
|
||||
{
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ namespace App\Interfaces\MarketData\Types;
|
||||
|
||||
use DateTime;
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\Interfaces\MarketData\Types\MarketDataType;
|
||||
|
||||
class Ohlc extends MarketDataType
|
||||
{
|
||||
public function setSymbol(string $symbol): self
|
||||
{
|
||||
$this->items['symbol'] = $symbol;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class Ohlc extends MarketDataType
|
||||
public function setOpen($open): self
|
||||
{
|
||||
$this->items['open'] = (float) $open;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -33,6 +34,7 @@ class Ohlc extends MarketDataType
|
||||
public function setHigh($high): self
|
||||
{
|
||||
$this->items['high'] = (float) $high;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -44,6 +46,7 @@ class Ohlc extends MarketDataType
|
||||
public function setLow($low): self
|
||||
{
|
||||
$this->items['low'] = (float) $low;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -55,6 +58,7 @@ class Ohlc extends MarketDataType
|
||||
public function setClose($close): self
|
||||
{
|
||||
$this->items['close'] = (float) $close;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -63,9 +67,10 @@ class Ohlc extends MarketDataType
|
||||
return $this->items['close'] ?? 0.0;
|
||||
}
|
||||
|
||||
public function setDate(String|DateTime $date): self
|
||||
public function setDate(string|DateTime $date): self
|
||||
{
|
||||
$this->items['date'] = Carbon::parse($date)->format('Y-m-d H:i:s');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ namespace App\Interfaces\MarketData\Types;
|
||||
|
||||
use DateTime;
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\Interfaces\MarketData\Types\MarketDataType;
|
||||
|
||||
class Quote extends MarketDataType
|
||||
{
|
||||
public function setName($name): self
|
||||
{
|
||||
$this->items['name'] = (string) $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class Quote extends MarketDataType
|
||||
public function setSymbol($symbol): self
|
||||
{
|
||||
$this->items['symbol'] = (string) $symbol;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -33,6 +34,7 @@ class Quote extends MarketDataType
|
||||
public function setMarketValue($marketValue): self
|
||||
{
|
||||
$this->items['market_value'] = (float) $marketValue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -44,6 +46,7 @@ class Quote extends MarketDataType
|
||||
public function setFiftyTwoWeekHigh($high): self
|
||||
{
|
||||
$this->items['fifty_two_week_high'] = (float) $high;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -55,6 +58,7 @@ class Quote extends MarketDataType
|
||||
public function setFiftyTwoWeekLow($low): self
|
||||
{
|
||||
$this->items['fifty_two_week_low'] = (float) $low;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -66,6 +70,7 @@ class Quote extends MarketDataType
|
||||
public function setForwardPE($pe): self
|
||||
{
|
||||
$this->items['forward_pe'] = (float) $pe;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -77,6 +82,7 @@ class Quote extends MarketDataType
|
||||
public function setTrailingPE($pe): self
|
||||
{
|
||||
$this->items['trailing_pe'] = (float) $pe;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -88,6 +94,7 @@ class Quote extends MarketDataType
|
||||
public function setMarketCap($cap): self
|
||||
{
|
||||
$this->items['market_cap'] = (int) $cap;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -99,6 +106,7 @@ class Quote extends MarketDataType
|
||||
public function setBookValue($value): self
|
||||
{
|
||||
$this->items['book_value'] = (float) $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -110,6 +118,7 @@ class Quote extends MarketDataType
|
||||
public function setLastDividendDate(mixed $date): self
|
||||
{
|
||||
$this->items['last_dividend_date'] = is_null($date) ? null : Carbon::parse($date)->format('Y-m-d H:i:s');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -121,6 +130,7 @@ class Quote extends MarketDataType
|
||||
public function setDividendYield($yield): self
|
||||
{
|
||||
$this->items['dividend_yield'] = (float) $yield;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,13 @@ namespace App\Interfaces\MarketData\Types;
|
||||
|
||||
use DateTime;
|
||||
use Illuminate\Support\Carbon;
|
||||
use App\Interfaces\MarketData\Types\MarketDataType;
|
||||
|
||||
class Split extends MarketDataType
|
||||
{
|
||||
public function setSymbol(string $symbol): self
|
||||
{
|
||||
$this->items['symbol'] = $symbol;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class Split extends MarketDataType
|
||||
public function setSplitAmount($splitAmount): self
|
||||
{
|
||||
$this->items['split_amount'] = (float) $splitAmount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -30,9 +31,10 @@ class Split extends MarketDataType
|
||||
return $this->items['split_amount'] ?? 0.0;
|
||||
}
|
||||
|
||||
public function setDate(String|DateTime $date): self
|
||||
public function setDate(string|DateTime $date): self
|
||||
{
|
||||
$this->items['date'] = Carbon::parse($date)->format('Y-m-d H:i:s');
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,36 +2,39 @@
|
||||
|
||||
namespace App\Interfaces\MarketData;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Scheb\YahooFinanceApi\ApiClient;
|
||||
use App\Interfaces\MarketData\Types\Dividend;
|
||||
use App\Interfaces\MarketData\Types\Ohlc;
|
||||
use App\Interfaces\MarketData\Types\Quote;
|
||||
use App\Interfaces\MarketData\Types\Split;
|
||||
use App\Interfaces\MarketData\Types\Dividend;
|
||||
use Illuminate\Support\Collection;
|
||||
use Scheb\YahooFinanceApi\ApiClient;
|
||||
use Scheb\YahooFinanceApi\ApiClientFactory as YahooFinance;
|
||||
|
||||
class YahooMarketData implements MarketDataInterface
|
||||
{
|
||||
public ApiClient $client;
|
||||
|
||||
public function __construct() {
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
// create yahoo finance client factory
|
||||
$this->client = YahooFinance::createApiClient();
|
||||
}
|
||||
|
||||
public function exists(String $symbol): Bool
|
||||
public function exists(string $symbol): bool
|
||||
{
|
||||
|
||||
return $this->quote($symbol)->isNotEmpty();
|
||||
}
|
||||
|
||||
public function quote(String $symbol): Quote
|
||||
public function quote(string $symbol): Quote
|
||||
{
|
||||
|
||||
$quote = $this->client->getQuote($symbol);
|
||||
|
||||
if (empty($quote)) return collect();
|
||||
if (empty($quote)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return new Quote([
|
||||
'name' => $quote->getLongName() ?? $quote->getShortName(),
|
||||
@@ -44,11 +47,11 @@ class YahooMarketData implements MarketDataInterface
|
||||
'market_cap' => $quote->getMarketCap(),
|
||||
'book_value' => $quote->getBookValue(),
|
||||
'last_dividend_date' => $quote->getDividendDate(),
|
||||
'dividend_yield' => $quote->getTrailingAnnualDividendYield() * 100
|
||||
'dividend_yield' => $quote->getTrailingAnnualDividendYield() * 100,
|
||||
]);
|
||||
}
|
||||
|
||||
public function dividends(String $symbol, $startDate, $endDate): Collection
|
||||
public function dividends(string $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
return collect($this->client->getHistoricalDividendData($symbol, $startDate, $endDate))
|
||||
@@ -62,7 +65,7 @@ class YahooMarketData implements MarketDataInterface
|
||||
});
|
||||
}
|
||||
|
||||
public function splits(String $symbol, $startDate, $endDate): Collection
|
||||
public function splits(string $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
return collect($this->client->getHistoricalSplitData($symbol, $startDate, $endDate))
|
||||
@@ -77,7 +80,7 @@ class YahooMarketData implements MarketDataInterface
|
||||
});
|
||||
}
|
||||
|
||||
public function history(String $symbol, $startDate, $endDate): Collection
|
||||
public function history(string $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
return collect($this->client->getHistoricalQuoteData($symbol, ApiClient::INTERVAL_1_DAY, $startDate, $endDate))
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Throwable;
|
||||
use App\Models\User;
|
||||
use App\Models\BackupImport;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use App\Notifications\ImportSucceededNotification;
|
||||
use App\Notifications\ImportFailedNotification;
|
||||
use App\Imports\BackupImport as BackupImportExcel;
|
||||
use App\Models\BackupImport;
|
||||
use App\Models\User;
|
||||
use App\Notifications\ImportFailedNotification;
|
||||
use App\Notifications\ImportSucceededNotification;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Throwable;
|
||||
|
||||
class BackupImportJob implements ShouldQueue
|
||||
{
|
||||
@@ -65,7 +65,7 @@ class BackupImportJob implements ShouldQueue
|
||||
'status' => 'failed',
|
||||
'message' => 'Error: '.substr($e->getMessage(), 0, 220),
|
||||
'has_errors' => true,
|
||||
'completed_at' => now()
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
|
||||
$this->user->notify(new ImportFailedNotification($e->getMessage()));
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AiChat extends Model
|
||||
{
|
||||
@@ -11,7 +11,7 @@ class AiChat extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'role',
|
||||
'content'
|
||||
'content',
|
||||
];
|
||||
|
||||
protected $hidden = [];
|
||||
@@ -26,7 +26,8 @@ class AiChat extends Model
|
||||
});
|
||||
}
|
||||
|
||||
public function user() {
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Imports\BackupImport as BackupImportExcel;
|
||||
use App\Jobs\BackupImportJob;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BackupImport extends Model
|
||||
{
|
||||
@@ -20,7 +18,7 @@ class BackupImport extends Model
|
||||
'status', // pending, in_progress, success, failed
|
||||
'message', // Import starting, Import is in progress, Importing portfolios, Importing transactions, Importing daily changes, Import completed successfully
|
||||
'has_errors',
|
||||
'completed_at'
|
||||
'completed_at',
|
||||
];
|
||||
|
||||
protected static function boot()
|
||||
@@ -47,7 +45,7 @@ class BackupImport extends Model
|
||||
{
|
||||
return [
|
||||
'has_errors' => 'boolean',
|
||||
'completed_at' => 'datetime'
|
||||
'completed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class ConnectedAccount extends Model
|
||||
];
|
||||
|
||||
protected $with = [
|
||||
'user'
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasCompositePrimaryKey;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class DailyChange extends Model
|
||||
{
|
||||
use HasFactory, HasCompositePrimaryKey;
|
||||
use HasCompositePrimaryKey, HasFactory;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
@@ -47,7 +47,8 @@ class DailyChange extends Model
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeWithoutWishlists($query) {
|
||||
public function scopeWithoutWishlists($query)
|
||||
{
|
||||
return $query->whereHas('portfolio', function ($query) {
|
||||
$query->where('portfolios.wishlist', 0);
|
||||
});
|
||||
|
||||
+11
-12
@@ -2,15 +2,12 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Holding;
|
||||
use App\Models\MarketData;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Interfaces\MarketData\MarketDataInterface;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Dividend extends Model
|
||||
{
|
||||
@@ -30,15 +27,18 @@ class Dividend extends Model
|
||||
'last_dividend_update' => 'datetime',
|
||||
];
|
||||
|
||||
public function marketData() {
|
||||
public function marketData()
|
||||
{
|
||||
return $this->belongsTo(MarketData::class, 'symbol', 'symbol');
|
||||
}
|
||||
|
||||
public function holdings() {
|
||||
public function holdings()
|
||||
{
|
||||
return $this->hasMany(Holding::class, 'symbol', 'symbol');
|
||||
}
|
||||
|
||||
public function transactions() {
|
||||
public function transactions()
|
||||
{
|
||||
return $this->hasMany(Transaction::class, 'symbol', 'symbol');
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ class Dividend extends Model
|
||||
|
||||
/**
|
||||
* Grab new dividend data
|
||||
*
|
||||
*/
|
||||
public static function refreshDividendData(string $symbol): void
|
||||
{
|
||||
@@ -132,7 +131,7 @@ class Dividend extends Model
|
||||
->each(function ($holding) use ($dividends) {
|
||||
$holding->update([
|
||||
'dividends_earned' => $dividends->where('portfolio_id', $holding->portfolio_id)
|
||||
->sum('total_received')
|
||||
->sum('total_received'),
|
||||
]);
|
||||
});
|
||||
}
|
||||
@@ -153,7 +152,7 @@ class Dividend extends Model
|
||||
'date' => $dividend['date'],
|
||||
'portfolio_id' => $holding->portfolio_id,
|
||||
'symbol' => $holding->symbol,
|
||||
'transaction_type' => "BUY",
|
||||
'transaction_type' => 'BUY',
|
||||
'reinvested_dividend' => true,
|
||||
'cost_basis' => 0,
|
||||
'quantity' => ($dividend['dividend_amount'] * $holding->qtyOwned(Carbon::parse($dividend['date']))) / $market_data->market_value,
|
||||
|
||||
+26
-24
@@ -2,16 +2,10 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Split;
|
||||
use App\Models\AiChat;
|
||||
use App\Models\Dividend;
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\MarketData;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class Holding extends Model
|
||||
{
|
||||
@@ -27,13 +21,13 @@ class Holding extends Model
|
||||
'realized_gain_dollars',
|
||||
'dividends_earned',
|
||||
'splits_synced_at',
|
||||
'reinvest_dividends'
|
||||
'reinvest_dividends',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'splits_synced_at' => 'datetime',
|
||||
'first_transaction_date' => 'datetime',
|
||||
'reinvest_dividends' => 'boolean'
|
||||
'reinvest_dividends' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -164,7 +158,8 @@ class Holding extends Model
|
||||
return $query->where('holdings.symbol', $symbol);
|
||||
}
|
||||
|
||||
public function scopeWithoutWishlists($query) {
|
||||
public function scopeWithoutWishlists($query)
|
||||
{
|
||||
return $query->whereHas('portfolio', function ($query) {
|
||||
$query->where('portfolios.wishlist', 0);
|
||||
});
|
||||
@@ -217,15 +212,17 @@ class Holding extends Model
|
||||
'realized_gain_dollars' => $query->qty_purchases > 0 && $query->total_sale_price > 0
|
||||
? $query->total_sale_price - ($query->qty_sales * ($query->total_cost_basis / $query->qty_purchases))
|
||||
: 0,
|
||||
'dividends_earned' => $this->dividends->sum('total_received')
|
||||
'dividends_earned' => $this->dividends->sum('total_received'),
|
||||
]);
|
||||
|
||||
$this->save();
|
||||
}
|
||||
|
||||
public function qtyOwned(\Illuminate\Support\Carbon $date = null)
|
||||
public function qtyOwned(?\Illuminate\Support\Carbon $date = null)
|
||||
{
|
||||
if ($date == null) $date = now();
|
||||
if ($date == null) {
|
||||
$date = now();
|
||||
}
|
||||
|
||||
$transactions = $this->transactions->where('date', '<=', $date);
|
||||
|
||||
@@ -237,13 +234,17 @@ class Holding extends Model
|
||||
}
|
||||
|
||||
public function dailyPerformance(
|
||||
\Illuminate\Support\Carbon $start_date = null,
|
||||
\Illuminate\Support\Carbon $end_date = null,
|
||||
?\Illuminate\Support\Carbon $start_date = null,
|
||||
?\Illuminate\Support\Carbon $end_date = null,
|
||||
) {
|
||||
if ($start_date == null) $start_date = now();
|
||||
if ($end_date == null) $end_date = now();
|
||||
if ($start_date == null) {
|
||||
$start_date = now();
|
||||
}
|
||||
if ($end_date == null) {
|
||||
$end_date = now();
|
||||
}
|
||||
|
||||
$date_interval = "DATE_ADD(date, INTERVAL 1 DAY)";
|
||||
$date_interval = 'DATE_ADD(date, INTERVAL 1 DAY)';
|
||||
|
||||
if (config('database.default') === 'sqlite') {
|
||||
|
||||
@@ -285,7 +286,7 @@ class Holding extends Model
|
||||
END)
|
||||
END, 0) AS cost_basis
|
||||
"),
|
||||
DB::raw("COALESCE(SUM(CASE WHEN transaction_type = 'SELL' THEN ((sale_price - cost_basis) * quantity) ELSE 0 END), 0) AS `realized_gains`")
|
||||
DB::raw("COALESCE(SUM(CASE WHEN transaction_type = 'SELL' THEN ((sale_price - cost_basis) * quantity) ELSE 0 END), 0) AS `realized_gains`"),
|
||||
])
|
||||
->leftJoin('transactions', function ($join) {
|
||||
$join->on(DB::raw('DATE(transactions.date)'), '<=', 'date_series.date')
|
||||
@@ -302,12 +303,13 @@ class Holding extends Model
|
||||
{
|
||||
$formattedTransactions = '';
|
||||
foreach ($this->transactions->sortByDesc('date') as $transaction) {
|
||||
$formattedTransactions .= " * ".$transaction->date->format('Y-m-d')
|
||||
." ". $transaction->transaction_type
|
||||
." ". $transaction->quantity
|
||||
." @ ". $transaction->cost_basis
|
||||
$formattedTransactions .= ' * '.$transaction->date->format('Y-m-d')
|
||||
.' '.$transaction->transaction_type
|
||||
.' '.$transaction->quantity
|
||||
.' @ '.$transaction->cost_basis
|
||||
." each \n\n";
|
||||
}
|
||||
|
||||
return $formattedTransactions;
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,18 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Interfaces\MarketData\MarketDataInterface;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class MarketData extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $primaryKey = 'symbol';
|
||||
|
||||
protected $keyType = 'string';
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
protected $fillable = [
|
||||
@@ -25,7 +27,7 @@ class MarketData extends Model
|
||||
'market_cap',
|
||||
'book_value',
|
||||
'last_dividend_date',
|
||||
'dividend_yield'
|
||||
'dividend_yield',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
@@ -37,7 +39,7 @@ class MarketData extends Model
|
||||
'trailing_pe' => 'float',
|
||||
'market_cap' => 'float',
|
||||
'book_value' => 'float',
|
||||
'dividend_yield' => 'float'
|
||||
'dividend_yield' => 'float',
|
||||
];
|
||||
|
||||
public function holdings()
|
||||
@@ -53,7 +55,7 @@ class MarketData extends Model
|
||||
public static function getMarketData($symbol, $force = false)
|
||||
{
|
||||
$market_data = self::firstOrNew([
|
||||
'symbol' => $symbol
|
||||
'symbol' => $symbol,
|
||||
]);
|
||||
|
||||
// check if new or stale
|
||||
|
||||
+21
-28
@@ -2,17 +2,16 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\AiChat;
|
||||
use App\Interfaces\MarketData\MarketDataInterface;
|
||||
use App\Notifications\InvitedOnboardingNotification;
|
||||
use Carbon\CarbonPeriod;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Interfaces\MarketData\MarketDataInterface;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use App\Notifications\InvitedOnboardingNotification;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Portfolio extends Model
|
||||
{
|
||||
@@ -40,7 +39,7 @@ class Portfolio extends Model
|
||||
protected $hidden = [];
|
||||
|
||||
protected $casts = [
|
||||
'wishlist' => 'boolean'
|
||||
'wishlist' => 'boolean',
|
||||
];
|
||||
|
||||
protected $with = ['users', 'transactions'];
|
||||
@@ -186,7 +185,7 @@ class Portfolio extends Model
|
||||
'total_cost_basis' => $daily_performance->get($date)->cost_basis,
|
||||
'total_gain' => $total_market_value - $daily_performance->get($date)->cost_basis,
|
||||
'realized_gains' => $daily_performance->get($date)->realized_gains,
|
||||
'total_dividends_earned' => $dividends_earned
|
||||
'total_dividends_earned' => $dividends_earned,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -218,7 +217,7 @@ class Portfolio extends Model
|
||||
'total_cost_basis',
|
||||
'total_gain',
|
||||
'realized_gains',
|
||||
'total_dividends_earned'
|
||||
'total_dividends_earned',
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -245,35 +244,32 @@ class Portfolio extends Model
|
||||
{
|
||||
$formattedHoldings = '';
|
||||
foreach ($this->holdings as $holding) {
|
||||
$formattedHoldings .= " * Holding of ".$holding->market_data->name." (".$holding->symbol.")"
|
||||
."; with ". ($holding->quantity > 0 ? $holding->quantity : 'ZERO') . " shares"
|
||||
."; avg cost basis ". $holding->average_cost_basis
|
||||
."; curr market value ". $holding->market_data->market_value
|
||||
."; unrealized gains ". $holding->market_gain_dollars
|
||||
."; realized gains ". $holding->realized_gain_dollars
|
||||
."; dividends earned ". $holding->dividends_earned
|
||||
$formattedHoldings .= ' * Holding of '.$holding->market_data->name.' ('.$holding->symbol.')'
|
||||
.'; with '.($holding->quantity > 0 ? $holding->quantity : 'ZERO').' shares'
|
||||
.'; avg cost basis '.$holding->average_cost_basis
|
||||
.'; curr market value '.$holding->market_data->market_value
|
||||
.'; unrealized gains '.$holding->market_gain_dollars
|
||||
.'; realized gains '.$holding->realized_gain_dollars
|
||||
.'; dividends earned '.$holding->dividends_earned
|
||||
."\n\n";
|
||||
}
|
||||
|
||||
return $formattedHoldings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Share a portfolio with a user
|
||||
*
|
||||
* @param string $email
|
||||
* @param boolean $fullAccess
|
||||
* @return void
|
||||
*/
|
||||
public function share(string $email, bool $fullAccess = false): void
|
||||
{
|
||||
$user = User::firstOrCreate([
|
||||
'email' => $email
|
||||
'email' => $email,
|
||||
], [
|
||||
'name' => Str::title(Str::before($email, '@'))
|
||||
'name' => Str::title(Str::before($email, '@')),
|
||||
]);
|
||||
|
||||
$permissions[$user->id] = [
|
||||
'full_access' => $fullAccess
|
||||
'full_access' => $fullAccess,
|
||||
];
|
||||
|
||||
$sync = $this->users()->syncWithoutDetaching($permissions);
|
||||
@@ -282,15 +278,12 @@ class Portfolio extends Model
|
||||
|
||||
foreach ($sync['attached'] as $newUserId) {
|
||||
User::find($newUserId)->notify(new InvitedOnboardingNotification($this, auth()->user()));
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Un-share a portfolio
|
||||
*
|
||||
* @param string $userId
|
||||
* @return void
|
||||
*/
|
||||
public function unShare(string $userId): void
|
||||
{
|
||||
|
||||
+12
-12
@@ -2,13 +2,12 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Interfaces\MarketData\MarketDataInterface;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Split extends Model
|
||||
{
|
||||
@@ -28,18 +27,19 @@ class Split extends Model
|
||||
'last_date' => 'datetime',
|
||||
];
|
||||
|
||||
public function holdings() {
|
||||
public function holdings()
|
||||
{
|
||||
return $this->hasMany(Holding::class, 'symbol', 'symbol');
|
||||
}
|
||||
|
||||
public function transactions() {
|
||||
public function transactions()
|
||||
{
|
||||
return $this->hasMany(Transaction::class, 'symbol', 'symbol');
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab new split data
|
||||
*
|
||||
* @param string $symbol
|
||||
* @param \DateTimeInterface|null $start_date
|
||||
* @return void
|
||||
*/
|
||||
@@ -94,7 +94,7 @@ class Split extends Model
|
||||
'splits.date',
|
||||
'splits.symbol',
|
||||
'splits.split_amount',
|
||||
'holdings.portfolio_id'
|
||||
'holdings.portfolio_id',
|
||||
])
|
||||
->where([
|
||||
'splits.symbol' => $symbol,
|
||||
@@ -110,7 +110,7 @@ class Split extends Model
|
||||
// get qty owned when split was issued
|
||||
$qty_owned = Transaction::where([
|
||||
'symbol' => $split->symbol,
|
||||
'portfolio_id' => $split->portfolio_id
|
||||
'portfolio_id' => $split->portfolio_id,
|
||||
])
|
||||
->whereDate('transactions.date', '<', $split->date->format('Y-m-d'))
|
||||
->selectRaw('SUM(CASE WHEN transaction_type = "BUY" THEN quantity ELSE 0 END) -
|
||||
@@ -128,14 +128,14 @@ class Split extends Model
|
||||
'cost_basis' => 0,
|
||||
'split' => true,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now()
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
Holding::where([
|
||||
'symbol' => $split->symbol,
|
||||
'portfolio_id' => $split->portfolio_id
|
||||
'portfolio_id' => $split->portfolio_id,
|
||||
])->update([
|
||||
'splits_synced_at' => now()
|
||||
'splits_synced_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\MarketData;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class Transaction extends Model
|
||||
{
|
||||
@@ -23,7 +22,7 @@ class Transaction extends Model
|
||||
'cost_basis',
|
||||
'sale_price',
|
||||
'split',
|
||||
'reinvested_dividend'
|
||||
'reinvested_dividend',
|
||||
];
|
||||
|
||||
protected $hidden = [];
|
||||
@@ -31,7 +30,7 @@ class Transaction extends Model
|
||||
protected $casts = [
|
||||
'date' => 'datetime',
|
||||
'split' => 'boolean',
|
||||
'reinvested_dividend' => 'boolean'
|
||||
'reinvested_dividend' => 'boolean',
|
||||
];
|
||||
|
||||
protected static function boot()
|
||||
@@ -166,7 +165,8 @@ class Transaction extends Model
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function syncToHolding() {
|
||||
public function syncToHolding()
|
||||
{
|
||||
|
||||
// if symbol name changed, sync previous symbol too
|
||||
if (Arr::has($this->changes, 'symbol')) {
|
||||
@@ -181,7 +181,7 @@ class Transaction extends Model
|
||||
// get the holding for a symbol and portfolio (or create one)
|
||||
Holding::firstOrNew([
|
||||
'portfolio_id' => $this->portfolio_id,
|
||||
'symbol' => $this->symbol
|
||||
'symbol' => $this->symbol,
|
||||
], [
|
||||
'portfolio_id' => $this->portfolio_id,
|
||||
'symbol' => $this->symbol,
|
||||
|
||||
+10
-10
@@ -3,27 +3,27 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasConnectedAccounts;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Laravel\Jetstream\HasProfilePhoto;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Staudenmeir\EloquentHasManyDeep\HasManyDeep;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Staudenmeir\EloquentHasManyDeep\HasRelationships;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Fortify\TwoFactorAuthenticatable;
|
||||
use Laravel\Jetstream\HasProfilePhoto;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Staudenmeir\EloquentHasManyDeep\HasManyDeep;
|
||||
use Staudenmeir\EloquentHasManyDeep\HasRelationships;
|
||||
|
||||
class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
use HasApiTokens;
|
||||
use HasConnectedAccounts;
|
||||
use HasFactory;
|
||||
use HasProfilePhoto;
|
||||
use HasRelationships;
|
||||
use HasUuids;
|
||||
use Notifiable;
|
||||
use TwoFactorAuthenticatable;
|
||||
use HasUuids;
|
||||
use HasRelationships;
|
||||
use HasConnectedAccounts;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class ImportFailedNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
@@ -35,10 +35,10 @@ class ImportFailedNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
return (new MailMessage)
|
||||
->greeting('Oh no!')
|
||||
->subject("Your Investbrain import failed!")
|
||||
->line("Heads up, your Investbrain import was unable to successfully complete. There were errors which caused the import to fail.")
|
||||
->action("Try again?", route('import-export'))
|
||||
->line("**Technical details:**")
|
||||
->subject('Your Investbrain import failed!')
|
||||
->line('Heads up, your Investbrain import was unable to successfully complete. There were errors which caused the import to fail.')
|
||||
->action('Try again?', route('import-export'))
|
||||
->line('**Technical details:**')
|
||||
->line($this->errorMessage);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Portfolio;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class ImportSucceededNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
@@ -35,9 +33,9 @@ class ImportSucceededNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
return (new MailMessage)
|
||||
->greeting('Woot! 🎉')
|
||||
->subject("Your Investbrain import was successful!")
|
||||
->line("Just a heads up that your Investbrain import succeeded! Your portfolios, transactions, and daily changes are now available in your account.")
|
||||
->action("Get Started", route('dashboard'));
|
||||
->subject('Your Investbrain import was successful!')
|
||||
->line('Just a heads up that your Investbrain import succeeded! Your portfolios, transactions, and daily changes are now available in your account.')
|
||||
->action('Get Started', route('dashboard'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class InvitedOnboardingNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
@@ -45,8 +45,8 @@ class InvitedOnboardingNotification extends Notification implements ShouldQueue
|
||||
->subject("You've been invited to {$this->portfolio->title} on Investbrain!")
|
||||
->line("{$this->sender->name} has invited you to **{$this->portfolio->title}** on Investbrain, a smart open-source investment tracker that consolidates and monitors market performance across your different brokerages.")
|
||||
->line("Once you're in, you'll be able to see all the holdings, dividends, market performance and more for {$this->portfolio->title}!")
|
||||
->action("Get Started", $url)
|
||||
->line("If you have any questions, you can reply to this email.")
|
||||
->action('Get Started', $url)
|
||||
->line('If you have any questions, you can reply to this email.')
|
||||
->salutation("See you there,\n".e($this->sender->name));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use App\Models\ConnectedAccount;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class VerifyConnectedAccountNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
@@ -44,7 +44,7 @@ class VerifyConnectedAccountNotification extends Notification implements ShouldQ
|
||||
->subject("Connect your $provider account with Investbrain")
|
||||
->line("You recently attempted to log into an existing Investbrain account using $provider. To safeguard your Investbrain account, please confirm this was you by pressing the 'Connect $provider' button below:")
|
||||
->action("Connect $provider", $url)
|
||||
->line("If you do not recognize this activity, we recommend [changing your password](".route('profile.show').") as soon as possible. Otherwise, you can disregard this message. This link will expire in {$days} days.");
|
||||
->line('If you do not recognize this activity, we recommend [changing your password]('.route('profile.show').") as soon as possible. Otherwise, you can disregard this message. This link will expire in {$days} days.");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,25 +2,18 @@
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\User;
|
||||
|
||||
class PortfolioPolicy
|
||||
{
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function readOnly(User $user, Portfolio $portfolio)
|
||||
{
|
||||
$pivot = $portfolio->users()->where('user_id', $user->id)->first();
|
||||
|
||||
return !!$pivot;
|
||||
return (bool) $pivot;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function fullAccess(User $user, Portfolio $portfolio)
|
||||
{
|
||||
$pivot = $portfolio->users()->where('user_id', $user->id)->first();
|
||||
@@ -28,9 +21,6 @@ class PortfolioPolicy
|
||||
return $pivot && ($pivot->pivot->full_access || $pivot->pivot->owner);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function owner(User $user, Portfolio $portfolio)
|
||||
{
|
||||
$pivot = $portfolio->users()->where('user_id', $user->id)->first();
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Laravel\Jetstream\Features;
|
||||
use App\Actions\Jetstream\DeleteUser;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Laravel\Jetstream\Features;
|
||||
use Laravel\Jetstream\Jetstream;
|
||||
|
||||
class JetstreamServiceProvider extends ServiceProvider
|
||||
|
||||
@@ -26,11 +26,6 @@ class QuantityValidationRule implements ValidationRule
|
||||
|
||||
/**
|
||||
* Validate the attribute.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
* @param \Closure $fail
|
||||
* @return void
|
||||
*/
|
||||
public function validate(string $attribute, mixed $value, \Closure $fail): void
|
||||
{
|
||||
|
||||
@@ -22,11 +22,6 @@ class SymbolValidationRule implements ValidationRule
|
||||
|
||||
/**
|
||||
* Validate the attribute.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
* @param \Closure $fail
|
||||
* @return void
|
||||
*/
|
||||
public function validate(string $attribute, mixed $value, \Closure $fail): void
|
||||
{
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\Holding;
|
||||
use App\Models\Portfolio;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class Spotlight
|
||||
@@ -28,7 +26,7 @@ class Spotlight
|
||||
'name' => 'Portfolio: '.$portfolio->title,
|
||||
'description' => null,
|
||||
'link' => route('portfolio.show', ['portfolio' => $portfolio->id]),
|
||||
'avatar' => null
|
||||
'avatar' => null,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -46,7 +44,7 @@ class Spotlight
|
||||
'name' => 'Holding: '.$holding->market_data->name.' ('.$holding->symbol.')',
|
||||
'description' => $holding->portfolio->title,
|
||||
'link' => route('holding.show', ['portfolio' => $holding->portfolio->id, 'symbol' => $holding->symbol]),
|
||||
'avatar' => null
|
||||
'avatar' => null,
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -24,11 +24,12 @@ trait HasCompositePrimaryKey
|
||||
{
|
||||
foreach ($this->getKeyName() as $key) {
|
||||
// UPDATE: Added isset() per devflow's comment.
|
||||
if (isset($this->$key))
|
||||
if (isset($this->$key)) {
|
||||
$query->where($key, '=', $this->$key);
|
||||
else
|
||||
} else {
|
||||
throw new \Exception(__METHOD__.'Missing part of the primary key: '.$key);
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
@@ -48,6 +49,7 @@ trait HasCompositePrimaryKey
|
||||
foreach ($me->getKeyName() as $key) {
|
||||
$query->where($key, '=', $ids[$key]);
|
||||
}
|
||||
|
||||
return $query->first($columns);
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ namespace App\Traits;
|
||||
|
||||
use App\Models\ConnectedAccount;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @property Collection $connectedAccounts
|
||||
|
||||
@@ -15,5 +15,5 @@ return [
|
||||
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
|
||||
'daily_change_time_of_day' => env('DAILY_CHANGE_TIME', '23:00')
|
||||
'daily_change_time_of_day' => env('DAILY_CHANGE_TIME', '23:00'),
|
||||
];
|
||||
+2
-3
@@ -13,7 +13,6 @@ return [
|
||||
* prefix => 'mary-'
|
||||
* <x-mary-button />
|
||||
* <x-mary-card />
|
||||
*
|
||||
*/
|
||||
'prefix' => '',
|
||||
|
||||
@@ -40,6 +39,6 @@ return [
|
||||
'components' => [
|
||||
'spotlight' => [
|
||||
'class' => 'App\Support\Spotlight',
|
||||
]
|
||||
]
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
+5
-5
@@ -41,7 +41,7 @@ return [
|
||||
'redirect' => '/auth/github/callback',
|
||||
'logo' => 'github-icon',
|
||||
'color' => '#393939',
|
||||
'name' => 'GitHub'
|
||||
'name' => 'GitHub',
|
||||
],
|
||||
|
||||
'google' => [
|
||||
@@ -49,7 +49,7 @@ return [
|
||||
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
|
||||
'redirect' => '/auth/google/callback',
|
||||
'color' => '#4285F4',
|
||||
'name' => 'Google'
|
||||
'name' => 'Google',
|
||||
],
|
||||
|
||||
'facebook' => [
|
||||
@@ -57,7 +57,7 @@ return [
|
||||
'client_secret' => env('FACEBOOK_CLIENT_SECRET'),
|
||||
'redirect' => '/auth/facebook/callback',
|
||||
'color' => '#0165E1',
|
||||
'name' => 'Facebook'
|
||||
'name' => 'Facebook',
|
||||
],
|
||||
|
||||
'linkedin-openid' => [
|
||||
@@ -65,10 +65,10 @@ return [
|
||||
'client_secret' => env('LINKEDIN_CLIENT_SECRET'),
|
||||
'redirect' => '/auth/linkedin-openid/callback',
|
||||
'color' => '#0a66c2',
|
||||
'name' => 'Linkedin'
|
||||
'name' => 'Linkedin',
|
||||
],
|
||||
|
||||
//
|
||||
'enabled_login_providers' => env('ENABLED_LOGIN_PROVIDERS', ''),
|
||||
'ai_chat_enabled' => env('AI_CHAT_ENABLED', false)
|
||||
'ai_chat_enabled' => env('AI_CHAT_ENABLED', false),
|
||||
];
|
||||
|
||||
@@ -83,7 +83,7 @@ class TransactionFactory extends Factory
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'transaction_type' => 'BUY',
|
||||
'cost_basis' => $this->faker->randomFloat(2, 10, 500),
|
||||
'sale_price' => null
|
||||
'sale_price' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class UserFactory extends Factory
|
||||
'two_factor_secret' => null,
|
||||
'two_factor_recovery_codes' => null,
|
||||
'remember_token' => Str::random(10),
|
||||
'profile_photo_path' => null
|
||||
'profile_photo_path' => null,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Portfolio;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
use Database\Seeders\MarketDataSeeder;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateMarketDataTable extends Migration
|
||||
{
|
||||
@@ -34,7 +34,7 @@ class CreateMarketDataTable extends Migration
|
||||
|
||||
Artisan::call('db:seed', [
|
||||
'--class' => MarketDataSeeder::class,
|
||||
'--force' => true
|
||||
'--force' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Portfolio;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateDailyChangeTable extends Migration
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Models\MarketData;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateSplitsTable extends Migration
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\MarketData;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use App\Models\Portfolio;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateTransactionsTable extends Migration
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\MarketData;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use App\Models\Portfolio;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateHoldingsTable extends Migration
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateAiChatsTable extends Migration
|
||||
{
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
|
||||
class MarketDataSeeder extends Seeder
|
||||
{
|
||||
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\ApiControllers\UserController;
|
||||
use App\Http\ApiControllers\HoldingController;
|
||||
use App\Http\ApiControllers\PortfolioController;
|
||||
use App\Http\ApiControllers\MarketDataController;
|
||||
use App\Http\ApiControllers\PortfolioController;
|
||||
use App\Http\ApiControllers\TransactionController;
|
||||
use App\Http\ApiControllers\UserController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware(['auth:sanctum'])->name('api.')->group(function () {
|
||||
|
||||
|
||||
+5
-6
@@ -1,35 +1,34 @@
|
||||
<?php
|
||||
|
||||
use App\Console\Commands\CaptureDailyChange;
|
||||
use App\Console\Commands\RefreshDividendData;
|
||||
use App\Console\Commands\RefreshMarketData;
|
||||
use App\Console\Commands\RefreshSplitData;
|
||||
use App\Console\Commands\SyncHoldingData;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
use App\Console\Commands\{RefreshMarketData, CaptureDailyChange, RefreshDividendData, RefreshSplitData, SyncHoldingData};
|
||||
|
||||
/**
|
||||
*
|
||||
* This scheduled job refreshes market data from your selected data provider
|
||||
* Update the cadence with the MARKET_DATA_REFRESH key in your env file
|
||||
*/
|
||||
Schedule::command(RefreshMarketData::class)->weekdays()->everyMinute();
|
||||
|
||||
/**
|
||||
*
|
||||
* This scheduled job records daily changes to your portfolios every weekday
|
||||
*/
|
||||
Schedule::command(CaptureDailyChange::class)->dailyAt(config('investbrain.daily_change_time_of_day'))->weekdays();
|
||||
|
||||
/**
|
||||
*
|
||||
* Refreshes dividend data for your holdings (and syncs new dividends to holdings)
|
||||
*/
|
||||
Schedule::command(RefreshDividendData::class)->daily()->days([1, 3, 5]);
|
||||
|
||||
/**
|
||||
*
|
||||
* Refreshes split data for your holdings (and creates new transactions for new splits)
|
||||
*/
|
||||
Schedule::command(RefreshSplitData::class)->weekly();
|
||||
|
||||
/**
|
||||
*
|
||||
* Periodically reconciles your holdings with transactions and dividends
|
||||
*/
|
||||
Schedule::command(SyncHoldingData::class)->yearly();
|
||||
|
||||
+5
-5
@@ -1,13 +1,13 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\HoldingController;
|
||||
use App\Http\Controllers\ConnectedAccountController;
|
||||
use App\Http\Controllers\DashboardController;
|
||||
use App\Http\Controllers\HoldingController;
|
||||
use App\Http\Controllers\InvitedOnboardingController;
|
||||
use App\Http\Controllers\PortfolioController;
|
||||
use App\Http\Controllers\TransactionController;
|
||||
use App\Http\Controllers\ConnectedAccountController;
|
||||
use App\Http\Controllers\InvitedOnboardingController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Laravel\Jetstream\Http\Controllers\Livewire\PrivacyPolicyController;
|
||||
use Laravel\Jetstream\Http\Controllers\Livewire\TermsOfServiceController;
|
||||
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
|
||||
namespace Tests\Api;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Models\User;
|
||||
use App\Models\Holding;
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class HoldingsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Portfolio $portfolio;
|
||||
|
||||
protected function setUp(): void
|
||||
@@ -36,7 +37,7 @@ class HoldingsTest extends TestCase
|
||||
->assertJsonStructure([
|
||||
'data' => [['id', 'symbol', 'portfolio_id', 'total_market_value', 'dividends_earned']],
|
||||
'meta' => ['current_page', 'last_page', 'total'],
|
||||
'links' => ['first', 'last', 'prev', 'next']
|
||||
'links' => ['first', 'last', 'prev', 'next'],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -88,14 +89,14 @@ class HoldingsTest extends TestCase
|
||||
$transaction = Transaction::factory()->create();
|
||||
|
||||
$data = [
|
||||
'reinvest_dividends' => true
|
||||
'reinvest_dividends' => true,
|
||||
];
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->putJson(route('api.holding.update', ['portfolio' => $transaction->portfolio_id, 'symbol' => $transaction->symbol]), $data)
|
||||
->assertOk()
|
||||
->assertJsonFragment([
|
||||
'reinvest_dividends' => true
|
||||
'reinvest_dividends' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -105,7 +106,7 @@ class HoldingsTest extends TestCase
|
||||
$transaction = Transaction::factory()->create();
|
||||
|
||||
$data = [
|
||||
'reinvest_dividends' => true
|
||||
'reinvest_dividends' => true,
|
||||
];
|
||||
|
||||
$otherUser = User::factory()->create();
|
||||
@@ -113,5 +114,4 @@ class HoldingsTest extends TestCase
|
||||
->putJson(route('api.holding.update', ['portfolio' => $transaction->portfolio_id, 'symbol' => $transaction->symbol]), $data)
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,16 +2,17 @@
|
||||
|
||||
namespace Tests\Api;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Models\User;
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PortfoliosTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Portfolio $portfolio;
|
||||
|
||||
protected function setUp(): void
|
||||
@@ -34,7 +35,7 @@ class PortfoliosTest extends TestCase
|
||||
->assertJsonStructure([
|
||||
'data' => [['id', 'title', 'owner', 'holdings', 'transactions']],
|
||||
'meta' => ['current_page', 'last_page', 'total'],
|
||||
'links' => ['first', 'last', 'prev', 'next']
|
||||
'links' => ['first', 'last', 'prev', 'next'],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -126,7 +127,7 @@ class PortfoliosTest extends TestCase
|
||||
->putJson(route('api.portfolio.update', $portfolio), ['title' => 'A brand new updated title'])
|
||||
->assertOk()
|
||||
->assertJsonFragment([
|
||||
'title' => 'A brand new updated title'
|
||||
'title' => 'A brand new updated title',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
|
||||
namespace Tests\Api;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Models\User;
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TransactionsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected Portfolio $portfolio;
|
||||
|
||||
protected function setUp(): void
|
||||
@@ -40,7 +41,7 @@ class TransactionsTest extends TestCase
|
||||
->assertJsonStructure([
|
||||
'data' => [['id', 'symbol', 'transaction_type', 'portfolio_id', 'date']],
|
||||
'meta' => ['current_page', 'last_page', 'total'],
|
||||
'links' => ['first', 'last', 'prev', 'next']
|
||||
'links' => ['first', 'last', 'prev', 'next'],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -88,7 +89,7 @@ class TransactionsTest extends TestCase
|
||||
'quantity',
|
||||
'date',
|
||||
'cost_basis',
|
||||
'sale_price'
|
||||
'sale_price',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -97,7 +98,7 @@ class TransactionsTest extends TestCase
|
||||
$this->actingAs($this->user)
|
||||
->postJson(route('api.transaction.store'), [
|
||||
'portfolio_id' => $this->portfolio->id,
|
||||
'symbol' => null
|
||||
'symbol' => null,
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['symbol']);
|
||||
@@ -133,7 +134,7 @@ class TransactionsTest extends TestCase
|
||||
'symbol' => 'ZZZ',
|
||||
'transaction_type' => 'BUY',
|
||||
'cost_basis' => 200.19,
|
||||
'quantity' => 5
|
||||
'quantity' => 5,
|
||||
];
|
||||
|
||||
$this->actingAs($this->user)
|
||||
@@ -162,7 +163,7 @@ class TransactionsTest extends TestCase
|
||||
->putJson(route('api.transaction.update', $transaction), ['symbol' => 'ZZZ'])
|
||||
->assertOk()
|
||||
->assertJsonFragment([
|
||||
'symbol' => 'ZZZ'
|
||||
'symbol' => 'ZZZ',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use Illuminate\Support\Str;
|
||||
use Laravel\Jetstream\Features;
|
||||
use Laravel\Jetstream\Http\Livewire\ApiTokenManager;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ApiTokenPermissionsTest extends TestCase
|
||||
{
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace Tests;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuthenticationTest extends TestCase
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Jetstream\Http\Livewire\LogoutOtherBrowserSessionsForm;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class BrowserSessionsTest extends TestCase
|
||||
{
|
||||
|
||||
@@ -2,19 +2,18 @@
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Models\User;
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\DailyChange;
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class CaptureDailyChangeTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
@@ -25,8 +24,6 @@ class CaptureDailyChangeTest extends TestCase
|
||||
$this->transaction = Transaction::factory()->sell()->lastMonth()->portfolio($this->portfolio->id)->symbol('AAPL')->create();
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public function test_daily_change_for_portfolios()
|
||||
{
|
||||
// Run the command
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Models\User;
|
||||
use App\Http\Controllers\ConnectedAccountController;
|
||||
use App\Models\ConnectedAccount;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Laravel\Socialite\Facades\Socialite;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use App\Http\Controllers\ConnectedAccountController;
|
||||
|
||||
class ConnectedAccountTest extends TestCase
|
||||
{
|
||||
@@ -19,7 +18,7 @@ class ConnectedAccountTest extends TestCase
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->controller = new ConnectedAccountController();
|
||||
$this->controller = new ConnectedAccountController;
|
||||
}
|
||||
|
||||
public function test_handle_provider_callback_with_already_connected_account()
|
||||
@@ -47,7 +46,7 @@ class ConnectedAccountTest extends TestCase
|
||||
'provider_id' => $providerUser->id,
|
||||
'user_id' => $user->id,
|
||||
'token' => $providerUser->token,
|
||||
'verified_at' => now()
|
||||
'verified_at' => now(),
|
||||
]);
|
||||
|
||||
Socialite::shouldReceive('driver')
|
||||
@@ -164,5 +163,4 @@ class ConnectedAccountTest extends TestCase
|
||||
$response->assertRedirect(route('dashboard'));
|
||||
$response->assertSessionHas('toast');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-10
@@ -2,19 +2,16 @@
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Models\User;
|
||||
use App\Models\Holding;
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
class DashboardTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
*/
|
||||
public function test_user_has_portfolios(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
@@ -24,8 +21,6 @@ class DashboardTest extends TestCase
|
||||
$this->assertCount(5, $user->portfolios);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public function test_user_has_transactions(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
@@ -35,8 +30,6 @@ class DashboardTest extends TestCase
|
||||
$this->assertCount(10, $user->transactions);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public function test_user_has_holdings(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
@@ -48,8 +41,6 @@ class DashboardTest extends TestCase
|
||||
$this->assertCount(1, $user->holdings);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public function test_user_has_dashboard_metrics(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
@@ -7,7 +7,6 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Jetstream\Features;
|
||||
use Laravel\Jetstream\Http\Livewire\DeleteUserForm;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DeleteAccountTest extends TestCase
|
||||
{
|
||||
|
||||
+4
-12
@@ -2,22 +2,18 @@
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Models\User;
|
||||
use App\Models\Split;
|
||||
use App\Models\Holding;
|
||||
use App\Models\Dividend;
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\Holding;
|
||||
use App\Models\MarketData;
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
class DividendsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
*/
|
||||
public function test_new_dividends_update_holding(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
@@ -36,8 +32,6 @@ class DividendsTest extends TestCase
|
||||
$this->assertEquals(4.95, $holding->dividends_earned);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public function test_new_dividends_are_reinvested(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
@@ -62,8 +56,6 @@ class DividendsTest extends TestCase
|
||||
$this->assertEqualsWithDelta(4.95, $dividendsReinvested * $market_data->market_value, 0.01);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public function test_do_not_duplicate_recent_dividends(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
@@ -76,7 +68,7 @@ class DividendsTest extends TestCase
|
||||
Dividend::create([
|
||||
'symbol' => 'ACME',
|
||||
'date' => now()->subDay(2),
|
||||
'dividend_amount' => .01
|
||||
'dividend_amount' => .01,
|
||||
]);
|
||||
|
||||
Dividend::refreshDividendData('ACME');
|
||||
|
||||
@@ -8,7 +8,6 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EmailVerificationTest extends TestCase
|
||||
{
|
||||
|
||||
@@ -2,24 +2,23 @@
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Mockery;
|
||||
use Tests\TestCase;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Interfaces\MarketData\YahooMarketData;
|
||||
use App\Interfaces\MarketData\FallbackInterface;
|
||||
use App\Interfaces\MarketData\AlphaVantageMarketData;
|
||||
use App\Interfaces\MarketData\FallbackInterface;
|
||||
use App\Interfaces\MarketData\Types\Quote;
|
||||
use App\Interfaces\MarketData\YahooMarketData;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mockery;
|
||||
|
||||
class FallbackInterfaceTest extends TestCase
|
||||
{
|
||||
public function setUp(): void
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Log::spy();
|
||||
}
|
||||
|
||||
public function testFallbackToNextProviderOnFailure()
|
||||
public function test_fallback_to_next_provider_on_failure()
|
||||
{
|
||||
config()->set('investbrain.provider', 'yahoo,alphavantage');
|
||||
config()->set('investbrain.interfaces', [
|
||||
@@ -29,7 +28,7 @@ class FallbackInterfaceTest extends TestCase
|
||||
|
||||
$yahooMock = Mockery::mock(YahooMarketData::class);
|
||||
$yahooMock->shouldReceive('quote')
|
||||
->andThrow(new \Exception("Yahoo failed"));
|
||||
->andThrow(new \Exception('Yahoo failed'));
|
||||
|
||||
$alphaMock = Mockery::mock(AlphaVantageMarketData::class);
|
||||
$alphaMock->shouldReceive('quote')
|
||||
@@ -38,7 +37,7 @@ class FallbackInterfaceTest extends TestCase
|
||||
$this->app->instance(YahooMarketData::class, $yahooMock);
|
||||
$this->app->instance(AlphaVantageMarketData::class, $alphaMock);
|
||||
|
||||
$fallbackInterface = new FallbackInterface();
|
||||
$fallbackInterface = new FallbackInterface;
|
||||
|
||||
$result = $fallbackInterface->quote('ACME');
|
||||
|
||||
@@ -47,7 +46,7 @@ class FallbackInterfaceTest extends TestCase
|
||||
Log::shouldHaveReceived('warning')->with('Failed calling method quote (yahoo): Yahoo failed');
|
||||
}
|
||||
|
||||
public function testAllProvidersFail()
|
||||
public function test_all_providers_fail()
|
||||
{
|
||||
config()->set('investbrain.provider', 'yahoo,alpha');
|
||||
config()->set('investbrain.interfaces', [
|
||||
@@ -57,16 +56,16 @@ class FallbackInterfaceTest extends TestCase
|
||||
|
||||
$yahooMock = Mockery::mock(YahooMarketData::class);
|
||||
$yahooMock->shouldReceive('quote')
|
||||
->andThrow(new \Exception("Yahoo failed"));
|
||||
->andThrow(new \Exception('Yahoo failed'));
|
||||
|
||||
$alphaMock = Mockery::mock(AlphaVantageMarketData::class);
|
||||
$alphaMock->shouldReceive('quote')
|
||||
->andThrow(new \Exception("Alpha failed"));
|
||||
->andThrow(new \Exception('Alpha failed'));
|
||||
|
||||
$this->app->instance(YahooMarketData::class, $yahooMock);
|
||||
$this->app->instance(AlphaVantageMarketData::class, $alphaMock);
|
||||
|
||||
$fallbackInterface = new FallbackInterface();
|
||||
$fallbackInterface = new FallbackInterface;
|
||||
|
||||
$this->expectException(\Exception::class);
|
||||
$this->expectExceptionMessage('Could not get market data: Provider [alpha] is not a valid market data interface.');
|
||||
|
||||
@@ -2,20 +2,17 @@
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Models\User;
|
||||
use App\Models\Transaction;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use App\Exports\BackupExport;
|
||||
use App\Models\BackupImport as BackupImportModel;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class ImportExportTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
*/
|
||||
public function test_can_create_exports(): void
|
||||
{
|
||||
Excel::fake();
|
||||
@@ -31,15 +28,13 @@ class ImportExportTest extends TestCase
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public function test_backup_job_completes(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
$backup_job = BackupImportModel::create([
|
||||
'user_id' => auth()->user()->id,
|
||||
'path' => __DIR__.'/0000_00_00_import_test.xlsx'
|
||||
'path' => __DIR__.'/0000_00_00_import_test.xlsx',
|
||||
]);
|
||||
|
||||
$backup_job->refresh();
|
||||
@@ -47,29 +42,25 @@ class ImportExportTest extends TestCase
|
||||
$this->assertEquals('success', $backup_job->status);
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public function test_backup_job_inserts_rows(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
BackupImportModel::create([
|
||||
'user_id' => auth()->user()->id,
|
||||
'path' => __DIR__.'/0000_00_00_import_test.xlsx'
|
||||
'path' => __DIR__.'/0000_00_00_import_test.xlsx',
|
||||
]);
|
||||
|
||||
$this->assertEquals(3, $user->transactions->count());
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
public function test_backup_job_calculates_correct_holding_data(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
BackupImportModel::create([
|
||||
'user_id' => auth()->user()->id,
|
||||
'path' => __DIR__.'/0000_00_00_import_test.xlsx'
|
||||
'path' => __DIR__.'/0000_00_00_import_test.xlsx',
|
||||
]);
|
||||
|
||||
$holding = $user->holdings->first();
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace Tests;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordConfirmationTest extends TestCase
|
||||
{
|
||||
|
||||
@@ -7,7 +7,6 @@ use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordResetTest extends TestCase
|
||||
{
|
||||
|
||||
@@ -2,27 +2,29 @@
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Models\User;
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\User;
|
||||
use App\Policies\PortfolioPolicy;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class PortfolioPolicyTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected $policy;
|
||||
|
||||
protected $owner;
|
||||
|
||||
protected $user;
|
||||
|
||||
protected $portfolio;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->policy = new PortfolioPolicy();
|
||||
$this->policy = new PortfolioPolicy;
|
||||
|
||||
$this->owner = User::factory()->create();
|
||||
Auth::login($this->owner);
|
||||
@@ -34,7 +36,7 @@ class PortfolioPolicyTest extends TestCase
|
||||
$this->user->id => [
|
||||
'full_access' => false,
|
||||
'owner' => false,
|
||||
]
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -109,5 +111,4 @@ class PortfolioPolicyTest extends TestCase
|
||||
$result = $this->policy->owner($this->user, $this->portfolio);
|
||||
$this->assertFalse($result, 'User should not be the owner');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user