Skip to content

Commit bfef01e

Browse files
authored
Add pay wall (#3)
* Added Laravel Cashier and started implementing a paywall. * Ran pint and fixed some issues that were found. * Added timezone to the created_at attribute. * Added the ability to checkout via stripe. * Added some methods to the user model to help with billing. Added some pages to display to the user when they need to upgrade. * Made some small improvements. * Opened up registration.
1 parent 361d3e8 commit bfef01e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1757
-677
lines changed

app/Http/Controllers/Auth/RegisteredUserController.php

+2-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Illuminate\Auth\Events\Registered;
99
use Illuminate\Http\RedirectResponse;
1010
use Illuminate\Http\Request;
11+
use Illuminate\Support\Facades\App;
1112
use Illuminate\Support\Facades\Auth;
1213
use Illuminate\Support\Facades\Hash;
1314
use Illuminate\Validation\Rules;
@@ -21,9 +22,7 @@ class RegisteredUserController extends Controller
2122
*/
2223
public function create(): Response
2324
{
24-
return Inertia::render('Auth/Register', [
25-
'canRegister' => false,
26-
]);
25+
return Inertia::render('Auth/Register');
2726
}
2827

2928
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Billing;
4+
5+
use App\Http\Controllers\Controller;
6+
use Illuminate\Http\Request;
7+
8+
class BillingPortalController extends Controller
9+
{
10+
public function __invoke(Request $request)
11+
{
12+
return $request->user()->redirectToBillingPortal(route('dashboard'));
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Billing;
4+
5+
use App\Http\Controllers\Controller;
6+
use Inertia\Inertia;
7+
8+
class QuantityExceededController extends Controller
9+
{
10+
public function __invoke()
11+
{
12+
return Inertia::render('Teachers/Billing/QuantityExceeded');
13+
}
14+
}

app/Http/Controllers/DashboardController.php

+1-3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ public function __invoke(PieChartService $pieChartService)
1313
{
1414
return Inertia::render('Dashboard', [
1515
'totalQuestions' => (int) PromptQuestion::whereHas('promptAnswer')->count(),
16-
'dailyQuestions' => (int) PromptQuestion::filterByDate(now()->toDateString())
17-
->whereHas('promptAnswer')
18-
->count(),
16+
'dailyQuestions' => (int) PromptQuestion::whereHas('promptAnswer')->count(),
1917
'pieChartData' => $pieChartService
2018
->data(PromptAnswer::class, 'subject_category')
2119
->labels('subject_category')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use Illuminate\Http\Request;
6+
7+
class StripeCheckoutController extends Controller
8+
{
9+
public function __invoke(Request $request)
10+
{
11+
if ($request->plan === 'monthly') {
12+
$price = config('app.stripe.prices.monthly');
13+
} else {
14+
$price = config('app.stripe.prices.annual');
15+
}
16+
17+
return $request->user()
18+
->newSubscription('default', $price)
19+
->quantity($request->student_count)
20+
->allowPromotionCodes()
21+
->checkout([
22+
'success_url' => route('dashboard'),
23+
'cancel_url' => route('subscription.checkout.options'),
24+
]);
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use Illuminate\Http\Request;
6+
use Inertia\Inertia;
7+
use Inertia\Response;
8+
9+
class StripeCheckoutOptionsController extends Controller
10+
{
11+
public function __invoke(Request $request): Response
12+
{
13+
return Inertia::render('StripeCheckout/StripeCheckoutOptions');
14+
}
15+
}

app/Http/Controllers/Student/Auth/AuthenticatedSessionController.php

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public function create(): Response
2222
return Inertia::render('Auth/Student/Login', [
2323
'canResetPassword' => Route::has('password.request'),
2424
'status' => session('status'),
25+
'email' => request()->input('email') ?? null,
2526
]);
2627
}
2728

app/Http/Controllers/Student/Auth/PasswordResetController.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Http\Controllers\Student\Auth;
44

55
use App\Http\Controllers\Controller;
6+
use Illuminate\Http\RedirectResponse;
67
use Illuminate\Http\Request;
78
use Illuminate\Support\Facades\Hash;
89
use Inertia\Inertia;
@@ -15,7 +16,7 @@ public function create(): Response
1516
return Inertia::render('Student/PasswordReset');
1617
}
1718

18-
public function store(Request $request): string
19+
public function store(Request $request): RedirectResponse
1920
{
2021
$request->validate([
2122
'password' => ['required', 'confirmed', 'min:8'],

app/Http/Controllers/Student/PromptController.php

+10-2
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,18 @@
66
use App\Http\Requests\PromptRequest;
77
use App\Models\Prompt;
88
use App\Services\OpenAIService;
9+
use Illuminate\Http\Request;
910
use Inertia\Inertia;
1011
use Inertia\Response;
1112
use OpenAI\Laravel\Facades\OpenAI;
1213

1314
class PromptController extends Controller
1415
{
15-
public function index(): Response
16+
public function index(Request $request): Response
1617
{
17-
return Inertia::render('Student/Prompts/Index');
18+
return Inertia::render('Student/Prompts/Index', [
19+
'canAskQuestions' => $request->user()->canAskQuestions(),
20+
]);
1821
}
1922

2023
public function store(PromptRequest $request, OpenAIService $openAIService): Response
@@ -23,13 +26,16 @@ public function store(PromptRequest $request, OpenAIService $openAIService): Res
2326
'question' => $request->question,
2427
]);
2528

29+
$request->user()->user->increment('total_questions_asked');
30+
2631
$moderation = OpenAI::moderations()->create([
2732
'model' => 'text-moderation-latest',
2833
'input' => $request->question,
2934
]);
3035

3136
if ($moderation->results[0]->flagged === true) {
3237
return Inertia::render('Student/Prompts/Index', [
38+
'canAskQuestions' => $request->user()->canAskQuestions(),
3339
'result' => [
3440
'flagged' => true,
3541
'message' => 'This question violates OpenAI\'s policies. Please try another question.',
@@ -46,11 +52,13 @@ public function store(PromptRequest $request, OpenAIService $openAIService): Res
4652

4753
if (isset($response['flagged']) && $response['flagged'] === true) {
4854
return Inertia::render('Student/Prompts/Index', [
55+
'canAskQuestions' => $request->user()->canAskQuestions(),
4956
'result' => $response,
5057
]);
5158
}
5259

5360
return Inertia::render('Student/Prompts/Index', [
61+
'canAskQuestions' => $request->user()->canAskQuestions(),
5462
'result' => [
5563
'flagged' => false,
5664
'message' => '',

app/Http/Controllers/Student/TopicController.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ public function show(Request $request, string $topic): Response
2323
->with('promptAnswer')
2424
->orderBy('created_at', 'desc')
2525
->paginate(10)
26-
->through(function (PromptQuestion $promptQuestion) {
26+
->through(function (PromptQuestion $promptQuestion) use ($request) {
2727
return [
2828
'id' => $promptQuestion->id,
2929
'question' => $promptQuestion->question,
3030
'prompt_answer' => $promptQuestion->promptAnswer,
31-
'created_at' => $promptQuestion->created_at->toFormattedDateString(),
31+
'created_at' => $promptQuestion->created_at->timezone($request->user()->timezone)->toFormattedDateString(),
3232
];
3333
}),
3434
]);

app/Http/Controllers/StudentController.php

+11-2
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ public function index(Request $request): Response
3030
{
3131
return Inertia::render('Teachers/Students/Index', [
3232
'students' => $request->user()->students()->withCount(['promptQuestions' => function (Builder $query) {
33-
$query->whereHas('promptAnswer')->filterByDate(today()->toDateString());
34-
}])->paginate(10),
33+
$query->whereHas('promptAnswer');
34+
}])
35+
->paginate(10),
36+
'showInitialPaymentPage' => $request->user()->showInitialPaymentPage(),
37+
'showExceededQuantityPage' => $request->user()->showExceededQuantityPage(),
3538
]);
3639
}
3740

@@ -69,6 +72,8 @@ public function edit(Student $student): Response
6972

7073
public function show(Request $request, Student $student): Response
7174
{
75+
$this->authorize('view', $student);
76+
7277
return Inertia::render('Teachers/Students/Show', [
7378
'student' => (new StudentResource($student->load('promptQuestions')))->resolve(),
7479
'totalQuestions' => $this->studentService->student($student)->totalQuestionsAsked(),
@@ -80,13 +85,17 @@ public function show(Request $request, Student $student): Response
8085

8186
public function update(StudentStoreRequest $request, Student $student): RedirectResponse
8287
{
88+
$this->authorize('update', $student);
89+
8390
$student->update($request->validated());
8491

8592
return to_route('students.index');
8693
}
8794

8895
public function destroy(Student $student): RedirectResponse
8996
{
97+
$this->authorize('update', $student);
98+
9099
$student->delete();
91100

92101
return to_route('students.index');

app/Http/Middleware/VerifyCsrfToken.php

+1-6
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,7 @@
66

77
class VerifyCsrfToken extends Middleware
88
{
9-
/**
10-
* The URIs that should be excluded from CSRF verification.
11-
*
12-
* @var array<int, string>
13-
*/
149
protected $except = [
15-
//
10+
'stripe/*',
1611
];
1712
}

app/Models/PromptQuestion.php

+5-3
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ public function promptAnswer(): HasOne
3535
public function scopeFilterByDate(Builder $query, string $date): Builder
3636
{
3737
return $query->when($date, function (Builder $query) use ($date) {
38-
$startOfDayUtc = Carbon::parse($date)->startOfDay()->timezone(auth()->user()->timezone);
39-
$endOfDayUtc = Carbon::parse($date)->endOfDay()->timezone(auth()->user()->timezone);
38+
$usersTimezone = auth()->user()->timezone;
4039

41-
$query->whereBetween('created_at', [$startOfDayUtc, $endOfDayUtc]);
40+
$startOfDay = Carbon::parse($date)->timezone($usersTimezone)->startOfDay()->utc();
41+
$endOfDay = Carbon::parse($date)->timezone($usersTimezone)->endOfDay()->utc();
42+
43+
$query->whereBetween('created_at', [$startOfDay, $endOfDay]);
4244
});
4345
}
4446
}

app/Models/Student.php

+13
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,17 @@ protected function timezone(): Attribute
6161
get: fn (?string $value) => $value ?? config('app.timezone')
6262
);
6363
}
64+
65+
public function canAskQuestions(): bool
66+
{
67+
if ($this->user->subscribed()) {
68+
return true;
69+
}
70+
71+
if (! $this->user->subscribed() && $this->user->total_questions_asked < config('app.student_free_question_count')) {
72+
return true;
73+
}
74+
75+
return false;
76+
}
6477
}

app/Models/User.php

+23-3
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,25 @@
22

33
namespace App\Models;
44

5-
// use Illuminate\Contracts\Auth\MustVerifyEmail;
5+
use Illuminate\Contracts\Auth\MustVerifyEmail;
66
use Illuminate\Database\Eloquent\Casts\Attribute;
77
use Illuminate\Database\Eloquent\Factories\HasFactory;
88
use Illuminate\Database\Eloquent\Relations\HasMany;
99
use Illuminate\Foundation\Auth\User as Authenticatable;
1010
use Illuminate\Notifications\Notifiable;
11+
use Laravel\Cashier\Billable;
1112
use Laravel\Sanctum\HasApiTokens;
1213

13-
class User extends Authenticatable
14+
class User extends Authenticatable implements MustVerifyEmail
1415
{
15-
use HasApiTokens, HasFactory, Notifiable;
16+
use Billable, HasApiTokens, HasFactory, Notifiable;
1617

1718
protected $fillable = [
1819
'name',
1920
'email',
2021
'password',
2122
'timezone',
23+
'total_questions_asked',
2224
];
2325

2426
protected $hidden = [
@@ -28,6 +30,7 @@ class User extends Authenticatable
2830

2931
protected $casts = [
3032
'email_verified_at' => 'datetime',
33+
'total_questions_asked' => 'integer',
3134
];
3235

3336
public function students(): HasMany
@@ -41,4 +44,21 @@ protected function timezone(): Attribute
4144
get: fn (?string $value) => $value ?? config('app.timezone')
4245
);
4346
}
47+
48+
public function subscriptionQuantity(): int
49+
{
50+
$subscriptionItem = $this->subscription('default')->items->first();
51+
52+
return $subscriptionItem->quantity;
53+
}
54+
55+
public function showInitialPaymentPage(): bool
56+
{
57+
return ! $this->subscribed() && $this->students->count() >= 1;
58+
}
59+
60+
public function showExceededQuantityPage(): bool
61+
{
62+
return $this->subscribed() && $this->students->count() >= $this->subscriptionQuantity();
63+
}
4464
}

app/Observers/PromptQuestionObserver.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ public function creating(PromptQuestion $promptQuestion): void
1111
{
1212
$user = request()->user();
1313

14-
$lastQuestionDate = $user->promptQuestions()->latest()->first()->created_at;
14+
$lastQuestionDate = $user->promptQuestions()->latest()->first()?->created_at;
1515

16-
if ($lastQuestionDate->isSameDay(Carbon::today())) {
16+
if ($lastQuestionDate && $lastQuestionDate->isSameDay(Carbon::today())) {
1717
return;
1818
}
1919

20-
if ($lastQuestionDate->isSameDay(Carbon::yesterday())) {
20+
if ($lastQuestionDate && $lastQuestionDate->isSameDay(Carbon::yesterday())) {
2121
$user->increment('current_streak');
2222
} else {
2323
$user->current_streak = 1;

0 commit comments

Comments
 (0)