feat(purchase): add checkout expiration settings and reservation status to purchase items

- Introduced a new configuration file for purchase settings, including checkout and payment expiration times.
- Added a migration to include a `reservation_status` column in the `compra_items` table, defaulting to 'active' and updating existing records based on purchase status.
- Created a migration to add an `expires_at` timestamp to the `compras` table, updating it for existing records based on their status.
- Scoped unique indexes in the `carritos` table to only active carts, adding virtual columns for active user and guest tokens.
- Implemented a console command to expire overdue purchases and release stock reservations, scheduled to run every minute.
- Added tests for Google token exchange and login functionality, ensuring proper cart handling and user authentication.
- Updated purchase-related tests to reflect changes in item handling and customer data validation.
This commit is contained in:
2026-07-27 12:49:27 -03:00
parent c37b8894e4
commit 8b26a2b63e
30 changed files with 1738 additions and 396 deletions

View File

@@ -0,0 +1,29 @@
<?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::table('compra_items', function (Blueprint $table): void {
$table->string('reservation_status')
->default('active')
->after('total');
});
DB::table('compra_items')
->whereIn('compra_id', DB::table('compras')->where('status', 'paid')->select('id'))
->update(['reservation_status' => 'committed']);
}
public function down(): void
{
Schema::table('compra_items', function (Blueprint $table): void {
$table->dropColumn('reservation_status');
});
}
};

View File

@@ -0,0 +1,32 @@
<?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::table('compras', function (Blueprint $table): void {
$table->timestamp('expires_at')->nullable()->index()->after('payment_method');
});
DB::table('compras')
->whereIn('status', ['created', 'pending_payment'])
->whereNull('expires_at')
->update([
'expires_at' => now()->addMinutes(
max(1, (int) config('purchase.checkout_expiration_minutes', 30)),
),
]);
}
public function down(): void
{
Schema::table('compras', function (Blueprint $table): void {
$table->dropColumn('expires_at');
});
}
};

View File

@@ -0,0 +1,115 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
// MySQL may use one of the old composite unique indexes to support the
// tenant foreign key. Give that foreign key its own index before the
// unique indexes are removed.
Schema::whenTableDoesntHaveIndex(
'carritos',
'carritos_tenant_codigo_index',
fn (Blueprint $table) => $table->index(
'tenant_codigo',
'carritos_tenant_codigo_index',
),
);
Schema::whenTableHasIndex(
'carritos',
'carritos_tenant_codigo_user_id_unique',
fn (Blueprint $table) => $table->dropUnique(
'carritos_tenant_codigo_user_id_unique',
),
);
Schema::whenTableHasIndex(
'carritos',
'carritos_tenant_codigo_guest_token_unique',
fn (Blueprint $table) => $table->dropUnique(
'carritos_tenant_codigo_guest_token_unique',
),
);
Schema::whenTableDoesntHaveColumn('carritos', 'active_user_id', function (Blueprint $table): void {
$table->unsignedBigInteger('active_user_id')
->virtualAs("CASE WHEN status = 'active' AND deleted_at IS NULL THEN user_id ELSE NULL END");
});
Schema::whenTableDoesntHaveColumn('carritos', 'active_guest_token', function (Blueprint $table): void {
$table->string('active_guest_token')
->virtualAs("CASE WHEN status = 'active' AND deleted_at IS NULL THEN guest_token ELSE NULL END");
});
Schema::whenTableDoesntHaveIndex(
'carritos',
'carritos_tenant_active_user_unique',
fn (Blueprint $table) => $table->unique(
['tenant_codigo', 'active_user_id'],
'carritos_tenant_active_user_unique',
),
);
Schema::whenTableDoesntHaveIndex(
'carritos',
'carritos_tenant_active_guest_unique',
fn (Blueprint $table) => $table->unique(
['tenant_codigo', 'active_guest_token'],
'carritos_tenant_active_guest_unique',
),
);
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::whenTableHasIndex(
'carritos',
'carritos_tenant_active_user_unique',
fn (Blueprint $table) => $table->dropUnique(
'carritos_tenant_active_user_unique',
),
);
Schema::whenTableHasIndex(
'carritos',
'carritos_tenant_active_guest_unique',
fn (Blueprint $table) => $table->dropUnique(
'carritos_tenant_active_guest_unique',
),
);
Schema::whenTableHasColumn(
'carritos',
'active_user_id',
fn (Blueprint $table) => $table->dropColumn('active_user_id'),
);
Schema::whenTableHasColumn(
'carritos',
'active_guest_token',
fn (Blueprint $table) => $table->dropColumn('active_guest_token'),
);
Schema::whenTableDoesntHaveIndex(
'carritos',
'carritos_tenant_codigo_user_id_unique',
fn (Blueprint $table) => $table->unique(['tenant_codigo', 'user_id']),
);
Schema::whenTableDoesntHaveIndex(
'carritos',
'carritos_tenant_codigo_guest_token_unique',
fn (Blueprint $table) => $table->unique(['tenant_codigo', 'guest_token']),
);
Schema::whenTableHasIndex(
'carritos',
'carritos_tenant_codigo_index',
fn (Blueprint $table) => $table->dropIndex('carritos_tenant_codigo_index'),
);
}
};