diff --git a/Dockerfile b/Dockerfile index 902cec7..c6d7f86 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,8 +14,7 @@ RUN install-php-extensions \ opcache \ zip \ gd \ - intl \ - bcmath + intl # Install Composer COPY --from=composer:2 /usr/bin/composer /usr/bin/composer diff --git a/Dockerfile.dev b/Dockerfile.dev index c36d143..f2a67c5 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -18,7 +18,6 @@ RUN install-php-extensions \ zip \ gd \ intl \ - bcmath \ xdebug # Install Composer diff --git a/app/Enums/AppModeEnum.php b/app/Enums/AppModeEnum.php deleted file mode 100644 index 9a75881..0000000 --- a/app/Enums/AppModeEnum.php +++ /dev/null @@ -1,24 +0,0 @@ -user(); - - if ($planner->subscribed()) { - return redirect()->route('dashboard'); - } - - $plan = $request->input('plan', 'monthly'); - $priceId = $plan === 'yearly' - ? config('services.stripe.price_yearly') - : config('services.stripe.price_monthly'); - - return $planner->newSubscription('default', $priceId) - ->checkout([ - 'success_url' => route('subscription.success') . '?session_id={CHECKOUT_SESSION_ID}', - 'cancel_url' => route('subscription.index'), - ]); - } - - public function success(Request $request): RedirectResponse - { - $sessionId = $request->query('session_id'); - - if ($sessionId) { - $planner = $request->user(); - $session = Cashier::stripe()->checkout->sessions->retrieve($sessionId, [ - 'expand' => ['subscription'], - ]); - - if ($session->subscription && ! $planner->subscribed()) { - $subscription = $session->subscription; - - $planner->subscriptions()->create([ - 'type' => 'default', - 'stripe_id' => $subscription->id, - 'stripe_status' => $subscription->status, - 'stripe_price' => $subscription->items->data[0]->price->id ?? null, - 'quantity' => $subscription->items->data[0]->quantity ?? 1, - 'trial_ends_at' => $subscription->trial_end ? now()->setTimestamp($subscription->trial_end) : null, - 'ends_at' => null, - ]); - } - } - - return redirect()->route('dashboard')->with('success', 'Subscription activated!'); - } - - public function billing(Request $request) - { - $planner = $request->user(); - $subscription = $planner->subscription(); - - if (! $subscription) { - return redirect()->route('subscription.index'); - } - - $planType = match ($subscription->stripe_price) { - config('services.stripe.price_yearly') => 'Yearly', - config('services.stripe.price_monthly') => 'Monthly', - default => 'Unknown', - }; - - $nextBillingDate = null; - if ($subscription->stripe_status === 'active') { - try { - $stripeSubscription = Cashier::stripe()->subscriptions->retrieve($subscription->stripe_id); - $nextBillingDate = $stripeSubscription->current_period_end - ? now()->setTimestamp($stripeSubscription->current_period_end) - : null; - } catch (\Exception $e) { - // Stripe API error - continue without next billing date - } - } - - return view('billing.index', [ - 'subscription' => $subscription, - 'planner' => $planner, - 'planType' => $planType, - 'nextBillingDate' => $nextBillingDate, - ]); - } - - public function cancel(Request $request): RedirectResponse - { - $planner = $request->user(); - - if (! $planner->subscribed()) { - return back()->with('error', 'No active subscription found.'); - } - - $planner->subscription()->cancel(); - - return back()->with('success', 'Subscription canceled. Access will continue until the end of your billing period.'); - } - - public function billingPortal(Request $request) - { - return $request->user()->redirectToBillingPortal(route('billing')); - } -} diff --git a/app/Http/Middleware/RequireSaasMode.php b/app/Http/Middleware/RequireSaasMode.php deleted file mode 100644 index 0950eb6..0000000 --- a/app/Http/Middleware/RequireSaasMode.php +++ /dev/null @@ -1,19 +0,0 @@ -user(); - - if (! $planner?->subscribed()) { - return redirect()->route('subscription.index'); - } - - return $next($request); - } -} diff --git a/app/Models/Planner.php b/app/Models/Planner.php index f8307fd..16614bb 100644 --- a/app/Models/Planner.php +++ b/app/Models/Planner.php @@ -6,7 +6,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -use Laravel\Cashier\Billable; use Laravel\Sanctum\HasApiTokens; /** @@ -16,7 +15,7 @@ */ class Planner extends Authenticatable { - use Billable, HasApiTokens, HasFactory, Notifiable; + use HasApiTokens, HasFactory, Notifiable; protected $fillable = [ 'name', 'email', 'password', diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 3067e8c..0bbbb1e 100755 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,12 +4,10 @@ use App\Exceptions\CustomException; use App\Models\Dish; -use App\Models\Planner; use App\Models\Schedule; use App\Models\ScheduledUserDish; use App\Models\User; use App\Models\UserDish; -use Laravel\Cashier\Cashier; use DishPlanner\Dish\Policies\DishPolicy; use DishPlanner\Schedule\Policies\SchedulePolicy; use DishPlanner\ScheduledUserDish\Policies\ScheduledUserDishPolicy; @@ -47,8 +45,6 @@ public function render($request, Throwable $e) public function boot(): void { - Cashier::useCustomerModel(Planner::class); - Gate::policy(Dish::class, DishPolicy::class); Gate::policy(Schedule::class, SchedulePolicy::class); Gate::policy(ScheduledUserDish::class, ScheduledUserDishPolicy::class); diff --git a/app/helpers.php b/app/helpers.php deleted file mode 100644 index 59bc43a..0000000 --- a/app/helpers.php +++ /dev/null @@ -1,17 +0,0 @@ -isApp(); - } -} - -if (! function_exists('is_mode_saas')) { - function is_mode_saas(): bool - { - return AppModeEnum::current()->isSaas(); - } -} \ No newline at end of file diff --git a/bootstrap/app.php b/bootstrap/app.php index 7fb61b3..c6a8eab 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,15 +1,17 @@ group(base_path('routes/web/subscription.php')); - }, ) ->withMiddleware(function (Middleware $middleware) { // Apply ForceJsonResponse only to API routes $middleware->api(ForceJsonResponse::class); - - $middleware->alias([ - 'subscription' => RequireSubscription::class, - 'saas' => RequireSaasMode::class, - ]); - - // Exclude Stripe webhook from CSRF verification - $middleware->validateCsrfTokens(except: [ - 'stripe/webhook', - ]); }) ->withExceptions(function (Exceptions $exceptions) { $exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) { @@ -77,4 +65,7 @@ } }); }) + ->withCommands([ + GenerateScheduleCommand::class, + ]) ->create(); diff --git a/composer.json b/composer.json index 2ecb17b..a4197fb 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,6 @@ "license": "MIT", "require": { "php": "^8.2", - "laravel/cashier": "^16.1", "laravel/framework": "^12.9.2", "laravel/sanctum": "^4.0", "laravel/tinker": "^2.9", @@ -27,9 +26,6 @@ "phpunit/phpunit": "^11.0.1" }, "autoload": { - "files": [ - "app/helpers.php" - ], "psr-4": { "App\\": "app/", "DishPlanner\\": "src/DishPlanner/", diff --git a/config/app.php b/config/app.php index 09dd952..df3f5f0 100644 --- a/config/app.php +++ b/config/app.php @@ -28,18 +28,6 @@ 'env' => env('APP_ENV', 'production'), - /* - |-------------------------------------------------------------------------- - | Application Mode - |-------------------------------------------------------------------------- - | - | Determines the application deployment mode: 'app' for self-hosted, - | 'saas' for multi-tenant SaaS, 'demo' for demonstration instances. - | - */ - - 'mode' => env('APP_MODE', 'app'), - /* |-------------------------------------------------------------------------- | Application Debug Mode diff --git a/config/services.php b/config/services.php index cf3ce61..27a3617 100644 --- a/config/services.php +++ b/config/services.php @@ -35,14 +35,4 @@ ], ], - 'stripe' => [ - 'key' => env('STRIPE_KEY'), - 'secret' => env('STRIPE_SECRET'), - 'webhook' => [ - 'secret' => env('STRIPE_WEBHOOK_SECRET'), - ], - 'price_monthly' => env('STRIPE_PRICE_MONTHLY'), - 'price_yearly' => env('STRIPE_PRICE_YEARLY'), - ], - ]; diff --git a/database/migrations/2026_01_06_000525_create_customer_columns.php b/database/migrations/2026_01_06_000525_create_customer_columns.php deleted file mode 100644 index 131d232..0000000 --- a/database/migrations/2026_01_06_000525_create_customer_columns.php +++ /dev/null @@ -1,34 +0,0 @@ -string('stripe_id')->nullable()->index(); - $table->string('pm_type')->nullable(); - $table->string('pm_last_four', 4)->nullable(); - $table->timestamp('trial_ends_at')->nullable(); - }); - } - - public function down(): void - { - Schema::table('planners', function (Blueprint $table) { - $table->dropIndex([ - 'stripe_id', - ]); - - $table->dropColumn([ - 'stripe_id', - 'pm_type', - 'pm_last_four', - 'trial_ends_at', - ]); - }); - } -}; diff --git a/database/migrations/2026_01_06_000526_create_subscriptions_table.php b/database/migrations/2026_01_06_000526_create_subscriptions_table.php deleted file mode 100644 index 9043296..0000000 --- a/database/migrations/2026_01_06_000526_create_subscriptions_table.php +++ /dev/null @@ -1,37 +0,0 @@ -id(); - $table->foreignId('planner_id'); - $table->string('type'); - $table->string('stripe_id')->unique(); - $table->string('stripe_status'); - $table->string('stripe_price')->nullable(); - $table->integer('quantity')->nullable(); - $table->timestamp('trial_ends_at')->nullable(); - $table->timestamp('ends_at')->nullable(); - $table->timestamps(); - - $table->index(['planner_id', 'stripe_status']); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('subscriptions'); - } -}; diff --git a/database/migrations/2026_01_06_000527_create_subscription_items_table.php b/database/migrations/2026_01_06_000527_create_subscription_items_table.php deleted file mode 100644 index 420e23f..0000000 --- a/database/migrations/2026_01_06_000527_create_subscription_items_table.php +++ /dev/null @@ -1,34 +0,0 @@ -id(); - $table->foreignId('subscription_id'); - $table->string('stripe_id')->unique(); - $table->string('stripe_product'); - $table->string('stripe_price'); - $table->integer('quantity')->nullable(); - $table->timestamps(); - - $table->index(['subscription_id', 'stripe_price']); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('subscription_items'); - } -}; diff --git a/database/migrations/2026_01_06_000528_add_meter_id_to_subscription_items_table.php b/database/migrations/2026_01_06_000528_add_meter_id_to_subscription_items_table.php deleted file mode 100644 index 033bb82..0000000 --- a/database/migrations/2026_01_06_000528_add_meter_id_to_subscription_items_table.php +++ /dev/null @@ -1,28 +0,0 @@ -string('meter_id')->nullable()->after('stripe_price'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('subscription_items', function (Blueprint $table) { - $table->dropColumn('meter_id'); - }); - } -}; diff --git a/database/migrations/2026_01_06_000529_add_meter_event_name_to_subscription_items_table.php b/database/migrations/2026_01_06_000529_add_meter_event_name_to_subscription_items_table.php deleted file mode 100644 index b157b3a..0000000 --- a/database/migrations/2026_01_06_000529_add_meter_event_name_to_subscription_items_table.php +++ /dev/null @@ -1,28 +0,0 @@ -string('meter_event_name')->nullable()->after('quantity'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('subscription_items', function (Blueprint $table) { - $table->dropColumn('meter_event_name'); - }); - } -}; diff --git a/resources/views/billing/index.blade.php b/resources/views/billing/index.blade.php deleted file mode 100644 index 00c8900..0000000 --- a/resources/views/billing/index.blade.php +++ /dev/null @@ -1,106 +0,0 @@ - -
-
-

