Skip to main content
Glama

Superprecio MCP Server

by bunkerapps
functional-shopping-list.test.tsβ€’21.9 kB
/** * Functional Test: Complete Shopping List Workflow * * This test simulates a real user creating a shopping list, * adding items, optimizing it, and finding nearby stores. * * Uses MOCK data to avoid hitting real APIs. */ import { describe, it, expect, beforeAll } from '@jest/globals'; // Mock API client class MockSuperPrecioApiClient { private lists: any[] = []; private alerts: any[] = []; private nextListId = 1; private nextAlertId = 1; // Mock supermarket data (11 supermarkets in Argentina) private mockSupermarkets = [ { id: 1, name: 'Carrefour', logo: 'carrefour.png', url: 'https://carrefour.com.ar' }, { id: 2, name: 'Disco', logo: 'disco.png', url: 'https://disco.com.ar' }, { id: 3, name: 'Jumbo', logo: 'jumbo.png', url: 'https://jumbo.com.ar' }, { id: 4, name: 'Dia', logo: 'dia.png', url: 'https://dia.com.ar' }, { id: 5, name: 'Coto', logo: 'coto.png', url: 'https://coto.com.ar' }, ]; // Mock product prices by supermarket private mockPrices: Record<string, Record<string, number>> = { 'leche descremada': { Carrefour: 850, Disco: 920, Jumbo: 880, Dia: 790, Coto: 870 }, 'pan lactal': { Carrefour: 450, Disco: 480, Jumbo: 460, Dia: 420, Coto: 465 }, 'arroz integral': { Carrefour: 620, Disco: 680, Jumbo: 650, Dia: 600, Coto: 640 }, 'aceite girasol': { Carrefour: 1200, Disco: 1350, Jumbo: 1280, Dia: 1150, Coto: 1300 }, 'azucar': { Carrefour: 380, Disco: 420, Jumbo: 400, Dia: 350, Coto: 390 }, 'cafe torrado': { Carrefour: 950, Disco: 1020, Jumbo: 980, Dia: 890, Coto: 960 }, 'yerba mate': { Carrefour: 1100, Disco: 1200, Jumbo: 1150, Dia: 1050, Coto: 1120 }, 'fideos': { Carrefour: 280, Disco: 320, Jumbo: 300, Dia: 260, Coto: 290 }, 'tomate': { Carrefour: 180, Disco: 200, Jumbo: 190, Dia: 170, Coto: 185 }, 'lechuga': { Carrefour: 150, Disco: 170, Jumbo: 160, Dia: 140, Coto: 155 }, }; // Mock supermarket locations (Buenos Aires area) private mockLocations = [ { id: 1, supermarketId: 1, supermarketName: 'Carrefour', branchName: 'Carrefour Express Palermo', address: 'Av. Santa Fe 3253', city: 'Buenos Aires', latitude: -34.5889, longitude: -58.4044, phone: '011-4823-5000', openingHours: '8:00 - 22:00', }, { id: 2, supermarketId: 1, supermarketName: 'Carrefour', branchName: 'Carrefour Recoleta', address: 'Av. Callao 1234', city: 'Buenos Aires', latitude: -34.5950, longitude: -58.3920, phone: '011-4812-6000', openingHours: '8:00 - 22:00', }, { id: 3, supermarketId: 4, supermarketName: 'Dia', branchName: 'Dia % Villa Crespo', address: 'Av. Corrientes 5678', city: 'Buenos Aires', latitude: -34.5990, longitude: -58.4350, phone: '011-4855-2000', openingHours: '7:30 - 21:30', }, ]; async createShoppingList(data: any) { const list = { id: this.nextListId++, name: data.name, description: data.description || null, userId: data.userId || null, isActive: true, items: data.items || [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; // Add IDs to items list.items = list.items.map((item: any, index: number) => ({ id: index + 1, shoppingListId: list.id, productName: item.productName, barcode: item.barcode || null, quantity: item.quantity || 1, notes: item.notes || null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), })); this.lists.push(list); return { success: true, data: list, message: 'Shopping list created successfully', }; } async addItemsToList(listId: number, items: any[]) { const list = this.lists.find((l) => l.id === listId); if (!list) { return { success: false, message: 'Shopping list not found', }; } const newItems = items.map((item, index) => ({ id: list.items.length + index + 1, shoppingListId: list.id, productName: item.productName, barcode: item.barcode || null, quantity: item.quantity || 1, notes: item.notes || null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), })); list.items.push(...newItems); list.updatedAt = new Date().toISOString(); return { success: true, data: list, addedItems: newItems.length, message: `${newItems.length} items added successfully`, }; } async getShoppingLists(params?: any) { let filtered = [...this.lists]; if (params?.userId) { filtered = filtered.filter((l) => l.userId === params.userId); } return { success: true, data: filtered, count: filtered.length, }; } async optimizeShoppingList(listId: number) { const list = this.lists.find((l) => l.id === listId); if (!list) { return { success: false, message: 'Shopping list not found', }; } if (list.items.length === 0) { return { success: false, message: 'Shopping list has no items', }; } // Calculate totals for each supermarket const results: Record<string, any> = {}; this.mockSupermarkets.forEach((market) => { results[market.name] = { marketId: market.id, marketName: market.name, marketLogo: market.logo, marketUrl: market.url, total: 0, items: [], foundItems: 0, missingItems: 0, }; }); // Calculate prices list.items.forEach((item: any) => { const productName = item.productName.toLowerCase(); const prices = this.mockPrices[productName]; if (prices) { Object.keys(prices).forEach((marketName) => { if (results[marketName]) { const unitPrice = prices[marketName]; const totalPrice = unitPrice * item.quantity; results[marketName].total += totalPrice; results[marketName].foundItems++; results[marketName].items.push({ listItemId: item.id, productName: item.productName, quantity: item.quantity, unitPrice, totalPrice, image: `https://example.com/${productName.replace(/\s/g, '-')}.jpg`, link: `https://example.com/product/${productName.replace(/\s/g, '-')}`, }); } }); } else { // Product not found in mock data this.mockSupermarkets.forEach((market) => { results[market.name].missingItems++; }); } }); // Sort by total (lowest first) const ranked = Object.values(results) .filter((r: any) => r.foundItems > 0) .sort((a: any, b: any) => a.total - b.total); const bestMarket = ranked[0]; const worstMarket = ranked[ranked.length - 1]; const savings = worstMarket.total - bestMarket.total; const savingsPercent = ((savings / worstMarket.total) * 100).toFixed(1); return { success: true, listId: list.id, listName: list.name, totalItems: list.items.length, bestMarket: { name: bestMarket.marketName, logo: bestMarket.marketLogo, total: parseFloat(bestMarket.total.toFixed(2)), foundItems: bestMarket.foundItems, missingItems: bestMarket.missingItems, items: bestMarket.items, }, savings: { amount: parseFloat(savings.toFixed(2)), percentage: parseFloat(savingsPercent), comparedTo: worstMarket.marketName, }, allMarkets: ranked.map((r: any) => ({ name: r.marketName, logo: r.marketLogo, total: parseFloat(r.total.toFixed(2)), foundItems: r.foundItems, missingItems: r.missingItems, })), marketsCompared: ranked.length, }; } async findNearbySupermarkets(params: { lat: number; lng: number; radius?: number }) { const { lat, lng, radius = 5 } = params; // Calculate distance using Haversine formula const calculateDistance = (lat1: number, lon1: number, lat2: number, lon2: number) => { const R = 6371; // Earth radius in km const dLat = ((lat2 - lat1) * Math.PI) / 180; const dLon = ((lon2 - lon1) * Math.PI) / 180; const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; }; const locationsWithDistance = this.mockLocations.map((loc) => ({ ...loc, distance: parseFloat(calculateDistance(lat, lng, loc.latitude, loc.longitude).toFixed(2)), distanceMeters: Math.round( calculateDistance(lat, lng, loc.latitude, loc.longitude) * 1000 ), coordinates: { latitude: loc.latitude, longitude: loc.longitude, }, })); const filtered = locationsWithDistance .filter((loc) => loc.distance <= radius) .sort((a, b) => a.distance - b.distance); return { success: true, searchLocation: { latitude: lat, longitude: lng }, radiusKm: radius, found: filtered.length, locations: filtered, }; } async createPriceAlert(data: any) { const alert = { id: this.nextAlertId++, userId: data.userId || null, productName: data.productName, barcode: data.barcode || null, targetPrice: data.targetPrice, currentPrice: null, isActive: true, notifyEnabled: data.notifyEnabled !== undefined ? data.notifyEnabled : true, lastCheckedAt: null, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; this.alerts.push(alert); return { success: true, data: alert, message: 'Price alert created successfully', }; } async checkPriceAlerts(params?: any) { let alertsToCheck = [...this.alerts]; if (params?.userId) { alertsToCheck = alertsToCheck.filter((a) => a.userId === params.userId); } if (params?.alertId) { alertsToCheck = alertsToCheck.filter((a) => a.id === params.alertId); } const results = alertsToCheck.map((alert) => { const productName = alert.productName.toLowerCase(); const prices = this.mockPrices[productName]; if (prices) { const lowestPrice = Math.min(...Object.values(prices)); const marketWithLowestPrice = Object.keys(prices).find( (k) => prices[k] === lowestPrice )!; const previousPrice = alert.currentPrice; alert.currentPrice = lowestPrice; alert.lastCheckedAt = new Date().toISOString(); const isTriggered = lowestPrice <= alert.targetPrice; const priceDifference = lowestPrice - alert.targetPrice; return { alertId: alert.id, productName: alert.productName, barcode: alert.barcode, targetPrice: alert.targetPrice, currentPrice: lowestPrice, previousPrice, priceChanged: previousPrice !== lowestPrice, priceDifference: parseFloat(priceDifference.toFixed(2)), isTriggered, bestMarket: marketWithLowestPrice, bestMarketLogo: `${marketWithLowestPrice.toLowerCase()}.png`, productImage: `https://example.com/${productName.replace(/\s/g, '-')}.jpg`, productLink: `https://example.com/product/${productName.replace(/\s/g, '-')}`, allPrices: Object.entries(prices).map(([market, price]) => ({ market, price, })), }; } return { alertId: alert.id, productName: alert.productName, barcode: alert.barcode, targetPrice: alert.targetPrice, currentPrice: null, isTriggered: false, error: 'Product not found in any supermarket', }; }); const triggeredCount = results.filter((r) => r.isTriggered).length; return { success: true, checked: alertsToCheck.length, triggered: triggeredCount, results, message: `Checked ${alertsToCheck.length} alerts, ${triggeredCount} triggered`, }; } } // Tests describe('Functional Test: Complete Shopping Workflow', () => { let client: MockSuperPrecioApiClient; let listId: number; beforeAll(() => { client = new MockSuperPrecioApiClient(); }); describe('πŸ“ Step 1: Create Shopping List', () => { it('should create a shopping list with initial items', async () => { const response = await client.createShoppingList({ name: 'Compra Semanal', description: 'Lista para la semana del 18-25 Octubre', items: [ { productName: 'Leche Descremada', quantity: 2 }, { productName: 'Pan Lactal', quantity: 1 }, { productName: 'Arroz Integral', quantity: 1 }, ], }); expect(response.success).toBe(true); expect(response.data.id).toBe(1); expect(response.data.name).toBe('Compra Semanal'); expect(response.data.items.length).toBe(3); expect(response.data.items[0].productName).toBe('Leche Descremada'); expect(response.data.items[0].quantity).toBe(2); listId = response.data.id; console.log('\nβœ… Shopping list created:'); console.log(` ID: ${response.data.id}`); console.log(` Name: ${response.data.name}`); console.log(` Items: ${response.data.items.length}`); }); }); describe('βž• Step 2: Add More Items', () => { it('should add additional items to the list', async () => { const response = await client.addItemsToList(listId, [ { productName: 'Aceite Girasol', quantity: 1 }, { productName: 'Azucar', quantity: 1 }, { productName: 'Cafe Torrado', quantity: 1 }, { productName: 'Yerba Mate', quantity: 2 }, ]); expect(response.success).toBe(true); expect(response.addedItems).toBe(4); expect(response.data.items.length).toBe(7); // 3 initial + 4 new console.log('\nβœ… Items added:'); console.log(` Added: ${response.addedItems} items`); console.log(` Total items in list: ${response.data.items.length}`); }); }); describe('πŸ“‹ Step 3: View All Lists', () => { it('should retrieve all shopping lists', async () => { const response = await client.getShoppingLists(); expect(response.success).toBe(true); expect(response.count).toBe(1); expect(response.data[0].items.length).toBe(7); console.log('\nβœ… Lists retrieved:'); console.log(` Total lists: ${response.count}`); console.log(` First list: "${response.data[0].name}" with ${response.data[0].items.length} items`); }); }); describe('πŸ”₯ Step 4: Optimize Shopping List (THE STAR FEATURE!)', () => { it('should find the best supermarket for the entire list', async () => { const response = await client.optimizeShoppingList(listId); expect(response.success).toBe(true); expect(response.bestMarket).toBeDefined(); expect(response.savings).toBeDefined(); expect(response.allMarkets.length).toBeGreaterThan(0); // Verify best market has lowest total const bestTotal = response.bestMarket.total; response.allMarkets.forEach((market: any) => { expect(market.total).toBeGreaterThanOrEqual(bestTotal); }); console.log('\nπŸ”₯ OPTIMIZATION RESULTS:'); console.log(` List: "${response.listName}"`); console.log(` Total items: ${response.totalItems}`); console.log(` Markets compared: ${response.marketsCompared}`); console.log('\n πŸ† BEST SUPERMARKET: ' + response.bestMarket.name); console.log(` πŸ’΅ Total: $${response.bestMarket.total.toLocaleString('es-AR')}`); console.log(` βœ… Found: ${response.bestMarket.foundItems}/${response.totalItems} items`); console.log('\n πŸ’° SAVINGS:'); console.log(` Amount: $${response.savings.amount.toLocaleString('es-AR')}`); console.log(` Percentage: ${response.savings.percentage}%`); console.log(` vs. ${response.savings.comparedTo}`); console.log('\n πŸ“Š ALL MARKETS (sorted by price):'); response.allMarkets.forEach((market: any, i: number) => { const badge = i === 0 ? 'πŸ†' : i === response.allMarkets.length - 1 ? '❌' : ' '; console.log(` ${badge} ${i + 1}. ${market.name.padEnd(15)} $${market.total.toLocaleString('es-AR').padStart(8)}`); }); }); it('should provide detailed breakdown of items at best market', async () => { const response = await client.optimizeShoppingList(listId); expect(response.bestMarket.items).toBeDefined(); expect(response.bestMarket.items.length).toBeGreaterThan(0); console.log(`\n πŸ›’ ITEMS AT ${response.bestMarket.name}:`); response.bestMarket.items.forEach((item: any, i: number) => { console.log( ` ${i + 1}. ${item.productName} - Qty: ${item.quantity} x $${item.unitPrice} = $${item.totalPrice}` ); }); }); }); describe('πŸ“ Step 5: Find Nearby Supermarkets', () => { it('should find supermarkets near Palermo, Buenos Aires', async () => { // Coordinates for Palermo, Buenos Aires const response = await client.findNearbySupermarkets({ lat: -34.5889, lng: -58.4044, radius: 5, }); expect(response.success).toBe(true); expect(response.found).toBeGreaterThan(0); expect(response.locations[0].distance).toBeLessThanOrEqual(5); // Verify sorted by distance for (let i = 1; i < response.locations.length; i++) { expect(response.locations[i].distance).toBeGreaterThanOrEqual( response.locations[i - 1].distance ); } console.log('\nπŸ“ NEARBY SUPERMARKETS:'); console.log(` Search location: ${response.searchLocation.latitude}, ${response.searchLocation.longitude}`); console.log(` Radius: ${response.radiusKm}km`); console.log(` Found: ${response.found} supermarkets\n`); response.locations.forEach((loc: any, i: number) => { console.log(` ${i + 1}. ${loc.supermarketName} - ${loc.branchName}`); console.log(` Distance: ${loc.distance}km`); console.log(` Address: ${loc.address}, ${loc.city}`); console.log(` Hours: ${loc.openingHours}\n`); }); }); }); describe('πŸ”” Step 6: Set Price Alert', () => { it('should create a price alert for an expensive product', async () => { const response = await client.createPriceAlert({ productName: 'Aceite Girasol', targetPrice: 1000, // Alert when price drops below $1000 }); expect(response.success).toBe(true); expect(response.data.id).toBe(1); expect(response.data.productName).toBe('Aceite Girasol'); expect(response.data.targetPrice).toBe(1000); expect(response.data.isActive).toBe(true); console.log('\nπŸ”” PRICE ALERT CREATED:'); console.log(` Product: ${response.data.productName}`); console.log(` Target Price: $${response.data.targetPrice}`); console.log(` ID: ${response.data.id}`); }); }); describe('πŸ” Step 7: Check Price Alerts', () => { it('should check current prices and trigger alerts if needed', async () => { const response = await client.checkPriceAlerts(); expect(response.success).toBe(true); expect(response.checked).toBe(1); expect(response.results.length).toBe(1); const alert = response.results[0]; expect(alert.currentPrice).toBeDefined(); expect(alert.bestMarket).toBeDefined(); console.log('\nπŸ” ALERT CHECK RESULTS:'); console.log(` Alerts checked: ${response.checked}`); console.log(` Alerts triggered: ${response.triggered}\n`); console.log(` Product: ${alert.productName}`); console.log(` Target: $${alert.targetPrice}`); console.log(` Current: $${alert.currentPrice}`); console.log(` Status: ${alert.isTriggered ? 'πŸŽ‰ TRIGGERED!' : 'πŸ” Monitoring'}`); console.log(` Best market: ${alert.bestMarket}`); console.log(` Price difference: ${alert.priceDifference >= 0 ? '+' : ''}$${alert.priceDifference}`); }); }); describe('πŸ“Š Final Summary', () => { it('should display complete workflow summary', async () => { const lists = await client.getShoppingLists(); const optimization = await client.optimizeShoppingList(listId); const alerts = await client.checkPriceAlerts(); console.log('\n' + '='.repeat(60)); console.log('πŸ“Š COMPLETE WORKFLOW SUMMARY'); console.log('='.repeat(60)); console.log('\nβœ… Shopping Lists:'); console.log(` Total: ${lists.count}`); console.log(` Items in main list: ${lists.data[0].items.length}`); console.log('\nπŸ† Best Supermarket:'); console.log(` ${optimization.bestMarket.name}`); console.log(` Total: $${optimization.bestMarket.total.toLocaleString('es-AR')}`); console.log(` Savings: $${optimization.savings.amount.toLocaleString('es-AR')} (${optimization.savings.percentage}%)`); console.log('\nπŸ”” Price Alerts:'); console.log(` Active: ${alerts.checked}`); console.log(` Triggered: ${alerts.triggered}`); console.log('\n' + '='.repeat(60)); console.log('βœ… All tests passed! MCP V2 is working perfectly!'); console.log('='.repeat(60) + '\n'); expect(lists.success).toBe(true); expect(optimization.success).toBe(true); expect(alerts.success).toBe(true); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bunkerapps/superprecio_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server