How to build proposed order with what the user has selected? - node.js

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

Related

Which condition is correct to add?

I'm doing a small project in Node and React
And I'm building a payment system with the help of Stripe
Now everything works fine I have only one small problem I want at the end of the process to update the quantity in the database the problem is that I can't make the right condition to do it
and I must a condition Because if I don't set a condition as soon as I activated Stripe and got to the payment page, even if I didn't pay and did a return to the previous page, my quantity is updated
I add here the code I try to implement
exports.getCheckoutSessionBook = async (req, res, next) => {
const book = req.body.bookID[0]
console.log(book._id)
const quantity = req.body.quantityBooks
if (book.quantity < quantity) return next(new AppError(`there is only ${book.quantity} in stuck `))
if (book.quantity <= 0) return next(new AppError(`this book is over please try another book `))
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
success_url: `http://127.0.0.1:3001`,
cancel_url: `http://127.0.0.1:3001/account`,
customer_email: req.user.email,
client_reference_id: req.user.id,
line_items: [{
price_data: {
currency: 'usd',
product_data: {
name: book.title,
description: book.description,
images: [book.image],
},
unit_amount: book.price * 100,
},
quantity: quantity,
}],
mode: 'payment',
});
res.status(200).json({
status: 'suceess',
session
})
//here is my problem
if (res.status == 200) {
console.log(321)
const doc = await Book.findByIdAndUpdate(book._id, { $inc: { quantity: - quantity } }, {
new: true,
runValidators: true
})
console.log(333)
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
The code you shared calls the Create Checkout Session API first. This creates a Session that indicates what you are trying to sell to your Customer. At that point, no payment has been taken yet though. After that call, you seem to immediately increase the quantity in your database, before the payment happened.
There's no condition you can add here because the payment will happen later so you need to delete that part of the code or move it elsewhere entirely.
After the Checkout Session is created, you get the url property back in the response. That is the URL that you should redirect your customer to in the browser. There, they'll see Checkout's UI and decide if they want to pay.
What you should do, is listen for the checkout.session.completed Event that will be sent to your webhook endpoint when a Session gets paid/completed successfully. At that point, you can update the quantity in your database.
Stripe has a detailed documentation on fulfillment with Checkout in this doc.
I must confess I'm not familiar with the Stripe API, but your approach to what is essentially a distributed transaction is giving me anxiety.
Your system needs assume it may crash at any point and still be able to any transaction.
Basically you need a series of interconnected backend systems using a ledger system in the database.
A ledger is an ordered set of actions taken on a transaction that allows you to see what has happened so far and pick up the transaction from where it was left.
Basic steps in an order transaction could be:
Create the order.
Allocate the inventory to the order.
Get an ID from the payment provider and associate it with the order.
Send the customer to complete the payment.
When payment is complete move the order ready for shipping.
Handle shipping.
Close the order and update inventory.
Some notes:
Inventory is not changed up/down for every order open/cancelled/closed inventory is updated once the item is actually shipped. Inventory for sale is inventory on hand minus any inventory allocated to orders.
Note also that if your system crashes (or the client disconnects) at any point you can reconcile the orders.
Failure after step 1 -> close the order.
Failure after step 2 -> close the order (inventory becomes immediately available since it is no longer allocated).
Failure after step 3 -> check the status of the payment using the session ID with the payment provider and either close the order or continue to step 4.
etc.
Final note: whether orders fails if inventory cannot be allocated to orders varies based on whether you can order more inventory or not.
If all this sounds complex it's because it is. Making an online store front system is not a weekend project.

How to query all active / online users?

According to the documentation, it seems that one needs to indicate the users list that we want to query e.g.:
const response = await client.queryUsers({ id: { $in: ['john', 'jack', 'jessie'] } });
In this example, we need to indicate that we want the details of these 3 users. Is there a way to query all active users instead of passing an array of ids / names?
I tried:
const response = await client.queryUsers(
{ presence: true },
);
But it returned an empty string.
presence: true registers your client to presence events (online/offline).
There is no support to get this in one go. Also, beware if user is hidden, the user is requesting this information might not be able to see the real online users.
Current recommendation is to get channel with state: true and iterate your members and also setting presence: true, you will be notified by status changes.

Creating and pushing to an array with MongoDB

I'm trying to make a messaging system that writes each message to a mongo entry. I'd like the message entry to reflect the user that sends the message, and the actual message content. This is the message schema:
const MessageSchema = new Schema({
id: {
type: String,
required: true
},
messages: {
type: Array,
required: true
},
date: {
type: Date,
default: Date.now
}
});
And this is where I either create a new entry, or append to an existing one:
Message.findOne({ id: chatId }).then(message => {
if(message){
Message.update.push({ messages: { 'name': user.name, 'message': user.message } })
} else {
const newMessage = new Message(
{ id: chatId },
{ push: { messages: { 'name': user.name, 'message': user.message } } }
)
newMessage
.save()
.catch(err => console.log(err))
}
})
I'd like the end result to look something like this:
id: '12345'
messages: [
{name: 'David', message: 'message from David'},
{name: 'Jason', message: 'message from Jason'},
etc.
]
Is something like this possible, and if so, any suggestions on how to get this to work?
This questions contains lots of topics (in my mind at least). I really want to try to break this questions to its core components:
Design
As David noted (first comment) there is a design problem here - an ever-growing array as a sub document is not ideal (please refer to this blog post for more details).
On the over hand - when we imagine how a separate collection of messages will looks like, it will be something like this:
_id: ObjectId('...') // how do I identify the message
channel_id: 'cn247f9' // the message belong to a private chat or a group
user_id: 1234 // which user posted this message
message: 'hello or something' // the message itself
Which is also not that great because we are repeating the channel and user ids as a function of time. This is why the bucket pattern is used
So... what is the "best" approach here?
Concept
The most relevant question right now is - "which features and loads this chat is suppose to support?". I mean, many chats are only support messages display without any further complexity (like searching inside a message). Keeping that in mind, there is a chance that we store in our database an information that is practically irrelevant.
This is (almost) like storing a binary data (such an image) inside our db. we can do this, but with no actual good reason. So, if we are not going to support a full-text search inside our messages, there is no point to store the messages inside our db.. at all
But.. what if we want to support a full-text search? well - who said that we need to give this task to our database? we can easily download messages (using pagination) and make the search operation on the client side itself (while keyword not found, download previous page and search it), taking the loads out of our database!
So.. it seems like that messages are not ideal for storage in database in terms of size, functionality and loads (you may consider this conclusion as a shocking one)
ReDesign
Using a hybrid approach where messages are stored in a separated collection with pagination (the bucket pattern supports this as described here)
Store messages outside your database (since your are using Node.js you may consider using chunk store), keeping only a reference to them in the database itself
Set your page with a size relevant to your application needs and also with calculated fields (for instances: number of current messages in page) to ease database loads as much as possible
Schema
channels:
_id: ObjectId
pageIndex: Int32
isLastPage: Boolean
// The number of items here should not exceed page size
// when it does - a new document will be created with incremental pageIndex value
// suggestion: update previous page isLastPage field to ease querying of next page
messages:
[
{ userId: ObjectID, link: string, timestamp: Date }
]
messagesCount: Int32
Final Conclusion
I know - it seems like a complete overkill for such a "simple" question, but - Dawid Esterhuizen convinced me that designing your database to support your future loads from the very beginning is crucial and always better than simplifying db design too much
The bottom line is that the question "which features and loads this chat is suppose to support?" is still need to be answered if you intend to desgin your db efficiently (e.g. to find the Goldilocks zone where your design suits your application needs in the most optimal way)

Mongoose: Bulk upsert but only update records if they meet certain criteria

I am designing an item inventory system for a website that I am building.
The user's inventory is loaded from a Web API. This information is then processed so that it is more suited to my web app. I am trying to combine all the item records into one MongoDB collection - so other user inventories will be cached in the same place. What I have to deal with is deleting old item records if they are missing from the user's inventory (i.e. they sold it to someone) and also upserting the new items. Please note I have looked through several Stack Overflow questions about bulk upserts but I was unable to find anything about conditional updates.
Each item has two unique identifiers (classId and instanceId) that allow me to look them up (I have to use both IDs to match it) which remain constant. Some information about the item, such as its name, can change and therefore I want to be able to update those records when I fetch new inventory information. I also want new items that my site hasn't seen before to be added to my database.
Once the data returned from the Web API has been processed, it is left in a large array of objects. This means I am able to use bulk writing, however, I am unaware of how to upsert with conditions with multiple records.
Here is part of my item schema:
const ItemSchema = new mongoose.Schema({
ownerId: {
type: String,
required: true
},
classId: {
type: String,
required: true
},
instanceId: {
type: String,
required: true
},
name: {
type: String,
required: true
}
// rest of item attributes...
});
User inventories typically contain 600 or more items, with a max count of 2500.
What is the most efficient way of upserting this much data? Thank you
Update:
I have had trouble implementing the solution to the bulk insert problem. I made a few assumptions and I don't know if they were right. I interpreted _ as lodash, response.body as the JSON returned by the API and myListOfItems also as that same array of items.
import Item from "../models/item.model";
import _ from 'lodash';
async function storeInventory(items) {
let bulkUpdate = Item.collection.initializeUnorderedBulkOp();
_.forEach(items, (data) => {
if (data !== null) {
let newItem = new Item(data);
bulkUpdate.find({
classId: newItem.classId,
instanceId: newItem.instanceId
}).upsert().updateOne(newItem);
items.push(newItem);
}
});
await bulkUpdate.execute();
}
Whenever I run this code, it throws an error that complains about an _id field being changed, when the schema objects I created don't specify anything to do with schemas, and the few nested schema objects don't make a difference to the outcome when I change them to just objects.
I understand that if no _id is sent to MongoDB it auto generates one, but if it is updating a record it wouldn't do that anyway. I also tried setting _id to null on each item but to no avail.
Have I misunderstood anything about the accepted answer? Or is my problem elsewhere in my code?
This is how I do it :
let bulkUpdate = MyModel.collection.initializeUnorderedBulkOp();
//myItems is your array of items
_.forEach(myItems, (item) => {
if (item !== null) {
let newItem = new MyModel(item);
bulkUpdate.find({ yyy: newItem.yyy }).upsert().updateOne(newItem);
}
});
await bulkUpdate.execute();
I think the code is pretty readable and understandable. You can adjust it to make it work with your case :)

