SEO additions
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\License;
|
||||
use Modules\Licensing\Models\License;
|
||||
use App\Models\User;
|
||||
use App\Services\Accounts\PurchasedAccountProvisioner;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseActivation;
|
||||
use Modules\Licensing\Models\License;
|
||||
use Modules\Licensing\Models\LicenseActivation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseActivation;
|
||||
use Modules\Licensing\Models\License;
|
||||
use Modules\Licensing\Models\LicenseActivation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
+8
-1
@@ -11,7 +11,14 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
protected $fillable = ['name', 'email', 'password', 'is_admin', 'disabled_at'];
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'email_verified_at',
|
||||
'password',
|
||||
'is_admin',
|
||||
'disabled_at',
|
||||
];
|
||||
|
||||
protected $hidden = ['password', 'remember_token'];
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
final class SonarDeskLicensingServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
RateLimiter::for('license-activation', function (Request $request): array {
|
||||
return [
|
||||
Limit::perMinute(12)->by($request->ip()),
|
||||
Limit::perMinute(6)->by(hash('sha256', (string) $request->input('installationID'))),
|
||||
];
|
||||
});
|
||||
|
||||
RateLimiter::for('login', fn (Request $request) => Limit::perMinute(5)
|
||||
->by(strtolower((string) $request->input('email')).'|'.$request->ip()));
|
||||
|
||||
RateLimiter::for('registration', fn (Request $request) => Limit::perHour(5)->by($request->ip()));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
<?php
|
||||
|
||||
use App\Providers\SonarDeskLicensingServiceProvider;
|
||||
|
||||
return [
|
||||
SonarDeskLicensingServiceProvider::class,
|
||||
];
|
||||
|
||||
+5
-2
@@ -3,7 +3,10 @@
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"framework"
|
||||
],
|
||||
"license": "MIT",
|
||||
"repositories": [
|
||||
{
|
||||
@@ -41,7 +44,7 @@
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/",
|
||||
"Modules\\": "Modules/"
|
||||
"Modules\\": "modules/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
|
||||
Generated
+11
-11
@@ -1542,16 +1542,16 @@
|
||||
},
|
||||
{
|
||||
"name": "league/commonmark",
|
||||
"version": "2.10.1",
|
||||
"version": "2.10.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/commonmark.git",
|
||||
"reference": "9d489ab67a02960fd8ffe624d93f751daf95439e"
|
||||
"reference": "6efbd9c472b91db0a3350fcd601c8332c2382e1f"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/9d489ab67a02960fd8ffe624d93f751daf95439e",
|
||||
"reference": "9d489ab67a02960fd8ffe624d93f751daf95439e",
|
||||
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6efbd9c472b91db0a3350fcd601c8332c2382e1f",
|
||||
"reference": "6efbd9c472b91db0a3350fcd601c8332c2382e1f",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1645,7 +1645,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-09-07T13:44:26+00:00"
|
||||
"time": "2026-09-21T13:07:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/config",
|
||||
@@ -4970,16 +4970,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php80",
|
||||
"version": "v1.37.0",
|
||||
"version": "v1.43.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php80.git",
|
||||
"reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411"
|
||||
"reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
|
||||
"reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74",
|
||||
"reference": "9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -5030,7 +5030,7 @@
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0"
|
||||
"source": "https://github.com/symfony/polyfill-php80/tree/v1.43.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -5050,7 +5050,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-10T16:19:22+00:00"
|
||||
"time": "2026-09-25T15:50:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php82",
|
||||
|
||||
@@ -28,21 +28,6 @@ return [
|
||||
'notify_admin_users' => filter_var(env('SONARDESK_SUPPORT_NOTIFY_ADMINS', true), FILTER_VALIDATE_BOOL),
|
||||
],
|
||||
|
||||
'paddle' => [
|
||||
'api_key' => env('PADDLE_API_KEY'),
|
||||
'client_side_token' => env('PADDLE_CLIENT_SIDE_TOKEN'),
|
||||
'environment' => env('PADDLE_ENVIRONMENT', env('APP_ENV') === 'production' ? 'production' : 'sandbox'),
|
||||
'api_url' => env('PADDLE_API_URL', env('APP_ENV') === 'production' ? 'https://api.paddle.com' : 'https://sandbox-api.paddle.com'),
|
||||
'webhook_secret' => env('PADDLE_WEBHOOK_SECRET'),
|
||||
'timestamp_tolerance' => (int) env('PADDLE_WEBHOOK_TOLERANCE', 300),
|
||||
|
||||
// SonarDesk is one Paddle product with four recurring prices.
|
||||
'product_id' => env('PADDLE_PRODUCT_ID'),
|
||||
'personal_monthly_price_id' => env('PADDLE_PERSONAL_MONTHLY_PRICE_ID'),
|
||||
'personal_annual_price_id' => env('PADDLE_PERSONAL_ANNUAL_PRICE_ID'),
|
||||
'professional_monthly_price_id' => env('PADDLE_PROFESSIONAL_MONTHLY_PRICE_ID'),
|
||||
'professional_annual_price_id' => env('PADDLE_PROFESSIONAL_ANNUAL_PRICE_ID'),
|
||||
],
|
||||
|
||||
'lemon_squeezy' => [
|
||||
'webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET'),
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
protected static ?string $password;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
'is_admin' => false,
|
||||
];
|
||||
}
|
||||
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
namespace Modules\Licensing\Console;
|
||||
|
||||
use App\Services\Licensing\LicenseDeliveryService;
|
||||
use Modules\Licensing\Services\LicenseDeliveryService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
final class SonarDeskLicenseDeliver extends Command
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
namespace Modules\Licensing\Console;
|
||||
|
||||
use App\Models\LicenseProduct;
|
||||
use App\Services\Licensing\LicenseIssuer;
|
||||
use Modules\Licensing\Models\LicenseProduct;
|
||||
use Modules\Licensing\Services\LicenseIssuer;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SonarDeskLicenseIssue extends Command
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
namespace Modules\Licensing\Console;
|
||||
|
||||
use App\Models\License;
|
||||
use Modules\Licensing\Models\License;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SonarDeskLicenseRevoke extends Command
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
namespace Modules\Licensing\Console;
|
||||
|
||||
use App\Models\LicenseProduct;
|
||||
use Modules\Licensing\Models\LicenseProduct;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SonarDeskProductCreate extends Command
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('license_products', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code')->unique();
|
||||
$table->string('name');
|
||||
$table->string('edition')->default('Professional');
|
||||
$table->unsignedInteger('activation_limit')->default(1);
|
||||
$table->unsignedInteger('max_major_version')->nullable();
|
||||
$table->string('payment_provider')->nullable();
|
||||
$table->string('external_product_id')->nullable();
|
||||
$table->string('external_price_id')->nullable();
|
||||
$table->boolean('active')->default(true);
|
||||
$table->timestamps();
|
||||
$table->unique(['payment_provider', 'external_price_id']);
|
||||
});
|
||||
|
||||
Schema::create('licenses', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignId('license_product_id')->constrained('license_products');
|
||||
$table->string('license_key_hash', 64)->unique();
|
||||
$table->string('license_key_last4', 4);
|
||||
$table->string('customer_name')->nullable();
|
||||
$table->string('customer_email')->nullable()->index();
|
||||
$table->string('edition');
|
||||
$table->unsignedInteger('activation_limit');
|
||||
$table->unsignedInteger('max_major_version')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamp('revoked_at')->nullable();
|
||||
$table->string('revoke_reason')->nullable();
|
||||
$table->string('payment_provider')->nullable();
|
||||
$table->string('external_order_id')->nullable();
|
||||
$table->string('external_customer_id')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['payment_provider', 'external_order_id']);
|
||||
});
|
||||
|
||||
Schema::create('license_delivery_outbox', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->uuid('license_id')->unique();
|
||||
$table->text('encrypted_license_key');
|
||||
$table->string('recipient_email')->nullable();
|
||||
$table->timestamp('delivered_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->foreign('license_id')->references('id')->on('licenses')->cascadeOnDelete();
|
||||
});
|
||||
|
||||
Schema::create('license_activations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->uuid('license_id');
|
||||
$table->uuid('installation_id');
|
||||
$table->string('platform', 32)->nullable();
|
||||
$table->string('app_version', 32)->nullable();
|
||||
$table->timestamp('activated_at');
|
||||
$table->timestamp('last_seen_at')->nullable();
|
||||
$table->timestamp('deactivated_at')->nullable();
|
||||
$table->foreign('license_id')->references('id')->on('licenses')->cascadeOnDelete();
|
||||
$table->unique(['license_id', 'installation_id']);
|
||||
$table->index(['license_id', 'deactivated_at']);
|
||||
});
|
||||
|
||||
Schema::create('license_webhook_events', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('provider');
|
||||
$table->string('external_event_id');
|
||||
$table->string('event_type');
|
||||
$table->string('payload_sha256', 64);
|
||||
$table->json('payload');
|
||||
$table->timestamp('processed_at')->nullable();
|
||||
$table->text('processing_error')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['provider', 'external_event_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('license_webhook_events');
|
||||
Schema::dropIfExists('license_activations');
|
||||
Schema::dropIfExists('license_delivery_outbox');
|
||||
Schema::dropIfExists('licenses');
|
||||
Schema::dropIfExists('license_products');
|
||||
}
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('users') && ! Schema::hasColumn('users', 'is_admin')) {
|
||||
Schema::table('users', fn (Blueprint $table) => $table->boolean('is_admin')->default(false)->index());
|
||||
}
|
||||
|
||||
if (Schema::hasTable('license_activations') && ! Schema::hasColumn('license_activations', 'installation_name')) {
|
||||
Schema::table('license_activations', fn (Blueprint $table) => $table->string('installation_name')->nullable()->after('installation_id'));
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (Schema::hasTable('license_activations') && Schema::hasColumn('license_activations', 'installation_name')) {
|
||||
Schema::table('license_activations', fn (Blueprint $table) => $table->dropColumn('installation_name'));
|
||||
}
|
||||
|
||||
if (Schema::hasTable('users') && Schema::hasColumn('users', 'is_admin')) {
|
||||
Schema::table('users', fn (Blueprint $table) => $table->dropColumn('is_admin'));
|
||||
}
|
||||
}
|
||||
};
|
||||
+6
-6
@@ -1,13 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
namespace Modules\Licensing\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseActivation;
|
||||
use App\Services\Licensing\LicenseKey;
|
||||
use App\Services\Licensing\LicenseSigner;
|
||||
use App\Services\Licensing\LicenseTokenVerifier;
|
||||
use Modules\Licensing\Models\License;
|
||||
use Modules\Licensing\Models\LicenseActivation;
|
||||
use Modules\Licensing\Services\LicenseKey;
|
||||
use Modules\Licensing\Services\LicenseSigner;
|
||||
use Modules\Licensing\Services\LicenseTokenVerifier;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
+5
-5
@@ -1,13 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
namespace Modules\Licensing\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\LicenseWebhookEvent;
|
||||
use App\Services\Licensing\LicenseDeliveryService;
|
||||
use Modules\Licensing\Models\LicenseWebhookEvent;
|
||||
use Modules\Licensing\Services\LicenseDeliveryService;
|
||||
use App\Services\Payments\LemonSqueezyWebhookVerifier;
|
||||
use App\Services\Payments\PaddleWebhookVerifier;
|
||||
use App\Services\Payments\PurchaseProvisioner;
|
||||
use Modules\Paddle\Services\PaddleWebhookVerifier;
|
||||
use Modules\Paddle\Services\PurchaseProvisioner;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
+7
-7
@@ -1,13 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Portal;
|
||||
namespace Modules\Licensing\Http\Controllers\Portal;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseDeliveryOutbox;
|
||||
use App\Models\LicenseProduct;
|
||||
use App\Services\Payments\PaddleClient;
|
||||
use App\Services\Licensing\LicenseDeliveryService;
|
||||
use Modules\Licensing\Models\License;
|
||||
use Modules\Licensing\Models\LicenseDeliveryOutbox;
|
||||
use Modules\Licensing\Models\LicenseProduct;
|
||||
use Modules\Paddle\Services\PaddleClient;
|
||||
use Modules\Licensing\Services\LicenseDeliveryService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use RuntimeException;
|
||||
@@ -131,7 +131,7 @@ final class PortalController extends Controller
|
||||
|
||||
private function professionalPriceFor(License $license): string
|
||||
{
|
||||
$paddle = config('sonardesk.paddle');
|
||||
$paddle = config('paddle');
|
||||
$current = (string) $license->external_price_id;
|
||||
|
||||
if ($current === (string) ($paddle['personal_annual_price_id'] ?? '')) {
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace Modules\Licensing\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace Modules\Licensing\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace Modules\Licensing\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace Modules\Licensing\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
namespace Modules\Licensing\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
namespace Modules\Licensing\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Database\Eloquent\Factory;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Modules\Licensing\Support\ModuleInfo;
|
||||
@@ -38,6 +41,19 @@ class LicensingServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot() : void
|
||||
{
|
||||
RateLimiter::for('login', fn (Request $request) => Limit::perMinute(5)
|
||||
->by(strtolower((string) $request->input('email')).'|'.$request->ip()));
|
||||
|
||||
RateLimiter::for('registration', fn (Request $request) =>
|
||||
Limit::perHour(5)->by($request->ip())
|
||||
);
|
||||
|
||||
RateLimiter::for('license-activation', function (Request $request): array {
|
||||
return [
|
||||
Limit::perMinute(12)->by($request->ip()),
|
||||
Limit::perMinute(6)->by(hash('sha256', (string) $request->input('installationID'))),
|
||||
];
|
||||
});
|
||||
$this->registerTranslations();
|
||||
$this->registerConfig();
|
||||
$this->registerViews();
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Licensing\Http\Controllers\Api\LicenseActivationController;
|
||||
use Modules\Licensing\Http\Controllers\Api\PaymentWebhookController;
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here is where you can register API routes for your application. These
|
||||
| routes are loaded by the RouteServiceProvider within a group which
|
||||
| is assigned the "api" middleware group. Enjoy building your API!
|
||||
|
|
||||
*/
|
||||
|
||||
Route::middleware('auth:api')->get('/licensing', function (Request $request) {
|
||||
return $request->user();
|
||||
});
|
||||
Route::prefix('v1')->group(function (): void {
|
||||
Route::post('/activate', [LicenseActivationController::class, 'activate'])->middleware('throttle:license-activation');
|
||||
Route::post('/validate', [LicenseActivationController::class, 'validateInstallation'])->middleware('throttle:license-activation');
|
||||
Route::post('/deactivate', [LicenseActivationController::class, 'deactivate'])->middleware('throttle:license-activation');
|
||||
Route::post('/webhooks/{provider}', PaymentWebhookController::class)->whereIn('provider', ['paddle', 'lemon-squeezy']);
|
||||
});
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Web Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here is where you can register web routes for your application. These
|
||||
| routes are loaded by the RouteServiceProvider within a group which
|
||||
| contains the "web" middleware group. Now create something great!
|
||||
|
|
||||
*/
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Licensing\Http\Controllers\Portal\PortalController;
|
||||
|
||||
Route::prefix('licensing')->group(function() {
|
||||
Route::get('/', 'LicensingController@index');
|
||||
Route::middleware(['auth', 'verified'])->prefix('portal')->name('portal.')->group(function (): void {
|
||||
Route::get('/', [PortalController::class, 'index'])->name('index');
|
||||
Route::get('/licences/{license}', [PortalController::class, 'show'])->name('show');
|
||||
Route::post('/licences/{license}/upgrade', [PortalController::class, 'upgrade'])->middleware('throttle:5,1')->name('upgrade');
|
||||
Route::delete('/licences/{license}/subscription', [PortalController::class, 'cancel'])->middleware('throttle:5,1')->name('cancel');
|
||||
Route::post('/licences/{license}/resend', [PortalController::class, 'resend'])->middleware('throttle:3,10')->name('resend');
|
||||
Route::delete('/licences/{license}/activations/{activation}', [PortalController::class, 'deactivate'])->name('deactivate');
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Licensing;
|
||||
namespace Modules\Licensing\Services;
|
||||
|
||||
use App\Models\LicenseDeliveryOutbox;
|
||||
use Modules\Licensing\Models\LicenseDeliveryOutbox;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Licensing;
|
||||
namespace Modules\Licensing\Services;
|
||||
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseDeliveryOutbox;
|
||||
use App\Models\LicenseProduct;
|
||||
use Modules\Licensing\Models\License;
|
||||
use Modules\Licensing\Models\LicenseDeliveryOutbox;
|
||||
use Modules\Licensing\Models\LicenseProduct;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Licensing;
|
||||
namespace Modules\Licensing\Services;
|
||||
|
||||
final class LicenseKey
|
||||
{
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Licensing;
|
||||
namespace Modules\Licensing\Services;
|
||||
|
||||
use App\Models\License;
|
||||
use Modules\Licensing\Models\License;
|
||||
use Carbon\CarbonInterface;
|
||||
use RuntimeException;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Licensing;
|
||||
namespace Modules\Licensing\Services;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
@@ -1,29 +1,15 @@
|
||||
{
|
||||
"name": "mpba/licensing",
|
||||
"description": "",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mark Gregory",
|
||||
"email": "mark.gregory@mpba.co.uk"
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [],
|
||||
"aliases": {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "SonarDesk Licensing module",
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\Licensing\\": ""
|
||||
},
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/",
|
||||
"Modules\\Licensing\\Tests\\": "modules/Licensing/Tests/"
|
||||
}
|
||||
"Modules\\Licensing\\Tests\\": "Tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "Licensing",
|
||||
"alias": "licensing",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"description": "SonarDesk Licensing module",
|
||||
"keywords": [],
|
||||
"priority": 0,
|
||||
"providers": [
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
return [
|
||||
'name' => 'Paddle',
|
||||
'features' =>
|
||||
[
|
||||
],
|
||||
'api_key' => env('PADDLE_API_KEY'),
|
||||
'client_side_token' => env('PADDLE_CLIENT_SIDE_TOKEN'),
|
||||
'environment' => env('PADDLE_ENVIRONMENT', env('APP_ENV') === 'production' ? 'production' : 'sandbox'),
|
||||
'api_url' => env('PADDLE_API_URL', env('APP_ENV') === 'production' ? 'https://api.paddle.com' : 'https://sandbox-api.paddle.com'),
|
||||
'webhook_secret' => env('PADDLE_WEBHOOK_SECRET'),
|
||||
'timestamp_tolerance' => (int) env('PADDLE_WEBHOOK_TOLERANCE', 300),
|
||||
'product_id' => env('PADDLE_PRODUCT_ID'),
|
||||
'personal_monthly_price_id' => env('PADDLE_PERSONAL_MONTHLY_PRICE_ID'),
|
||||
'personal_annual_price_id' => env('PADDLE_PERSONAL_ANNUAL_PRICE_ID'),
|
||||
'professional_monthly_price_id' => env('PADDLE_PROFESSIONAL_MONTHLY_PRICE_ID'),
|
||||
'professional_annual_price_id' => env('PADDLE_PROFESSIONAL_ANNUAL_PRICE_ID'),
|
||||
];
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('licenses', function (Blueprint $table) {
|
||||
$table->string('external_subscription_id')->nullable()->after('external_customer_id');
|
||||
$table->string('external_price_id')->nullable()->after('external_subscription_id');
|
||||
$table->string('subscription_status')->nullable()->after('external_price_id');
|
||||
$table->timestamp('current_period_ends_at')->nullable()->after('subscription_status');
|
||||
$table->timestamp('cancel_scheduled_at')->nullable()->after('current_period_ends_at');
|
||||
$table->unique(['payment_provider', 'external_subscription_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('licenses', function (Blueprint $table) {
|
||||
$table->dropUnique(['payment_provider', 'external_subscription_id']);
|
||||
$table->dropColumn([
|
||||
'external_subscription_id',
|
||||
'external_price_id',
|
||||
'subscription_status',
|
||||
'current_period_ends_at',
|
||||
'cancel_scheduled_at',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
+6
-6
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
namespace Modules\Paddle\Database\Seeders;
|
||||
|
||||
use App\Models\LicenseProduct;
|
||||
use Modules\Licensing\Models\LicenseProduct;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class PaddleCatalogSeeder extends Seeder
|
||||
@@ -10,10 +10,10 @@ class PaddleCatalogSeeder extends Seeder
|
||||
public function run(): void
|
||||
{
|
||||
$products = [
|
||||
['code' => 'personal-monthly', 'name' => 'SonarDesk Personal Monthly', 'edition' => 'Personal', 'activation_limit' => 1, 'external_product_id' => config('sonardesk.paddle.product_id'), 'external_price_id' => config('sonardesk.paddle.personal_monthly_price_id')],
|
||||
['code' => 'personal-annual', 'name' => 'SonarDesk Personal Annual', 'edition' => 'Personal', 'activation_limit' => 1, 'external_product_id' => config('sonardesk.paddle.product_id'), 'external_price_id' => config('sonardesk.paddle.personal_annual_price_id')],
|
||||
['code' => 'professional-monthly', 'name' => 'SonarDesk Professional Monthly', 'edition' => 'Professional', 'activation_limit' => 3, 'external_product_id' => config('sonardesk.paddle.product_id'), 'external_price_id' => config('sonardesk.paddle.professional_monthly_price_id')],
|
||||
['code' => 'professional-annual', 'name' => 'SonarDesk Professional Annual', 'edition' => 'Professional', 'activation_limit' => 3, 'external_product_id' => config('sonardesk.paddle.product_id'), 'external_price_id' => config('sonardesk.paddle.professional_annual_price_id')],
|
||||
['code' => 'personal-monthly', 'name' => 'SonarDesk Personal Monthly', 'edition' => 'Personal', 'activation_limit' => 1, 'external_product_id' => config('paddle.product_id'), 'external_price_id' => config('paddle.personal_monthly_price_id')],
|
||||
['code' => 'personal-annual', 'name' => 'SonarDesk Personal Annual', 'edition' => 'Personal', 'activation_limit' => 1, 'external_product_id' => config('paddle.product_id'), 'external_price_id' => config('paddle.personal_annual_price_id')],
|
||||
['code' => 'professional-monthly', 'name' => 'SonarDesk Professional Monthly', 'edition' => 'Professional', 'activation_limit' => 3, 'external_product_id' => config('paddle.product_id'), 'external_price_id' => config('paddle.professional_monthly_price_id')],
|
||||
['code' => 'professional-annual', 'name' => 'SonarDesk Professional Annual', 'edition' => 'Professional', 'activation_limit' => 3, 'external_product_id' => config('paddle.product_id'), 'external_price_id' => config('paddle.professional_annual_price_id')],
|
||||
];
|
||||
|
||||
foreach ($products as $data) {
|
||||
+4
-2
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
namespace Modules\Paddle\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use RuntimeException;
|
||||
@@ -9,7 +11,7 @@ final class CheckoutController extends Controller
|
||||
{
|
||||
public function __invoke(): View
|
||||
{
|
||||
$paddle = config('sonardesk.paddle');
|
||||
$paddle = config('paddle');
|
||||
$token = trim((string) ($paddle['client_side_token'] ?? ''));
|
||||
|
||||
if ($token === '') {
|
||||
@@ -1,16 +1,6 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Web Routes
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here is where you can register web routes for your application. These
|
||||
| routes are loaded by the RouteServiceProvider within a group which
|
||||
| contains the "web" middleware group. Now create something great!
|
||||
|
|
||||
*/
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Paddle\Http\Controllers\CheckoutController;
|
||||
|
||||
Route::prefix('paddle')->group(function() {
|
||||
Route::get('/', 'PaddleController@index');
|
||||
});
|
||||
Route::get('/buy', CheckoutController::class)->name('buy');
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payments;
|
||||
namespace Modules\Paddle\Services;
|
||||
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Http\Client\Response;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
|
||||
final class PaddleClient
|
||||
class PaddleClient
|
||||
{
|
||||
public function customer(?string $customerID): ?array
|
||||
{
|
||||
if (! $customerID || ! config('sonardesk.paddle.api_key')) {
|
||||
if (! $customerID || ! config('paddle.api_key')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -118,10 +118,10 @@ final class PaddleClient
|
||||
private function request(): PendingRequest
|
||||
{
|
||||
return Http::baseUrl(
|
||||
(string) config('sonardesk.paddle.api_url')
|
||||
(string) config('paddle.api_url')
|
||||
)
|
||||
->withToken(
|
||||
(string) config('sonardesk.paddle.api_key')
|
||||
(string) config('paddle.api_key')
|
||||
)
|
||||
->acceptJson()
|
||||
->timeout(15);
|
||||
@@ -131,7 +131,7 @@ final class PaddleClient
|
||||
{
|
||||
if (
|
||||
trim(
|
||||
(string) config('sonardesk.paddle.api_key')
|
||||
(string) config('paddle.api_key')
|
||||
) === ''
|
||||
) {
|
||||
throw new RuntimeException(
|
||||
+5
-3
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payments;
|
||||
namespace Modules\Paddle\Services;
|
||||
|
||||
use App\Services\Payments\WebhookVerifier;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -10,7 +12,7 @@ final class PaddleWebhookVerifier implements WebhookVerifier
|
||||
{
|
||||
public function verify(Request $request, string $rawBody): void
|
||||
{
|
||||
$secret = trim((string) config('sonardesk.paddle.webhook_secret'));
|
||||
$secret = trim((string) config('paddle.webhook_secret'));
|
||||
$header = trim((string) $request->header('Paddle-Signature'));
|
||||
|
||||
if ($secret === '') {
|
||||
@@ -55,7 +57,7 @@ final class PaddleWebhookVerifier implements WebhookVerifier
|
||||
|
||||
$tolerance = max(
|
||||
1,
|
||||
(int) config('sonardesk.paddle.timestamp_tolerance', 300)
|
||||
(int) config('paddle.timestamp_tolerance', 300)
|
||||
);
|
||||
|
||||
$age = abs(time() - $timestamp);
|
||||
+15
-5
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payments;
|
||||
namespace Modules\Paddle\Services;
|
||||
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseDeliveryOutbox;
|
||||
use App\Models\LicenseProduct;
|
||||
use Modules\Licensing\Models\License;
|
||||
use Modules\Licensing\Models\LicenseDeliveryOutbox;
|
||||
use Modules\Licensing\Models\LicenseProduct;
|
||||
use App\Services\Accounts\PurchasedAccountProvisioner;
|
||||
use App\Services\Licensing\LicenseIssuer;
|
||||
use Modules\Licensing\Services\LicenseIssuer;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use RuntimeException;
|
||||
@@ -743,6 +743,16 @@ final class PurchaseProvisioner
|
||||
* prevents provider supplied identifiers from creating unusual
|
||||
* lock names.
|
||||
*/
|
||||
/*
|
||||
* MySQL advisory locks are only available when using MySQL/MariaDB.
|
||||
*
|
||||
* Tests use SQLite in-memory, where GET_LOCK() and RELEASE_LOCK()
|
||||
* do not exist.
|
||||
*/
|
||||
if (DB::connection()->getDriverName() !== 'mysql') {
|
||||
return $callback();
|
||||
}
|
||||
|
||||
$lockName = 'sd:'.substr(hash('sha256', $name), 0, 61);
|
||||
|
||||
$row = DB::selectOne(
|
||||
@@ -1,29 +1,15 @@
|
||||
{
|
||||
"name": "mpba/paddle",
|
||||
"description": "",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mark Gregory",
|
||||
"email": "mark.gregory@mpba.co.uk"
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [],
|
||||
"aliases": {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "SonarDesk Paddle module",
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\Paddle\\": ""
|
||||
},
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/",
|
||||
"Modules\\Paddle\\Tests\\": "modules/Paddle/Tests/"
|
||||
}
|
||||
"Modules\\Paddle\\Tests\\": "Tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "Paddle",
|
||||
"alias": "paddle",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"description": "SonarDesk Paddle module",
|
||||
"keywords": [],
|
||||
"priority": 0,
|
||||
"providers": [
|
||||
@@ -10,5 +10,7 @@
|
||||
],
|
||||
"aliases": {},
|
||||
"files": [],
|
||||
"requires": []
|
||||
"requires": [
|
||||
"Licensing"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
{!! '<?xml version="1.0" encoding="utf-8"?>' !!}
|
||||
@php echo '<?xml version="1.0" encoding="utf-8"?>'; @endphp
|
||||
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"><channel><title>SonarDesk Updates</title><link>{{ route('updates.appcast') }}</link><description>SonarDesk stable release feed</description>@foreach($releases as $release)<item><title>SonarDesk {{ $release->version }}</title><pubDate>{{ $release->published_at?->toRfc2822String() }}</pubDate><sparkle:version>{{ $release->build_number }}</sparkle:version><sparkle:shortVersionString>{{ $release->version }}</sparkle:shortVersionString>@if($release->minimum_macos)<sparkle:minimumSystemVersion>{{ $release->minimum_macos }}</sparkle:minimumSystemVersion>@endif<description><![CDATA[{!! nl2br(e($release->release_notes ?: 'SonarDesk update')) !!}]]></description><enclosure url="{{ route('download.release',$release) }}" length="{{ $release->file_size }}" type="application/octet-stream"@if($release->sparkle_signature) sparkle:edSignature="{{ $release->sparkle_signature }}"@endif /></item>@endforeach</channel></rss>
|
||||
|
||||
+1
-10
@@ -1,12 +1,3 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\LicenseActivationController;
|
||||
use App\Http\Controllers\Api\PaymentWebhookController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::prefix('v1')->group(function () {
|
||||
Route::post('/activate', [LicenseActivationController::class, 'activate'])->middleware('throttle:license-activation');
|
||||
Route::post('/validate', [LicenseActivationController::class, 'validateInstallation'])->middleware('throttle:license-activation');
|
||||
Route::post('/deactivate', [LicenseActivationController::class, 'deactivate'])->middleware('throttle:license-activation');
|
||||
Route::post('/webhooks/{provider}', PaymentWebhookController::class)->whereIn('provider', ['paddle', 'lemon-squeezy']);
|
||||
});
|
||||
// Module-owned API routes are loaded by their RouteServiceProviders.
|
||||
|
||||
+1
-1
@@ -6,5 +6,5 @@ use Illuminate\Support\Facades\Schedule;
|
||||
Schedule::command('model:prune')->dailyAt('03:20')->withoutOverlapping();
|
||||
|
||||
Schedule::call(function (): void {
|
||||
app(\App\Services\Licensing\LicenseDeliveryService::class)->deliverPending(50);
|
||||
app(\Modules\Licensing\Services\LicenseDeliveryService::class)->deliverPending(50);
|
||||
})->name('license-delivery')->everyMinute()->withoutOverlapping();
|
||||
|
||||
@@ -12,9 +12,7 @@ use App\Http\Controllers\Auth\AuthenticatedSessionController;
|
||||
use App\Http\Controllers\Auth\EmailVerificationController;
|
||||
use App\Http\Controllers\Auth\PasswordController;
|
||||
use App\Http\Controllers\Auth\RegisteredUserController;
|
||||
use App\Http\Controllers\CheckoutController;
|
||||
use App\Http\Controllers\ReleaseController;
|
||||
use App\Http\Controllers\Portal\PortalController;
|
||||
use App\Http\Controllers\Support\SupportController;
|
||||
use App\Http\Middleware\AdminOnly;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -23,7 +21,6 @@ Route::get('/sitemap.xml', [SeoController::class, 'sitemap'])->name('sitemap');
|
||||
Route::get('/robots.txt', [SeoController::class, 'robots'])->name('robots');
|
||||
|
||||
Route::get('/', fn () => view('portal.welcome', ['latestRelease' => \App\Models\SonarDeskRelease::published()->where('channel', 'stable')->latest('published_at')->first()]))->name('home');
|
||||
Route::get('/buy', CheckoutController::class)->name('buy');
|
||||
Route::view('/features', 'features')->name('features');
|
||||
Route::get('/release', [ReleaseController::class, 'index'])->name('release');
|
||||
Route::get('/download', [ReleaseController::class, 'latest'])->name('download.latest');
|
||||
@@ -51,14 +48,6 @@ Route::middleware('auth')->group(function () {
|
||||
Route::get('/verify-email/{id}/{hash}', [EmailVerificationController::class, 'verify'])->middleware(['signed', 'throttle:6,1'])->name('verification.verify');
|
||||
Route::post('/email/verification-notification', [EmailVerificationController::class, 'resend'])->middleware('throttle:6,1')->name('verification.send');
|
||||
});
|
||||
Route::middleware(['auth', 'verified'])->prefix('portal')->name('portal.')->group(function () {
|
||||
Route::get('/', [PortalController::class, 'index'])->name('index');
|
||||
Route::get('/licences/{license}', [PortalController::class, 'show'])->name('show');
|
||||
Route::post('/licences/{license}/upgrade', [PortalController::class, 'upgrade'])->middleware('throttle:5,1')->name('upgrade');
|
||||
Route::delete('/licences/{license}/subscription', [PortalController::class, 'cancel'])->middleware('throttle:5,1')->name('cancel');
|
||||
Route::post('/licences/{license}/resend', [PortalController::class, 'resend'])->middleware('throttle:3,10')->name('resend');
|
||||
Route::delete('/licences/{license}/activations/{activation}', [PortalController::class, 'deactivate'])->name('deactivate');
|
||||
});
|
||||
Route::middleware(['auth', 'verified', AdminOnly::class])->prefix('admin')->name('admin.')->group(function () {
|
||||
Route::get('/', [AdminController::class, 'index'])->name('index');
|
||||
Route::get('/licences', [AdminController::class, 'licenses'])->name('licenses');
|
||||
|
||||
@@ -2,18 +2,20 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class CheckoutPageTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
public function test_buy_page_contains_all_four_configured_paddle_prices(): void
|
||||
{
|
||||
config()->set('sonardesk.paddle.client_side_token', 'test_example');
|
||||
config()->set('sonardesk.paddle.environment', 'sandbox');
|
||||
config()->set('sonardesk.paddle.personal_monthly_price_id', 'pri_personal_monthly');
|
||||
config()->set('sonardesk.paddle.personal_annual_price_id', 'pri_personal_annual');
|
||||
config()->set('sonardesk.paddle.professional_monthly_price_id', 'pri_professional_monthly');
|
||||
config()->set('sonardesk.paddle.professional_annual_price_id', 'pri_professional_annual');
|
||||
config()->set('paddle.client_side_token', 'test_example');
|
||||
config()->set('paddle.environment', 'sandbox');
|
||||
config()->set('paddle.personal_monthly_price_id', 'pri_personal_monthly');
|
||||
config()->set('paddle.personal_annual_price_id', 'pri_personal_annual');
|
||||
config()->set('paddle.professional_monthly_price_id', 'pri_professional_monthly');
|
||||
config()->set('paddle.professional_annual_price_id', 'pri_professional_annual');
|
||||
|
||||
$this->get('/buy')
|
||||
->assertOk()
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\LicenseActivation;
|
||||
use App\Models\LicenseProduct;
|
||||
use App\Services\Licensing\LicenseIssuer;
|
||||
use Modules\Licensing\Models\LicenseActivation;
|
||||
use Modules\Licensing\Models\LicenseProduct;
|
||||
use Modules\Licensing\Services\LicenseIssuer;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseProduct;
|
||||
use Modules\Licensing\Models\License;
|
||||
use Modules\Licensing\Models\LicenseProduct;
|
||||
use App\Models\User;
|
||||
use App\Services\Payments\PurchaseProvisioner;
|
||||
use Modules\Paddle\Services\PurchaseProvisioner;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -84,7 +84,7 @@ class PaddleSubscriptionProvisioningTest extends TestCase
|
||||
$this->product('pri_personal_monthly', 'Personal', 1);
|
||||
app(PurchaseProvisioner::class)->provision('paddle', $this->subscription(
|
||||
'subscription.updated', 'active', 'pri_personal_monthly', 'pro_personal',
|
||||
['data' => ['scheduled_change' => ['action' => 'cancel', 'effective_at' => '2026-10-09T00:00:00Z']]]
|
||||
['scheduled_change' => ['action' => 'cancel', 'effective_at' => '2026-10-09T00:00:00Z']]
|
||||
));
|
||||
|
||||
$license = License::firstOrFail();
|
||||
@@ -97,7 +97,7 @@ class PaddleSubscriptionProvisioningTest extends TestCase
|
||||
$this->product('pri_personal_monthly', 'Personal', 1);
|
||||
$service = app(PurchaseProvisioner::class);
|
||||
$service->provision('paddle', $this->subscription('subscription.created', 'active', 'pri_personal_monthly', 'pro_personal'));
|
||||
$service->provision('paddle', $this->subscription('subscription.canceled', 'canceled', 'pri_personal_monthly', 'pro_personal', ['data' => ['current_billing_period' => null]]));
|
||||
$service->provision('paddle', $this->subscription('subscription.canceled', 'canceled', 'pri_personal_monthly', 'pro_personal', ['current_billing_period' => null]));
|
||||
|
||||
$license = License::firstOrFail();
|
||||
$this->assertNotNull($license->revoked_at);
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseProduct;
|
||||
use Modules\Licensing\Models\License;
|
||||
use Modules\Licensing\Models\LicenseProduct;
|
||||
use App\Models\User;
|
||||
use App\Services\Payments\PaddleClient;
|
||||
use Modules\Paddle\Services\PaddleClient;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
|
||||
use Mockery\MockInterface;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -31,8 +32,10 @@ class PortalSubscriptionManagementTest extends TestCase
|
||||
|
||||
public function test_personal_subscription_is_upgraded_in_place(): void
|
||||
{
|
||||
config()->set('sonardesk.paddle.personal_monthly_price_id', 'pri_personal_monthly');
|
||||
config()->set('sonardesk.paddle.professional_monthly_price_id', 'pri_professional_monthly');
|
||||
$this->withoutMiddleware(PreventRequestForgery::class);
|
||||
|
||||
config()->set('paddle.personal_monthly_price_id', 'pri_personal_monthly');
|
||||
config()->set('paddle.professional_monthly_price_id', 'pri_professional_monthly');
|
||||
|
||||
$personal = $this->product('personal-monthly', 'Personal', 'pri_personal_monthly', 1);
|
||||
$professional = $this->product('professional-monthly', 'Professional', 'pri_professional_monthly', 3);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
Reference in New Issue
Block a user