BILLING

- - @if (session('success')) -
-

{{ session('success') }}

-
- @endif - - @if (session('error')) -
-

{{ session('error') }}

-
- @endif - -
-

Subscription Details

- -
-
- Plan - {{ $planType }} -
- -
- Status - - {{ ucfirst($subscription->stripe_status) }} - -
- - @if($nextBillingDate) -
- Next billing date - {{ $nextBillingDate->format('F j, Y') }} -
- @endif - - @if($subscription->ends_at) -
- Access until - {{ $subscription->ends_at->format('F j, Y') }} -
- @endif - - @if($planner->pm_last_four) -
- Payment method - - {{ ucfirst($planner->pm_type ?? 'Card') }} - •••• {{ $planner->pm_last_four }} - Update - -
- @endif -
- - @if(!$subscription->ends_at) -
- -
- @endif -
-
- - -
-
-

Cancel Subscription?

-

- Are you sure you want to cancel your subscription? You will retain access until the end of your current billing period. -

-
- -
- @csrf - -
-
-
-
-
-
diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/components/layouts/app.blade.php index d893086..c007beb 100644 --- a/resources/views/components/layouts/app.blade.php +++ b/resources/views/components/layouts/app.blade.php @@ -65,11 +65,6 @@ class="inline-flex items-center px-3 py-2 text-sm font-medium {{ request()->rout x-transition class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-gray-700 ring-1 ring-secondary">
- @if(is_mode_saas()) - - Billing - - @endif
@csrf -
-
- @else -
-

