Symfony 5 stripe v3 can't find CHECKOUT_SESSION_ID - symfony

I'm trying to handle monthly subscription with stripe :
I create a Payment Controller with actions :
create-checkout-session Action :
/**
* #Route ("/create-checkout-session", name="checkout")
*/
public function checkout(Request $request)
{
$data = json_decode($request->getContent());
\Stripe\Stripe::setApiKey('sk_test_...');
try {
$checkout_session = \Stripe\Checkout\Session::create([
'payment_method_types' => ['card'],
'line_items' => [[
'price' => $data->priceId,
// For metered billing, do not pass quantity
'quantity' => 1,
]],
'mode' => 'subscription',
'success_url' => $this->generateUrl('success', ["session_id" => "{CHECKOUT_SESSION_ID}"], UrlGeneratorInterface::ABSOLUTE_URL),
'cancel_url' => $this->generateUrl('error', [], UrlGeneratorInterface::ABSOLUTE_URL),
]);
} catch (\Exception $e) {
return new JsonResponse(["error" => ['message' => $e->getMessage()]]);
}
return new JsonResponse(["sessionId" => $checkout_session['id']]);
}
success Action :
/**
* #Route(
* "/{_locale}/success/{session_id}",
* name="success",
* defaults={"_locale"="en"},
* requirements={"_locale"="en|fr"}
* )
* #param Request $request
* #return Response
*/
public function success(Request $request): Response
{
$locale = $request->getLocale();
// Get current user
$user = $this->getUser();
\Stripe\Stripe::setApiKey('sk_test_...');
$session = \Stripe\Checkout\Session::retrieve($request->get('session_id'));
$customer = \Stripe\Customer::retrieve($session->customer);
return $this->render('payment/success.html.twig', [
'current_language' => $locale,
'user' => $user,
'session' => $session,
'customer' => $customer,
]);
}
And i created a script :
// Create an instance of the Stripe object with your publishable API key
const stripe = Stripe('pk_test_..');
const createCheckoutSession = function (priceId) {
return fetch("/create-checkout-session", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
priceId: priceId
})
}).then(function (result) {
return result.json();
});
};
$(".subscription-btn").click(function (e) {
e.preventDefault();
let PRICE_ID = $(this).data('priceCode');
console.log(PRICE_ID);
createCheckoutSession(PRICE_ID).then(function (data) {
// Call Stripe.js method to redirect to the new Checkout page
stripe
.redirectToCheckout({
sessionId: data.sessionId
})
.then(function (result) {
// If `redirectToCheckout` fails due to a browser or network
// error, you should display the localized error message to your
// customer using `error.message`.
if (result.error) {
alert(result.error.message);
}
})
});
});
The payment work but i got this eror after redirect to success
Invalid checkout.session id: {CHECKOUT_SESSION_ID}
How can i fix this error and how can i handle monthly payment inside my project ?

I had the same problem. I just add :
$this->router->generate('my_confirm_route', ['myrouteparam' => $myRouteParam], UrlGeneratorInterface::ABSOLUTE_URL).'?session_id={CHECKOUT_SESSION_ID}';
And in controller :
$request->query->get('session_id')

Leroy is correct, but I'd suggest you confirm that the success_url you're generating actually looks the way it's supposed to - i.e. isn't having those {}s urlencoded - as that might be what's going on here.
Beyond that I believe your approach is correct, and Leroy's is not - I think you're not providing Stripe with an unencoded URL so it's not recognizing {CHECKOUT_SESSION_ID} as a template string as it should be.

I was able to fix the problem by simply decoding the url before sending it.
urldecode($this->generateUrl('stripe-checkout-success', ['session' => '{CHECKOUT_SESSION_ID}'], UrlGeneratorInterface::ABSOLUTE_URL))

You pass that exact value to your PSP as success_url.
'success_url' => $this->generateUrl('success', ["session_id" => "{CHECKOUT_SESSION_ID}"], UrlGeneratorInterface::ABSOLUTE_URL),
Then you are getting back that redirect url, as literal : /en/success/{CHECKOUT_SESSION_ID}
Next you lookup that exact value {CHECKOUT_SESSION_ID} from some api.
$session = \Stripe\Checkout\Session::retrieve($request->get('session_id'));
I'm not sure, but I think you need to fetch the session id from the success_url's body instead of the the $request object.
I also think you can skip the session_id in the success_url, because that's the users's callback url. While the user is being redirected back to your application, the user is still logged in, You don't want to send your session id to your PSP.
I think your feedback is in the post body, based on what you've configured at stripe. Doing something like this should give you what you want.
First of all, remove the session id from the success_url
'success_url' => $this->generateUrl('success', UrlGeneratorInterface::ABSOLUTE_URL),
Next, change your success url logic to use the post body.
/**
* #Route(
* "/{_locale}/success",
* name="success",
* defaults={"_locale"="en"},
* requirements={"_locale"="en|fr"}
* )
* #param Request $request
* #return Response
*/
public function success(Request $request): Response
{
...
$body = $request->getContent();
$feedback = json_decode($body, true);
$session = \Stripe\Checkout\Session::retrieve($feedback['id']);
...
}
PS, bare in mind that I haven't read the documentation about Stripe. But I strongly believe that they send you their session id (which you created) back in some form.

