Squashed commit of the following:
commitcd99a6489bMerge:ebcbbb1e63a036Author: ncoronel <ncoronel@quo.ar> Date: Wed Aug 26 10:12:44 2026 -0300 Merge branch 'fix/simplify_stock_reservations' of https://gitea.quo.ar/tbianchini/shopit-back into fix/simplify_stock_reservations commitebcbbb1774Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 16:33:39 2026 -0300 fix(cart): reconcile expired cart mutations commit3dd378ed06Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 16:10:15 2026 -0300 docs(stock): describe associated entity expiration commit4f705175bdAuthor: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 16:10:14 2026 -0300 test(stock): cover propagated reservation expiration commit82570fe941Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 16:09:52 2026 -0300 refactor(stock): propagate reservation expiration commitd2a2b323e0Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:58:47 2026 -0300 docs(stock): document terminal reservation recovery commitec67623b7bAuthor: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:58:47 2026 -0300 test(stock): cover terminal reservation lifecycle commitf6f138e180Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:58:41 2026 -0300 refactor(stock): make expired reservations terminal commitd612b7a118Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:28:30 2026 -0300 docs(stock): describe unified expiration ownership commit49f42d4507Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:28:23 2026 -0300 test(stock): cover authoritative reservation expiration commit1881cc1d4bAuthor: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:28:17 2026 -0300 refactor(stock): make reservation expiration authoritative commit00ec37d8a0Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:05:34 2026 -0300 docs(stock): document centralized reservation flow commitd9f374c718Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:05:29 2026 -0300 test(stock): cover aggregate reservation lifecycle commitbbfdf8f342Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:05:23 2026 -0300 refactor(stock): centralize reservation aggregate commit8a0f29bdaeAuthor: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 16:33:39 2026 -0300 fix(cart): reconcile expired cart mutations commit198ed400a9Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 16:10:15 2026 -0300 docs(stock): describe associated entity expiration commit23d83166e6Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 16:10:14 2026 -0300 test(stock): cover propagated reservation expiration commitcb090402d0Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 16:09:52 2026 -0300 refactor(stock): propagate reservation expiration commitc120b6f7c0Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:58:47 2026 -0300 docs(stock): document terminal reservation recovery commit1106fa28b4Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:58:47 2026 -0300 test(stock): cover terminal reservation lifecycle commit896fb81bcfAuthor: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:58:41 2026 -0300 refactor(stock): make expired reservations terminal commit8545fbc645Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:28:30 2026 -0300 docs(stock): describe unified expiration ownership commitdd6dcb708aAuthor: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:28:23 2026 -0300 test(stock): cover authoritative reservation expiration commit624b246ff5Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:28:17 2026 -0300 refactor(stock): make reservation expiration authoritative commit007b95b904Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:05:34 2026 -0300 docs(stock): document centralized reservation flow commit0b3c8c93beAuthor: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:05:29 2026 -0300 test(stock): cover aggregate reservation lifecycle commitede718e448Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:05:23 2026 -0300 refactor(stock): centralize reservation aggregate commite63a03601aAuthor: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 16:33:39 2026 -0300 fix(cart): reconcile expired cart mutations commit389373ad8cAuthor: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 16:10:15 2026 -0300 docs(stock): describe associated entity expiration commitace02a3133Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 16:10:14 2026 -0300 test(stock): cover propagated reservation expiration commitd69a6210f6Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 16:09:52 2026 -0300 refactor(stock): propagate reservation expiration commit3c0b43fea3Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:58:47 2026 -0300 docs(stock): document terminal reservation recovery commit9b0008626bAuthor: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:58:47 2026 -0300 test(stock): cover terminal reservation lifecycle commit61861e331aAuthor: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:58:41 2026 -0300 refactor(stock): make expired reservations terminal commit2cc9d7dd97Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:28:30 2026 -0300 docs(stock): describe unified expiration ownership commitf982bcead1Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:28:23 2026 -0300 test(stock): cover authoritative reservation expiration commit7a85e1731dAuthor: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:28:17 2026 -0300 refactor(stock): make reservation expiration authoritative commit6db99e775aAuthor: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:05:34 2026 -0300 docs(stock): document centralized reservation flow commite5f7ba3615Author: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:05:29 2026 -0300 test(stock): cover aggregate reservation lifecycle commit24bfef431bAuthor: ncoronel <ncoronel@quo.ar> Date: Tue Aug 25 15:05:23 2026 -0300 refactor(stock): centralize reservation aggregate
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::rename('stock_reservations', 'stock_reservation_lines');
|
||||
|
||||
Schema::create('stock_reservations', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('status')->default('active');
|
||||
$table->dateTime('expires_at')->nullable();
|
||||
$table->dateTime('committed_at')->nullable();
|
||||
$table->dateTime('released_at')->nullable();
|
||||
$table->dateTime('expired_at')->nullable();
|
||||
$table->string('release_reason')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['status', 'expires_at']);
|
||||
});
|
||||
|
||||
Schema::table('stock_reservation_lines', function (Blueprint $table): void {
|
||||
$table->foreignId('stock_reservation_id')->nullable()->after('id');
|
||||
$table->boolean('tracks_inventory')->default(true)->after('quantity');
|
||||
});
|
||||
|
||||
Schema::table('carritos', function (Blueprint $table): void {
|
||||
$table->foreignId('current_stock_reservation_id')->nullable()->after('current_purchase_id');
|
||||
});
|
||||
|
||||
Schema::table('compras', function (Blueprint $table): void {
|
||||
$table->foreignId('stock_reservation_id')->nullable()->after('cart_id');
|
||||
});
|
||||
|
||||
$cartIdsByItem = DB::table('carrito_items')->pluck('cart_id', 'id');
|
||||
$unlimitedInventoryIds = DB::table('catalog_items')
|
||||
->where('inventory_policy', 'unlimited')
|
||||
->whereNotNull('inventory_id')
|
||||
->pluck('inventory_id')
|
||||
->merge(
|
||||
DB::table('variantes')
|
||||
->join('catalog_items', 'catalog_items.id', '=', 'variantes.catalog_item_id')
|
||||
->where('catalog_items.inventory_policy', 'unlimited')
|
||||
->pluck('variantes.inventory_id'),
|
||||
)
|
||||
->map(fn ($id): int => (int) $id)
|
||||
->unique();
|
||||
$legacyRows = DB::table('stock_reservation_lines')->orderBy('id')->get();
|
||||
$groups = $legacyRows->groupBy(function (object $row) use ($cartIdsByItem): string {
|
||||
if ($row->purchase_id !== null) {
|
||||
return 'purchase:'.$row->purchase_id;
|
||||
}
|
||||
|
||||
$cartId = $row->cart_item_id === null ? null : $cartIdsByItem->get($row->cart_item_id);
|
||||
|
||||
return $cartId === null ? 'legacy:'.$row->id : 'cart:'.$cartId;
|
||||
});
|
||||
|
||||
foreach ($groups as $key => $rows) {
|
||||
$statuses = $rows->pluck('status');
|
||||
$status = $statuses->contains('active')
|
||||
? 'active'
|
||||
: ($statuses->contains('committed')
|
||||
? 'committed'
|
||||
: ($statuses->contains('expired') ? 'expired' : 'released'));
|
||||
$first = $rows->first();
|
||||
$reservationId = DB::table('stock_reservations')->insertGetId([
|
||||
'status' => $status,
|
||||
'expires_at' => $status === 'active' ? $rows->pluck('expires_at')->filter()->max() : null,
|
||||
'committed_at' => $status === 'committed' ? $rows->pluck('committed_at')->filter()->max() : null,
|
||||
'released_at' => $status === 'released' ? $rows->pluck('released_at')->filter()->max() : null,
|
||||
'expired_at' => $status === 'expired' ? $rows->pluck('released_at')->filter()->max() : null,
|
||||
'release_reason' => null,
|
||||
'created_at' => $first->created_at,
|
||||
'updated_at' => $rows->pluck('updated_at')->filter()->max() ?? $first->updated_at,
|
||||
]);
|
||||
|
||||
foreach ($rows->groupBy('inventory_id') as $inventoryRows) {
|
||||
$line = $inventoryRows->first();
|
||||
DB::table('stock_reservation_lines')->where('id', $line->id)->update([
|
||||
'stock_reservation_id' => $reservationId,
|
||||
'quantity' => $inventoryRows->sum('quantity'),
|
||||
'tracks_inventory' => ! $unlimitedInventoryIds->contains((int) $line->inventory_id),
|
||||
]);
|
||||
DB::table('stock_reservation_lines')
|
||||
->whereIn('id', $inventoryRows->pluck('id')->skip(1))
|
||||
->delete();
|
||||
}
|
||||
|
||||
if (str_starts_with($key, 'purchase:')) {
|
||||
$purchaseId = (int) substr($key, strlen('purchase:'));
|
||||
DB::table('compras')->where('id', $purchaseId)->update([
|
||||
'stock_reservation_id' => $reservationId,
|
||||
]);
|
||||
$cartId = DB::table('compras')->where('id', $purchaseId)->value('cart_id');
|
||||
$isCurrent = $cartId !== null
|
||||
&& (int) DB::table('carritos')->where('id', $cartId)->value('current_purchase_id') === $purchaseId;
|
||||
|
||||
if ($status === 'active' && $isCurrent) {
|
||||
DB::table('carritos')->where('id', $cartId)->update([
|
||||
'current_stock_reservation_id' => $reservationId,
|
||||
]);
|
||||
}
|
||||
} elseif (str_starts_with($key, 'cart:') && $status === 'active') {
|
||||
DB::table('carritos')->where('id', (int) substr($key, strlen('cart:')))->update([
|
||||
'current_stock_reservation_id' => $reservationId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (DB::getDriverName() !== 'sqlite') {
|
||||
Schema::table('stock_reservation_lines', function (Blueprint $table): void {
|
||||
$table->dropForeign('stock_reservations_cart_item_id_foreign');
|
||||
$table->dropForeign('stock_reservations_purchase_id_foreign');
|
||||
$table->dropUnique('stock_reservations_cart_item_id_inventory_id_unique');
|
||||
$table->dropIndex('stock_reservations_purchase_id_status_index');
|
||||
$table->dropIndex('stock_reservations_status_expires_at_index');
|
||||
});
|
||||
}
|
||||
|
||||
Schema::table('stock_reservation_lines', function (Blueprint $table): void {
|
||||
$table->unsignedBigInteger('stock_reservation_id')->nullable(false)->change();
|
||||
$table->dropColumn([
|
||||
'cart_item_id',
|
||||
'purchase_id',
|
||||
'status',
|
||||
'expires_at',
|
||||
'committed_at',
|
||||
'released_at',
|
||||
]);
|
||||
$table->foreign('stock_reservation_id', 'reservation_lines_reservation_fk')
|
||||
->references('id')
|
||||
->on('stock_reservations')
|
||||
->cascadeOnDelete();
|
||||
$table->unique(
|
||||
['stock_reservation_id', 'inventory_id'],
|
||||
'reservation_lines_reservation_inventory_unique',
|
||||
);
|
||||
});
|
||||
|
||||
Schema::table('carritos', function (Blueprint $table): void {
|
||||
$table->foreign('current_stock_reservation_id', 'carts_current_stock_reservation_fk')
|
||||
->references('id')
|
||||
->on('stock_reservations')
|
||||
->nullOnDelete();
|
||||
$table->unique('current_stock_reservation_id', 'carts_current_stock_reservation_unique');
|
||||
});
|
||||
|
||||
Schema::table('compras', function (Blueprint $table): void {
|
||||
$table->foreign('stock_reservation_id', 'purchases_stock_reservation_fk')
|
||||
->references('id')
|
||||
->on('stock_reservations')
|
||||
->nullOnDelete();
|
||||
$table->unique('stock_reservation_id', 'purchases_stock_reservation_unique');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('compras', function (Blueprint $table): void {
|
||||
$table->dropUnique('purchases_stock_reservation_unique');
|
||||
$table->dropForeign('purchases_stock_reservation_fk');
|
||||
$table->dropColumn('stock_reservation_id');
|
||||
});
|
||||
|
||||
Schema::table('carritos', function (Blueprint $table): void {
|
||||
$table->dropUnique('carts_current_stock_reservation_unique');
|
||||
$table->dropForeign('carts_current_stock_reservation_fk');
|
||||
$table->dropColumn('current_stock_reservation_id');
|
||||
});
|
||||
|
||||
Schema::dropIfExists('stock_reservation_lines');
|
||||
Schema::dropIfExists('stock_reservations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('compras')
|
||||
->whereNotNull('stock_reservation_id')
|
||||
->orderBy('id')
|
||||
->chunkById(500, function ($purchases): void {
|
||||
foreach ($purchases as $purchase) {
|
||||
DB::table('stock_reservations')
|
||||
->where('id', $purchase->stock_reservation_id)
|
||||
->where('status', 'active')
|
||||
->update(['expires_at' => $purchase->expires_at]);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('compras', function (Blueprint $table): void {
|
||||
$table->dropIndex(['expires_at']);
|
||||
$table->dropColumn('expires_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('compras', function (Blueprint $table): void {
|
||||
$table->timestamp('expires_at')->nullable()->index()->after('payment_method');
|
||||
});
|
||||
|
||||
DB::table('compras')
|
||||
->whereNotNull('stock_reservation_id')
|
||||
->orderBy('id')
|
||||
->chunkById(500, function ($purchases): void {
|
||||
foreach ($purchases as $purchase) {
|
||||
DB::table('compras')
|
||||
->where('id', $purchase->id)
|
||||
->update([
|
||||
'expires_at' => DB::table('stock_reservations')
|
||||
->where('id', $purchase->stock_reservation_id)
|
||||
->value('expires_at'),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$expiredReservationIds = fn ($query) => $query
|
||||
->select('id')
|
||||
->from('stock_reservations')
|
||||
->where('status', 'expired');
|
||||
|
||||
DB::table('compras')
|
||||
->whereIn('status', ['created', 'pending_payment'])
|
||||
->whereIn('stock_reservation_id', $expiredReservationIds)
|
||||
->update(['status' => 'expired']);
|
||||
|
||||
DB::table('carritos')
|
||||
->whereIn('status', ['active', 'checkout'])
|
||||
->whereIn('current_stock_reservation_id', $expiredReservationIds)
|
||||
->update(['status' => 'expired']);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Terminal business states cannot be reversed without inventing their prior state.
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user