I'm sure I'm missing something obvious here, but I can't get my head around how to check for an existing card against a customer.
I'm using the stripe connect api within an laravel app to manage payments on behalf of others, and the basic process is as follows:
a stripe token is created via stripe.js and submitted with the payment form
if the customer exists in the local database, I grab their stripe_id, otherwise a new customer is created using the token as the source/card
a charge is then created using the retrieved or new customer stripe_id
Currently, if the customer returns and uses a different card, as the charge only includes a customer, not source, it'll be charged against their default card regardless.
What I'd like to do is:
create a stripe token
check customer against local database etc
check card fingerprint against customer's cards
if necessary, create new card on customer's record
create charge using both customer and card ids
Simply put: I can't see where in the process a persistent card_id is generated; both those used in the stripe.js response, and when created in the stripe dashboard, appear to be unique, meaning every charge creates a brand-new card object in stripe.
I know I can retrieve a list of cards stored against a customer's account - but where do I get the initial card_id from to search against?
I've seen a question that touches on this here - Can I check whether stripe a card is already existed before going to create new one? - but I don't know Ruby, so can't make head nor tail of it.
EDIT:
Simpler version - is there a way to get the fingerprint as described in the stripe docs here - https://stripe.com/docs/api/php#card_object - without having to first create a card object ?
So the idea here would be to use the fingerprint on the Card object or the Token object and not the id itself as those would be different if you add the same card multiple times.
When you get a new card token you can retrieve it through the Retrieve Token API and look for the fingerprint in the card hash.
You would keep a list of known fingerprints in your database associated with a specific customer and/or card so that you can detect duplicate cards.
NOTE: make sure you are using the secret keys to get those information. otherwise if you are using the publishable key, you might not get the fingerprint value.
I created a function to do this:
$customer is the stripe customer object
$stripe_account is either your account's stripe ID or the connected account's stripe ID
$token comes from stripe.js elements
$check_exp allows you to decide if you want to check the card's expiration date as well, because the fingerprint does not change if the card's number is the same
stripe PHP API 7.0.0
function check_duplicate_card($customer, $stripe_account, $token, $check_exp) {
$loc = "check_duplicate_card >> ";
$debug = true;
if ($debug) {
// see here for an explanation for logging: http://php.net/set_error_handler >> Examples
trigger_error("$loc started", E_USER_NOTICE);
}
try
{
// get token data
$response = \Stripe\Token::retrieve(
$token,
["stripe_account" => $stripe_account]
);
$token_fingerprint = $response->card->fingerprint;
$token_exp_month = $response->card->exp_month;
$token_exp_year = $response->card->exp_year;
if ($debug) {
trigger_error("$loc token_fingerprint = $token_fingerprint; token_exp_month = $token_exp_month; token_exp_year = $token_exp_year", E_USER_NOTICE);
}
// check for duplicate source
if ($debug) {
trigger_error("$loc customer sources = " . json_encode($customer->sources), E_USER_NOTICE);
}
$duplicate_found = false;
foreach ($customer->sources->data as &$value) {
// get data
$fingerprint = $value->fingerprint;
$exp_month = $value->exp_month;
$exp_year = $value->exp_year;
if ($fingerprint == $token_fingerprint) {
if ($check_exp) {
if (($exp_month == $token_exp_month) && ($exp_year == $token_exp_year)) {
$duplicate_found = true;
break;
}
} else {
$duplicate_found = true;
break;
}
}
}
if ($debug) {
trigger_error("$loc duplicate_found = " . json_encode($duplicate_found), E_USER_NOTICE);
}
} catch (Exception $e) {
if ($e instanceof \Stripe\Exception\ApiErrorException) {
$return_array = [
"status" => $e->getHttpStatus(),
"type" => $e->getError()->type,
"code" => $e->getError()->code,
"param" => $e->getError()->param,
"message" => $e->getError()->message,
];
$return_str = json_encode($return_array);
trigger_error("$loc $return_str", E_USER_WARNING);
http_response_code($e->getHttpStatus());
echo $return_str;
} else {
$return_array = [
"message" => $e->getMessage(),
];
$return_str = json_encode($return_array);
trigger_error("$loc $return_str", E_USER_ERROR);
http_response_code(500); // Internal Server Error
echo $return_str;
}
}
if ($debug) {
trigger_error("$loc ended", E_USER_NOTICE);
}
return $duplicate_found;
}
Related
I've finally managed to implement the new Stripe Payment Element in Laravel via the Payment Intents API. However, I now need to capture information about the payments and store them in my database - specifically, I need the following data:
Transaction ID
Payment status (failed/pending/successful, etc.)
Payment method type (card/Google Pay/Apple Pay/etc.)
The amount actually charged to the customer
The currency the customer actually paid in
The postcode entered by the user in the payment form
All of this information seems to be available in the Payment Intent object but none of the several Stripe guides specify how to capture them on the server. I want to avoid using webhooks because they seem like overkill for grabbing and persisting data that I'm already retrieving.
It also doesn't help that, thanks to how the Stripe documentation's AJAX/PHP solution is set up, trying to dump and die any variables on the server-side causes the entire client-side flow to break, stopping the payment form from rendering and blocking any debugging information. Essentially, this makes the entire implementation of Payment Intents API impossible to debug on the server.
Does anyone who's been here before know how I would go about capturing this information?
Relevant portion of JavaScript/AJAX:
const stripe = Stripe(<TEST_PUBLISHABLE_KEY>);
const fonts = [
{
cssSrc:
"https://fonts.googleapis.com/css2?family=Open+Sans:wght#300;400;500;600;700&display=swap",
},
];
const appearance = {
theme: "stripe",
labels: "floating",
variables: {
colorText: "#2c2c2c",
fontFamily: "Open Sans, Segoe UI, sans-serif",
borderRadius: "4px",
},
};
let elements;
initialize();
checkStatus();
document
.querySelector("#payment-form")
.addEventListener("submit", handleSubmit);
// Fetches a payment intent and captures the client secret
async function initialize() {
const { clientSecret } = await fetch("/payment/stripe", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": document.querySelector('input[name="_token"]').value,
},
}).then((r) => r.json());
elements = stripe.elements({ fonts, appearance, clientSecret });
const paymentElement = elements.create("payment");
paymentElement.mount("#payment-element");
}
async function handleSubmit(e) {
e.preventDefault();
setLoading(true);
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
// Make sure to change this to your payment completion page
return_url: "http://localhost.rc/success"
},
});
if (error.type === "card_error" || error.type === "validation_error") {
showMessage(error.message);
} else {
showMessage("An unexpected error occured.");
}
setLoading(false);
}
// Fetches the payment intent status after payment submission
async function checkStatus() {
const clientSecret = new URLSearchParams(window.location.search).get(
"payment_intent_client_secret"
);
if (!clientSecret) {
return;
}
const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
switch (paymentIntent.status) {
case "succeeded":
showMessage("Payment succeeded!");
break;
case "processing":
showMessage("Your payment is processing.");
break;
case "requires_payment_method":
showMessage("Your payment was not successful, please try again.");
break;
default:
showMessage("Something went wrong.");
break;
}
}
Routes file:
Route::post('/payment/stripe', [TransactionController::class, "stripe"]);
TransactionController:
public function stripe(Request $request) {
Stripe\Stripe::setApiKey(env(<TEST_SECRET_KEY>));
header('Content-Type: application/json');
try {
$paymentIntent = Stripe\PaymentIntent::create([
'amount' => 2.99,
'currency' => 'gbp',
'automatic_payment_methods' => [
'enabled' => true,
],
]);
$output = [
'clientSecret' => $paymentIntent->client_secret,
];
$this->storeStripe($paymentIntent, $output);
echo json_encode($output);
} catch (Stripe\Exception\CardException $e) {
echo 'Error code is:' . $e->getError()->code;
$paymentIntentId = $e->getError()->payment_intent->id;
$paymentIntent = Stripe\PaymentIntent::retrieve($paymentIntentId);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
}
How can I capture the above information from the Payment Intent to store in my database?
I know you're not going to like this but I'll say it anyway. I honestly think implementing a webhook endpoint, listener and receiver function is your best bet and here is why:
The Stripe Payment Intent captures the lifecycle of a payment as it moves through multiple states. Because the various payment networks outside of Stripe do not guarantee specific response times these transitions can be asynchronous.
Therefore you cannot be certain when is the appropriate time to query the API for your completed Payment Intent unless you are listening for the payment_intent.succeeded event. Additionally, in some cases payment methods can be declined well after initial processing (e.g. suspected fraudulent cards, etc.). Using the webhooks approach keeps you informed of these changes.
Lastly, while you may only care to store this data in your DB now, scope does tend to increase and implementing webhook listeners early means you will have a solution ready if you need to take additional actions like
Sending email notifications to your customers
Adjusting revenue reconciliation
processing fulfillment actions
Other stuff.....
At the recommendation of RyanM I opted instead for the webhook solution, which turned out to be easier than I expected using Spatie's Stripe Webhooks package (although it seems to be run by someone who cares more about closing issues than fixing potential bugs, so opting for Stripe Cashier instead would probably be both easier and a more pleasant developer experience).
Note that by default Stripe webhooks return an Event object that itself contains other objects relevant to the event, such as a PaymentIntent for payment_intent.succeeded, for example, and any associated Charge objects. Therefore it's necessary to drill down a little to get all of the information needed.
$paymentIntent = $this->webhookCall->payload["data"]["object"];
$paymentID = $this->webhookCall->payload["data"]["object"]["id"]; // Transaction ID
$charge = $this->webhookCall->payload["data"]["object"]["charges"]["data"][0];
$transaction = Transaction::where("gateway_payment_id", $paymentID)->first();
$transaction->payment_status = strtoupper($paymentIntent["status"]); // Payment status
$transaction->payment_method = $charge["payment_method_details"]["type"]; // Payment method
$transaction->amount = ($paymentIntent["amount_received"]/100); // Amount charged, in pounds
$transaction->currency = strtoupper($paymentIntent["currency"]); // Currency charged in
$transaction->postcode = $charge["billing_details"]["address"]["postal_code"] ?? "N/A"; // Postcode if entered by the user - otherwise default to N/A
Hello and thanks for your time,
I am using the legacy version of Stripe Checkout, the one with the modal dialog, in a PHP environment.
<script src="https://checkout.stripe.com/checkout.js" class="stripe-button"
data-key="<?php echo $stripe['pub_key']; ?>"
data-email="<?php echo $loggedInUser->email; ?>"
data-amount="1000"
data-description="One Credit Purchase ($10.00)"
data-image="img/charge-logo.png"
data-name="Sheet"
data-panel-label="One Credit - "
data-label="One Credit - $10.00"
data-zip-code=TRUE
data-billing-address=TRUE>
</script>
And the charge page:
try {
$charge = \Stripe\Charge::create(array(
'amount' => 1000,
'currency' => 'usd',
'customer' => $_POST['customer_id'],
'description' => "Single Credit Purchase"
));
} catch(Stripe_CardError $e) {
$errors[] = $e->getMessage();
} catch (Stripe_InvalidRequestError $e) {
// Invalid parameters were supplied to Stripe's API
$errors[] = $e->getMessage();
} catch (Stripe_AuthenticationError $e) {
// Authentication with Stripe's API failed
$errors[] = $e->getMessage();
} catch (Stripe_ApiConnectionError $e) {
// Network communication with Stripe failed
$errors[] = $e->getMessage();
} catch (Stripe_Error $e) {
// Display a very generic error to the user, and maybe send
// yourself an email
$errors[] = $e->getMessage();
} catch (Exception $e) {
// Something else happened, completely unrelated to Stripe
$errors[] = $e->getMessage();
}
My Stripe account is in test mode, and I am attempting to test this workflow using their published test cards. Specifically the ones that are designed to decline:
4000000000000002 card_declined
4000000000009995 insufficient_funds
4000000000009987 lost_card
and so on. The problem is, no matter what input I provide, the charge always succeeds. I can put in any of the above card numbers, a bogus expiration date (i.e. 03/1969), and any CVC number, and the charge just goes right through.
The charge.succeeded event shows the charge object (abridged) as successful:
{
"object": {
"object": "charge",
"amount": 1000,
"paid": true,
"status": "succeeded"
}
}
I would expect to see something different. I have tried the new smart payment page hosted by Stripe that creates payments for you, and all of the cards work as expected, providing the appropriate decline reasons. Perhaps it is because the old version is deprecated? Or am I missing something?
Just wondering if anyone still uses the legacy version, or has had experience with the test cards not working as expected, or again, am I missing something?
Thanks for your time!
Cheers!
That code you posted doesn't seem to actually charge the token created by Checkout. You only pass the 'customer' => $_POST['customer_id'], so what happens is the Charge API charges the default card already attached to the particular customer(which is probably something else like a successful test card).
Hard to say without seeing all your code, but probably you don't actually use the token from the card information entered in Checkout(which would be $_POST[stripeToken]) anywhere so you're not actually testing that.
Also you should not use this integration at all and should switch to the new hosted page, Stripe considers it fully deprecated and it doesn't work for 3D Secure which is really important.
i have integrated coinbase api in my web app. When charge is created, users are directed to coinbase commerce website to make payment. How do i check if the user has finished paying or not and if the exact amount has been paid. Below is my code
<?php
require_once __DIR__ . "/vendor/autoload.php";
use CoinbaseCommerce\ApiClient;
use CoinbaseCommerce\Resources\Charge;
/**
* Init ApiClient with your Api Key
* Your Api Keys are available in the Coinbase Commerce Dashboard.
* Make sure you don't store your API Key in your source code!
*/
ApiClient::init("MY API KEY HERE");
$chargeObj = new Charge();
$chargeObj->name = 'Bitcoin Deposit';
$chargeObj->description = 'Testing the payment system';
$chargeObj->local_price = [
'amount' => '100.00',
'currency' => 'USD'
];
$chargeObj->pricing_type = 'fixed_price';
try {
$chargeObj->save();
// insert into database with status pending
$queryobject->insertTransaction($_SESSION['user_id'],$chargeObj->id, $amount, $status, $currentTime,
$chargeObj->name, $chargeObj->currency);
} catch (\Exception $exception) {
echo sprintf("Sorry! payment could not be created. Error: %s \n", $exception->getMessage());
}
if ($chargeObj->id) {
$chargeObj->description = "New description";
// Retrieve charge by "id"
try {
$retrievedCharge = Charge::retrieve($chargeObj->id);
$hosted_url = $retrievedCharge->hosted_url;
header('location: '.$hosted_url);
} catch (\Exception $exception) {
echo sprintf("Enable to retrieve charge. Error: %s \n", $exception->getMessage());
}
}
You need to use webhooks for this, you can create an endpoint for this. Unfortunately Coinbase does not have a sandbox environment so you are going to need a bit of cryptocurrency in your account.
I'm successfully able to create a new customer in Stripe via my PHP backend. That being said, when my user wants to view their existing cards, the PHP below just creates another new user and ephemeral key. If I already have a customer ID, how can I change my below code to make it so that if I'm providing the customer ID, existing cards are returned? Help is greatly appreciated!
if (!isset($_POST['api_version']))
{
header('HTTP/1.1 400 Bad Request');
}
//USER DETAILS
$email = $_POST['email'];
$customer = \Stripe\Customer::create(array(
'email' => $email,
));
try {
$key = \Stripe\EphemeralKey::create(
["customer" => $customer->id],
["stripe_version" => $_POST['api_version']]
);
header('Content-Type: application/json');
exit(json_encode($key));
} catch (Exception $e) {
header('HTTP/1.1 500 Internal Server Error');
}
You need to store the customer id you get back from stripe the first time you create a customer and then you can retrieve the user with:
\Stripe\Customer::retrieve($customer_id);
Something like this:
$customer_id = $_POST['customer_id']; //get this id from somewhere a database table, post parameter, etc.
// if the customer id doesn't exist create the customer
if ($customer_id !== null) {
// create the customer as you are now
} else {
$cards = \Stripe\Customer::retrieve($customer_id)->sources->all()
// return the cards
}
I'm trying to create a payment with Omnipay and Mollie in my Laravel project. I'm using the following 2 libraries:
https://github.com/barryvdh/laravel-omnipay
https://github.com/thephpleague/omnipay-mollie
I'm doing the following in my code:
$gateway = Omnipay\Omnipay::create('Mollie');
$gateway->setApiKey('test_gSDS4xNA96AfNmmdwB3fAA47zS84KN');
$params = [
'amount' => $ticket_order['order_total'] + $ticket_order['organiser_booking_fee'],
'description' => 'Bestelling voor klant: ' . $request->get('order_email'),
'returnUrl' => URL::action('EventCheckoutController#fallback'),
];
$response = $gateway->purchase($params)->send();
if ($response->isSuccessful()) {
session()->push('ticket_order_' . $event_id . '.transaction_id',
$response->getTransactionReference());
return $this->completeOrder($event_id);
}
The payment works. When the payment is done he goes back to the function fallback. But I don't know what to put in this function and how to go back to the line if($response->isSuccesfull()...).
The most important thing I need to do after the payment is :
session()->push('ticket_order_' . $event_id . '.transaction_id',
$response->getTransactionReference());
return $this->completeOrder($event_id);
Can someone help me figure out how to work with the fallback function and above?
A typical setup using Mollie consists of three separate pages:
a page to create the payment;
a page where Mollie posts the final payment state to in the background; and
a page where the consumer returns to after the payment.
The full flow is described in the Mollie docs. Also take a look at the flow diagram at that page.
DISCLAIMER: I've never used Omnipay myself and did not test the following code, but it should at least give you an idea how to set up your project.
Creating the payment:
$gateway = Omnipay\Omnipay::create('Mollie');
$gateway->setApiKey('test_gSDS4xNA96AfNmmdwB3fAA47zS84KN');
$params = [
'amount' => $ticket_order['order_total'] + $ticket_order['organiser_booking_fee'],
'description' => 'Bestelling voor klant: ' . $request->get('order_email'),
'notifyUrl' => '', // URL to the second script
'returnUrl' => '', // URL to the third script
];
$response = $gateway->purchase($params)->send();
if ($response->isRedirect()) {
// Store the Mollie transaction ID in your local database
store_in_database($response->getTransactionReference());
// Redirect to the Mollie payment screen
$response->redirect();
} else {
// Payment failed: display message to the customer
echo $response->getMessage();
}
Receiving the webhook:
$gateway = Omnipay\Omnipay::create('Mollie');
$gateway->setApiKey('test_gSDS4xNA96AfNmmdwB3fAA47zS84KN');
$params = [
'transactionReference' => $_POST['id'],
];
$response = $gateway->fetchTransaction($params);
if ($response->isPaid()) {
// Store in your local database that the transaction was paid successfully
} elseif ($response->isCancelled() || $response->isExpired()) {
// Store in your local database that the transaction has failed
}
Page where the consumer returns to:
// Check the payment status of your order in your database. If the payment was paid
// successfully, you can display an 'OK' message. If the payment has failed, you
// can show a 'try again' screen.
// Most of the time the webhook will be called before the consumer is returned. For
// some payment methods however the payment state is not known immediately. In
// these cases you can just show a 'payment is pending' screen.