Related

Symfony 5 Reset Password how do i get the right url?

I'll try to explain my problem.
I have recently started a new projet and wanted to implement a reset password functionnality.
Everything seems to work except the generation of the url which is send by email.
picture of my URL
My URL should look like this : http://localhost/projectName/public/reset-password/reset/xOdfPc0iGC7nmReqX02jcemgX4EIlt2tb5vNYgTZ
But the "/projectName/public/" is missing.
I don't understand what i did wrong.
Here is my twig template for the email :
<h1>Bonjour !</h1>
<p>Pour réinitialiser votre mot de passe, merci de vous rendre sur le lien suivant</p>
{{ url('app_reset_password', { token: resetToken.token }) }}
<p>Ce lien expirera dans {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.</p>
<p>A bientôt !</p>
Here is the function in the controller that generates the templated email :
private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer, TranslatorInterface $translator): RedirectResponse
{
$user = $this->entityManager->getRepository(User::class)->findOneBy([
'email' => $emailFormData,
]);
// Do not reveal whether a user account was found or not.
if (!$user) {
return $this->redirectToRoute('app_check_email');
}
try {
$resetToken = $this->resetPasswordHelper->generateResetToken($user);
} catch (ResetPasswordExceptionInterface $e) {
// If you want to tell the user why a reset email was not sent, uncomment
// the lines below and change the redirect to 'app_forgot_password_request'.
// Caution: This may reveal if a user is registered or not.
//
// $this->addFlash('reset_password_error', sprintf(
// '%s - %s',
// $translator->trans(ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE, [], 'ResetPasswordBundle'),
// $translator->trans($e->getReason(), [], 'ResetPasswordBundle')
// ));
return $this->redirectToRoute('app_check_email');
}
$email = (new TemplatedEmail())
->from(new Address('assistance#asintel.com', 'AS Intel - Assistance'))
->to($user->getEmail())
->subject('Your password reset request')
->htmlTemplate('reset_password/email.html.twig')
->context([
'resetToken' => $resetToken,
])
;
$mailer->send($email);
// Store the token object in session for retrieval in check-email route.
$this->setTokenObjectInSession($resetToken);
return $this->redirectToRoute('app_check_email');
}
And this is the function with app_reset_password route :
/**
* Validates and process the reset URL that the user clicked in their email.
*
* #Route("/reset/{token}", name="app_reset_password")
*/
public function reset(Request $request, UserPasswordHasherInterface $userPasswordHasher, TranslatorInterface $translator, string $token = null): Response
{
if ($token) {
// We store the token in session and remove it from the URL, to avoid the URL being
// loaded in a browser and potentially leaking the token to 3rd party JavaScript.
$this->storeTokenInSession($token);
return $this->redirectToRoute('app_reset_password');
}
$token = $this->getTokenFromSession();
if (null === $token) {
throw $this->createNotFoundException('No reset password token found in the URL or in the session.');
}
try {
$user = $this->resetPasswordHelper->validateTokenAndFetchUser($token);
} catch (ResetPasswordExceptionInterface $e) {
$this->addFlash('reset_password_error', sprintf(
'%s - %s',
$translator->trans(ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE, [], 'ResetPasswordBundle'),
$translator->trans($e->getReason(), [], 'ResetPasswordBundle')
));
return $this->redirectToRoute('app_forgot_password_request');
}
// The token is valid; allow the user to change their password.
$form = $this->createForm(ChangePasswordFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// A password reset token should be used only once, remove it.
$this->resetPasswordHelper->removeResetRequest($token);
// Encode(hash) the plain password, and set it.
$encodedPassword = $userPasswordHasher->hashPassword(
$user,
$form->get('plainPassword')->getData()
);
$user->setPassword($encodedPassword);
$this->entityManager->flush();
// The session is cleaned up after the password has been changed.
$this->cleanSessionAfterReset();
return $this->redirectToRoute('main_index');
}
return $this->render('reset_password/reset.html.twig', [
'resetForm' => $form->createView(),
]);
}
Does someone have an idea what i should do to fix this problem ?
Thanks a lot

Symfony, route conflicts between controller and bundle

