587 lines
18 KiB
PHP
587 lines
18 KiB
PHP
<?php
|
|
|
|
namespace Blax\Shop\Tests\Feature;
|
|
|
|
use Blax\Shop\Enums\CartStatus;
|
|
use Blax\Shop\Enums\OrderStatus;
|
|
use Blax\Shop\Enums\PurchaseStatus;
|
|
use Blax\Shop\Models\Cart;
|
|
use Blax\Shop\Models\Order;
|
|
use Blax\Shop\Models\OrderNote;
|
|
use Blax\Shop\Models\Product;
|
|
use Blax\Shop\Models\ProductPurchase;
|
|
use Blax\Shop\Tests\TestCase;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Workbench\App\Models\User;
|
|
|
|
class OrderCheckoutFlowTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
// =========================================================================
|
|
// CHECKOUT ORDER CREATION TESTS
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function checkout_creates_order_from_cart()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 50.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 2);
|
|
|
|
$cart = $user->checkoutCart();
|
|
|
|
$this->assertNotNull($cart->converted_at);
|
|
$this->assertNotNull($cart->order);
|
|
$this->assertInstanceOf(Order::class, $cart->order);
|
|
}
|
|
|
|
#[Test]
|
|
public function order_has_correct_cart_id()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 25.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
|
|
$this->assertEquals($cart->id, $order->cart_id);
|
|
}
|
|
|
|
#[Test]
|
|
public function order_has_correct_customer_info()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 25.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
|
|
$this->assertEquals(get_class($user), $order->customer_type);
|
|
$this->assertEquals($user->id, $order->customer_id);
|
|
$this->assertTrue($order->customer->is($user));
|
|
}
|
|
|
|
#[Test]
|
|
public function order_has_correct_currency()
|
|
{
|
|
$user = User::factory()->create();
|
|
$cart = Cart::factory()->forCustomer($user)->create([
|
|
'currency' => 'EUR',
|
|
]);
|
|
$product = Product::factory()->withPrices(unit_amount: 25.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$cart->addToCart($product, quantity: 1);
|
|
$cart->checkout();
|
|
|
|
$order = $cart->fresh()->order;
|
|
|
|
$this->assertEquals('EUR', $order->currency);
|
|
}
|
|
|
|
#[Test]
|
|
public function order_has_correct_total_amount()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product1 = Product::factory()->withPrices(unit_amount: 50.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
$product2 = Product::factory()->withPrices(unit_amount: 30.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product1, quantity: 2); // 100.00
|
|
$user->addToCart($product2, quantity: 3); // 90.00
|
|
|
|
$cart = $user->checkoutCart();
|
|
$order = $cart->order;
|
|
|
|
// Total should be 190.00 (19000 cents)
|
|
$this->assertEquals(19000, $order->amount_total);
|
|
$this->assertEquals(19000, $order->amount_subtotal);
|
|
}
|
|
|
|
#[Test]
|
|
public function order_starts_with_pending_status()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 25.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
|
|
$this->assertEquals(OrderStatus::PENDING, $order->status);
|
|
}
|
|
|
|
#[Test]
|
|
public function order_starts_with_zero_paid_amount()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 25.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
|
|
$this->assertEquals(0, $order->amount_paid);
|
|
$this->assertFalse($order->is_paid);
|
|
$this->assertFalse($order->is_fully_paid);
|
|
}
|
|
|
|
#[Test]
|
|
public function order_has_unique_order_number()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 25.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart1 = $user->checkoutCart();
|
|
$order1 = $cart1->order;
|
|
|
|
// Create another cart and checkout
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart2 = $user->checkoutCart();
|
|
$order2 = $cart2->order;
|
|
|
|
$this->assertNotEquals($order1->order_number, $order2->order_number);
|
|
}
|
|
|
|
#[Test]
|
|
public function order_creation_adds_system_note()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 25.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
|
|
$this->assertTrue(
|
|
$order->notes()
|
|
->where('type', OrderNote::TYPE_SYSTEM)
|
|
->where('content', 'like', '%created from cart%')
|
|
->exists()
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// ORDER PURCHASES RELATIONSHIP TESTS
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function order_has_purchases_through_cart()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product1 = Product::factory()->withPrices(unit_amount: 50.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
$product2 = Product::factory()->withPrices(unit_amount: 30.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product1, quantity: 2);
|
|
$user->addToCart($product2, quantity: 1);
|
|
|
|
$cart = $user->checkoutCart();
|
|
$order = $cart->order;
|
|
|
|
$this->assertCount(2, $order->directPurchases);
|
|
}
|
|
|
|
#[Test]
|
|
public function order_purchases_have_correct_status()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 50.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$purchase = $cart->purchases()->first();
|
|
|
|
$this->assertEquals(PurchaseStatus::UNPAID, $purchase->status);
|
|
}
|
|
|
|
// =========================================================================
|
|
// ORDER PAYMENT FLOW TESTS
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function order_payment_updates_status()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 100.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
$order->recordPayment(10000, 'pi_test123', 'card', 'stripe');
|
|
|
|
$order->refresh();
|
|
|
|
$this->assertEquals(10000, $order->amount_paid);
|
|
$this->assertEquals(OrderStatus::PROCESSING, $order->status);
|
|
$this->assertTrue($order->is_fully_paid);
|
|
$this->assertNotNull($order->paid_at);
|
|
}
|
|
|
|
#[Test]
|
|
public function order_payment_updates_purchase_status()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 100.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
$order->recordPayment(10000);
|
|
|
|
$purchase = $cart->purchases()->first();
|
|
|
|
$this->assertEquals(PurchaseStatus::COMPLETED, $purchase->status);
|
|
$this->assertEquals($purchase->amount, $purchase->amount_paid);
|
|
}
|
|
|
|
#[Test]
|
|
public function order_partial_payment_does_not_complete_order()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 100.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
$order->recordPayment(5000); // 50%
|
|
|
|
$order->refresh();
|
|
|
|
$this->assertEquals(5000, $order->amount_paid);
|
|
$this->assertEquals(5000, $order->amount_outstanding);
|
|
$this->assertFalse($order->is_fully_paid);
|
|
$this->assertNull($order->paid_at);
|
|
}
|
|
|
|
// =========================================================================
|
|
// ORDER STATUS WORKFLOW TESTS
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function order_can_be_processed_after_payment()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 100.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
$order->recordPayment(10000);
|
|
$order->markAsInPreparation();
|
|
|
|
$this->assertEquals(OrderStatus::IN_PREPARATION, $order->fresh()->status);
|
|
}
|
|
|
|
#[Test]
|
|
public function order_can_be_shipped_with_tracking()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 100.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
$order->recordPayment(10000);
|
|
$order->markAsShipped('TRACK123', 'FedEx');
|
|
|
|
$order->refresh();
|
|
|
|
$this->assertEquals(OrderStatus::SHIPPED, $order->status);
|
|
$this->assertEquals('TRACK123', $order->getMeta('tracking_number'));
|
|
$this->assertNotNull($order->shipped_at);
|
|
}
|
|
|
|
#[Test]
|
|
public function order_can_be_completed()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 100.00)->create([
|
|
'manage_stock' => false,
|
|
'virtual' => true, // Virtual product
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
$order->recordPayment(10000);
|
|
$order->markAsCompleted();
|
|
|
|
$order->refresh();
|
|
|
|
$this->assertEquals(OrderStatus::COMPLETED, $order->status);
|
|
$this->assertNotNull($order->completed_at);
|
|
}
|
|
|
|
// =========================================================================
|
|
// ORDER CANCELLATION TESTS
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function order_can_be_cancelled()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 100.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
$order->cancel('Customer changed their mind');
|
|
|
|
$order->refresh();
|
|
|
|
$this->assertEquals(OrderStatus::CANCELLED, $order->status);
|
|
$this->assertNotNull($order->cancelled_at);
|
|
|
|
// Verify the reason is logged
|
|
$this->assertTrue(
|
|
$order->notes()
|
|
->where('content', 'Customer changed their mind')
|
|
->exists()
|
|
);
|
|
}
|
|
|
|
// =========================================================================
|
|
// ORDER REFUND TESTS
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function order_can_be_refunded()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 100.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
$order->recordPayment(10000);
|
|
$order->recordRefund(10000, 'Full refund');
|
|
|
|
$order->refresh();
|
|
|
|
$this->assertEquals(OrderStatus::REFUNDED, $order->status);
|
|
$this->assertEquals(10000, $order->amount_refunded);
|
|
$this->assertNotNull($order->refunded_at);
|
|
}
|
|
|
|
#[Test]
|
|
public function order_partial_refund_does_not_change_status()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 100.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
$order->recordPayment(10000);
|
|
$order->recordRefund(3000, 'Partial refund');
|
|
|
|
$order->refresh();
|
|
|
|
$this->assertEquals(OrderStatus::PROCESSING, $order->status);
|
|
$this->assertEquals(3000, $order->amount_refunded);
|
|
}
|
|
|
|
// =========================================================================
|
|
// ORDER NOTES DURING LIFECYCLE TESTS
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function order_logs_status_changes()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 100.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
$order->recordPayment(10000);
|
|
$order->markAsShipped('TRACK123');
|
|
$order->markAsCompleted();
|
|
|
|
$statusNotes = $order->notes()
|
|
->where('type', OrderNote::TYPE_STATUS_CHANGE)
|
|
->get();
|
|
|
|
$this->assertGreaterThanOrEqual(3, $statusNotes->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function order_logs_payment_notes()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 100.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$order = $cart->order;
|
|
$order->recordPayment(5000);
|
|
$order->recordPayment(5000);
|
|
|
|
$paymentNotes = $order->notes()
|
|
->where('type', OrderNote::TYPE_PAYMENT)
|
|
->get();
|
|
|
|
$this->assertCount(2, $paymentNotes);
|
|
}
|
|
|
|
// =========================================================================
|
|
// CART STATUS UPDATE TESTS
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function cart_status_is_converted_after_checkout()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 100.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart = $user->checkoutCart();
|
|
|
|
$this->assertEquals(CartStatus::CONVERTED, $cart->status);
|
|
}
|
|
|
|
// =========================================================================
|
|
// MULTIPLE PRODUCTS CHECKOUT TESTS
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function checkout_handles_multiple_products_correctly()
|
|
{
|
|
$user = User::factory()->create();
|
|
|
|
$products = Product::factory()
|
|
->withPrices(unit_amount: 25.00)
|
|
->count(5)
|
|
->create(['manage_stock' => false]);
|
|
|
|
foreach ($products as $product) {
|
|
$user->addToCart($product, quantity: 2);
|
|
}
|
|
|
|
$cart = $user->checkoutCart();
|
|
$order = $cart->order;
|
|
|
|
$this->assertCount(5, $order->directPurchases);
|
|
$this->assertEquals(25000, $order->amount_total); // 5 products * 2 qty * 25.00 = 250.00
|
|
}
|
|
|
|
// =========================================================================
|
|
// ORDER QUERY TESTS
|
|
// =========================================================================
|
|
|
|
#[Test]
|
|
public function can_find_orders_for_customer()
|
|
{
|
|
$user1 = User::factory()->create();
|
|
$user2 = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 25.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
// Create 3 orders for user1
|
|
for ($i = 0; $i < 3; $i++) {
|
|
$user1->addToCart($product, quantity: 1);
|
|
$user1->checkoutCart();
|
|
}
|
|
|
|
// Create 2 orders for user2
|
|
for ($i = 0; $i < 2; $i++) {
|
|
$user2->addToCart($product, quantity: 1);
|
|
$user2->checkoutCart();
|
|
}
|
|
|
|
$this->assertCount(3, Order::forCustomer($user1)->get());
|
|
$this->assertCount(2, Order::forCustomer($user2)->get());
|
|
}
|
|
|
|
#[Test]
|
|
public function can_find_paid_orders()
|
|
{
|
|
$user = User::factory()->create();
|
|
$product = Product::factory()->withPrices(unit_amount: 50.00)->create([
|
|
'manage_stock' => false,
|
|
]);
|
|
|
|
// Create and pay one order
|
|
$user->addToCart($product, quantity: 1);
|
|
$cart1 = $user->checkoutCart();
|
|
$cart1->order->recordPayment(5000);
|
|
|
|
// Create unpaid order
|
|
$user->addToCart($product, quantity: 1);
|
|
$user->checkoutCart();
|
|
|
|
$this->assertCount(1, Order::paid()->get());
|
|
$this->assertCount(1, Order::unpaid()->get());
|
|
}
|
|
}
|