feat(refund): add support for event date suspension in cart invalidation process
This commit is contained in:
parent
df6892c662
commit
830f65c402
|
|
@ -15,9 +15,12 @@ class InvalidateEventDateCartsService
|
||||||
public function __construct(private readonly StockReservationService $reservations) {}
|
public function __construct(private readonly StockReservationService $reservations) {}
|
||||||
|
|
||||||
/** @param Collection<int, int> $eventDateIds */
|
/** @param Collection<int, int> $eventDateIds */
|
||||||
public function invalidate(Tenant $tenant, Collection $eventDateIds): void
|
public function invalidate(
|
||||||
{
|
Tenant $tenant,
|
||||||
DB::transaction(function () use ($tenant, $eventDateIds): void {
|
Collection $eventDateIds,
|
||||||
|
string $reason = StockReservationService::REASON_EVENT_DATE_RESCHEDULED,
|
||||||
|
): void {
|
||||||
|
DB::transaction(function () use ($tenant, $eventDateIds, $reason): void {
|
||||||
$carts = Cart::query()
|
$carts = Cart::query()
|
||||||
->where('tenant_codigo', $tenant->codigo)
|
->where('tenant_codigo', $tenant->codigo)
|
||||||
->where('status', Cart::STATUS_ACTIVE)
|
->where('status', Cart::STATUS_ACTIVE)
|
||||||
|
|
@ -44,7 +47,7 @@ class InvalidateEventDateCartsService
|
||||||
|
|
||||||
$this->reservations->releaseCurrentCartReservation(
|
$this->reservations->releaseCurrentCartReservation(
|
||||||
$cart,
|
$cart,
|
||||||
StockReservationService::REASON_EVENT_DATE_RESCHEDULED,
|
$reason,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reuse the existing expired-cart flow: stale mutations receive the
|
// Reuse the existing expired-cart flow: stale mutations receive the
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ class StockReservationService
|
||||||
|
|
||||||
public const REASON_EVENT_DATE_RESCHEDULED = 'event_date_rescheduled';
|
public const REASON_EVENT_DATE_RESCHEDULED = 'event_date_rescheduled';
|
||||||
|
|
||||||
|
public const REASON_EVENT_DATE_SUSPENDED = 'event_date_suspended';
|
||||||
|
|
||||||
public const REASON_PURCHASE_SUPERSEDED = 'purchase_superseded';
|
public const REASON_PURCHASE_SUPERSEDED = 'purchase_superseded';
|
||||||
|
|
||||||
public const REASON_PURCHASE_CANCELLED = 'purchase_cancelled';
|
public const REASON_PURCHASE_CANCELLED = 'purchase_cancelled';
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ namespace App\Domains\Event\Services;
|
||||||
use App\Domains\Auth\Models\User;
|
use App\Domains\Auth\Models\User;
|
||||||
use App\Domains\Cart\Services\InvalidateEventDateCartsService;
|
use App\Domains\Cart\Services\InvalidateEventDateCartsService;
|
||||||
use App\Domains\Catalog\Models\Variant;
|
use App\Domains\Catalog\Models\Variant;
|
||||||
|
use App\Domains\Catalog\Services\StockReservationService;
|
||||||
use App\Domains\Catalog\Services\VariantReplacementService;
|
use App\Domains\Catalog\Services\VariantReplacementService;
|
||||||
use App\Domains\Event\Enums\EventDateChangeType;
|
use App\Domains\Event\Enums\EventDateChangeType;
|
||||||
use App\Domains\Event\Events\EventDateRescheduled;
|
use App\Domains\Event\Events\EventDateRescheduled;
|
||||||
|
|
@ -177,10 +178,13 @@ class EventService
|
||||||
return $date->load('validityTime');
|
return $date->load('validityTime');
|
||||||
}
|
}
|
||||||
|
|
||||||
$purchaseTickets = $this->affectedPurchaseResolver->resolve(
|
$affectedDateIds = $this->affectedDateIds($tenant, $date);
|
||||||
|
$this->invalidateEventDateCarts->invalidate(
|
||||||
$tenant,
|
$tenant,
|
||||||
$this->affectedDateIds($tenant, $date),
|
$affectedDateIds,
|
||||||
|
StockReservationService::REASON_EVENT_DATE_SUSPENDED,
|
||||||
);
|
);
|
||||||
|
$purchaseTickets = $this->affectedPurchaseResolver->resolve($tenant, $affectedDateIds);
|
||||||
$date->update(['suspended_at' => now()]);
|
$date->update(['suspended_at' => now()]);
|
||||||
$this->variantReplacementService->disableForSuspension($date);
|
$this->variantReplacementService->disableForSuspension($date);
|
||||||
$this->disableTicketsWithoutUsableDates($tenant, $date);
|
$this->disableTicketsWithoutUsableDates($tenant, $date);
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ use Illuminate\Support\Facades\Event;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Laravel\Sanctum\Sanctum;
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class AdminAppEventControllerTest extends TestCase
|
class AdminAppEventControllerTest extends TestCase
|
||||||
|
|
@ -422,9 +423,18 @@ class AdminAppEventControllerTest extends TestCase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_rescheduling_invalidates_reserved_carts_before_replacing_variants(): void
|
public static function cartInvalidatingDateChanges(): array
|
||||||
{
|
{
|
||||||
Event::fake([EventDateRescheduled::class]);
|
return [
|
||||||
|
'reschedule' => ['reschedule', ['date' => '2027-10-20'], 'event_date_rescheduled'],
|
||||||
|
'suspend' => ['suspend', [], 'event_date_suspended'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('cartInvalidatingDateChanges')]
|
||||||
|
public function test_date_changes_invalidate_reserved_carts(string $action, array $payload, string $reason): void
|
||||||
|
{
|
||||||
|
Event::fake([EventDateRescheduled::class, EventDateSuspended::class]);
|
||||||
$tenant = $this->createActiveEvent($this->createTenant('acme'), 'Festival Acme');
|
$tenant = $this->createActiveEvent($this->createTenant('acme'), 'Festival Acme');
|
||||||
$source = $tenant->eventDates()->create([
|
$source = $tenant->eventDates()->create([
|
||||||
'date' => '2027-10-09', 'time_start' => '09:00', 'time_end' => '18:30',
|
'date' => '2027-10-09', 'time_start' => '09:00', 'time_end' => '18:30',
|
||||||
|
|
@ -449,13 +459,15 @@ class AdminAppEventControllerTest extends TestCase
|
||||||
]);
|
]);
|
||||||
Sanctum::actingAs($admin);
|
Sanctum::actingAs($admin);
|
||||||
|
|
||||||
$this->postJson("/api/v1/adminapp/tenant/event-dates/{$source->id}/reschedule", [
|
$this->postJson("/api/v1/adminapp/tenant/event-dates/{$source->id}/{$action}", $payload)->assertOk();
|
||||||
'date' => '2027-10-20',
|
|
||||||
])->assertOk();
|
|
||||||
|
|
||||||
$this->assertSame(Cart::STATUS_EXPIRED, $cart->fresh()->status);
|
$this->assertSame(Cart::STATUS_EXPIRED, $cart->fresh()->status);
|
||||||
$this->assertSame(StockReservation::STATUS_RELEASED, $reservation->fresh()->status);
|
$this->assertSame(StockReservation::STATUS_RELEASED, $reservation->fresh()->status);
|
||||||
$this->assertSame(0, $variant->fresh()->replacement->inventory->reserved_stock);
|
$this->assertSame($reason, $reservation->fresh()->release_reason);
|
||||||
|
$this->assertSame(0, $variant->fresh()->inventory->reserved_stock);
|
||||||
|
if ($action === 'reschedule') {
|
||||||
|
$this->assertSame(0, $variant->fresh()->replacement->inventory->reserved_stock);
|
||||||
|
}
|
||||||
$this->getJson('/api/tenants/acme/cart')->assertOk()->assertJsonCount(0, 'data.items');
|
$this->getJson('/api/tenants/acme/cart')->assertOk()->assertJsonCount(0, 'data.items');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ use App\Domains\Cart\Models\Cart;
|
||||||
use App\Domains\Cart\Services\InvalidateEventDateCartsService;
|
use App\Domains\Cart\Services\InvalidateEventDateCartsService;
|
||||||
use App\Domains\Catalog\Models\Inventory;
|
use App\Domains\Catalog\Models\Inventory;
|
||||||
use App\Domains\Catalog\Models\StockReservation;
|
use App\Domains\Catalog\Models\StockReservation;
|
||||||
|
use App\Domains\Catalog\Services\StockReservationService;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class InvalidateEventDateCartsServiceTest extends TestCase
|
class InvalidateEventDateCartsServiceTest extends TestCase
|
||||||
|
|
@ -82,7 +84,16 @@ class InvalidateEventDateCartsServiceTest extends TestCase
|
||||||
DB::table('variant_event_dates')->insert(['variant_id' => 3, 'event_date_id' => 1]);
|
DB::table('variant_event_dates')->insert(['variant_id' => 3, 'event_date_id' => 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_invalidates_whole_cart_and_releases_all_its_stock_only_once(): void
|
public static function releaseReasons(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[StockReservationService::REASON_EVENT_DATE_RESCHEDULED],
|
||||||
|
[StockReservationService::REASON_EVENT_DATE_SUSPENDED],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
#[DataProvider('releaseReasons')]
|
||||||
|
public function test_invalidates_whole_cart_and_releases_all_its_stock_only_once(string $reason): void
|
||||||
{
|
{
|
||||||
$cart = $this->cart(1);
|
$cart = $this->cart(1);
|
||||||
$otherInventory = DB::table('inventories')->insertGetId([]);
|
$otherInventory = DB::table('inventories')->insertGetId([]);
|
||||||
|
|
@ -93,8 +104,8 @@ class InvalidateEventDateCartsServiceTest extends TestCase
|
||||||
'quantity' => 2,
|
'quantity' => 2,
|
||||||
]);
|
]);
|
||||||
$reservationId = $cart->current_stock_reservation_id;
|
$reservationId = $cart->current_stock_reservation_id;
|
||||||
$this->invalidate();
|
$this->invalidate($reason);
|
||||||
$this->invalidate();
|
$this->invalidate($reason);
|
||||||
|
|
||||||
$this->assertSame(Cart::STATUS_EXPIRED, $cart->fresh()->status);
|
$this->assertSame(Cart::STATUS_EXPIRED, $cart->fresh()->status);
|
||||||
$this->assertNull($cart->fresh()->current_stock_reservation_id);
|
$this->assertNull($cart->fresh()->current_stock_reservation_id);
|
||||||
|
|
@ -102,7 +113,7 @@ class InvalidateEventDateCartsServiceTest extends TestCase
|
||||||
$this->assertSame(20, (int) Inventory::query()->sum('real_stock'));
|
$this->assertSame(20, (int) Inventory::query()->sum('real_stock'));
|
||||||
$this->assertDatabaseHas('stock_reservations', [
|
$this->assertDatabaseHas('stock_reservations', [
|
||||||
'id' => $reservationId, 'status' => StockReservation::STATUS_RELEASED,
|
'id' => $reservationId, 'status' => StockReservation::STATUS_RELEASED,
|
||||||
'release_reason' => 'event_date_rescheduled',
|
'release_reason' => $reason,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,9 +149,9 @@ class InvalidateEventDateCartsServiceTest extends TestCase
|
||||||
$this->assertSame(2, (int) Inventory::query()->sum('reserved_stock'));
|
$this->assertSame(2, (int) Inventory::query()->sum('reserved_stock'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function invalidate(): void
|
private function invalidate(string $reason = StockReservationService::REASON_EVENT_DATE_RESCHEDULED): void
|
||||||
{
|
{
|
||||||
app(InvalidateEventDateCartsService::class)->invalidate(new Tenant(['codigo' => 'acme']), collect([1]));
|
app(InvalidateEventDateCartsService::class)->invalidate(new Tenant(['codigo' => 'acme']), collect([1]), $reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function cart(int $variantId, string $tenantCode = 'acme'): Cart
|
private function cart(int $variantId, string $tenantCode = 'acme'): Cart
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue