Feature: Order Updates, Events, Order Flows, Shipment And COD support
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Splits Lunar's single flat `status` column into three independently
|
||||
* tracked axes — payment, fulfillment, return — so a payment refund and a
|
||||
* fulfillment dispatch stop racing to write the same field, and each axis
|
||||
* can be filtered/queried directly instead of overloading one string for
|
||||
* three unrelated concerns. See Modules\Core\Order\Enums\OrderPaymentStatus/
|
||||
* OrderFulfillmentStatus/OrderReturnStatus for the value vocabularies, and
|
||||
* Modules\Core\Order\Listeners\ApplyResolvedPaymentStatus and friends for
|
||||
* where these columns actually get written. `status` itself is left in
|
||||
* place, unchanged — Lunar core still reads/writes it in places this
|
||||
* package doesn't own — but nothing in this package's business logic keys
|
||||
* off it anymore after this migration's consumers land.
|
||||
*
|
||||
* lunar_customers already has a direct precedent for a boboko-core
|
||||
* migration altering a Lunar-owned table (see
|
||||
* 2026_07_02_000002_drop_otp_from_lunar_customers_table.php) — this is not
|
||||
* a new pattern for this codebase, just the first time it's applied to
|
||||
* lunar_orders.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('lunar_orders', function (Blueprint $table) {
|
||||
$table->string('payment_status')->default('awaiting_payment')->after('status')->index();
|
||||
$table->string('fulfillment_status')->default('unfulfilled')->after('payment_status')->index();
|
||||
$table->string('return_status')->default('none')->after('fulfillment_status')->index();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('lunar_orders', function (Blueprint $table) {
|
||||
$table->dropColumn(['payment_status', 'fulfillment_status', 'return_status']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Append-only audit trail for Order's three status axes (see
|
||||
* 2026_09_11_000001_add_status_axes_to_orders_table.php) — the thing
|
||||
* `Lunar\Models\Order::getDefaultLogExcept()` explicitly denies (`status`
|
||||
* is excluded from Lunar's own Spatie activity log), so this is a
|
||||
* from-scratch mechanism, not a gap in an existing one.
|
||||
*
|
||||
* No `updated_at` — a row is never edited after it's written, only ever
|
||||
* inserted. `event_class` is the FQCN of whatever business event/action
|
||||
* caused the write (e.g. Modules\Core\Order\Events\OrderDispatched, or a
|
||||
* plain string like 'Modules\Core\Shipping\Extensions\OrderViewExtension::
|
||||
* markDispatchedAction' for a manual Filament action that has no backing
|
||||
* event class of its own) — see Modules\Core\Order\Services\
|
||||
* OrderStatusTransitionRecorder.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('order_status_transitions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('order_id')->constrained('lunar_orders')->cascadeOnDelete();
|
||||
$table->string('axis');
|
||||
$table->string('from_status')->nullable();
|
||||
$table->string('to_status');
|
||||
$table->string('event_class');
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->index(['order_id', 'axis']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('order_status_transitions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Lunar\Models\Order;
|
||||
use Modules\Core\Order\Enums\PaymentStatus;
|
||||
use Modules\Core\Order\Support\OrderStatus;
|
||||
|
||||
/**
|
||||
* Maps every existing order's flat `status` (as it stood before
|
||||
* 2026_09_11_000001_add_status_axes_to_orders_table.php) onto the new
|
||||
* payment_status/fulfillment_status/return_status columns. A separate
|
||||
* migration from the schema change so the schema migration stays simply
|
||||
* reversible via down(), and this data pass can be independently re-run.
|
||||
*
|
||||
* The flat status never captured refunds at all (no 'refunded' value was
|
||||
* ever added to config('lunar.orders.statuses')), so the table-driven
|
||||
* mapping below is corrected per-order by re-deriving
|
||||
* Modules\Core\Order\Support\OrderStatus::payment() — the existing,
|
||||
* unchanged derived-enum logic — and overriding payment_status to
|
||||
* refunded/partially_refunded wherever it disagrees with the flat-status
|
||||
* mapping. This is the one place the "keep the old derived enums" design
|
||||
* decision earns its keep: refund-fraction math isn't reimplemented here,
|
||||
* just reused.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
private const MAP = [
|
||||
'awaiting-payment' => ['payment_status' => 'awaiting_payment', 'fulfillment_status' => 'unfulfilled'],
|
||||
'payment-offline' => ['payment_status' => 'awaiting_payment', 'fulfillment_status' => 'unfulfilled'],
|
||||
'payment-received' => ['payment_status' => 'paid', 'fulfillment_status' => 'unfulfilled'],
|
||||
'ready-for-dispatch' => ['payment_status' => 'paid', 'fulfillment_status' => 'ready'],
|
||||
'ready-for-pickup' => ['payment_status' => 'paid', 'fulfillment_status' => 'ready'],
|
||||
'dispatched' => ['payment_status' => 'paid', 'fulfillment_status' => 'in_transit'],
|
||||
'completed' => ['payment_status' => 'paid', 'fulfillment_status' => 'completed'],
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Order::query()->with('transactions')->chunkById(200, function ($orders) {
|
||||
foreach ($orders as $order) {
|
||||
$mapped = self::MAP[$order->status] ?? null;
|
||||
|
||||
if ($mapped === null) {
|
||||
Log::warning('Order status axis backfill: unmapped status, leaving column defaults', [
|
||||
'order_id' => $order->id,
|
||||
'status' => $order->status,
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$paymentStatus = $mapped['payment_status'];
|
||||
|
||||
$derived = OrderStatus::payment($order);
|
||||
|
||||
if ($derived === PaymentStatus::Refunded) {
|
||||
$paymentStatus = 'refunded';
|
||||
} elseif ($derived === PaymentStatus::PartialRefund) {
|
||||
$paymentStatus = 'partially_refunded';
|
||||
}
|
||||
|
||||
DB::table('lunar_orders')->where('id', $order->id)->update([
|
||||
'payment_status' => $paymentStatus,
|
||||
'fulfillment_status' => $mapped['fulfillment_status'],
|
||||
'return_status' => 'none',
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Column defaults (set in the schema migration) are the correct
|
||||
// "undo" — no need to reverse-map back to the flat status, since
|
||||
// `status` itself was never touched by this migration.
|
||||
}
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* captured_status/authorized_status/refunded_status let a merchant pick
|
||||
* which per-method Order::status label a payment outcome resulted in — a
|
||||
* mechanism that only made sense while Order.status was the single field
|
||||
* carrying that meaning. Modules\Core\Order\Listeners\
|
||||
* ApplyResolvedPaymentStatus now writes a fixed 3-value payment_status
|
||||
* column instead (see 2026_09_11_000001_add_status_axes_to_orders_table.php);
|
||||
* there is no longer any per-method flexibility to preserve — "paid" is
|
||||
* "paid" regardless of which method captured it. Dropped rather than left
|
||||
* vestigial: keeping them visible in the admin would let a merchant
|
||||
* configure something that silently does nothing.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('payment_methods', function (Blueprint $table) {
|
||||
$table->dropColumn(['captured_status', 'authorized_status', 'refunded_status']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('payment_methods', function (Blueprint $table) {
|
||||
$table->string('captured_status')->nullable();
|
||||
$table->string('authorized_status')->nullable();
|
||||
$table->string('refunded_status')->nullable();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Order::paid/paid_at — entirely independent of the `status` column (see
|
||||
* Modules\Core\Order\Services\OrderStatusFlow's own docblock for why
|
||||
* payment timing, especially for cash-on-delivery, cannot be modeled as a
|
||||
* status-sequence step). `paid` is the fast-filter boolean; `paid_at` is
|
||||
* when it actually happened.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('lunar_orders', function (Blueprint $table) {
|
||||
$table->boolean('paid')->default(false)->after('status')->index();
|
||||
$table->timestamp('paid_at')->nullable()->after('paid');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('lunar_orders', function (Blueprint $table) {
|
||||
$table->dropColumn(['paid', 'paid_at']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Lunar\Models\Order;
|
||||
use Modules\Core\Order\Enums\PaymentStatus;
|
||||
use Modules\Core\Order\Support\OrderStatus;
|
||||
|
||||
/**
|
||||
* Collapses the 3-axis (payment_status/fulfillment_status/return_status)
|
||||
* model this session briefly built — abandoned before shipping — back
|
||||
* onto a single `status` column plus the new independent `paid`/`paid_at`
|
||||
* fields. Must run after 2026_09_12_000001 (adds paid/paid_at) and before
|
||||
* 2026_09_12_000003 (drops the axis columns this migration still reads).
|
||||
*
|
||||
* Priority rule: axis data where it's genuinely non-default (this order
|
||||
* was really moved through the axis system during this session's manual
|
||||
* testing); the legacy `status` column (which may still hold pre-session
|
||||
* hyphenated values) as fallback everywhere else.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
private const LEGACY_MAP = [
|
||||
'awaiting-payment' => 'awaiting_payment',
|
||||
'payment-offline' => 'awaiting_payment',
|
||||
'payment-received' => 'processing',
|
||||
'ready-for-dispatch' => 'ready_for_dispatch',
|
||||
'ready-for-pickup' => 'ready_for_pickup',
|
||||
'dispatched' => 'dispatched',
|
||||
'completed' => 'completed',
|
||||
];
|
||||
|
||||
/**
|
||||
* Axis fulfillment_status -> new single status, given branch. Axis
|
||||
* 'delivered' folds into 'return_window_open' (same combined-value
|
||||
* decision the going-forward design makes). Axis payment_status is
|
||||
* used only to decide whether a fully-unfulfilled order should read
|
||||
* as 'awaiting_payment' or 'processing'.
|
||||
*/
|
||||
private function mapFromAxes(string $payment, string $fulfillment, string $return, bool $isPickup): ?string
|
||||
{
|
||||
if ($return === 'returned') {
|
||||
return 'returned';
|
||||
}
|
||||
if ($return === 'requested') {
|
||||
return 'return_requested';
|
||||
}
|
||||
|
||||
return match ($fulfillment) {
|
||||
'unfulfilled' => $payment === 'paid' ? 'processing' : 'awaiting_payment',
|
||||
'processing' => 'processing',
|
||||
'ready' => $isPickup ? 'ready_for_pickup' : 'ready_for_dispatch',
|
||||
'in_transit' => 'dispatched',
|
||||
'delivered', 'return_window_open' => 'return_window_open',
|
||||
'picked_up' => 'picked_up',
|
||||
'completed' => 'completed',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
Order::query()->with('transactions')->chunkById(200, function ($orders) {
|
||||
foreach ($orders as $order) {
|
||||
$isPickup = $order->isStorePickupOrder();
|
||||
|
||||
$axisIsDefault = $order->payment_status === 'awaiting_payment'
|
||||
&& $order->fulfillment_status === 'unfulfilled'
|
||||
&& $order->return_status === 'none';
|
||||
|
||||
$status = $axisIsDefault
|
||||
? (self::LEGACY_MAP[$order->status] ?? null)
|
||||
: $this->mapFromAxes($order->payment_status, $order->fulfillment_status, $order->return_status, $isPickup);
|
||||
|
||||
if ($status === null) {
|
||||
Log::warning('Single-status backfill: unmapped order, defaulting to awaiting_payment', [
|
||||
'order_id' => $order->id,
|
||||
'status' => $order->status,
|
||||
'payment_status' => $order->payment_status,
|
||||
'fulfillment_status' => $order->fulfillment_status,
|
||||
'return_status' => $order->return_status,
|
||||
]);
|
||||
$status = 'awaiting_payment';
|
||||
}
|
||||
|
||||
$derived = OrderStatus::payment($order);
|
||||
$paid = $order->payment_status === 'paid'
|
||||
|| in_array($derived, [PaymentStatus::Captured, PaymentStatus::Refunded, PaymentStatus::PartialRefund], true);
|
||||
|
||||
// A refund implies the order concluded via a return —
|
||||
// even one backfilled to an early status (e.g. an order
|
||||
// refunded before fulfillment ever started) is corrected
|
||||
// to refunded/partially_refunded here, not left stuck
|
||||
// pre-fulfillment with no sign a refund ever happened.
|
||||
if ($derived === PaymentStatus::Refunded) {
|
||||
$status = 'refunded';
|
||||
} elseif ($derived === PaymentStatus::PartialRefund) {
|
||||
$status = 'partially_refunded';
|
||||
}
|
||||
|
||||
DB::table('lunar_orders')->where('id', $order->id)->update([
|
||||
'status' => $status,
|
||||
'paid' => $paid,
|
||||
'paid_at' => $paid ? ($order->placed_at ?? now()) : null,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// No reverse mapping — column defaults (post-rollback of the
|
||||
// schema migrations) are the correct "undo".
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Reverses 2026_09_11_000001_add_status_axes_to_orders_table.php — the
|
||||
* 3-axis model was abandoned before shipping in favor of a single
|
||||
* `status` column plus independent `paid`/`paid_at` (see
|
||||
* 2026_09_12_000001/000002). Must run after 2026_09_12_000002, which
|
||||
* still reads these columns for the backfill.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('lunar_orders', function (Blueprint $table) {
|
||||
$table->dropColumn(['payment_status', 'fulfillment_status', 'return_status']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Mirrors 2026_09_11_000001's own down() — restores columns
|
||||
// empty/defaulted, does not attempt to resurrect real per-order
|
||||
// values.
|
||||
Schema::table('lunar_orders', function (Blueprint $table) {
|
||||
$table->string('payment_status')->default('awaiting_payment')->after('paid_at')->index();
|
||||
$table->string('fulfillment_status')->default('unfulfilled')->after('payment_status')->index();
|
||||
$table->string('return_status')->default('none')->after('fulfillment_status')->index();
|
||||
});
|
||||
}
|
||||
};
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* There is only one status column left to audit (plus the synthetic
|
||||
* 'paid' entry — see Modules\Core\Order\Listeners\RecordStatusTransition),
|
||||
* so the `axis` column this table was created with
|
||||
* (2026_09_11_000002_create_order_status_transitions_table.php) no longer
|
||||
* means anything.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('order_status_transitions', function (Blueprint $table) {
|
||||
$table->dropIndex(['order_id', 'axis']);
|
||||
$table->dropColumn('axis');
|
||||
$table->index('order_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('order_status_transitions', function (Blueprint $table) {
|
||||
$table->dropIndex(['order_id']);
|
||||
$table->string('axis')->default('status')->after('order_id');
|
||||
$table->index(['order_id', 'axis']);
|
||||
});
|
||||
}
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* The seeded 'cash-on-delivery' PaymentMethod row
|
||||
* (Modules\Core\Command\InstallLunarCommand::seedPaymentMethods()) was
|
||||
* wired to driver => 'offline' — the same immediate-capture driver as
|
||||
* cash-in-hand. That's the bug that made COD "pay immediately" instead of
|
||||
* waiting for staff to confirm cash was actually received. Repoints
|
||||
* already-seeded environments to the new dedicated
|
||||
* Modules\Core\Payment\Drivers\CashOnDeliveryPaymentDriver; the seeder
|
||||
* itself is fixed separately for fresh installs.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('payment_methods')->where('type', 'cash-on-delivery')->update(['driver' => 'cash-on-delivery']);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('payment_methods')->where('type', 'cash-on-delivery')->update(['driver' => 'offline']);
|
||||
}
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 'return_window_open' is renamed to 'delivered' — same status value,
|
||||
* same meaning (the parcel arrived AND the return window is now open,
|
||||
* still one combined moment — see Modules\Core\Order\Listeners\
|
||||
* AdvanceFulfillmentOnDelivered), just a name a merchant expects to read
|
||||
* on the order page rather than an internal mechanic. Also renames it in
|
||||
* order_status_transitions' audit rows so the history stays consistent
|
||||
* with `status` going forward.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('lunar_orders')->where('status', 'return_window_open')->update(['status' => 'delivered']);
|
||||
DB::table('order_status_transitions')->where('from_status', 'return_window_open')->update(['from_status' => 'delivered']);
|
||||
DB::table('order_status_transitions')->where('to_status', 'return_window_open')->update(['to_status' => 'delivered']);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('lunar_orders')->where('status', 'delivered')->update(['status' => 'return_window_open']);
|
||||
DB::table('order_status_transitions')->where('from_status', 'delivered')->update(['from_status' => 'return_window_open']);
|
||||
DB::table('order_status_transitions')->where('to_status', 'delivered')->update(['to_status' => 'return_window_open']);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* A real record of "a manifest was issued", not just a loose
|
||||
* manifest_reference string stamped onto each Shipment row — ACS's own
|
||||
* ACS_Issue_Pickup_List call returns nothing beyond a PickupList_No (see
|
||||
* Modules\Core\Shipping\Carriers\Acs\AcsFulfillmentService::issueManifest()),
|
||||
* so this table is entirely our own bookkeeping: when the manifest was
|
||||
* issued and how many shipments it included, not something re-derivable
|
||||
* from the carrier later. `shipment_count` is denormalized (also
|
||||
* countable via shipments()->count()) purely so the manifests list can
|
||||
* render without an extra query per row.
|
||||
*
|
||||
* carrier-agnostic by design — see Modules\Core\Shipping\Contracts\
|
||||
* SupportsManifestBatching, the same contract any future carrier
|
||||
* (Speedex, etc.) implements to get manifest batching at all; this table
|
||||
* has no ACS-specific columns.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('manifests', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('carrier');
|
||||
$table->string('reference');
|
||||
$table->unsignedInteger('shipment_count')->default(0);
|
||||
$table->timestamp('issued_at');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['carrier', 'reference']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('manifests');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* Replaces the loose manifest_reference string with a real manifests
|
||||
* relation — see 2026_09_13_000002_create_manifests_table.php. Backfills
|
||||
* one Manifest row per distinct (carrier, manifest_reference) pair
|
||||
* already present in shipments, using the earliest label_printed_at (or
|
||||
* updated_at as a fallback) among that group as a best-effort issued_at,
|
||||
* since the exact original issue time was never recorded anywhere.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('shipments', function (Blueprint $table) {
|
||||
$table->foreignId('manifest_id')->nullable()->after('manifest_reference')->constrained()->nullOnDelete();
|
||||
});
|
||||
|
||||
$groups = DB::table('shipments')
|
||||
->select('carrier', 'manifest_reference')
|
||||
->whereNotNull('manifest_reference')
|
||||
->distinct()
|
||||
->get();
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$shipments = DB::table('shipments')
|
||||
->where('carrier', $group->carrier)
|
||||
->where('manifest_reference', $group->manifest_reference)
|
||||
->get();
|
||||
|
||||
$issuedAt = $shipments->pluck('label_printed_at')->filter()->min()
|
||||
?? $shipments->pluck('updated_at')->min();
|
||||
|
||||
$manifestId = DB::table('manifests')->insertGetId([
|
||||
'carrier' => $group->carrier,
|
||||
'reference' => $group->manifest_reference,
|
||||
'shipment_count' => $shipments->count(),
|
||||
'issued_at' => $issuedAt,
|
||||
'created_at' => $issuedAt,
|
||||
'updated_at' => $issuedAt,
|
||||
]);
|
||||
|
||||
DB::table('shipments')
|
||||
->where('carrier', $group->carrier)
|
||||
->where('manifest_reference', $group->manifest_reference)
|
||||
->update(['manifest_id' => $manifestId]);
|
||||
}
|
||||
|
||||
Schema::table('shipments', function (Blueprint $table) {
|
||||
$table->dropColumn('manifest_reference');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('shipments', function (Blueprint $table) {
|
||||
$table->string('manifest_reference')->nullable()->after('parent_reference');
|
||||
});
|
||||
|
||||
DB::table('shipments')
|
||||
->whereNotNull('manifest_id')
|
||||
->orderBy('id')
|
||||
->each(function ($shipment) {
|
||||
$manifest = DB::table('manifests')->find($shipment->manifest_id);
|
||||
|
||||
if ($manifest) {
|
||||
DB::table('shipments')->where('id', $shipment->id)->update([
|
||||
'manifest_reference' => $manifest->reference,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('shipments', function (Blueprint $table) {
|
||||
$table->dropConstrainedForeignId('manifest_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user