How should I model my MongoDB collection for nested documents?

I'm managing a MongoDB database for a building products store. The most immediate collection is products, right?
There are quite several products, however they all belong to one among a set of 5-8 categories and then to one subcatefory among a small set of subcategories.
For example:
-Electrical
*Wires
p1
p2
..
*Tools
p5
pn
..
*Sockets
p11
p23
..
-Plumber
*Pipes
..
*Tools
..
PVC
..
I will use Angular at web site client side to show whole products catalog, I think about AJAX for querying the right subset of products I want.
Then, I wonder whether I should manage one only collection like:
{
MainCategory1: {
SubCategory1: {
{},{},{},{},{},{},{}
}
SubCategory2: {
{},{},{},{},{},{},{}
}
SubCategoryn: {
{},{},{},{},{},{},{}
}
},
MainCategory2: {
SubCategory1: {
{},{},{},{},{},{},{}
}
SubCategory2: {
{},{},{},{},{},{},{}
}
SubCategoryn: {
{},{},{},{},{},{},{}
}
},
MainCategoryn: {
SubCategory1: {
{},{},{},{},{},{},{}
}
SubCategory2: {
{},{},{},{},{},{},{}
}
SubCategoryn: {
{},{},{},{},{},{},{}
}
}
}
Or a single collection per each category. The number of documents might not be higher than 500. However I care about a balance for:
quick DB answer,
easy server side DB querying, and
client-side Angular code for rendering results to html.
I'm using mongodb node.js module, not Mongoose now.
What CRUD operations will I do?
Inserts of products, I'd also like to have a way to obtain autogenerated ids (maybe sequential) per each new register. However, as it might seem natural I wouldn't offer the _id to the user.
Querying the whole documents set of a subcategory. Maybe just obtaining a few attributes at first.
Querying whole or a specific subset of attributes of a document (product) in particular.
Modifying a product's attributes values.
I agree client side should get the easiest result to render. However, to nest categories into products is still a bad idea. The trade off is once you want to change, for example, the name of a category, it will be a disaster. And if you think about the possible usecases, for example:
list all categories
find all subcategories of a certain category
find all products in a certain category
You'll find it hard to do these stuff with your data structure.
I had same situation in my current project. So here's what I do for your reference.
First, categories should be in a separate collection. DON'T nest categories into each other, as it will complicate the procedure to find all subcategories. The traditional way for finding all subcategories is to maintain an idPath property. For example, your categories are divided into 3 levels:
{
_id: 100,
name: "level1 category"
parentId: 0, // means it's the top category
idPath: "0-100"
}
{
_id: 101,
name: "level2 category"
parentId: 100,
idPath: "0-100-101"
}
{
_id: 102,
name: "level3 category"
parentId: 101,
idPath: "0-100-101-102"
}
Note with idPath, parentId is not necessary anymore. It's for you to understand the structure easier.
Once you need to find all subcategories of category 100, simply do the query:
db.collection("category").find({_id: /^0-100-/}, function(err, doc) {
// whatever you want to do
})
With category stored in a separate collection, in your product you'll need to reference them by _id, just like when we use RDBMS. For example:
{
... // other fields of product
categories: [100, 101, 102, ...]
}
Now if you want to find all products in a certain category:
db.collection("category").find({_id: new RegExp("/^" + idPath + "-/"}, function(err, categories) {
var cateIds = _.pluck(categories, "_id"); // I'm using underscore to pluck category ids
db.collection("product").find({categories: { $in: cateIds }}, function(err, products) {
// products are here
}
})
Fortunately, category collection is usually very small, with only hundreds of records inside (or thousands). And it doesn't varies a lot. So you can always store a live copy of categories inside memory, and it can be constructed as nested objects like:
[{
id: 100,
name: "level 1 category",
... // other fields
subcategories: [{
id: 101,
... // other fields
subcategories: [...]
}, {
id: 103,
... // other fields
subcategories: [...]
},
...]
}, {
// another top1 category
}, ...]
You may want to refresh this copy every several hours, so:
setTimeout(3600000, function() {
// refresh your memory copy of categories.
});
That's all I get in mind right now. Hope it helps.
EDIT:
to provide int ID for each user, $inc and findAndModify is very useful. you may have a idSeed collection:
{
_id: ...,
seedValue: 1,
forCollection: "user"
}
When you want to get an unique ID:
db.collection("idSeed").findAndModify({forCollection: "user"}, {}, {$inc: {seedValue: 1}}, {}, function(err, doc) {
var newId = doc.seedValue;
});
The findAndModify is an atomic operator provided by mongodb. It will guarantee thread safety. and the find and modify actually happens in a "transaction".
2nd question is in my answer already.
query subsets of properties is described with mongodb Manual. NodeJS API is almost the same. Read the document of projection parameter.
update subsets is also supported by $set of mongodb operator.

Resources