For example simple controller:
/**
* #Route("/{identifier}", name="page")
*/
public function page(Request $request, string $identifier)
{
$page = $this->pageRepository->findOneBy(['identifier' => $identifier]);
if (!$page || !$page->getEnabled()) {
throw $this->createNotFoundException();
}
return $this->render('cms/index.html.twig', []);
}
And a have a bundle to manage images from admin page elfinder, which will enter the /elfinder link.
And instead of getting the bundle controller, my controller gets.
/{identifier} === /elfinder
How do people usually act in such situations?
I tried to set different priority, but it does not help
Try adding your controllers with the priority required in the annotations.yaml file. Thus, if you get a 404 in the first one, Symfony will try to open the route from the next controller
Add your controllers to config/routes/annotations.yaml
page:
resource: App\Controller\_YourFistController_
type: annotation
elfinder:
resource: FM\ElfinderBundle\Controller\ElFinderController
type: annotation
Or if this option does not suit you, then you can try the optional parameter priority. symfony doc
Add to config file config/routes.yaml:
elfinder:
path: /elfinder/{instance}/{homeFolder}
priority: 2
controller: FM\ElfinderBundle\Controller\ElFinderController::show
I tried to set the priority through the configuration file. But unfortunately it didn't work.
The only thing that helped was to create your own methods that will redirect
/**
* #Route("/elfinder", name="elfinder", priority=10)
*/
public function elfinder()
{
return $this->forward('FM\ElfinderBundle\Controller\ElFinderController::show', [
'homeFolder' => '',
'instance' => 'default',
]);
}
/**
* #Route("/efconnect", name="efconnect", priority=11)
*/
public function elfinderLoad(Request $request, SessionInterface $session, EventDispatcherInterface $dispatcher)
{
return $this->forward('FM\ElfinderBundle\Controller\ElFinderController::load', [
'session' => $session,
'eventDispatcher' => $dispatcher,
'request' => $request,
'homeFolder' => '',
'instance' => 'default',
]);
}

Sending Guzzle request to itemize api