Subscribe to Dish Planner

- -
-
- @csrf - -
-

Monthly

-

Billed monthly

- -
-
- -
- @csrf - -
-

Yearly

-

Billed annually

- -
-
-
-
- @endif - - - diff --git a/routes/web.php b/routes/web.php index f23fbc5..eb414a9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,7 +3,6 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\Auth\LoginController; use App\Http\Controllers\Auth\RegisterController; -use App\Http\Controllers\SubscriptionController; Route::get('/', function () { return redirect()->route('dashboard'); @@ -24,27 +23,22 @@ // Authenticated routes Route::middleware('auth')->group(function () { + Route::get('/dashboard', function () { + return view('dashboard'); + })->name('dashboard'); + Route::post('/logout', [LoginController::class, 'logout'])->name('logout'); - - // Routes requiring active subscription in SaaS mode - Route::middleware('subscription')->group(function () { - Route::get('/dashboard', function () { - return view('dashboard'); - })->name('dashboard'); - - Route::get('/dishes', function () { - return view('dishes.index'); - })->name('dishes.index'); - - Route::get('/schedule', function () { - return view('schedule.index'); - })->name('schedule.index'); - - Route::get('/users', function () { - return view('users.index'); - })->name('users.index'); - - Route::get('/billing', [SubscriptionController::class, 'billing'])->name('billing')->middleware('saas'); - Route::get('/billing/portal', [SubscriptionController::class, 'billingPortal'])->name('billing.portal')->middleware('saas'); - }); + + // Placeholder routes for future Livewire components + Route::get('/dishes', function () { + return view('dishes.index'); + })->name('dishes.index'); + + Route::get('/schedule', function () { + return view('schedule.index'); + })->name('schedule.index'); + + Route::get('/users', function () { + return view('users.index'); + })->name('users.index'); }); diff --git a/routes/web/subscription.php b/routes/web/subscription.php deleted file mode 100644 index d3258b9..0000000 --- a/routes/web/subscription.php +++ /dev/null @@ -1,18 +0,0 @@ -name('cashier.webhook'); - -Route::middleware('auth')->group(function () { - Route::get('/subscription', function () { - return view('subscription.index'); - })->name('subscription.index'); - - Route::post('/subscription/checkout', [SubscriptionController::class, 'checkout'])->name('subscription.checkout'); - Route::get('/subscription/success', [SubscriptionController::class, 'success'])->name('subscription.success'); - Route::post('/subscription/cancel', [SubscriptionController::class, 'cancel'])->name('subscription.cancel'); -}); diff --git a/shell.nix b/shell.nix index 4ff3195..b8509d8 100644 --- a/shell.nix +++ b/shell.nix @@ -88,7 +88,7 @@ pkgs.mkShell { local REGISTRY="codeberg.org" local NAMESPACE="lvl0" local IMAGE_NAME="dish-planner" - + echo "🔨 Building production image..." podman build -f Dockerfile -t ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG} . @@ -112,19 +112,6 @@ pkgs.mkShell { fi } - prod-build-nc() { - local TAG="''${1:-latest}" - local REGISTRY="codeberg.org" - local NAMESPACE="lvl0" - local IMAGE_NAME="dish-planner" - - echo "🔨 Building production image (no cache)..." - podman build --no-cache -f Dockerfile -t ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG} . - - echo "✅ Build complete: ''${REGISTRY}/''${NAMESPACE}/''${IMAGE_NAME}:''${TAG}" - echo "Run 'prod-push' to push to Codeberg" - } - prod-build-push() { local TAG="''${1:-latest}" prod-build "$TAG" && prod-push "$TAG"