• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Seniru / defendxstore / 14995965526

13 May 2025 11:56AM UTC coverage: 77.843%. First build
14995965526

Pull #98

github

web-flow
Merge cbbbfb315 into 53385cce8
Pull Request #98: [WIP] New test cases

462 of 701 branches covered (65.91%)

Branch coverage included in aggregate %.

87 of 95 new or added lines in 10 files covered. (91.58%)

1400 of 1691 relevant lines covered (82.79%)

82.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.51
/backend/src/controllers/items.js
1
const mongoose = require("mongoose")
3✔
2
const Item = require("../models/Item")
3✔
3
const User = require("../models/User")
3✔
4
const Expense = require("../models/Expense")
3✔
5
const createResponse = require("../utils/createResponse")
3✔
6
const { StatusCodes } = require("http-status-codes")
3✔
7
const { sendMail } = require("../services/email")
3✔
8
const { roles } = require("../utils/getRoles")
3✔
9
const Supply = require("../models/Supply")
3✔
10
const ExcelJS = require("exceljs")
3✔
11

12
const { columns, addTable, createAttachment } = require("../utils/spreadsheets")
3✔
13

14
require("dotenv").config()
3✔
15

16
const getAllItemsSpreadsheet = (res, items) => {
3✔
17
    const workbook = new ExcelJS.Workbook()
×
18
    const worksheet = workbook.addWorksheet("Inventory")
×
19
    worksheet.columns = columns.items
×
20
    addTable(
×
21
        worksheet,
22
        [
23
            "Item Name",
24
            "Category",
25
            "Description",
26
            "Colors",
27
            "Price (LKR)",
28
            "Size",
29
            "Quantity",
30
            "Stock Status",
31
        ],
32
        items.map((product) => [
×
33
            product.itemName,
34
            product.category,
35
            product.description || "",
×
36
            Array.isArray(product.colors) ? product.colors.join(", ") : "",
×
37
            product.price,
38
            product.size,
39
            product.quantity,
40
            product.stock,
41
        ]),
42
    )
43

44
    worksheet.eachRow((row, rowNumber) => {
×
45
        if (rowNumber > 1) {
×
46
            row.eachCell((cell, colNumber) => {
×
47
                // Add color coding for stock status
48
                if (colNumber === 8) {
×
49
                    // Stock status column
50
                    const stockStatus = cell.value
×
51
                    if (stockStatus === "Out of Stock") {
×
52
                        cell.font = { color: { argb: "FFFF0000" } } // Red
×
53
                    } else if (stockStatus === "Running Low") {
×
54
                        cell.font = { color: { argb: "FFFF9900" } } //yellow
×
55
                    } else if (stockStatus === "In Stock") {
×
56
                        cell.font = { color: { argb: "FF008000" } } //green
×
57
                    }
58
                }
59
            })
60
        }
61
    })
62

63
    return createAttachment(workbook, res)
×
64
}
65

66
// Get All Items
67
const getAllItems = async (req, res, next) => {
3✔
68
    try {
3✔
69
        const { downloadSheet } = req.query
3✔
70
        const items = await Item.find().sort({ _id: -1 }).exec()
3✔
71
        if (downloadSheet == "true") return getAllItemsSpreadsheet(res, items)
3!
72
        createResponse(res, StatusCodes.OK, items)
3✔
73
    } catch (error) {
74
        next(error)
×
75
    }
76
}
77

78
// Get Item by ID
79
const getItemById = async (req, res) => {
3✔
80
    const { id } = req.params
9✔
81
    try {
9✔
82
        if (!mongoose.Types.ObjectId.isValid(id)) {
9✔
83
            return createResponse(res, StatusCodes.BAD_REQUEST, "Invalid id for item")
3✔
84
        }
85
        const item = await Item.findById(id)
6✔
86
        if (!item) {
6✔
87
            return createResponse(res, StatusCodes.NOT_FOUND, "Item not found")
3✔
88
        }
89
        return createResponse(res, StatusCodes.OK, item)
3✔
90
    } catch (error) {
91
        next(error)
×
92
    }
93
}
94

