Depublikation: abgebrochener Checkout blockiert nicht mehr
Der Klick auf "Kostenpflichtig depublizieren" legte sofort einen Pending-Kauf an; wer bei Stripe abbrach, sah danach dauerhaft "liegt bereits ein Depublikations-Vorgang vor". Jetzt sperrt nur ein bezahlter Vorgang — ein offener Pending-Kauf wird beim nächsten Klick wiederverwendet (kein Duplikat), und die Detailseite zeigt das Angebot weiterhin an. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9b55e11159
commit
1ef1e13c19
3 changed files with 50 additions and 13 deletions
|
|
@ -206,19 +206,22 @@ class CheckoutController extends Controller
|
|||
return $redirect;
|
||||
}
|
||||
|
||||
// Doppelbuchungs-Bremse: eine offene oder bereits bezahlte
|
||||
// Depublikation zu dieser PM reicht.
|
||||
$alreadyRequested = SinglePurchase::query()
|
||||
// Doppelbuchungs-Bremse: nur eine BEZAHLTE Depublikation blockiert.
|
||||
// Ein offener Pending-Kauf (abgebrochener Stripe-Checkout) wird
|
||||
// wiederverwendet, damit der Abbruch den Weg nicht dauerhaft
|
||||
// versperrt (QA-Befund 07.07.).
|
||||
$existing = SinglePurchase::query()
|
||||
->where('press_release_id', $pressRelease->id)
|
||||
->where('type', SinglePurchaseType::Depublication->value)
|
||||
->whereIn('status', [SinglePurchaseStatus::Pending->value, SinglePurchaseStatus::Paid->value])
|
||||
->exists();
|
||||
->latest('id')
|
||||
->first();
|
||||
|
||||
if ($alreadyRequested) {
|
||||
if ($existing && $existing->status !== SinglePurchaseStatus::Pending) {
|
||||
return $this->backToBookings(__('Für diese Pressemitteilung liegt bereits ein Depublikations-Vorgang vor.'));
|
||||
}
|
||||
|
||||
$purchase = SinglePurchase::query()->create([
|
||||
$purchase = $existing ?? SinglePurchase::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'type' => SinglePurchaseType::Depublication->value,
|
||||
'status' => SinglePurchaseStatus::Pending->value,
|
||||
|
|
|
|||
|
|
@ -238,10 +238,13 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
|
|||
'statusLogs' => $pr->statusLogs,
|
||||
// T12: Depublikation ist config-gated (ohne Preis kein Angebot).
|
||||
'depublicationPriceCents' => (int) config('billing.depublication_price_cents'),
|
||||
// Nur ein BEZAHLTER Vorgang blendet das Angebot aus — ein
|
||||
// abgebrochener Checkout (Pending) wird beim nächsten Klick
|
||||
// wiederverwendet (CheckoutController::depublication).
|
||||
'depublicationRequested' => SinglePurchase::query()
|
||||
->where('press_release_id', $pr->id)
|
||||
->where('type', SinglePurchaseType::Depublication->value)
|
||||
->whereIn('status', [SinglePurchaseStatus::Pending->value, SinglePurchaseStatus::Paid->value])
|
||||
->where('status', SinglePurchaseStatus::Paid->value)
|
||||
->exists(),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,8 @@ test('the depublication checkout creates a pending purchase and hands it to stri
|
|||
$checkout = Mockery::mock(Checkout::class);
|
||||
$checkout->shouldReceive('toResponse')->andReturn(new RedirectResponse('https://checkout.stripe.com/c/pay/test'));
|
||||
$this->mock(StripeCheckoutService::class, function ($mock) use ($checkout) {
|
||||
$mock->shouldReceive('forDepublication')->once()->andReturn($checkout);
|
||||
// Zweimal: der zweite Aufruf verwendet den offenen Kauf wieder.
|
||||
$mock->shouldReceive('forDepublication')->twice()->andReturn($checkout);
|
||||
});
|
||||
|
||||
$this->actingAs($user)
|
||||
|
|
@ -107,10 +108,33 @@ test('the depublication checkout creates a pending purchase and hands it to stri
|
|||
expect($purchase->price_cents)->toBe(19900);
|
||||
expect($purchase->press_release_id)->toBe($release->id);
|
||||
|
||||
// Doppelbuchungs-Bremse: zweiter Aufruf legt keinen weiteren Kauf an.
|
||||
// Abgebrochener Checkout blockiert nicht: erneuter Aufruf nutzt den
|
||||
// Pending-Kauf wieder statt einen zweiten anzulegen.
|
||||
$this->actingAs($user)
|
||||
->get(route('me.checkout.depublication', $release->id))
|
||||
->assertRedirect(route('me.bookings.index'));
|
||||
->assertRedirect('https://checkout.stripe.com/c/pay/test');
|
||||
|
||||
expect(SinglePurchase::count())->toBe(1);
|
||||
});
|
||||
|
||||
test('a paid depublication blocks a second checkout for the same release', function () {
|
||||
/** @var TestCase $this */
|
||||
$user = depublicationCustomer();
|
||||
$release = publishedReleaseFor($user);
|
||||
|
||||
SinglePurchase::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'type' => SinglePurchaseType::Depublication->value,
|
||||
'status' => SinglePurchaseStatus::Paid->value,
|
||||
'price_cents' => 19900,
|
||||
'currency' => 'EUR',
|
||||
'press_release_id' => $release->id,
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('me.checkout.depublication', $release->id))
|
||||
->assertRedirect(route('me.bookings.index'))
|
||||
->assertSessionHas('checkout-notice');
|
||||
|
||||
expect(SinglePurchase::count())->toBe(1);
|
||||
});
|
||||
|
|
@ -145,12 +169,12 @@ test('the detail page hides the depublication offer without a price and after a
|
|||
LivewireVolt::test('customer.press-releases.show', ['id' => $release->id])
|
||||
->assertDontSee('Depublikation (Service)');
|
||||
|
||||
// Mit laufendem Vorgang: Hinweis statt erneutem Kauf-Button.
|
||||
// Mit BEZAHLTEM Vorgang: Hinweis statt erneutem Kauf-Button.
|
||||
config()->set('billing.depublication_price_cents', 19900);
|
||||
SinglePurchase::query()->create([
|
||||
$purchase = SinglePurchase::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'type' => SinglePurchaseType::Depublication->value,
|
||||
'status' => SinglePurchaseStatus::Pending->value,
|
||||
'status' => SinglePurchaseStatus::Paid->value,
|
||||
'price_cents' => 19900,
|
||||
'currency' => 'EUR',
|
||||
'press_release_id' => $release->id,
|
||||
|
|
@ -158,6 +182,13 @@ test('the detail page hides the depublication offer without a price and after a
|
|||
LivewireVolt::test('customer.press-releases.show', ['id' => $release->id])
|
||||
->assertSee('Für diese Pressemitteilung liegt bereits ein Depublikations-Vorgang vor.')
|
||||
->assertDontSee('Kostenpflichtig depublizieren');
|
||||
|
||||
// Ein offener Pending-Kauf (abgebrochener Checkout) blockiert das
|
||||
// Angebot dagegen nicht — der Button bleibt nutzbar.
|
||||
$purchase->update(['status' => SinglePurchaseStatus::Pending->value]);
|
||||
LivewireVolt::test('customer.press-releases.show', ['id' => $release->id])
|
||||
->assertSee('Kostenpflichtig depublizieren')
|
||||
->assertDontSee('Für diese Pressemitteilung liegt bereits ein Depublikations-Vorgang vor.');
|
||||
});
|
||||
|
||||
test('the paid webhook depublishes the release and the public page shows a tombstone', function () {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue