fix(pos-micro): Align frontend with backend API contracts

Critical fixes:
- Fix LoginPage to use phone+pin for login (was using businessName)
- Add ownerName field for registration flow
- Fix useProducts hooks: /products?isFavorite=true, /toggle-favorite
- Fix useSales hooks: /sales/recent, /sales/today, POST for cancel
- Align TypeScript types with backend entities:
  - stockQuantity/lowStockAlert instead of currentStock/minStock
  - discountPercent/discountAmount for proper discount handling
  - Add SubscriptionStatus type for tenant state

Cart store improvements:
- Properly calculate subtotals with discount percentage
- Compute tax amount (16% IVA included)
- Track discountAmount and taxAmount separately

Documentation:
- Add comprehensive ANALISIS-GAPS.md comparing:
  - POS Micro vs ERP Core architecture patterns
  - POS Micro vs Odoo POS feature parity
  - Identified gaps and roadmap for future phases

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rckrdmrd 2025-12-08 12:25:06 -06:00
parent e85b548b8f
commit 35020e02f1
9 changed files with 400 additions and 142 deletions

View File

@ -0,0 +1,192 @@
# POS Micro - Analisis de Gaps vs ERP Core y Odoo POS
## Resumen Ejecutivo
Este documento analiza las diferencias entre POS Micro (producto MVP), ERP Core (arquitectura base) y Odoo POS (referencia de mercado) para identificar gaps y oportunidades de mejora.
## 1. Comparativa de Arquitectura
### ERP Core (Express + TypeScript)
- Framework: Express.js con TypeScript
- ORM: Raw PostgreSQL queries con Pool
- Autenticacion: JWT + Bcrypt + RBAC completo
- Validacion: Zod schemas
- Multi-tenancy: Schema isolation con RLS
- Modulos: 13 modulos completos (auth, users, companies, partners, inventory, products, warehouses, pickings, lots, financial, purchases, sales, crm, hr, projects, system)
### POS Micro (NestJS + TypeORM)
- Framework: NestJS con TypeScript
- ORM: TypeORM
- Autenticacion: JWT + Bcrypt + PIN simplificado
- Validacion: class-validator decorators
- Multi-tenancy: tenant_id column (simplificado)
- Modulos: 5 modulos minimos (auth, products, categories, sales, payments)
### Odoo POS (Python + ORM)
- Framework: Odoo 18 (Python)
- ORM: Odoo ORM
- Modulos POS: 40+ tablas/modelos interconectados
- Funcionalidades avanzadas: Restaurant, Loyalty, IoT, Multiple payment terminals
## 2. Gaps Identificados
### 2.1 Seguridad
| Feature | ERP Core | POS Micro | Odoo POS | Gap |
|---------|----------|-----------|----------|-----|
| RBAC completo | ✅ | ❌ | ✅ | POS Micro solo tiene owner/cashier |
| Rate limiting | ❌ | ❌ | ✅ | Ninguno implementa |
| Audit logs | ✅ | ❌ | ✅ | POS Micro no tiene |
| Session management | ✅ | ❌ | ✅ | POS Micro no maneja sesiones de caja |
### 2.2 Funcionalidad POS
| Feature | ERP Core | POS Micro | Odoo POS | Gap |
|---------|----------|-----------|----------|-----|
| Sesiones de caja | N/A | ❌ | ✅ | Critico para control de caja |
| Cierre de caja | N/A | ❌ | ✅ | Critico para contabilidad |
| Arqueo de caja | N/A | ❌ | ✅ | Control de efectivo |
| Devoluciones | N/A | Parcial | ✅ | Solo cancelacion same-day |
| Descuentos globales | N/A | ❌ | ✅ | Solo descuento por linea |
| Impuestos configurables | N/A | Hardcoded 16% | ✅ | No flexible |
| Multi-tarifa | N/A | ❌ | ✅ | Un precio por producto |
| Combos/Kits | N/A | ❌ | ✅ | No soportado |
| Variantes producto | N/A | ❌ | ✅ | No soportado |
### 2.3 Integraciones
| Feature | ERP Core | POS Micro | Odoo POS | Gap |
|---------|----------|-----------|----------|-----|
| Inventario | ✅ Completo | Basico | ✅ Completo | Sin lotes/series |
| Contabilidad | ✅ | ❌ | ✅ | No genera asientos |
| Facturacion | ❌ | ❌ | ✅ | No hay CFDI |
| WhatsApp | ❌ | Tabla vacia | ❌ | Preparado pero no implementado |
| Impresoras | ❌ | ❌ | ✅ | No hay soporte |
| Terminal pago | ❌ | ❌ | ✅ | No integrado |
### 2.4 Reportes
| Feature | ERP Core | POS Micro | Odoo POS | Gap |
|---------|----------|-----------|----------|-----|
| Ventas del dia | ❌ | ✅ Basico | ✅ Completo | Solo totales |
| Por vendedor | ❌ | ❌ | ✅ | No soportado |
| Por producto | ❌ | ❌ | ✅ | No soportado |
| Por hora | ❌ | ❌ | ✅ | No soportado |
| Margen | ❌ | ❌ | ✅ | No soportado |
| Exportable | ❌ | ❌ | ✅ | No hay exports |
## 3. Correcciones Aplicadas
### 3.1 Frontend - Login
- **Problema**: Frontend enviaba `businessName` donde backend esperaba `phone`
- **Solucion**: Actualizado LoginPage para usar `phone` y agregar `ownerName` para registro
### 3.2 Frontend - Endpoints
- **Problema**: Endpoints no coincidian con backend
- **Correcciones**:
- `/products/favorites``/products?isFavorite=true`
- `/products/{id}/favorite``/products/{id}/toggle-favorite`
- `/sales?date=``/sales/recent?limit=50`
- `PATCH /sales/{id}/cancel``POST /sales/{id}/cancel`
- `/sales/summary/{date}``/sales/today`
### 3.3 Frontend - Tipos TypeScript
- **Problema**: Tipos no alineados con entidades backend
- **Correcciones**:
- `currentStock``stockQuantity`
- `minStock``lowStockAlert`
- `discount``discountAmount/discountPercent`
- `tax``taxAmount`
- `change``changeAmount`
- Agregado `SubscriptionStatus` type
### 3.4 Frontend - Cart Store
- **Problema**: Calculo de descuentos no funcionaba
- **Solucion**: Actualizado para aplicar `discountPercent` correctamente al subtotal
## 4. Gaps Pendientes (Roadmap)
### Fase 2 - Funcionalidad Core
1. **Sesiones de caja**: Apertura, cierre, arqueo
2. **Devoluciones completas**: No solo same-day
3. **Descuentos globales**: Por orden, no solo por linea
4. **Impuestos configurables**: Permitir diferentes tasas
### Fase 3 - Integraciones
1. **WhatsApp Business**: Tickets por WhatsApp
2. **Facturacion CFDI**: Integracion con PAC
3. **Impresoras termicas**: Soporte ESC/POS
4. **Terminales de pago**: Integracion basica
### Fase 4 - Reportes
1. **Reporte por producto**: Top ventas, margenes
2. **Reporte por periodo**: Semanal, mensual
3. **Exportacion**: CSV, PDF
## 5. Patrones de ERP Core a Adoptar
### 5.1 Base Service Pattern
```typescript
// ERP Core tiene un servicio base reutilizable
abstract class BaseService<T, CreateDto, UpdateDto> {
// findAll con paginacion, busqueda y filtros
// findById, findByIdOrFail
// exists, softDelete, hardDelete
// withTransaction
}
```
**Recomendacion**: Implementar en POS Micro para consistencia
### 5.2 Error Handling
```typescript
// ERP Core usa clases de error personalizadas
class ValidationError extends AppError { }
class NotFoundError extends AppError { }
class ConflictError extends AppError { }
```
**Recomendacion**: Adoptar mismo patron en NestJS
### 5.3 Audit Fields
```typescript
// ERP Core tiene campos de auditoria consistentes
created_at, created_by
updated_at, updated_by
deleted_at, deleted_by
```
**Recomendacion**: Agregar `created_by`, `updated_by` a todas las tablas
## 6. Funcionalidades de Odoo a Considerar
### 6.1 Session Management (Critico)
- Apertura de sesion con saldo inicial
- Estado: opening_control → opened → closing_control → closed
- Validacion de diferencias de caja
- Asientos contables automaticos
### 6.2 Loyalty Programs (Futuro)
- Puntos por compra
- Recompensas configurables
- Tarjetas de cliente
- Cupones/Promociones
### 6.3 Restaurant Mode (Futuro)
- Mesas/pisos
- Ordenes abiertas
- Impresion a cocina
- Division de cuentas
## 7. Conclusion
POS Micro esta correctamente posicionado como un MVP minimo para el mercado mexicano informal (vendedores ambulantes, tienditas, fondas). Las correcciones aplicadas resuelven los problemas criticos de comunicacion frontend-backend.
Los gaps identificados son caracteristicas para fases futuras, no bloquean el lanzamiento del MVP que cumple con:
- ✅ Registro/Login simple con PIN
- ✅ Catalogo de productos (max 500)
- ✅ Ventas rapidas (max 1000/mes)
- ✅ Multiples formas de pago
- ✅ Reportes basicos del dia
- ✅ Offline-first PWA
---
*Documento generado: 2025-12-08*
*Version: 1.0*

View File

@ -36,15 +36,15 @@ export function CheckoutModal({ isOpen, onClose, onSuccess }: CheckoutModalProps
if (!paymentMethod) return;
try {
// Payload that matches backend CreateSaleDto
const sale = await createSale.mutateAsync({
items: items.map((item) => ({
productId: item.product.id,
quantity: item.quantity,
unitPrice: item.unitPrice,
discount: item.discount,
discountPercent: item.discountPercent || 0,
})),
paymentMethodId: paymentMethod.id,
amountReceived: paymentMethod.type === 'cash' ? amountReceived : undefined,
amountReceived: paymentMethod.type === 'cash' ? amountReceived : total,
});
onSuccess(sale);

View File

@ -9,8 +9,8 @@ interface ProductCardProps {
}
export function ProductCard({ product, onSelect, onToggleFavorite }: ProductCardProps) {
const isLowStock = product.trackStock && product.currentStock <= product.minStock;
const isOutOfStock = product.trackStock && product.currentStock <= 0;
const isLowStock = product.trackStock && product.stockQuantity <= product.lowStockAlert;
const isOutOfStock = product.trackStock && product.stockQuantity <= 0;
return (
<div
@ -69,7 +69,7 @@ export function ProductCard({ product, onSelect, onToggleFavorite }: ProductCard
isOutOfStock ? 'text-red-500' : isLowStock ? 'text-yellow-600' : 'text-gray-400'
)}
>
{isOutOfStock ? 'Agotado' : `${product.currentStock} disponibles`}
{isOutOfStock ? 'Agotado' : `${product.stockQuantity} disponibles`}
</p>
)}
</div>

View File

@ -2,7 +2,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '@/services/api';
import type { Product, Category } from '@/types';
// Products
// Products - matches backend ProductsController
export function useProducts(categoryId?: string) {
return useQuery({
queryKey: ['products', categoryId],
@ -21,7 +21,8 @@ export function useFavoriteProducts() {
return useQuery({
queryKey: ['products', 'favorites'],
queryFn: async () => {
const { data } = await api.get<Product[]>('/products/favorites');
// Backend: GET /products?isFavorite=true
const { data } = await api.get<Product[]>('/products?isFavorite=true&isActive=true');
return data;
},
});
@ -32,6 +33,7 @@ export function useSearchProduct() {
return useMutation({
mutationFn: async (barcode: string) => {
// Backend: GET /products/barcode/:barcode
const { data } = await api.get<Product>(`/products/barcode/${barcode}`);
return data;
},
@ -46,7 +48,8 @@ export function useToggleFavorite() {
return useMutation({
mutationFn: async (productId: string) => {
const { data } = await api.patch<Product>(`/products/${productId}/favorite`);
// Backend: PATCH /products/:id/toggle-favorite
const { data } = await api.patch<Product>(`/products/${productId}/toggle-favorite`);
return data;
},
onSuccess: () => {
@ -55,7 +58,7 @@ export function useToggleFavorite() {
});
}
// Categories
// Categories - matches backend CategoriesController
export function useCategories() {
return useQuery({
queryKey: ['categories'],

View File

@ -1,26 +1,37 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import api from '@/services/api';
import { useCartStore } from '@/store/cart';
import type { Sale, DailySummary } from '@/types';
import type { Sale } from '@/types';
// DTO que coincide con backend CreateSaleDto
interface CreateSaleDto {
items: {
productId: string;
quantity: number;
unitPrice: number;
discount: number;
discountPercent?: number;
}[];
paymentMethodId: string;
amountReceived?: number;
amountReceived: number;
customerName?: string;
customerPhone?: string;
notes?: string;
}
// Response del backend para summary
interface TodaySummary {
totalSales: number;
totalRevenue: number;
totalTax: number;
avgTicket: number;
}
export function useCreateSale() {
const queryClient = useQueryClient();
const clearCart = useCartStore((state) => state.clear);
return useMutation({
mutationFn: async (sale: CreateSaleDto) => {
// Backend: POST /sales
const { data } = await api.post<Sale>('/sales', sale);
return data;
},
@ -37,20 +48,19 @@ export function useTodaySales() {
return useQuery({
queryKey: ['sales', 'today'],
queryFn: async () => {
const today = new Date().toISOString().split('T')[0];
const { data } = await api.get<Sale[]>(`/sales?date=${today}`);
// Backend: GET /sales/recent?limit=50
const { data } = await api.get<Sale[]>('/sales/recent?limit=50');
return data;
},
});
}
export function useDailySummary(date?: string) {
const queryDate = date || new Date().toISOString().split('T')[0];
export function useDailySummary() {
return useQuery({
queryKey: ['daily-summary', queryDate],
queryKey: ['daily-summary'],
queryFn: async () => {
const { data } = await api.get<DailySummary>(`/sales/summary/${queryDate}`);
// Backend: GET /sales/today
const { data } = await api.get<TodaySummary>('/sales/today');
return data;
},
});
@ -60,8 +70,9 @@ export function useCancelSale() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (saleId: string) => {
const { data } = await api.patch<Sale>(`/sales/${saleId}/cancel`);
mutationFn: async ({ saleId, reason }: { saleId: string; reason?: string }) => {
// Backend: POST /sales/:id/cancel
const { data } = await api.post<Sale>(`/sales/${saleId}/cancel`, { reason });
return data;
},
onSuccess: () => {

View File

@ -6,13 +6,18 @@ import { useAuthStore } from '@/store/auth';
import type { AuthResponse } from '@/types';
export function LoginPage() {
const [businessName, setBusinessName] = useState('');
// Campos compartidos
const [phone, setPhone] = useState('');
const [pin, setPin] = useState('');
const [showPin, setShowPin] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [isRegister, setIsRegister] = useState(false);
// Campos solo para registro
const [businessName, setBusinessName] = useState('');
const [ownerName, setOwnerName] = useState('');
const login = useAuthStore((state) => state.login);
const navigate = useNavigate();
@ -24,8 +29,8 @@ export function LoginPage() {
try {
const endpoint = isRegister ? '/auth/register' : '/auth/login';
const payload = isRegister
? { businessName, pin }
: { businessName, pin };
? { businessName, ownerName, phone, pin }
: { phone, pin };
const { data } = await api.post<AuthResponse>(endpoint, payload);
@ -36,7 +41,7 @@ export function LoginPage() {
err instanceof Error
? err.message
: (err as { response?: { data?: { message?: string } } })?.response?.data?.message ||
(isRegister ? 'Error al registrar negocio' : 'PIN o negocio incorrecto');
(isRegister ? 'Error al registrar negocio' : 'Telefono o PIN incorrecto');
setError(errorMessage);
} finally {
setIsLoading(false);
@ -61,23 +66,59 @@ export function LoginPage() {
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
{isRegister && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nombre del negocio
</label>
<input
type="text"
className="input"
placeholder="Mi Tiendita"
value={businessName}
onChange={(e) => setBusinessName(e.target.value)}
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tu nombre
</label>
<input
type="text"
className="input"
placeholder="Juan Perez"
value={ownerName}
onChange={(e) => setOwnerName(e.target.value)}
required
/>
</div>
</>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nombre del negocio
Telefono (10 digitos)
</label>
<input
type="text"
type="tel"
className="input"
placeholder="Mi Tiendita"
value={businessName}
onChange={(e) => setBusinessName(e.target.value)}
placeholder="5512345678"
value={phone}
onChange={(e) => setPhone(e.target.value.replace(/\D/g, '').slice(0, 10))}
required
minLength={10}
maxLength={10}
inputMode="numeric"
pattern="[0-9]*"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
PIN ({isRegister ? '4-6 digitos' : '4-6 digitos'})
PIN (4-6 digitos)
</label>
<div className="relative">
<input

View File

@ -1,4 +1,4 @@
import { ArrowLeft, TrendingUp, ShoppingCart, XCircle, Banknote, CreditCard, Smartphone } from 'lucide-react';
import { ArrowLeft, TrendingUp, ShoppingCart, DollarSign, Receipt } from 'lucide-react';
import { Link } from 'react-router-dom';
import { useDailySummary, useTodaySales } from '@/hooks/useSales';
@ -24,56 +24,34 @@ export function ReportsPage() {
</div>
) : (
<div className="p-4 space-y-4">
{/* Summary Cards */}
{/* Summary Cards - aligned with backend TodaySummary */}
<div className="grid grid-cols-2 gap-4">
<SummaryCard
icon={<TrendingUp className="w-6 h-6 text-green-600" />}
label="Ventas totales"
value={`$${summary?.totalSales.toFixed(2) || '0.00'}`}
icon={<DollarSign className="w-6 h-6 text-green-600" />}
label="Ingresos"
value={`$${summary?.totalRevenue?.toFixed(2) || '0.00'}`}
bgColor="bg-green-50"
/>
<SummaryCard
icon={<ShoppingCart className="w-6 h-6 text-blue-600" />}
label="Transacciones"
value={summary?.salesCount.toString() || '0'}
label="Ventas"
value={summary?.totalSales?.toString() || '0'}
bgColor="bg-blue-50"
/>
<SummaryCard
icon={<XCircle className="w-6 h-6 text-red-600" />}
label="Canceladas"
value={summary?.cancelledCount.toString() || '0'}
bgColor="bg-red-50"
icon={<Receipt className="w-6 h-6 text-yellow-600" />}
label="IVA recaudado"
value={`$${summary?.totalTax?.toFixed(2) || '0.00'}`}
bgColor="bg-yellow-50"
/>
<SummaryCard
icon={<TrendingUp className="w-6 h-6 text-purple-600" />}
label="Promedio"
value={`$${summary?.salesCount ? (summary.totalSales / summary.salesCount).toFixed(2) : '0.00'}`}
label="Ticket promedio"
value={`$${summary?.avgTicket?.toFixed(2) || '0.00'}`}
bgColor="bg-purple-50"
/>
</div>
{/* Payment breakdown */}
<div className="card p-4">
<h2 className="font-bold text-gray-900 mb-4">Por forma de pago</h2>
<div className="space-y-3">
<PaymentRow
icon={<Banknote className="w-5 h-5" />}
label="Efectivo"
amount={summary?.cashSales || 0}
/>
<PaymentRow
icon={<CreditCard className="w-5 h-5" />}
label="Tarjeta"
amount={summary?.cardSales || 0}
/>
<PaymentRow
icon={<Smartphone className="w-5 h-5" />}
label="Transferencia"
amount={summary?.transferSales || 0}
/>
</div>
</div>
{/* Recent sales */}
<div className="card">
<div className="p-4 border-b">
@ -135,23 +113,3 @@ function SummaryCard({ icon, label, value, bgColor }: SummaryCardProps) {
</div>
);
}
interface PaymentRowProps {
icon: React.ReactNode;
label: string;
amount: number;
}
function PaymentRow({ icon, label, amount }: PaymentRowProps) {
return (
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center text-gray-600">
{icon}
</div>
<span className="text-gray-700">{label}</span>
</div>
<span className="font-medium">${amount.toFixed(2)}</span>
</div>
);
}

View File

@ -9,8 +9,8 @@ interface CartState {
// Computed
subtotal: number;
discount: number;
tax: number;
discountAmount: number;
taxAmount: number;
total: number;
itemCount: number;
@ -18,7 +18,7 @@ interface CartState {
addItem: (product: Product, quantity?: number) => void;
removeItem: (productId: string) => void;
updateQuantity: (productId: string, quantity: number) => void;
applyItemDiscount: (productId: string, discount: number) => void;
applyItemDiscount: (productId: string, discountPercent: number) => void;
setPaymentMethod: (method: PaymentMethod) => void;
setAmountReceived: (amount: number) => void;
setNotes: (notes: string) => void;
@ -26,13 +26,20 @@ interface CartState {
}
const calculateTotals = (items: CartItem[]) => {
// Subtotal = sum of line subtotals (already with discount applied)
const subtotal = items.reduce((sum, item) => sum + item.subtotal, 0);
const discount = items.reduce((sum, item) => sum + (item.unitPrice * item.quantity * item.discount / 100), 0);
const tax = 0; // No tax in simplified version
const total = subtotal - discount + tax;
// Discount amount = original price - discounted price
const discountAmount = items.reduce((sum, item) => {
const originalPrice = item.unitPrice * item.quantity;
return sum + (originalPrice - item.subtotal);
}, 0);
// Tax included in price (16% IVA)
const taxRate = 0.16;
const taxAmount = subtotal - (subtotal / (1 + taxRate));
const total = subtotal;
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
return { subtotal, discount, tax, total, itemCount };
return { subtotal, discountAmount, taxAmount, total, itemCount };
};
export const useCartStore = create<CartState>((set, get) => ({
@ -41,8 +48,8 @@ export const useCartStore = create<CartState>((set, get) => ({
amountReceived: 0,
notes: '',
subtotal: 0,
discount: 0,
tax: 0,
discountAmount: 0,
taxAmount: 0,
total: 0,
itemCount: 0,
@ -54,22 +61,25 @@ export const useCartStore = create<CartState>((set, get) => ({
if (existingIndex >= 0) {
// Update existing item
newItems = items.map((item, index) =>
index === existingIndex
? {
...item,
quantity: item.quantity + quantity,
subtotal: (item.quantity + quantity) * item.unitPrice,
}
: item
);
newItems = items.map((item, index) => {
if (index === existingIndex) {
const newQty = item.quantity + quantity;
const discountMultiplier = 1 - (item.discountPercent / 100);
return {
...item,
quantity: newQty,
subtotal: newQty * item.unitPrice * discountMultiplier,
};
}
return item;
});
} else {
// Add new item
const newItem: CartItem = {
product,
quantity,
unitPrice: product.price,
discount: 0,
discountPercent: 0,
subtotal: quantity * product.price,
};
newItems = [...items, newItem];
@ -89,18 +99,32 @@ export const useCartStore = create<CartState>((set, get) => ({
return;
}
const newItems = get().items.map((item) =>
item.product.id === productId
? { ...item, quantity, subtotal: quantity * item.unitPrice }
: item
);
const newItems = get().items.map((item) => {
if (item.product.id === productId) {
const discountMultiplier = 1 - (item.discountPercent / 100);
return {
...item,
quantity,
subtotal: quantity * item.unitPrice * discountMultiplier,
};
}
return item;
});
set({ items: newItems, ...calculateTotals(newItems) });
},
applyItemDiscount: (productId, discount) => {
const newItems = get().items.map((item) =>
item.product.id === productId ? { ...item, discount } : item
);
applyItemDiscount: (productId, discountPercent) => {
const newItems = get().items.map((item) => {
if (item.product.id === productId) {
const discountMultiplier = 1 - (discountPercent / 100);
return {
...item,
discountPercent,
subtotal: item.quantity * item.unitPrice * discountMultiplier,
};
}
return item;
});
set({ items: newItems, ...calculateTotals(newItems) });
},
@ -117,8 +141,8 @@ export const useCartStore = create<CartState>((set, get) => ({
amountReceived: 0,
notes: '',
subtotal: 0,
discount: 0,
tax: 0,
discountAmount: 0,
taxAmount: 0,
total: 0,
itemCount: 0,
}),

View File

@ -1,10 +1,14 @@
// =============================================================================
// POS MICRO - TypeScript Types
// POS MICRO - TypeScript Types (aligned with backend entities)
// =============================================================================
// Matches backend Product entity
export interface Product {
id: string;
tenantId: string;
name: string;
description?: string;
sku?: string;
barcode?: string;
price: number;
cost?: number;
@ -12,87 +16,112 @@ export interface Product {
category?: Category;
imageUrl?: string;
trackStock: boolean;
currentStock: number;
minStock: number;
stockQuantity: number;
lowStockAlert: number;
isFavorite: boolean;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
// Matches backend Category entity
export interface Category {
id: string;
tenantId: string;
name: string;
description?: string;
color?: string;
sortOrder: number;
productCount?: number;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
// Frontend cart item
export interface CartItem {
product: Product;
quantity: number;
unitPrice: number;
discount: number;
discountPercent: number;
subtotal: number;
}
// Matches backend Sale entity
export interface Sale {
id: string;
tenantId: string;
ticketNumber: string;
items: SaleItem[];
subtotal: number;
discount: number;
tax: number;
discountAmount: number;
taxAmount: number;
total: number;
paymentMethod: PaymentMethod;
amountReceived?: number;
change?: number;
status: 'completed' | 'cancelled';
paymentMethodId: string;
paymentMethod?: PaymentMethod;
amountReceived: number;
changeAmount: number;
customerName?: string;
customerPhone?: string;
status: 'completed' | 'cancelled' | 'refunded';
notes?: string;
cancelledAt?: string;
cancelReason?: string;
createdAt: string;
updatedAt: string;
}
// Matches backend SaleItem entity
export interface SaleItem {
id: string;
saleId: string;
productId: string;
productName: string;
productSku?: string;
quantity: number;
unitPrice: number;
discount: number;
discountPercent: number;
subtotal: number;
}
// Matches backend PaymentMethod entity
export interface PaymentMethod {
id: string;
tenantId: string;
name: string;
type: 'cash' | 'card' | 'transfer' | 'other';
isActive: boolean;
isDefault: boolean;
requiresReference: boolean;
sortOrder: number;
}
export interface DailySummary {
date: string;
// Matches backend TodaySummary response
export interface TodaySummary {
totalSales: number;
salesCount: number;
cancelledCount: number;
cashSales: number;
cardSales: number;
transferSales: number;
totalRevenue: number;
totalTax: number;
avgTicket: number;
}
// Matches backend User entity
export interface User {
id: string;
name: string;
role: 'owner' | 'cashier';
isOwner: boolean;
}
// Matches backend Tenant entity (partial for auth response)
export type SubscriptionStatus = 'trial' | 'active' | 'past_due' | 'cancelled' | 'suspended';
export interface Tenant {
id: string;
businessName: string;
planType: 'pos_micro';
maxProducts: number;
maxSalesPerMonth: number;
currentMonthSales: number;
plan: string;
subscriptionStatus: SubscriptionStatus;
trialEndsAt: string | null;
}
// Matches backend AuthResponse
export interface AuthResponse {
accessToken: string;
refreshToken: string;