Need to mark paid installments in an array using mongoDB aggregation - mongodb

I have a schema named orders which looks like this :
{
"_id" : ObjectId("5cd42f7b16c2654ea9138ece"),
"customerId" : ObjectId("5c8222109146d119ccc5243f"),
"orderAmount" : NumberInt(10000),
"paidAmount" : NumberInt(4000),
"installments" : [
{
"dueDate" : ISODate("2020-01-01"),
"amount" : NumberInt(2000)
},
{
"dueDate" : ISODate("2020-01-07"),
"amount" : NumberInt(6000)
},
{
"dueDate" : ISODate("2020-01-04"),
"amount" : NumberInt(2000)
}
]
}
I want to write an aggregation function that sorts the installments according to dueDate and mark them paid according to paidAmount. For example for this case the function should return
{
"_id" : ObjectId("5cd42f7b16c2654ea9138ece"),
"customerId" : ObjectId("5c8222109146d119ccc5243f"),
"orderAmount" : NumberInt(10000),
"paidAmount" : NumberInt(4000),
"installments" : [
{
"dueDate" : ISODate("2020-01-01"),
"amount" : NumberInt(2000),
"paid" : true
},
{
"dueDate" : ISODate("2020-01-04"),
"amount" : NumberInt(2000),
"paid" : true
},
{
"dueDate" : ISODate("2020-01-07"),
"amount" : NumberInt(6000),
"paid" : false
}
]
}
Now I can sort the array using the $unwind and $sort functions like this:
db.orders.aggregate([
{$unwind : "$installments"},
{$sort : {"dueDate" : 1}}
]);
What I am stuck on is how to group the array back so that it gives me the desired result. I can only use aggregation here.

You need to $group installments. But, if you need to put paid field with some logic, it's necessary to add extra pipeline stages.
ASSUMPTION
paidAmount value calculated by ordered installments.[].paid
paidAmount installments.[].paid
4000 <= 2000(t) 2000(t) 6000(f)
4000 <≠ 2000(t) 6000(f) 2000(f)
4000 <≠ 6000(f) 2000(f) 2000(f)
6000 <= 6000(t) 2000(f) 2000(f)
6000 <≠ 1000(t) 6000(f) 1000(f)
6000 <= 1000(t) 4000(t) 1000(t)
EXPLANATION paid:true|false LOGIC
We order installments and create extra tmp field with installments value (for paid field).
For each installments item i, we mark paid:true if paidAmount - sum(amount0 - i) >= 0.
db.orders.aggregate([
{
$unwind: "$installments"
},
{
$sort: {
"installments.dueDate": 1
}
},
{
$group: {
_id: "$_id",
orders: {
$first: "$$ROOT"
},
installments: {
$push: "$installments"
},
tmp: {
$push: "$installments"
}
}
},
{
$unwind: "$installments"
},
{
$addFields: {
"installments.paid": {
$cond: [
{
$gte: [
{
$reduce: {
input: {
$slice: [
"$tmp",
{
$sum: [
{
$indexOfArray: [
"$tmp",
"$installments"
]
},
1
]
}
]
},
initialValue: "$orders.paidAmount",
in: {
$sum: [
{
$multiply: [
"$$this.amount",
-1
]
},
"$$value"
]
}
}
},
0
]
},
true,
false
]
}
}
},
{
$group: {
_id: "$_id",
customerId: {
$first: "$orders.customerId"
},
orderAmount: {
$first: "$orders.orderAmount"
},
paidAmount: {
$first: "$orders.paidAmount"
},
installments: {
$push: "$installments"
}
}
}
])
MongoPlayground

