fix: cart price display showing cents as reais (R19 instead of R.19)

Root cause: Cart.tsx used formatCurrency(819) -> '819,00' instead of formatCents(819) -> 'R$ 8,19'

Changes:
- Cart.tsx: Replace formatCurrency with formatCents at all 5 price display points
- Cart.tsx: Add debug logging (logPrice) to trace price values
- cartStore.ts: Add debug logging for addItem, updateQuantity, etc.
- cartStore.ts: Document unitPrice as CENTS in interface
- cartStore.test.ts: Add 7 new price conversion tests for cents handling

All 95 tests pass.
This commit is contained in:
Tiago Yamamoto 2025-12-26 23:08:55 -03:00
parent 670af4ea67
commit 285f586371
3 changed files with 183 additions and 12 deletions

View file

@ -2,7 +2,17 @@ import { ShoppingBasket } from 'lucide-react'
import { Link } from 'react-router-dom'
import { Shell } from '../layouts/Shell'
import { selectGroupedCart, selectCartSummary, useCartStore } from '../stores/cartStore'
import { formatCurrency } from '../utils/format'
import { formatCents } from '../utils/format'
// Debug logging for price values
const logPrice = (label: string, value: number | undefined) => {
console.log(`%c🛒 [Cart] ${label}:`, 'color: #10B981; font-weight: bold', {
rawValue: value,
inCents: value,
inReais: value ? (value / 100).toFixed(2) : '0.00',
formatted: value ? formatCents(value) : 'N/A'
})
}
export function CartPage() {
const items = useCartStore((state) => state.items)
@ -27,7 +37,9 @@ export function CartPage() {
{!isEmpty && (
<div className="text-right">
<p className="text-sm text-gray-600">Itens: {summary.totalItems}</p>
<p className="text-lg font-semibold text-medicalBlue">R$ {formatCurrency(summary.totalValue)}</p>
{/* Log total value */}
{(() => { logPrice('Total do carrinho', summary.totalValue); return null })()}
<p className="text-lg font-semibold text-medicalBlue">{formatCents(summary.totalValue)}</p>
</div>
)}
</div>
@ -81,7 +93,9 @@ export function CartPage() {
</div>
<div className="text-right">
<p className="text-xs text-gray-500">Preço</p>
<p className="font-semibold text-medicalBlue">R$ {formatCurrency(item.unitPrice)}</p>
{/* Log unit price */}
{(() => { logPrice(`Preço unitário ${item.name}`, item.unitPrice); return null })()}
<p className="font-semibold text-medicalBlue">{formatCents(item.unitPrice)}</p>
</div>
<div className="flex items-center justify-end gap-2">
<input
@ -95,8 +109,10 @@ export function CartPage() {
</div>
<div className="text-right">
<p className="text-xs text-gray-500">Subtotal</p>
{/* Log subtotal */}
{(() => { logPrice(`Subtotal ${item.name} (${item.quantity}x)`, item.quantity * item.unitPrice); return null })()}
<p className="font-semibold text-gray-800">
R$ {formatCurrency(item.quantity * item.unitPrice)}
{formatCents(item.quantity * item.unitPrice)}
</p>
<button
className="text-xs text-red-500 hover:underline"
@ -109,7 +125,9 @@ export function CartPage() {
))}
<div className="flex items-center justify-between rounded bg-gray-100 px-3 py-2 text-sm">
<span className="font-semibold text-gray-700">Total do fornecedor</span>
<span className="font-bold text-medicalBlue">R$ {formatCurrency(group.total)}</span>
{/* Log vendor total */}
{(() => { logPrice('Total fornecedor', group.total); return null })()}
<span className="font-bold text-medicalBlue">{formatCents(group.total)}</span>
</div>
</div>
</div>

View file

@ -198,3 +198,129 @@ describe('selectors', () => {
})
})
})
/**
* PRICE CONVERSION TESTS
* These tests verify that prices are handled correctly in CENTS
* The API returns price_cents (e.g., 819 = R$ 8,19)
*/
describe('price conversion (cents)', () => {
const mockItemCents: Omit<CartItem, 'quantity'> = {
id: 'prod-cents-1',
name: 'Paracetamol 750mg',
activeIngredient: 'Paracetamol',
lab: 'EMS',
batch: 'L2025840',
expiry: '2027-09-15',
vendorId: 'vendor-anapolis',
vendorName: 'Anápolis/GO',
unitPrice: 819 // 819 cents = R$ 8,19
}
const mockItemCents2: Omit<CartItem, 'quantity'> = {
id: 'prod-cents-2',
name: 'Dipirona 500mg',
activeIngredient: 'Dipirona',
lab: 'Medley',
batch: 'L2025123',
expiry: '2026-06-30',
vendorId: 'vendor-anapolis',
vendorName: 'Anápolis/GO',
unitPrice: 1299 // 1299 cents = R$ 12,99
}
beforeEach(() => {
useCartStore.setState({ items: [] })
})
describe('storing prices in cents', () => {
it('should store unitPrice as cents (819 = R$ 8,19)', () => {
useCartStore.getState().addItem(mockItemCents)
const state = useCartStore.getState()
expect(state.items[0].unitPrice).toBe(819)
// Verify it's NOT stored as reais (8.19)
expect(state.items[0].unitPrice).not.toBe(8.19)
})
it('should preserve cents precision for subtotal calculation', () => {
useCartStore.getState().addItem(mockItemCents, 3)
const state = useCartStore.getState()
const item = state.items[0]
const subtotal = item.quantity * item.unitPrice
// 3 * 819 = 2457 cents = R$ 24,57
expect(subtotal).toBe(2457)
expect(subtotal / 100).toBe(24.57)
})
})
describe('cents calculations in selectors', () => {
it('should calculate vendor total in cents', () => {
// 819 cents * 2 = 1638 cents
useCartStore.getState().addItem(mockItemCents, 2)
// 1299 cents * 1 = 1299 cents
useCartStore.getState().addItem(mockItemCents2, 1)
const state = useCartStore.getState()
const groups = selectGroupedCart(state)
// Total: 1638 + 1299 = 2937 cents = R$ 29,37
expect(groups['vendor-anapolis'].total).toBe(2937)
expect(groups['vendor-anapolis'].total / 100).toBeCloseTo(29.37)
})
it('should calculate cart summary total in cents', () => {
useCartStore.getState().addItem(mockItemCents, 2) // 1638 cents
useCartStore.getState().addItem(mockItemCents2, 1) // 1299 cents
const state = useCartStore.getState()
const summary = selectCartSummary(state)
expect(summary.totalItems).toBe(3)
expect(summary.totalValue).toBe(2937) // cents
expect(summary.totalValue / 100).toBeCloseTo(29.37) // reais
})
})
describe('edge cases for cents', () => {
it('should handle single unit correctly', () => {
useCartStore.getState().addItem(mockItemCents, 1)
const state = useCartStore.getState()
const summary = selectCartSummary(state)
expect(summary.totalValue).toBe(819)
expect(summary.totalValue / 100).toBe(8.19)
})
it('should handle large quantities without overflow', () => {
useCartStore.getState().addItem(mockItemCents, 1000)
const state = useCartStore.getState()
const summary = selectCartSummary(state)
// 819 * 1000 = 819000 cents = R$ 8,190.00
expect(summary.totalValue).toBe(819000)
expect(summary.totalValue / 100).toBe(8190)
})
it('should handle small cent values (R$ 0,01)', () => {
const cheapItem: Omit<CartItem, 'quantity'> = {
...mockItemCents,
id: 'cheap-1',
unitPrice: 1 // 1 cent = R$ 0,01
}
useCartStore.getState().addItem(cheapItem, 10)
const state = useCartStore.getState()
const summary = selectCartSummary(state)
expect(summary.totalValue).toBe(10) // 10 cents
expect(summary.totalValue / 100).toBe(0.10)
})
})
})

View file

@ -10,7 +10,7 @@ export interface CartItem {
expiry: string
vendorId: string
vendorName: string
unitPrice: number
unitPrice: number // Price in CENTS (e.g., 819 = R$ 8,19)
quantity: number
}
@ -23,14 +23,31 @@ interface CartState {
clearAll: () => void
}
// Debug logging for cart operations
const logCartOp = (operation: string, data: Record<string, unknown>) => {
console.log(`%c🛒 [CartStore] ${operation}`, 'color: #8B5CF6; font-weight: bold', data)
}
export const useCartStore = create<CartState>()(
persist(
(set, get) => ({
items: [],
addItem: (item, quantity = 1) => {
// Log the incoming item with price details
logCartOp('addItem', {
id: item.id,
name: item.name,
unitPrice: item.unitPrice,
unitPriceInReais: (item.unitPrice / 100).toFixed(2),
quantity,
subtotalCents: item.unitPrice * quantity,
subtotalReais: ((item.unitPrice * quantity) / 100).toFixed(2)
})
set((state) => {
const exists = state.items.find((i) => i.id === item.id)
if (exists) {
logCartOp('addItem - updating existing', { existingQty: exists.quantity, newQty: exists.quantity + quantity })
return {
items: state.items.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + quantity } : i
@ -40,14 +57,24 @@ export const useCartStore = create<CartState>()(
return { items: [...state.items, { ...item, quantity }] }
})
},
updateQuantity: (id, quantity) =>
updateQuantity: (id, quantity) => {
logCartOp('updateQuantity', { id, newQuantity: quantity })
set((state) => ({
items: state.items.map((i) => (i.id === id ? { ...i, quantity } : i))
})),
removeItem: (id) => set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
clearVendor: (vendorId) =>
set((state) => ({ items: state.items.filter((i) => i.vendorId !== vendorId) })),
clearAll: () => set({ items: [] })
}))
},
removeItem: (id) => {
logCartOp('removeItem', { id })
set((state) => ({ items: state.items.filter((i) => i.id !== id) }))
},
clearVendor: (vendorId) => {
logCartOp('clearVendor', { vendorId })
set((state) => ({ items: state.items.filter((i) => i.vendorId !== vendorId) }))
},
clearAll: () => {
logCartOp('clearAll', {})
set({ items: [] })
}
}),
{
name: 'mp-cart-storage',