8.1 KiB
8.1 KiB
Design: add-toldo-poc-capacitor
Architecture Overview
This project follows a clean, layered architecture to ensure testability, maintainability, and ease of evolution.
┌─────────────────────────────────────────────────────────────┐
│ apps/toldo-app │
│ (Vite + Capacitor UI) │
└─────────────────────────┬───────────────────────────────────┘
│ depends on
┌─────────────────────────▼───────────────────────────────────┐
│ packages/ui │
│ (Reusable, small UI components) │
└─────────────────────────┬───────────────────────────────────┘
│ depends on
┌─────────────────────────▼───────────────────────────────────┐
│ packages/services │
│ (Application use cases/orchestration) │
└─────────────┬───────────────────────────┬───────────────────┘
│ depends on │ depends on
┌─────────────▼───────────┐ ┌───────────▼───────────────────┐
│ packages/data │ │ packages/core │
│ (Dexie, repositories) │ │ (Domain, entities, rules) │
└─────────────────────────┘ └───────────────────────────────┘
Dependency Rules (Critical)
- core/: NO dependencies on data/, services/, ui/, apps/, @capacitor/*, or any framework. Pure TypeScript only.
- data/: Depends only on core/. Implements repository interfaces defined in core/ or data/contracts.
- services/: Depends on core/ and data/. Contains application logic/use cases. No direct UI dependencies.
- ui/: Depends on core/ and services/ via interfaces/contracts. No direct Dexie or Capacitor imports.
- apps/toldo-app/: Assembles everything, configures DI/composition root, handles Capacitor plugins (mockable), UI composition only here.
SOLID Principles
- Single Responsibility (SRP): Each class/function/module has one reason to change. Components are small and focused.
- Open/Closed (OCP): Open for extension, closed for modification. Use interfaces and composition.
- Liskov Substitution (LSP): Subtypes must be substitutable for base types.
- Interface Segregation (ISP): Many specific interfaces over one general interface. Repository interfaces are granular.
- Dependency Inversion (DIP): Depend on abstractions, not concretions. core defines interfaces, data implements them.
Domain Model
Core Entities
// Producer
interface Productor {
id: string;
nombre: string;
activo: boolean;
createdAt: Date;
}
// Product - different per producer even if same name
interface Producto {
id: string;
productorId: string;
nombrePresentacion: string; // e.g., "Café molido 250gr Santa Ana"
precioUnitario: number;
activo: boolean;
createdAt: Date;
}
// Initial inventory / movements
interface MovimientoInventario {
id: string;
fecha: Date;
tipo: 'INGRESO_INICIAL' | 'INGRESO_SEMANA' | 'AJUSTE' | 'VENTA';
productoId: string;
cantidad: number;
motivo?: string; // required for AJUSTE
recibidoPor?: string;
referenciaId?: string; // links to venta/ajuste
createdAt: Date;
}
// Sale
interface Venta {
id: string;
fecha: Date;
lineas: VentaLinea[];
subtotal: number;
total: number;
createdAt: Date;
}
interface VentaLinea {
id: string;
ventaId: string;
productoId: string;
cantidad: number;
precioUnitario: number;
subtotal: number;
formaPago: 'EFECTIVO' | 'QR';
}
// Expense
interface Gasto {
id: string;
fecha: Date;
concepto: string;
monto: number;
formaPago?: 'EFECTIVO' | 'QR';
createdAt: Date;
}
// Adjustment with reason
interface Ajuste {
id: string;
fecha: Date;
productoId: string;
cantidadAnterior: number;
cantidadNueva: number;
diferencia: number;
motivo: string; // REQUIRED
modificadoPor?: string;
createdAt: Date;
}
// Cash reconciliation
interface CuadreCaja {
id: string;
fecha: Date;
baseInicial: number;
totalVentasEfectivo: number;
totalVentasQR: number;
totalGastosEfectivo: number;
totalGastosQR: number;
saldoEsperadoEfectivo: number;
saldoRealEfectivo?: number;
descuadreEfectivo?: number;
totalQRRegistrado?: number;
totalQRBanco?: number;
descuadreQR?: number;
totalVentas: number;
observaciones?: string;
createdAt: Date;
}
Critical Rules
- Products unique by producer:
productois uniquely identified by(productorId, nombrePresentacion). Same name from different producers = different products. - Adjustments require reason: All adjustments must have a non-empty
motivo. Adjustments are tracked separately and marked as changed in reports. - Inventory reconciliation: Initial inventory vs real differences must be recorded. End-of-day inventory should match initial inventory minus sold products.
- Cash vs QR separation: Track cash and QR separately in sales, expenses, and reconciliation. Compare QR registered vs bank.
- Traceability: All modifications (adjustments to inventory/sales/cash discrepancies/QR discrepancies) must be logged with reason.
- Movements: Track inventory movements both on market day and during the week (week restocks).
Data Layer (Dexie)
Schema Versioning
- Database schema versioned with Dexie migrations
- Export/import JSON has
schemaVersion,appVersion,exportedAt - Import validates versions and migrates or rejects with clear message
Tables
db.version(1).stores({
productores: 'id, nombre, activo',
productos: 'id, productorId, nombrePresentacion, [productorId+nombrePresentacion]',
movimientosInventario: 'id, fecha, tipo, productoId',
ventas: 'id, fecha',
ventaLineas: 'id, ventaId, productoId, formaPago',
gastos: 'id, fecha',
ajustes: 'id, fecha, productoId',
cuadres: 'id, fecha'
});
Repository Pattern
All data access goes through repository interfaces in core/contracts or data/contracts. Implementations in packages/data using Dexie.
Testing Strategy
| Layer | Tool | Approach |
|---|---|---|
| Core (domain/rules) | Vitest | TDD mandatory - write tests first, pure unit tests, no mocks needed |
| Data (repos) | Vitest + fake-indexeddb | Integration tests with in-memory IndexedDB |
| Services (use cases) | Vitest | Unit tests with mocked repositories |
| UI components | Vitest + @testing-library/* | Test behavior, not implementation; user interactions |
| Capacitor plugins | vi.mock | Always mock @capacitor/* in unit tests |
Build & Distribution
- Dev:
vitedev server - Web build:
vite buildoutputs todist/, servable by static HTTP server - Capacitor sync:
npx cap sync androidcopies web assets and updates native project - APK: Build via Android Studio or
./gradlew assembleDebugin android/ folder - Offline: All assets bundled in APK, IndexedDB persists data, no network required
Quality Gates
- TypeScript strict mode enabled
- All core logic tested (TDD)
- Components small and focused (< ~150 lines when possible)
- Linting passes
- Type checking passes
- Tests pass
- Build succeeds
- SOLID principles followed (validated by architecture skill)