Skip to content

Commit eefe500

Browse files
simonhampclaude
andauthored
Include product purchases in dashboard and purchase history (#278)
Product licenses (Plugin Dev Kit, Masterclass) were missing from the purchase history page and dashboard total count. Added product license query to PurchaseHistoryController, included count in dashboard totalPurchases, and added amber Product badge/icon to the view. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8f12e25 commit eefe500

4 files changed

Lines changed: 153 additions & 2 deletions

File tree

app/Http/Controllers/CustomerLicenseController.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ public function index(): View
6363
default => 'No accounts connected',
6464
};
6565

66-
// Total purchases (licenses + plugins)
67-
$totalPurchases = $licenseCount + $pluginLicenseCount;
66+
// Total purchases (licenses + plugins + products)
67+
$productLicenseCount = $user->productLicenses()->count();
68+
$totalPurchases = $licenseCount + $pluginLicenseCount + $productLicenseCount;
6869

6970
return view('customer.dashboard', compact(
7071
'licenseCount',

app/Http/Controllers/PurchaseHistoryController.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,28 @@ public function index(): View
6161
];
6262
});
6363

64+
// Fetch product licenses (e.g. Plugin Dev Kit, Masterclass)
65+
$productLicenses = $user->productLicenses()
66+
->with('product')
67+
->orderBy('purchased_at', 'desc')
68+
->get()
69+
->map(function ($productLicense) {
70+
return [
71+
'type' => 'product',
72+
'name' => $productLicense->product->name ?? 'Product',
73+
'description' => $productLicense->product->description ?? null,
74+
'price' => $productLicense->price_paid,
75+
'currency' => $productLicense->currency,
76+
'purchased_at' => $productLicense->purchased_at,
77+
'expires_at' => null,
78+
'is_active' => true,
79+
'href' => $productLicense->product ? route('products.show', $productLicense->product) : null,
80+
];
81+
});
82+
6483
// Combine and sort by purchased_at descending
6584
$purchases = $licenses->concat($pluginLicenses)
85+
->concat($productLicenses)
6686
->sortByDesc('purchased_at')
6787
->values();
6888

resources/views/customer/purchase-history/index.blade.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
<div class="grid size-10 place-items-center rounded-lg bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400">
4242
<x-heroicon-o-key class="size-5" />
4343
</div>
44+
@elseif($purchase['type'] === 'product')
45+
<div class="grid size-10 place-items-center rounded-lg bg-amber-100 text-amber-600 dark:bg-amber-900/30 dark:text-amber-400">
46+
<x-heroicon-o-shopping-bag class="size-5" />
47+
</div>
4448
@else
4549
<div class="grid size-10 place-items-center rounded-lg bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400">
4650
<x-heroicon-o-puzzle-piece class="size-5" />
@@ -63,6 +67,10 @@
6367
<span class="ml-2 inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900/30 dark:text-blue-400">
6468
License
6569
</span>
70+
@elseif($purchase['type'] === 'product')
71+
<span class="ml-2 inline-flex items-center rounded-full bg-amber-100 px-2.5 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-900/30 dark:text-amber-400">
72+
Product
73+
</span>
6674
@else
6775
<span class="ml-2 inline-flex items-center rounded-full bg-purple-100 px-2.5 py-0.5 text-xs font-medium text-purple-800 dark:bg-purple-900/30 dark:text-purple-400">
6876
Plugin
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
namespace Tests\Feature;
4+
5+
use App\Features\ShowAuthButtons;
6+
use App\Models\Product;
7+
use App\Models\ProductLicense;
8+
use App\Models\User;
9+
use Illuminate\Foundation\Testing\RefreshDatabase;
10+
use Laravel\Pennant\Feature;
11+
use Tests\TestCase;
12+
13+
class PurchaseHistoryTest extends TestCase
14+
{
15+
use RefreshDatabase;
16+
17+
protected function setUp(): void
18+
{
19+
parent::setUp();
20+
21+
Feature::define(ShowAuthButtons::class, true);
22+
}
23+
24+
public function test_guest_cannot_view_purchase_history(): void
25+
{
26+
$response = $this->get('/customer/purchase-history');
27+
28+
$response->assertRedirect('/login');
29+
}
30+
31+
public function test_purchase_history_shows_product_licenses(): void
32+
{
33+
$user = User::factory()->create();
34+
$product = Product::factory()->create(['name' => 'Plugin Dev Kit']);
35+
36+
ProductLicense::factory()->create([
37+
'user_id' => $user->id,
38+
'product_id' => $product->id,
39+
'price_paid' => 4900,
40+
'currency' => 'USD',
41+
'purchased_at' => now()->subDay(),
42+
]);
43+
44+
$response = $this->actingAs($user)->get('/customer/purchase-history');
45+
46+
$response->assertStatus(200);
47+
$response->assertSee('Plugin Dev Kit');
48+
$response->assertSee('Product');
49+
$response->assertSee('$49.00');
50+
}
51+
52+
public function test_purchase_history_shows_multiple_product_types(): void
53+
{
54+
$user = User::factory()->create();
55+
56+
$devKit = Product::factory()->create(['name' => 'Plugin Dev Kit']);
57+
$masterclass = Product::factory()->create(['name' => 'The NativePHP Masterclass']);
58+
59+
ProductLicense::factory()->create([
60+
'user_id' => $user->id,
61+
'product_id' => $devKit->id,
62+
'price_paid' => 4900,
63+
'purchased_at' => now()->subDays(2),
64+
]);
65+
66+
ProductLicense::factory()->create([
67+
'user_id' => $user->id,
68+
'product_id' => $masterclass->id,
69+
'price_paid' => 10100,
70+
'purchased_at' => now()->subDay(),
71+
]);
72+
73+
$response = $this->actingAs($user)->get('/customer/purchase-history');
74+
75+
$response->assertStatus(200);
76+
$response->assertSee('Plugin Dev Kit');
77+
$response->assertSee('The NativePHP Masterclass');
78+
}
79+
80+
public function test_dashboard_total_purchases_includes_product_licenses(): void
81+
{
82+
$user = User::factory()->create();
83+
$product = Product::factory()->create();
84+
85+
ProductLicense::factory()->create([
86+
'user_id' => $user->id,
87+
'product_id' => $product->id,
88+
]);
89+
90+
$response = $this->actingAs($user)->get('/dashboard');
91+
92+
$response->assertStatus(200);
93+
$response->assertViewHas('totalPurchases', 1);
94+
}
95+
96+
public function test_purchase_history_shows_empty_state_when_no_purchases(): void
97+
{
98+
$user = User::factory()->create();
99+
100+
$response = $this->actingAs($user)->get('/customer/purchase-history');
101+
102+
$response->assertStatus(200);
103+
$response->assertSee('No purchases yet');
104+
}
105+
106+
public function test_product_purchases_show_as_active(): void
107+
{
108+
$user = User::factory()->create();
109+
$product = Product::factory()->create(['name' => 'Plugin Dev Kit']);
110+
111+
ProductLicense::factory()->create([
112+
'user_id' => $user->id,
113+
'product_id' => $product->id,
114+
'purchased_at' => now(),
115+
]);
116+
117+
$response = $this->actingAs($user)->get('/customer/purchase-history');
118+
119+
$response->assertStatus(200);
120+
$response->assertSee('Active');
121+
}
122+
}

0 commit comments

Comments
 (0)