Compare commits
41 Commits
10655b8c07
...
fix/simpli
| Author | SHA1 | Date | |
|---|---|---|---|
| 152d44cd0c | |||
| cd99a6489b | |||
| ebcbbb1774 | |||
| 3dd378ed06 | |||
| 4f705175bd | |||
| 82570fe941 | |||
| d2a2b323e0 | |||
| ec67623b7b | |||
| f6f138e180 | |||
| d612b7a118 | |||
| 49f42d4507 | |||
| 1881cc1d4b | |||
| 00ec37d8a0 | |||
| d9f374c718 | |||
| bbfdf8f342 | |||
| 8a0f29bdae | |||
| 198ed400a9 | |||
| 23d83166e6 | |||
| cb090402d0 | |||
| c120b6f7c0 | |||
| 1106fa28b4 | |||
| 896fb81bcf | |||
| 8545fbc645 | |||
| dd6dcb708a | |||
| 624b246ff5 | |||
| 007b95b904 | |||
| 0b3c8c93be | |||
| ede718e448 | |||
| e63a03601a | |||
| 389373ad8c | |||
| ace02a3133 | |||
| d69a6210f6 | |||
| 3c0b43fea3 | |||
| 9b0008626b | |||
| 61861e331a | |||
| 2cc9d7dd97 | |||
| f982bcead1 | |||
| 7a85e1731d | |||
| 6db99e775a | |||
| e5f7ba3615 | |||
| 24bfef431b |
@@ -5,6 +5,9 @@ APP_DEBUG=false
|
|||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
|
|
||||||
PURCHASE_CHECKOUT_EXPIRATION_MINUTES=30
|
PURCHASE_CHECKOUT_EXPIRATION_MINUTES=30
|
||||||
|
PURCHASE_QR_EXPIRATION_MINUTES=15
|
||||||
|
PURCHASE_TELEPAGOS_EXPIRATION_MINUTES=30
|
||||||
|
PURCHASE_TRANSFER_EXPIRATION_MINUTES=1440
|
||||||
STOCK_RESERVATION_EXPIRATION_MINUTES=30
|
STOCK_RESERVATION_EXPIRATION_MINUTES=30
|
||||||
FRONTEND_URLS=http://localhost:4200
|
FRONTEND_URLS=http://localhost:4200
|
||||||
|
|
||||||
|
|||||||
@@ -343,9 +343,9 @@ class StockReservationService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function clearExpirationForReview(Purchase $purchase): void
|
public function refreshForPurchase(Purchase $purchase, ?Carbon $expiresAt): void
|
||||||
{
|
{
|
||||||
DB::transaction(function () use ($purchase): void {
|
DB::transaction(function () use ($purchase, $expiresAt): void {
|
||||||
/** @var Purchase $purchase */
|
/** @var Purchase $purchase */
|
||||||
$purchase = Purchase::query()->lockForUpdate()->findOrFail($purchase->getKey());
|
$purchase = Purchase::query()->lockForUpdate()->findOrFail($purchase->getKey());
|
||||||
if ($purchase->stock_reservation_id === null) {
|
if ($purchase->stock_reservation_id === null) {
|
||||||
@@ -363,7 +363,7 @@ class StockReservationService
|
|||||||
throw new StockReservationExpiredException;
|
throw new StockReservationExpiredException;
|
||||||
}
|
}
|
||||||
|
|
||||||
$reservation->update(['expires_at' => null]);
|
$reservation->update(['expires_at' => $expiresAt]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ class PurchaseController extends Controller
|
|||||||
PaymentIntentRequest $request,
|
PaymentIntentRequest $request,
|
||||||
Tenant $tenant,
|
Tenant $tenant,
|
||||||
Purchase $compra,
|
Purchase $compra,
|
||||||
|
CheckoutService $checkoutService,
|
||||||
PurchaseStateGuard $purchaseState,
|
PurchaseStateGuard $purchaseState,
|
||||||
): JsonResponse {
|
): JsonResponse {
|
||||||
$compra = $this->resolveScopedPurchase($tenant, $request->user()->id, $compra);
|
$compra = $this->resolveScopedPurchase($tenant, $request->user()->id, $compra);
|
||||||
@@ -102,6 +103,7 @@ class PurchaseController extends Controller
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
$updated = DB::transaction(function () use (
|
$updated = DB::transaction(function () use (
|
||||||
|
$checkoutService,
|
||||||
$compra,
|
$compra,
|
||||||
$method,
|
$method,
|
||||||
$purchaseState,
|
$purchaseState,
|
||||||
@@ -140,6 +142,12 @@ class PurchaseController extends Controller
|
|||||||
$purchaseUpdate['transfer_payer_dni'] = $transferPayerDni;
|
$purchaseUpdate['transfer_payer_dni'] = $transferPayerDni;
|
||||||
}
|
}
|
||||||
$purchase->update($purchaseUpdate);
|
$purchase->update($purchaseUpdate);
|
||||||
|
$checkoutService->refreshReservationExpiration(
|
||||||
|
$purchase,
|
||||||
|
now()->addMinutes(
|
||||||
|
max(1, (int) config("purchase.payment_expiration_minutes.{$method}", 30)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ class PurchaseResource extends JsonResource
|
|||||||
*/
|
*/
|
||||||
public function toArray(Request $request): array
|
public function toArray(Request $request): array
|
||||||
{
|
{
|
||||||
$serverTime = now();
|
|
||||||
$expiresAt = $this->stockReservation?->expires_at;
|
|
||||||
$items = $this->resource->relationLoaded('items')
|
$items = $this->resource->relationLoaded('items')
|
||||||
? $this->resource->getRelation('items')
|
? $this->resource->getRelation('items')
|
||||||
: collect();
|
: collect();
|
||||||
@@ -50,11 +48,7 @@ class PurchaseResource extends JsonResource
|
|||||||
'created_at' => $this->created_at,
|
'created_at' => $this->created_at,
|
||||||
'status' => $this->status,
|
'status' => $this->status,
|
||||||
'payment_method' => $this->payment_method,
|
'payment_method' => $this->payment_method,
|
||||||
'expires_at' => $expiresAt,
|
'expires_at' => $this->stockReservation?->expires_at,
|
||||||
'expires_in_seconds' => $expiresAt === null
|
|
||||||
? null
|
|
||||||
: max(0, $expiresAt->getTimestamp() - $serverTime->getTimestamp()),
|
|
||||||
'server_time' => $serverTime,
|
|
||||||
'dni' => $this->dni,
|
'dni' => $this->dni,
|
||||||
'transfer_payer_dni' => $this->transfer_payer_dni,
|
'transfer_payer_dni' => $this->transfer_payer_dni,
|
||||||
'telefono' => $this->telefono,
|
'telefono' => $this->telefono,
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class CompleteCheckoutService
|
|||||||
$purchase->update([
|
$purchase->update([
|
||||||
'status' => Purchase::STATUS_IN_REVIEW,
|
'status' => Purchase::STATUS_IN_REVIEW,
|
||||||
]);
|
]);
|
||||||
$this->reservations->clearExpirationForReview($purchase);
|
$this->reservations->refreshForPurchase($purchase, null);
|
||||||
|
|
||||||
return $this->loadPurchase($purchase);
|
return $this->loadPurchase($purchase);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
namespace App\Domains\Purchase\Services;
|
namespace App\Domains\Purchase\Services;
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Services\StockReservationService;
|
||||||
use App\Domains\Purchase\Models\Purchase;
|
use App\Domains\Purchase\Models\Purchase;
|
||||||
use App\Domains\Purchase\Services\Checkout\CompleteCheckoutService;
|
use App\Domains\Purchase\Services\Checkout\CompleteCheckoutService;
|
||||||
use App\Domains\Purchase\Services\Checkout\EditCheckoutService;
|
use App\Domains\Purchase\Services\Checkout\EditCheckoutService;
|
||||||
use App\Domains\Purchase\Services\Checkout\ReleaseCheckoutService;
|
use App\Domains\Purchase\Services\Checkout\ReleaseCheckoutService;
|
||||||
use App\Domains\Purchase\Services\Checkout\StartCheckoutService;
|
use App\Domains\Purchase\Services\Checkout\StartCheckoutService;
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,6 +24,7 @@ class CheckoutService
|
|||||||
private readonly EditCheckoutService $editor,
|
private readonly EditCheckoutService $editor,
|
||||||
private readonly CompleteCheckoutService $completer,
|
private readonly CompleteCheckoutService $completer,
|
||||||
private readonly ReleaseCheckoutService $releaser,
|
private readonly ReleaseCheckoutService $releaser,
|
||||||
|
private readonly StockReservationService $reservations,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** @param array<string, mixed> $purchaseData */
|
/** @param array<string, mixed> $purchaseData */
|
||||||
@@ -75,4 +78,9 @@ class CheckoutService
|
|||||||
{
|
{
|
||||||
return $this->releaser->expire($purchase);
|
return $this->releaser->expire($purchase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function refreshReservationExpiration(Purchase $purchase, ?Carbon $expiresAt): void
|
||||||
|
{
|
||||||
|
$this->reservations->refreshForPurchase($purchase, $expiresAt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,10 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'checkout_expiration_minutes' => (int) env('PURCHASE_CHECKOUT_EXPIRATION_MINUTES', 30),
|
'checkout_expiration_minutes' => (int) env('PURCHASE_CHECKOUT_EXPIRATION_MINUTES', 30),
|
||||||
|
|
||||||
|
'payment_expiration_minutes' => [
|
||||||
|
'qr' => (int) env('PURCHASE_QR_EXPIRATION_MINUTES', 15),
|
||||||
|
'telepagos' => (int) env('PURCHASE_TELEPAGOS_EXPIRATION_MINUTES', 30),
|
||||||
|
'transfer' => (int) env('PURCHASE_TRANSFER_EXPIRATION_MINUTES', 1440),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ class TelepagosWebhookTest extends TestCase
|
|||||||
->assertJsonValidationErrors(['transfer_payer_dni']);
|
->assertJsonValidationErrors(['transfer_payer_dni']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_transfer_payment_intent_persists_data_without_extending_checkout_expiration(): void
|
public function test_transfer_payment_intent_persists_transfer_payer_dni_without_replacing_customer_dni(): void
|
||||||
{
|
{
|
||||||
config()->set('purchase.checkout_expiration_minutes', 30);
|
config()->set('purchase.payment_expiration_minutes.transfer', 60);
|
||||||
$now = now()->startOfSecond();
|
$now = now()->startOfSecond();
|
||||||
$this->travelTo($now);
|
$this->travelTo($now);
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ class TelepagosWebhookTest extends TestCase
|
|||||||
$this->assertDatabaseHas('stock_reservations', [
|
$this->assertDatabaseHas('stock_reservations', [
|
||||||
'id' => $purchase->stock_reservation_id,
|
'id' => $purchase->stock_reservation_id,
|
||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
'expires_at' => $now->copy()->addMinutes(30)->toDateTimeString(),
|
'expires_at' => $now->copy()->addMinutes(60)->toDateTimeString(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->travelBack();
|
$this->travelBack();
|
||||||
|
|||||||
@@ -796,18 +796,12 @@ class StorePurchaseTest extends TestCase
|
|||||||
|
|
||||||
public function test_it_updates_customer_data_for_a_pending_payment_purchase(): void
|
public function test_it_updates_customer_data_for_a_pending_payment_purchase(): void
|
||||||
{
|
{
|
||||||
config()->set('purchase.checkout_expiration_minutes', 30);
|
|
||||||
$now = now()->startOfSecond();
|
|
||||||
$this->travelTo($now);
|
|
||||||
|
|
||||||
$this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
|
$this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$variant = $this->createVariantForTenant('sonder', 10, '50.00');
|
$variant = $this->createVariantForTenant('sonder', 10, '50.00');
|
||||||
$purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 1);
|
$purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 1);
|
||||||
$purchase->update(['status' => Purchase::STATUS_PENDING_PAYMENT]);
|
$purchase->update(['status' => Purchase::STATUS_PENDING_PAYMENT]);
|
||||||
|
|
||||||
$this->travel(10)->minutes();
|
|
||||||
|
|
||||||
$this->actingAs($user, 'sanctum')
|
$this->actingAs($user, 'sanctum')
|
||||||
->patchJson("/api/tenants/sonder/compras/{$purchase->id}/customer-data", [
|
->patchJson("/api/tenants/sonder/compras/{$purchase->id}/customer-data", [
|
||||||
'dni' => '987654321',
|
'dni' => '987654321',
|
||||||
@@ -824,12 +818,6 @@ class StorePurchaseTest extends TestCase
|
|||||||
'status' => Purchase::STATUS_PENDING_PAYMENT,
|
'status' => Purchase::STATUS_PENDING_PAYMENT,
|
||||||
'dni' => '987654321',
|
'dni' => '987654321',
|
||||||
]);
|
]);
|
||||||
$this->assertDatabaseHas('stock_reservations', [
|
|
||||||
'id' => $purchase->stock_reservation_id,
|
|
||||||
'expires_at' => $now->copy()->addMinutes(30)->toDateTimeString(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->travelBack();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_checkout_items_are_immutable_and_editing_routes_are_unavailable(): void
|
public function test_checkout_items_are_immutable_and_editing_routes_are_unavailable(): void
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Unit\Purchase;
|
|
||||||
|
|
||||||
use App\Domains\Catalog\Models\StockReservation;
|
|
||||||
use App\Domains\Purchase\Models\Purchase;
|
|
||||||
use App\Domains\Purchase\Resources\PurchaseResource;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class PurchaseResourceTest extends TestCase
|
|
||||||
{
|
|
||||||
public function test_it_exposes_the_remaining_checkout_time_using_the_server_clock(): void
|
|
||||||
{
|
|
||||||
$now = now()->startOfSecond();
|
|
||||||
$this->travelTo($now);
|
|
||||||
$expiresAt = $now->copy()->addMinutes(12);
|
|
||||||
|
|
||||||
$resource = $this->resourceFor(Purchase::STATUS_PENDING_PAYMENT, $expiresAt);
|
|
||||||
|
|
||||||
$this->assertTrue($expiresAt->equalTo($resource['expires_at']));
|
|
||||||
$this->assertSame(720, $resource['expires_in_seconds']);
|
|
||||||
$this->assertTrue($now->equalTo($resource['server_time']));
|
|
||||||
|
|
||||||
$this->travelBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_clamps_an_overdue_checkout_to_zero_seconds(): void
|
|
||||||
{
|
|
||||||
$now = now()->startOfSecond();
|
|
||||||
$this->travelTo($now);
|
|
||||||
|
|
||||||
$resource = $this->resourceFor(
|
|
||||||
Purchase::STATUS_CREATED,
|
|
||||||
$now->copy()->subSecond(),
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertSame(0, $resource['expires_in_seconds']);
|
|
||||||
|
|
||||||
$this->travelBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_exposes_null_expiration_after_the_purchase_enters_review(): void
|
|
||||||
{
|
|
||||||
$resource = $this->resourceFor(
|
|
||||||
Purchase::STATUS_IN_REVIEW,
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->assertNull($resource['expires_at']);
|
|
||||||
$this->assertNull($resource['expires_in_seconds']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
|
||||||
private function resourceFor(string $status, ?Carbon $expiresAt): array
|
|
||||||
{
|
|
||||||
$purchase = (new Purchase)->forceFill([
|
|
||||||
'status' => $status,
|
|
||||||
'total' => '0.00',
|
|
||||||
]);
|
|
||||||
$purchase->setRelation('stockReservation', (new StockReservation)->forceFill([
|
|
||||||
'status' => StockReservation::STATUS_ACTIVE,
|
|
||||||
'expires_at' => $expiresAt,
|
|
||||||
]));
|
|
||||||
|
|
||||||
return (new PurchaseResource($purchase))->toArray(Request::create('/'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user