From 5208744e110fe0deb40862b33e0a1111b14446aa Mon Sep 17 00:00:00 2001 From: Austin Kregel Date: Wed, 5 Aug 2020 23:10:02 -0400 Subject: [PATCH 1/7] Update the budget bits some --- .../Api/AbstractResourceController.php | 14 +++- app/Http/Controllers/Api/TagController.php | 1 + .../AbstractRouteServiceProvider.php | 2 + app/Providers/AppServiceProvider.php | 73 +++++++++++++++++++ composer.json | 1 + composer.lock | 47 +++++++++++- package.json | 2 +- resources/js/app.js | 7 ++ resources/js/components/Utils/Card.vue | 2 +- resources/js/routes/BaseRoute.vue | 5 ++ resources/js/settings-app.js | 1 + resources/js/settings/Plaid/DatePicker.vue | 34 +++++---- resources/js/store.js | 1 + routes/web.php | 1 + 14 files changed, 172 insertions(+), 19 deletions(-) diff --git a/app/Http/Controllers/Api/AbstractResourceController.php b/app/Http/Controllers/Api/AbstractResourceController.php index 3f850695..ab245c2a 100644 --- a/app/Http/Controllers/Api/AbstractResourceController.php +++ b/app/Http/Controllers/Api/AbstractResourceController.php @@ -5,7 +5,9 @@ use App\FailedJob; use App\Http\Controllers\Controller; use App\Models\Transaction; +use App\Tag; use Exception; +use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Validation\ValidatesRequests; use Kregel\LaravelAbstract\AbstractEloquentModel; @@ -43,6 +45,10 @@ public function index(IndexRequest $request, AbstractEloquentModel $model) $query->where('user_id', auth()->id()); } + if (get_class($model) === Tag::class) { + $query->withSum('transactions.amount'); + } + return $this->json($action->execute($query)); } @@ -73,9 +79,15 @@ public function show(ViewRequest $request, AbstractEloquentModel $model, Abstrac return $this->json($result); } + /** + * @param UpdateRequest $request + * @param AbstractEloquentModel $model + * @param AbstractEloquentModel|Model $abstractEloquentModel + * @return \Illuminate\Http\JsonResponse|object + */ public function update(UpdateRequest $request, AbstractEloquentModel $model, AbstractEloquentModel $abstractEloquentModel) { - $abstractEloquentModel->update($request->all()); + $abstractEloquentModel->update($request->validated()); return $this->json($abstractEloquentModel->refresh()); } diff --git a/app/Http/Controllers/Api/TagController.php b/app/Http/Controllers/Api/TagController.php index 9fac2024..34d1dbc5 100644 --- a/app/Http/Controllers/Api/TagController.php +++ b/app/Http/Controllers/Api/TagController.php @@ -8,6 +8,7 @@ use App\Http\Requests\Tag\ConditionalUpdateRequest; use App\Http\Requests\Tag\DestroyRequest; use App\Jobs\SyncTagsWithTransactionsInDatabase; +use App\Models\Transaction; use App\Tag; use Exception; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; diff --git a/app/Providers/AbstractRouteServiceProvider.php b/app/Providers/AbstractRouteServiceProvider.php index 2568807c..bcd772da 100644 --- a/app/Providers/AbstractRouteServiceProvider.php +++ b/app/Providers/AbstractRouteServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\AccountKpi; +use App\Budget; use App\FailedJob; use App\Models\Alert; use App\Models\DatabaseNotification; @@ -41,6 +42,7 @@ public function boot() 'groups' => Tag::class, 'alerts' => Alert::class, 'failed_jobs' => FailedJob::class, + 'budgets' => Budget::class, ]); Route::bind('abstract_model', abstracted()->resolveModelsUsing ?? function ($value) { diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index f9685db6..666e7ae4 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -7,6 +7,9 @@ use App\Repositories\AccountRepositoryEloquent; use App\Services\Banking\PlaidService; use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Str; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Query\Expression; class AppServiceProvider extends ServiceProvider { @@ -19,6 +22,76 @@ public function register() { $this->app->bind(AccountRepository::class, AccountRepositoryEloquent::class); $this->app->bind(PlaidServiceContract::class, PlaidService::class); + /** + * @see https://stackoverflow.com/a/58668526/2736425 + */ + Builder::macro('withSum', function ($columns) { + if (empty($columns)) { + return $this; + } + + if (is_null($this->query->columns)) { + $this->query->select([$this->query->from.'.*']); + } + + $columns = is_array($columns) ? $columns : func_get_args(); + $columnAndConstraints = []; + + foreach ($columns as $name => $constraints) { + // If the "name" value is a numeric key, we can assume that no + // constraints have been specified. We'll just put an empty + // Closure there, so that we can treat them all the same. + if (is_numeric($name)) { + $name = $constraints; + $constraints = static function () { + // + }; + } + + $columnAndConstraints[$name] = $constraints; + } + + foreach ($columnAndConstraints as $name => $constraints) { + $segments = explode(' ', $name); + + unset($alias); + + if (count($segments) === 3 && Str::lower($segments[1]) === 'as') { + [$name, $alias] = [$segments[0], $segments[2]]; + } + + // Here we'll extract the relation name and the actual column name that's need to sum. + $segments = explode('.', $name); + + $relationName = $segments[0]; + $column = $segments[1]; + + $relation = $this->getRelationWithoutConstraints($relationName); + + $query = $relation->getRelationExistenceQuery( + $relation->getRelated()->newQuery(), + $this, + new Expression("sum(`$column`)") + )->setBindings([], 'select'); + + $query->callScope($constraints); + + $query = $query->mergeConstraintsFrom($relation->getQuery())->toBase(); + + if (count($query->columns) > 1) { + $query->columns = [$query->columns[0]]; + } + + // Finally we will add the proper result column alias to the query and run the subselect + // statement against the query builder. Then we will return the builder instance back + // to the developer for further constraint chaining that needs to take place on it. + $column = $alias ?? Str::snake(Str::replaceFirst('.', ' ', $name.'_sum')); + + $this->selectSub($query, $column); + } + + return $this; + }); } /** diff --git a/composer.json b/composer.json index b871484e..0773d554 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "laravel/ui": "^2.0", "mustache/mustache": "^2.13", "predis/predis": "^1.1", + "rlanvin/php-rrule": "^2.2", "spatie/laravel-tags": "^2.6", "staudenmeir/belongs-to-through": "^2.10", "staudenmeir/eloquent-has-many-deep": "^1.7" diff --git a/composer.lock b/composer.lock index c1b44de6..b1e167ba 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "76e1d63ca0c3267ad89fe5ec62e07e5d", + "content-hash": "595eb33b973d57075e80fb3b2eda0ed6", "packages": [ { "name": "asm89/stack-cors", @@ -3905,6 +3905,51 @@ ], "time": "2020-03-29T20:13:32+00:00" }, + { + "name": "rlanvin/php-rrule", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/rlanvin/php-rrule.git", + "reference": "931d53d162cd84b46f6fa388cb4ea916bec02c18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rlanvin/php-rrule/zipball/931d53d162cd84b46f6fa388cb4ea916bec02c18", + "reference": "931d53d162cd84b46f6fa388cb4ea916bec02c18", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "require-dev": { + "phpmd/phpmd": "@stable", + "phpunit/phpunit": "^4.8|^5.5|^6.5" + }, + "suggest": { + "ext-intl": "Intl extension is needed for humanReadable()" + }, + "type": "library", + "autoload": { + "psr-4": { + "RRule\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Lightweight and fast recurrence rules for PHP (RFC 5545)", + "homepage": "https://github.com/rlanvin/php-rrule", + "keywords": [ + "date", + "ical", + "recurrence", + "recurring", + "rrule" + ], + "time": "2019-11-01T11:51:17+00:00" + }, { "name": "spatie/eloquent-sortable", "version": "3.8.2", diff --git a/package.json b/package.json index 60d46f5b..ee8e6e04 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "bootstrap": "^4.1.0", "browser-sync": "^2.26.7", "browser-sync-webpack-plugin": "2.0.1", - "cross-env": "^5.1", + "cross-env": "^5.2.1", "jquery": "^3.2", "laravel-mix": "^4.0.7", "lodash": "^4.17.19", diff --git a/resources/js/app.js b/resources/js/app.js index f723e5f7..bacb603b 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -13,6 +13,7 @@ import locale from 'dayjs/plugin/localizedFormat' import relativeTime from 'dayjs/plugin/relativeTime' window.buildUrl = buildUrl +dayjs.extend(require('dayjs/plugin/utc')) dayjs.extend(relativeTime) dayjs.extend(locale) dayjs.extend(require('./FormatToLocaleTimezone').default) @@ -83,6 +84,11 @@ const router = new VueRouter({ component: require('./routes/TransactionAlerts').default, props: true, }, + { + path: '/budgets', + component: require('./routes/Budgets').default, + props: true, + }, { path: '*', component: require('./routes/404').default, @@ -108,6 +114,7 @@ const start = async () => { await store.dispatch('fetchAccounts'); store.dispatch('fetchGroups'); store.dispatch('fetchAlerts'); + store.dispatch('fetchBudgets'); } catch (e) { console.error(e) } diff --git a/resources/js/components/Utils/Card.vue b/resources/js/components/Utils/Card.vue index 0abd2ab4..8a0baaf4 100644 --- a/resources/js/components/Utils/Card.vue +++ b/resources/js/components/Utils/Card.vue @@ -1,7 +1,7 @@ + + diff --git a/resources/js/routes/Budgets.vue b/resources/js/routes/Budgets.vue new file mode 100644 index 00000000..75e051e0 --- /dev/null +++ b/resources/js/routes/Budgets.vue @@ -0,0 +1,102 @@ + + + + diff --git a/resources/js/state/Budgets.js b/resources/js/state/Budgets.js new file mode 100644 index 00000000..ed16346e --- /dev/null +++ b/resources/js/state/Budgets.js @@ -0,0 +1,53 @@ +export default { + state: { + budgets: { + loading: true, + data: [], + links: {}, + meta: { + current_page: 0 + } + }, + }, + getters: { + budgets: (state) => state.budgets, + budgetsById: (state) => state.budgets.data.reduce((budgets, budget) => ({ + ...budgets, + [budget.id]: budget, + }), {}), + }, + actions: { + async fetchBudgets({ dispatch, state, commit }) { + const { data: budgets } = await axios.get(buildUrl('/abstract-api/budgets', { + include: 'tags', + filter: { + totalSpends: true, + } + })); + + state.budgets = { + ...budgets, + loading: false, + }; + }, + async saveBudget({ state, dispatch }, formData) { + try { + const { data: budget } = await axios.post('/abstract-api/budgets', formData) + } catch (e) { + console.error(e); + } finally { + await dispatch('fetchBudgets') + } + }, + async updateBudget({ state, dispatch }, { original, updated, tags }) { + try { + await axios.put('/abstract-api/budgets/' + original.id, updated) + await axios.put('/api/budgets/' + original.id +'/tags', tags); + } catch (e) { + console.error(e); + } finally { + await dispatch('fetchBudgets') + } + } + } +} From 7bc9834f925fbd678c82bf27ec3c3d62c31002e3 Mon Sep 17 00:00:00 2001 From: Austin Kregel Date: Mon, 10 Aug 2020 01:37:54 -0400 Subject: [PATCH 3/7] Add logic to app to send alerts when a budget breaches, and also make the budget variable available. --- app/Budget.php | 58 +++++++++- ...okens.php => CheckBudgetBreachCommand.php} | 14 +-- .../GenerateChannelsAndAlertsFile.php | 20 +--- app/Console/Kernel.php | 6 +- .../BudgetBreachedEstablishedAmount.php | 25 +++++ app/Jobs/CheckBudgetsForBreachesOfAmount.php | 45 ++++++++ .../TriggerAlertForBreachedBudget.php | 37 +++++++ ...fConditionsPassListenerForTransaction.php} | 2 +- .../BudgetBreachEstablishedAmountMail.php | 38 +++++++ app/Models/Alert.php | 26 +++-- app/Models/AlertLog.php | 5 + app/Notifications/AlertNotiffication.php | 14 ++- ...tBreachedEstablishedAmountNotification.php | 30 ++++++ app/Providers/EventServiceProvider.php | 14 ++- database/factories/BudgetFactory.php | 20 ++++ ...2020_07_28_035054_create_budgets_table.php | 3 +- ...08_10_002938_add_triggered_by_budget_d.php | 34 ++++++ public/mix-manifest.json | 3 +- resources/js/alert-events.js | 4 + resources/js/routes/Budgets.vue | 6 +- resources/views/mail/budget-breach.blade.php | 5 + .../CheckBudgetsForBreachesOfAmountTest.php | 102 ++++++++++++++++++ ...ToTransactionAutomaticallyListenerTest.php | 2 +- ...iggerAlertIfConditionsPassListenerTest.php | 26 +++-- tests/Unit/ExampleTest.php | 19 ---- 25 files changed, 480 insertions(+), 78 deletions(-) rename app/Console/Commands/{SyncTokens.php => CheckBudgetBreachCommand.php} (62%) create mode 100644 app/Events/BudgetBreachedEstablishedAmount.php create mode 100644 app/Jobs/CheckBudgetsForBreachesOfAmount.php create mode 100644 app/Listeners/TriggerAlertForBreachedBudget.php rename app/Listeners/{TriggerAlertIfConditionsPassListener.php => TriggerAlertIfConditionsPassListenerForTransaction.php} (97%) create mode 100644 app/Mail/BudgetBreachEstablishedAmountMail.php create mode 100644 app/Notifications/BudgetBreachedEstablishedAmountNotification.php create mode 100644 database/factories/BudgetFactory.php create mode 100644 database/migrations/2020_08_10_002938_add_triggered_by_budget_d.php create mode 100644 resources/views/mail/budget-breach.blade.php create mode 100644 tests/Integration/Jobs/CheckBudgetsForBreachesOfAmountTest.php delete mode 100644 tests/Unit/ExampleTest.php diff --git a/app/Budget.php b/app/Budget.php index fae8e3d5..0a973c3e 100644 --- a/app/Budget.php +++ b/app/Budget.php @@ -10,11 +10,48 @@ use Illuminate\Validation\Rule; use Kregel\LaravelAbstract\AbstractEloquentModel; use Kregel\LaravelAbstract\AbstractModelTrait; +use RRule\RRule; use Spatie\QueryBuilder\AllowedFilter; use Spatie\QueryBuilder\Filters\FiltersScope; use Spatie\Tags\HasTags; use Znck\Eloquent\Traits\BelongsToThrough; +/** + * App\Budget + * + * @property int $id + * @property int $user_id + * @property string $name + * @property float $amount + * @property string $frequency + * @property string $interval + * @property \Illuminate\Support\Carbon $started_at + * @property int|null $count + * @property \Illuminate\Support\Carbon|null $created_at + * @property \Illuminate\Support\Carbon|null $updated_at + * @property \Illuminate\Database\Eloquent\Collection|\Spatie\Tags\Tag[] $tags + * @property-read int|null $tags_count + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget newModelQuery() + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget newQuery() + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget q($string) + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget query() + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget totalSpends() + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget whereAmount($value) + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget whereCount($value) + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget whereCreatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget whereFrequency($value) + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget whereId($value) + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget whereInterval($value) + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget whereName($value) + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget whereStartedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget whereUpdatedAt($value) + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget whereUserId($value) + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget withAllTags($tags, $type = null) + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget withAllTagsOfAnyType($tags) + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget withAnyTags($tags, $type = null) + * @method static \Illuminate\Database\Eloquent\Builder|\App\Budget withAnyTagsOfAnyType($tags) + * @mixin \Eloquent + */ class Budget extends Model implements AbstractEloquentModel { use AbstractModelTrait, BelongsToThrough, HasTags; @@ -22,7 +59,8 @@ class Budget extends Model implements AbstractEloquentModel public $guarded = []; protected $casts = [ - 'started_at' => 'datetime' + 'started_at' => 'datetime', + 'breached_at' => 'datetime', ]; public static function booted() @@ -48,7 +86,7 @@ public function scopeTotalSpends(Builder $query) WHEN frequency='DAILY' THEN @periodStart:=if(DATE_ADD(started_at, INTERVAL @diff DAY) < now(), DATE_ADD(started_at, INTERVAL @diff DAY), DATE_ADD(started_at, INTERVAL @diff-1 DAY)) WHEN frequency='WEEKLY' THEN @periodStart:=if(DATE_ADD(started_at, INTERVAL @diff WEEK) < now(), DATE_ADD(started_at, INTERVAL @diff WEEK), DATE_ADD(started_at, INTERVAL @diff-1 WEEK)) END as period_started_at"), - \DB::raw(" CASE + \DB::raw("CASE WHEN frequency='YEARLY' THEN if(DATE_ADD(started_at, INTERVAL @diff YEAR) < now(), DATE_ADD(started_at, INTERVAL @diff+1 YEAR), DATE_ADD(started_at, INTERVAL @diff YEAR)) WHEN frequency='MONTHLY' THEN if(DATE_ADD(started_at, INTERVAL @diff MONTH) < now(), DATE_ADD(started_at, INTERVAL @diff+1 MONTH), DATE_ADD(started_at, INTERVAL @diff MONTH)) WHEN frequency='DAILY' THEN if(DATE_ADD(started_at, INTERVAL @diff DAY) < now(), DATE_ADD(started_at, INTERVAL @diff+1 DAY), DATE_ADD(started_at, INTERVAL @diff DAY)) @@ -139,4 +177,20 @@ public function getAbstractSearchableFields (): array { return []; } + + public function getRule(): RRule + { + return new RRule(array_merge([ + 'FREQ' => $this->frequency, + 'INTERVAL' => $this->interval, + 'DTSTART' => $this->started_at, + ], $this->count ? [ + 'COUNT' => $this->count, + ] : [])); + } + + public function user() + { + return $this->belongsTo(User::class); + } } diff --git a/app/Console/Commands/SyncTokens.php b/app/Console/Commands/CheckBudgetBreachCommand.php similarity index 62% rename from app/Console/Commands/SyncTokens.php rename to app/Console/Commands/CheckBudgetBreachCommand.php index 268eaf4f..01246f1d 100644 --- a/app/Console/Commands/SyncTokens.php +++ b/app/Console/Commands/CheckBudgetBreachCommand.php @@ -2,23 +2,24 @@ namespace App\Console\Commands; +use App\Jobs\CheckBudgetsForBreachesOfAmount; use Illuminate\Console\Command; -class SyncTokens extends Command +class CheckBudgetBreachCommand extends Command { /** * The name and signature of the console command. * * @var string */ - protected $signature = 'sync:tokens'; + protected $signature = 'check:budget-breach'; /** * The console command description. * * @var string */ - protected $description = 'Sync the accounts from the stored plaid access tokens.'; + protected $description = 'Check the budgets to see if they\'re over budget'; /** * Create a new command instance. @@ -30,13 +31,8 @@ public function __construct() parent::__construct(); } - /** - * Execute the console command. - * - * @return mixed - */ public function handle() { - // + CheckBudgetsForBreachesOfAmount::dispatch(); } } diff --git a/app/Console/Commands/GenerateChannelsAndAlertsFile.php b/app/Console/Commands/GenerateChannelsAndAlertsFile.php index a85ea754..fb4854c5 100644 --- a/app/Console/Commands/GenerateChannelsAndAlertsFile.php +++ b/app/Console/Commands/GenerateChannelsAndAlertsFile.php @@ -4,6 +4,7 @@ use App\AccountKpi; use App\Condition; +use App\Events\BudgetBreachedEstablishedAmount; use App\Events\TransactionCreated; use App\Events\TransactionGroupedEvent; use App\Events\TransactionUpdated; @@ -76,6 +77,10 @@ public function handle() 'type' => TransactionGroupedEvent::class, 'name' => 'When a transaction is added to a group (this gives you access to the `tag` variable in your title, body and payload.)' ], + [ + 'type' => BudgetBreachedEstablishedAmount::class, + 'name' => 'When a budget\'s total spend amount for a period exceeds the set amount.' + ] ]); $this->writeToDisk('js/condition-parameters.js', [ @@ -109,21 +114,6 @@ public function handle() ], ]); - $this->writeToDisk('js/alert-events.js', [ - [ - 'type' => TransactionUpdated::class, - 'name' => 'When a transaction is updated (moving from pending to not pending, updating amounts, etc...)' - ], - [ - 'type' => TransactionCreated::class, - 'name' => 'When a transaction is initially created (only fired once per transaction)', - ], - [ - 'type' => TransactionGroupedEvent::class, - 'name' => 'When a transaction is added to a group (this gives you access to the `tag` variable in your title, body and payload.)' - ], - ]); - $this->writeToDisk('js/condition-comparator.js', array_map(function ($comparator) { return [ 'value' => $comparator, diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 07dd49ea..219dc5d9 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -22,8 +22,12 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule) { - $schedule->command('sync:plaid 7')->hourly(); + $schedule->command('plaid:sync-institutions')->monthly(); + $schedule->command('plaid:sync-categories')->monthly(); $schedule->command('generate:account-kpis')->dailyAt('23:55'); + $schedule->command('sync:plaid 1')->hourlyAt(0); + // Small job offset so we don't flood the queue. Not really ever going to be a problem... but meh :shrug: + $schedule->command('check:budget-breach')->hourlyAt(10); } /** diff --git a/app/Events/BudgetBreachedEstablishedAmount.php b/app/Events/BudgetBreachedEstablishedAmount.php new file mode 100644 index 00000000..f3f08150 --- /dev/null +++ b/app/Events/BudgetBreachedEstablishedAmount.php @@ -0,0 +1,25 @@ +budget = $budget; + } + + public function getBudget(): Budget + { + return $this->budget; + } +} diff --git a/app/Jobs/CheckBudgetsForBreachesOfAmount.php b/app/Jobs/CheckBudgetsForBreachesOfAmount.php new file mode 100644 index 00000000..9d3e3b7c --- /dev/null +++ b/app/Jobs/CheckBudgetsForBreachesOfAmount.php @@ -0,0 +1,45 @@ +paginate(50, [], 'page', $page++); + + /** @var Budget $budget */ + foreach ($budgets->items() as $budget) { + // The last time the budget started it's current period. + $startOfTheLastBudgetPeriod = Carbon::parse($budget->getRule()->getOccurrencesBefore(now(), true ,1)[0]); + // 80 minutes should give the system time to catch in-consistent runs. + // The cron should run every hour, so things will only trigger once. + if (!empty($budget->breached_at) && $startOfTheLastBudgetPeriod->diffInMinutes($budget->breached_at) > 80) { + // If the budget has already breached, and it did so several hours ago we don't want to spam users... + return; + } + + if ($budget->amount < $budget->total_spend && empty($budget->breached_at)) { + event(new BudgetBreachedEstablishedAmount($budget)); + $budget->update([ + 'breached_at' => now() + ]); + } + } + } while ($budgets->hasMorePages()); + } +} diff --git a/app/Listeners/TriggerAlertForBreachedBudget.php b/app/Listeners/TriggerAlertForBreachedBudget.php new file mode 100644 index 00000000..1bfe93e8 --- /dev/null +++ b/app/Listeners/TriggerAlertForBreachedBudget.php @@ -0,0 +1,37 @@ +getBudget(); + + $budget = Budget::totalSpends()->with('user')->find($budget->id); + + $user = $budget->user; + + /** @var Collection $alertsToTrigger */ + $alertsToTrigger = $user->alerts() + ->whereJsonContains('events', BudgetBreachedEstablishedAmount::class) + ->get(); + + $alertsToTrigger->map->createBudgetBreachNotification($budget); + } +} diff --git a/app/Listeners/TriggerAlertIfConditionsPassListener.php b/app/Listeners/TriggerAlertIfConditionsPassListenerForTransaction.php similarity index 97% rename from app/Listeners/TriggerAlertIfConditionsPassListener.php rename to app/Listeners/TriggerAlertIfConditionsPassListenerForTransaction.php index be4d3cd1..76fdae53 100644 --- a/app/Listeners/TriggerAlertIfConditionsPassListener.php +++ b/app/Listeners/TriggerAlertIfConditionsPassListenerForTransaction.php @@ -11,7 +11,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Collection; -class TriggerAlertIfConditionsPassListener implements ShouldQueue +class TriggerAlertIfConditionsPassListenerForTransaction implements ShouldQueue { use InteractsWithQueue; diff --git a/app/Mail/BudgetBreachEstablishedAmountMail.php b/app/Mail/BudgetBreachEstablishedAmountMail.php new file mode 100644 index 00000000..8427fc06 --- /dev/null +++ b/app/Mail/BudgetBreachEstablishedAmountMail.php @@ -0,0 +1,38 @@ +alertLog = $alertLog; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + $this->alertLog->load([ + 'budget', + ]); + + return $this->markdown('mail.budget-breach', [ + 'alertLog' => $this->alertLog, + 'budget' => $this->alertLog->budget, + ]); + } +} diff --git a/app/Models/Alert.php b/app/Models/Alert.php index 26016770..0e05a460 100644 --- a/app/Models/Alert.php +++ b/app/Models/Alert.php @@ -2,9 +2,11 @@ namespace App\Models; +use App\Budget; use App\Contracts\ConditionableContract; use App\Models\Traits\Conditionable; use App\Notifications\AlertNotiffication; +use App\Notifications\BudgetBreachedEstablishedAmountNotification; use App\Notifications\TransactionAlertNotification; use App\Notifications\TransactionTagAlertNotification; use App\Tag; @@ -116,6 +118,12 @@ public function getAbstractSearchableFields(): array return ['user_id', 'name', 'order_column', 'type']; } + protected function notifyAbout($notification) + { + $this->load('user'); + $this->user->notify($notification); + } + public function createNotification(Transaction $transaction) { /** @var AlertLog $log */ @@ -125,9 +133,7 @@ public function createNotification(Transaction $transaction) // do the same thing as create notification, but give access to the tag variab $notification = new TransactionTagAlertNotification($log); - foreach ($this->channels as $channel) { - $this->user->notify($notification); - } + $this->notifyAbout($notification); } public function createNotificationWithTag(Transaction $transaction, Tag $tag) @@ -140,9 +146,15 @@ public function createNotificationWithTag(Transaction $transaction, Tag $tag) // do the same thing as create notification, but give access to the tag variab $notification = new TransactionTagAlertNotification($log); - foreach ($this->channels as $channel) { - $this->user->notify($notification); -// \Notification::channel($channel)->send($this->user, $notification); - } + $this->notifyAbout($notification); + } + + public function createBudgetBreachNotification(Budget $budget) + { + $notification = new BudgetBreachedEstablishedAmountNotification($this->logs()->create([ + 'triggered_by_budget_id' => $budget->id, + ])); + + $this->notifyAbout($notification); } } diff --git a/app/Models/AlertLog.php b/app/Models/AlertLog.php index afb8b5f7..193dcf2a 100644 --- a/app/Models/AlertLog.php +++ b/app/Models/AlertLog.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Budget; use App\Tag; use Illuminate\Database\Eloquent\Model; @@ -46,4 +47,8 @@ public function transaction() { return $this->belongsTo(Transaction::class, 'triggered_by_transaction_id'); } + public function budget() + { + return $this->belongsTo(Budget::class, 'triggered_by_budget_id'); + } } diff --git a/app/Notifications/AlertNotiffication.php b/app/Notifications/AlertNotiffication.php index 41ea704d..1c53a6d0 100644 --- a/app/Notifications/AlertNotiffication.php +++ b/app/Notifications/AlertNotiffication.php @@ -19,7 +19,7 @@ class AlertNotiffication extends Notification public function __construct(AlertLog $alertLog) { - $alertLog->load(['alert', 'transaction']); + $alertLog->load(['alert', 'transaction', 'budget']); $this->alertLog = $alertLog; } @@ -42,11 +42,15 @@ public function toArray($notifiable) protected function renderField($field) { if (Str::contains($field, ['{{', '}}'])) { - return $this->render($field, array_merge([ + return $this->render($field, array_merge($this->alertLog->transaction ? [ 'transaction' => $this->alertLog->transaction->toArray(), - ], $this->alertLog->tag ? [ - 'tag' => $this->alertLog->tag->toArray(), - ] : [])); + ] : [], + $this->alertLog->tag ? [ + 'tag' => $this->alertLog->tag->toArray(), + ] : [], + $this->alertLog->budget ? [ + 'budget' => $this->alertLog->budget->toArray(), + ] : [])); } return $field; diff --git a/app/Notifications/BudgetBreachedEstablishedAmountNotification.php b/app/Notifications/BudgetBreachedEstablishedAmountNotification.php new file mode 100644 index 00000000..38b9e71b --- /dev/null +++ b/app/Notifications/BudgetBreachedEstablishedAmountNotification.php @@ -0,0 +1,30 @@ +alertLog))->to($notifiable->email); + } + + public function toSlack($notifiable) + { + return (new SlackMessage) + ->content(sprintf('Your budget %s breached the set amount!', $this->alertLog->budget->name)) + ->to($this->alertLog->alert->messaging_service_channel) + ->warning(); + } + + public function toDiscord($notifiable) + { + return (new DiscordMessage) + ->body(sprintf('Your budget %s breached the set amount!', $this->alertLog->budget->name)); + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index d3b87ba2..0924ccf8 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\Events\BudgetBreachedEstablishedAmount; use App\Events\RegroupEvent; use App\Events\TransactionCreated; use App\Events\TransactionGroupedEvent; @@ -9,7 +10,8 @@ use App\Listeners\ApplyGroupToTransactionAutomaticallyListener; use App\Listeners\CreateDefaultAlertsForUser; use App\Listeners\CreateDefaultTagsForUser; -use App\Listeners\TriggerAlertIfConditionsPassListener; +use App\Listeners\TriggerAlertForBreachedBudget; +use App\Listeners\TriggerAlertIfConditionsPassListenerForTransaction; use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; @@ -30,21 +32,25 @@ class EventServiceProvider extends ServiceProvider TransactionUpdated::class => [ ApplyGroupToTransactionAutomaticallyListener::class, - TriggerAlertIfConditionsPassListener::class, + TriggerAlertIfConditionsPassListenerForTransaction::class, ], TransactionCreated::class => [ ApplyGroupToTransactionAutomaticallyListener::class, - TriggerAlertIfConditionsPassListener::class, + TriggerAlertIfConditionsPassListenerForTransaction::class, ], RegroupEvent::class => [ ApplyGroupToTransactionAutomaticallyListener::class, ], + BudgetBreachedEstablishedAmount::class => [ + TriggerAlertForBreachedBudget::class, + ], + TransactionGroupedEvent::class => [ // A transaction was newly grouped into some group. Do something. - TriggerAlertIfConditionsPassListener::class + TriggerAlertIfConditionsPassListenerForTransaction::class ], ]; diff --git a/database/factories/BudgetFactory.php b/database/factories/BudgetFactory.php new file mode 100644 index 00000000..09a07e69 --- /dev/null +++ b/database/factories/BudgetFactory.php @@ -0,0 +1,20 @@ +define(App\Budget::class, function (Faker $faker) { + return [ + 'user_id' => function () { + return factory(\App\User::class)->create()->id; + }, + 'name' => $faker->name, + 'amount' => $faker->numberBetween(10, 30), + 'frequency' => 1, + 'interval' => 'MONTHLY', + 'started_at' => $faker->dateTime, + 'count' => null, + 'breached_at' => null, + ]; +}); diff --git a/database/migrations/2020_07_28_035054_create_budgets_table.php b/database/migrations/2020_07_28_035054_create_budgets_table.php index f81163dd..092c68df 100644 --- a/database/migrations/2020_07_28_035054_create_budgets_table.php +++ b/database/migrations/2020_07_28_035054_create_budgets_table.php @@ -20,8 +20,9 @@ public function up() $table->double('amount'); $table->string('frequency'); // Year, month, week, $table->string('interval'); - $table->timestamp('started_at'); + $table->dateTime('started_at'); $table->integer('count')->nullable(); + $table->dateTime('breached_at')->nullable(); $table->timestamps(); }); } diff --git a/database/migrations/2020_08_10_002938_add_triggered_by_budget_d.php b/database/migrations/2020_08_10_002938_add_triggered_by_budget_d.php new file mode 100644 index 00000000..5c251590 --- /dev/null +++ b/database/migrations/2020_08_10_002938_add_triggered_by_budget_d.php @@ -0,0 +1,34 @@ +unsignedInteger('triggered_by_transaction_id')->nullable()->change(); + $table->unsignedInteger('triggered_by_budget_id')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('alert_logs', function (Blueprint $table) { + $table->unsignedInteger('triggered_by_transaction_id')->change(); + $table->dropColumn('triggered_by_budget_id'); + }); + } +} diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 62b469ea..56dab799 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -1,5 +1,6 @@ { "/js/app.js": "/js/app.js", "/css/app.css": "/css/app.css", - "/js/settings-app.js": "/js/settings-app.js" + "/js/settings-app.js": "/js/settings-app.js", + "/js/app.0ae4edddf3ef7bb70617.hot-update.js": "/js/app.0ae4edddf3ef7bb70617.hot-update.js" } diff --git a/resources/js/alert-events.js b/resources/js/alert-events.js index c3fd9deb..0afd55cb 100644 --- a/resources/js/alert-events.js +++ b/resources/js/alert-events.js @@ -10,5 +10,9 @@ module.exports = [ { "type": "App\\Events\\TransactionGroupedEvent", "name": "When a transaction is added to a group (this gives you access to the `tag` variable in your title, body and payload.)" + }, + { + "type": "App\\Events\\BudgetBreachedEstablishedAmount", + "name": "When a budget's total spend amount for a period exceeds the set amount." } ] \ No newline at end of file diff --git a/resources/js/routes/Budgets.vue b/resources/js/routes/Budgets.vue index 75e051e0..8dab9f65 100644 --- a/resources/js/routes/Budgets.vue +++ b/resources/js/routes/Budgets.vue @@ -13,7 +13,7 @@
- A budget here is a dollar amount and time period for a group... + A budget is a dollar amount and time period for one or more groups...
@@ -45,8 +45,8 @@
-
-
+
+
{{ tag.name.en }}
diff --git a/resources/views/mail/budget-breach.blade.php b/resources/views/mail/budget-breach.blade.php new file mode 100644 index 00000000..03e0d579 --- /dev/null +++ b/resources/views/mail/budget-breach.blade.php @@ -0,0 +1,5 @@ +@component('mail::message') +# You budget {{ $budget->name }} has breached! + +You set the amount ${{$budget->amount}}, but you have spent ${{$budget->total_spend}} +@endcomponent diff --git a/tests/Integration/Jobs/CheckBudgetsForBreachesOfAmountTest.php b/tests/Integration/Jobs/CheckBudgetsForBreachesOfAmountTest.php new file mode 100644 index 00000000..b9e8ddf5 --- /dev/null +++ b/tests/Integration/Jobs/CheckBudgetsForBreachesOfAmountTest.php @@ -0,0 +1,102 @@ +handle(); + + $this->doesntExpectEvents([ + BudgetBreachedEstablishedAmount::class + ]); + } + + public function testHandleBudgets() + { + Carbon::setTestNow($now = Carbon::create(2020, 1, 1, 0, 0, 0)); + + $user = factory(User::class)->create(); + + $token = $user->accessTokens()->create([ + 'token' => Str::random(16), + 'should_sync' => true, + ]); + + $account = $token->accounts()->create(factory(Account::class)->make()->toArray()); + + factory(Budget::class)->create([ + 'name' => 'Fake budget', + 'amount' => 100, + 'frequency' => 1, + 'interval' => 'MONTHLY', + 'started_at' => $now, + 'count' => 1, + ]); + + $budget = factory(Budget::class)->create([ + 'name' => 'food', + 'amount' => 10, + 'frequency' => 1, + 'interval' => 'MONTHLY', + 'started_at' => $now, + 'count' => 1, + ]); + + $category = factory(Category::class)->create(); + + factory(Budget::class)->create([ + 'name' => 'This other budget', + 'amount' => 100, + 'frequency' => 1, + 'interval' => 'MONTHLY', + 'started_at' => $now, + 'count' => 1, + 'user_id' => $user->id, + ]); + + $tag = factory(Tag::class)->create(); + + $budget->tags()->sync([$tag->id]); + + $transaction = Transaction::create([ + 'name' => 'Subway', + 'amount' => 15, + 'account_id' => $account->account_id, + 'date' => $now->addDay(), + 'pending' => false, + 'category_id' => $category->category_id, + 'transaction_id' => Str::random(32), + 'transaction_type' => 'special', + ]); + + $transaction->tags()->sync([$tag->id]); + Carbon::setTestNow($now = Carbon::create(2020, 1, 15, 0, 0, 0)); + + $job = new CheckBudgetsForBreachesOfAmount; + + $job->handle(); + + $this->doesntExpectEvents([ + BudgetBreachedEstablishedAmount::class + ]); + } +} \ No newline at end of file diff --git a/tests/Integration/Listeners/ApplyGroupToTransactionAutomaticallyListenerTest.php b/tests/Integration/Listeners/ApplyGroupToTransactionAutomaticallyListenerTest.php index 2a2144d3..6ef57bcc 100644 --- a/tests/Integration/Listeners/ApplyGroupToTransactionAutomaticallyListenerTest.php +++ b/tests/Integration/Listeners/ApplyGroupToTransactionAutomaticallyListenerTest.php @@ -6,7 +6,7 @@ use App\Events\TransactionCreated; use App\Events\TransactionGroupedEvent; use App\Listeners\ApplyGroupToTransactionAutomaticallyListener; -use App\Listeners\TriggerAlertIfConditionsPassListener; +use App\Listeners\TriggerAlertIfConditionsPassListenerForTransaction; use App\Models\Alert; use App\Models\Category; use App\Models\Transaction; diff --git a/tests/Integration/Listeners/TriggerAlertIfConditionsPassListenerTest.php b/tests/Integration/Listeners/TriggerAlertIfConditionsPassListenerTest.php index 7cb4d3d9..6890f263 100644 --- a/tests/Integration/Listeners/TriggerAlertIfConditionsPassListenerTest.php +++ b/tests/Integration/Listeners/TriggerAlertIfConditionsPassListenerTest.php @@ -5,7 +5,7 @@ use App\Condition; use App\Events\TransactionCreated; use App\Events\TransactionGroupedEvent; -use App\Listeners\TriggerAlertIfConditionsPassListener; +use App\Listeners\TriggerAlertIfConditionsPassListenerForTransaction; use App\Models\Alert; use App\Models\Category; use App\Models\Transaction; @@ -24,6 +24,7 @@ public function testHandleWithNoConditionalsCreatesAlert() /** @var Transaction $transaction */ $transaction = factory(Transaction::class)->create(); + $transaction->load('user.alerts'); /** @var Alert $alert */ $alert = $transaction->user->alerts()->create([ 'name' => 'Alert', @@ -38,7 +39,7 @@ public function testHandleWithNoConditionalsCreatesAlert() $event = new TransactionCreated($transaction); - $listener = new TriggerAlertIfConditionsPassListener(); + $listener = new TriggerAlertIfConditionsPassListenerForTransaction(); $this->assertEmpty(\DB::table('notifications')->get()->all()); $listener->handle($event); @@ -54,6 +55,7 @@ public function testHandleWithConditionalCreatesAlert() $transaction = factory(Transaction::class)->create([ 'amount' => 100, ]); + $transaction->load('user.alerts'); /** @var Alert $alert */ $alert = $transaction->user->alerts()->create([ @@ -73,7 +75,7 @@ public function testHandleWithConditionalCreatesAlert() ]); $event = new TransactionCreated($transaction); - $listener = new TriggerAlertIfConditionsPassListener(); + $listener = new TriggerAlertIfConditionsPassListenerForTransaction(); $this->assertEmpty(\DB::table('notifications')->get()->all()); $listener->handle($event); @@ -89,6 +91,7 @@ public function testHandleWithConditionalDoesntCreatesAlert() $transaction = factory(Transaction::class)->create([ 'amount' => 10, ]); + $transaction->load('user.alerts'); /** @var Alert $alert */ $alert = $transaction->user->alerts()->create([ @@ -108,7 +111,7 @@ public function testHandleWithConditionalDoesntCreatesAlert() ]); $event = new TransactionCreated($transaction); - $listener = new TriggerAlertIfConditionsPassListener(); + $listener = new TriggerAlertIfConditionsPassListenerForTransaction(); $this->assertEmpty(\DB::table('notifications')->get()->all()); $listener->handle($event); @@ -122,10 +125,11 @@ public function testHandleNothingHappensWhenNoAlertExists() $transaction = factory(Transaction::class)->create([ 'amount' => 100, ]); + $transaction->load('user.alerts'); $event = new TransactionCreated($transaction); - $listener = new TriggerAlertIfConditionsPassListener(); + $listener = new TriggerAlertIfConditionsPassListenerForTransaction(); $this->assertEmpty(\DB::table('notifications')->get()->all()); $listener->handle($event); @@ -136,6 +140,7 @@ public function testHandleCanUseTagInAlert() { /** @var Transaction $transaction */ $transaction = factory(Transaction::class)->create(); + $transaction->load('user.alerts'); /** @var Alert $alert */ $alert = $transaction->user->alerts()->create([ @@ -155,7 +160,7 @@ public function testHandleCanUseTagInAlert() $event = new TransactionGroupedEvent($tag, $transaction); - $listener = new TriggerAlertIfConditionsPassListener(); + $listener = new TriggerAlertIfConditionsPassListenerForTransaction(); $this->assertEmpty(\DB::table('notifications')->get()->all()); $listener->handle($event); @@ -169,6 +174,7 @@ public function testHandleDoesNothingWhenThereIsNoChannel() { /** @var Transaction $transaction */ $transaction = factory(Transaction::class)->create(); + $transaction->load('user.alerts'); /** @var Alert $alert */ $alert = $transaction->user->alerts()->create([ @@ -188,7 +194,7 @@ public function testHandleDoesNothingWhenThereIsNoChannel() $event = new TransactionGroupedEvent($tag, $transaction); - $listener = new TriggerAlertIfConditionsPassListener(); + $listener = new TriggerAlertIfConditionsPassListenerForTransaction(); $this->assertEmpty(\DB::table('notifications')->get()->all()); $listener->handle($event); @@ -199,6 +205,7 @@ public function testHandleDoesNothingWhenThereEventIsNotSelected() { /** @var Transaction $transaction */ $transaction = factory(Transaction::class)->create(); + $transaction->load('user.alerts'); /** @var Alert $alert */ $alert = $transaction->user->alerts()->create([ @@ -218,7 +225,7 @@ public function testHandleDoesNothingWhenThereEventIsNotSelected() $event = new TransactionGroupedEvent($tag, $transaction); - $listener = new TriggerAlertIfConditionsPassListener(); + $listener = new TriggerAlertIfConditionsPassListenerForTransaction(); $this->assertEmpty(\DB::table('notifications')->get()->all()); $listener->handle($event); @@ -232,6 +239,7 @@ public function testHandleCreateAlertBasedOnTag() 'category_id' => Category::firstWhere('name', 'Loans and Mortgages')->category_id, ]); + $transaction->load('user.alerts'); /** @var Alert $alert */ $alert = $transaction->user->alerts()->create([ 'name' => 'Bill paid!', @@ -264,7 +272,7 @@ public function testHandleCreateAlertBasedOnTag() $transaction->setRelations([]); $event = new TransactionGroupedEvent($tag, $transaction); - $listener = new TriggerAlertIfConditionsPassListener(); + $listener = new TriggerAlertIfConditionsPassListenerForTransaction(); $this->assertEmpty(\DB::table('notifications')->get()->all()); $listener->handle($event); diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php deleted file mode 100644 index e9fe19c6..00000000 --- a/tests/Unit/ExampleTest.php +++ /dev/null @@ -1,19 +0,0 @@ -assertTrue(true); - } -} From 21bd02e8edef833d32a355128ce2c3c924e53370 Mon Sep 17 00:00:00 2001 From: Austin Kregel Date: Mon, 10 Aug 2020 01:38:47 -0400 Subject: [PATCH 4/7] Update ide helpers --- _ide_helper.php | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/_ide_helper.php b/_ide_helper.php index c9f7adea..d5cc42e4 100644 --- a/_ide_helper.php +++ b/_ide_helper.php @@ -3,7 +3,7 @@ /** * A helper file for Laravel, to provide autocomplete information to your IDE - * Generated for Laravel 7.17.2 on 2020-07-05 03:21:41. + * Generated for Laravel 7.21.0 on 2020-08-06 04:21:14. * * This file should not be included in your code, only analyzed by your IDE! * @@ -868,6 +868,18 @@ public static function getLocale() return $instance->getLocale(); } + /** + * Get the current application fallback locale. + * + * @return string + * @static + */ + public static function getFallbackLocale() + { + /** @var \Illuminate\Foundation\Application $instance */ + return $instance->getFallbackLocale(); + } + /** * Set the current application locale. * @@ -881,6 +893,19 @@ public static function setLocale($locale) $instance->setLocale($locale); } + /** + * Set the current application fallback locale. + * + * @param string $fallbackLocale + * @return void + * @static + */ + public static function setFallbackLocale($fallbackLocale) + { + /** @var \Illuminate\Foundation\Application $instance */ + $instance->setFallbackLocale($fallbackLocale); + } + /** * Determine if application locale is the given locale. * @@ -5481,7 +5506,7 @@ public static function assertDispatched($event, $callback = null) } /** - * Assert if a event was dispatched a number of times. + * Assert if an event was dispatched a number of times. * * @param string $event * @param int $times @@ -6978,7 +7003,7 @@ public static function extend($driver, $callback) /** * Unset the given channel instance. * - * @param string|null $name + * @param string|null $driver * @return \Illuminate\Log\LogManager * @static */ @@ -18497,6 +18522,8 @@ public static function forPageAfterId($perPage = 15, $lastId = 0, $column = 'id' /** * Remove all existing orders and optionally add a new order. * + * @param string|null $column + * @param string $direction * @return \Illuminate\Database\Query\Builder * @static */ From bc50189da6b324f16c9221d25095418679ebea19 Mon Sep 17 00:00:00 2001 From: Austin Kregel Date: Mon, 10 Aug 2020 01:44:13 -0400 Subject: [PATCH 5/7] Update the tests --- tests/Integration/Jobs/CheckBudgetsForBreachesOfAmountTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Integration/Jobs/CheckBudgetsForBreachesOfAmountTest.php b/tests/Integration/Jobs/CheckBudgetsForBreachesOfAmountTest.php index b9e8ddf5..3cb6ca7e 100644 --- a/tests/Integration/Jobs/CheckBudgetsForBreachesOfAmountTest.php +++ b/tests/Integration/Jobs/CheckBudgetsForBreachesOfAmountTest.php @@ -50,6 +50,7 @@ public function testHandleBudgets() 'interval' => 'MONTHLY', 'started_at' => $now, 'count' => 1, + 'user_id' => $user->id, ]); $budget = factory(Budget::class)->create([ @@ -59,6 +60,7 @@ public function testHandleBudgets() 'interval' => 'MONTHLY', 'started_at' => $now, 'count' => 1, + 'user_id' => $user->id, ]); $category = factory(Category::class)->create(); From c236985864973bd0d7b1b090970c6f6d6369286e Mon Sep 17 00:00:00 2001 From: Austin Kregel Date: Mon, 10 Aug 2020 02:14:32 -0400 Subject: [PATCH 6/7] Fix tests --- app/Budget.php | 4 +++- database/factories/BudgetFactory.php | 4 +--- phpunit.xml | 2 +- .../CheckBudgetsForBreachesOfAmountTest.php | 22 ++++++++++--------- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/Budget.php b/app/Budget.php index 0a973c3e..0d94667e 100644 --- a/app/Budget.php +++ b/app/Budget.php @@ -66,7 +66,9 @@ class Budget extends Model implements AbstractEloquentModel public static function booted() { static::creating(function ($budget) { - $budget->user_id = auth()->id(); + if (empty($budget->user_id) && auth()->check()) { + $budget->user_id = auth()->id(); + } }); } diff --git a/database/factories/BudgetFactory.php b/database/factories/BudgetFactory.php index 09a07e69..c3af9907 100644 --- a/database/factories/BudgetFactory.php +++ b/database/factories/BudgetFactory.php @@ -6,9 +6,7 @@ $factory->define(App\Budget::class, function (Faker $faker) { return [ - 'user_id' => function () { - return factory(\App\User::class)->create()->id; - }, + 'user_id' => factory(\App\User::class), 'name' => $faker->name, 'amount' => $faker->numberBetween(10, 30), 'frequency' => 1, diff --git a/phpunit.xml b/phpunit.xml index 7b3031c4..c7b4a0e5 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -22,7 +22,7 @@ - + diff --git a/tests/Integration/Jobs/CheckBudgetsForBreachesOfAmountTest.php b/tests/Integration/Jobs/CheckBudgetsForBreachesOfAmountTest.php index 3cb6ca7e..bc8b1195 100644 --- a/tests/Integration/Jobs/CheckBudgetsForBreachesOfAmountTest.php +++ b/tests/Integration/Jobs/CheckBudgetsForBreachesOfAmountTest.php @@ -34,7 +34,9 @@ public function testHandleBudgets() { Carbon::setTestNow($now = Carbon::create(2020, 1, 1, 0, 0, 0)); - $user = factory(User::class)->create(); + $user = factory(User::class)->create([ + 'id' => 1 + ]); $token = $user->accessTokens()->create([ 'token' => Str::random(16), @@ -46,21 +48,21 @@ public function testHandleBudgets() factory(Budget::class)->create([ 'name' => 'Fake budget', 'amount' => 100, - 'frequency' => 1, - 'interval' => 'MONTHLY', + 'frequency' => 'MONTHLY', + 'interval' => 1, 'started_at' => $now, 'count' => 1, - 'user_id' => $user->id, + 'user_id' => 1, ]); $budget = factory(Budget::class)->create([ 'name' => 'food', 'amount' => 10, - 'frequency' => 1, - 'interval' => 'MONTHLY', + 'frequency' => 'MONTHLY', + 'interval' => 1, 'started_at' => $now, 'count' => 1, - 'user_id' => $user->id, + 'user_id' => 1, ]); $category = factory(Category::class)->create(); @@ -68,11 +70,11 @@ public function testHandleBudgets() factory(Budget::class)->create([ 'name' => 'This other budget', 'amount' => 100, - 'frequency' => 1, - 'interval' => 'MONTHLY', + 'frequency' => 'MONTHLY', + 'interval' => 1, 'started_at' => $now, 'count' => 1, - 'user_id' => $user->id, + 'user_id' => 1, ]); $tag = factory(Tag::class)->create(); From 799814f18e13e4ac40d81b1e58dbad33107c1754 Mon Sep 17 00:00:00 2001 From: Austin Kregel Date: Mon, 10 Aug 2020 02:17:08 -0400 Subject: [PATCH 7/7] Fix the dang env var for the pipelines --- phpunit.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index c7b4a0e5..7b3031c4 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -22,7 +22,7 @@ - +