Try this :
db.yourCollectionName.aggregate([{ $unwind: '$installments' },{ $sort: { 'installments.dueDate': 1 } },
{ $addFields: { 'installments.paid': { $cond: [{ $lte: ["$installments.amount", '$paidAmount'] }, true, false] } } },
{ $group: { _id: '$id', data: { $first: '$$ROOT' }, installments: { $push: '$installments' } } },
{ $addFields: { 'data.installments': '$installments' } },
{ $replaceRoot: { newRoot: "$data" } }])
Collection Data :
/* 1 */
{
"_id" : ObjectId("5cd42f7b16c2654ea9138ece"),
"customerId" : ObjectId("5c8222109146d119ccc5243f"),
"orderAmount" : 10000,
"paidAmount" : 4000,
"installments" : [
{
"dueDate" : ISODate("2020-01-01T21:21:20.202Z"),
"amount" : 2000
},
{
"dueDate" : ISODate("2020-01-07T21:27:20.202Z"),
"amount" : 6000
},
{
"dueDate" : ISODate("2020-01-04T21:24:20.202Z"),
"amount" : 2000
}
]
}
Result :
/* 1 */
{
"_id" : ObjectId("5cd42f7b16c2654ea9138ece"),
"customerId" : ObjectId("5c8222109146d119ccc5243f"),
"orderAmount" : 10000,
"paidAmount" : 4000,
"installments" : [
{
"dueDate" : ISODate("2020-01-01T21:21:20.202Z"),
"amount" : 2000,
"paid" : true
},
{
"dueDate" : ISODate("2020-01-04T21:24:20.202Z"),
"amount" : 2000,
"paid" : true
},
{
"dueDate" : ISODate("2020-01-07T21:27:20.202Z"),
"amount" : 6000,
"paid" : false
}
]
}
Ref : aggregation-pipeline

Related

MongoDB - Group by and count value, but treat per record as one

I want to group by and count follow_user.tags.tag_id per record, so no matter how many times the same tag_id show up on the same record, it only counts as 1.
My database structure looks like this:
{
"external_userid" : "EXID1",
"follow_user" : [
{
"userid" : "USERID1",
"tags" : [
{
"tag_id" : "TAG1"
}
]
},
{
"userid" : "USERID2",
"tags" : [
{
"tag_id" : "TAG1"
},
{
"tag_id" : "TAG2"
}
]
}
]
},
{
"external_userid" : "EXID2",
"follow_user" : [
{
"userid" : "USERID1",
"tags" : [
{
"tag_id" : "TAG2"
}
]
}
]
}
Here's my query:
[
{ "$unwind": "$follow_user" }, { "$unwind": "$follow_user.tags" },
{ "$group" : { "_id" : { "follow_user᎐tags᎐tag_id" : "$follow_user.tags.tag_id" }, "COUNT(_id)" : { "$sum" : 1 } } },
{ "$project" : { "total" : "$COUNT(_id)", "tagId" : "$_id.follow_user᎐tags᎐tag_id", "_id" : 0 } }
]
What I expected:
{
"total" : 1,
"tagId" : "TAG1"
},
{
"total" : 2,
"tagId" : "TAG2"
}
What I get:
{
"total" : 2,
"tagId" : "TAG1"
},
{
"total" : 2,
"tagId" : "TAG2"
}
$set - Create a new field follow_user_tags.
1.1. $setUnion - To distinct the value from the Result 1.1.1.
1.1.1. $reduce - Add the value of follow_user.tags.tag_id into array.
$unwind - Deconstruct follow_user_tags array field to multiple documents.
$group - Group by follow_user_tags and perform total count via $sum.
$project - Decorate output document.
db.collection.aggregate([
{
$set: {
follow_user_tags: {
$setUnion: {
"$reduce": {
"input": "$follow_user.tags",
"initialValue": [],
"in": {
"$concatArrays": [
"$$value",
"$$this.tag_id"
]
}
}
}
}
}
},
{
$unwind: "$follow_user_tags"
},
{
$group: {
_id: "$follow_user_tags",
total: {
$sum: 1
}
}
},
{
$project: {
_id: 0,
tagId: "$_id",
total: 1
}
}
])
Sample Mongo Playground

MongoDB Aggregation with match shows all the records

