# Changelog
## 3.0.60.1 - 2023-05-11

### Changes
- Hotfix for Google Analytics 4 adapter. When GA4 had no data to send we would get a division by 0 error. (src/Services/Google/Adapter/AdapterGA4.php)

## 3.0.60 - 2023-05-05

### Changes
- Hotfix to fix issue with datatables where it will not not work without updating how we use our table formatters (yajra/laravel-datatables-oracle:9.20.0) and HorizonCheck.

## 3.0.59 - 2023-04-20

### Changes
- Added `HorizonCheck` console command for checking the status of Horizon and attempting to restart after 5 minutes. ([WDS-3786](https://brightspot.atlassian.net/browse/WDS-3786))

### Upgrading
- To ensure the application properly utilizes the new `HorizonCheck` console command, the task schedule must be defined in the `app/Console/Kernel.php` file's schedule method as follows:
```php
protected function schedule(Schedule $schedule)
{
    ...
    $schedule->command('ignite:health-check:horizon')->everyMinute();
    ...
}
```

## 3.0.58 - 2023-04-20

### Changes
- Updated `composer.json` and `composer.lock` to fix an issue with `yajra/laravel-datatables-buttons` that introduced a breaking change with their specification of `"maatwebsite/excel": "^3.0"`. Version `"maatwebsite/excel": "3.1.43"` to ensure the module can be installed.
- Updated `.gitignore` to ignore file extension `.bak` which is generally used for backup files that don't need to be included in a project.

## 3.0.57.1 - 2023-03-26

### Changes
- Hotfix to `config/config.php` to make G4 to GA4 for consistency.

### Upgrading
- `GOOGLE_ANALYTICS_PROPERTY_ID` is deprecated; use `GOOGLE_ANALYTICS4_PROPERTY_ID` instead in your `.env.example` and `.env` file.
- You should update your `ignite/theme-default` package to version `3.15.1` or higher.

## 3.0.57- 2023-03-05

### Changes
- The Google Analytics dashboard widgets now support GA4 API by default if you have it configured with the new `GOOGLE_ANALYTICS_PROPERTY_ID` in the `.env` file. Otherwise it will fall back to the UA API. ([WDS-3422](https://brightspot.atlassian.net/browse/WDS-3422))
- This update effects the four widgets: `AverageSessionDurationStat`, `BounceRateStat`, `PageviewsStat`, and `UniqueSessionsStat`.

### Upgrading
- You should add to your `config/core.php` with the changes from `vendor/ignite/modules/Core/config/config.php` for `core.google.analytics.property`, such as:
```php
    'google' => [
        'analytics' => [
            // ...
            // the GA4 Property ID
            'property' => env('GOOGLE_ANALYTICS4_PROPERTY_ID', ''),
            // ...
        ],
    ],
```
- You should also update your `.env.example` and `.env` file with the value or example for `GOOGLE_ANALYTICS_PROPERTY_ID` that is for your program from https://analytics.google.com/analytics/web.
- To get the Property ID for a GA4 (assuming you have already created a GA4 account for your program), go to [Google Analytics](https://analytics.google.com/analytics/web) >> Admin >> [YOUR ACCOUNT NAME] >> [YOUR PROPERTY NAME] >> Property Settings >> Property ID. It should be a 9-digit number.
- It is recommended as well to upgrade your googleapi packages to the latest as
  possible with `composer update google/apiclient google/apiclient-services`.
- If you have an overwrite of any of these classes in your Program, then you may need to update them manually.
    - `Ignite\Core\Models\Dashboard\Charts\AverageSessionDurationStat`
    - `Ignite\Core\Models\Dashboard\Charts\BaseGoogleAnalyticsMetric`
    - `Ignite\Core\Models\Dashboard\Charts\PageviewsStat`
    - `Ignite\Core\Services\Google\Analytics\Client`

## 3.0.56 - 2023-02-13

### Changes
- Updated `Ignite\Core\Entities\Audit::class` `getReadableIdentifier()` method to allow extending of the `getReadableValueByAttributeKey()` method. ([WDS-3616](https://brightspot.atlassian.net/browse/WDS-3616))
- Updated `Ignite\Core\Entities\Audit::class` `getReadableValueByAttributeKey()` method visibility to allow method extension and use of customized items in an auditable entity's `getAuditFriendlyField()` method used in displaying history. ([WDS-3616](https://brightspot.atlassian.net/browse/WDS-3616))
- An example of how this can be used is as follows:
```php
    /**
     * Get a value from the correct value array by attribute.
     *
     * @param  string  $attribute
     * @param  object|null  $model
     * @return string
     */
    protected function getReadableValueByAttributeKey(string $attribute, object $model = null): string
    {
        if ('activity_id' == $attribute) {
            $entity = (new $model->auditable_type)::find($model->auditable_id);
            $attribute = 'Lead ID: '.(new $model->auditable_type)::find($model->auditable_id)->activity->submission->id ?? 
            $attribute;
        }

        if ('product_type' == $attribute) {
            $attribute = 'Product Type: '.
                (
                    (new ProductType)->toDropdown()[$model->new_values[$attribute]] ??
                    (new ProductType)->toDropdown()[$model->old_values[$attribute]] ??
                    $attribute
                );
        }

        if ('customer_name' == $attribute) {
            $attribute = 'Customer Name: '.
                (
                    $model->new_values[$attribute] ??
                    $model->old_values[$attribute] ??
                    $attribute
                );
        }

        if ($model->event === 'deleted') {
            return $model->old_values[$attribute] ?? $attribute;
        }

        return $model->new_values[$attribute] ?? $attribute;
    }
```

## 3.0.55 - 2023-01-30

### Changes
- Load `last_login_at` at the Participant level on demand using `getLastLoginAtAttribute()` method. ([WDS-3397](https://brightspot.atlassian.net/browse/WDS-3397))

## 3.0.54 - 2023-01-19

### Changes
- Send transaction import points status email to participants using `sendTransactionEmailNotification()`. ([WDS-3271](https://brightspot.atlassian.net/browse/WDS-3271))

### Upgrading
- If you have an overwrite of the `Ignite\Core\Services\Importers\Transactions` class in your Program and overwrite the `saveTransaction()` method in that class, then you may need to update it in order to send email notifications of newly imported transactions. The `saveTransaction()` should now call `$this->sendPointsEarnedNotification($transaction);` right before it returns if you want to have that feature.

## 3.0.53 - 2023-01-10

### Changes
- Updated formatters (Formatter, BlockFormatter, HistoryFormatter, ParticipantFormatter, TransactionFormatter, and UserFormatter) and helpers (Date, DateTime, Format) to display datetimes in Ignite's preferred `Y-m-d H:i:s` format and dates in `Y-m-d` format. ([WDS-3274](https://brightspot.atlassian.net/browse/WDS-3274))

### Upgrading
- The reason for this change is to standardize the way dates and datetimes are formatted in the Ignite platform. This should alleviate the need to use moment.js for any sorting of datatables. Datatables, in particular, has issues with sorting by the previous format of `m/d/Y` or `m/d/Y g:ia` out of the box properly since they are treated as a string.
- Ensure **ALL** custom views/grids/reports are updated to display dates in the preferred Ignite formats.
- To ensure formatting -- specifically dates (`Y-m-d`) and datetimes (`Y-m-d H:i:s`) -- is consistent according to Ignite's preferred formats in custom views/grids, the generic `Formatter` class can be used. This class can also be extended if the code needs to use a base formatter and a custom formatter for formats that do not exist in the generic `Formatter` class. For instance, to use this formatter in a custom `CompanyTable` class that extends the `QueryTable` class, the code can be implemented similar to the following:

```php
<?php

namespace Ignite\Program\Models\Grid;

use Ignite\Core\Models\Grid\Formatter;
use Ignite\Core\Models\Grid\QueryTable;

class CompanyTable extends QueryTable
{
    /**
     * @var Formatter
     */
    protected $formatter = null;

    ...
    
    /**
     * Resolve the base formatter
     * 
     * @return Formatter
     */
    protected function formatter(): Formatter
    {
        if (null === $this->formatter) {
            $this->formatter = resolve(Formatter::class);
        }

        return $this->formatter;
    }

    /**
     * Get a map of column keys => functions to format columns.
     *
     * @return array
     */
    protected function getColumnFormattingMap()
    {
        return [
            ...
            'created_at' => function ($model) {
                return $this->formatter()->formatDate($model, 'created_at', 'Y-m-d');
            },
            ...
        ];
    }
}
```

## 3.0.52.3 - 2023-01-11

### Changes
- Hotfix for NotifyProgramManagerWithPostImportInfo where the namespace is incorrect. ([WDS-3571](https://brightspot.atlassian.net/browse/WDS-3571))
- Fix filename listed in for NotifyProgramManagerWithPostImportInfo.
- Also the email blade needs to be in markdown format like other email blade files.

## 3.0.52.2 - 2023-01-11

### Changes
- Hotfix for WaitingTransactionsTable, where instead of hard-coding which field index is transaction_date, it tries to deduce it based on the columns. Then your child class will not have to overwrite this method just because it added new fields and the index has shifted, especially with all the other parameters that getBuilderParameters() now sends. ([WDS-3567](https://brightspot.atlassian.net/browse/WDS-3567))

## 3.0.52 - 2023-01-08

### Changes
- Upgrade Tax Report to show earned/redeemed points and USD. There are default dates and tax year if no filters are selected. The transaction types can be configured now as well. ([WDS-3519](https://brightspot.atlassian.net/browse/WDS-3519))
- Fix the Tax Report filter configurations since they were nested too deep and not working, and applied the wrong date field. The report can be filtered by tax date and country now.
- Minor: Moved `ignite:decrypt-column` to `ignite:data:decrypt`, and made some arguments optional.
- Minor: Make `Ignite\Core\Models\Grid\HistoryFormatter` show `m/d/Y` instead of `d/m/Y`.
- Minor: Make sensitive data shows 'x' instead of '*' if decrypting fails.
- Add a Core composer dependency to require a minimum version of `ignite/theme-default`, which right now is `^3.0.14`.

### Upgrading
- To get the new filters working on the Tax Report, you will need to update your `config/core.php` with the changes from `vendor/ignite/modules/Core/config/config.php` on `core.report.filters` for `Ignite\Core\Models\Report\TaxReport::class` (or `Ignite\Program\Models\Report\TaxReport::class` if you have your own custom Tax Report).
- You should add to your `config/core.php` with the changes from `vendor/ignite/modules/Core/config/config.php` for `core.report.tax-report`, in case you will need to change them from the defaults in the future.
- Because `ignite/theme-default` is now a new Core dependency, you may not be able to update to this version of Core unless you update `ignite/theme-default` as well. See note below in `3.0.50` on what is the best recommendation to to allow an update to this version.

## 3.0.50 - 2023-01-06

### Changes
- Send email notification as part of the `postImport()` method using `NotifyProgramManagerWithPostImportInfo` class. ([WDS-3272](https://brightspot.atlassian.net/browse/WDS-3272))
- Minor cleanup on the `ImportCommand`.
- Also, this change requires adding a new blade template in the default theme from version `3.0.14`.

### Upgrading
- TLDR version - Please run `php -d memory_limit=-1 /usr/local/bin/composer remove ignite/theme-default` and then `php -d memory_limit=-1 /usr/local/bin/composer update ignite/core` for next version of Core.
    - The default theme needs an update to at least `3.0.14`, so you may need to run `'php -d memory_limit=-1 /usr/local/bin/composer update ignite/core ignite/theme-default`. However, starting in next version `3.0.52`, you may not be able to update to it or future versions of Core because `ignite/theme-default` is now a new Core module dependency. You may have a root dependency of `ignite/theme-default` in your program `composer.json`, and your `composer.lock` may already have it locked on `3.0.13` or whatever, so it won't update Core to `3.0.52` or future versions,
    unless you have the right minimum version of `ignite/theme-default` or you also update `ignite/theme-default` first.
        - A `composer update` may be a solution but it will update ALL your dependencies, and that may be dangerous. You may want to focus on updating only the Core module and whatever it requires.
        - When you run `composer update ignite/core` (at least for version 1), it will see Core `3.0.52` requires `ignite/theme-default` minimum version `3.0.14`, however, it will not update your `ignite/theme-default` automatically because it is also a root dependency in your program.
        - So it will just update to this version Core `3.0.50` and leaves you to update `ignite/theme-default` yourself. But if you did not know that, it would leave you scratching your head on why it would not update Core all the way to `3.0.52`. Even if you have the right version of `ignite/theme-default` this time, you may not next time because Core requires a new minimum version of `ignite/theme-default`, and then you would be stuck on the not-latest version of Core and would not be sure why.
    - So the recommendation is to remove `ignite/theme-default` as a root dependency from your program `composer.json`. You can do this with `php -d memory_limit=-1 /usr/local/bin/composer remove ignite/theme-default`. Then run your `php -d memory_limit=-1 /usr/local/bin/composer update ignite/core` after that.
        - Then this will allow the `composer.json` in the Core module to manage what version of `ignite/theme-default` is required in the future, and so you would not be held back by future versions of Core module due to that root dependency in your program `composer.json`.

## 3.0.47 - 2022-12-21

### Changes

- Added `postImport()` to `ImportCommand` (in contrast to `prepare()`) so your specific importer can wrap up the import without using a generic `ImportCompleted` event.
- Fix handling `ImportException` in `ImportCommand` so that `data` contents can be displayed in the import page for rejected rows.
- Fix writer in `ImportCommand` so it does not store every record in memory as it iterates.
- Added `GenericMailable` for when you need a non-abstract one-off Mailable class. It can be used as:
```php
$mailable = new GenericMailable();
$mailable
  ->to(config('mail.from.address'), config('mail.from.name'))
  ->subject('SUBJECT')
  ->markdown('emails.general')
  ->with('message', $message);
Mail::queue($mailable);
// Or the markdown can be done as:
$mailable->markdown('emails.general', compact('message'))
```

## 3.0.46 - 2022-11-29

### Changes
- Added soft deletes to waiting transactions.

### Upgrading
- May need to update your config/core.php file under `query_filters`.`core.transactions.waiting.delete`.
- You may need to re-run seeders for `core_permission.json` with `php artisan db:seed --class="Ignite\Core\Database\Seeders\PermissionTableSeeder"` for `core.transactions.waiting.delete`;
- You could re-run seeders for `core_group_permission.json` with `php artisan db:seed --class="Ignite\Core\Database\Seeders\GroupPermissionTableSeeder"` for `core.transactions.waiting.delete`. But to be safe, either run your Program custom version of it, or manually update it through the admin Permissions page, to not over-write any custom permissions set by the site.
- Note the `WaitingTransactionsTable` class now extends `EloquentTable` instead of `QueryTable`. So if you extend this class and have a reference to grandparent class `QueryTable`, then you may want to change it to `EloquentTable`.

## 3.0.44 - 2022-09-06

### Changes
- Hotfix for reCAPTCHA blade. ([WDS-2869](https://brightspot.atlassian.net/browse/WDS-2869))

## 3.0.43 - 2022-09-05

### Changes
- Implemented Google reCAPTCHA. This is currently WIP, we will update to implement the other versions and types of reCAPTCHA. Currently V2 Robot is the only implementation. ([WDS-2869](https://brightspot.atlassian.net/browse/WDS-2869))

### Upgrading
- If your site doesn't currently have reCAPTCHA installed:
```shell
composer require google/recaptcha "^1.2"
```
- In `config/services.php`, add the following:
```php
<?php
return [
    ...
    'recaptcha' => [
        /* See reCAPTCHA Documentation
         * https://developers.google.com/recaptcha/docs/versions
         * -------------------------------------------------- //
         * ACCEPTABLE OPTIONS
         *
         * Required
         * - sitekey: <string>
         * - secret:  <string>
         *
         * Optional (not fully implemented -- will be in Core)
         * - version:    2 (default)
         *               3
         * - field_type: android
         *               invisible
         *               recaptcha_v2_robot (default) (will update to 'robot' at a later time)
         *               score (version 3 only)
         * -------------------------------------------------- */
        'sitekey' => env('RECAPTCHA_SITEKEY'),
        'secret' => env('RECAPTCHA_SECRET'),
        'version' => env('RECAPTCHA_VERSION', 2),
        'field_type' => env('RECAPTCHA_FIELD_TYPE', 'recaptcha_v2_robot'),
        'field_name' => env('RECAPTCHA_FIELD_NAME', 'g-recaptcha-response'),
    ],
    ...
];
```
- In `.env`, add the following:
```dotenv
RECAPTCHA_SITEKEY=<recaptcha sitekey>
RECAPTCHA_SECRET=<recaptcha secret>
```
- In `config/laravel-form-builder.php`, add the following:
```php
    ...
    'custom_fields' => [
        ...
        'recaptcha_v2_robot' => Ignite\Core\Models\Form\Field\ReCaptchaV2Robot::class,
        ...
    ],
```
- In `enrollment.json`, or custom enrollment file, add the following to add to the list of form fields:
```json
{
  "frontend_type": "recaptcha_v2_robot",
  "name": "g-recaptcha-response",
  "label": "ReCaptcha",
  "rules": [
      "bail",
      "ReCaptchaV2Robot"
  ]
},
```
- If the site provides more than one or a custom enrollment URL, extend the following method from `Ignite\Core\Http\Forms\ProfileForm`:
```php
/**
 * Returns an array of routes in which reCAPTCHA should be used.
 * This is meant to be extended by sites that use more than one
 * enrollment form. It will pass the built-in enrollment form
 * by default.
 *
 * @return array
 */
public function routesForRecaptcha(): array
{
    $routes = [];
    if (config('services.recaptcha.sitekey')) {
        $routes[] = route('participant.enroll.create');
    }

    return $routes;
}
```
- If the site overwrites the `store` method in `Ignite\Core\Http\Controllers\EnrollmentController`, ensure the following is included in your store method before `$form->redirectIfNotValid();`:
```php
if (config('services.recaptcha.sitekey')) {
    $recaptchaFieldName = config('services.recaptcha.field_name');
    $recaptchaFieldType = config('services.recaptcha.field_type');
    $form->addBefore(
        array_key_first($form->getFields()),
        $recaptchaFieldName,
        $recaptchaFieldType,
        $form->getField($recaptchaFieldName)->getOptions()
    );
}
```
and add the following in the `try` block:
```php
if (config('services.recaptcha.sitekey')) {
    $form->remove($recaptchaFieldName);
}
```
- If the site overwrites the `getColumns` method from `Ignite\Core\Models\Grid\ParticipantTable` and does not reference the parent method, add the following in the `getColumns` method:
```php
...
unset($columns[config('services.recaptcha.field_name')]);
...
```

## 3.0.42 - 2022-08-19

### Changes
- Update default client admin access to user pages. ([WDS-3002](https://brightspot.atlassian.net/browse/WDS-3002))
- In `AbstractTable::getColumnNames()`, you can now pass in the $except values a list that contains either the original actual keys or the new keys based on the name attribute. Beforehand, you could only use the keys based on the name attribute.
- Deprecated `core.dashboard.startCurrent`, `endCurrent`, `startPrevious`, `endPrevious` because they are dynamic, but once config is cached, then the values become fixed (stale), which is a bug. See new static config values `core.dashboard.startCurrentOffsetDays`, `endCurrentOffsetDays`, `startPreviousOffsetDays`, and `endPreviousOffsetDays`. ([WDS-3027](https://brightspot.atlassian.net/browse/WDS-3027))
- Fix a bug in ParticipantEngagementGauge where instead of getting the percentage of users logged in the past 90 days, it incorrectly gets the percentage of all logged in users ever.
- Updated `ParticipantEngagementGauge` so it pulls from the `core.participant.types` first, but then gets DISTINCT `types` from database if it has to.

### Upgrading
- In your `config/core.php`, you may want to remove the deprecated configs `core.dashboard.startCurrent`, `endCurrent`, `startPrevious`, `endPrevious`, and replace with new config values `core.dashboard.startCurrentOffsetDays`, `endCurrentOffsetDays`, `startPreviousOffsetDays`, and `endPreviousOffsetDays` from `modules\Core\config\config.php`.
- If desired, you can change or re-seed the client admin access to include user pages.

## 3.0.41 - 2022-07-01

### Changes
- Fix permissions with `core.transactions.waiting.assign` not being used correctly ([WDS-2902](https://brightspot.atlassian.net/browse/WDS-2902))

## 3.0.40 - 2022-06-13

### Changes
- Updated admin dashboard table widget blade ([WDS-2489](https://brightspot.atlassian.net/browse/WDS-2489))

## 3.0.39 - 2022-05-25

### Changes
- Fixed CMS pages in PageController so that if they are not logged in, it will redirect back to the CMS page after the valid login.
- Deprecated `AbstractTable::_dataTable()` to `initDataTable()` instead since `_dataTable()` does not comply with PHP standards. If you needed to overwrite `_dataTable()`, it would produce possible code-sniffing errors, so overwrite `initDataTable()` instead.

## 3.0.38 - 2022-05-24

### Changes
- Updates for allowing the incorporation of the Activity Status Import into the import form partial view and ImportForm ViewComposer. Note: This may be a temporary implementation until we can find a more suitable solution for this cross module implementation ([WDS-2823](https://brightspot.atlassian.net/browse/WDS-2823))

## 3.0.37 - 2022-05-23

### Changes
- Updated to fix issue with loading the Participant too early in the config introduced in 3.0.36 ([WDS-2844](https://brightspot.atlassian.net/browse/WDS-2844))

## 3.0.36 - 2022-05-16

### Changes
- Updated config to include `core.report.participant-types`. This differs from `core.participant.types` which defaults to all Participant types. ([WDS-2844](https://brightspot.atlassian.net/browse/WDS-2844))
- Updated `ParticipantEngagementGauge` to use new core configuration with a default of `['Participant']` if the configuration does not exist to cover existing implementations. ([WDS-2844](https://brightspot.atlassian.net/browse/WDS-2844))
- Updated `ParticipantsEnrolledStat` to use new core configuration. ([WDS-2844](https://brightspot.atlassian.net/browse/WDS-2844))
- Updated `TransactionsRedeemedStat` to correct stat to show the number of redeemed transactions and not the amount awarded. ([WDS-2844](https://brightspot.atlassian.net/browse/WDS-2844))

# 3.0.35
- [X] Updated incorrect media library index page permission from `core.cms.media.index` to `core.cms.media.browse`
- [X] SecureHeadersMiddleware cleanup (alphabetized)

# 3.0.34
- [F] Added query permission filter to Transaction table (using `core.user.transaction.browse` permission).
- [F] Only outputting permission-related debug output if asked specifically for it now :).

# 3.0.33
- [F] Added SecureHeadersMiddleware due to Rapid7 scan results. Make sure to add the following middleware to the application's
  `App\Http\Kernel` `$middleware` property. It may be changed to be auto-loaded in the future, but as of right now, it is not:
  `\Ignite\Core\Http\Middleware\SecureHeadersMiddleware::class,`

# 3.0.32
- [F] Added the ability to get singular or plural version of the "points" translation key.
- [F] Made participant factory and user factory easier to work with. Can pass in a created user without creating
  orphaned users. Passed in user's first, last & email will be used, rather than copying from orphaned user. Status and
  internal states are synced between participant and user records, unless specifically overridden. Added an "external"
  state.
- [X] Removed fast excel from Participant and Enrollment tables. They have getColumnFormattingMap defined (and therefore
  have formatting done in PHP that fast excel will ignore). These were added with the assumption that they would not
  change how the table rendered in the UI versus when exported. Now that we know about this limitation, I think we
  should only add this back after confirming not including this formatting in exports is okay.

# 3.0.31
- [F] Programs no longer need to add a query_filters.permissions array to their core config with any entries packages
  are using. They can now only specify the permissions they care about filtering by, if any.
- [F] QueryFilters are a bit more flexible in that they only require a table and a key name be specified and no longer
  require an Eloquent model, though providing an Eloquent model is still preferred as a shorthand.
- [F] Programs no longer need to ask buildWhereExists for the initial query when building query filters. The provided
  query is now the initialized where-exists query.
- [X] NullQueryPermissionFilter wasn't returning the query and so breaking the QueryFilter interface.

# 3.0.30
- [FB] Added some query filter classes for scoping queries based on a permission action (Rights & Roles). See here for
  details: https://bitbucket.org/brightspotdev/core/pull-requests/3. See also config.php's query_filters.permissions
  array. All of the Global scopes that weren't being used by AT&T (businessrefer&reward) have been removed. If a program
  is relying on a global scope that was removed (none others that I know of), you will either need to add it back to
  Core or use a QueryFilter instead. Make sure to add the query_filters.permissions array to the program's core config.

# 3.0.29
- [F] Added a `postProcessedSource` and `postProcessedDestination` property to the `FileTransmitter`. This is useful
  when userland code overrides the `afterTransmission` method and wants to do something with these files.

# 3.0.28
- [F] Added a `Report::addReport($class)` and a `Report::removeReport($class)` method as helpers.
- [F] Added an `ignite:dev:override-class` console command. This is a start to automating overriding Ignite classes via
  Laravel's container. Right now, it copies the class to the Program module, clears it out and sets it to override the
  Ignite class.

# 3.0.27
- [F] Added the ability to encrypt and decrypt files (with PGP supported out of the box).
  See `\Ignite\Core\Contracts\Files\FileEncrypter` and file_encryption in config/config.php. (use gpg
  --full-generate-key to generate a key for testing. Also
  see: https://start.1password.com/open/i?a=GZULADRXBZBKRCDLSU6O5DLWZA&v=m5hvyi7jjlywfkscea4sdzunee&i=xtpuqj6dpdu3dylassgpptk4t4&h=my.1password.com)
- [F] Added the ability to create file transmitters in userland projects. See `\Ignite\Core\Files\FileTransmitter`. The
  idea is that, because sending and receiving files between a program and a client's SFTP server is common, each program
  can define the transmitters they need as first-class concepts. We can then just run `->transmit()` on them. It is also
  possible to use the FileTransmitter directly, without creating a Transmitter class. As it is now, the FileTransmitter
  expects to pull the file from a filesystem and place it on a filesystem. The source can also be provided as a
  FileGenerator. In the future, if we needed to pull the source file in a different way, we could allow for that as well
  by making source resolution aware of that type. The same is true for destinations. File processors are another new
  concept. See `\Ignite\Core\Contracts\Files\FileProcessor`. They can be added to a transmission. They provide a
  declarative way to specify what should happen to the files before and after transmission.
- [F] Added a `File` and `TemporaryFile` class. These represent files on a filesystem. The main impetus behind these was
  not wanting to have to pass around a relative file path along with a disk name. These also make interacting with files
  a bit nicer IMO. For instance, if you have a file instance and want to get its full path, you just need to
  call `$file->path()`. This is shorthand for `Storage::disk($fileDisk)->path($filePath)`. There is also additional
  functionality provided on top of what Laravel's filesystem package provides. For example, if you want to encrypt a
  file, simply execute `$file->encrypt()`. `TemporaryFile`s are the same as `File`s, except we will request they be
  deleted when the object instance leaves scope.
- [F] Added FileGenerators. See `\Ignite\Core\Contracts\Files\FileGenerator`. This was mainly to provide a generic
  interface that FileTransmitter could use as a source. If you have a maatwebsite/excel Export, you can use
  the `\Ignite\Core\Files\CsvFileGenerator` to generate that export. FileGenerators save to a temporary file by default.
- [F] Added the ability to specify an import directory & disk. Added so we can reference this in one place. Actually
  changing these will probably be very rare. See config.php's import array.
- [X] Fixed a bug where displaying a user's history would blow up if it had a "restored" event.

# 3.0.26
- [XB] config/datatables-buttons.php parameters.buttons was basically being ignored so we can not configure all buttons
  for all reports. It is fixed now so it does use that configuration to merge in Ignite\Core\Models\Grid\AbstractTable.
- [X] Removed distinct() from some queries because it may be hiding some bugs. Your query should return distinct values
  anyway and not need distinct() on most occasions. But if there are duplicate values or rows, then the distinct() could
  be hiding duplicate rows in the database or a bad join, and we need to see those instead not knowing of their
  existence.
- [X] Fix DefaultStragey when creating or updating users/participants so it uses the User or Participant binded by the
  program (like Program\Entities\User) instead of just using the one from Core. So you can get a return class type based
  off the binding instead of having to manually replace the object with your new object.

# 3.0.25
- [X] Removed fast-excel from the UserTable and the TaxReport.

# 3.0.24
- [X] When generating a filename for the next import, we're now making sure there isn't a file with that name already.
  This means if we create two import records within the same second, we can now generate unique file names for those two
  imports. For the Traveler's MyClick (Bond) project, we need to run 8 imports to migrate the data from the previous
  system. I created a command, so we can run this predictably to test setting up the site and to set up the database
  locally & in beta. And, when we're ready to go-live, we can run this same command as part of bootstrapping the site.
  This command creates the 8 imports in the import table and copies the import files for those import records. It then
  queues up a chain of jobs that will run the imports in sequence. So, this is kinda not really a bug since the
  likelihood of two imports being uploaded at the same time is very likely. But, these changes are more of a bug fix
  than an enhancement, so I went with bug fix on this one.

# 3.0.23
- [F] Added the ability to create an import file based on a path (`\Ignite\Core\Models\Import\File::fromPath()`). We
  previously had the ability to create an import file from a Request. This does the same thing, but instead of asking
  for a request, it asks for a file path, an import type and optionally, a disk.
- [F] Added the ability to create an Import entity based on an import file (`\Ignite\Core\Models\Import\File`). Prior to
  this, we needed to pass in an array of the data for the import. This new method (`ImportRepository->createFromFile()`)
  gets the majority of the information we need from the file. You can optionally pass a created_by user. I wanted to
  make the `ImportRepository-->create()` method take either an array or a File instance, but I also didn't want to
  change its signature in order to allow for the created by user to be passed in.
- [F] Added a `findAdminUser()` method to the `UserRepository`. This fetches the user with
  the `config('core.admin.email')`, email address.
- [F] When creating an Import entity, we use the admin user's ID (see previous bullet) if we don't have a logged-in
  user. We were using ID of `1` in this case. We're still falling back to using `1` if we can't find an admin user,
  though maybe we should remove that now.

# 3.0.21
- [X] There was an issue fetching the message out of log lines that had a `{` or `[` in them. The changes here allow for
  any characters in log messages when formatting them. In order to enjoy these changes, you will need to update
  the `config/core.php` file's `core.import.reader.pattern` regex. If this setting was set specifically for your
  program, make sure to only apply this change if you know it won't affect how your program currently works.

# 3.0.20
- [B] The config core.seeder.path now has a default of 'modules/Program/database/data' instead of null.
- [F] Several new composer scripts are available, including: php-security-checker, dos2unix, php-lint,
  php-code-beautify, php-code-style, and php-copy-paste-detector.
- [F] The seeders now can be extended and provide a different json file name, like for your own module. So instead of
  copy-paste the whole class to provide an additional json file (and not a replacement), you can just extend the
  appropriate seeder and provide a different name with the $file property. If you actually wanted a replacement json
  using the same seeder class, see config('core.seeder.path').

# 3.0.14
- [F] You may want to add POSTMARK_MESSAGE_STREAM_ID to your .env file.
- [B] If you are already handling postmark message stream in a different way in existing applications, you may want to
  switch over to the new environment variable, and use the EventServiceProvider in Core instead of handling it in your
  program's PrograServiceProvider or EventServiceProvider.