Skip to content

Commit fb978f6

Browse files
committed
Preparation for webhooks
1 parent 60f83d7 commit fb978f6

File tree

10 files changed

+186
-25
lines changed

10 files changed

+186
-25
lines changed

.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ PUZZLE_PUZZLE_PASSWORD=
3939
MAILER_DSN=smtp://mailer:1025
4040

4141
STRIPE_API_KEY=
42+
STRIPE_WEBHOOK_SECRET=

config/services.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
$parameters->set('puzzlePuzzleUsername', '%env(PUZZLE_PUZZLE_USERNAME)%');
2727
$parameters->set('puzzlePuzzlePassword', '%env(PUZZLE_PUZZLE_PASSWORD)%');
2828

29+
$parameters->set('stripeWebhookSecret', '%env(STRIPE_WEBHOOK_SECRET)%');
30+
2931
$services = $configurator->services();
3032

3133
$services->defaults()

public/img/puzzlie-1.png

44 KB
Loading

src/Controller/StripeWebhookController.php

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,34 @@
33

44
namespace SpeedPuzzling\Web\Controller;
55

6+
use SpeedPuzzling\Web\Services\MembershipManagement;
7+
use Stripe\StripeClient;
8+
use Stripe\Webhook;
69
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
10+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
11+
use Symfony\Component\HttpFoundation\Request;
712
use Symfony\Component\HttpFoundation\Response;
813
use Symfony\Component\Routing\Attribute\Route;
914

1015
final class StripeWebhookController extends AbstractController
1116
{
12-
#[Route(
13-
path: [
14-
'cs' => '/scan-puzzli/{code}',
15-
'en' => '/en/scan-puzzle/{code}',
16-
],
17-
name: 'stripe_webhook',
18-
)]
19-
public function __invoke(null|string $code): Response
17+
public function __construct(
18+
#[Autowire(param: 'stripeWebhookSecret')]
19+
readonly private string $stripeWebhookSecret,
20+
readonly private MembershipManagement $membershipManagement,
21+
) {
22+
}
23+
24+
#[Route(path: '/webhook/stripe', name: 'stripe_webhook')]
25+
public function __invoke(Request $request): Response
2026
{
27+
/** @var string $signHeader */
28+
$signHeader = $request->headers->get('Stripe-Signature', '');
29+
30+
$event = Webhook::constructEvent($request->getContent(), $signHeader, $this->stripeWebhookSecret);
31+
32+
$this->membershipManagement->handleEvent($event);
33+
34+
return new Response('ok');
2135
}
2236
}

src/Services/MembershipManagement.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44

55
namespace SpeedPuzzling\Web\Services;
66

7+
use Psr\Log\LoggerInterface;
78
use SpeedPuzzling\Web\Message\CreatePlayerStripeCustomer;
89
use SpeedPuzzling\Web\Query\GetPlayerProfile;
10+
use Stripe\Event;
911
use Stripe\Price;
1012
use Stripe\StripeClient;
13+
use Stripe\Subscription;
1114
use Symfony\Component\Messenger\MessageBusInterface;
1215
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
1316
use Symfony\Component\Routing\RouterInterface;
@@ -22,6 +25,8 @@ public function __construct(
2225
private StripeClient $stripeClient,
2326
private GetPlayerProfile $getPlayerProfile,
2427
private MessageBusInterface $messageBus,
28+
private LoggerInterface $logger,
29+
private StripeWebhookHandler $stripeWebhookHandler,
2530
) {
2631
}
2732

@@ -92,4 +97,43 @@ private function createCustomerId(string $playerId): string
9297