I have json object as following.
{
"_id" : ObjectId("123209sfekjern"),
"Name" : "Test1",
"Orders" : [
{
"Date" : "2020-05-05",
"Total" : "100.00"
},
{
"Date" : "2020-05-10",
"Total" : "110.00"
},
{
"Date" : "2020-05-11",
"Total" : "100.00"
},
{
"Date" : "2020-05-14",
"Total" : "110.00"
},
{
"Date" : "2020-05-20",
"Total" : "100.00"
},
{
"Date" : "2020-05-15",
"Total" : "100.00"
},
{
"Date" : "2020-05-12",
"Total" : "110.00"
},
{
"Date" : "2020-05-18",
"Total" : "100.00"
},
{
"Date" : "2020-05-31",
"Total" : "110.00"
}
]
}
I need customername, orders.Date and Order.Total for all the orders which is greater than 100.00
I tried following query..
db.Customers.aggregate
(
[
{
$match: {
$and: [
{"Orders.Date":{$gte:"2020-05-15"}},//ISODate('2020-05-15 10:00:00.000Z')
{ "Orders.Total": { $gte: "100.00" } },
]
}
},
{ $project: { _id:0, Name: 1, "Orders.Total": 1, "Orders.Date": 1} },
]
)
The above query returns all the records. I m still beginner and learning mongodb.
any help would be appreciated.
thank you.
$match filters on a document level so entire document will be returned if at least one subdocument matches your conditions. In order to filter a nested array you need $filter:
db.Customers.aggregate([
{
$project: {
_id: 0,
Name: 1,
Orders: {
$filter: {
input: "$Orders",
cond: {
$and: [
{ $gte: [ "$$this.Date", "2020-05-15" ] },
{ $gte: [ "$$this.Total", "100.00" ] },
]
}
}
}
}
}
])
Mongo Playground

How to compare nested array elements with each other and count the total sub documents?

my mongodb document set look like this
{
"_id" : ObjectId("59093a8e1104a53169"),
"createdAt" : ISODate("2017-05-03T02:03:58.249+0000"),
"phone" : "0000000000",
"email" : "abc#gmail.com",
"dob" : "12/26/1976",
"password" : "*******",
"stripeID" : "***",
"picture" : "htt://g",
"name" : {
"first" : "P",
"last" : "e"
},
"addresses" : [
{
"description" : "237 S ABCD, USA",
"_id" : ObjectId("59093bsaaudua"),
"loc" : [
-008.2478742,
124.0517012
]
},
{
"apartment" : "",
"description" : "787 S Defghsvd USA",
"_id" : ObjectId("5a26b77dfhgswj"),
"loc" : [
-18.01,
34.039058
]
},
{
"description" : "13210 hdsg sdjhf 90284, USA",
"_id" : ObjectId("5d2482basasas17be1"),
"loc" : [
-18.01,
-18.01
]
}
]
}
what i need to do is compare loc[0] with loc[1] if addresses exists in the document and know how many of them has this x === y. i don't know how to approach this. any help would be great. thanks in advance.
i.e. what i want is in all the documents if any user has equal loc array element's, then i want to find those documents. my query should return like:
{
"description" : "13210 hdsg sdjhf 90284, USA",
"_id" : ObjectId("5d2482basasas17be1"),
"loc" : [
-18.01,
-18.01
]
}
this should do the trick:
db.collection.aggregate([
{
$unwind: '$addresses'
},
{
$match: {
$expr: {
$eq: [
{ $arrayElemAt: ["$addresses.loc", 0] },
{ $arrayElemAt: ["$addresses.loc", 1] }
]
}
}
},
{
$replaceRoot: {
newRoot: "$addresses"
}
}
])
https://mongoplayground.net/p/YRnbPm-qfe6
if you also want the count, you can do this:
db.collection.aggregate([
{
$unwind: '$addresses'
},
{
$match: {
$expr: {
$eq: [
{ $arrayElemAt: ["$addresses.loc", 0] },
{ $arrayElemAt: ["$addresses.loc", 1] }
]
}
}
},
{
$replaceRoot: {
newRoot: "$addresses"
}
},
{
$group: {
_id: null,
count: {
$sum: 1
},
addresses: {
$push: '$$ROOT'
}
}
},
{
$project: {
_id: 0
}
}
])
https://mongoplayground.net/p/Kqi4J7f-4go

Is it possible to join one field of an array unwind onto the unwound array?

