Guard Paddle sync mapping
This commit is contained in:
@@ -90,7 +90,7 @@
|
||||
{"id":"fotospiel-app-pcz","title":"Security review: route/middleware inventory for marketing/API","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:03:54.409559375+01:00","created_by":"soeren","updated_at":"2026-01-01T16:04:00.000669934+01:00","closed_at":"2026-01-01T16:04:00.000669934+01:00","close_reason":"Completed in codebase (verified)"}
|
||||
{"id":"fotospiel-app-poe","title":"Security review checklist: Cross-cutting headers/CSRF/rate limits","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T16:05:03.2320643+01:00","created_by":"soeren","updated_at":"2026-01-01T16:05:03.2320643+01:00"}
|
||||
{"id":"fotospiel-app-q2n","title":"Checkout refactor: wizard foundations + updated steps","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:05:58.701443698+01:00","created_by":"soeren","updated_at":"2026-01-01T16:06:04.313207281+01:00","closed_at":"2026-01-01T16:06:04.313207281+01:00","close_reason":"Completed in codebase (verified)"}
|
||||
{"id":"fotospiel-app-qlj","title":"Paddle catalog sync: verify legacy packages mapped before auto-sync","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T15:59:43.333792314+01:00","created_by":"soeren","updated_at":"2026-01-01T15:59:43.333792314+01:00"}
|
||||
{"id":"fotospiel-app-qlj","title":"Paddle catalog sync: verify legacy packages mapped before auto-sync","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T15:59:43.333792314+01:00","created_by":"soeren","updated_at":"2026-01-02T21:46:52.797515024+01:00","closed_at":"2026-01-02T21:46:52.797515024+01:00","close_reason":"Completed"}
|
||||
{"id":"fotospiel-app-qtn","title":"Security review kickoff mitigations (CORS allowlist, headers, upload hardening, signed URLs)","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:09:46.310873311+01:00","created_by":"soeren","updated_at":"2026-01-01T16:09:51.914359487+01:00","closed_at":"2026-01-01T16:09:51.914359487+01:00","close_reason":"Completed in codebase (verified)"}
|
||||
{"id":"fotospiel-app-sbs","title":"Compliance tools: data export + retention overrides","description":"GDPR-compliant export requests and retention override workflows for tenants/events.","status":"closed","priority":3,"issue_type":"feature","created_at":"2026-01-01T14:20:16.530289009+01:00","updated_at":"2026-01-02T20:13:31.704875591+01:00","closed_at":"2026-01-02T20:13:31.704875591+01:00","close_reason":"Closed"}
|
||||
{"id":"fotospiel-app-swb","title":"Security review: replace public asset URLs with signed routes","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:04:05.610098299+01:00","created_by":"soeren","updated_at":"2026-01-01T16:04:11.215921463+01:00","closed_at":"2026-01-01T16:04:11.215921463+01:00","close_reason":"Completed in codebase (verified)"}
|
||||
|
||||
@@ -15,6 +15,7 @@ class PaddleSyncPackages extends Command
|
||||
{--package=* : Limit sync to the given package IDs or slugs}
|
||||
{--dry-run : Generate payload snapshots without calling Paddle}
|
||||
{--pull : Fetch remote Paddle state instead of pushing local changes}
|
||||
{--allow-unmapped : Allow sync when packages are missing Paddle product/price IDs}
|
||||
{--queue : Dispatch jobs onto the queue instead of running synchronously}';
|
||||
|
||||
protected $description = 'Synchronise local packages with Paddle products and prices.';
|
||||
@@ -32,6 +33,13 @@ class PaddleSyncPackages extends Command
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$pull = (bool) $this->option('pull');
|
||||
$queue = (bool) $this->option('queue');
|
||||
$allowUnmapped = (bool) $this->option('allow-unmapped');
|
||||
|
||||
if (! $pull && ! $allowUnmapped && ! $this->hasPackageFilter()) {
|
||||
if (! $this->guardUnmappedPackages($packages)) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
$packages->each(function (Package $package) use ($dryRun, $pull, $queue) {
|
||||
if ($pull) {
|
||||
@@ -82,6 +90,42 @@ class PaddleSyncPackages extends Command
|
||||
return $query->orderByDesc('id')->get();
|
||||
}
|
||||
|
||||
protected function hasPackageFilter(): bool
|
||||
{
|
||||
return collect((array) $this->option('package'))->filter()->isNotEmpty();
|
||||
}
|
||||
|
||||
protected function guardUnmappedPackages(Collection $packages): bool
|
||||
{
|
||||
$unmapped = $packages->filter(fn (Package $package) => blank($package->paddle_product_id) || blank($package->paddle_price_id));
|
||||
|
||||
if ($unmapped->isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->error('Unmapped Paddle package IDs detected. Resolve legacy mappings or pass --allow-unmapped.');
|
||||
$this->table(
|
||||
['ID', 'Slug', 'Missing'],
|
||||
$unmapped->map(function (Package $package): array {
|
||||
$missing = [];
|
||||
if (blank($package->paddle_product_id)) {
|
||||
$missing[] = 'product_id';
|
||||
}
|
||||
if (blank($package->paddle_price_id)) {
|
||||
$missing[] = 'price_id';
|
||||
}
|
||||
|
||||
return [
|
||||
$package->id,
|
||||
$package->slug,
|
||||
implode(', ', $missing),
|
||||
];
|
||||
})->values()->all()
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function dispatchSyncJob(Package $package, bool $dryRun, bool $queue): void
|
||||
{
|
||||
$context = [
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Tests\Feature;
|
||||
|
||||
use App\Jobs\SyncPackageToPaddle;
|
||||
use App\Models\Package;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus as BusFacade;
|
||||
use Tests\TestCase;
|
||||
@@ -14,7 +15,10 @@ class PaddleSyncPackagesCommandTest extends TestCase
|
||||
|
||||
public function test_command_dispatches_jobs_for_packages(): void
|
||||
{
|
||||
Package::factory()->count(2)->create();
|
||||
Package::factory()->count(2)->create([
|
||||
'paddle_product_id' => 'pro_test',
|
||||
'paddle_price_id' => 'pri_test',
|
||||
]);
|
||||
|
||||
BusFacade::fake();
|
||||
|
||||
@@ -44,6 +48,41 @@ class PaddleSyncPackagesCommandTest extends TestCase
|
||||
});
|
||||
}
|
||||
|
||||
public function test_command_blocks_bulk_sync_with_unmapped_packages(): void
|
||||
{
|
||||
Package::factory()->create([
|
||||
'paddle_product_id' => null,
|
||||
'paddle_price_id' => null,
|
||||
]);
|
||||
|
||||
BusFacade::fake();
|
||||
|
||||
$this->artisan('paddle:sync-packages', [
|
||||
'--dry-run' => true,
|
||||
'--queue' => true,
|
||||
])->assertExitCode(Command::FAILURE);
|
||||
|
||||
BusFacade::assertNotDispatched(SyncPackageToPaddle::class);
|
||||
}
|
||||
|
||||
public function test_command_allows_unmapped_packages_when_overridden(): void
|
||||
{
|
||||
Package::factory()->create([
|
||||
'paddle_product_id' => null,
|
||||
'paddle_price_id' => null,
|
||||
]);
|
||||
|
||||
BusFacade::fake();
|
||||
|
||||
$this->artisan('paddle:sync-packages', [
|
||||
'--dry-run' => true,
|
||||
'--queue' => true,
|
||||
'--allow-unmapped' => true,
|
||||
])->assertExitCode(Command::SUCCESS);
|
||||
|
||||
BusFacade::assertDispatched(SyncPackageToPaddle::class, 1);
|
||||
}
|
||||
|
||||
protected function getJobPackageId(SyncPackageToPaddle $job): int
|
||||
{
|
||||
$reflection = new \ReflectionClass($job);
|
||||
|
||||
Reference in New Issue
Block a user