new stage code
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\License;
|
||||
use App\Models\User;
|
||||
use App\Services\Accounts\PurchasedAccountProvisioner;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
final class SonarDeskAccountsBackfill extends Command
|
||||
{
|
||||
protected $signature = 'sonardesk:accounts:backfill {--dry-run : Report missing customer accounts without creating them} {--no-email : Create accounts without sending password setup links}';
|
||||
|
||||
protected $description = 'Create portal accounts for existing licence holders that do not already have one.';
|
||||
|
||||
public function handle(PurchasedAccountProvisioner $accounts): int
|
||||
{
|
||||
$customers = License::query()
|
||||
->whereNotNull('customer_email')
|
||||
->where('customer_email', '<>', '')
|
||||
->select(['customer_email', 'customer_name'])
|
||||
->orderBy('customer_email')
|
||||
->get()
|
||||
->unique(fn (License $license) => strtolower(trim((string) $license->customer_email)));
|
||||
|
||||
$missing = 0;
|
||||
$created = 0;
|
||||
|
||||
foreach ($customers as $license) {
|
||||
$email = strtolower(trim((string) $license->customer_email));
|
||||
if ($email === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (User::query()->whereRaw('lower(email) = ?', [$email])->exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$missing++;
|
||||
if ($this->option('dry-run')) {
|
||||
$this->line("MISSING {$email}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($accounts->ensure($email, $license->customer_name, ! $this->option('no-email'))) {
|
||||
$created++;
|
||||
$this->info("CREATED {$email}");
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->option('dry-run')) {
|
||||
$this->info("{$missing} account(s) would be created.");
|
||||
} else {
|
||||
$this->info("{$created} account(s) created.");
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Accounts;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
final class PurchasedAccountProvisioner
|
||||
{
|
||||
public function ensure(?string $email, ?string $name = null, bool $sendSetupLink = true): ?User
|
||||
{
|
||||
$email = strtolower(trim((string) $email));
|
||||
if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = User::query()->whereRaw('lower(email) = ?', [$email])->first();
|
||||
if ($user) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$user = User::create([
|
||||
'name' => $this->displayName($name, $email),
|
||||
'email' => $email,
|
||||
'email_verified_at' => now(),
|
||||
'password' => Str::random(64),
|
||||
'is_admin' => false,
|
||||
]);
|
||||
|
||||
if ($sendSetupLink) {
|
||||
DB::afterCommit(static function () use ($email): void {
|
||||
Password::sendResetLink(['email' => $email]);
|
||||
});
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function displayName(?string $name, string $email): string
|
||||
{
|
||||
$name = trim((string) $name);
|
||||
if ($name !== '') {
|
||||
return mb_substr($name, 0, 120);
|
||||
}
|
||||
|
||||
$localPart = strstr($email, '@', true) ?: 'SonarDesk Customer';
|
||||
$localPart = trim(str_replace(['.', '_', '-'], ' ', $localPart));
|
||||
|
||||
return mb_substr($localPart !== '' ? ucwords($localPart) : 'SonarDesk Customer', 0, 120);
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,18 @@ namespace App\Services\Payments;
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseProduct;
|
||||
use App\Models\LicenseDeliveryOutbox;
|
||||
use App\Services\Accounts\PurchasedAccountProvisioner;
|
||||
use App\Services\Licensing\LicenseIssuer;
|
||||
use Illuminate\Support\Carbon;
|
||||
use RuntimeException;
|
||||
|
||||
final class PurchaseProvisioner
|
||||
{
|
||||
public function __construct(private LicenseIssuer $issuer, private PaddleClient $paddle) {}
|
||||
public function __construct(
|
||||
private LicenseIssuer $issuer,
|
||||
private PaddleClient $paddle,
|
||||
private PurchasedAccountProvisioner $accounts,
|
||||
) {}
|
||||
|
||||
public function provision(string $provider, array $event): void
|
||||
{
|
||||
@@ -89,6 +94,7 @@ final class PurchaseProvisioner
|
||||
$this->nullableString($data['customer_id'] ?? null),
|
||||
true,
|
||||
);
|
||||
$this->accounts->ensure($license->customer_email, $license->customer_name);
|
||||
$license->external_subscription_id = $subscriptionID;
|
||||
} elseif ($priceID !== '' && $priceID !== $license->external_price_id) {
|
||||
// Upgrades/downgrades change the entitlement attached to the existing key.
|
||||
@@ -107,6 +113,7 @@ final class PurchaseProvisioner
|
||||
|
||||
$license->external_customer_id = $this->nullableString($data['customer_id'] ?? null) ?? $license->external_customer_id;
|
||||
$this->backfillPaddleCustomer($license, $data);
|
||||
$this->accounts->ensure($license->customer_email, $license->customer_name);
|
||||
$license->external_price_id = $priceID !== '' ? $priceID : $license->external_price_id;
|
||||
$license->subscription_status = $status !== '' ? $status : $license->subscription_status;
|
||||
$license->current_period_ends_at = $periodEnd ? Carbon::parse($periodEnd) : $license->current_period_ends_at;
|
||||
@@ -155,7 +162,7 @@ final class PurchaseProvisioner
|
||||
$custom = is_array($data['custom_data'] ?? null) ? $data['custom_data'] : [];
|
||||
$customer = $this->paddle->customer($this->nullableString($data['customer_id'] ?? null)) ?? [];
|
||||
|
||||
$this->issuer->issue(
|
||||
[$license] = $this->issuer->issue(
|
||||
$product,
|
||||
$this->nullableString($custom['customer_name'] ?? $custom['name'] ?? $customer['name'] ?? null),
|
||||
$this->nullableString($custom['customer_email'] ?? $custom['email'] ?? $customer['email'] ?? null),
|
||||
@@ -164,6 +171,7 @@ final class PurchaseProvisioner
|
||||
$this->nullableString($data['customer_id'] ?? null),
|
||||
true,
|
||||
);
|
||||
$this->accounts->ensure($license->customer_email, $license->customer_name);
|
||||
}
|
||||
|
||||
private function provisionLemonSqueezy(array $event): void
|
||||
@@ -191,7 +199,7 @@ final class PurchaseProvisioner
|
||||
(string) ($attributes['product_id'] ?? ''),
|
||||
);
|
||||
|
||||
$this->issuer->issue(
|
||||
[$license] = $this->issuer->issue(
|
||||
$product,
|
||||
$this->nullableString($attributes['user_name'] ?? null),
|
||||
$this->nullableString($attributes['user_email'] ?? null),
|
||||
@@ -200,6 +208,7 @@ final class PurchaseProvisioner
|
||||
$this->nullableString($attributes['customer_id'] ?? null),
|
||||
true,
|
||||
);
|
||||
$this->accounts->ensure($license->customer_email, $license->customer_name);
|
||||
}
|
||||
|
||||
private function backfillPaddleCustomer(License $license, array $data): void
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Tests\Feature;
|
||||
|
||||
use App\Models\License;
|
||||
use App\Models\LicenseProduct;
|
||||
use App\Models\User;
|
||||
use App\Services\Payments\PurchaseProvisioner;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
@@ -102,4 +103,30 @@ class PaddleSubscriptionProvisioningTest extends TestCase
|
||||
$this->assertNotNull($license->revoked_at);
|
||||
$this->assertFalse($license->is_usable);
|
||||
}
|
||||
|
||||
public function test_new_purchase_creates_a_portal_user_for_the_customer(): void
|
||||
{
|
||||
$this->product('pri_personal_monthly', 'Personal', 1);
|
||||
|
||||
app(PurchaseProvisioner::class)->provision('paddle', $this->subscription(
|
||||
'subscription.created', 'active', 'pri_personal_monthly', 'pro_personal'
|
||||
));
|
||||
|
||||
$user = User::where('email', 'mark@example.com')->firstOrFail();
|
||||
$this->assertSame('Mark', $user->name);
|
||||
$this->assertNotNull($user->email_verified_at);
|
||||
}
|
||||
|
||||
public function test_existing_customer_account_is_reused(): void
|
||||
{
|
||||
User::factory()->create(['name' => 'Existing Mark', 'email' => 'mark@example.com']);
|
||||
$this->product('pri_personal_monthly', 'Personal', 1);
|
||||
|
||||
app(PurchaseProvisioner::class)->provision('paddle', $this->subscription(
|
||||
'subscription.created', 'active', 'pri_personal_monthly', 'pro_personal'
|
||||
));
|
||||
|
||||
$this->assertSame(1, User::where('email', 'mark@example.com')->count());
|
||||
$this->assertSame('Existing Mark', User::where('email', 'mark@example.com')->firstOrFail()->name);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user