Pretty new to mongo and haven't been able to figure out how to perform a query.
I have an accounts collection that looks like this:
{
"_id" : ObjectId("1"),
"time" : ISODate("2018-10-20T05:57:15.372Z"),
"profileId" : "1",
"totalUSD" : "1015.5513030613",
"accounts" : [
{
"_id" : ObjectId("2"),
"accountId" : "1",
"currency" : "USD",
"balance" : "530.7934159683763000",
"available" : "530.7934159683763",
"hold" : "0.0000000000000000",
"exchangeRateUSD" : "1"
},
{
"_id" : ObjectId("5"),
"accountId" : "4",
"currency" : "BTC",
"balance" : "0.0759214200000000",
"available" : "0.07592142",
"hold" : "0.0000000000000000",
"exchangeRateUSD" : "6384.995"
},
],
}
I store only exchangeRateUSD for each currency, and not exchangeRateXXX where XXX is currency name, because there can be an arbitrary number of currencies and currency pairs. But when I query the accounts collection it will always be queried by a currency pair, eg: BTC-USD. Keeping it simple for now, I can assume the currency pair will always be XXX-USD.
When I query the accounts collection I'd like to add a 'virtual' field to each account object: exchangeRateCrypto and then on the top-level accounts document I'd like to add totalCrypto which would just be the total account value in the given crypto. Eg: USD account balance * exchangeRateCrypto + crypto account balance * exchangeRateCrypto (which would equal 1).
My current query without the exchangeRateCrypto and totalCrypto looks like:
db.accounts.aggregate([
{ $unwind: '$accounts' },
{ $match: { 'accounts.currency': { $in: [ 'USD', 'BTC' ] }}},
{
$group: {
_id: '$_id',
time: { $first: '$time' },
profileId: { $first: '$profileId' },
accounts: { $push: '$accounts' },
totalUSD: { $sum: { $multiply: [ { $toDouble: '$accounts.balance' }, { $toDouble: '$accounts.exchangeRateUSD' } ] } }
}
}
]);
I'm trying to figure out how to 'reach' into the BTC row and calculate the exchangeRateCrypto by simply doing 1 / exchangeRateUSD and then projecting/returning the accounts document and subdocument as:
{
"_id" : ObjectId("1"),
"time" : ISODate("2018-10-20T05:57:15.372Z"),
"profileId" : "1",
"totalUSD" : "1015.5513030613",
"totalCrypto" : "0.1590527953", // 530.7934159683763 * 0.0001566171939 + 0.07592142 * 1
"accounts" : [
{
"_id" : ObjectId("2"),
"accountId" : "1",
"currency" : "USD",
"balance" : "530.7934159683763000",
"available" : "530.7934159683763",
"hold" : "0.0000000000000000",
"exchangeRateUSD" : "1",
"exchangeRateCrypto" : "0.0001566171939", // 1 / 6384.995
},
{
"_id" : ObjectId("5"),
"accountId" : "4",
"currency" : "BTC",
"balance" : "0.0759214200000000",
"available" : "0.07592142",
"hold" : "0.0000000000000000",
"exchangeRateUSD" : "6384.995",
"exchangeRateCrypto" : "1"
},
],
}
but haven't been able to figure out a good way of doing this.
It seems it should be pretty straightforward, but still learning Mongo.
Any tips?
Thanks!
The solution might be a bit long and probably it can be shortened however I want you to understand proposed way of thinking step by step.
var secondCurrency = "BTC";
var secondCurrencyFieldName = "exchangeRate" + secondCurrency;
var secondCurrencyFieldNameRef = "$" + secondCurrencyFieldName;
var totalFieldName = "total" + secondCurrency;
db.accounts.aggregate([
{ $unwind: "$accounts" },
{ $match: { "accounts.currency": { $in: [ "USD", secondCurrency ] }}},
{
$group: {
_id: "$_id",
time: { $first: "$time" },
profileId: { $first: "$profileId" },
accounts: { $push: "$accounts" },
totalUSD: { $sum: { $multiply: [ { $toDouble: "$accounts.balance" }, { $toDouble: "$accounts.exchangeRateUSD" } ] } }
}
},
{
$addFields: {
[secondCurrencyFieldName]: {
$filter: {
input: "$accounts",
as: "account",
cond: { $eq: [ "$$account.currency", secondCurrency ] }
}
}
}
},
{
$addFields: {
[secondCurrencyFieldName]: {
$let: {
vars: { first: { $arrayElemAt: [ secondCurrencyFieldNameRef, 0 ] } },
in: { $toDouble: "$$first.exchangeRateUSD" }
}
}
}
},
{
$addFields: {
accounts: {
$map: {
input: "$accounts",
as: "account",
in: {
$mergeObjects: [
"$$account",
{
[secondCurrencyFieldName]: {
$cond: [ { $eq: [ "$$account.currency", secondCurrency ] }, 1, { $divide: [ 1, secondCurrencyFieldNameRef ] } ]
}
}
]
}
}
}
}
},
{
$addFields: {
[totalFieldName]: {
$reduce: {
input: "$accounts",
initialValue: 0,
in: {
$add: [
"$$value",
{ $multiply: [ { $toDouble: "$$this.balance" }, "$$this." + secondCurrencyFieldName ] }
]
}
}
}
}
}
]).pretty()
So we can start with $addFields which can either add new field to existing document or repace existing field. After the $group stage you have to find the USD-XXX exchange rate (using $filter and $let + $arrayElemAt in the next pipeline stage). Having this value you can use $addFields again combined with $map and $mergeObjects to add new field to nested array and that field will represent the ratio between USD and XXX currency. Then you can use $addFields again with $reduce to get the total of all accounts for XXX currency.
Output:
{
"_id" : ObjectId("5beeec9fef99bb86541abf7f"),
"time" : ISODate("2018-10-20T05:57:15.372Z"),
"profileId" : "1",
"accounts" : [
{
"_id" : ObjectId("5beeec9fef99bb86541abf7d"),
"accountId" : "1",
"currency" : "USD",
"balance" : "530.7934159683763000",
"available" : "530.7934159683763",
"hold" : "0.0000000000000000",
"exchangeRateUSD" : "1",
"exchangeRateBTC" : 0.00015661719390539853
},
{
"_id" : ObjectId("5beeec9fef99bb86541abf7e"),
"accountId" : "4",
"currency" : "BTC",
"balance" : "0.0759214200000000",
"available" : "0.07592142",
"hold" : "0.0000000000000000",
"exchangeRateUSD" : "6384.995",
"exchangeRateBTC" : 1
}
],
"totalUSD" : 1015.5513030612763,
"exchangeRateBTC" : 6384.995,
"totalexchangeRateBTC" : 0.15905279535242806
}

MongoDB $group will not allow me to $project extra fields

I almost got this one working, but I simply cannot figure out why the $project part does not work for normal fields....
This is "invoice" table:
{
"_id" : "AS6D0",
"invoiceNumber" : 23,
"bookingId" : "AS6D0",
"createDate" : 1490369414,
"dueDate" : 1490369414,
"invoiceLines" : [
{
"lineText" : "Rent Price",
"amountPcs" : "8 x 7500",
"amountTotal" : 60000
},
{
"lineText" : "Discount(TIKO10)",
"amountPcs" : "10%",
"amountTotal" : -10000
},
{
"lineText" : "Final cleaning",
"amountPcs" : "1 x 5000",
"amountTotal" : 5000
},
{
"lineText" : "Reservation fee paid already",
"amountPcs" : "1 x -20000",
"amountTotal" : -20000
}
],
"managerId" : "4M4KE"
}
And this is my query
db.getCollection('invoice').aggregate([
{
$match: {
bookingId: "AS6D0"
}
},
{
$unwind: "$invoiceLines"
},
{
$group: {
_id: "$_id",
sum: {$sum: "$invoiceLines.amountTotal"}
}
},
{
$project:{
"_id" : 0,
"invoiceNumber" : 1,
"dueDate" : 1,
"sum" : 1
}
}
])
I get the _id and the sum, but it wont show invoiceNumber and dueDate
You could use a trick like this :
db.getCollection('invoice').aggregate([
{ $match: { } },
{ $unwind: "$invoiceLines" },
{ $group: { _id: "$_id",
sum: {$sum: "$invoiceLines.amountTotal"},
invoiceNumber: { $addToSet: "$invoiceNumber" },
dueDate: { $addToSet: "$dueDate" } } }
]);
Thanks to Mateo, this is what I ended up with:
(I do the unwind on the fields to avoid single value arrays)
Update : You don't have to $addToSet to reduce the fields into single value arrays and $unwind. Use $first instead.
db.getCollection('invoice').aggregate([
{
$match: {
bookingId: "AS6D0"
}
},
{
$unwind: "$invoiceLines"
},
{
$group: {
_id: "$_id",
sum: {$sum: "$invoiceLines.amountTotal"},
invoiceNumber: { $first: "$invoiceNumber" },
dueDate: { $first: "$dueDate" }
}
},
{
$project:{
"_id" : 0,
"invoiceNumber" : 1,
"dueDate" : 1,
"sum" : 1
}
}
])