The situation:
I need to send the request to the api to update the account info.
The API docs say I need to do send a PUT request to the API.
I trying to do this in Laravel 5.6, although I don't think this matters.
What I have so far:
A working constructor for the Guzzle client;
A working function to retrieve account info.
What is not working:
Upon submitting the request I get a Guzzle exception
Client error: \`PUT https://sandbox.proapi.itemize.com/api/enterprise/v1/accounts/<my account id>\` resulted in a \`400 Bad Request\` response: IOException:
This is the code I have so far:
<?php
namespace App\Http\Controllers;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
class ApiController extends Controller {
private $apiKey;
private $uri;
private $client;
public function __construct() {
$this->apiKey = 'my api key';
$this->uri = 'https://sandbox.proapi.itemize.com/api/enterprise/v1/accounts/my account id';
$this->client = new Client([
'base_uri' => $this->uri,
'auth' => [null, $this->apiKey]]);
}
public function accountInfo() {
$response = $this->client->request('GET','');
echo $response->getBody()->getContents();
}
public function updateAccountInfo() {
$response = $this->client->request('PUT','',[
'headers' => [
'Content-Type' => 'application/json',
],
'body' => '{"markets":"UK"}'
]);
echo $response->getBody()->getContents();
}
}
?>
400 Bad Request means: request sent by the client due to invalid syntax.
According itemize api documentation "markets" should be passed as an array of strings. And you can also use json format.
Try this:
public function updateAccountInfo() {
$response = $this->client->request('PUT', '', [
'json' => ["markets" => ["UK"]],
]);
echo $response->getBody()->getContents();
}

Laravel Testing login Credential using phpunit + multiprocess

I'm pretty new on unit testing and I want to try to test my login page
my Goal for this unit are :
-> if it match in database -> redirect to route '/'
-> if not -> redirect to route '/login'
<?php
namespace Tests\Feature;
use App\Domain\Core\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Session;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class userTest extends TestCase
{
use DatabaseMigrations;
/**
* A basic test example.
*
* #return void
*/
public function testLoginTrue()
{
$credential = [
'email' => 'user#ad.com',
'password' => 'user'
];
$this->post('login',$credential)->assertRedirect('/');
}
public function testLoginFalse()
{
$credential = [
'email' => 'user#ad.com',
'password' => 'usera'
];
$this->post('login',$credential)->assertRedirect('/login');
}
}
when I test on TestLoginTrue , its successfully return to '/' But when i try the TestLoginFalse ... it return same like TestLoginTrue, it should be stayed on '/login' route,
Any Idea?
Plus I want to try to check if when I already login I couldn't access the login page so my initial idea is :
public function testLoginTrue()
{
$credential = [
'email' => 'user#ad.com',
'password' => 'user'
];
$this->post('login',$credential)
->assertRedirect('/')
->get('/login')
->assertRedirect('/');
}
but... it returns
1) Tests\Feature\userTest::testLoginTrue BadMethodCallException:
Method [get] does not exist on Redirect.
So how to do it correctly?
Thanks in advance
I am also a bit stuck with Laravel 5.4 testing follow redirects case.
As a workaround, you may check $response->assertSessionHasErrors(). This way it should work:
public function testLoginFalse()
{
$credential = [
'email' => 'user#ad.com',
'password' => 'incorrectpass'
];
$response = $this->post('login',$credential);
$response->assertSessionHasErrors();
}
Also, in testLoginTrue() you may check, that session missing errors:
$response = $this->post('login',$credential);
$response->assertSessionMissing('errors');
Hope this helps!

Batch requests on Symfony

I am trying to reproduce the behaviour of the facebook batch requests function on their graph api.
So I think that the easiest solution is to make several requests on a controller to my application like:
public function batchAction (Request $request)
{
$requests = $request->all();
$responses = [];
foreach ($requests as $req) {
$response = $this->get('some_http_client')
->request($req['method'],$req['relative_url'],$req['options']);
$responses[] = [
'method' => $req['method'],
'url' => $req['url'],
'code' => $response->getCode(),
'headers' => $response->getHeaders(),
'body' => $response->getContent()
]
}
return new JsonResponse($responses)
}
So with this solution, I think that my functional tests would be green.
However, I fill like initializing the service container X times might make the application much slower. Because for each request, every bundle is built, the service container is rebuilt each time etc...
Do you see any other solution for my problem?
In other words, do I need to make complete new HTTP requests to my server to get responses from other controllers in my application?
Thank you in advance for your advices!
Internally Symfony handle a Request with the http_kernel component. So you can simulate a Request for every batch action you want to execute and then pass it to the http_kernel component and then elaborate the result.
Consider this Example controller:
/**
* #Route("/batchAction", name="batchAction")
*/
public function batchAction()
{
// Simulate a batch request of existing route
$requests = [
[
'method' => 'GET',
'relative_url' => '/b',
'options' => 'a=b&cd',
],
[
'method' => 'GET',
'relative_url' => '/c',
'options' => 'a=b&cd',
],
];
$kernel = $this->get('http_kernel');
$responses = [];
foreach($requests as $aRequest){
// Construct a query params. Is only an example i don't know your input
$options=[];
parse_str($aRequest['options'], $options);
// Construct a new request object for each batch request
$req = Request::create(
$aRequest['relative_url'],
$aRequest['method'],
$options
);
// process the request
// TODO handle exception
$response = $kernel->handle($req);
$responses[] = [
'method' => $aRequest['method'],
'url' => $aRequest['relative_url'],
'code' => $response->getStatusCode(),
'headers' => $response->headers,
'body' => $response->getContent()
];
}
return new JsonResponse($responses);
}
With the following controller method:
/**
* #Route("/a", name="route_a_")
*/
public function aAction(Request $request)
{
return new Response('A');
}
/**
* #Route("/b", name="route_b_")
*/
public function bAction(Request $request)
{
return new Response('B');
}
/**
* #Route("/c", name="route_c_")
*/
public function cAction(Request $request)
{
return new Response('C');
}
The output of the request will be:
[
{"method":"GET","url":"\/b","code":200,"headers":{},"body":"B"},
{"method":"GET","url":"\/c","code":200,"headers":{},"body":"C"}
]
PS: I hope that I have correctly understand what you need.
There are ways to optimise test-speed, both with PHPunit configuration (for example, xdebug config, or running the tests with the phpdbg SAPI instead of including the Xdebug module into the usual PHP instance).
Because the code will always be running the AppKernel class, you can also put some optimisations in there for specific environments - including initiali[zs]ing the container less often during a test.
I'm using one such example by Kris Wallsmith. Here is his sample code.
class AppKernel extends Kernel
{
// ... registerBundles() etc
// In dev & test, you can also set the cache/log directories
// with getCacheDir() & getLogDir() to a ramdrive (/tmpfs).
// particularly useful when running in VirtualBox
protected function initializeContainer()
{
static $first = true;
if ('test' !== $this->getEnvironment()) {
parent::initializeContainer();
return;
}
$debug = $this->debug;
if (!$first) {
// disable debug mode on all but the first initialization
$this->debug = false;
}
// will not work with --process-isolation
$first = false;
try {
parent::initializeContainer();
} catch (\Exception $e) {
$this->debug = $debug;
throw $e;
}
$this->debug = $debug;
}

Resources