Skip to content

Commit f267354

Browse files
[6.x] Database Orders: Store status log in its own table (#983)
* Store status log events in their own database table * Fix querying orders by date * Refactor order date accessor * Fix styling * Re-work the "run update scripts" command * Publish status log migration upon upgrade * swap around the steps * Migrate status log events to new table * Ensure gateway data is copied across properly when migrating orders to a database * Move status log logic until after the order model has been created --------- Co-authored-by: duncanmcclean <duncanmcclean@users.noreply.github.com>
1 parent 3802574 commit f267354

14 files changed

+335
-61
lines changed

docs/upgrade-guides/v5-x-to-v6-0.md

+16-8
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,14 @@ To get started with the upgrade process, follow the below steps:
2222
composer update doublethreedigital/simple-commerce --with-dependencies
2323
```
2424

25-
**3.** Next, manually run the update scripts. This step will make changes to your config files & order data.
25+
**3.** If you're storing your orders in the database, you should uninstall & re-install Runway. It has moved to the Rad Pack:
26+
27+
```
28+
composer remove doublethreedigital/runway
29+
composer require statamic-rad-pack/runway
30+
```
31+
32+
**4.** Next, manually run the update scripts. This step will make changes to your config files & order data.
2633

2734
```
2835
php please sc:run-update-scripts
@@ -32,13 +39,6 @@ php please sc:run-update-scripts
3239
If you have excluded your orders from Git or you're storing your orders in a database, you will need to re-run this command after deploying Simple Commerce v6.
3340
:::
3441

35-
**4.** If you're storing your orders in the database, you should uninstall & re-install Runway. It has moved to the Rad Pack:
36-
37-
```
38-
composer remove doublethreedigital/runway
39-
composer require statamic-rad-pack/runway
40-
```
41-
4242
**5.** You may also want to clear your route & view caches:
4343

4444
```
@@ -56,6 +56,14 @@ php artisan view:clear
5656

5757
If you're storing orders & customers in the database, you should also follow the [Runway v6 Upgrade Guide](https://runway.duncanmcclean.com/upgrade-guides/v5-x-to-v6-0).
5858

59+
### High: Database Migrations
60+
61+
If you're storing orders in the database, you will need to run the migrations, both locally & when deploying to any other environments:
62+
63+
```
64+
php artisan migrate
65+
```
66+
5967
## High: References to gateways & shipping methods in orders have changed
6068

6169
Previously, when referencing a Payment Gateway or Shipping Method in an order, Simple Commerce would use its fully-qualified class name, like so:

src/Console/Commands/MigrateOrdersToDatabase.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,11 @@ protected function migrateOrders(string $collectionHandle): self
158158
}
159159

160160
if ($gateway = $entry->get('gateway')) {
161-
$order->gatewayData(gateway: $gateway);
161+
$order->gatewayData(
162+
gateway: $gateway['use'],
163+
data: $gateway['data'],
164+
refund: $gateway['refund'] ?? []
165+
);
162166
}
163167

164168
$order->data($data);

src/Console/Commands/RunUpdateScripts.php

+11-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace DoubleThreeDigital\SimpleCommerce\Console\Commands;
44

5-
use Facades\Statamic\UpdateScripts\Manager as UpdateScriptManager;
65
use Illuminate\Console\Command;
76
use Statamic\Console\RunsInPlease;
87

@@ -18,10 +17,16 @@ public function handle()
1817
{
1918
$this->info('Running update scripts...');
2019

21-
UpdateScriptManager::runUpdatesForSpecificPackageVersion(
22-
package: 'doublethreedigital/simple-commerce',
23-
oldVersion: '5.0.0',
24-
console: $this
25-
);
20+
// For some reason, the "proper" way of doing this didn't work so we're
21+
// doing it manually here.
22+
app('statamic.update-scripts')
23+
->filter(function (array $script) {
24+
return $script['package'] === 'doublethreedigital/simple-commerce';
25+
})
26+
->each(function (array $script) {
27+
$updateScript = new $script['class']($script['package'], $this);
28+
29+
$updateScript->update();
30+
});
2631
}
2732
}

src/Console/Commands/SwitchToDatabase.php

+4
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ protected function copyMigrationStubs(): self
6767
File::copy($this->stubsPath.'/create_orders_table.php', database_path('migrations/'.date('Y_m_d_His').'_create_orders_table.php'));
6868
}
6969

70+
if (count(File::glob(database_path('migrations').'/*_create_status_log_table.php')) < 1) {
71+
File::copy($this->stubsPath.'/create_status_log_table.php', database_path('migrations/'.date('Y_m_d_His').'_create_status_log_table.php'));
72+
}
73+
7074
return $this;
7175
}
7276

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
use DoubleThreeDigital\SimpleCommerce\Orders\OrderModel;
4+
use Illuminate\Database\Migrations\Migration;
5+
use Illuminate\Database\Schema\Blueprint;
6+
use Illuminate\Support\Arr;
7+
use Illuminate\Support\Facades\Schema;
8+
9+
class CreateStatusLogTable extends Migration
10+
{
11+
/**
12+
* Run the migrations.
13+
*
14+
* @return void
15+
*/
16+
public function up()
17+
{
18+
Schema::create('status_log', function (Blueprint $table) {
19+
$table->id();
20+
$table->string('order_id')->index();
21+
$table->string('status')->index();
22+
$table->timestamp('timestamp')->index();
23+
$table->json('data')->nullable();
24+
});
25+
26+
OrderModel::query()->chunkById(100, function ($orders) {
27+
$orders->each(function (OrderModel $order) {
28+
$statusLog = Arr::get($order->data, 'status_log', []);
29+
30+
foreach ($statusLog as $statusLogEvent) {
31+
$order->statusLog()->createOrFirst([
32+
'status' => $statusLogEvent['status'],
33+
'timestamp' => $statusLogEvent['timestamp'],
34+
'data' => $statusLogEvent['data'],
35+
]);
36+
}
37+
38+
$order->updateQuietly([
39+
'data' => collect($order->data)
40+
->reject(fn ($value, $key) => $key === 'status_log')
41+
->all(),
42+
]);
43+
});
44+
});
45+
}
46+
47+
/**
48+
* Reverse the migrations.
49+
*
50+
* @return void
51+
*/
52+
public function down()
53+
{
54+
Schema::dropIfExists('status_log');
55+
}
56+
}

src/Orders/EloquentOrderRepository.php

+23-1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ public function fromModel(OrderModel $model)
9595
})
9696
->toArray()
9797
)
98+
->merge([
99+
'status_log' => $model->statusLog()->get()->map(fn ($statusLog) => [
100+
'status' => $statusLog->status,
101+
'timestamp' => $statusLog->timestamp->timestamp,
102+
'data' => $statusLog->data ?? [],
103+
]),
104+
])
98105
);
99106

100107
if ($model->gateway) {
@@ -170,6 +177,14 @@ public function save($order): void
170177

171178
$model->save();
172179

180+
// Loop through status log events & create/update them in the database.
181+
$order->statusLog()->map(function (StatusLogEvent $statusLogEvent) use ($model) {
182+
StatusLogModel::updateOrCreate(
183+
['order_id' => $model->id, 'status' => $statusLogEvent->status, 'timestamp' => $statusLogEvent->date()],
184+
['data' => $statusLogEvent->data ?? []]
185+
);
186+
});
187+
173188
$order->id = $model->id;
174189
$order->orderNumber = $model->order_number;
175190
$order->status = OrderStatus::from($model->order_status);
@@ -208,7 +223,14 @@ public function save($order): void
208223
return [$columnName => $model->{$columnName}];
209224
})
210225
->toArray()
211-
);
226+
)
227+
->merge([
228+
'status_log' => $model->statusLog()->get()->map(fn ($statusLog) => [
229+
'status' => $statusLog->status,
230+
'timestamp' => $statusLog->timestamp->timestamp,
231+
'data' => $statusLog->data ?? [],
232+
]),
233+
]);
212234

213235
$order->resource = $model;
214236
}

src/Orders/EloquentQueryBuilder.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ public function wherePaymentStatus(PaymentStatus $paymentStatus)
3838

3939
public function whereStatusLogDate(OrderStatus|PaymentStatus $status, Carbon $date)
4040
{
41-
return $this->whereJsonContains('data->status_log', function ($query) use ($status, $date) {
41+
return $this->whereHas('statusLog', function ($query) use ($status, $date) {
4242
return $query
4343
->where('status', $status->value)
44-
->whereRaw('DATE(FROM_UNIXTIME(timestamp)) = ?', [$date->format('Y-m-d')]);
44+
->whereDate('timestamp', $date->format('Y-m-d'));
4545
});
4646
}
4747
}

src/Orders/OrderModel.php

+10-10
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33
namespace DoubleThreeDigital\SimpleCommerce\Orders;
44

55
use DoubleThreeDigital\SimpleCommerce\Customers\CustomerModel;
6-
use DoubleThreeDigital\SimpleCommerce\Exceptions\OrderNotFound;
7-
use DoubleThreeDigital\SimpleCommerce\Facades\Order as OrderFacade;
86
use Illuminate\Database\Eloquent\Casts\Attribute;
97
use Illuminate\Database\Eloquent\Factories\HasFactory;
108
use Illuminate\Database\Eloquent\Model;
119
use Illuminate\Database\Eloquent\Relations\BelongsTo;
12-
use Illuminate\Support\Carbon;
10+
use Illuminate\Database\Eloquent\Relations\HasMany;
1311
use StatamicRadPack\Runway\Traits\HasRunwayResource;
1412

1513
class OrderModel extends Model
@@ -38,17 +36,19 @@ public function customer(): BelongsTo
3836
return $this->belongsTo(CustomerModel::class);
3937
}
4038

39+
public function statusLog(): HasMany
40+
{
41+
return $this->hasMany(StatusLogModel::class, 'order_id');
42+
}
43+
4144
public function orderDate(): Attribute
4245
{
4346
return Attribute::make(
4447
get: function () {
45-
try {
46-
$order = OrderFacade::find($this->id);
47-
48-
return $order->statusLog()->where('status', OrderStatus::Placed)->map->date()->last();
49-
} catch (OrderNotFound $e) {
50-
return Carbon::now();
51-
}
48+
return $this->statusLog()
49+
->where('status', OrderStatus::Placed)
50+
->map->date()
51+
->last();
5252
},
5353
);
5454
}

src/Orders/StatusLogModel.php

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace DoubleThreeDigital\SimpleCommerce\Orders;
4+
5+
use Illuminate\Database\Eloquent\Factories\HasFactory;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
8+
9+
class StatusLogModel extends Model
10+
{
11+
use HasFactory;
12+
13+
protected $table = 'status_log';
14+
15+
public $timestamps = false;
16+
17+
protected $guarded = [];
18+
19+
protected $casts = [
20+
'order_id' => 'integer',
21+
'timestamp' => 'datetime',
22+
'data' => 'json',
23+
];
24+
25+
public function order(): BelongsTo
26+
{
27+
return $this->belongsTo(OrderModel::class, 'order_id');
28+
}
29+
}

src/ServiceProvider.php

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ class ServiceProvider extends AddonServiceProvider
113113
];
114114

115115
protected $updateScripts = [
116+
UpdateScripts\v6_0\PublishMigrations::class,
116117
UpdateScripts\v6_0\UpdateClassReferences::class,
117118
];
118119

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace DoubleThreeDigital\SimpleCommerce\UpdateScripts\v6_0;
4+
5+
use Illuminate\Support\Facades\File;
6+
use Statamic\UpdateScripts\UpdateScript;
7+
8+
class PublishMigrations extends UpdateScript
9+
{
10+
protected $stubsPath;
11+
12+
public function __construct()
13+
{
14+
$this->stubsPath = __DIR__.'/../../Console/Commands/stubs';
15+
}
16+
17+
public function shouldUpdate($newVersion, $oldVersion)
18+
{
19+
return $this->isUpdatingTo('6.0.0');
20+
}
21+
22+
public function update()
23+
{
24+
if ($this->isOrExtendsClass(SimpleCommerce::orderDriver()['repository'], EloquentOrderRepository::class)) {
25+
if (count(File::glob(database_path('migrations').'/*_create_status_log_table.php')) < 1) {
26+
File::copy($this->stubsPath.'/create_status_log_table.php', database_path('migrations/'.date('Y_m_d_His').'_create_status_log_table.php'));
27+
}
28+
}
29+
}
30+
31+
protected function isOrExtendsClass(string $class, string $classToCheckAgainst): bool
32+
{
33+
return is_subclass_of($class, $classToCheckAgainst)
34+
|| $class === $classToCheckAgainst;
35+
}
36+
}

tests/Helpers/UseDatabaseContentDrivers.php

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ public function setUpDatabaseContentDrivers()
1818
File::copy($this->stubsPath.'/create_orders_table.php', database_path('migrations/'.date('Y_m_d_His').'_create_orders_table.php'));
1919
}
2020

21+
if (count(File::glob(database_path('migrations').'/*_create_status_log_table.php')) < 1) {
22+
File::copy($this->stubsPath.'/create_status_log_table.php', database_path('migrations/'.date('Y_m_d_His').'_create_status_log_table.php'));
23+
}
24+
2125
$this->runLaravelMigrations();
2226

2327
$this->app['config']->set('simple-commerce.content.customers', [

0 commit comments

Comments
 (0)