@quadcore-lib/storefront-core
v0.1.1
Published
Hooks de lógica para construir un storefront (catálogo, carrito, checkout y seguimiento de orden) contra el backend de Quadcore. **Sin componentes visuales** — eso vive en `@quadcore-lib/storefront-ui` (roadmap 3.2). Este paquete es el equivalente, del la
Readme
@quadcore-lib/storefront-core
Hooks de lógica para construir un storefront (catálogo, carrito, checkout y
seguimiento de orden) contra el backend de Quadcore. Sin componentes
visuales — eso vive en @quadcore-lib/storefront-ui (roadmap 3.2). Este
paquete es el equivalente, del lado del cliente final, a lo que admin-core +
admin-* son para el panel de administración.
Instalación
npm install @quadcore-lib/storefront-coreUso
import { useProducts, useCart, useCheckout } from "@quadcore-lib/storefront-core";
const API_URL = "https://api.mitienda.com";
function Catalog() {
const { data, loading } = useProducts(API_URL, { page: 1, pageSize: 20 });
const { addItem, items, subtotal } = useCart();
if (loading) return <p>Cargando...</p>;
return (
<ul>
{data?.items.map((p) => (
<li key={p.id}>
{p.name} — ${p.price}
<button onClick={() => addItem({ productId: p.id, name: p.name, unitPrice: p.price, currency: p.currency })}>
Agregar al carrito
</button>
</li>
))}
</ul>
);
}
function Checkout() {
const { items, clear } = useCart();
const { checkout, submitting, error, order } = useCheckout(API_URL);
async function handleSubmit() {
const created = await checkout(
items.map((i) => ({ productId: i.productId, quantity: i.quantity })),
{ customerEmail: "[email protected]" },
);
clear();
}
return (
<button disabled={submitting} onClick={handleSubmit}>
{submitting ? "Procesando..." : "Confirmar compra"}
</button>
);
}API
| Export | Tipo | Descripción |
|---|---|---|
| useProducts(apiUrl, params?) | hook | Lista paginada de productos activos (GET /api/products, fuerza isActive=true) |
| useProduct(apiUrl, id) | hook | Detalle de un producto (GET /api/products/:id) |
| useCart() | hook | Carrito persistido en localStorage, 100% cliente |
| useCheckout(apiUrl) | hook | Crea la orden (POST /api/orders) |
| useOrderStatus(apiUrl, orderId, options?) | hook | Polling del estado de una orden — ver gap conocido abajo |
| listProducts, getProduct | función | Cliente API de productos, usados internamente por los hooks |
| createOrder, getOrder | función | Cliente API de órdenes, usados internamente por los hooks |
| newIdempotencyKey() | función | Genera una clave de idempotencia (uuid v4, con fallback sin Web Crypto) |
| Product, Paginated<T>, ListProductsParams | tipo | — |
| CartItem, CheckoutItemInput, CheckoutInput | tipo | — |
| Order, OrderItem, OrderStatus, ORDER_STATUSES | tipo/const | — |
useCart()
Carrito 100% cliente: no pega contra el backend. unitPrice/currency en
CartItem son solo un snapshot para mostrar — el precio real que se cobra
siempre lo resuelve orders-server en el momento de crear la orden (precio
autoritativo de servidor). Persiste en localStorage bajo la key
qc-storefront-cart; si localStorage no está disponible (SSR, modo privado,
cuota llena) el carrito sigue funcionando en memoria para esa sesión, solo que
no persiste entre recargas.
useCheckout(apiUrl)
Llama a POST /api/orders. No vacía el carrito automáticamente — es
responsabilidad del componente que llama a checkout(...) hacer
cart.clear() una vez que la promesa resuelve con éxito.
Manda un header Idempotency-Key por intento de compra, y conserva la misma
clave mientras el checkout falle: si el usuario reintenta tras un error de
red, o hace doble click, el backend devuelve la orden original en vez de crear
una segunda y descontar stock dos veces. Al confirmar con éxito la clave se
descarta, así que el próximo checkout es una compra nueva.
No hay que hacer nada para que funcione. Si armás tu propio flujo con
createOrder(...) directo, pasale vos la clave como cuarto argumento y
respetá esa misma regla — newIdempotencyKey() está exportado para eso:
import { createOrder, newIdempotencyKey } from "@quadcore-lib/storefront-core";
const key = newIdempotencyKey(); // una por intento de compra
await createOrder(apiUrl, items, input, key); // reusala si reintentásGap conocido: seguimiento de orden (useOrderStatus)
useOrderStatus hace polling sobre GET /api/orders/:id cada intervalMs
(default 5000ms, configurable) hasta que el estado de la orden llega a uno de
stopOn (default ['delivered', 'cancelled']). Usa setTimeout recursivo
(no setInterval) para no superponer pedidos si una respuesta tarda más que
el intervalo, y reintenta indefinidamente ante errores de red (no frena el
polling por un error transitorio).
Hoy, GET /orders/:id en orders-server es un endpoint admin-only
(@Roles('admin')) — no existe un endpoint público o de cliente para que un
comprador no autenticado (o autenticado sin rol admin) consulte el estado de
su propia orden. Este paquete no resuelve ese gap: expone el hook
asumiendo que el proyecto consumidor agrega, del lado del backend, una forma
de exponer ese lookup de forma segura para el dueño de la orden — por ejemplo:
- Un endpoint público
GET /orders/track/:orderNumberque exija además elcustomerEmailcomo query param y solo devuelva la orden si coincide. - Un token de seguimiento opaco emitido al crear la orden (devuelto en la
respuesta de
POST /orders) que el cliente guarda y usa en vez delid. - Autenticar al comprador (Auth0) y agregar un guard que permita
GET /orders/:idcuandoorder.userId === req.user.id, además de aadmin.
Hasta que exista alguna de esas variantes, useOrderStatus fallará con 401/403
contra un backend que no haya sido extendido. No se implementó ninguna de las
opciones anteriores en este paquete porque las tres implican una decisión de
producto (qué tan fácil de adivinar puede ser el orderNumber, si vale la
pena forzar login para comprar, etc.) que le corresponde a cada proyecto, no a
la librería.
Requisitos
react>= 18,react-dom>= 18 (peer).@quadcore-lib/core^0.1.5 (dependency — proveefetchWithConfig,useFetch,ApiResponse).- Un backend con
products-serveryorders-servermontados (verpackages/backend/products-serverypackages/backend/orders-server).