95
const getTrendingItems = async (req, res, next) => {
3✔
96
    try {
3✔
97
        const response = await fetch(`${process.env.AI_SERVICES_URI}/trending/items`)
3✔
98
        if (!response.ok)
2!
99
            return createResponse(res, response.status, response.body || response.statusText)
×
100

101
        let result = await response.json()
2✔
102
        result = result.slice(0, 8)
2✔
103

104
        const items = await Item.find({ _id: { $in: result.map((item) => item[0]) } }).exec()
2✔
105
        const itemMap = new Map(items.map((item) => [item._id.toString(), item]))
2✔
106
        const sortedItems = result.map((item) => itemMap.get(item[0]))
2✔
107

108
        return createResponse(res, StatusCodes.OK, sortedItems)
2✔
109
    } catch (error) {
110
        next(error)
1✔
111
    }
112
}
113

114
const getRecommendedItems = async (req, res, next) => {
3✔
115
    try {
3✔
116
        const user = await User.findOne({ username: req.user.username }).exec()
3✔
117
        const response = await fetch(
3✔
118
            `${process.env.AI_SERVICES_URI}/recommendations/items?user_id=${user._id}`,
119
        )
120
        if (!response.ok)
2!
121
            return createResponse(res, response.status, response.body || response.statusText)
×
122

123
        let result = await response.json()
2✔
124
        result = result.slice(0, 8)
2✔
125

126
        const items = await Item.find({ _id: { $in: result } }).exec()
2✔
127
        const itemMap = new Map(items.map((item) => [item._id.toString(), item]))
2✔
128
        const sortedItems = result.map((item) => itemMap.get(item))
2✔
129

130
        return createResponse(res, StatusCodes.OK, sortedItems)
2✔
131
    } catch (error) {
132
        next(error)
1✔
133
    }
134
}
135

136
// Create Item
137
const createItem = async (req, res, next) => {
3✔
138
    try {
6✔
139
        const item = req.body
6✔
140
        if (!item.itemName || !item.category || !item.price || !item.quantity || !item.colors)
6✔
141
            return createResponse(res, StatusCodes.BAD_REQUEST, "Missing required fields")
3✔
142
        item.size = item.size.split(",")
3✔
143
        const newItem = new Item(item)
3✔
144
        await newItem.save()
3✔
145
        return createResponse(res, StatusCodes.CREATED, newItem)
3✔
146
    } catch (error) {
NEW
147
        if (error instanceof mongoose.Error.ValidationError)
×
NEW
148
            return createResponse(res, StatusCodes.BAD_REQUEST, error.message)
×
149
        next(error)
×
150
    }
151
}
152

153
// Update Item
154
const updateItem = async (req, res, next) => {
3✔
155
    const { id } = req.params
9✔
156
    const item = req.body
9✔
157
    if (typeof item.size == "string") item.size = item.size.split(",")
9!
158
    try {
9✔
159
        if (!mongoose.Types.ObjectId.isValid(id)) {
9✔
160
            return createResponse(res, StatusCodes.BAD_REQUEST, "Invalid id for item")
3✔
161
        }
162

163
        const oldItem = await Item.findById(id)
6✔
164
        if (!oldItem) {
6✔
165
            return createResponse(res, StatusCodes.NOT_FOUND, "Item not found")
3✔
166
        }
167

168
        const updatedItem = await Item.findByIdAndUpdate(id, item, {
3✔
169
            new: true,
170
            runValidators: true,
171
        })
172

173
        if (oldItem.stock !== "Out of Stock" && updatedItem.stock === "Out of Stock") {
3!
174
            // Find all admin users
175
            const adminUsers = await User.find({
×
176
                role: { $bitsAllSet: roles.ADMIN },
177
            })
178

179
            if (adminUsers && adminUsers.length > 0) {
×
180
                //  table row for the email template
181
                const itemRow = `
×
182
                    <tr>
183
                        <td style="padding: 12px; border: 1px solid #ddd;">${updatedItem.itemName}</td>
184
                        <td style="padding: 12px; border: 1px solid #ddd;">${updatedItem.category}</td>
185
                        <td style="padding: 12px; border: 1px solid #ddd; text-align: center;">${updatedItem.quantity}</td>
186
                        <td style="padding: 12px; border: 1px solid #ddd; text-align: center; color: #ff0000; font-weight: bold;">Out of Stock</td>
187
                    </tr>
188
                `
189

190
                // Send email to all admin users
191
                for (const admin of adminUsers) {
×
192
                    sendMail(admin.email, "URGENT: Item Out of Stock Alert", "stock_alert", {
×
193
                        title: "Out of Stock Alert",
194
                        itemCount: "1",
195
                        items: itemRow,
196
                        date: new Date().toLocaleDateString(),
197
                    })
198
                }
199
            }
200
        }
201

202
        return createResponse(res, StatusCodes.OK, updatedItem)
3✔
203
    } catch (error) {
NEW
204
        if (error instanceof mongoose.Error.ValidationError)
×
NEW
205
            return createResponse(res, StatusCodes.BAD_REQUEST, error.message)
×
206
        next(error)
×
207
    }
208
}
209

