move into sonardesk
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
namespace Modules\Licensing\Http\Controllers;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Contracts\View\View;
|
||||
|
||||
/** Checkout handoff only. Billing/webhook processing remains on the legacy service. */
|
||||
final class CheckoutHandoffController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('licensing::checkout.index', ['enabled' => $this->destination() !== null]);
|
||||
}
|
||||
|
||||
public function redirect(): RedirectResponse
|
||||
{
|
||||
$destination = $this->destination();
|
||||
abort_unless($destination !== null, 503, 'Checkout is not configured.');
|
||||
return redirect()->away($destination, 302);
|
||||
}
|
||||
|
||||
private function destination(): ?string
|
||||
{
|
||||
if (! config('sonardesk-integration.checkout_enabled')) {
|
||||
return null;
|
||||
}
|
||||
$url = trim((string) config('sonardesk-integration.checkout_url'));
|
||||
$parts = parse_url($url);
|
||||
if (! is_array($parts) || strtolower($parts['scheme'] ?? '') !== 'https'
|
||||
|| ! isset($parts['host']) || isset($parts['user']) || isset($parts['pass'])
|
||||
|| isset($parts['fragment']) || isset($parts['port'])) {
|
||||
return null;
|
||||
}
|
||||
$host = strtolower(rtrim($parts['host'], '.'));
|
||||
$allowed = array_map(static fn ($value) => strtolower(rtrim((string) $value, '.')), config('sonardesk-integration.allowed_checkout_hosts', []));
|
||||
if (! in_array($host, $allowed, true)) {
|
||||
return null;
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ final class IntegratedLicensingServiceProvider extends ServiceProvider
|
||||
]);
|
||||
|
||||
$this->mergeConfigFrom(base_path('config/sonardesk-paddle.php'), 'sonardesk-paddle');
|
||||
$this->mergeConfigFrom(base_path('config/sonardesk-integration.php'), 'sonardesk-integration');
|
||||
$this->loadMigrationsFrom(__DIR__.'/../Database/Migrations');
|
||||
$this->loadViewsFrom(__DIR__.'/../Resources/views', 'licensing');
|
||||
$this->mergeConfigFrom(__DIR__.'/../Config/config.php', 'licensing');
|
||||
@@ -34,5 +35,6 @@ final class IntegratedLicensingServiceProvider extends ServiceProvider
|
||||
Route::group([], __DIR__.'/../Routes/admin.php');
|
||||
Route::group([], __DIR__.'/../Routes/customer.php');
|
||||
Route::group([], __DIR__.'/../Routes/distribution.php');
|
||||
Route::group([], __DIR__.'/../Routes/checkout.php');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
@extends('licensing::customer.layout')
|
||||
@section('title', 'SonarDesk · Purchase')
|
||||
@section('content')
|
||||
<section class="card">
|
||||
<h1>SonarDesk</h1>
|
||||
<p>Native SonarQube project monitoring for macOS.</p>
|
||||
<p>Choose your licence on our secure checkout service. Your subscription and licence are processed by the existing SonarDesk licensing platform.</p>
|
||||
@if($enabled)
|
||||
<p><a href="{{ route('sonardesk.checkout') }}" style="display:inline-block;background:#163b70;color:#fff;padding:12px 20px;border-radius:8px;text-decoration:none">View plans & buy</a></p>
|
||||
@else
|
||||
<p role="status">Online checkout is not yet enabled on this site. No payment will be taken here.</p>
|
||||
@endif
|
||||
<p><a href="{{ route('sonardesk.downloads') }}">Downloads & release notes</a> · <a href="{{ route('account.sonardesk.index') }}">My licences</a></p>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Licensing\Http\Controllers\CheckoutHandoffController;
|
||||
|
||||
Route::middleware(['web', 'throttle:30,1'])->prefix('sonardesk')->name('sonardesk.')->group(function (): void {
|
||||
Route::get('/buy', [CheckoutHandoffController::class, 'index'])->name('buy');
|
||||
Route::get('/checkout', [CheckoutHandoffController::class, 'redirect'])->name('checkout');
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
return [
|
||||
// Off by default. The existing SonarDesk service remains the billing authority.
|
||||
'checkout_enabled' => (bool) env('SONARDESK_CHECKOUT_ENABLED', false),
|
||||
'checkout_url' => env('SONARDESK_CHECKOUT_URL', ''),
|
||||
'allowed_checkout_hosts' => array_values(array_filter(array_map('trim', explode(',', (string) env('SONARDESK_CHECKOUT_ALLOWED_HOSTS', ''))))),
|
||||
];
|
||||
@@ -0,0 +1,37 @@
|
||||
# Phase 12 — Controlled checkout handoff and production-readiness gate
|
||||
|
||||
This is a **safe first cutover increment**, not a completed production migration. It introduces a unified SonarDesk purchase landing page in ABSDev and an explicit, allowlisted HTTPS handoff to the *existing* licensing checkout. It deliberately does NOT duplicate payment processing, webhooks, licence issuance, subscription mutations or customer ownership rules.
|
||||
|
||||
## Install
|
||||
Extract this archive **into the existing ABSDEV project root**, preserving `Modules/Licensing`. No root-level module copies are included.
|
||||
|
||||
```sh
|
||||
composer dump-autoload
|
||||
php artisan optimize:clear
|
||||
php artisan route:list --path=sonardesk/buy
|
||||
php artisan route:list --path=sonardesk/checkout
|
||||
php artisan test --filter=SonarDeskCheckoutHandoffTest
|
||||
```
|
||||
|
||||
## Staging-only opt-in
|
||||
Add to ABSDev `.env` (use the real existing hosted checkout URL):
|
||||
|
||||
```dotenv
|
||||
SONARDESK_CHECKOUT_ENABLED=false
|
||||
SONARDESK_CHECKOUT_URL=https://sonardesk.stage.absdev.net/buy
|
||||
SONARDESK_CHECKOUT_ALLOWED_HOSTS=sonardesk.stage.absdev.net
|
||||
```
|
||||
|
||||
After verifying the hosted path exists and the correct Paddle **sandbox** account is used, set `SONARDESK_CHECKOUT_ENABLED=true` in staging and clear configuration cache. Hostnames are exact-match allowlisted. Do not use production until separately approved.
|
||||
|
||||
## Acceptance and cutover gates — NOT automatically satisfied
|
||||
1. Verify the checkout path and Paddle sandbox checkout end to end.
|
||||
2. Verify webhook signatures, idempotency, retry handling, licence issuance and delivery in the **existing** service.
|
||||
3. Verify customer portal email ownership, subscription visibility and activation limits.
|
||||
4. Verify signed Sparkle appcast and actual updater in a signed test build; merely serving RSS is insufficient.
|
||||
5. Test product downloads, checksums, platform restrictions, admin permissions and backup/restore.
|
||||
6. Address `npm audit` findings; perform Laravel tests, integration tests and manual UI checks.
|
||||
7. Plan DB and file migration, dual-write/rollback, DNS and webhook destination cutover **before** switching billing authority.
|
||||
8. Keep legacy production licensing service and webhook endpoint unchanged until a separate validated cutover.
|
||||
|
||||
No production deployment or billing authority transfer is performed by this patch.
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Tests\TestCase;
|
||||
|
||||
final class SonarDeskCheckoutHandoffTest extends TestCase
|
||||
{
|
||||
public function test_checkout_is_disabled_by_default(): void
|
||||
{
|
||||
config()->set('sonardesk-integration.checkout_enabled', false);
|
||||
$this->get('/sonardesk/buy')->assertOk()->assertSee('not yet enabled');
|
||||
$this->get('/sonardesk/checkout')->assertStatus(503);
|
||||
}
|
||||
|
||||
public function test_checkout_rejects_unlisted_host(): void
|
||||
{
|
||||
config()->set('sonardesk-integration.checkout_enabled', true);
|
||||
config()->set('sonardesk-integration.checkout_url', 'https://evil.example/buy');
|
||||
config()->set('sonardesk-integration.allowed_checkout_hosts', ['sonardesk.example']);
|
||||
$this->get('/sonardesk/checkout')->assertStatus(503);
|
||||
}
|
||||
|
||||
public function test_checkout_redirects_only_to_approved_https_host(): void
|
||||
{
|
||||
config()->set('sonardesk-integration.checkout_enabled', true);
|
||||
config()->set('sonardesk-integration.checkout_url', 'https://sonardesk.example/buy');
|
||||
config()->set('sonardesk-integration.allowed_checkout_hosts', ['sonardesk.example']);
|
||||
$this->get('/sonardesk/checkout')->assertRedirect('https://sonardesk.example/buy');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user