9398
return $playerProfile->stripeCustomerId;
9499
}
100+
101+
public function handleEvent(Event $event): void
102+
{
103+
switch ($event->type) {
104+
case 'customer.subscription.trial_will_end':
105+
$subscription = $event->data->object ?? null;
106+
107+
if ($subscription instanceof Subscription) {
108+
$this->stripeWebhookHandler->handleTrialWillEnd($subscription);
109+
}
110+
break;
111+
case 'customer.subscription.created':
112+
$subscription = $event->data->object ?? null;
113+
114+
if ($subscription instanceof Subscription) {
115+
$this->stripeWebhookHandler->handleSubscriptionCreated($subscription);
116+
}
117+
break;
118+
case 'customer.subscription.deleted':
119+
$subscription = $event->data->object ?? null;
120+
121+
if ($subscription instanceof Subscription) {
122+
$this->stripeWebhookHandler->handleSubscriptionDeleted($subscription);
123+
}
124+
break;
125+
case 'customer.subscription.updated':
126+
$subscription = $event->data->object ?? null;
127+
128+
if ($subscription instanceof Subscription) {
129+
$this->stripeWebhookHandler->handleSubscriptionUpdated($subscription);
130+
}
131+
break;
132+
default:
133+
$this->logger->error('Unsupported Stripe webhook event', [
134+
'event_id' => $event->id,
135+
'event_type' => $event->type,
136+
]);
137+
}
138+
}
95139
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SpeedPuzzling\Web\Services;
6+
7+
use Stripe\Subscription;
8+
9+
readonly final class StripeWebhookHandler
10+
{
11+
public function handleTrialWillEnd(Subscription $stripeSubscription): void
12+
{
13+
}
14+
15+
public function handleSubscriptionCreated(Subscription $stripeSubscription): void
16+
{
17+
}
18+
19+
public function handleSubscriptionDeleted(Subscription $stripeSubscription): void
20+
{
21+
}
22+
23+
public function handleSubscriptionUpdated(Subscription $stripeSubscription): void
24+
{
25+
}
26+
}

templates/membership-checkout-cancel.html.twig

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
{% extends 'base.html.twig' %}
22

3-
{% block content %}
3+
{% block title %}{{ 'membership_payment_cancelled.meta.title'|trans }}{% endblock %}
4+
{% block meta_description %}{{ 'membership_payment_cancelled.meta.description'|trans }}{% endblock %}
45

5-
{% endblock %}
6+
{% block content %}
7+
<div class="pb-4">
8+
<div class="row gy-3 pb-3">
9+
<div class="col-lg-5 col-md-8 col-sm-10">
10+
<h1 class="h2">{{ 'membership_payment_cancelled.title'|trans }}</h1>
611

7-
{% block title %}
12+
<p>{{ 'membership_payment_cancelled.text'|trans }}</p>
813

14+
<p>
15+
<a class="btn btn-outline-primary me-2" href="{{ path('membership') }}"><i class="ci-arrow-left"></i> {{ 'membership_payment_cancelled.membership_link'|trans }}</a>
16+
<a class="btn btn-primary" href="{{ path('buy_membership') }}"><i class="ci-card"></i> {{ 'membership_payment_cancelled.payment_link'|trans }}</a>
17+
</p>
18+
</div>
19+
</div>
20+
</div>
921
{% endblock %}
10-

templates/membership.html.twig

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,48 @@
66
{% block content %}
77
<div class="pb-4">
88
<div class="row gy-3 pb-3">
9-
<div class="col-lg-5 col-md-8 col-sm-10">
9+
<div class="col-lg-5 col-md-8">
1010
<h1 class="h2">{{ 'membership.title'|trans }}</h1>
1111

12-
<p>{{ 'membership.intro_text'|trans }}</p>
1312

1413
{% if membership %}
14+
<p class="text-success fs-3">
15+
<i class="ci-check-circle me-2"></i> {{ 'membership.membership_enabled'|trans }}
16+
</p>
17+
1518
{% if membership.billingPeriodEndsAt %}
16-
Příští platba: {{ membership.billingPeriodEndsAt|date('d.m.Y') }}
19+
<p>
20+
{{ 'membership.next_payment'|trans }}: <strong>{{ membership.billingPeriodEndsAt|date('d.m.Y H:i') }}</strong>
21+
</p>
1722
{% endif %}
1823

1924
{% if membership.endsAt %}
20-
Platnost členství do: {{ membership.billingPeriodEndsAt|date('d.m.Y') }}
25+
{{ 'membership.membership_expiration'|trans }}: <strong>{{ membership.billingPeriodEndsAt|date('d.m.Y') }}</strong>
2126
{% endif %}
2227

2328
<p>
24-
<a class="btn btn-outline-primary" href="{{ path('billing_portal') }}">Správa platební metody</a>
29+
<a class="btn btn-outline-primary" href="{{ path('billing_portal') }}">{{ 'membership.payment_portal_button'|trans }}</a>
2530
</p>
2631
{% else %}
27-
Koupit členství
28-
29-
<div class="row text-center">
30-
<div class="col-6">
31-
<a class="btn btn-outline-primary mt-2" href="{{ path('buy_membership', {'priceLookupKey': 'puzzlership_monthly_promo'}) }}"><del class="small">6 € / month</del><br>3 € / month</a>
32-
</div>
33-
</div>
32+
<p>{{ 'membership.become_member'|trans }}</p>
33+
34+
<p class="mt-2 text-center">
35+
<a class="btn btn-outline-primary mt-2 text-center fs-6" href="{{ path('buy_membership') }}">
36+
<small><i class="ci-card me-1"></i> {{ 'membership.subscribe_for'|trans }}</small>
37+
<br><del class="small">{{ 'membership.original_price_per_month'|trans }}</del>
38+
<br>{{ 'membership.price_per_month'|trans }}
39+
</a>
40+
</p>
3441
{% endif %}
3542

36-
<p>Seznam věcí, které získám jako člen..</p>
43+
<h2 class="mt-4">{{ 'membership.description_title'|trans }}</h2>
44+
{{ 'membership.full_description'|trans|raw }}
45+
</div>
46+
47+
<div class="col-lg-5 offset-lg-1 col-md-4 text-center">
48+
<img class="img-fluid" src="{{ asset('img/puzzlie-1.png') }}" alt="Mascot">
3749
</div>
3850
</div>
51+
3952
</div>
4053
{% endblock %}

translations/messages.cs.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,3 +443,28 @@ added_time_recap:
443443
add_time: "Přidat další čas"
444444
the_same_puzzle: "Ke stejným puzzlím"
445445
puzzle_detail: "Výsledky"
446+
447+
membership:
448+
meta:
449+
title: "Členství"
450+
description: ""
451+
title: "MySpeedPuzzling členství"
452+
become_member: "Předplaťte si MySpeedPuzzling členství - podpořte vývoj a získejte přístup k exkluzivním funkcím pouze pro členy. Nyní s výhodnější zavádějící cenou, která vám zůstane po celou dobu členství."
453+
membership_enabled: "Děkujeme, že jste součástí!"
454+
next_payment: "Příští platba"
455+
membership_expiration: "Platnost členství do"
456+
payment_portal_button: "Upravit předplatné (platební portál)"
457+
description_title: "Proč se stát členem?"
458+
full_description: "<p>To brzy doplníme ... :-)</p>"
459+
subscribe_for: "Předplatit za"
460+
original_price_per_month: "150 Kč / měsíc"
461+
price_per_month: "100 Kč / měsíc"
462+
463+
membership_payment_cancelled:
464+
meta:
465+
title: "Platba byla zrušena"
466+
description: ""
467+
title: "Platba byla zrušena"
468+
text: "Platební proces byl přerušen, nejspíše vámi :-)."
469+
membership_link: "Zpět na přehled o členství"
470+
payment_link: "Opakovat platbu"

translations/messages.en.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,3 +441,28 @@ added_time_recap:
441441
add_time: "Add new time"
442442
the_same_puzzle: "For the same puzzle"
443443
puzzle_detail: "Results"
444+
445+
membership:
446+
meta:
447+
title: "Membership"
448+
description: ""
449+
title: "MySpeedPuzzling Membership"
450+
become_member: "Subscribe to MySpeedPuzzling Membership - support development and gain access to exclusive member-only features. Now at an introductory price that stays with you for the duration of your membership."
451+
membership_enabled: "Thank you for being part of the community!"
452+
next_payment: "Next payment"
453+
membership_expiration: "Membership valid until"
454+
payment_portal_button: "Manage subscription (payment portal)"
455+
description_title: "Why become a member?"
456+
full_description: "<p>We will fill this in soon ... :-)</p>"
457+
subscribe_for: "Subscribe for"
458+
original_price_per_month: "6€ / month"
459+
price_per_month: "4€ / month"
460+
461+
membership_payment_cancelled:
462+
meta:
463+
title: "Payment Cancelled"
464+
description: ""
465+
title: "Payment Cancelled"
466+
text: "The payment process was interrupted, most likely by you :-)."
467+
membership_link: "Back to membership overview"
468+
payment_link: "Retry payment"

0 commit comments

Comments
 (0)