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

atlp-rwanda / e-commerce-ones-and-zeroes-bn / 4caa14ed-4935-440c-92e8-758f07a8399c

05 Jul 2024 06:02PM UTC coverage: 90.476% (-1.9%) from 92.387%
4caa14ed-4935-440c-92e8-758f07a8399c

push

circleci

web-flow
Merge pull request #65 from atlp-rwanda/bg-fix-productAvailability-pagination

[fixes #187355110] product availability as paginated

251 of 293 branches covered (85.67%)

Branch coverage included in aggregate %.

0 of 8 new or added lines in 1 file covered. (0.0%)

4 existing lines in 1 file now uncovered.

813 of 883 relevant lines covered (92.07%)

2.9 hits per line

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

87.22
/src/controllers/productController.ts
1
import { Request, Response } from 'express';
2
import cloudinary from '../helps/cloudinaryConfig';
4✔
3
import { db } from '../database/models/index';
4✔
4
import { verify } from 'crypto';
5
import { authenticateToken } from '../config/jwt.token';
6
import ProductService from '../services/productService';
4✔
7
import CollectionService from '../services/collectionService';
4✔
8
import jwt from 'jsonwebtoken';
4✔
9
import dotenv from 'dotenv';
10

11
import { validateEmail, validatePassword } from '../validations/validations';
12
import path from 'path';
13
import { Console, log } from 'console';
14
import { logger } from 'sequelize/types/utils/logger';
15
import upload from '../middleware/multer';
16
import { UploadApiResponse, ResourceType } from 'cloudinary';
17

18
interface User {
19
  role: string;
20
  userId: string;
21
  userproductId: string;
22
}
23

24
export interface CustomRequest extends Request {
25
  user?: User;
26
  files?: any;
27
}
28

29
export async function createCollection(req: CustomRequest, res: Response) {
4✔
30
  try {
5✔
31
    const userInfo = req.user;
5✔
32
    const { name } = req.body;
5✔
33
    const sellerId = userInfo?.userId;
5✔
34

35
    if (!name || !sellerId) {
5✔
36
      return res.status(400).json({ error: 'Name and sellerId are required' });
1✔
37
    }
38

39
    const user = await db.User.findByPk(sellerId);
4✔
40
    if (!user) {
3✔
41
      return res.status(404).json({ error: 'User not found' });
1✔
42
    }
43

44
    const existingCollection = await db.Collection.findOne({
2✔
45
      where: {
46
        name: name,
47
        sellerId: sellerId,
48
      },
49
    });
50

51
    if (existingCollection) {
2✔
52
      return res.status(400).json({ error: 'Collection already exists' });
1✔
53
    }
54

55
    const collection = await db.Collection.create({
1✔
56
      name: name,
57
      sellerId: sellerId,
58
    });
59

60
    return res.status(201).json(collection);
1✔
61
  } catch (error) {
62
    return res.status(500).json({ error: 'Internal Server Error' });
1✔
63
  }
64
}
65

66
export async function createProduct(req: CustomRequest, res: Response) {
4✔
67
  try {
8✔
68
    const { collectionId } = req.params;
8✔
69
    const { name, price, category, quantity, expiryDate, bonus } = req.body;
8✔
70

71
    if (!name || !price || !category || !quantity) {
8✔
72
      return res.status(400).json({ error: 'All fields are required' });
1✔
73
    }
74
    const fileImages = req.files;
7✔
75
    if (!fileImages || fileImages.length === 0) {
7✔
76
      return res.status(400).json({ message: 'No images given' });
1✔
77
    }
78

79
    const collection = await db.Collection.findByPk(collectionId);
6✔
80
    if (!collection) {
6✔
81
      return res.status(404).json({ message: 'Collection not found' });
1✔
82
    }
83

84
    const existingProduct = await db.Product.findOne({
5✔
85
      where: {
86
        name: name,
87
        collectionId: collectionId,
88
      },
89
    });
90

91
    if (existingProduct) {
5✔
92
      return res.status(400).json({
1✔
93
        message: 'Product already exists in this collection',
94
        existingProduct,
95
      });
96
    }
97

98
    if (fileImages.length < 4 || fileImages.length > 8) {
4✔
99
      return res
2✔
100
        .status(400)
101
        .json({ error: 'Product must have between 4 to 8 images' });
102
    }
103

104
    let uploadedImageUrls: any = [];
2✔
105
    for (let i = 0; i < fileImages.length; i++) {
2✔
106
      const file = fileImages[i];
8✔
107
      const base64String = file.buffer.toString('base64');
8✔
108
      const fileBase64 = `data:${file.mimetype};base64,${base64String}`;
8✔
109
      const result = await cloudinary.uploader.upload(fileBase64);
8✔
110
      uploadedImageUrls.push(result.secure_url);
8✔
111
    }
112

113
    const product = await db.Product.create({
2✔
114
      name,
115
      price,
116
      category,
117
      quantity,
118
      expiryDate: expiryDate || null,
2!
119
      bonus: bonus || null,
2!
120
      images: uploadedImageUrls,
121
      collectionId,
122
    });
123

124
    return res
1✔
125
      .status(201)
126
      .json({ message: 'Product added successfully', product });
127
  } catch (error) {
128
    return res.status(500).json({ message: 'Internal Server Error', error });
1✔
129
  }
130
}
131

132
export async function getProducts(req: any, res: Response) {
4✔
133
  const products = await db.Product.findAll({
2✔
134
    where: {
135
      expired: false,
136
    },
137
  });
138
  if (products.length <= 0) {
2✔
139
    return res.status(404).json({ message: 'no Products in store' });
1✔
140
  }
141
  return res.status(200).json(products);
1✔
142
}
143

144
export class ProductController {
4✔
145
  static async getAvailableProduct(req: Request, res: Response) {
UNCOV
146
    try {
×
NEW
147
      const page = parseInt(req.query.page as string, 10) || 1;
×
NEW
148
      const productPerPage = 10;
×
NEW
149
      const offset = (page - 1) * productPerPage;
×
150

151
      const { count, rows: allAvailableProducts } =
NEW
152
        await db.Product.findAndCountAll({
×
153
          where: {
154
            isAvailable: true,
155
          },
156
          limit: productPerPage,
157
          offset: offset,
158
        });
159

UNCOV
160
      if (!allAvailableProducts.length) {
×
NEW
161
        return res.status(200).json([]);
×
162
      }
163

NEW
164
      const totalPages = Math.ceil(count / productPerPage);
×
165

NEW
166
      const nextPage = page < totalPages ? page + 1 : null;
×
NEW
167
      const prevPage = page > 1 ? page - 1 : null;
×
168

UNCOV
169
      res.status(200).json({
×
170
        message: 'List of available products in our store',
171
        data: allAvailableProducts,
172
        pagination: {
173
          totalProducts: count,
174
          totalPages,
175
          productPerPage,
176
          currentPage: page,
177
          nextPage: nextPage
×
178
            ? `${req.protocol}://${req.get('host')}${req.baseUrl}${req.path}?page=${nextPage}`
179
            : null,
180
          prevPage: prevPage
×
181
            ? `${req.protocol}://${req.get('host')}${req.baseUrl}${req.path}?page=${prevPage}`
182
            : null,
183
        },
184
      });
185
    } catch (error) {
UNCOV
186
      return res.status(500).json({ message: 'Internal Server Error' });
×
187
    }
188
  }
189

190
  static async updateSingleProduct(req: Request, res: Response) {
191
    try {
5✔
192
      const { productId } = req.params;
5✔
193
      if (!productId) {
5✔
194
        return res
1✔
195
          .status(400)
196
          .json({ message: 'Product productId is required' });
197
      }
198

199
      const product = await db.Product.findOne({ where: { productId } });
4✔
200

201
      if (!product) {
3✔
202
        return res.status(404).json({ message: 'Product not found' });
1✔
203
      }
204

205
      const newStatus = !product.isAvailable;
2✔
206

207
      await db.Product.update(
2✔
208
        { isAvailable: newStatus },
209
        { where: { productId } },
210
      );
211

212
      res.status(200).json({
2✔
213
        message: `Product is successfully marked as ${newStatus ? 'available' : 'unavailable'}`,
2✔
214
        isAvailable: newStatus,
215
      });
216
    } catch (error) {
217
      res.status(500).json({ message: 'Internal Server Error' });
1✔
218
    }
219
  }
220

221
  static async getSingleProduct(req: Request, res: Response) {
222
    try {
2✔
223
      const { productId } = req.params;
2✔
224
      const singleProduct = await db.Product.findOne({
2✔
225
        where: {
226
          productId,
227
        },
228
      });
229
      return res.status(200).json({
1✔
230
        status: 'success',
231
        message: 'Retreived Product',
232
        data: singleProduct,
233
      });
234
    } catch (error: any) {
235
      return res
1✔
236
        .status(500)
237
        .json({ status: 'fail', message: 'Internal server error' });
238
    }
239
  }
240

241
  static async updateProduct(req: Request, res: Response) {
242
    try {
3✔
243
      const { productId } = req.params;
3✔
244
      const { name, description, category, bonus, price, quantity, discount } =
245
        req.body;
3✔
246
      // Find the product by ID
247
      const singleProduct = await db.Product.findOne({
3✔
248
        where: { productId },
249
      });
250

251
      if (!singleProduct) {
2✔
252
        return res.status(404).json({
1✔
253
          status: 'error',
254
          message: 'Product not found',
255
        });
256
      }
257

258
      if (
1!
259
        !req.body.name ||
7✔
260
        !req.body.description ||
261
        !req.body.category ||
262
        !req.body.bonus ||
263
        !req.body.price ||
264
        !req.body.quantity ||
265
        !req.body.discount
266
      ) {
267
        return res.status(400).json({
×
268
          status: 'error',
269
          message:
270
            'All fields (name, description, category, bonus, price, quantity, discount) are required',
271
        });
272
      }
273

274
      if (req.body.name) {
1✔
275
        singleProduct.name = req.body.name;
1✔
276
      }
277
      if (req.body.description) {
1✔
278
        singleProduct.description = req.body.description;
1✔
279
      }
280
      if (req.body.category) {
1✔
281
        singleProduct.category = req.body.category;
1✔
282
      }
283
      if (req.body.bonus) {
1✔
284
        singleProduct.bonus = req.body.bonus;
1✔
285
      }
286
      if (req.body.price) {
1✔
287
        singleProduct.price = req.body.price;
1✔
288
      }
289
      if (req.body.quantity) {
1✔
290
        singleProduct.quantity = req.body.quantity;
1✔
291
      }
292
      if (req.body.discount) {
1✔
293
        singleProduct.discount = req.body.discount;
1✔
294
      }
295

296
      // Handle multiple file uploads if present
297
      if (req.files && Array.isArray(req.files)) {
1✔
298
        // Define resourceType if necessary. For image uploads, resource_type is typically 'image'.
299
        const resourceType = 'image';
1✔
300

301
        // Check if the total images will exceed the maximum allowed number
302
        if (req.files.length > 9) {
1!
303
          return res.status(400).json({
×
304
            status: 'error',
305
            message:
306
              'You reached the maximum number of images a product can have',
307
          });
308
        }
309

310
        // Upload each file to Cloudinary and update the product's images array
311
        const uploadPromises = req.files.map((file) =>
1✔
312
          cloudinary.uploader.upload(file.path, {
2✔
313
            resource_type: resourceType,
314
          }),
315
        );
316

317
        const results = await Promise.all(uploadPromises);
1✔
318
        const uploadedUrls = results.map((result) => result.secure_url);
2✔
319
        singleProduct.images = [...uploadedUrls];
1✔
320
      }
321

322
      // Update the updatedAt field
323
      singleProduct.updatedAt = new Date();
1✔
324

325
      // Save the updated product
326
      await singleProduct.save();
1✔
327

328
      return res.status(200).json({
1✔
329
        status: 'success',
330
        message: 'Product updated successfully',
331
        data: singleProduct,
332
      });
333
    } catch (error: any) {
334
      return res.status(500).json({
1✔
335
        status: 'error',
336
        message: 'Internal Server Error',
337
        error: error.message,
338
      });
339
    }
340
  }
341
  static async removeProductImage(req: Request, res: Response) {
342
    const { id, images } = req.body;
5✔
343

344
    try {
5✔
345
      // Find the product by ID
346
      const product = await db.Product.findOne({ where: { id } });
5✔
347

348
      if (!product) {
5✔
349
        return res.status(404).json({
1✔
350
          status: 'Internal Server Error',
351
          error: 'Invalid image_url array in database',
352
        });
353
      }
354

355
      // Ensure image_url is a valid array
356
      if (!Array.isArray(product.images)) {
4✔
357
        return res.status(400).json({
1✔
358
          status: 'fail',
359
          error: 'Invalid image_url array in database',
360
        });
361
      }
362

363
      // Remove the image URL
364
      const updatedImages = product.images.filter(
3✔
365
        (url: string) =>
366
          url.trim().toLowerCase() !== images.trim().toLowerCase(),
4✔
367
      );
368

369
      // Check if any image was removed
370
      if (updatedImages.length === product.images.length) {
3✔
371
        return res.status(400).json({
1✔
372
          status: 'Bad Request',
373
          error: 'Image URL not found in product',
374
        });
375
      }
376

377
      // Update the product's images
378
      product.images = updatedImages;
2✔
379
      await product.save();
2✔
380

381
      return res.status(200).json({
1✔
382
        status: 'Image removed successfully',
383
        data: product,
384
      });
385
    } catch (err: any) {
386
      return res.status(500).json({
1✔
387
        status: 'Internal Server Error',
388
        error: err.message,
389
      });
390
    }
391
  }
392
  static async deleteProduct(req: any, res: any) {
393
    const { id } = req.params;
8✔
394

395
    const authHeader = req.headers.authorization;
8✔
396
    if (!authHeader) {
8✔
397
      return res.status(401).json({ error: 'Unauthorized' });
1✔
398
    }
399

400
    const token = authHeader.split(' ')[1];
7✔
401

402
    const jwtSecret = process.env.JWT_SECRET;
7✔
403
    if (!jwtSecret) {
7✔
404
      throw new Error('JWT_SECRET is not defined ');
1✔
405
    }
406

407
    let decoded: any;
408
    try {
6✔
409
      decoded = jwt.verify(token, jwtSecret);
6✔
410
    } catch (error) {
411
      return res.status(401).json({ error: 'Invalid or expired token' });
1✔
412
    }
413

414
    const { userId, role } = decoded;
5✔
415

416
    // Scenario 3: If the request body fails validation checks or the user is not a seller
417
    if (role !== 'seller') {
5✔
418
      return res
1✔
419
        .status(403)
420
        .json({ error: 'You must be a seller to delete a product.' });
421
    }
422

423
    const product = await ProductService.getProductById(id);
4✔
424

425
    // Scenario 1: Check if the user is a seller and if the product exists
426
    if (!product) {
4✔
427
      return res.status(404).json({ error: 'Product not found.' });
1✔
428
    }
429

430
    // Fetch the collection that the product belongs to
431
    const collection = await CollectionService.getCollectionById(
3✔
432
      product.collectionId,
433
    );
434

435
    if (!collection) {
3✔
436
      return res.status(404).json({ error: 'Collection not found.' });
1✔
437
    }
438

439
    if (collection.sellerId !== userId) {
2✔
440
      return res
1✔
441
        .status(403)
442
        .json({ error: 'You can only delete your own products.' });
443
    }
444

445
    // Scenario 2: If the user is a seller and the product exists, delete the product
446
    const deletedProduct = await ProductService.deleteProduct(id);
1✔
447

448
    return res.status(200).json({
1✔
449
      message: 'Product deleted successfully.',
450
      product: deletedProduct,
451
    });
452
  }
453
}
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