Skip to content

Commit b658a15

Browse files
committed
feat: E-commerce storefront additional tests + checkout coupon UI
- EcommerceStorefrontTest.php: 10 additional Pest tests for cart, coupon validation, review submission and admin moderation - Checkout.tsx: coupon code input with Apply button, discount line in summary https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent 29ab535 commit b658a15

2 files changed

Lines changed: 216 additions & 3 deletions

File tree

erp/resources/js/Pages/Ecommerce/Storefront/Checkout.tsx

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,48 @@ export default function StorefrontCheckout({ store, cartItems }: Props) {
215215
</div>
216216
))}
217217
</div>
218-
<div className="border-t border-gray-100 pt-3 flex justify-between font-semibold text-gray-900">
219-
<span>Total</span>
220-
<span>{store.currency_code} {subtotal.toFixed(2)}</span>
218+
<div className="space-y-1 border-t border-gray-100 pt-3 text-sm">
219+
<div className="flex justify-between text-gray-600">
220+
<span>Subtotal</span>
221+
<span>{store.currency_code} {subtotal.toFixed(2)}</span>
222+
</div>
223+
{couponResult?.valid && (
224+
<div className="flex justify-between text-green-600">
225+
<span>Coupon Discount</span>
226+
<span>-{store.currency_code} {discount.toFixed(2)}</span>
227+
</div>
228+
)}
229+
<div className="flex justify-between font-semibold text-gray-900 pt-1 border-t border-gray-100">
230+
<span>Total</span>
231+
<span>{store.currency_code} {total.toFixed(2)}</span>
232+
</div>
233+
</div>
234+
235+
{/* Coupon Code */}
236+
<div className="mt-4">
237+
<label className="block text-xs font-medium text-gray-600 mb-1">Coupon Code</label>
238+
<div className="flex gap-2">
239+
<input
240+
type="text"
241+
value={couponCode}
242+
onChange={e => setCouponCode(e.target.value)}
243+
placeholder="Enter code"
244+
className="flex-1 rounded border border-gray-300 px-2 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"
245+
/>
246+
<button
247+
type="button"
248+
onClick={applyCoupon}
249+
disabled={applyingCoupon || !couponCode}
250+
className="px-3 py-1.5 bg-gray-800 text-white text-sm rounded hover:bg-gray-700 disabled:opacity-50"
251+
>
252+
Apply
253+
</button>
254+
</div>
255+
{couponResult && (
256+
<p className={`mt-1 text-xs ${couponResult.valid ? 'text-green-600' : 'text-red-600'}`}>
257+
{couponResult.message}
258+
</p>
259+
)}
221260
</div>
222261
</div>
223262
</div>
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<?php
2+
3+
use App\Models\User;
4+
use App\Modules\Core\Models\Tenant;
5+
use App\Modules\Ecommerce\Models\StoreCoupon;
6+
use App\Modules\Ecommerce\Models\StoreProduct;
7+
use App\Modules\Ecommerce\Models\StoreReview;
8+
use App\Modules\Ecommerce\Models\StoreSettings;
9+
use App\Modules\Inventory\Models\Product;
10+
use Database\Seeders\RolePermissionSeeder;
11+
12+
beforeEach(function () {
13+
$this->seed(RolePermissionSeeder::class);
14+
$this->tenant = Tenant::create(['name' => 'Storefront Co', 'slug' => 'storefront-co-' . uniqid()]);
15+
$this->admin = User::factory()->create(['tenant_id' => $this->tenant->id]);
16+
$this->admin->assignRole('super-admin');
17+
$this->actingAs($this->admin);
18+
app()->instance('tenant', $this->tenant);
19+
20+
$this->store = StoreSettings::create([
21+
'tenant_id' => $this->tenant->id,
22+
'store_name' => 'Test Shop',
23+
'store_slug' => 'test-shop-' . uniqid(),
24+
'currency_code' => 'USD',
25+
'is_active' => true,
26+
]);
27+
28+
$inventoryProduct = Product::create([
29+
'tenant_id' => $this->tenant->id,
30+
'sku' => 'TEST-' . uniqid(),
31+
'name' => 'Test Product',
32+
'cost_price' => 10.00,
33+
'sale_price' => 20.00,
34+
'is_active' => true,
35+
'is_bundle' => false,
36+
]);
37+
38+
$this->storeProduct = StoreProduct::create([
39+
'tenant_id' => $this->tenant->id,
40+
'product_id' => $inventoryProduct->id,
41+
'store_price' => 25.00,
42+
'is_featured' => true,
43+
'is_visible' => true,
44+
'sort_order' => 0,
45+
]);
46+
});
47+
48+
test('views cart', function () {
49+
$this->get("/store/{$this->store->store_slug}/cart")
50+
->assertStatus(200);
51+
});
52+
53+
test('adds item to cart', function () {
54+
$this->post("/store/{$this->store->store_slug}/cart", [
55+
'store_product_id' => $this->storeProduct->id,
56+
'quantity' => 2,
57+
])->assertRedirect();
58+
59+
$this->assertDatabaseHas('store_carts', [
60+
'store_product_id' => $this->storeProduct->id,
61+
'quantity' => 2,
62+
]);
63+
});
64+
65+
test('updates cart quantity', function () {
66+
$this->post("/store/{$this->store->store_slug}/cart", [
67+
'store_product_id' => $this->storeProduct->id,
68+
'quantity' => 1,
69+
]);
70+
71+
$cartItem = \App\Modules\Ecommerce\Models\StoreCart::where('store_product_id', $this->storeProduct->id)->first();
72+
73+
$this->patch("/store/{$this->store->store_slug}/cart/{$cartItem->id}", [
74+
'quantity' => 5,
75+
])->assertRedirect();
76+
77+
expect($cartItem->fresh()->quantity)->toBe(5);
78+
});
79+
80+
test('removes cart item', function () {
81+
$this->post("/store/{$this->store->store_slug}/cart", [
82+
'store_product_id' => $this->storeProduct->id,
83+
'quantity' => 1,
84+
]);
85+
86+
$cartItem = \App\Modules\Ecommerce\Models\StoreCart::where('store_product_id', $this->storeProduct->id)->first();
87+
88+
$this->delete("/store/{$this->store->store_slug}/cart/{$cartItem->id}")
89+
->assertRedirect();
90+
91+
expect(\App\Modules\Ecommerce\Models\StoreCart::find($cartItem->id))->toBeNull();
92+
});
93+
94+
test('validates coupon and returns valid true', function () {
95+
StoreCoupon::create([
96+
'tenant_id' => $this->tenant->id,
97+
'code' => 'SAVE10',
98+
'type' => 'percentage',
99+
'value' => 10,
100+
'is_active' => true,
101+
'uses_count' => 0,
102+
]);
103+
104+
$this->postJson("/store/{$this->store->store_slug}/coupon/validate", [
105+
'code' => 'SAVE10',
106+
'subtotal' => 100,
107+
])->assertJson(['valid' => true]);
108+
});
109+
110+
test('rejects invalid coupon', function () {
111+
StoreCoupon::create([
112+
'tenant_id' => $this->tenant->id,
113+
'code' => 'EXPIRED',
114+
'type' => 'percentage',
115+
'value' => 10,
116+
'is_active' => true,
117+
'valid_until' => now()->subDay()->toDateString(),
118+
'uses_count' => 0,
119+
]);
120+
121+
$this->postJson("/store/{$this->store->store_slug}/coupon/validate", [
122+
'code' => 'EXPIRED',
123+
'subtotal' => 100,
124+
])->assertJson(['valid' => false]);
125+
});
126+
127+
test('submits a review with is_approved false', function () {
128+
$this->post("/store/{$this->store->store_slug}/products/{$this->storeProduct->id}/reviews", [
129+
'reviewer_name' => 'John Doe',
130+
'rating' => 5,
131+
'title' => 'Great product',
132+
'body' => 'Loved it!',
133+
])->assertRedirect();
134+
135+
$this->assertDatabaseHas('store_reviews', [
136+
'store_product_id' => $this->storeProduct->id,
137+
'reviewer_name' => 'John Doe',
138+
'rating' => 5,
139+
'is_approved' => false,
140+
]);
141+
});
142+
143+
test('lists admin reviews', function () {
144+
$this->get('/ecommerce/reviews')
145+
->assertStatus(200);
146+
});
147+
148+
test('approves a review', function () {
149+
$review = StoreReview::create([
150+
'tenant_id' => $this->tenant->id,
151+
'store_product_id' => $this->storeProduct->id,
152+
'reviewer_name' => 'Jane',
153+
'rating' => 4,
154+
'is_approved' => false,
155+
]);
156+
157+
$this->post("/ecommerce/reviews/{$review->id}/approve")
158+
->assertRedirect();
159+
160+
expect($review->fresh()->is_approved)->toBeTrue();
161+
});
162+
163+
test('creates a coupon', function () {
164+
$this->post('/ecommerce/coupons', [
165+
'code' => 'NEWCODE',
166+
'type' => 'fixed',
167+
'value' => 5.00,
168+
])->assertRedirect();
169+
170+
$this->assertDatabaseHas('store_coupons', [
171+
'tenant_id' => $this->tenant->id,
172+
'code' => 'NEWCODE',
173+
]);
174+
});

0 commit comments

Comments
 (0)