fix(release): 0.2.8 harden admin proxy, cart stock caps, selfpay and refund timeline

- admin proxy: 25s timeout, body guards, structured failure logs
- cart: addItem enforces stock cap (409 INSUFFICIENT_STOCK), UI clamps qty
- tpv selfpay: hide sidebar/discounts/save-pending, rename button, receipt-settings 400 fix
- pos admin: quick products slot count aligned to 8
- returns: human-readable history message + metadata jsonb (migrations 064-065) + admin fallback formatter
- storefront: product card white background
- product page: remove duplicate stock label under add-to-cart button
This commit is contained in:
Deploy
2026-08-25 23:41:29 +02:00
parent b6adf681d1
commit b6af852b54
29 changed files with 671 additions and 279 deletions

View File

@@ -78,6 +78,35 @@ function formatPrice(cents: number) {
return `${(cents / 100).toFixed(2)}`;
}
/**
* F-138: legacy return events were stored as raw JSON. If the migration
* hasn't caught up (or didn't run on a particular row) we still want the UI
* to show something readable instead of dumping the payload. Falls back to
* the original string when the value isn't JSON.
*/
function formatRefundMessage(raw: string): string {
const trimmed = raw?.trim() ?? '';
if (!trimmed.startsWith('{')) return raw;
try {
const payload = JSON.parse(trimmed) as {
refundedCents?: number;
status?: 'refund' | 'partial_refund';
lines?: Array<{ returnedQuantity?: number }>;
reason?: string;
};
const euros = ((payload.refundedCents ?? 0) / 100).toFixed(2);
const action = payload.status === 'refund' ? 'Devolución total' : 'Devolución parcial';
const lineCount = Array.isArray(payload.lines)
? payload.lines.reduce((sum, line) => sum + (line.returnedQuantity ?? 0), 0)
: 0;
const reason = payload.reason?.trim();
const detail = lineCount > 0 ? ` (${lineCount} uds.)` : '';
return `${action} de ${euros}${detail}${reason ? `${reason}` : ''}`;
} catch {
return raw;
}
}
export default function OrderDetailPage() {
const { id } = useParams<{ id: string }>();
const [order, setOrder] = useState<Order | null>(null);
@@ -710,6 +739,9 @@ export default function OrderDetailPage() {
// ORDERS-FIX: detectar refunds para mostrar de forma más legible
const isRefund = /refund|reembolso|devolu/i.test(event.message);
const isRefundEvent = event.eventType === 'REFUND' || isRefund;
const formattedMessage = isRefundEvent
? formatRefundMessage(event.message)
: event.message;
return (
<div key={event.id} className="flex gap-3">
<div className={`w-2 h-2 rounded-full mt-1.5 flex-shrink-0 ${
@@ -718,7 +750,7 @@ export default function OrderDetailPage() {
}`} />
<div className="min-w-0">
<p className={`text-sm break-words ${isRefundEvent ? 'text-pink-700 font-medium' : 'text-gray-800'}`}>
{isRefundEvent && <span className="mr-1">💸</span>}{event.message}
{isRefundEvent && <span className="mr-1">💸</span>}{formattedMessage}
</p>
<p className="text-xs text-gray-400">
{new Date(event.createdAt).toLocaleString('es-ES')}

View File

@@ -199,7 +199,7 @@ export default function PosAdminPage() {
setTouchEnabled(terminal.settings?.touchNavigationEnabled !== false);
setDiscountsEnabled(terminal.settings?.lineDiscountsEnabled !== false);
const configured = terminal.settings?.quickProductVariantIds ?? [];
setQuickSlots(Array.from({ length: 10 }, (_, slot) => configured[slot] ?? null));
setQuickSlots(Array.from({ length: 8 }, (_, slot) => configured[slot] ?? null));
setTouchMessage('');
setSelfpayMode(terminal.settings?.selfpayMode ?? false);
setClosePinRequired(terminal.settings?.closeSessionRequiresPin ?? false);