Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions controllers/cartController.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const prisma = require('../config/prisma');
const redisClient = require('../config/redisConfig');
const logger = require('../utils/logger');
const { validateCart } = require('../utils/validator');
const { fetchGrossRates } = require('../services/currencyService');

exports.addToCart = async (req, res) => {
const isValid = validateCart(req.body);
Expand All @@ -13,7 +14,7 @@ exports.addToCart = async (req, res) => {
const userId = req.user.id;

try {
// 1. Validate product exists and is active
// Validate product exists and is active
const product = await prisma.product.findUnique({
where: { id: productId }
});
Expand All @@ -22,7 +23,7 @@ exports.addToCart = async (req, res) => {
return res.status(404).json({ message: 'Product not found or currently inactive.' });
}

// 2. Upsert cart item
// Insert cart item into the database
const cartItem = await prisma.cartItem.upsert({
where: { unique_user_product: { userId, productId } },
update: { quantity: { increment: quantity } },
Expand All @@ -39,7 +40,7 @@ exports.addToCart = async (req, res) => {
exports.getCart = async (req, res) => {
const userId = req.user.id;
// Assume req.user.baseCurrency is attached by your Auth middleware from the User model
const userCurrency = req.user.baseCurrency || 'USD';
const userCurrency = req.user.baseCurrency;

try {
const items = await prisma.cartItem.findMany({
Expand All @@ -48,26 +49,31 @@ exports.getCart = async (req, res) => {
});

const cachedRates = await redisClient.get('rates-cache:global');
const ratesInfo = cachedRates ? JSON.parse(cachedRates) : null;
const ratesInfo = cachedRates ? JSON.parse(cachedRates) : await fetchGrossRates();
const rates = ratesInfo?.rates;

let totalInBase = 0;
const formattedItems = items.map(item => {
const priceUSD = item.product.price;

// Logic: Your sync tool saves { "NGN": { "USD": 0.00074 } }
// To get NGN from USD, we divide by the NGN-to-USD rate
const rateToUSD = rates?.[userCurrency]?.['USD'] || 1;
const convertedPrice = priceUSD / rateToUSD;
const productPrice = item.product.price;
const merchantCurrency = item.product.currency;

const subtotal = convertedPrice * item.quantity;
// To get the total payment amount for each product depending on the rates and quantity
let unitPrice;
if (merchantCurrency !== userCurrency) {
unitPrice = rates?.[merchantCurrency]?.[userCurrency] * productPrice;
} else {
unitPrice = productPrice;
}

const subtotal = unitPrice * item.quantity;
totalInBase += subtotal;

return {
id: item.id,
productName: item.product.name,
quantity: item.quantity,
unitPriceInBase: Number(convertedPrice.toFixed(2)),
unitPriceInBase: Number(unitPrice.toFixed(2)),
subtotalInBase: Number(subtotal.toFixed(2))
};
});
Expand All @@ -90,7 +96,7 @@ exports.removeFromCart = async (req, res) => {
const userId = req.user.id;

try {
// Ensure user owns the item before deleting
// Ensure the user owns the item before deleting
const deleted = await prisma.cartItem.deleteMany({
where: { id: itemId, userId }
});
Expand Down
1 change: 0 additions & 1 deletion controllers/productController.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ exports.createProduct = async (req, res) => {
success: false,
error: error.details[0].message
});

}
try {
const merchantId = req.user.id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,12 +330,12 @@ <h1><a href="../../index.html">All files</a> / <a href="index.html">PanAfrik_Sto
&nbsp;
let totalInBase = 0;
const formattedItems = items.map(item =&gt; {
const priceUSD = item.product.price;
const merchantCurrency = item.product.price;
&nbsp;
// Logic: Your sync tool saves { "NGN": { "USD": 0.00074 } }
// To get NGN from USD, we divide by the NGN-to-USD rate
const rateToUSD = rates?.[userCurrency]?.['USD'] || 1;
const convertedPrice = priceUSD / rateToUSD;
const convertedPrice = merchantCurrency / rateToUSD;
&nbsp;
const subtotal = convertedPrice * item.quantity;
totalInBase += subtotal;
Expand Down
55 changes: 38 additions & 17 deletions test/cartController.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ describe('🛒 Comprehensive Cart Controller Suite', () => {
describe('POST /cart (Add Item)', () => {
test('❌ Should return 400 if validation fails', async () => {
validateCart.mockReturnValue(false);
const res = await request(app).post('/cart').send({ quantity: -1 });
const res = await request(app)
.post('/cart')
.send({ quantity: -1 });
expect(res.status).toBe(400);
expect(res.body.message).toContain('Invalid cart item data');
});
Expand Down Expand Up @@ -70,44 +72,63 @@ describe('🛒 Comprehensive Cart Controller Suite', () => {
});

describe('GET /cart (Fetch & Convert)', () => {
// Helper to mock the auth middleware behavior
const mockUser = { id: 'u123', baseCurrency: 'NGN' };

test('💰 Should correctly calculate totals and convert to NGN', async () => {
// Mock Redis Rates: 1 NGN = 0.00074 USD
redisClient.get.mockResolvedValue(JSON.stringify({
rates: { "NGN": { "USD": 0.00074 } },
/**
* The controller uses: unitPrice = rates[merchantCurrency][userCurrency] * price
* Rate Mock: GHS to NGN = 150.50
*/
const mockRates = {
rates: {
"GHS": { "NGN": 150.50 },
"NGN": { "NGN": 1 }
},
fetched_at: new Date().toISOString(),
stale: false
}));
};

redisClient.get.mockResolvedValue(JSON.stringify(mockRates));

// Mock 2 items in a cart (Prices in USD)
// Mock 2 items
prisma.cartItem.findMany.mockResolvedValue([
{ id: 'c1', quantity: 1, product: { name: 'Item A', price: 10.00 } }, // $10
{ id: 'c2', quantity: 2, product: { name: 'Item B', price: 5.00 } } // $10
{ id: 'c1', quantity: 2, product: { name: 'Item A', price: 10.00, currency: 'GHS'} }, // 10 * 150.50 = 1505.00
{ id: 'c2', quantity: 1, product: { name: 'Item B', price: 500.00, currency: 'NGN' } } // Same currency, stays 500
]);

const res = await request(app).get('/cart');
// Note: You must ensure your test setup/middleware applies 'mockUser' to 'req.user'
const res = await request(app)
.get('/cart')
.set('user', JSON.stringify(mockUser)); // Adjustment depends on your auth mock strategy

expect(res.status).toBe(200);
expect(res.body.currency).toBe('NGN');

/**
* Math Check:
* Total USD = 20.00
* Converted Total = 20 / 0.00074 = 27027.03
* Item A: (10 * 150.50) * 2 = 3010.00
* Item B: (500 * 1) * 1 = 500.00
* Total = 3510.00
*/
expect(res.body.total).toBe(27027.03);
expect(res.body.items[0].unitPriceInBase).toBe(13513.51); // 10 / 0.00074
expect(res.body.items[0].unitPriceInBase).toBe(1505.00);
expect(res.body.items[0].subtotalInBase).toBe(3010.00);
expect(res.body.total).toBe(3510.00);
});

test('🛡️ Should fallback to USD (rate 1) if Redis is empty', async () => {
test('🛡️ Should fallback to original price if rates are missing', async () => {
// Mock Redis returning null (triggering fetchGrossRates or default logic)
redisClient.get.mockResolvedValue(null);

// Mocking the scenario where ratesInfo?.rates is undefined
prisma.cartItem.findMany.mockResolvedValue([
{ id: 'c1', quantity: 1, product: { name: 'Item A', price: 50.00 } }
{ id: 'c1', quantity: 1, product: { name: 'Item A', price: 50.00, currency: 'NGN' } }
]);

const res = await request(app)
.get('/cart');
const res = await request(app).get('/cart');

expect(res.status).toBe(200);
// Since currency matches or rates are missing, price should remain unchanged
expect(res.body.total).toBe(50.00);
});
});
Expand Down