210
const restockItem = async (req, res, next) => {
3✔
211
    try {
18✔
212
        const { id } = req.params
18✔
213
        const { amount } = req.body
18✔
214

215
        if (!mongoose.Types.ObjectId.isValid(id))
18✔
216
            return createResponse(res, StatusCodes.BAD_REQUEST, "Invalid id for item")
3✔
217

218
        if (!amount && amount !== 0)
15✔
219
            return createResponse(res, StatusCodes.BAD_REQUEST, "amount is not provided")
9✔
220
        if (amount < 0)
6!
NEW
221
            return createResponse(res, StatusCodes.BAD_REQUEST, "amount cannot be negative")
×
222
        if (amount % 1 !== 0)
6!
NEW
223
            return createResponse(res, StatusCodes.BAD_REQUEST, "amount must be an integer")
×
224

225
        if (!amount) return createResponse(res, StatusCodes.BAD_REQUEST, "amount is not provided")
6!
226

227
        const item = await Item.findByIdAndUpdate(id, { quantity: amount }).exec()
6✔
228
        if (!item) return createResponse(res, StatusCodes.NOT_FOUND, "Item not found")
6✔
229
        const restockedAmount = amount - item.quantity
3✔
230
        const sellingPrice = item.price * restockedAmount
3✔
231
        const cost = sellingPrice * 0.85
3✔
232

233
        await Expense.create({
3✔
234
            date: Date.now(),
235
            amount: cost,
236
            description: `Restock ${item.itemName}`,
237
            category: "Supply costs",
238
        })
239

240
        await Supply.create({
3✔
241
            item: id,
242
            date: Date.now(),
243
            orderedQuantity: restockedAmount,
244
            estimatedCost: cost,
245
            estimatedSellingPrice: sellingPrice,
246
            estimatedProfit: sellingPrice - cost,
247
        })
248

249
        if (!item) return createResponse(res, StatusCodes.NOT_FOUND, "Item not found")
3!
250
        return createResponse(res, StatusCodes.OK, "Restocked")
3✔
251
    } catch (error) {
252
        next(error)
×
253
    }
254
}
255

256
// Delete Item
257
const deleteItem = async (req, res) => {
3✔
258
    const { id } = req.params
9✔
259
    try {
9✔
260
        if (!mongoose.Types.ObjectId.isValid(id)) {
9✔
261
            return createResponse(res, StatusCodes.BAD_REQUEST, "Invalid id for item")
3✔
262
        }
263
        const deletedItem = await Item.findByIdAndDelete(id)
6✔
264
        if (!deletedItem) {
6✔
265
            return createResponse(res, StatusCodes.NOT_FOUND, "Item not found")
3✔
266
        }
267
        return createResponse(res, StatusCodes.OK, "Item deleted")
3✔
268
    } catch (error) {
269
        next(error)
×
270
    }
271
}
272

273
module.exports = {
3✔
274
    getAllItems,
275
    getItemById,
276
    getTrendingItems,
277
    getRecommendedItems,
278
    createItem,
279
    updateItem,
280
    restockItem,
281
    deleteItem,
282
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc