How to make subscription session with prefilled removable promotion code or discount - stripe-payments

I've managed to create a checkout page with promotion code like this
const sessionParams: Stripe.Checkout.SessionCreateParams = {
customer_email: user.email,
mode: 'subscription',
line_items: [
{
price: process.env.STRIPE_PRICE_CODE,
quantity: 1,
},
],
metadata: {
userId: user.userId,
},
// {CHECKOUT_SESSION_ID} is a string literal; do not change it!
// the actual Session ID is returned in the query parameter when your customer
// is redirected to the success page.
success_url: `${origin}/account/download-app?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/account/plan-preview`,
};
if (req.query.coupon === 'special-access') {
sessionParams.allow_promotion_codes = true;
} else {
sessionParams.discounts = [{ promotion_code: '***' }];
}
const session = await stripe.checkout.sessions.create(sessionParams);
But I've noticed that user can't remove the promo code and add his custom code.
I see other companies checkouts that they do have prefilled promo code with an X to remove it and add their own
I've tried using discount object with allow_promotion_codes: true, but it seems not allowed by the API.

Checkout does not currently support pre-filling a promotion code that can be removed by the customer on the payment page. Equally, you cannot pass both the allow_promotions_code parameter and pre-fill a promotion code via the discounts hash. The API will error (as you've discovered). In order for the promotion code to be removable by your customer, you will need to only use the allow_promotion_codes boolean.
Payment Links do support pre-filling of promotion codes via the URL parameter (e.g. https://buy.stripe.com/foo?prefilled_promo_code=bar), which is likely what you're referencing.

Related

Stripe checkout session in subscription mode, with free trial and without requesting payment method

Is it possible to remove the payment method field or make it not "required" with Stripe Checkout API ?
Here is my backend code:
const freeTrial = {
trial_from_plan: realSubscription.id === 4 ? true : false
}
if(customerStripeId != undefined || customerStripeId != null){
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
line_items: [
{
price: formData.monthlySetting === true ? realSubscription.monthly_price_id : realSubscription.annual_price_id,
quantity: 1
}
],
customer: customerStripeId,
success_url: `${BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: BASE_URL,
subscription_data: freeTrial
})
return {customerHasId: 'true', sessionData: session }
}else{
return {customerHasId: 'false'}
}
In Stripe UI I have set up a 30 day trial for my first plan. It's working but when I checkout, Stripe still ask for a payment methode even though.
It's like this checkout demo from Stripe:
link to demo
This is not possible today. Stripe Checkout requires valid payment method details to maximize the chance that you get the first invoice paid at the end of the trial. There is no way to skip payment method details collection on Checkout.
If you don't want a payment method upfront then you would create the Subscription with a trial on your end without using Stripe Checkout.

Stripe subscription webhooks missing metadata and client_reference_id

I'm having issues linking stripe webhooks to customers, since I generally use the client_reference_id or metadata field, however subscription webhooks seem to not have these fields. For example the event checkout.session.completed does contain the client_reference_id, whereas invoice.paid does not.
NodeJS code to generate payment:
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name: `Premium license`,
},
unit_amount: 600,
recurring: {
interval: "month",
interval_count: 1
},
},
quantity: 1
}],
subscription_data: {
trial_period_days: 1,
},
metadata: { 'userId': userId },
client_reference_id: userId,
mode: 'subscription',
customer_email: customerEmail,
success_url: `...`,
cancel_url: `...`,
});
Yes, it's a major missing that there is no option to link between events. This becomes critical when you can't rely on the order of webhook events.
I did work around this using metadata though. Pass your own reference id, say, my_database_reference_id to the metadata of both checkout.session.create and also in the subscription_data in checkout creation. This can be the same value you pass inside client_reference_id. Now you can use this while listening to webhook, to connect between the checkout session and subscription object, irrespective of the order it comes in.
NB: Please be warned that metadata can be edited by the Stripe Account, so please be careful if you are a platform and relying on this for any logic.
The client_reference_id is a property of Checkout Session objects in Stripe, but not any other Stripe objects.
Subscription events (like customer.subscription.created) describe Subscription objects, and Invoice events (like invoice.paid) describe Invoices, neither of which are Checkout Sessions, so the property is missing.
Typically the way all of this is linked together is with the Customer object (cus_123) in Stripe, which should be present on all of the events mentioned in the customer property. Checkout will create the Customer object for you if you don't specify an existing one.

Once off payment with Stripe using the API ID

I have created a subscription service using Stripe. I can subscribe a user to use recurring payments. This is the relevant code (node):
// Create the subscription
const subscription = await stripe.subscriptions.create({
customer: req.body.customerId,
items: [{ price: req.body.priceId }],
expand: ['latest_invoice.payment_intent'],
});
This works and uses the priceId as shown in the dashboard:
However, it falls over when I sell a product which isn't recurring. I get the error:
The price specified is set to `type=one_time` but this field only accepts prices with `type=recurring`
I understand the error, but I am not sure if I can set a subscription to not be recurring.
My app has 3 tiers:
once off
monthly
yearly
Ideally, I would like not to add a whole new section of code to handle what seems like a subset of what subscriptions do, but even if I do, the paymentIntent object seems to only take an amount rather than the API ID as shown in the picture. Is there any way to do this using the infrastructure I have already built?
You can't create a subscription with a non-recurring price, instead for one-off payments you'd use a PaymentIntent.
Prices are meant for use with subscriptions and the Checkout product, but you can still use the data in them for PaymentIntents. For instance:
// get the price
const price = await stripe.prices.retrieve(req.body.priceId);
// check if the price is recurring or not
if (price.recurring !== null) {
// Create the subscription
const subscription = await stripe.subscriptions.create({
customer: req.body.customerId,
items: [{ price: req.body.priceId }],
expand: ['latest_invoice.payment_intent'],
});
// do something with the subscription
} else {
const pi = await stripe.paymentIntents.create({
customer: req.body.customerId,
currency: 'usd',
amount: price.unit_amount,
});
// do something with the PaymentIntent
}
you can also attach one-time items via the subscription.create call by placing them in add_invoice_items.
see: https://stripe.com/docs/api/subscriptions/create?lang=node#create_subscription-add_invoice_items

Stripe v3 - SetupIntents and Subscriptions

I'm trying to use SetupIntents to validate and save a Payment Method and then create a Subscription using it to charge the customer (inmediately and then monthly) for the required amount.
This appears to be working fine:
The Card is validated (including SCA if needed)
The Payment Method is created, attached to customer as default and enabled for future usage with the status SUCEEDED.
The Subscription is created and uses the above payment method
The problem is that Stripe then generates the corresponding Invoice and Payment Intent but the latter with the status "requires_action" whenever the provided Card requires Secure Customer Authorization (SCA), even though the right payment method (enabled for future usage) is being used and the card validation has been already performed.
I thought the whole point of using SetupIntents was precisely to validate the payment method beforehand and be able to charge the customer afterwards.
Is my assumption simply wrong or this is actually possible and I might just be missing something?
Thanks in advance
EDIT: This is the subscription creation code in the backend:
# Set the default payment method on the customer
Stripe::Customer.update(
stripe_customer_id,
invoice_settings: {
default_payment_method: #requested_source
}
)
subscription = Stripe::Subscription.create({
"customer" => stripe_customer_id,
"proration_behavior" => 'create_prorations',
"items" => [
[
"plan" => "#{#stripe_plan_api_id}",
],
],
'default_tax_rates' => [
"#{#stripe_tax_rate_id}",
],
"expand" => ["latest_invoice.payment_intent"]
});
Thanks for the question, Eduardo.
There are a couple ways to create a Subscription while gaining the Customers SCA authentication and permission to charge the card later. Assuming we already have a Stripe Customer object created and their ID is stored in stripe_customer_id. With the flow you have now there are a couple steps:
Create the SetupIntent. Note that if you create it with usage: 'off_session' and the Customer it'll attach the resulting PaymentMethod when confirmed.
setup_intent = Stripe::SetupIntent.create({
customer: stripe_customer_id,
usage: 'off_session',
})
Collect payment details and confirm the SetupIntent using its client_secret on the client which will attach the PaymentMethod to the customer. Note that it will attach but will not be set as the invoice_settings.default_payment_method by default, so you'll need to make a separate API call later to update the Customer (see step 3).
stripe.confirmCardSetup(
'{{setup_intent.client_secret}}',
{
payment_method: {
card: cardElement,
},
}
).then(function(result) {
// Handle result.error or result.setupIntent
});
Update the Customer and set its invoice_settings.default_payment_method equal to the ID of the PaymentMethod on the successfully confirmed SetupIntent.
Stripe::Customer.update(
stripe_customer_id, {
invoice_settings: {
default_payment_method: 'pm_xxxx', # passed to server from client. On the client this is available on the result of confirmCardSetup
}
})
Create the Subscription with off_session: true
subscription = Stripe::Subscription.create({
customer: stripe_customer_id,
proration_behavior: 'create_prorations',
items: [{
plan: "#{#stripe_plan_api_id}",
}],
default_tax_rates: [
"#{#stripe_tax_rate_id}",
],
expand: ["latest_invoice.payment_intent"],
off_session: true,
})
This uses what's called a "Merchant Initiated Transaction" (MIT) for the Subscription's first payment. This is technically okay if the Subscription is created later after the Customer leaves and should technically work.
If the customer is on your site/app when you create the Subscription, there's another flow that is a bit more correct and doesn't require using a MIT exemption for SCA. The flow is the following for a Subscription without a trial:
Collect card details with createPaymentMethod on the client (no SetupIntent)
stripe.createPaymentMethod({
type: 'card',
card: cardElement,
}).then(function(result) {
//pass result to your server.
})
Attach those card details to the Customer
Stripe::PaymentMethod.attach(
"pm_xxx", {
customer: stripe_customer_id
}
)
Update the Customer's invoice_settings.default_payment_method
Stripe::Customer.update(
stripe_customer_id,
invoice_settings: {
default_payment_method: #requested_source
}
)
Create the Subscription (without off_session: true)
subscription = Stripe::Subscription.create(
customer: data['customerId'],
items: [
{
price: 'price_H1NlVtpo6ubk0m'
}
],
expand: ['latest_invoice.payment_intent']
)
Use the Subscription's latest_invoice's payment_intent's client_secret to collect payment details and confirm on the client.
stripe.confirmCardPayment(
'{{subscription.latest_invoice.payment_intent.client_secret}}', { ......
This second payment flow is a bit more correct from an SCA standpoint for getting authorization to charge the card. The second approach is outlined in the guide here: https://stripe.com/docs/billing/subscriptions/fixed-price
We also have a Stripe Sample you can use to experiment here: https://github.com/stripe-samples/subscription-use-cases/tree/master/fixed-price-subscriptions

How to build proposed order with what the user has selected?

I'm building an AOG (actions on google) project that will do basic transaction functionality. Since I'm still a bit new to AOG, I'm completely stuck on how to take what the user selects (whether it be a carousel, a basic card etc.) and pass that argument value/key that they selected into the proposed order or the order preview before they finish their transaction.
Here is basically what I have tried (This isn't the actual code because it's rather long, but it still gets the idea across)
app.intent('delivery_address_complete', (conv) => {
const arg = conv.arguments.get('DELIVERY_ADDRESS_VALUE');
if (arg.userDecision ==='ACCEPTED') {
conv.ask('Ok, what would you like to order?');
conv.ask(new Suggestions(intentSuggestions));
conv.ask(new Carousel({
items: {
// Add the first item to the carousel
SELECTION_KEY_COFFEE: {
synonyms: [
'Coffee'
],
title: 'Coffee',
description: 'Sweet cream and sugar coffee.',
image: new Image({
url: IMG_URL_COFFEE,
alt: 'Image alternate text',
}),
},
}));
}
});
const yesOrno = [
'Yes',
'No'
];
app.intent('actions.intent.OPTION', (conv ) => {
conv.ask('Okay, are you ready to proceed?');
conv.ask(new Suggestions(yesOrno));
});
app.intent('transaction_decision_action', (conv) => {
const order = {
id: UNIQUE_ORDER_ID,
cart: {
merchant: {
id: 'coffee',
name: 'Coffee Store',
},
lineItems: [
{
name: 'My Memoirs',
id: 'coffee_1',
price: {
amount: {
currencyCode: 'USD',
nanos: 990000000,
units: 3,
},
type: 'ACTUAL',
},
quantity: 1,
subLines: [
{
note: 'coffee',
},
],
type: 'REGULAR',
},
otherItems: [
{
name: 'Subtotal',
id: 'subtotal',
price: {
amount: {
currencyCode: 'USD',
nanos: 220000000,
units: 32,
},
type: 'ESTIMATE',
},
type: 'SUBTOTAL',
},
{
name: 'Tax',
id: 'tax',
price: {
amount: {
currencyCode: 'USD',
nanos: 780000000,
units: 2,
},
type: 'ESTIMATE',
},
type: 'TAX',
},
],
totalPrice: {
amount: {
currencyCode: 'USD',
nanos: 0,
units: 35,
},
type: 'ESTIMATE',
},
};
Please note: This is mostly dummy code, so if some things like over charging or prices not making sense is happening, it's not the problem I'm trying to fix.
How can I take what the user selected from whatever method, and get it so it will appear on the order preview or proposed order? I do not need help with anything regarding making carousels or basic cards ect. Just how to get this selected information to the order preview.
To be more specific:
I can create an order object that is required, and I know how to send it to Google (and then to the user) as part of a ProposedOrder object that becomes part of the TransactionDecision object. (The "transaction_decision_action" Intent handler in the code above.)
What I don't understand is how to build the order based on the user saying things or by selecting on carousel or list items that I've shown them. (What do I do in the "actions.intent.OPTION" Intent handler above, for example?)
edit: This also may clear up any confusion. This is a video representation of what I'm attempting to do (mentioned in comments below):
youtube.com/watch?v=LlgMcJBnNN8 from 1:02 to 1:29 I know how to do, I'm confused (In the video example) how they were able to get the 'turkey sandwich' and the 'Green smoothie' added to the order preview at 1:35 ish from the carousel selections
What you're looking to do is what Google refers to as building the order. As it notes at that link
Once you have the user information you need, you'll build a "cart
assembly" experience that guides the user to build an order. Every
Action will likely have a slightly different cart assembly flow as
appropriate for your product or service.
You could build a cart assembly experience that enables the user to
re-order their most recent purchase via a simple yes or no question.
You could also present the user a carousel or list card of the top
"featured" or "recommended" items. We recommend using rich responses
to present the user's options visually, but also design the
conversation such that the user can build their cart using only their
voice.
For more information on how to build a high-quality cart assembly
experience, see the Transactions Design Guidelines.
So there is no one way to do what you're asking about. However, there are a few tips of things you can and should be doing to build the proposed order.
Managing the order
The big thing you need to do is to keep track of all the things that the user is ordering as you go through the process. There are a number of ways you can store this information:
In a Dialogflow Context
In the user session store
In a database or data store for the session
In short, any of the current ways you have to store session information. All of the information below assumes you've picked some way to do this.
Since everything will become one of the lineItems, an easy solution is to build this array as you go along, and then you can just copy the array directly into the order object. Another approach is to just store a list of item IDs, and then populate the rest of the information later when we build the order.
For this example, we're going to go with this latter scheme (because its easier to show) and store it in the session storage object using the actions-on-google library.
So for starters, when we start the Action, or when we know we'll be taking the order, we need to initialize our list of items being ordered with something like
conv.user.data.items = [];
Now that we have our initial item list, we can explore different ways to add to this list.
Adding an item: "my regular"
For some types of orders, it may make sense for the user to be able to say "I'll have my usual". In cases like this, we want an Intent that handles this phrase (or handles a "yes" response to our prompting), and an Intent Handler that looks up the user's regular order and adds it to the items. Perhaps something like this:
app.intent('order.usual', conv => {
// Get their user profile from our database
// The "loadUser" function is up to you, and has little to do with AoG
return loadUser( conv )
.then( user => {
// Add each item in their usual order to the current items
let usualOrder = user.usualOrder;
usualOrder.forEach( item => conv.user.data.items.push( item ) );
// Send a message back to the user
conv.add( "You got it! Do you want anything else?" );
});
});
Adding an item from a list
If you've presented a carousel or a list to the user of possible items, your life is a little easier (although you may not think it at the moment). You do need to setup a Dialogflow Intent that handles the actions_intent_OPTION event (which I'll call order.option in this case).
In the handler for this, we'll assume that the key you used for the option also happens to be the item ID, so you can just add it to the list
app.intent('order.option', (conv, params, option) => {
// The item is the option sent
let item = option;
// Add the item to the list of items
conv.user.data.items.push( item );
// Send a message back to the user
conv.add( "I've put that in your cart. Anything else?" );
});
Adding an item by name
But remember, the user can take the conversation in any direction at any time. So they may ask for an item that you currently aren't showing in the carousel. The best way to handle this is by creating an Entity Type in Dialogflow (which I'll call item, as an example)
And then an Intent that captures some phrases that expresses the user asking to add them (which I'll call order.name and which has an itemName parameter that the user has to include).
[
In the handler, you need to get the name that they spoke, look up what the item is, and add this to the list of items they've ordered.
app.intent('order.name', (conv, params) => {
// Get the name
let itemName = params['itemName'];
// Look it up to find out what they ordered
// You need to implement the itemFromName function
return itemFromName( itemName )
.then( item => {
// Add the item
conv.user.data.items.push( item );
// And reply
conv.add( "You got it! Anything else?" );
});
});
Finish building the order
Once you've finished collecting everything they want, your Intent Handler should put the order together, assembling the full list of lineItems from the conv.user.data.items array that we've been putting together, calculating tax, totals, and all the other parts of the order.
We then need to propose the order by sending a TransactionDecision object that contains our order in the proposedOrder parameter. Clever, no? Possibly something like this:
app.intent('review', conv => {
// Get the items the user has saved
let items = conv.user.data.items;
// Turn these into more complete lineItems
// You will need to provide the "itemToLineItem" function
let lineItems = items.map( itemToLineItem );
// Get some other objects we need
// You'll need to define these functions, too
let orderId = generateOrderId();
let subtotal = computeSubtotal( lineItems );
let tax = computeTax( lineItems );
let total = computerTotal( subtotal, tax );
// Build the order object
let order = buildOrder( lineItems, subtotal, tax, total );
conv.ask(new TransactionDecision({
orderOptions: {
requestDeliveryAddress: false,
},
paymentOptions: {
googleProvidedOptions: {
prepaidCardDisallowed: false,
supportedCardNetworks: ['VISA', 'AMEX'],
// These will be provided by payment processor,
// like Stripe, Braintree, or Vantiv.
tokenizationParameters: {
tokenizationType: 'PAYMENT_GATEWAY',
parameters: {
'gateway': 'stripe',
'stripe:publishableKey': (conv.sandbox ? 'pk_test_key' : 'pk_live_key'),
'stripe:version': '2017-04-06'
},
},
},
},
proposedOrder: order,
}));
});
I broke most of the stuff out as a function since there is nothing specific about them, except the format of the order (which you illustrate in your example). You can really build it any way you want.
Conclusion
Much of what you need to do really boils down to
Collecting the information of what the user wants to order, mostly storing the IDs of these items
Turning this list of items into the complete order object
Sending this order for the user to review

Resources