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 { Link } from 'react-router-dom'
|
||||||
import { Shell } from '../layouts/Shell'
|
import { Shell } from '../layouts/Shell'
|
||||||
import { selectGroupedCart, selectCartSummary, useCartStore } from '../stores/cartStore'
|
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() {
|
export function CartPage() {
|
||||||
const items = useCartStore((state) => state.items)
|
const items = useCartStore((state) => state.items)
|
||||||
|
|
@ -27,7 +37,9 @@ export function CartPage() {
|
||||||
{!isEmpty && (
|
{!isEmpty && (
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-sm text-gray-600">Itens: {summary.totalItems}</p>
|
<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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -81,7 +93,9 @@ export function CartPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-xs text-gray-500">Preço</p>
|
<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>
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<input
|
<input
|
||||||
|
|
@ -95,8 +109,10 @@ export function CartPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-xs text-gray-500">Subtotal</p>
|
<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">
|
<p className="font-semibold text-gray-800">
|
||||||
R$ {formatCurrency(item.quantity * item.unitPrice)}
|
{formatCents(item.quantity * item.unitPrice)}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
className="text-xs text-red-500 hover:underline"
|
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">
|
<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-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>
|
</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
|
expiry: string
|
||||||
vendorId: string
|
vendorId: string
|
||||||
vendorName: string
|
vendorName: string
|
||||||
unitPrice: number
|
unitPrice: number // Price in CENTS (e.g., 819 = R$ 8,19)
|
||||||
quantity: number
|
quantity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,14 +23,31 @@ interface CartState {
|
||||||
clearAll: () => void
|
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>()(
|
export const useCartStore = create<CartState>()(
|
||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
items: [],
|
items: [],
|
||||||
addItem: (item, quantity = 1) => {
|
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) => {
|
set((state) => {
|
||||||
const exists = state.items.find((i) => i.id === item.id)
|
const exists = state.items.find((i) => i.id === item.id)
|
||||||
if (exists) {
|
if (exists) {
|
||||||
|
logCartOp('addItem - updating existing', { existingQty: exists.quantity, newQty: exists.quantity + quantity })
|
||||||
return {
|
return {
|
||||||
items: state.items.map((i) =>
|
items: state.items.map((i) =>
|
||||||
i.id === item.id ? { ...i, quantity: i.quantity + quantity } : 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 }] }
|
return { items: [...state.items, { ...item, quantity }] }
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateQuantity: (id, quantity) =>
|
updateQuantity: (id, quantity) => {
|
||||||
|
logCartOp('updateQuantity', { id, newQuantity: quantity })
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
items: state.items.map((i) => (i.id === id ? { ...i, quantity } : i))
|
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) =>
|
removeItem: (id) => {
|
||||||
set((state) => ({ items: state.items.filter((i) => i.vendorId !== vendorId) })),
|
logCartOp('removeItem', { id })
|
||||||
clearAll: () => set({ items: [] })
|
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',
|
name: 'mp-cart-storage',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue