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:
parent
670af4ea67
commit
285f586371
3 changed files with 183 additions and 12 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue