Skip to content

Commit

Permalink
[BDE] create donation form page (#10150)
Browse files Browse the repository at this point in the history
  • Loading branch information
ottaviano committed Apr 27, 2024
1 parent f461a1c commit 3b5e7b6
Show file tree
Hide file tree
Showing 16 changed files with 500 additions and 22 deletions.
4 changes: 4 additions & 0 deletions .env
Expand Up @@ -154,3 +154,7 @@ CAPTAIN_VERIFY_API_KEY=
NATIONAL_EVENT_WEBHOOK_HOST=http://webhook.renaissance.code
NATIONAL_EVENT_TICKET_HOST=http://webhook.renaissance.code
NATIONAL_EVENT_TICKET_API_KEY=123

SYSTEMPAY_KEY=123
SYSTEMPAY_SITE_ID=123
SYSTEMPAY_MODE=TEST
7 changes: 3 additions & 4 deletions assets/pages/donation_funnel/components/FirstFormStep.js
Expand Up @@ -19,8 +19,7 @@ function closest(num, arr) {
* First Step component for funnel
* @returns {AlpineComponent}
*/
const FirstForm = () => {
const uniqAmounts = [30, 60, 120, 250, 500, 1000];
const FirstForm = ({ amounts: uniqAmounts = [30, 60, 120, 250, 500, 1000] } = {}) => {
const monthlyAmounts = [5, 10, 20, 30, 60, 100];
return ({
...CommonFormStep(),
Expand All @@ -32,7 +31,7 @@ const FirstForm = () => {
defaultCustomAmount: '',

getTaxTextReduction() {
return `${(this.amount * 0.34).toFixed(2)}${'-1' === this.duration ? '/ mois' : ''}`;
return `${(this.amount * 0.34).toFixed(2).toLocaleString()}${'-1' === this.duration ? '/ mois' : ''}`;
},

handleAmountClick(amount) {
Expand Down Expand Up @@ -78,7 +77,7 @@ const FirstForm = () => {
this.defaultCustomAmount = this.getCustomAmount();
},

getAmounts(duration) {
getAmounts() {
return '0' === this.duration ? uniqAmounts : monthlyAmounts;
},
getCustomAmount() {
Expand Down
2 changes: 1 addition & 1 deletion assets/pages/donation_funnel/components/Page.js
Expand Up @@ -20,7 +20,7 @@ const Page = (props) => ({
},
disableDispatchToStepper: false,
amount: props.amount,
duration: props.duration,
duration: props.duration ?? '0',
localDestination: props.localDestination,

handleStepperChange(step) {
Expand Down
3 changes: 1 addition & 2 deletions assets/style/components/_bde-theme.scss
Expand Up @@ -17,7 +17,7 @@ $tertiary: #959CA0;
}
}

a {
a:not(.re-button) {
color: $secondary !important;
text-decoration: none;
transition: all 0.2s ease-in-out;
Expand Down Expand Up @@ -48,7 +48,6 @@ $tertiary: #959CA0;
.re-linked-toggle .re-linked-toggle-content {
input {
&[type=radio] {

&:checked + label {
@apply bg-white border-black text-black z-10;
border-width: 2px;
Expand Down
3 changes: 3 additions & 0 deletions config/services.yaml
Expand Up @@ -78,6 +78,9 @@ services:
$openAIApiKey: '%env(OPENAI_API_KEY)%'
$friendlyCaptchaEuropeSiteKey: '%env(FRIENDLY_CAPTCHA_EUROPE_SITE_KEY)%'
$captainVerifyApiKey: '%env(CAPTAIN_VERIFY_API_KEY)%'
$systemPayMode: '%env(SYSTEMPAY_MODE)%'
$systemPaySiteId: '%env(SYSTEMPAY_SITE_ID)%'
$systemPayKey: '%env(SYSTEMPAY_KEY)%'

_instanceof:
App\Adherent\Unregistration\Handlers\UnregistrationAdherentHandlerInterface:
Expand Down
89 changes: 89 additions & 0 deletions src/BesoinDEurope/Donation/DonationRequest.php
@@ -0,0 +1,89 @@
<?php

namespace App\BesoinDEurope\Donation;

use App\Address\Address;
use App\Entity\Adherent;
use App\Recaptcha\RecaptchaChallengeInterface;
use App\Recaptcha\RecaptchaChallengeTrait;
use App\Validator\Recaptcha as AssertRecaptcha;
use App\Validator\StrictEmail;
use Symfony\Component\Validator\Constraints as Assert;

/**
* @AssertRecaptcha(api="friendly_captcha")
*/
class DonationRequest implements RecaptchaChallengeInterface
{
use RecaptchaChallengeTrait;

/**
* @Assert\NotBlank
* @Assert\Range(min=10, max=4600)
*/
public ?int $amount = null;

/**
* @Assert\NotBlank
* @StrictEmail(dnsCheck=false)
*/
public ?string $email = null;

/**
* @Assert\NotBlank
* @Assert\Choice(callback={"App\ValueObject\Genders", "all"}, message="common.invalid_choice")
*/
public ?string $civility = null;

/**
* @Assert\NotBlank
* @Assert\Length(
* allowEmptyString=true,
* min=2,
* max=50,
* minMessage="common.first_name.min_length",
* maxMessage="common.first_name.max_length"
* )
*/
public ?string $firstName = null;

/**
* @Assert\NotBlank
* @Assert\Length(
* allowEmptyString=true,
* min=1,
* max=50,
* minMessage="common.last_name.min_length",
* maxMessage="common.last_name.max_length"
* )
*/
public ?string $lastName = null;

/**
* @Assert\Valid
*/
public ?Address $address = null;

/**
* @Assert\Country
*/
public ?string $nationality = null;

public ?string $utmSource = null;
public ?string $utmCampaign = null;

public function updateFromAdherent(Adherent $user): void
{
$this->email = $user->getEmailAddress();
$this->firstName = $user->getFirstName();
$this->lastName = $user->getLastName();
$this->civility = $user->getGender();
$this->nationality = $user->getNationality();
$this->address = Address::createFromAddress($user->getPostAddress());
}

public function hasAmount(): bool
{
return $this->amount >= 10;
}
}
70 changes: 70 additions & 0 deletions src/Controller/BesoinDEurope/DonationController.php
@@ -0,0 +1,70 @@
<?php

namespace App\Controller\BesoinDEurope;

use App\BesoinDEurope\Donation\DonationRequest;
use App\Donation\Systempay\RequestParamsBuilder;
use App\Form\BesoinDEurope\DonationRequestType;
use App\Security\Http\Session\AnonymousFollowerSession;
use App\Utils\UtmParams;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;

#[Route(path: '/don', name: 'app_bde_donation', methods: ['GET', 'POST'])]
class DonationController extends AbstractController
{
public function __construct(
private readonly CsrfTokenManagerInterface $csrfTokenManager,
private readonly RequestParamsBuilder $requestParamsBuilder,
private readonly AnonymousFollowerSession $anonymousFollowerSession,
) {
}

public function __invoke(Request $request): Response
{
if ($response = $this->anonymousFollowerSession->start($request)) {
return $response;
}

$donationRequest = $this->getDonationRequest($request);

$form = $this
->createForm(DonationRequestType::class, $donationRequest)
->handleRequest($request)
;

if ($form->isSubmitted() && $form->isValid()) {
return $this->render('besoindeurope/donation/payment.html.twig', [
'params' => $this->requestParamsBuilder->build($donationRequest),
]);
}

return $this->renderForm('besoindeurope/donation/form.html.twig', [
'form' => $form,
'email_validation_token' => $this->csrfTokenManager->getToken('email_validation_token'),
'step' => $donationRequest->hasAmount() ? 1 : 0,
]);
}

private function getDonationRequest(Request $request): DonationRequest
{
$donationRequest = new DonationRequest();

if ($user = $this->getUser()) {
$donationRequest->updateFromAdherent($user);
}

$donationRequest->email = $request->query->get('email');
$donationRequest->amount = $request->query->getInt('amount');

if ($request->query->has(UtmParams::UTM_SOURCE)) {
$donationRequest->utmSource = UtmParams::filterUtmParameter($request->query->get(UtmParams::UTM_SOURCE));
$donationRequest->utmCampaign = UtmParams::filterUtmParameter($request->query->get(UtmParams::UTM_CAMPAIGN));
}

return $donationRequest;
}
}
Expand Up @@ -76,7 +76,7 @@ private function getDonationRequest(Request $request, ?Adherent $currentUser): D
$isSubscription ? DonationRequest::MIN_AMOUNT : DonationRequest::MIN_AMOUNT_SUBSCRIPTION
);

$localDestination = $request->query->getBoolean('localDestination', false);
$localDestination = $request->query->getBoolean('localDestination');

$donationRequest = $currentUser
? DonationRequest::createFromAdherent($currentUser, $clientIp, $amount)
Expand Down
59 changes: 59 additions & 0 deletions src/Donation/Systempay/RequestParamsBuilder.php
@@ -0,0 +1,59 @@
<?php

namespace App\Donation\Systempay;

use App\BesoinDEurope\Donation\DonationRequest;
use App\ValueObject\Genders;

class RequestParamsBuilder
{
public function __construct(
private readonly string $systemPaySiteId,
private readonly string $systemPayMode,
private readonly string $systemPayKey,
) {
}

public function build(DonationRequest $donationRequest): array
{
$params = [
'vads_site_id' => $this->systemPaySiteId,
'vads_ctx_mode' => $this->systemPayMode,
'vads_trans_id' => sprintf("%'.06d", (strtotime('tomorrow') - time()) * 10 + ((microtime(true) * 10) % 10)),
'vads_trans_date' => date('YmdHis'),
'vads_amount' => $donationRequest->amount * 100,
'vads_currency' => '978',
'vads_action_mode' => 'INTERACTIVE',
'vads_page_action' => 'PAYMENT',
'vads_version' => 'V2',
'vads_payment_config' => 'SINGLE',
'vads_capture_delay' => '0',
'vads_validation_mode' => '0',
'vads_cust_title' => match ($donationRequest->civility) {
Genders::MALE => 'Monsieur',
Genders::FEMALE => 'Madame',
default => '',
},
'vads_cust_first_name' => $donationRequest->firstName,
'vads_cust_last_name' => $donationRequest->lastName,
'vads_cust_email' => $donationRequest->email,
'vads_cust_address' => $donationRequest->address->getAddress(),
'vads_cust_address2' => $donationRequest->address->getAdditionalAddress(),
'vads_cust_zip' => $donationRequest->address->getPostalCode(),
'vads_cust_city' => $donationRequest->address->getCityName(),
'vads_cust_country' => $donationRequest->address->getCountry(),
'vads_ext_info_nationalite' => $donationRequest->nationality,
'vads_ext_info_utm_source' => $donationRequest->utmSource,
'vads_ext_info_utm_campagne' => $donationRequest->utmCampaign,
];
ksort($params);
$data = implode('+', $params);
$data .= '+'.$this->systemPayKey;

$signData = base64_encode(hash_hmac('sha256', $data, $this->systemPayKey, true));

$params['signature'] = $signData;

return $params;
}
}
36 changes: 36 additions & 0 deletions src/Form/BesoinDEurope/DonationRequestType.php
@@ -0,0 +1,36 @@
<?php

namespace App\Form\BesoinDEurope;

use App\Address\AddressInterface;
use App\Form\AutocompleteAddressType;
use App\Form\CivilityType;
use App\Form\RequiredCheckboxType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CountryType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

class DonationRequestType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('amount', HiddenType::class)
->add('email', EmailType::class)
->add('civility', CivilityType::class)
->add('firstName', TextType::class)
->add('lastName', TextType::class)
->add('nationality', CountryType::class, ['preferred_choices' => [AddressInterface::FRANCE]])
->add('address', AutocompleteAddressType::class, ['with_additional_address' => true])
->add('autorisations', RequiredCheckboxType::class)
;
}

public function getBlockPrefix(): string
{
return 'donation_request';
}
}
17 changes: 6 additions & 11 deletions src/Twig/AnonymousRuntime.php
Expand Up @@ -9,20 +9,15 @@

class AnonymousRuntime implements RuntimeExtensionInterface
{
private const USER_LOGIN_ROUTE = 'app_renaissance_login';

private $urlGenerator;
private $requestStack;

public function __construct(UrlGeneratorInterface $urlGenerator, RequestStack $requestStack)
{
$this->urlGenerator = $urlGenerator;
$this->requestStack = $requestStack;
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
private readonly RequestStack $requestStack,
) {
}

public function generateLoginPathForAnonymousFollower(string $callbackRoute = '', array $params = []): string
{
return $this->doGeneratePathForAnonymousFollower(self::USER_LOGIN_ROUTE, $callbackRoute, $params);
return $this->doGeneratePathForAnonymousFollower('/connexion', $callbackRoute, $params);
}

private function doGeneratePathForAnonymousFollower(
Expand All @@ -38,7 +33,7 @@ private function doGeneratePathForAnonymousFollower(
$params = $this->requestStack->getMainRequest()->attributes->get('_route_params');
}

$params[AnonymousFollowerSession::AUTHENTICATION_INTENTION] = $this->urlGenerator->generate($intention);
$params[AnonymousFollowerSession::AUTHENTICATION_INTENTION] = $intention;

return $this->urlGenerator->generate($callbackRoute, $params);
}
Expand Down

0 comments on commit 3b5e7b6

Please sign in to comment.