Änderungen (relevant):
- Add‑on Checkout auf Transactions + Transaction‑ID speichern: app/Services/Addons/EventAddonCheckoutService.php
- Paket/Marketing Checkout auf Transactions: app/Services/Paddle/PaddleCheckoutService.php
- Gift‑Voucher Checkout: Customer anlegen/finden + Transactions: app/Services/GiftVouchers/
GiftVoucherCheckoutService.php
- Tests aktualisiert: tests/Feature/Tenant/EventAddonCheckoutTest.php, tests/Unit/PaddleCheckoutServiceTest.php,
tests/Unit/GiftVoucherCheckoutServiceTest.php
This commit is contained in:
50
tests/Feature/Api/Tenant/OnboardingStatusTest.php
Normal file
50
tests/Feature/Api/Tenant/OnboardingStatusTest.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Api\Tenant;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Tests\Feature\Tenant\TenantTestCase;
|
||||
|
||||
class OnboardingStatusTest extends TenantTestCase
|
||||
{
|
||||
public function test_tenant_can_dismiss_onboarding(): void
|
||||
{
|
||||
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/onboarding', [
|
||||
'step' => 'dismissed',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$this->tenant->refresh();
|
||||
|
||||
$dismissedAt = Arr::get($this->tenant->settings ?? [], 'onboarding.dismissed_at');
|
||||
|
||||
$this->assertNotNull($dismissedAt);
|
||||
|
||||
$show = $this->authenticatedRequest('GET', '/api/v1/tenant/onboarding');
|
||||
|
||||
$show->assertOk();
|
||||
$show->assertJsonPath('steps.dismissed_at', $dismissedAt);
|
||||
}
|
||||
|
||||
public function test_tenant_can_mark_onboarding_completed(): void
|
||||
{
|
||||
$response = $this->authenticatedRequest('POST', '/api/v1/tenant/onboarding', [
|
||||
'step' => 'completed',
|
||||
'meta' => [],
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$this->tenant->refresh();
|
||||
|
||||
$completedAt = Arr::get($this->tenant->settings ?? [], 'onboarding.completed_at');
|
||||
|
||||
$this->assertNotNull($completedAt);
|
||||
|
||||
$show = $this->authenticatedRequest('GET', '/api/v1/tenant/onboarding');
|
||||
|
||||
$show->assertOk();
|
||||
$show->assertJsonPath('steps.completed_at', $completedAt);
|
||||
}
|
||||
}
|
||||
42
tests/Feature/Console/ScheduleConfigurationTest.php
Normal file
42
tests/Feature/Console/ScheduleConfigurationTest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Console;
|
||||
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ScheduleConfigurationTest extends TestCase
|
||||
{
|
||||
public function test_storage_and_checkout_commands_are_scheduled(): void
|
||||
{
|
||||
Artisan::call('schedule:list', [
|
||||
'--json' => true,
|
||||
'--no-interaction' => true,
|
||||
]);
|
||||
|
||||
$tasks = json_decode(Artisan::output(), true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
$this->assertScheduledCommand($tasks, 'storage:monitor', '*/5 * * * *');
|
||||
$this->assertScheduledCommand($tasks, 'storage:check-upload-queues', '*/5 * * * *');
|
||||
$this->assertScheduledCommand($tasks, 'storage:archive-pending', '0 1 * * *');
|
||||
$this->assertScheduledCommand($tasks, 'checkout:send-reminders', '0 * * * *');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $tasks
|
||||
*/
|
||||
private function assertScheduledCommand(array $tasks, string $command, string $expression): void
|
||||
{
|
||||
foreach ($tasks as $task) {
|
||||
$taskCommand = (string) ($task['command'] ?? '');
|
||||
if ($taskCommand !== '' && Str::contains($taskCommand, $command)) {
|
||||
$this->assertSame($expression, $task['expression'] ?? null);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->fail(sprintf('Scheduled command not found: %s', $command));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Queue\Events\JobFailed;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -55,4 +59,156 @@ class SentryReportingTest extends TestCase
|
||||
|
||||
$this->assertSame(0, $fake->captured, 'Sentry should ignore 4xx errors');
|
||||
}
|
||||
|
||||
public function test_reports_scheduled_task_failures_to_sentry(): void
|
||||
{
|
||||
$fake = new class
|
||||
{
|
||||
public int $captured = 0;
|
||||
|
||||
public function captureException(mixed $exception = null): void
|
||||
{
|
||||
$this->captured++;
|
||||
}
|
||||
};
|
||||
|
||||
$this->app->instance('sentry', $fake);
|
||||
|
||||
Artisan::call('schedule:list', [
|
||||
'--json' => true,
|
||||
'--no-interaction' => true,
|
||||
]);
|
||||
|
||||
$schedule = $this->app->make(Schedule::class);
|
||||
$task = collect($schedule->events())
|
||||
->first(fn ($event) => str_contains((string) $event->command, 'storage:monitor'));
|
||||
|
||||
$this->assertNotNull($task, 'Expected scheduled storage:monitor task');
|
||||
|
||||
$task->finish($this->app, 1);
|
||||
|
||||
$this->assertSame(1, $fake->captured, 'Sentry should receive scheduled task failures');
|
||||
}
|
||||
|
||||
public function test_reports_queue_failures_to_sentry(): void
|
||||
{
|
||||
$fake = new class
|
||||
{
|
||||
public int $captured = 0;
|
||||
|
||||
public function captureException(mixed $exception = null): void
|
||||
{
|
||||
$this->captured++;
|
||||
}
|
||||
};
|
||||
|
||||
$this->app->instance('sentry', $fake);
|
||||
|
||||
$job = new class implements \Illuminate\Contracts\Queue\Job
|
||||
{
|
||||
public function uuid(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getJobId(): string
|
||||
{
|
||||
return 'job-id';
|
||||
}
|
||||
|
||||
public function payload(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function fire(): void {}
|
||||
|
||||
public function release($delay = 0): void {}
|
||||
|
||||
public function isReleased(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function delete(): void {}
|
||||
|
||||
public function isDeleted(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isDeletedOrReleased(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function attempts(): int
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function hasFailed(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function markAsFailed(): void {}
|
||||
|
||||
public function fail($e = null): void {}
|
||||
|
||||
public function maxTries(): ?int
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function maxExceptions(): ?int
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function timeout(): ?int
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function retryUntil(): ?int
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'FakeJob';
|
||||
}
|
||||
|
||||
public function resolveName(): string
|
||||
{
|
||||
return 'FakeJob';
|
||||
}
|
||||
|
||||
public function resolveQueuedJobClass(): string
|
||||
{
|
||||
return 'FakeJob';
|
||||
}
|
||||
|
||||
public function getConnectionName(): string
|
||||
{
|
||||
return 'redis';
|
||||
}
|
||||
|
||||
public function getQueue(): string
|
||||
{
|
||||
return 'default';
|
||||
}
|
||||
|
||||
public function getRawBody(): string
|
||||
{
|
||||
return '{}';
|
||||
}
|
||||
};
|
||||
|
||||
Event::dispatch(new JobFailed('redis', $job, new \RuntimeException('Queue failure')));
|
||||
|
||||
$this->assertSame(1, $fake->captured, 'Sentry should receive queue failures');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,17 @@ class EventAddonCheckoutTest extends TenantTestCase
|
||||
|
||||
// Fake Paddle response
|
||||
Http::fake([
|
||||
'*/checkout/links' => Http::response([
|
||||
'*/customers' => Http::response([
|
||||
'data' => [
|
||||
'url' => 'https://checkout.paddle.test/abcd',
|
||||
'id' => 'chk_addon_123',
|
||||
'expires_at' => now()->addHour()->toIso8601String(),
|
||||
'id' => 'ctm_addon_123',
|
||||
],
|
||||
], 200),
|
||||
'*/transactions' => Http::response([
|
||||
'data' => [
|
||||
'id' => 'txn_addon_123',
|
||||
'checkout' => [
|
||||
'url' => 'https://checkout.paddle.test/abcd',
|
||||
],
|
||||
],
|
||||
], 200),
|
||||
]);
|
||||
@@ -67,14 +73,14 @@ class EventAddonCheckoutTest extends TenantTestCase
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('checkout_id', 'chk_addon_123');
|
||||
$response->assertJsonPath('checkout_id', 'txn_addon_123');
|
||||
|
||||
$this->assertDatabaseHas('event_package_addons', [
|
||||
'event_package_id' => $eventPackage->id,
|
||||
'addon_key' => 'extra_photos_small',
|
||||
'status' => 'pending',
|
||||
'quantity' => 2,
|
||||
'checkout_id' => 'chk_addon_123',
|
||||
'transaction_id' => 'txn_addon_123',
|
||||
]);
|
||||
|
||||
$addon = EventPackageAddon::where('event_package_id', $eventPackage->id)->latest()->first();
|
||||
|
||||
@@ -37,12 +37,18 @@ class GiftVoucherCheckoutServiceTest extends TestCase
|
||||
$client = Mockery::mock(PaddleClient::class);
|
||||
$client->shouldReceive('post')
|
||||
->once()
|
||||
->with('/checkout/links', Mockery::on(function ($payload) {
|
||||
->with('/customers', Mockery::on(function ($payload) {
|
||||
return $payload['email'] === 'buyer@example.com';
|
||||
}))
|
||||
->andReturn(['data' => ['id' => 'ctm_123']]);
|
||||
$client->shouldReceive('post')
|
||||
->once()
|
||||
->with('/transactions', Mockery::on(function ($payload) {
|
||||
return $payload['items'][0]['price_id'] === 'pri_a'
|
||||
&& $payload['customer_email'] === 'buyer@example.com'
|
||||
&& $payload['customer_id'] === 'ctm_123'
|
||||
&& $payload['custom_data']['type'] === 'gift_voucher';
|
||||
}))
|
||||
->andReturn(['data' => ['url' => 'https://paddle.test/checkout/123', 'expires_at' => '2025-12-31T00:00:00Z', 'id' => 'chk_123']]);
|
||||
->andReturn(['data' => ['checkout' => ['url' => 'https://paddle.test/checkout/123'], 'id' => 'txn_123']]);
|
||||
|
||||
$this->app->instance(PaddleClient::class, $client);
|
||||
|
||||
@@ -57,6 +63,6 @@ class GiftVoucherCheckoutServiceTest extends TestCase
|
||||
]);
|
||||
|
||||
$this->assertSame('https://paddle.test/checkout/123', $checkout['checkout_url']);
|
||||
$this->assertSame('chk_123', $checkout['id']);
|
||||
$this->assertSame('txn_123', $checkout['id']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,15 +35,18 @@ class PaddleCheckoutServiceTest extends TestCase
|
||||
|
||||
$client->shouldReceive('post')
|
||||
->once()
|
||||
->with('/checkout/links', Mockery::on(function (array $payload) use ($tenant, $package) {
|
||||
->with('/transactions', Mockery::on(function (array $payload) use ($tenant, $package) {
|
||||
return $payload['items'][0]['price_id'] === 'pri_123'
|
||||
&& $payload['customer_id'] === 'ctm_123'
|
||||
&& ($payload['custom_data']['tenant_id'] ?? null) === (string) $tenant->id
|
||||
&& ($payload['custom_data']['package_id'] ?? null) === (string) $package->id
|
||||
&& ($payload['custom_data']['source'] ?? null) === 'test'
|
||||
&& ! isset($payload['metadata']);
|
||||
&& ! isset($payload['metadata'])
|
||||
&& ! isset($payload['success_url'])
|
||||
&& ! isset($payload['cancel_url'])
|
||||
&& ! isset($payload['customer_email']);
|
||||
}))
|
||||
->andReturn(['data' => ['url' => 'https://paddle.test/checkout/123', 'id' => 'chk_123']]);
|
||||
->andReturn(['data' => ['checkout' => ['url' => 'https://paddle.test/checkout/123'], 'id' => 'txn_123']]);
|
||||
|
||||
$this->app->instance(PaddleClient::class, $client);
|
||||
$this->app->instance(PaddleCustomerService::class, $customers);
|
||||
@@ -57,6 +60,6 @@ class PaddleCheckoutServiceTest extends TestCase
|
||||
]);
|
||||
|
||||
$this->assertSame('https://paddle.test/checkout/123', $checkout['checkout_url']);
|
||||
$this->assertSame('chk_123', $checkout['id']);
|
||||
$this->assertSame('txn_123', $checkout['id']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user