[SIMCO-V38] feat: Actualizar a SIMCO v3.8.0 + cambios apps
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Cambios en backend y frontend Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3bba4ce6d7
commit
928eb795e6
@ -13,6 +13,11 @@ import { SubscriptionsModule } from './modules/subscriptions/subscriptions.modul
|
||||
import { MessagingModule } from './modules/messaging/messaging.module';
|
||||
import { BillingModule } from './modules/billing/billing.module';
|
||||
import { IntegrationsModule } from './modules/integrations/integrations.module';
|
||||
import { ReferralsModule } from './modules/referrals/referrals.module';
|
||||
import { CodiSpeiModule } from './modules/codi-spei/codi-spei.module';
|
||||
import { WidgetsModule } from './modules/widgets/widgets.module';
|
||||
import { InvoicesModule } from './modules/invoices/invoices.module';
|
||||
import { MarketplaceModule } from './modules/marketplace/marketplace.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -54,6 +59,11 @@ import { IntegrationsModule } from './modules/integrations/integrations.module';
|
||||
MessagingModule,
|
||||
BillingModule,
|
||||
IntegrationsModule,
|
||||
ReferralsModule,
|
||||
CodiSpeiModule,
|
||||
WidgetsModule,
|
||||
InvoicesModule,
|
||||
MarketplaceModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
111
apps/backend/src/modules/codi-spei/codi-spei.controller.ts
Normal file
111
apps/backend/src/modules/codi-spei/codi-spei.controller.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { CodiSpeiService } from './codi-spei.service';
|
||||
import { GenerateQrDto } from './dto/generate-qr.dto';
|
||||
|
||||
@ApiTags('codi-spei')
|
||||
@Controller('v1')
|
||||
export class CodiSpeiController {
|
||||
constructor(private readonly codiSpeiService: CodiSpeiService) {}
|
||||
|
||||
// ==================== CODI ====================
|
||||
|
||||
@Post('codi/generate-qr')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ summary: 'Generar QR CoDi para cobro' })
|
||||
generateQr(@Request() req, @Body() dto: GenerateQrDto) {
|
||||
return this.codiSpeiService.generateQr(req.user.tenantId, dto);
|
||||
}
|
||||
|
||||
@Get('codi/status/:id')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ summary: 'Obtener estado de transaccion CoDi' })
|
||||
getCodiStatus(@Param('id') id: string) {
|
||||
return this.codiSpeiService.getCodiStatus(id);
|
||||
}
|
||||
|
||||
@Get('codi/transactions')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ summary: 'Listar transacciones CoDi' })
|
||||
@ApiQuery({ name: 'limit', required: false })
|
||||
getCodiTransactions(@Request() req, @Query('limit') limit?: number) {
|
||||
return this.codiSpeiService.getCodiTransactions(req.user.tenantId, limit);
|
||||
}
|
||||
|
||||
@Post('codi/webhook')
|
||||
@ApiOperation({ summary: 'Webhook para confirmacion CoDi' })
|
||||
async codiWebhook(@Body() payload: any) {
|
||||
await this.codiSpeiService.handleCodiWebhook(payload);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ==================== SPEI ====================
|
||||
|
||||
@Get('spei/clabe')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ summary: 'Obtener CLABE virtual del tenant' })
|
||||
async getClabe(@Request() req) {
|
||||
const account = await this.codiSpeiService.getVirtualAccount(req.user.tenantId);
|
||||
if (!account) {
|
||||
return { clabe: null, message: 'No tiene CLABE virtual configurada' };
|
||||
}
|
||||
return {
|
||||
clabe: account.clabe,
|
||||
beneficiaryName: account.beneficiaryName,
|
||||
status: account.status,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('spei/create-clabe')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ summary: 'Crear CLABE virtual para el tenant' })
|
||||
createClabe(@Request() req, @Body() body: { beneficiaryName: string }) {
|
||||
return this.codiSpeiService.createVirtualAccount(
|
||||
req.user.tenantId,
|
||||
body.beneficiaryName,
|
||||
);
|
||||
}
|
||||
|
||||
@Get('spei/transactions')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ summary: 'Listar transacciones SPEI recibidas' })
|
||||
@ApiQuery({ name: 'limit', required: false })
|
||||
getSpeiTransactions(@Request() req, @Query('limit') limit?: number) {
|
||||
return this.codiSpeiService.getSpeiTransactions(req.user.tenantId, limit);
|
||||
}
|
||||
|
||||
@Post('spei/webhook')
|
||||
@ApiOperation({ summary: 'Webhook para notificacion SPEI' })
|
||||
async speiWebhook(@Body() payload: any) {
|
||||
await this.codiSpeiService.handleSpeiWebhook(payload.clabe, payload);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ==================== SUMMARY ====================
|
||||
|
||||
@Get('payments/summary')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ summary: 'Resumen de pagos CoDi/SPEI del dia' })
|
||||
@ApiQuery({ name: 'date', required: false })
|
||||
async getSummary(@Request() req, @Query('date') date?: string) {
|
||||
const targetDate = date ? new Date(date) : undefined;
|
||||
return this.codiSpeiService.getSummary(req.user.tenantId, targetDate);
|
||||
}
|
||||
}
|
||||
17
apps/backend/src/modules/codi-spei/codi-spei.module.ts
Normal file
17
apps/backend/src/modules/codi-spei/codi-spei.module.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CodiSpeiController } from './codi-spei.controller';
|
||||
import { CodiSpeiService } from './codi-spei.service';
|
||||
import { VirtualAccount } from './entities/virtual-account.entity';
|
||||
import { CodiTransaction } from './entities/codi-transaction.entity';
|
||||
import { SpeiTransaction } from './entities/spei-transaction.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([VirtualAccount, CodiTransaction, SpeiTransaction]),
|
||||
],
|
||||
controllers: [CodiSpeiController],
|
||||
providers: [CodiSpeiService],
|
||||
exports: [CodiSpeiService],
|
||||
})
|
||||
export class CodiSpeiModule {}
|
||||
263
apps/backend/src/modules/codi-spei/codi-spei.service.ts
Normal file
263
apps/backend/src/modules/codi-spei/codi-spei.service.ts
Normal file
@ -0,0 +1,263 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource, LessThan } from 'typeorm';
|
||||
import { VirtualAccount, VirtualAccountStatus } from './entities/virtual-account.entity';
|
||||
import { CodiTransaction, CodiTransactionStatus } from './entities/codi-transaction.entity';
|
||||
import { SpeiTransaction, SpeiTransactionStatus } from './entities/spei-transaction.entity';
|
||||
import { GenerateQrDto } from './dto/generate-qr.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CodiSpeiService {
|
||||
constructor(
|
||||
@InjectRepository(VirtualAccount)
|
||||
private readonly virtualAccountRepo: Repository<VirtualAccount>,
|
||||
@InjectRepository(CodiTransaction)
|
||||
private readonly codiRepo: Repository<CodiTransaction>,
|
||||
@InjectRepository(SpeiTransaction)
|
||||
private readonly speiRepo: Repository<SpeiTransaction>,
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
// ==================== VIRTUAL ACCOUNTS (CLABE) ====================
|
||||
|
||||
async getVirtualAccount(tenantId: string): Promise<VirtualAccount | null> {
|
||||
return this.virtualAccountRepo.findOne({
|
||||
where: { tenantId, status: VirtualAccountStatus.ACTIVE },
|
||||
});
|
||||
}
|
||||
|
||||
async createVirtualAccount(
|
||||
tenantId: string,
|
||||
beneficiaryName: string,
|
||||
provider: string = 'stp',
|
||||
): Promise<VirtualAccount> {
|
||||
// Check if already has one
|
||||
const existing = await this.getVirtualAccount(tenantId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
// In production, this would call the provider API to create a CLABE
|
||||
// For now, generate a mock CLABE
|
||||
const mockClabe = `646180${Math.floor(Math.random() * 1000000000000).toString().padStart(12, '0')}`;
|
||||
|
||||
const account = this.virtualAccountRepo.create({
|
||||
tenantId,
|
||||
provider,
|
||||
clabe: mockClabe,
|
||||
beneficiaryName,
|
||||
status: VirtualAccountStatus.ACTIVE,
|
||||
});
|
||||
|
||||
return this.virtualAccountRepo.save(account);
|
||||
}
|
||||
|
||||
// ==================== CODI ====================
|
||||
|
||||
async generateQr(tenantId: string, dto: GenerateQrDto): Promise<CodiTransaction> {
|
||||
// Generate unique reference
|
||||
const result = await this.dataSource.query(
|
||||
`SELECT generate_codi_reference($1) as reference`,
|
||||
[tenantId],
|
||||
);
|
||||
const reference = result[0].reference;
|
||||
|
||||
// Set expiry (5 minutes)
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setMinutes(expiresAt.getMinutes() + 5);
|
||||
|
||||
// In production, this would call Banxico/PAC API to generate real CoDi QR
|
||||
// For now, generate mock QR data
|
||||
const qrData = JSON.stringify({
|
||||
type: 'codi',
|
||||
amount: dto.amount,
|
||||
reference,
|
||||
merchant: tenantId,
|
||||
expires: expiresAt.toISOString(),
|
||||
});
|
||||
|
||||
const transaction = this.codiRepo.create({
|
||||
tenantId,
|
||||
saleId: dto.saleId,
|
||||
qrData,
|
||||
amount: dto.amount,
|
||||
reference,
|
||||
description: dto.description || `Cobro ${reference}`,
|
||||
status: CodiTransactionStatus.PENDING,
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
return this.codiRepo.save(transaction);
|
||||
}
|
||||
|
||||
async getCodiStatus(id: string): Promise<CodiTransaction> {
|
||||
const transaction = await this.codiRepo.findOne({ where: { id } });
|
||||
if (!transaction) {
|
||||
throw new NotFoundException('Transaccion CoDi no encontrada');
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
if (
|
||||
transaction.status === CodiTransactionStatus.PENDING &&
|
||||
new Date() > transaction.expiresAt
|
||||
) {
|
||||
transaction.status = CodiTransactionStatus.EXPIRED;
|
||||
await this.codiRepo.save(transaction);
|
||||
}
|
||||
|
||||
return transaction;
|
||||
}
|
||||
|
||||
async confirmCodi(id: string, providerData: any): Promise<CodiTransaction> {
|
||||
const transaction = await this.getCodiStatus(id);
|
||||
|
||||
if (transaction.status !== CodiTransactionStatus.PENDING) {
|
||||
throw new BadRequestException(`Transaccion no esta pendiente: ${transaction.status}`);
|
||||
}
|
||||
|
||||
transaction.status = CodiTransactionStatus.CONFIRMED;
|
||||
transaction.confirmedAt = new Date();
|
||||
transaction.providerResponse = providerData;
|
||||
|
||||
return this.codiRepo.save(transaction);
|
||||
}
|
||||
|
||||
async getCodiTransactions(
|
||||
tenantId: string,
|
||||
limit = 50,
|
||||
): Promise<CodiTransaction[]> {
|
||||
return this.codiRepo.find({
|
||||
where: { tenantId },
|
||||
order: { createdAt: 'DESC' },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== SPEI ====================
|
||||
|
||||
async getSpeiTransactions(
|
||||
tenantId: string,
|
||||
limit = 50,
|
||||
): Promise<SpeiTransaction[]> {
|
||||
return this.speiRepo.find({
|
||||
where: { tenantId },
|
||||
order: { receivedAt: 'DESC' },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
async receiveSpei(
|
||||
tenantId: string,
|
||||
data: {
|
||||
amount: number;
|
||||
senderClabe?: string;
|
||||
senderName?: string;
|
||||
senderRfc?: string;
|
||||
senderBank?: string;
|
||||
reference?: string;
|
||||
trackingKey?: string;
|
||||
providerData?: any;
|
||||
},
|
||||
): Promise<SpeiTransaction> {
|
||||
const account = await this.getVirtualAccount(tenantId);
|
||||
|
||||
const transaction = this.speiRepo.create({
|
||||
tenantId,
|
||||
virtualAccountId: account?.id,
|
||||
amount: data.amount,
|
||||
senderClabe: data.senderClabe,
|
||||
senderName: data.senderName,
|
||||
senderRfc: data.senderRfc,
|
||||
senderBank: data.senderBank,
|
||||
reference: data.reference,
|
||||
trackingKey: data.trackingKey,
|
||||
status: SpeiTransactionStatus.RECEIVED,
|
||||
receivedAt: new Date(),
|
||||
providerData: data.providerData,
|
||||
});
|
||||
|
||||
return this.speiRepo.save(transaction);
|
||||
}
|
||||
|
||||
async reconcileSpei(id: string, saleId: string): Promise<SpeiTransaction> {
|
||||
const transaction = await this.speiRepo.findOne({ where: { id } });
|
||||
if (!transaction) {
|
||||
throw new NotFoundException('Transaccion SPEI no encontrada');
|
||||
}
|
||||
|
||||
transaction.saleId = saleId;
|
||||
transaction.status = SpeiTransactionStatus.RECONCILED;
|
||||
transaction.reconciledAt = new Date();
|
||||
|
||||
return this.speiRepo.save(transaction);
|
||||
}
|
||||
|
||||
// ==================== STATS ====================
|
||||
|
||||
async getSummary(tenantId: string, date?: Date) {
|
||||
const targetDate = date || new Date();
|
||||
const dateStr = targetDate.toISOString().split('T')[0];
|
||||
|
||||
const result = await this.dataSource.query(
|
||||
`SELECT * FROM get_codi_spei_summary($1, $2::date)`,
|
||||
[tenantId, dateStr],
|
||||
);
|
||||
|
||||
return result[0] || {
|
||||
codi_count: 0,
|
||||
codi_total: 0,
|
||||
spei_count: 0,
|
||||
spei_total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== WEBHOOKS ====================
|
||||
|
||||
async handleCodiWebhook(payload: any): Promise<void> {
|
||||
// In production, validate webhook signature
|
||||
// Find transaction by reference and confirm
|
||||
const { reference, status, transactionId } = payload;
|
||||
|
||||
const transaction = await this.codiRepo.findOne({
|
||||
where: { reference },
|
||||
});
|
||||
|
||||
if (!transaction) {
|
||||
throw new NotFoundException('Transaccion no encontrada');
|
||||
}
|
||||
|
||||
if (status === 'confirmed') {
|
||||
await this.confirmCodi(transaction.id, {
|
||||
providerTransactionId: transactionId,
|
||||
...payload,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleSpeiWebhook(clabe: string, payload: any): Promise<void> {
|
||||
// Find virtual account by CLABE
|
||||
const account = await this.virtualAccountRepo.findOne({
|
||||
where: { clabe },
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
throw new NotFoundException('Cuenta virtual no encontrada');
|
||||
}
|
||||
|
||||
// Record incoming SPEI
|
||||
await this.receiveSpei(account.tenantId, {
|
||||
amount: payload.amount,
|
||||
senderClabe: payload.senderClabe,
|
||||
senderName: payload.senderName,
|
||||
senderRfc: payload.senderRfc,
|
||||
senderBank: payload.senderBank,
|
||||
reference: payload.reference,
|
||||
trackingKey: payload.trackingKey,
|
||||
providerData: payload,
|
||||
});
|
||||
}
|
||||
}
|
||||
31
apps/backend/src/modules/codi-spei/dto/generate-qr.dto.ts
Normal file
31
apps/backend/src/modules/codi-spei/dto/generate-qr.dto.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber, IsString, IsOptional, Min, Max } from 'class-validator';
|
||||
|
||||
export class GenerateQrDto {
|
||||
@ApiProperty({
|
||||
example: 150.5,
|
||||
description: 'Monto a cobrar',
|
||||
})
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(8000) // CoDi max limit
|
||||
amount: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Venta #123',
|
||||
description: 'Descripcion del cobro',
|
||||
required: false,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'sale-uuid',
|
||||
description: 'ID de la venta asociada',
|
||||
required: false,
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
saleId?: string;
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
export enum CodiTransactionStatus {
|
||||
PENDING = 'pending',
|
||||
CONFIRMED = 'confirmed',
|
||||
EXPIRED = 'expired',
|
||||
CANCELLED = 'cancelled',
|
||||
}
|
||||
|
||||
@Entity({ schema: 'sales', name: 'codi_transactions' })
|
||||
export class CodiTransaction {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id' })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ name: 'sale_id', nullable: true })
|
||||
saleId: string;
|
||||
|
||||
@Column({ name: 'qr_data', type: 'text' })
|
||||
qrData: string;
|
||||
|
||||
@Column({ name: 'qr_image_url', type: 'text', nullable: true })
|
||||
qrImageUrl: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
amount: number;
|
||||
|
||||
@Column({ length: 50, nullable: true })
|
||||
reference: string;
|
||||
|
||||
@Column({ length: 200, nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
default: CodiTransactionStatus.PENDING,
|
||||
})
|
||||
status: CodiTransactionStatus;
|
||||
|
||||
@Column({ name: 'expires_at', type: 'timestamptz' })
|
||||
expiresAt: Date;
|
||||
|
||||
@Column({ name: 'confirmed_at', type: 'timestamptz', nullable: true })
|
||||
confirmedAt: Date;
|
||||
|
||||
@Column({ name: 'provider_transaction_id', length: 100, nullable: true })
|
||||
providerTransactionId: string;
|
||||
|
||||
@Column({ name: 'provider_response', type: 'jsonb', nullable: true })
|
||||
providerResponse: Record<string, any>;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { VirtualAccount } from './virtual-account.entity';
|
||||
|
||||
export enum SpeiTransactionStatus {
|
||||
RECEIVED = 'received',
|
||||
RECONCILED = 'reconciled',
|
||||
DISPUTED = 'disputed',
|
||||
}
|
||||
|
||||
@Entity({ schema: 'sales', name: 'spei_transactions' })
|
||||
export class SpeiTransaction {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id' })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ name: 'virtual_account_id', nullable: true })
|
||||
virtualAccountId: string;
|
||||
|
||||
@Column({ name: 'sale_id', nullable: true })
|
||||
saleId: string;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2 })
|
||||
amount: number;
|
||||
|
||||
@Column({ name: 'sender_clabe', length: 18, nullable: true })
|
||||
senderClabe: string;
|
||||
|
||||
@Column({ name: 'sender_name', length: 100, nullable: true })
|
||||
senderName: string;
|
||||
|
||||
@Column({ name: 'sender_rfc', length: 13, nullable: true })
|
||||
senderRfc: string;
|
||||
|
||||
@Column({ name: 'sender_bank', length: 50, nullable: true })
|
||||
senderBank: string;
|
||||
|
||||
@Column({ length: 50, nullable: true })
|
||||
reference: string;
|
||||
|
||||
@Column({ length: 200, nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ name: 'tracking_key', length: 50, nullable: true })
|
||||
trackingKey: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
default: SpeiTransactionStatus.RECEIVED,
|
||||
})
|
||||
status: SpeiTransactionStatus;
|
||||
|
||||
@Column({ name: 'received_at', type: 'timestamptz' })
|
||||
receivedAt: Date;
|
||||
|
||||
@Column({ name: 'reconciled_at', type: 'timestamptz', nullable: true })
|
||||
reconciledAt: Date;
|
||||
|
||||
@Column({ name: 'provider_data', type: 'jsonb', nullable: true })
|
||||
providerData: Record<string, any>;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => VirtualAccount)
|
||||
@JoinColumn({ name: 'virtual_account_id' })
|
||||
virtualAccount: VirtualAccount;
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
export enum VirtualAccountStatus {
|
||||
ACTIVE = 'active',
|
||||
SUSPENDED = 'suspended',
|
||||
CLOSED = 'closed',
|
||||
}
|
||||
|
||||
@Entity({ schema: 'sales', name: 'virtual_accounts' })
|
||||
export class VirtualAccount {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id' })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ length: 20, default: 'stp' })
|
||||
provider: string;
|
||||
|
||||
@Column({ length: 18, unique: true })
|
||||
clabe: string;
|
||||
|
||||
@Column({ name: 'beneficiary_name', length: 100, nullable: true })
|
||||
beneficiaryName: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
default: VirtualAccountStatus.ACTIVE,
|
||||
})
|
||||
status: VirtualAccountStatus;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
metadata: Record<string, any>;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
120
apps/backend/src/modules/invoices/dto/create-invoice.dto.ts
Normal file
120
apps/backend/src/modules/invoices/dto/create-invoice.dto.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
IsString,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
Length,
|
||||
IsEmail,
|
||||
Min,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class InvoiceItemDto {
|
||||
@ApiProperty({ example: '50202301', description: 'Clave producto/servicio SAT' })
|
||||
@IsString()
|
||||
@Length(8, 8)
|
||||
claveProdServ: string;
|
||||
|
||||
@ApiProperty({ example: 'Coca-Cola 600ml', description: 'Descripcion del producto' })
|
||||
@IsString()
|
||||
descripcion: string;
|
||||
|
||||
@ApiProperty({ example: 2, description: 'Cantidad' })
|
||||
@IsNumber()
|
||||
@Min(0.000001)
|
||||
cantidad: number;
|
||||
|
||||
@ApiProperty({ example: 'H87', description: 'Clave unidad SAT' })
|
||||
@IsString()
|
||||
@Length(2, 3)
|
||||
claveUnidad: string;
|
||||
|
||||
@ApiProperty({ example: 'Pieza', description: 'Descripcion de la unidad', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
unidad?: string;
|
||||
|
||||
@ApiProperty({ example: 18.00, description: 'Valor unitario' })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
valorUnitario: number;
|
||||
|
||||
@ApiProperty({ example: 0, description: 'Descuento', required: false })
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
descuento?: number;
|
||||
|
||||
@ApiProperty({ example: 'product-uuid', description: 'ID del producto', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
productId?: string;
|
||||
}
|
||||
|
||||
export class CreateInvoiceDto {
|
||||
@ApiProperty({ example: 'sale-uuid', description: 'ID de la venta asociada', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
saleId?: string;
|
||||
|
||||
// Receptor
|
||||
@ApiProperty({ example: 'XAXX010101000', description: 'RFC del receptor' })
|
||||
@IsString()
|
||||
@Length(12, 13)
|
||||
receptorRfc: string;
|
||||
|
||||
@ApiProperty({ example: 'Juan Perez', description: 'Nombre o razon social' })
|
||||
@IsString()
|
||||
receptorNombre: string;
|
||||
|
||||
@ApiProperty({ example: '601', description: 'Regimen fiscal del receptor', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Length(3, 3)
|
||||
receptorRegimenFiscal?: string;
|
||||
|
||||
@ApiProperty({ example: '06600', description: 'Codigo postal del receptor' })
|
||||
@IsString()
|
||||
@Length(5, 5)
|
||||
receptorCodigoPostal: string;
|
||||
|
||||
@ApiProperty({ example: 'G03', description: 'Uso del CFDI' })
|
||||
@IsString()
|
||||
@Length(3, 4)
|
||||
receptorUsoCfdi: string;
|
||||
|
||||
@ApiProperty({ example: 'cliente@email.com', description: 'Email para envio', required: false })
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
receptorEmail?: string;
|
||||
|
||||
// Pago
|
||||
@ApiProperty({ example: '01', description: 'Forma de pago SAT (01=Efectivo, 04=Tarjeta)' })
|
||||
@IsString()
|
||||
@Length(2, 2)
|
||||
formaPago: string;
|
||||
|
||||
@ApiProperty({ example: 'PUE', description: 'Metodo de pago (PUE=Una sola exhibicion)' })
|
||||
@IsString()
|
||||
@Length(3, 3)
|
||||
metodoPago: string;
|
||||
|
||||
@ApiProperty({ example: 'Contado', description: 'Condiciones de pago', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
condicionesPago?: string;
|
||||
|
||||
// Items
|
||||
@ApiProperty({ type: [InvoiceItemDto], description: 'Conceptos de la factura' })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => InvoiceItemDto)
|
||||
items: InvoiceItemDto[];
|
||||
|
||||
// Opcional
|
||||
@ApiProperty({ description: 'Notas adicionales', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
notes?: string;
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Invoice } from './invoice.entity';
|
||||
|
||||
@Entity({ schema: 'billing', name: 'invoice_items' })
|
||||
export class InvoiceItem {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'invoice_id' })
|
||||
invoiceId: string;
|
||||
|
||||
@Column({ name: 'product_id', nullable: true })
|
||||
productId: string;
|
||||
|
||||
// Clave SAT
|
||||
@Column({ name: 'clave_prod_serv', length: 8 })
|
||||
claveProdServ: string;
|
||||
|
||||
@Column({ name: 'no_identificacion', length: 100, nullable: true })
|
||||
noIdentificacion: string;
|
||||
|
||||
// Descripcion
|
||||
@Column({ length: 1000 })
|
||||
descripcion: string;
|
||||
|
||||
// Cantidad
|
||||
@Column({ type: 'decimal', precision: 12, scale: 6 })
|
||||
cantidad: number;
|
||||
|
||||
@Column({ name: 'clave_unidad', length: 3 })
|
||||
claveUnidad: string;
|
||||
|
||||
@Column({ length: 20, nullable: true })
|
||||
unidad: string;
|
||||
|
||||
// Precios
|
||||
@Column({ name: 'valor_unitario', type: 'decimal', precision: 12, scale: 6 })
|
||||
valorUnitario: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0 })
|
||||
descuento: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 12, scale: 2 })
|
||||
importe: number;
|
||||
|
||||
// Objeto de impuesto
|
||||
@Column({ name: 'objeto_imp', length: 2, default: '02' })
|
||||
objetoImp: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => Invoice, (invoice) => invoice.items)
|
||||
@JoinColumn({ name: 'invoice_id' })
|
||||
invoice: Invoice;
|
||||
}
|
||||
152
apps/backend/src/modules/invoices/entities/invoice.entity.ts
Normal file
152
apps/backend/src/modules/invoices/entities/invoice.entity.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { InvoiceItem } from './invoice-item.entity';
|
||||
|
||||
export enum InvoiceType {
|
||||
INGRESO = 'I',
|
||||
EGRESO = 'E',
|
||||
TRASLADO = 'T',
|
||||
PAGO = 'P',
|
||||
NOMINA = 'N',
|
||||
}
|
||||
|
||||
export enum InvoiceStatus {
|
||||
DRAFT = 'draft',
|
||||
PENDING = 'pending',
|
||||
STAMPED = 'stamped',
|
||||
SENT = 'sent',
|
||||
CANCELLED = 'cancelled',
|
||||
}
|
||||
|
||||
@Entity({ schema: 'billing', name: 'invoices' })
|
||||
export class Invoice {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id' })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ name: 'sale_id', nullable: true })
|
||||
saleId: string;
|
||||
|
||||
// Tipo
|
||||
@Column({ name: 'tipo_comprobante', length: 1, default: InvoiceType.INGRESO })
|
||||
tipoComprobante: InvoiceType;
|
||||
|
||||
// Folio fiscal
|
||||
@Column({ length: 36, unique: true, nullable: true })
|
||||
uuid: string;
|
||||
|
||||
@Column({ length: 10, nullable: true })
|
||||
serie: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
folio: number;
|
||||
|
||||
// Receptor
|
||||
@Column({ name: 'receptor_rfc', length: 13 })
|
||||
receptorRfc: string;
|
||||
|
||||
@Column({ name: 'receptor_nombre', length: 200 })
|
||||
receptorNombre: string;
|
||||
|
||||
@Column({ name: 'receptor_regimen_fiscal', length: 3, nullable: true })
|
||||
receptorRegimenFiscal: string;
|
||||
|
||||
@Column({ name: 'receptor_codigo_postal', length: 5 })
|
||||
receptorCodigoPostal: string;
|
||||
|
||||
@Column({ name: 'receptor_uso_cfdi', length: 4 })
|
||||
receptorUsoCfdi: string;
|
||||
|
||||
@Column({ name: 'receptor_email', length: 200, nullable: true })
|
||||
receptorEmail: string;
|
||||
|
||||
// Montos
|
||||
@Column({ type: 'decimal', precision: 12, scale: 2 })
|
||||
subtotal: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 12, scale: 2, default: 0 })
|
||||
descuento: number;
|
||||
|
||||
@Column({ name: 'total_impuestos_trasladados', type: 'decimal', precision: 12, scale: 2, default: 0 })
|
||||
totalImpuestosTrasladados: number;
|
||||
|
||||
@Column({ name: 'total_impuestos_retenidos', type: 'decimal', precision: 12, scale: 2, default: 0 })
|
||||
totalImpuestosRetenidos: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 12, scale: 2 })
|
||||
total: number;
|
||||
|
||||
// Pago
|
||||
@Column({ name: 'forma_pago', length: 2 })
|
||||
formaPago: string;
|
||||
|
||||
@Column({ name: 'metodo_pago', length: 3 })
|
||||
metodoPago: string;
|
||||
|
||||
@Column({ name: 'condiciones_pago', length: 100, nullable: true })
|
||||
condicionesPago: string;
|
||||
|
||||
// Moneda
|
||||
@Column({ length: 3, default: 'MXN' })
|
||||
moneda: string;
|
||||
|
||||
@Column({ name: 'tipo_cambio', type: 'decimal', precision: 10, scale: 6, default: 1 })
|
||||
tipoCambio: number;
|
||||
|
||||
// Archivos
|
||||
@Column({ name: 'xml_url', type: 'text', nullable: true })
|
||||
xmlUrl: string;
|
||||
|
||||
@Column({ name: 'pdf_url', type: 'text', nullable: true })
|
||||
pdfUrl: string;
|
||||
|
||||
@Column({ name: 'qr_url', type: 'text', nullable: true })
|
||||
qrUrl: string;
|
||||
|
||||
// Estado
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
default: InvoiceStatus.DRAFT,
|
||||
})
|
||||
status: InvoiceStatus;
|
||||
|
||||
// Cancelacion
|
||||
@Column({ name: 'cancelled_at', type: 'timestamptz', nullable: true })
|
||||
cancelledAt: Date;
|
||||
|
||||
@Column({ name: 'cancel_reason', length: 2, nullable: true })
|
||||
cancelReason: string;
|
||||
|
||||
@Column({ name: 'cancel_uuid_replacement', length: 36, nullable: true })
|
||||
cancelUuidReplacement: string;
|
||||
|
||||
// Timbrado
|
||||
@Column({ name: 'stamped_at', type: 'timestamptz', nullable: true })
|
||||
stampedAt: Date;
|
||||
|
||||
@Column({ name: 'pac_response', type: 'jsonb', nullable: true })
|
||||
pacResponse: Record<string, any>;
|
||||
|
||||
// Metadata
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
// Relations
|
||||
@OneToMany(() => InvoiceItem, (item) => item.invoice)
|
||||
items: InvoiceItem[];
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
export enum TaxConfigStatus {
|
||||
PENDING = 'pending',
|
||||
ACTIVE = 'active',
|
||||
SUSPENDED = 'suspended',
|
||||
}
|
||||
|
||||
@Entity({ schema: 'billing', name: 'tax_configs' })
|
||||
export class TaxConfig {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id', unique: true })
|
||||
tenantId: string;
|
||||
|
||||
// Datos fiscales
|
||||
@Column({ length: 13 })
|
||||
rfc: string;
|
||||
|
||||
@Column({ name: 'razon_social', length: 200 })
|
||||
razonSocial: string;
|
||||
|
||||
@Column({ name: 'regimen_fiscal', length: 3 })
|
||||
regimenFiscal: string;
|
||||
|
||||
@Column({ name: 'codigo_postal', length: 5 })
|
||||
codigoPostal: string;
|
||||
|
||||
// CSD (encrypted fields)
|
||||
@Column({ name: 'csd_certificate', type: 'text', nullable: true })
|
||||
csdCertificate: string;
|
||||
|
||||
@Column({ name: 'csd_private_key_encrypted', type: 'text', nullable: true })
|
||||
csdPrivateKeyEncrypted: string;
|
||||
|
||||
@Column({ name: 'csd_password_encrypted', type: 'text', nullable: true })
|
||||
csdPasswordEncrypted: string;
|
||||
|
||||
@Column({ name: 'csd_valid_from', type: 'timestamptz', nullable: true })
|
||||
csdValidFrom: Date;
|
||||
|
||||
@Column({ name: 'csd_valid_to', type: 'timestamptz', nullable: true })
|
||||
csdValidTo: Date;
|
||||
|
||||
// PAC
|
||||
@Column({ name: 'pac_provider', length: 20, default: 'facturapi' })
|
||||
pacProvider: string;
|
||||
|
||||
@Column({ name: 'pac_api_key_encrypted', type: 'text', nullable: true })
|
||||
pacApiKeyEncrypted: string;
|
||||
|
||||
@Column({ name: 'pac_sandbox', default: true })
|
||||
pacSandbox: boolean;
|
||||
|
||||
// Configuracion
|
||||
@Column({ length: 10, default: 'A' })
|
||||
serie: string;
|
||||
|
||||
@Column({ name: 'folio_actual', default: 1 })
|
||||
folioActual: number;
|
||||
|
||||
@Column({ name: 'auto_send_email', default: true })
|
||||
autoSendEmail: boolean;
|
||||
|
||||
@Column({ name: 'logo_url', type: 'text', nullable: true })
|
||||
logoUrl: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
default: TaxConfigStatus.PENDING,
|
||||
})
|
||||
status: TaxConfigStatus;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
117
apps/backend/src/modules/invoices/invoices.controller.ts
Normal file
117
apps/backend/src/modules/invoices/invoices.controller.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
Request,
|
||||
UseGuards,
|
||||
ParseUUIDPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { InvoicesService } from './invoices.service';
|
||||
import { CreateInvoiceDto } from './dto/create-invoice.dto';
|
||||
import { TaxConfig } from './entities/tax-config.entity';
|
||||
import { InvoiceStatus } from './entities/invoice.entity';
|
||||
|
||||
@ApiTags('Invoices')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('invoices')
|
||||
export class InvoicesController {
|
||||
constructor(private readonly invoicesService: InvoicesService) {}
|
||||
|
||||
// ==================== TAX CONFIG ====================
|
||||
|
||||
@Get('tax-config')
|
||||
@ApiOperation({ summary: 'Obtener configuracion fiscal del tenant' })
|
||||
async getTaxConfig(@Request() req): Promise<TaxConfig | null> {
|
||||
return this.invoicesService.getTaxConfig(req.user.tenantId);
|
||||
}
|
||||
|
||||
@Post('tax-config')
|
||||
@ApiOperation({ summary: 'Guardar/actualizar configuracion fiscal' })
|
||||
async saveTaxConfig(
|
||||
@Request() req,
|
||||
@Body() data: Partial<TaxConfig>,
|
||||
): Promise<TaxConfig> {
|
||||
return this.invoicesService.saveTaxConfig(req.user.tenantId, data);
|
||||
}
|
||||
|
||||
// ==================== INVOICES ====================
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Crear nueva factura' })
|
||||
async createInvoice(
|
||||
@Request() req,
|
||||
@Body() dto: CreateInvoiceDto,
|
||||
) {
|
||||
return this.invoicesService.createInvoice(req.user.tenantId, dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Listar facturas del tenant' })
|
||||
@ApiQuery({ name: 'status', required: false, enum: InvoiceStatus })
|
||||
@ApiQuery({ name: 'from', required: false, type: String })
|
||||
@ApiQuery({ name: 'to', required: false, type: String })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
async getInvoices(
|
||||
@Request() req,
|
||||
@Query('status') status?: InvoiceStatus,
|
||||
@Query('from') from?: string,
|
||||
@Query('to') to?: string,
|
||||
@Query('limit') limit?: number,
|
||||
) {
|
||||
return this.invoicesService.getInvoices(req.user.tenantId, {
|
||||
status,
|
||||
from: from ? new Date(from) : undefined,
|
||||
to: to ? new Date(to) : undefined,
|
||||
limit: limit ? Number(limit) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('summary')
|
||||
@ApiOperation({ summary: 'Obtener resumen de facturacion del mes' })
|
||||
@ApiQuery({ name: 'month', required: false, type: String, description: 'YYYY-MM-DD' })
|
||||
async getSummary(
|
||||
@Request() req,
|
||||
@Query('month') month?: string,
|
||||
) {
|
||||
return this.invoicesService.getSummary(
|
||||
req.user.tenantId,
|
||||
month ? new Date(month) : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Obtener factura por ID' })
|
||||
async getInvoice(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.invoicesService.getInvoice(id);
|
||||
}
|
||||
|
||||
@Post(':id/stamp')
|
||||
@ApiOperation({ summary: 'Timbrar factura (enviar al SAT)' })
|
||||
async stampInvoice(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.invoicesService.stampInvoice(id);
|
||||
}
|
||||
|
||||
@Post(':id/cancel')
|
||||
@ApiOperation({ summary: 'Cancelar factura' })
|
||||
async cancelInvoice(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() body: { reason: string; uuidReplacement?: string },
|
||||
) {
|
||||
return this.invoicesService.cancelInvoice(id, body.reason, body.uuidReplacement);
|
||||
}
|
||||
|
||||
@Post(':id/send')
|
||||
@ApiOperation({ summary: 'Enviar factura por email' })
|
||||
async sendInvoice(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() body: { email?: string },
|
||||
) {
|
||||
return this.invoicesService.sendInvoice(id, body.email);
|
||||
}
|
||||
}
|
||||
17
apps/backend/src/modules/invoices/invoices.module.ts
Normal file
17
apps/backend/src/modules/invoices/invoices.module.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { TaxConfig } from './entities/tax-config.entity';
|
||||
import { Invoice } from './entities/invoice.entity';
|
||||
import { InvoiceItem } from './entities/invoice-item.entity';
|
||||
import { InvoicesService } from './invoices.service';
|
||||
import { InvoicesController } from './invoices.controller';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([TaxConfig, Invoice, InvoiceItem]),
|
||||
],
|
||||
controllers: [InvoicesController],
|
||||
providers: [InvoicesService],
|
||||
exports: [InvoicesService],
|
||||
})
|
||||
export class InvoicesModule {}
|
||||
252
apps/backend/src/modules/invoices/invoices.service.ts
Normal file
252
apps/backend/src/modules/invoices/invoices.service.ts
Normal file
@ -0,0 +1,252 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { TaxConfig, TaxConfigStatus } from './entities/tax-config.entity';
|
||||
import { Invoice, InvoiceStatus, InvoiceType } from './entities/invoice.entity';
|
||||
import { InvoiceItem } from './entities/invoice-item.entity';
|
||||
import { CreateInvoiceDto } from './dto/create-invoice.dto';
|
||||
|
||||
@Injectable()
|
||||
export class InvoicesService {
|
||||
constructor(
|
||||
@InjectRepository(TaxConfig)
|
||||
private readonly taxConfigRepo: Repository<TaxConfig>,
|
||||
@InjectRepository(Invoice)
|
||||
private readonly invoiceRepo: Repository<Invoice>,
|
||||
@InjectRepository(InvoiceItem)
|
||||
private readonly itemRepo: Repository<InvoiceItem>,
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
// ==================== TAX CONFIG ====================
|
||||
|
||||
async getTaxConfig(tenantId: string): Promise<TaxConfig | null> {
|
||||
return this.taxConfigRepo.findOne({ where: { tenantId } });
|
||||
}
|
||||
|
||||
async saveTaxConfig(
|
||||
tenantId: string,
|
||||
data: Partial<TaxConfig>,
|
||||
): Promise<TaxConfig> {
|
||||
let config = await this.getTaxConfig(tenantId);
|
||||
|
||||
if (config) {
|
||||
Object.assign(config, data);
|
||||
} else {
|
||||
config = this.taxConfigRepo.create({ tenantId, ...data });
|
||||
}
|
||||
|
||||
return this.taxConfigRepo.save(config);
|
||||
}
|
||||
|
||||
// ==================== INVOICES ====================
|
||||
|
||||
async createInvoice(tenantId: string, dto: CreateInvoiceDto): Promise<Invoice> {
|
||||
const taxConfig = await this.getTaxConfig(tenantId);
|
||||
if (!taxConfig || taxConfig.status !== TaxConfigStatus.ACTIVE) {
|
||||
throw new BadRequestException('Configuracion fiscal no activa');
|
||||
}
|
||||
|
||||
// Calculate totals
|
||||
let subtotal = 0;
|
||||
let totalIva = 0;
|
||||
|
||||
for (const item of dto.items) {
|
||||
const importe = item.cantidad * item.valorUnitario - (item.descuento || 0);
|
||||
subtotal += importe;
|
||||
// IVA 16%
|
||||
totalIva += importe * 0.16;
|
||||
}
|
||||
|
||||
const total = subtotal + totalIva;
|
||||
|
||||
// Get next folio
|
||||
const folioResult = await this.dataSource.query(
|
||||
`SELECT get_next_invoice_folio($1, $2) as folio`,
|
||||
[tenantId, taxConfig.serie],
|
||||
);
|
||||
const folio = folioResult[0].folio;
|
||||
|
||||
// Create invoice
|
||||
const invoice = this.invoiceRepo.create({
|
||||
tenantId,
|
||||
saleId: dto.saleId,
|
||||
tipoComprobante: InvoiceType.INGRESO,
|
||||
serie: taxConfig.serie,
|
||||
folio,
|
||||
receptorRfc: dto.receptorRfc.toUpperCase(),
|
||||
receptorNombre: dto.receptorNombre,
|
||||
receptorRegimenFiscal: dto.receptorRegimenFiscal,
|
||||
receptorCodigoPostal: dto.receptorCodigoPostal,
|
||||
receptorUsoCfdi: dto.receptorUsoCfdi,
|
||||
receptorEmail: dto.receptorEmail,
|
||||
subtotal,
|
||||
totalImpuestosTrasladados: totalIva,
|
||||
total,
|
||||
formaPago: dto.formaPago,
|
||||
metodoPago: dto.metodoPago,
|
||||
condicionesPago: dto.condicionesPago,
|
||||
status: InvoiceStatus.DRAFT,
|
||||
notes: dto.notes,
|
||||
});
|
||||
|
||||
await this.invoiceRepo.save(invoice);
|
||||
|
||||
// Create items
|
||||
for (const itemDto of dto.items) {
|
||||
const importe = itemDto.cantidad * itemDto.valorUnitario - (itemDto.descuento || 0);
|
||||
|
||||
const item = this.itemRepo.create({
|
||||
invoiceId: invoice.id,
|
||||
productId: itemDto.productId,
|
||||
claveProdServ: itemDto.claveProdServ,
|
||||
descripcion: itemDto.descripcion,
|
||||
cantidad: itemDto.cantidad,
|
||||
claveUnidad: itemDto.claveUnidad,
|
||||
unidad: itemDto.unidad,
|
||||
valorUnitario: itemDto.valorUnitario,
|
||||
descuento: itemDto.descuento || 0,
|
||||
importe,
|
||||
});
|
||||
|
||||
await this.itemRepo.save(item);
|
||||
}
|
||||
|
||||
return this.getInvoice(invoice.id);
|
||||
}
|
||||
|
||||
async getInvoice(id: string): Promise<Invoice> {
|
||||
const invoice = await this.invoiceRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['items'],
|
||||
});
|
||||
|
||||
if (!invoice) {
|
||||
throw new NotFoundException('Factura no encontrada');
|
||||
}
|
||||
|
||||
return invoice;
|
||||
}
|
||||
|
||||
async getInvoices(
|
||||
tenantId: string,
|
||||
options?: {
|
||||
status?: InvoiceStatus;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
limit?: number;
|
||||
},
|
||||
): Promise<Invoice[]> {
|
||||
const query = this.invoiceRepo.createQueryBuilder('invoice')
|
||||
.where('invoice.tenant_id = :tenantId', { tenantId })
|
||||
.leftJoinAndSelect('invoice.items', 'items')
|
||||
.orderBy('invoice.created_at', 'DESC');
|
||||
|
||||
if (options?.status) {
|
||||
query.andWhere('invoice.status = :status', { status: options.status });
|
||||
}
|
||||
|
||||
if (options?.from) {
|
||||
query.andWhere('invoice.created_at >= :from', { from: options.from });
|
||||
}
|
||||
|
||||
if (options?.to) {
|
||||
query.andWhere('invoice.created_at <= :to', { to: options.to });
|
||||
}
|
||||
|
||||
if (options?.limit) {
|
||||
query.limit(options.limit);
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
async stampInvoice(id: string): Promise<Invoice> {
|
||||
const invoice = await this.getInvoice(id);
|
||||
|
||||
if (invoice.status !== InvoiceStatus.DRAFT && invoice.status !== InvoiceStatus.PENDING) {
|
||||
throw new BadRequestException(`No se puede timbrar factura con estado: ${invoice.status}`);
|
||||
}
|
||||
|
||||
const taxConfig = await this.getTaxConfig(invoice.tenantId);
|
||||
if (!taxConfig) {
|
||||
throw new BadRequestException('Configuracion fiscal no encontrada');
|
||||
}
|
||||
|
||||
// In production, this would call the PAC API (Facturapi, etc.)
|
||||
// For now, generate mock UUID
|
||||
const mockUuid = `${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 9)}`.toUpperCase();
|
||||
|
||||
invoice.uuid = mockUuid;
|
||||
invoice.status = InvoiceStatus.STAMPED;
|
||||
invoice.stampedAt = new Date();
|
||||
invoice.pacResponse = {
|
||||
provider: taxConfig.pacProvider,
|
||||
sandbox: taxConfig.pacSandbox,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return this.invoiceRepo.save(invoice);
|
||||
}
|
||||
|
||||
async cancelInvoice(
|
||||
id: string,
|
||||
reason: string,
|
||||
uuidReplacement?: string,
|
||||
): Promise<Invoice> {
|
||||
const invoice = await this.getInvoice(id);
|
||||
|
||||
if (invoice.status !== InvoiceStatus.STAMPED && invoice.status !== InvoiceStatus.SENT) {
|
||||
throw new BadRequestException(`No se puede cancelar factura con estado: ${invoice.status}`);
|
||||
}
|
||||
|
||||
// In production, this would call the PAC API to cancel
|
||||
invoice.status = InvoiceStatus.CANCELLED;
|
||||
invoice.cancelledAt = new Date();
|
||||
invoice.cancelReason = reason;
|
||||
invoice.cancelUuidReplacement = uuidReplacement;
|
||||
|
||||
return this.invoiceRepo.save(invoice);
|
||||
}
|
||||
|
||||
async sendInvoice(id: string, email?: string): Promise<Invoice> {
|
||||
const invoice = await this.getInvoice(id);
|
||||
|
||||
if (invoice.status !== InvoiceStatus.STAMPED) {
|
||||
throw new BadRequestException('Solo se pueden enviar facturas timbradas');
|
||||
}
|
||||
|
||||
const targetEmail = email || invoice.receptorEmail;
|
||||
if (!targetEmail) {
|
||||
throw new BadRequestException('No hay email de destino');
|
||||
}
|
||||
|
||||
// In production, this would send the email with PDF and XML
|
||||
invoice.status = InvoiceStatus.SENT;
|
||||
|
||||
return this.invoiceRepo.save(invoice);
|
||||
}
|
||||
|
||||
// ==================== SUMMARY ====================
|
||||
|
||||
async getSummary(tenantId: string, month?: Date) {
|
||||
const targetMonth = month || new Date();
|
||||
const monthStr = targetMonth.toISOString().split('T')[0];
|
||||
|
||||
const result = await this.dataSource.query(
|
||||
`SELECT * FROM get_invoice_summary($1, $2::date)`,
|
||||
[tenantId, monthStr],
|
||||
);
|
||||
|
||||
return result[0] || {
|
||||
total_invoices: 0,
|
||||
total_amount: 0,
|
||||
total_cancelled: 0,
|
||||
by_status: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
IsString,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
IsUUID,
|
||||
Min,
|
||||
IsDateString,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class SupplierOrderItemDto {
|
||||
@ApiProperty({ description: 'ID del producto del proveedor' })
|
||||
@IsUUID()
|
||||
productId: string;
|
||||
|
||||
@ApiProperty({ example: 10, description: 'Cantidad a ordenar' })
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
quantity: number;
|
||||
|
||||
@ApiProperty({ description: 'Notas para este item', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class CreateSupplierOrderDto {
|
||||
@ApiProperty({ description: 'ID del proveedor' })
|
||||
@IsUUID()
|
||||
supplierId: string;
|
||||
|
||||
@ApiProperty({ type: [SupplierOrderItemDto], description: 'Items del pedido' })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => SupplierOrderItemDto)
|
||||
items: SupplierOrderItemDto[];
|
||||
|
||||
@ApiProperty({ description: 'Direccion de entrega' })
|
||||
@IsString()
|
||||
deliveryAddress: string;
|
||||
|
||||
@ApiProperty({ description: 'Ciudad', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
deliveryCity?: string;
|
||||
|
||||
@ApiProperty({ description: 'Codigo postal', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
deliveryZip?: string;
|
||||
|
||||
@ApiProperty({ description: 'Telefono de contacto', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
deliveryPhone?: string;
|
||||
|
||||
@ApiProperty({ description: 'Nombre de contacto', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
deliveryContact?: string;
|
||||
|
||||
@ApiProperty({ description: 'Fecha solicitada de entrega (YYYY-MM-DD)', required: false })
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
requestedDate?: string;
|
||||
|
||||
@ApiProperty({ description: 'Notas adicionales', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
notes?: string;
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
IsString,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
Min,
|
||||
Max,
|
||||
} from 'class-validator';
|
||||
|
||||
export class CreateSupplierReviewDto {
|
||||
@ApiProperty({ description: 'ID del proveedor' })
|
||||
@IsUUID()
|
||||
supplierId: string;
|
||||
|
||||
@ApiProperty({ description: 'ID de la orden (opcional)', required: false })
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
orderId?: string;
|
||||
|
||||
@ApiProperty({ example: 5, description: 'Rating general (1-5)' })
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(5)
|
||||
rating: number;
|
||||
|
||||
@ApiProperty({ description: 'Titulo de la resena', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
title?: string;
|
||||
|
||||
@ApiProperty({ description: 'Comentario', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
comment?: string;
|
||||
|
||||
@ApiProperty({ description: 'Rating de calidad (1-5)', required: false })
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(5)
|
||||
@IsOptional()
|
||||
ratingQuality?: number;
|
||||
|
||||
@ApiProperty({ description: 'Rating de entrega (1-5)', required: false })
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(5)
|
||||
@IsOptional()
|
||||
ratingDelivery?: number;
|
||||
|
||||
@ApiProperty({ description: 'Rating de precio (1-5)', required: false })
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(5)
|
||||
@IsOptional()
|
||||
ratingPrice?: number;
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { SupplierOrder } from './supplier-order.entity';
|
||||
import { SupplierProduct } from './supplier-product.entity';
|
||||
|
||||
@Entity({ schema: 'marketplace', name: 'supplier_order_items' })
|
||||
export class SupplierOrderItem {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'order_id' })
|
||||
orderId: string;
|
||||
|
||||
@Column({ name: 'product_id' })
|
||||
productId: string;
|
||||
|
||||
@Column({ name: 'product_name', length: 300 })
|
||||
productName: string;
|
||||
|
||||
@Column({ name: 'product_sku', length: 100, nullable: true })
|
||||
productSku: string;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
quantity: number;
|
||||
|
||||
@Column({ name: 'unit_price', type: 'decimal', precision: 10, scale: 2 })
|
||||
unitPrice: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 12, scale: 2 })
|
||||
total: number;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => SupplierOrder, (order) => order.items)
|
||||
@JoinColumn({ name: 'order_id' })
|
||||
order: SupplierOrder;
|
||||
|
||||
@ManyToOne(() => SupplierProduct)
|
||||
@JoinColumn({ name: 'product_id' })
|
||||
product: SupplierProduct;
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Supplier } from './supplier.entity';
|
||||
import { SupplierOrderItem } from './supplier-order-item.entity';
|
||||
|
||||
export enum SupplierOrderStatus {
|
||||
PENDING = 'pending',
|
||||
CONFIRMED = 'confirmed',
|
||||
PREPARING = 'preparing',
|
||||
SHIPPED = 'shipped',
|
||||
DELIVERED = 'delivered',
|
||||
CANCELLED = 'cancelled',
|
||||
REJECTED = 'rejected',
|
||||
}
|
||||
|
||||
@Entity({ schema: 'marketplace', name: 'supplier_orders' })
|
||||
export class SupplierOrder {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id' })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ name: 'supplier_id' })
|
||||
supplierId: string;
|
||||
|
||||
@Column({ name: 'order_number', type: 'int', generated: 'increment' })
|
||||
orderNumber: number;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 30,
|
||||
default: SupplierOrderStatus.PENDING,
|
||||
})
|
||||
status: SupplierOrderStatus;
|
||||
|
||||
@Column({ type: 'decimal', precision: 12, scale: 2 })
|
||||
subtotal: number;
|
||||
|
||||
@Column({ name: 'delivery_fee', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
deliveryFee: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
discount: number;
|
||||
|
||||
@Column({ type: 'decimal', precision: 12, scale: 2 })
|
||||
total: number;
|
||||
|
||||
@Column({ name: 'delivery_address', type: 'text' })
|
||||
deliveryAddress: string;
|
||||
|
||||
@Column({ name: 'delivery_city', length: 100, nullable: true })
|
||||
deliveryCity: string;
|
||||
|
||||
@Column({ name: 'delivery_zip', length: 10, nullable: true })
|
||||
deliveryZip: string;
|
||||
|
||||
@Column({ name: 'delivery_phone', length: 20, nullable: true })
|
||||
deliveryPhone: string;
|
||||
|
||||
@Column({ name: 'delivery_contact', length: 200, nullable: true })
|
||||
deliveryContact: string;
|
||||
|
||||
@Column({ name: 'requested_date', type: 'date', nullable: true })
|
||||
requestedDate: Date;
|
||||
|
||||
@Column({ name: 'confirmed_date', type: 'date', nullable: true })
|
||||
confirmedDate: Date;
|
||||
|
||||
@Column({ name: 'estimated_delivery', type: 'timestamptz', nullable: true })
|
||||
estimatedDelivery: Date;
|
||||
|
||||
@Column({ name: 'delivered_at', type: 'timestamptz', nullable: true })
|
||||
deliveredAt: Date;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
notes: string;
|
||||
|
||||
@Column({ name: 'supplier_notes', type: 'text', nullable: true })
|
||||
supplierNotes: string;
|
||||
|
||||
@Column({ name: 'cancelled_at', type: 'timestamptz', nullable: true })
|
||||
cancelledAt: Date;
|
||||
|
||||
@Column({ name: 'cancel_reason', type: 'text', nullable: true })
|
||||
cancelReason: string;
|
||||
|
||||
@Column({ name: 'cancelled_by', length: 20, nullable: true })
|
||||
cancelledBy: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => Supplier, (supplier) => supplier.orders)
|
||||
@JoinColumn({ name: 'supplier_id' })
|
||||
supplier: Supplier;
|
||||
|
||||
@OneToMany(() => SupplierOrderItem, (item) => item.order)
|
||||
items: SupplierOrderItem[];
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Supplier } from './supplier.entity';
|
||||
|
||||
@Entity({ schema: 'marketplace', name: 'supplier_products' })
|
||||
export class SupplierProduct {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'supplier_id' })
|
||||
supplierId: string;
|
||||
|
||||
@Column({ length: 300 })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ length: 100, nullable: true })
|
||||
sku: string;
|
||||
|
||||
@Column({ length: 50, nullable: true })
|
||||
barcode: string;
|
||||
|
||||
@Column({ length: 100, nullable: true })
|
||||
category: string;
|
||||
|
||||
@Column({ length: 100, nullable: true })
|
||||
subcategory: string;
|
||||
|
||||
@Column({ name: 'image_url', type: 'text', nullable: true })
|
||||
imageUrl: string;
|
||||
|
||||
@Column({ name: 'unit_price', type: 'decimal', precision: 10, scale: 2 })
|
||||
unitPrice: number;
|
||||
|
||||
@Column({ name: 'unit_type', length: 50, default: 'pieza' })
|
||||
unitType: string;
|
||||
|
||||
@Column({ name: 'min_quantity', default: 1 })
|
||||
minQuantity: number;
|
||||
|
||||
@Column({ name: 'tiered_pricing', type: 'jsonb', default: '[]' })
|
||||
tieredPricing: { min: number; price: number }[];
|
||||
|
||||
@Column({ name: 'in_stock', default: true })
|
||||
inStock: boolean;
|
||||
|
||||
@Column({ name: 'stock_quantity', nullable: true })
|
||||
stockQuantity: number;
|
||||
|
||||
@Column({ default: true })
|
||||
active: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => Supplier, (supplier) => supplier.products)
|
||||
@JoinColumn({ name: 'supplier_id' })
|
||||
supplier: Supplier;
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Supplier } from './supplier.entity';
|
||||
import { SupplierOrder } from './supplier-order.entity';
|
||||
|
||||
@Entity({ schema: 'marketplace', name: 'supplier_reviews' })
|
||||
export class SupplierReview {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id' })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ name: 'supplier_id' })
|
||||
supplierId: string;
|
||||
|
||||
@Column({ name: 'order_id', nullable: true })
|
||||
orderId: string;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
rating: number;
|
||||
|
||||
@Column({ length: 200, nullable: true })
|
||||
title: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
comment: string;
|
||||
|
||||
@Column({ name: 'rating_quality', type: 'int', nullable: true })
|
||||
ratingQuality: number;
|
||||
|
||||
@Column({ name: 'rating_delivery', type: 'int', nullable: true })
|
||||
ratingDelivery: number;
|
||||
|
||||
@Column({ name: 'rating_price', type: 'int', nullable: true })
|
||||
ratingPrice: number;
|
||||
|
||||
@Column({ name: 'supplier_response', type: 'text', nullable: true })
|
||||
supplierResponse: string;
|
||||
|
||||
@Column({ name: 'responded_at', type: 'timestamptz', nullable: true })
|
||||
respondedAt: Date;
|
||||
|
||||
@Column({ default: false })
|
||||
verified: boolean;
|
||||
|
||||
@Column({ length: 20, default: 'active' })
|
||||
status: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => Supplier, (supplier) => supplier.reviews)
|
||||
@JoinColumn({ name: 'supplier_id' })
|
||||
supplier: Supplier;
|
||||
|
||||
@ManyToOne(() => SupplierOrder, { nullable: true })
|
||||
@JoinColumn({ name: 'order_id' })
|
||||
order: SupplierOrder;
|
||||
}
|
||||
124
apps/backend/src/modules/marketplace/entities/supplier.entity.ts
Normal file
124
apps/backend/src/modules/marketplace/entities/supplier.entity.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { SupplierProduct } from './supplier-product.entity';
|
||||
import { SupplierOrder } from './supplier-order.entity';
|
||||
import { SupplierReview } from './supplier-review.entity';
|
||||
|
||||
export enum SupplierStatus {
|
||||
PENDING = 'pending',
|
||||
ACTIVE = 'active',
|
||||
SUSPENDED = 'suspended',
|
||||
INACTIVE = 'inactive',
|
||||
}
|
||||
|
||||
@Entity({ schema: 'marketplace', name: 'suppliers' })
|
||||
export class Supplier {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ length: 200 })
|
||||
name: string;
|
||||
|
||||
@Column({ name: 'legal_name', length: 300, nullable: true })
|
||||
legalName: string;
|
||||
|
||||
@Column({ length: 13, nullable: true })
|
||||
rfc: string;
|
||||
|
||||
@Column({ type: 'text', array: true, default: '{}' })
|
||||
categories: string[];
|
||||
|
||||
@Column({ name: 'coverage_zones', type: 'text', array: true, default: '{}' })
|
||||
coverageZones: string[];
|
||||
|
||||
@Column({ name: 'contact_name', length: 200, nullable: true })
|
||||
contactName: string;
|
||||
|
||||
@Column({ name: 'contact_phone', length: 20, nullable: true })
|
||||
contactPhone: string;
|
||||
|
||||
@Column({ name: 'contact_email', length: 200, nullable: true })
|
||||
contactEmail: string;
|
||||
|
||||
@Column({ name: 'contact_whatsapp', length: 20, nullable: true })
|
||||
contactWhatsapp: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
address: string;
|
||||
|
||||
@Column({ length: 100, nullable: true })
|
||||
city: string;
|
||||
|
||||
@Column({ length: 100, nullable: true })
|
||||
state: string;
|
||||
|
||||
@Column({ name: 'zip_code', length: 10, nullable: true })
|
||||
zipCode: string;
|
||||
|
||||
@Column({ name: 'logo_url', type: 'text', nullable: true })
|
||||
logoUrl: string;
|
||||
|
||||
@Column({ name: 'banner_url', type: 'text', nullable: true })
|
||||
bannerUrl: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ name: 'min_order_amount', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
minOrderAmount: number;
|
||||
|
||||
@Column({ name: 'delivery_fee', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
deliveryFee: number;
|
||||
|
||||
@Column({ name: 'free_delivery_min', type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||
freeDeliveryMin: number;
|
||||
|
||||
@Column({ name: 'delivery_days', type: 'text', array: true, default: '{}' })
|
||||
deliveryDays: string[];
|
||||
|
||||
@Column({ name: 'lead_time_days', default: 1 })
|
||||
leadTimeDays: number;
|
||||
|
||||
@Column({ default: false })
|
||||
verified: boolean;
|
||||
|
||||
@Column({ name: 'verified_at', type: 'timestamptz', nullable: true })
|
||||
verifiedAt: Date;
|
||||
|
||||
@Column({ type: 'decimal', precision: 2, scale: 1, default: 0 })
|
||||
rating: number;
|
||||
|
||||
@Column({ name: 'total_reviews', default: 0 })
|
||||
totalReviews: number;
|
||||
|
||||
@Column({ name: 'total_orders', default: 0 })
|
||||
totalOrders: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 20, default: SupplierStatus.PENDING })
|
||||
status: SupplierStatus;
|
||||
|
||||
@Column({ name: 'user_id', nullable: true })
|
||||
userId: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
// Relations
|
||||
@OneToMany(() => SupplierProduct, (product) => product.supplier)
|
||||
products: SupplierProduct[];
|
||||
|
||||
@OneToMany(() => SupplierOrder, (order) => order.supplier)
|
||||
orders: SupplierOrder[];
|
||||
|
||||
@OneToMany(() => SupplierReview, (review) => review.supplier)
|
||||
reviews: SupplierReview[];
|
||||
}
|
||||
180
apps/backend/src/modules/marketplace/marketplace.controller.ts
Normal file
180
apps/backend/src/modules/marketplace/marketplace.controller.ts
Normal file
@ -0,0 +1,180 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
Request,
|
||||
UseGuards,
|
||||
ParseUUIDPipe,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { MarketplaceService } from './marketplace.service';
|
||||
import { CreateSupplierOrderDto } from './dto/create-supplier-order.dto';
|
||||
import { CreateSupplierReviewDto } from './dto/create-supplier-review.dto';
|
||||
import { SupplierOrderStatus } from './entities/supplier-order.entity';
|
||||
|
||||
@ApiTags('Marketplace')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('marketplace')
|
||||
export class MarketplaceController {
|
||||
constructor(private readonly marketplaceService: MarketplaceService) {}
|
||||
|
||||
// ==================== SUPPLIERS ====================
|
||||
|
||||
@Get('suppliers')
|
||||
@ApiOperation({ summary: 'Listar proveedores' })
|
||||
@ApiQuery({ name: 'category', required: false })
|
||||
@ApiQuery({ name: 'zipCode', required: false })
|
||||
@ApiQuery({ name: 'search', required: false })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
async findSuppliers(
|
||||
@Query('category') category?: string,
|
||||
@Query('zipCode') zipCode?: string,
|
||||
@Query('search') search?: string,
|
||||
@Query('limit') limit?: number,
|
||||
) {
|
||||
return this.marketplaceService.findSuppliers({
|
||||
category,
|
||||
zipCode,
|
||||
search,
|
||||
limit: limit ? Number(limit) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('suppliers/:id')
|
||||
@ApiOperation({ summary: 'Obtener detalle de proveedor' })
|
||||
async getSupplier(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.marketplaceService.getSupplier(id);
|
||||
}
|
||||
|
||||
@Get('suppliers/:id/products')
|
||||
@ApiOperation({ summary: 'Obtener productos de un proveedor' })
|
||||
@ApiQuery({ name: 'category', required: false })
|
||||
@ApiQuery({ name: 'search', required: false })
|
||||
@ApiQuery({ name: 'inStock', required: false, type: Boolean })
|
||||
async getSupplierProducts(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Query('category') category?: string,
|
||||
@Query('search') search?: string,
|
||||
@Query('inStock') inStock?: boolean,
|
||||
) {
|
||||
return this.marketplaceService.getSupplierProducts(id, {
|
||||
category,
|
||||
search,
|
||||
inStock,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('suppliers/:id/reviews')
|
||||
@ApiOperation({ summary: 'Obtener resenas de un proveedor' })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
@ApiQuery({ name: 'offset', required: false, type: Number })
|
||||
async getSupplierReviews(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Query('limit') limit?: number,
|
||||
@Query('offset') offset?: number,
|
||||
) {
|
||||
return this.marketplaceService.getReviews(id, {
|
||||
limit: limit ? Number(limit) : undefined,
|
||||
offset: offset ? Number(offset) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== ORDERS ====================
|
||||
|
||||
@Post('orders')
|
||||
@ApiOperation({ summary: 'Crear pedido a proveedor' })
|
||||
async createOrder(
|
||||
@Request() req,
|
||||
@Body() dto: CreateSupplierOrderDto,
|
||||
) {
|
||||
return this.marketplaceService.createOrder(req.user.tenantId, dto);
|
||||
}
|
||||
|
||||
@Get('orders')
|
||||
@ApiOperation({ summary: 'Listar mis pedidos' })
|
||||
@ApiQuery({ name: 'status', required: false, enum: SupplierOrderStatus })
|
||||
@ApiQuery({ name: 'supplierId', required: false })
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number })
|
||||
async getOrders(
|
||||
@Request() req,
|
||||
@Query('status') status?: SupplierOrderStatus,
|
||||
@Query('supplierId') supplierId?: string,
|
||||
@Query('limit') limit?: number,
|
||||
) {
|
||||
return this.marketplaceService.getOrders(req.user.tenantId, {
|
||||
status,
|
||||
supplierId,
|
||||
limit: limit ? Number(limit) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@Get('orders/:id')
|
||||
@ApiOperation({ summary: 'Obtener detalle de pedido' })
|
||||
async getOrder(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.marketplaceService.getOrder(id);
|
||||
}
|
||||
|
||||
@Put('orders/:id/cancel')
|
||||
@ApiOperation({ summary: 'Cancelar pedido' })
|
||||
async cancelOrder(
|
||||
@Request() req,
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() body: { reason: string },
|
||||
) {
|
||||
return this.marketplaceService.cancelOrder(id, req.user.tenantId, body.reason);
|
||||
}
|
||||
|
||||
// ==================== REVIEWS ====================
|
||||
|
||||
@Post('reviews')
|
||||
@ApiOperation({ summary: 'Crear resena de proveedor' })
|
||||
async createReview(
|
||||
@Request() req,
|
||||
@Body() dto: CreateSupplierReviewDto,
|
||||
) {
|
||||
return this.marketplaceService.createReview(req.user.tenantId, dto);
|
||||
}
|
||||
|
||||
// ==================== FAVORITES ====================
|
||||
|
||||
@Get('favorites')
|
||||
@ApiOperation({ summary: 'Obtener proveedores favoritos' })
|
||||
async getFavorites(@Request() req) {
|
||||
return this.marketplaceService.getFavorites(req.user.tenantId);
|
||||
}
|
||||
|
||||
@Post('favorites/:supplierId')
|
||||
@ApiOperation({ summary: 'Agregar proveedor a favoritos' })
|
||||
async addFavorite(
|
||||
@Request() req,
|
||||
@Param('supplierId', ParseUUIDPipe) supplierId: string,
|
||||
) {
|
||||
await this.marketplaceService.addFavorite(req.user.tenantId, supplierId);
|
||||
return { message: 'Agregado a favoritos' };
|
||||
}
|
||||
|
||||
@Delete('favorites/:supplierId')
|
||||
@ApiOperation({ summary: 'Quitar proveedor de favoritos' })
|
||||
async removeFavorite(
|
||||
@Request() req,
|
||||
@Param('supplierId', ParseUUIDPipe) supplierId: string,
|
||||
) {
|
||||
await this.marketplaceService.removeFavorite(req.user.tenantId, supplierId);
|
||||
return { message: 'Eliminado de favoritos' };
|
||||
}
|
||||
|
||||
// ==================== STATS ====================
|
||||
|
||||
@Get('stats')
|
||||
@ApiOperation({ summary: 'Obtener estadisticas del marketplace' })
|
||||
async getStats() {
|
||||
return this.marketplaceService.getMarketplaceStats();
|
||||
}
|
||||
}
|
||||
25
apps/backend/src/modules/marketplace/marketplace.module.ts
Normal file
25
apps/backend/src/modules/marketplace/marketplace.module.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Supplier } from './entities/supplier.entity';
|
||||
import { SupplierProduct } from './entities/supplier-product.entity';
|
||||
import { SupplierOrder } from './entities/supplier-order.entity';
|
||||
import { SupplierOrderItem } from './entities/supplier-order-item.entity';
|
||||
import { SupplierReview } from './entities/supplier-review.entity';
|
||||
import { MarketplaceService } from './marketplace.service';
|
||||
import { MarketplaceController } from './marketplace.controller';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
Supplier,
|
||||
SupplierProduct,
|
||||
SupplierOrder,
|
||||
SupplierOrderItem,
|
||||
SupplierReview,
|
||||
]),
|
||||
],
|
||||
controllers: [MarketplaceController],
|
||||
providers: [MarketplaceService],
|
||||
exports: [MarketplaceService],
|
||||
})
|
||||
export class MarketplaceModule {}
|
||||
455
apps/backend/src/modules/marketplace/marketplace.service.ts
Normal file
455
apps/backend/src/modules/marketplace/marketplace.service.ts
Normal file
@ -0,0 +1,455 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { Supplier, SupplierStatus } from './entities/supplier.entity';
|
||||
import { SupplierProduct } from './entities/supplier-product.entity';
|
||||
import { SupplierOrder, SupplierOrderStatus } from './entities/supplier-order.entity';
|
||||
import { SupplierOrderItem } from './entities/supplier-order-item.entity';
|
||||
import { SupplierReview } from './entities/supplier-review.entity';
|
||||
import { CreateSupplierOrderDto } from './dto/create-supplier-order.dto';
|
||||
import { CreateSupplierReviewDto } from './dto/create-supplier-review.dto';
|
||||
|
||||
@Injectable()
|
||||
export class MarketplaceService {
|
||||
constructor(
|
||||
@InjectRepository(Supplier)
|
||||
private readonly supplierRepo: Repository<Supplier>,
|
||||
@InjectRepository(SupplierProduct)
|
||||
private readonly productRepo: Repository<SupplierProduct>,
|
||||
@InjectRepository(SupplierOrder)
|
||||
private readonly orderRepo: Repository<SupplierOrder>,
|
||||
@InjectRepository(SupplierOrderItem)
|
||||
private readonly orderItemRepo: Repository<SupplierOrderItem>,
|
||||
@InjectRepository(SupplierReview)
|
||||
private readonly reviewRepo: Repository<SupplierReview>,
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
// ==================== SUPPLIERS ====================
|
||||
|
||||
async findSuppliers(options?: {
|
||||
category?: string;
|
||||
zipCode?: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
}): Promise<Supplier[]> {
|
||||
const query = this.supplierRepo.createQueryBuilder('supplier')
|
||||
.where('supplier.status = :status', { status: SupplierStatus.ACTIVE })
|
||||
.orderBy('supplier.rating', 'DESC')
|
||||
.addOrderBy('supplier.total_orders', 'DESC');
|
||||
|
||||
if (options?.category) {
|
||||
query.andWhere(':category = ANY(supplier.categories)', {
|
||||
category: options.category,
|
||||
});
|
||||
}
|
||||
|
||||
if (options?.zipCode) {
|
||||
query.andWhere(
|
||||
'(supplier.coverage_zones = \'{}\' OR :zipCode = ANY(supplier.coverage_zones))',
|
||||
{ zipCode: options.zipCode },
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.search) {
|
||||
query.andWhere(
|
||||
'(supplier.name ILIKE :search OR supplier.description ILIKE :search)',
|
||||
{ search: `%${options.search}%` },
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.limit) {
|
||||
query.limit(options.limit);
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
async getSupplier(id: string): Promise<Supplier> {
|
||||
const supplier = await this.supplierRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['products', 'reviews'],
|
||||
});
|
||||
|
||||
if (!supplier) {
|
||||
throw new NotFoundException('Proveedor no encontrado');
|
||||
}
|
||||
|
||||
return supplier;
|
||||
}
|
||||
|
||||
async getSupplierProducts(
|
||||
supplierId: string,
|
||||
options?: {
|
||||
category?: string;
|
||||
search?: string;
|
||||
inStock?: boolean;
|
||||
},
|
||||
): Promise<SupplierProduct[]> {
|
||||
const query = this.productRepo.createQueryBuilder('product')
|
||||
.where('product.supplier_id = :supplierId', { supplierId })
|
||||
.andWhere('product.active = true')
|
||||
.orderBy('product.category', 'ASC')
|
||||
.addOrderBy('product.name', 'ASC');
|
||||
|
||||
if (options?.category) {
|
||||
query.andWhere('product.category = :category', { category: options.category });
|
||||
}
|
||||
|
||||
if (options?.search) {
|
||||
query.andWhere(
|
||||
'(product.name ILIKE :search OR product.description ILIKE :search)',
|
||||
{ search: `%${options.search}%` },
|
||||
);
|
||||
}
|
||||
|
||||
if (options?.inStock !== undefined) {
|
||||
query.andWhere('product.in_stock = :inStock', { inStock: options.inStock });
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
// ==================== ORDERS ====================
|
||||
|
||||
async createOrder(
|
||||
tenantId: string,
|
||||
dto: CreateSupplierOrderDto,
|
||||
): Promise<SupplierOrder> {
|
||||
const supplier = await this.supplierRepo.findOne({
|
||||
where: { id: dto.supplierId, status: SupplierStatus.ACTIVE },
|
||||
});
|
||||
|
||||
if (!supplier) {
|
||||
throw new NotFoundException('Proveedor no encontrado o no activo');
|
||||
}
|
||||
|
||||
// Get products and calculate totals
|
||||
const productIds = dto.items.map((item) => item.productId);
|
||||
const products = await this.productRepo.findByIds(productIds);
|
||||
|
||||
if (products.length !== productIds.length) {
|
||||
throw new BadRequestException('Algunos productos no fueron encontrados');
|
||||
}
|
||||
|
||||
// Create product map for easy lookup
|
||||
const productMap = new Map(products.map((p) => [p.id, p]));
|
||||
|
||||
// Validate min quantities and calculate subtotal
|
||||
let subtotal = 0;
|
||||
for (const item of dto.items) {
|
||||
const product = productMap.get(item.productId);
|
||||
if (!product) {
|
||||
throw new BadRequestException(`Producto ${item.productId} no encontrado`);
|
||||
}
|
||||
|
||||
if (item.quantity < product.minQuantity) {
|
||||
throw new BadRequestException(
|
||||
`Cantidad minima para ${product.name} es ${product.minQuantity}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!product.inStock) {
|
||||
throw new BadRequestException(`${product.name} no esta disponible`);
|
||||
}
|
||||
|
||||
// Calculate price based on tiered pricing
|
||||
let unitPrice = Number(product.unitPrice);
|
||||
if (product.tieredPricing && product.tieredPricing.length > 0) {
|
||||
for (const tier of product.tieredPricing.sort((a, b) => b.min - a.min)) {
|
||||
if (item.quantity >= tier.min) {
|
||||
unitPrice = tier.price;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subtotal += unitPrice * item.quantity;
|
||||
}
|
||||
|
||||
// Calculate delivery fee
|
||||
let deliveryFee = Number(supplier.deliveryFee);
|
||||
if (supplier.freeDeliveryMin && subtotal >= Number(supplier.freeDeliveryMin)) {
|
||||
deliveryFee = 0;
|
||||
}
|
||||
|
||||
// Check minimum order
|
||||
if (subtotal < Number(supplier.minOrderAmount)) {
|
||||
throw new BadRequestException(
|
||||
`Pedido minimo es $${supplier.minOrderAmount}`,
|
||||
);
|
||||
}
|
||||
|
||||
const total = subtotal + deliveryFee;
|
||||
|
||||
// Create order
|
||||
const order = this.orderRepo.create({
|
||||
tenantId,
|
||||
supplierId: dto.supplierId,
|
||||
status: SupplierOrderStatus.PENDING,
|
||||
subtotal,
|
||||
deliveryFee,
|
||||
total,
|
||||
deliveryAddress: dto.deliveryAddress,
|
||||
deliveryCity: dto.deliveryCity,
|
||||
deliveryZip: dto.deliveryZip,
|
||||
deliveryPhone: dto.deliveryPhone,
|
||||
deliveryContact: dto.deliveryContact,
|
||||
requestedDate: dto.requestedDate ? new Date(dto.requestedDate) : null,
|
||||
notes: dto.notes,
|
||||
});
|
||||
|
||||
await this.orderRepo.save(order);
|
||||
|
||||
// Create order items
|
||||
for (const item of dto.items) {
|
||||
const product = productMap.get(item.productId);
|
||||
|
||||
let unitPrice = Number(product.unitPrice);
|
||||
if (product.tieredPricing && product.tieredPricing.length > 0) {
|
||||
for (const tier of product.tieredPricing.sort((a, b) => b.min - a.min)) {
|
||||
if (item.quantity >= tier.min) {
|
||||
unitPrice = tier.price;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const orderItem = this.orderItemRepo.create({
|
||||
orderId: order.id,
|
||||
productId: item.productId,
|
||||
productName: product.name,
|
||||
productSku: product.sku,
|
||||
quantity: item.quantity,
|
||||
unitPrice,
|
||||
total: unitPrice * item.quantity,
|
||||
notes: item.notes,
|
||||
});
|
||||
|
||||
await this.orderItemRepo.save(orderItem);
|
||||
}
|
||||
|
||||
return this.getOrder(order.id);
|
||||
}
|
||||
|
||||
async getOrder(id: string): Promise<SupplierOrder> {
|
||||
const order = await this.orderRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['items', 'supplier'],
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
throw new NotFoundException('Pedido no encontrado');
|
||||
}
|
||||
|
||||
return order;
|
||||
}
|
||||
|
||||
async getOrders(
|
||||
tenantId: string,
|
||||
options?: {
|
||||
status?: SupplierOrderStatus;
|
||||
supplierId?: string;
|
||||
limit?: number;
|
||||
},
|
||||
): Promise<SupplierOrder[]> {
|
||||
const query = this.orderRepo.createQueryBuilder('order')
|
||||
.where('order.tenant_id = :tenantId', { tenantId })
|
||||
.leftJoinAndSelect('order.supplier', 'supplier')
|
||||
.leftJoinAndSelect('order.items', 'items')
|
||||
.orderBy('order.created_at', 'DESC');
|
||||
|
||||
if (options?.status) {
|
||||
query.andWhere('order.status = :status', { status: options.status });
|
||||
}
|
||||
|
||||
if (options?.supplierId) {
|
||||
query.andWhere('order.supplier_id = :supplierId', { supplierId: options.supplierId });
|
||||
}
|
||||
|
||||
if (options?.limit) {
|
||||
query.limit(options.limit);
|
||||
}
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
async updateOrderStatus(
|
||||
id: string,
|
||||
status: SupplierOrderStatus,
|
||||
notes?: string,
|
||||
): Promise<SupplierOrder> {
|
||||
const order = await this.getOrder(id);
|
||||
|
||||
// Validate status transitions
|
||||
const validTransitions: Record<SupplierOrderStatus, SupplierOrderStatus[]> = {
|
||||
[SupplierOrderStatus.PENDING]: [SupplierOrderStatus.CONFIRMED, SupplierOrderStatus.CANCELLED, SupplierOrderStatus.REJECTED],
|
||||
[SupplierOrderStatus.CONFIRMED]: [SupplierOrderStatus.PREPARING, SupplierOrderStatus.CANCELLED],
|
||||
[SupplierOrderStatus.PREPARING]: [SupplierOrderStatus.SHIPPED, SupplierOrderStatus.CANCELLED],
|
||||
[SupplierOrderStatus.SHIPPED]: [SupplierOrderStatus.DELIVERED, SupplierOrderStatus.CANCELLED],
|
||||
[SupplierOrderStatus.DELIVERED]: [],
|
||||
[SupplierOrderStatus.CANCELLED]: [],
|
||||
[SupplierOrderStatus.REJECTED]: [],
|
||||
};
|
||||
|
||||
if (!validTransitions[order.status].includes(status)) {
|
||||
throw new BadRequestException(
|
||||
`No se puede cambiar estado de ${order.status} a ${status}`,
|
||||
);
|
||||
}
|
||||
|
||||
order.status = status;
|
||||
|
||||
if (status === SupplierOrderStatus.CONFIRMED) {
|
||||
order.confirmedDate = new Date();
|
||||
}
|
||||
|
||||
if (status === SupplierOrderStatus.DELIVERED) {
|
||||
order.deliveredAt = new Date();
|
||||
}
|
||||
|
||||
if (status === SupplierOrderStatus.CANCELLED || status === SupplierOrderStatus.REJECTED) {
|
||||
order.cancelledAt = new Date();
|
||||
order.cancelReason = notes;
|
||||
}
|
||||
|
||||
if (notes) {
|
||||
order.supplierNotes = notes;
|
||||
}
|
||||
|
||||
return this.orderRepo.save(order);
|
||||
}
|
||||
|
||||
async cancelOrder(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
reason: string,
|
||||
): Promise<SupplierOrder> {
|
||||
const order = await this.getOrder(id);
|
||||
|
||||
if (order.tenantId !== tenantId) {
|
||||
throw new BadRequestException('No autorizado');
|
||||
}
|
||||
|
||||
if (![SupplierOrderStatus.PENDING, SupplierOrderStatus.CONFIRMED].includes(order.status)) {
|
||||
throw new BadRequestException('No se puede cancelar el pedido en este estado');
|
||||
}
|
||||
|
||||
order.status = SupplierOrderStatus.CANCELLED;
|
||||
order.cancelledAt = new Date();
|
||||
order.cancelReason = reason;
|
||||
order.cancelledBy = 'tenant';
|
||||
|
||||
return this.orderRepo.save(order);
|
||||
}
|
||||
|
||||
// ==================== REVIEWS ====================
|
||||
|
||||
async createReview(
|
||||
tenantId: string,
|
||||
dto: CreateSupplierReviewDto,
|
||||
): Promise<SupplierReview> {
|
||||
const supplier = await this.supplierRepo.findOne({
|
||||
where: { id: dto.supplierId },
|
||||
});
|
||||
|
||||
if (!supplier) {
|
||||
throw new NotFoundException('Proveedor no encontrado');
|
||||
}
|
||||
|
||||
// Check if order exists and belongs to tenant
|
||||
let verified = false;
|
||||
if (dto.orderId) {
|
||||
const order = await this.orderRepo.findOne({
|
||||
where: { id: dto.orderId, tenantId, supplierId: dto.supplierId },
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
throw new BadRequestException('Orden no encontrada');
|
||||
}
|
||||
|
||||
if (order.status === SupplierOrderStatus.DELIVERED) {
|
||||
verified = true;
|
||||
}
|
||||
}
|
||||
|
||||
const review = this.reviewRepo.create({
|
||||
tenantId,
|
||||
supplierId: dto.supplierId,
|
||||
orderId: dto.orderId,
|
||||
rating: dto.rating,
|
||||
title: dto.title,
|
||||
comment: dto.comment,
|
||||
ratingQuality: dto.ratingQuality,
|
||||
ratingDelivery: dto.ratingDelivery,
|
||||
ratingPrice: dto.ratingPrice,
|
||||
verified,
|
||||
});
|
||||
|
||||
return this.reviewRepo.save(review);
|
||||
}
|
||||
|
||||
async getReviews(
|
||||
supplierId: string,
|
||||
options?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
},
|
||||
): Promise<SupplierReview[]> {
|
||||
return this.reviewRepo.find({
|
||||
where: { supplierId, status: 'active' },
|
||||
order: { createdAt: 'DESC' },
|
||||
take: options?.limit || 20,
|
||||
skip: options?.offset || 0,
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== FAVORITES ====================
|
||||
|
||||
async addFavorite(tenantId: string, supplierId: string): Promise<void> {
|
||||
await this.dataSource.query(
|
||||
`INSERT INTO marketplace.supplier_favorites (tenant_id, supplier_id)
|
||||
VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
||||
[tenantId, supplierId],
|
||||
);
|
||||
}
|
||||
|
||||
async removeFavorite(tenantId: string, supplierId: string): Promise<void> {
|
||||
await this.dataSource.query(
|
||||
`DELETE FROM marketplace.supplier_favorites WHERE tenant_id = $1 AND supplier_id = $2`,
|
||||
[tenantId, supplierId],
|
||||
);
|
||||
}
|
||||
|
||||
async getFavorites(tenantId: string): Promise<Supplier[]> {
|
||||
const result = await this.dataSource.query(
|
||||
`SELECT s.* FROM marketplace.suppliers s
|
||||
JOIN marketplace.supplier_favorites f ON s.id = f.supplier_id
|
||||
WHERE f.tenant_id = $1`,
|
||||
[tenantId],
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ==================== STATS ====================
|
||||
|
||||
async getMarketplaceStats() {
|
||||
const result = await this.dataSource.query(
|
||||
`SELECT * FROM marketplace.get_marketplace_stats()`,
|
||||
);
|
||||
|
||||
return result[0] || {
|
||||
total_suppliers: 0,
|
||||
active_suppliers: 0,
|
||||
total_products: 0,
|
||||
total_orders: 0,
|
||||
total_gmv: 0,
|
||||
avg_rating: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
15
apps/backend/src/modules/referrals/dto/apply-code.dto.ts
Normal file
15
apps/backend/src/modules/referrals/dto/apply-code.dto.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, Length, Matches } from 'class-validator';
|
||||
|
||||
export class ApplyCodeDto {
|
||||
@ApiProperty({
|
||||
example: 'MCH-ABC123',
|
||||
description: 'Codigo de referido a aplicar',
|
||||
})
|
||||
@IsString()
|
||||
@Length(3, 20)
|
||||
@Matches(/^[A-Z0-9-]+$/, {
|
||||
message: 'El codigo solo puede contener letras mayusculas, numeros y guiones',
|
||||
})
|
||||
code: string;
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity({ schema: 'subscriptions', name: 'referral_codes' })
|
||||
export class ReferralCode {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id' })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ length: 20, unique: true })
|
||||
code: string;
|
||||
|
||||
@Column({ default: true })
|
||||
active: boolean;
|
||||
|
||||
@Column({ name: 'uses_count', default: 0 })
|
||||
usesCount: number;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { Referral } from './referral.entity';
|
||||
|
||||
export enum RewardType {
|
||||
FREE_MONTH = 'free_month',
|
||||
DISCOUNT = 'discount',
|
||||
}
|
||||
|
||||
export enum RewardStatus {
|
||||
AVAILABLE = 'available',
|
||||
USED = 'used',
|
||||
EXPIRED = 'expired',
|
||||
}
|
||||
|
||||
@Entity({ schema: 'subscriptions', name: 'referral_rewards' })
|
||||
export class ReferralReward {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'tenant_id' })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ name: 'referral_id' })
|
||||
referralId: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
default: RewardType.FREE_MONTH,
|
||||
})
|
||||
type: RewardType;
|
||||
|
||||
@Column({ name: 'months_earned', default: 0 })
|
||||
monthsEarned: number;
|
||||
|
||||
@Column({ name: 'months_used', default: 0 })
|
||||
monthsUsed: number;
|
||||
|
||||
@Column({ name: 'discount_percent', default: 0 })
|
||||
discountPercent: number;
|
||||
|
||||
@Column({ name: 'expires_at', type: 'timestamptz', nullable: true })
|
||||
expiresAt: Date;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
default: RewardStatus.AVAILABLE,
|
||||
})
|
||||
status: RewardStatus;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => Referral)
|
||||
@JoinColumn({ name: 'referral_id' })
|
||||
referral: Referral;
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
export enum ReferralStatus {
|
||||
PENDING = 'pending',
|
||||
CONVERTED = 'converted',
|
||||
REWARDED = 'rewarded',
|
||||
EXPIRED = 'expired',
|
||||
}
|
||||
|
||||
@Entity({ schema: 'subscriptions', name: 'referrals' })
|
||||
export class Referral {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'referrer_tenant_id' })
|
||||
referrerTenantId: string;
|
||||
|
||||
@Column({ name: 'referred_tenant_id', unique: true })
|
||||
referredTenantId: string;
|
||||
|
||||
@Column({ name: 'code_used', length: 20 })
|
||||
codeUsed: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 20,
|
||||
default: ReferralStatus.PENDING,
|
||||
})
|
||||
status: ReferralStatus;
|
||||
|
||||
@Column({ name: 'referred_discount_applied', default: false })
|
||||
referredDiscountApplied: boolean;
|
||||
|
||||
@Column({ name: 'referrer_reward_applied', default: false })
|
||||
referrerRewardApplied: boolean;
|
||||
|
||||
@Column({ name: 'converted_at', type: 'timestamptz', nullable: true })
|
||||
convertedAt: Date;
|
||||
|
||||
@Column({ name: 'reward_applied_at', type: 'timestamptz', nullable: true })
|
||||
rewardAppliedAt: Date;
|
||||
|
||||
@Column({ name: 'expires_at', type: 'timestamptz', nullable: true })
|
||||
expiresAt: Date;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
85
apps/backend/src/modules/referrals/referrals.controller.ts
Normal file
85
apps/backend/src/modules/referrals/referrals.controller.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Param,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { ReferralsService } from './referrals.service';
|
||||
import { ApplyCodeDto } from './dto/apply-code.dto';
|
||||
|
||||
@ApiTags('referrals')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('v1/referrals')
|
||||
export class ReferralsController {
|
||||
constructor(private readonly referralsService: ReferralsService) {}
|
||||
|
||||
// ==================== CODES ====================
|
||||
|
||||
@Get('my-code')
|
||||
@ApiOperation({ summary: 'Obtener mi codigo de referido' })
|
||||
getMyCode(@Request() req) {
|
||||
return this.referralsService.getMyCode(req.user.tenantId);
|
||||
}
|
||||
|
||||
@Post('generate-code')
|
||||
@ApiOperation({ summary: 'Generar nuevo codigo de referido' })
|
||||
generateCode(@Request() req) {
|
||||
return this.referralsService.generateCode(req.user.tenantId);
|
||||
}
|
||||
|
||||
@Get('validate/:code')
|
||||
@ApiOperation({ summary: 'Validar un codigo de referido' })
|
||||
@ApiParam({ name: 'code', description: 'Codigo a validar' })
|
||||
validateCode(@Param('code') code: string) {
|
||||
return this.referralsService.validateCode(code);
|
||||
}
|
||||
|
||||
// ==================== REFERRALS ====================
|
||||
|
||||
@Post('apply-code')
|
||||
@ApiOperation({ summary: 'Aplicar codigo de referido (al registrarse)' })
|
||||
applyCode(@Request() req, @Body() dto: ApplyCodeDto) {
|
||||
return this.referralsService.applyCode(req.user.tenantId, dto.code);
|
||||
}
|
||||
|
||||
@Get('list')
|
||||
@ApiOperation({ summary: 'Listar mis referidos' })
|
||||
getMyReferrals(@Request() req) {
|
||||
return this.referralsService.getMyReferrals(req.user.tenantId);
|
||||
}
|
||||
|
||||
@Get('stats')
|
||||
@ApiOperation({ summary: 'Estadisticas de referidos' })
|
||||
getStats(@Request() req) {
|
||||
return this.referralsService.getStats(req.user.tenantId);
|
||||
}
|
||||
|
||||
// ==================== REWARDS ====================
|
||||
|
||||
@Get('rewards')
|
||||
@ApiOperation({ summary: 'Mis recompensas de referidos' })
|
||||
getRewards(@Request() req) {
|
||||
return this.referralsService.getMyRewards(req.user.tenantId);
|
||||
}
|
||||
|
||||
@Get('rewards/available-months')
|
||||
@ApiOperation({ summary: 'Meses gratis disponibles' })
|
||||
getAvailableMonths(@Request() req) {
|
||||
return this.referralsService.getAvailableMonths(req.user.tenantId);
|
||||
}
|
||||
|
||||
// ==================== DISCOUNT ====================
|
||||
|
||||
@Get('discount')
|
||||
@ApiOperation({ summary: 'Descuento disponible como referido' })
|
||||
async getDiscount(@Request() req) {
|
||||
const discount = await this.referralsService.getReferredDiscount(req.user.tenantId);
|
||||
return { discountPercent: discount };
|
||||
}
|
||||
}
|
||||
15
apps/backend/src/modules/referrals/referrals.module.ts
Normal file
15
apps/backend/src/modules/referrals/referrals.module.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ReferralsController } from './referrals.controller';
|
||||
import { ReferralsService } from './referrals.service';
|
||||
import { ReferralCode } from './entities/referral-code.entity';
|
||||
import { Referral } from './entities/referral.entity';
|
||||
import { ReferralReward } from './entities/referral-reward.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ReferralCode, Referral, ReferralReward])],
|
||||
controllers: [ReferralsController],
|
||||
providers: [ReferralsService],
|
||||
exports: [ReferralsService],
|
||||
})
|
||||
export class ReferralsModule {}
|
||||
266
apps/backend/src/modules/referrals/referrals.service.ts
Normal file
266
apps/backend/src/modules/referrals/referrals.service.ts
Normal file
@ -0,0 +1,266 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { ReferralCode } from './entities/referral-code.entity';
|
||||
import { Referral, ReferralStatus } from './entities/referral.entity';
|
||||
import { ReferralReward, RewardType, RewardStatus } from './entities/referral-reward.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ReferralsService {
|
||||
constructor(
|
||||
@InjectRepository(ReferralCode)
|
||||
private readonly codeRepo: Repository<ReferralCode>,
|
||||
@InjectRepository(Referral)
|
||||
private readonly referralRepo: Repository<Referral>,
|
||||
@InjectRepository(ReferralReward)
|
||||
private readonly rewardRepo: Repository<ReferralReward>,
|
||||
private readonly dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
// ==================== CODES ====================
|
||||
|
||||
async getMyCode(tenantId: string): Promise<ReferralCode> {
|
||||
let code = await this.codeRepo.findOne({ where: { tenantId } });
|
||||
|
||||
if (!code) {
|
||||
code = await this.generateCode(tenantId);
|
||||
}
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
async generateCode(tenantId: string): Promise<ReferralCode> {
|
||||
// Check if already has a code
|
||||
const existing = await this.codeRepo.findOne({ where: { tenantId } });
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
// Generate unique code using database function
|
||||
const result = await this.dataSource.query(
|
||||
`SELECT generate_referral_code('MCH') as code`,
|
||||
);
|
||||
const newCode = result[0].code;
|
||||
|
||||
const referralCode = this.codeRepo.create({
|
||||
tenantId,
|
||||
code: newCode,
|
||||
active: true,
|
||||
});
|
||||
|
||||
return this.codeRepo.save(referralCode);
|
||||
}
|
||||
|
||||
async validateCode(code: string): Promise<ReferralCode> {
|
||||
const referralCode = await this.codeRepo.findOne({
|
||||
where: { code: code.toUpperCase(), active: true },
|
||||
});
|
||||
|
||||
if (!referralCode) {
|
||||
throw new NotFoundException('Codigo de referido no valido o inactivo');
|
||||
}
|
||||
|
||||
return referralCode;
|
||||
}
|
||||
|
||||
// ==================== REFERRALS ====================
|
||||
|
||||
async applyCode(referredTenantId: string, code: string): Promise<Referral> {
|
||||
// Validate code exists
|
||||
const referralCode = await this.validateCode(code);
|
||||
|
||||
// Cannot refer yourself
|
||||
if (referralCode.tenantId === referredTenantId) {
|
||||
throw new BadRequestException('No puedes usar tu propio codigo de referido');
|
||||
}
|
||||
|
||||
// Check if already referred
|
||||
const existingReferral = await this.referralRepo.findOne({
|
||||
where: { referredTenantId },
|
||||
});
|
||||
|
||||
if (existingReferral) {
|
||||
throw new ConflictException('Ya tienes un codigo de referido aplicado');
|
||||
}
|
||||
|
||||
// Create referral with 30 day expiry
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setDate(expiresAt.getDate() + 30);
|
||||
|
||||
const referral = this.referralRepo.create({
|
||||
referrerTenantId: referralCode.tenantId,
|
||||
referredTenantId,
|
||||
codeUsed: referralCode.code,
|
||||
status: ReferralStatus.PENDING,
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
await this.referralRepo.save(referral);
|
||||
|
||||
// Increment uses count
|
||||
referralCode.usesCount += 1;
|
||||
await this.codeRepo.save(referralCode);
|
||||
|
||||
return referral;
|
||||
}
|
||||
|
||||
async getReferralByReferred(referredTenantId: string): Promise<Referral | null> {
|
||||
return this.referralRepo.findOne({ where: { referredTenantId } });
|
||||
}
|
||||
|
||||
async getMyReferrals(tenantId: string): Promise<Referral[]> {
|
||||
return this.referralRepo.find({
|
||||
where: { referrerTenantId: tenantId },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async convertReferral(referredTenantId: string): Promise<Referral> {
|
||||
const referral = await this.referralRepo.findOne({
|
||||
where: { referredTenantId, status: ReferralStatus.PENDING },
|
||||
});
|
||||
|
||||
if (!referral) {
|
||||
throw new NotFoundException('No se encontro referido pendiente');
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
if (referral.expiresAt && new Date() > referral.expiresAt) {
|
||||
referral.status = ReferralStatus.EXPIRED;
|
||||
await this.referralRepo.save(referral);
|
||||
throw new BadRequestException('El periodo de conversion ha expirado');
|
||||
}
|
||||
|
||||
// Mark as converted
|
||||
referral.status = ReferralStatus.CONVERTED;
|
||||
referral.convertedAt = new Date();
|
||||
await this.referralRepo.save(referral);
|
||||
|
||||
// Create reward for referrer (1 month free)
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setFullYear(expiresAt.getFullYear() + 1); // 1 year to use
|
||||
|
||||
const reward = this.rewardRepo.create({
|
||||
tenantId: referral.referrerTenantId,
|
||||
referralId: referral.id,
|
||||
type: RewardType.FREE_MONTH,
|
||||
monthsEarned: 1,
|
||||
monthsUsed: 0,
|
||||
expiresAt,
|
||||
status: RewardStatus.AVAILABLE,
|
||||
});
|
||||
|
||||
await this.rewardRepo.save(reward);
|
||||
|
||||
// Update referral as rewarded
|
||||
referral.status = ReferralStatus.REWARDED;
|
||||
referral.referrerRewardApplied = true;
|
||||
referral.rewardAppliedAt = new Date();
|
||||
await this.referralRepo.save(referral);
|
||||
|
||||
return referral;
|
||||
}
|
||||
|
||||
// ==================== REWARDS ====================
|
||||
|
||||
async getMyRewards(tenantId: string): Promise<ReferralReward[]> {
|
||||
return this.rewardRepo.find({
|
||||
where: { tenantId },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async getAvailableMonths(tenantId: string): Promise<number> {
|
||||
const rewards = await this.rewardRepo.find({
|
||||
where: { tenantId, status: RewardStatus.AVAILABLE, type: RewardType.FREE_MONTH },
|
||||
});
|
||||
|
||||
return rewards.reduce((sum, r) => sum + (r.monthsEarned - r.monthsUsed), 0);
|
||||
}
|
||||
|
||||
async useReferralMonth(tenantId: string): Promise<boolean> {
|
||||
// Find first available reward
|
||||
const reward = await this.rewardRepo.findOne({
|
||||
where: { tenantId, status: RewardStatus.AVAILABLE, type: RewardType.FREE_MONTH },
|
||||
order: { createdAt: 'ASC' },
|
||||
});
|
||||
|
||||
if (!reward || reward.monthsEarned <= reward.monthsUsed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
reward.monthsUsed += 1;
|
||||
|
||||
if (reward.monthsUsed >= reward.monthsEarned) {
|
||||
reward.status = RewardStatus.USED;
|
||||
}
|
||||
|
||||
await this.rewardRepo.save(reward);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ==================== STATS ====================
|
||||
|
||||
async getStats(tenantId: string) {
|
||||
const result = await this.dataSource.query(
|
||||
`SELECT * FROM get_referral_stats($1)`,
|
||||
[tenantId],
|
||||
);
|
||||
|
||||
const stats = result[0] || {
|
||||
total_invited: 0,
|
||||
total_converted: 0,
|
||||
total_pending: 0,
|
||||
total_expired: 0,
|
||||
months_earned: 0,
|
||||
months_available: 0,
|
||||
};
|
||||
|
||||
const code = await this.getMyCode(tenantId);
|
||||
|
||||
return {
|
||||
code: code.code,
|
||||
totalInvited: stats.total_invited,
|
||||
totalConverted: stats.total_converted,
|
||||
totalPending: stats.total_pending,
|
||||
totalExpired: stats.total_expired,
|
||||
monthsEarned: stats.months_earned,
|
||||
monthsAvailable: stats.months_available,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== DISCOUNT FOR REFERRED ====================
|
||||
|
||||
async getReferredDiscount(tenantId: string): Promise<number> {
|
||||
const referral = await this.referralRepo.findOne({
|
||||
where: {
|
||||
referredTenantId: tenantId,
|
||||
referredDiscountApplied: false,
|
||||
status: ReferralStatus.PENDING,
|
||||
},
|
||||
});
|
||||
|
||||
if (!referral) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 50% discount for first month
|
||||
return 50;
|
||||
}
|
||||
|
||||
async markDiscountApplied(tenantId: string): Promise<void> {
|
||||
const referral = await this.referralRepo.findOne({
|
||||
where: { referredTenantId: tenantId },
|
||||
});
|
||||
|
||||
if (referral) {
|
||||
referral.referredDiscountApplied = true;
|
||||
await this.referralRepo.save(referral);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
apps/backend/src/modules/widgets/widgets.controller.ts
Normal file
51
apps/backend/src/modules/widgets/widgets.controller.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
UseGuards,
|
||||
Request,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import { WidgetsService } from './widgets.service';
|
||||
|
||||
@ApiTags('widgets')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('v1/widget')
|
||||
export class WidgetsController {
|
||||
constructor(private readonly widgetsService: WidgetsService) {}
|
||||
|
||||
@Get('summary')
|
||||
@ApiOperation({ summary: 'Obtener resumen para widget (optimizado)' })
|
||||
getSummary(@Request() req) {
|
||||
return this.widgetsService.getSummary(req.user.tenantId);
|
||||
}
|
||||
|
||||
@Get('alerts')
|
||||
@ApiOperation({ summary: 'Obtener alertas para widget' })
|
||||
getAlerts(@Request() req) {
|
||||
return this.widgetsService.getAlerts(req.user.tenantId);
|
||||
}
|
||||
|
||||
@Get('quick-actions')
|
||||
@ApiOperation({ summary: 'Obtener acciones rapidas para widget' })
|
||||
getQuickActions() {
|
||||
return this.widgetsService.getQuickActions();
|
||||
}
|
||||
|
||||
@Get('full')
|
||||
@ApiOperation({ summary: 'Obtener todos los datos del widget en una llamada' })
|
||||
async getFullWidgetData(@Request() req) {
|
||||
const [summary, alerts, quickActions] = await Promise.all([
|
||||
this.widgetsService.getSummary(req.user.tenantId),
|
||||
this.widgetsService.getAlerts(req.user.tenantId),
|
||||
this.widgetsService.getQuickActions(),
|
||||
]);
|
||||
|
||||
return {
|
||||
summary,
|
||||
alerts,
|
||||
quickActions,
|
||||
};
|
||||
}
|
||||
}
|
||||
10
apps/backend/src/modules/widgets/widgets.module.ts
Normal file
10
apps/backend/src/modules/widgets/widgets.module.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { WidgetsController } from './widgets.controller';
|
||||
import { WidgetsService } from './widgets.service';
|
||||
|
||||
@Module({
|
||||
controllers: [WidgetsController],
|
||||
providers: [WidgetsService],
|
||||
exports: [WidgetsService],
|
||||
})
|
||||
export class WidgetsModule {}
|
||||
192
apps/backend/src/modules/widgets/widgets.service.ts
Normal file
192
apps/backend/src/modules/widgets/widgets.service.ts
Normal file
@ -0,0 +1,192 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
export interface WidgetSummary {
|
||||
salesToday: number;
|
||||
transactionsCount: number;
|
||||
pendingOrders: number;
|
||||
lowStockCount: number;
|
||||
pendingCredits: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface WidgetAlert {
|
||||
type: 'low_stock' | 'pending_order' | 'overdue_credit' | 'new_message';
|
||||
title: string;
|
||||
message: string;
|
||||
count: number;
|
||||
priority: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class WidgetsService {
|
||||
constructor(private readonly dataSource: DataSource) {}
|
||||
|
||||
/**
|
||||
* Get lightweight summary data for widgets
|
||||
* Optimized for quick loading on mobile widgets
|
||||
*/
|
||||
async getSummary(tenantId: string): Promise<WidgetSummary> {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Sales today
|
||||
const salesResult = await this.dataSource.query(
|
||||
`SELECT
|
||||
COALESCE(SUM(total), 0) as total,
|
||||
COUNT(*) as count
|
||||
FROM sales.sales
|
||||
WHERE tenant_id = $1
|
||||
AND DATE(created_at) = $2
|
||||
AND status = 'completed'`,
|
||||
[tenantId, today],
|
||||
);
|
||||
|
||||
// Pending orders
|
||||
const ordersResult = await this.dataSource.query(
|
||||
`SELECT COUNT(*) as count
|
||||
FROM orders.orders
|
||||
WHERE tenant_id = $1
|
||||
AND status IN ('pending', 'confirmed', 'preparing')`,
|
||||
[tenantId],
|
||||
);
|
||||
|
||||
// Low stock products
|
||||
const stockResult = await this.dataSource.query(
|
||||
`SELECT COUNT(*) as count
|
||||
FROM catalog.products
|
||||
WHERE tenant_id = $1
|
||||
AND stock <= min_stock
|
||||
AND status = 'active'`,
|
||||
[tenantId],
|
||||
);
|
||||
|
||||
// Pending credits (fiado)
|
||||
const creditsResult = await this.dataSource.query(
|
||||
`SELECT COUNT(DISTINCT customer_id) as count
|
||||
FROM customers.customer_credits
|
||||
WHERE tenant_id = $1
|
||||
AND status = 'active'
|
||||
AND balance > 0`,
|
||||
[tenantId],
|
||||
);
|
||||
|
||||
return {
|
||||
salesToday: parseFloat(salesResult[0]?.total || '0'),
|
||||
transactionsCount: parseInt(salesResult[0]?.count || '0'),
|
||||
pendingOrders: parseInt(ordersResult[0]?.count || '0'),
|
||||
lowStockCount: parseInt(stockResult[0]?.count || '0'),
|
||||
pendingCredits: parseInt(creditsResult[0]?.count || '0'),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get alerts for widget display
|
||||
*/
|
||||
async getAlerts(tenantId: string): Promise<WidgetAlert[]> {
|
||||
const alerts: WidgetAlert[] = [];
|
||||
|
||||
// Low stock alerts
|
||||
const lowStock = await this.dataSource.query(
|
||||
`SELECT name, stock, min_stock
|
||||
FROM catalog.products
|
||||
WHERE tenant_id = $1
|
||||
AND stock <= min_stock
|
||||
AND status = 'active'
|
||||
ORDER BY stock ASC
|
||||
LIMIT 5`,
|
||||
[tenantId],
|
||||
);
|
||||
|
||||
if (lowStock.length > 0) {
|
||||
alerts.push({
|
||||
type: 'low_stock',
|
||||
title: 'Stock bajo',
|
||||
message: lowStock.length === 1
|
||||
? `${lowStock[0].name}: ${lowStock[0].stock} unidades`
|
||||
: `${lowStock.length} productos con stock bajo`,
|
||||
count: lowStock.length,
|
||||
priority: lowStock.some((p: any) => p.stock === 0) ? 'high' : 'medium',
|
||||
});
|
||||
}
|
||||
|
||||
// Pending orders
|
||||
const pendingOrders = await this.dataSource.query(
|
||||
`SELECT COUNT(*) as count
|
||||
FROM orders.orders
|
||||
WHERE tenant_id = $1
|
||||
AND status = 'pending'`,
|
||||
[tenantId],
|
||||
);
|
||||
|
||||
if (parseInt(pendingOrders[0]?.count || '0') > 0) {
|
||||
const count = parseInt(pendingOrders[0].count);
|
||||
alerts.push({
|
||||
type: 'pending_order',
|
||||
title: 'Pedidos pendientes',
|
||||
message: count === 1
|
||||
? '1 pedido por confirmar'
|
||||
: `${count} pedidos por confirmar`,
|
||||
count,
|
||||
priority: count > 3 ? 'high' : 'medium',
|
||||
});
|
||||
}
|
||||
|
||||
// Overdue credits
|
||||
const overdueCredits = await this.dataSource.query(
|
||||
`SELECT COUNT(DISTINCT cc.customer_id) as count
|
||||
FROM customers.customer_credits cc
|
||||
WHERE cc.tenant_id = $1
|
||||
AND cc.status = 'active'
|
||||
AND cc.due_date < CURRENT_DATE`,
|
||||
[tenantId],
|
||||
);
|
||||
|
||||
if (parseInt(overdueCredits[0]?.count || '0') > 0) {
|
||||
const count = parseInt(overdueCredits[0].count);
|
||||
alerts.push({
|
||||
type: 'overdue_credit',
|
||||
title: 'Fiados vencidos',
|
||||
message: count === 1
|
||||
? '1 cliente con fiado vencido'
|
||||
: `${count} clientes con fiado vencido`,
|
||||
count,
|
||||
priority: 'high',
|
||||
});
|
||||
}
|
||||
|
||||
return alerts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get quick actions configuration for the widget
|
||||
*/
|
||||
getQuickActions() {
|
||||
return [
|
||||
{
|
||||
id: 'new_sale',
|
||||
title: 'Nueva Venta',
|
||||
icon: 'cart',
|
||||
deepLink: 'michangarrito://pos/new',
|
||||
},
|
||||
{
|
||||
id: 'scan_product',
|
||||
title: 'Escanear Producto',
|
||||
icon: 'barcode',
|
||||
deepLink: 'michangarrito://scan',
|
||||
},
|
||||
{
|
||||
id: 'view_orders',
|
||||
title: 'Ver Pedidos',
|
||||
icon: 'clipboard',
|
||||
deepLink: 'michangarrito://orders',
|
||||
},
|
||||
{
|
||||
id: 'view_inventory',
|
||||
title: 'Ver Inventario',
|
||||
icon: 'box',
|
||||
deepLink: 'michangarrito://inventory',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
144
apps/frontend/e2e/auth.spec.ts
Normal file
144
apps/frontend/e2e/auth.spec.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* E2E Tests - Autenticacion
|
||||
* MiChangarrito Frontend
|
||||
*/
|
||||
|
||||
test.describe('Autenticacion', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
});
|
||||
|
||||
test('debe mostrar la pagina de login', async ({ page }) => {
|
||||
// Verificar titulo
|
||||
await expect(page.locator('h1')).toContainText('MiChangarrito');
|
||||
await expect(page.locator('h2')).toContainText('Iniciar sesion');
|
||||
|
||||
// Verificar campos del formulario
|
||||
await expect(page.locator('input[name="phone"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="pin"]')).toBeVisible();
|
||||
await expect(page.locator('button[type="submit"]')).toBeVisible();
|
||||
|
||||
// Verificar link a registro
|
||||
await expect(page.locator('a[href="/register"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('debe validar campos requeridos', async ({ page }) => {
|
||||
// Intentar enviar formulario vacio
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Los campos tienen required, el navegador debe validar
|
||||
const phoneInput = page.locator('input[name="phone"]');
|
||||
await expect(phoneInput).toHaveAttribute('required', '');
|
||||
});
|
||||
|
||||
test('debe aceptar solo numeros en telefono', async ({ page }) => {
|
||||
const phoneInput = page.locator('input[name="phone"]');
|
||||
|
||||
// Escribir texto con letras
|
||||
await phoneInput.fill('abc123def456');
|
||||
|
||||
// Debe filtrar solo numeros
|
||||
await expect(phoneInput).toHaveValue('123456');
|
||||
});
|
||||
|
||||
test('debe limitar telefono a 10 digitos', async ({ page }) => {
|
||||
const phoneInput = page.locator('input[name="phone"]');
|
||||
|
||||
await phoneInput.fill('12345678901234');
|
||||
|
||||
// maxLength=10
|
||||
await expect(phoneInput).toHaveAttribute('maxLength', '10');
|
||||
});
|
||||
|
||||
test('debe aceptar solo numeros en PIN', async ({ page }) => {
|
||||
const pinInput = page.locator('input[name="pin"]');
|
||||
|
||||
await pinInput.fill('abc1234');
|
||||
|
||||
// Debe filtrar solo numeros
|
||||
await expect(pinInput).toHaveValue('1234');
|
||||
});
|
||||
|
||||
test('debe mostrar error con credenciales invalidas', async ({ page }) => {
|
||||
// Llenar formulario con datos invalidos
|
||||
await page.locator('input[name="phone"]').fill('5500000000');
|
||||
await page.locator('input[name="pin"]').fill('0000');
|
||||
|
||||
// Enviar
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
// Esperar respuesta del servidor
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Debe mostrar mensaje de error (si el backend esta corriendo)
|
||||
// Si no hay backend, verificamos que el boton vuelve a estar habilitado
|
||||
const submitButton = page.locator('button[type="submit"]');
|
||||
await expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
test('debe mostrar estado de carga al enviar', async ({ page }) => {
|
||||
// Llenar formulario
|
||||
await page.locator('input[name="phone"]').fill('5512345678');
|
||||
await page.locator('input[name="pin"]').fill('1234');
|
||||
|
||||
// Enviar y verificar estado de carga
|
||||
const submitButton = page.locator('button[type="submit"]');
|
||||
await submitButton.click();
|
||||
|
||||
// Verificar que muestra "Ingresando..." mientras carga
|
||||
// Nota: Esto es rapido, puede no capturarse siempre
|
||||
await expect(submitButton).toContainText(/Ingresar|Ingresando/);
|
||||
});
|
||||
|
||||
test('debe navegar a registro', async ({ page }) => {
|
||||
await page.locator('a[href="/register"]').click();
|
||||
|
||||
await expect(page).toHaveURL('/register');
|
||||
});
|
||||
|
||||
test('debe redirigir a login si no esta autenticado', async ({ page }) => {
|
||||
// Intentar acceder a ruta protegida
|
||||
await page.goto('/dashboard');
|
||||
|
||||
// Debe redirigir a login
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
|
||||
test('debe redirigir a login desde rutas protegidas', async ({ page }) => {
|
||||
const protectedRoutes = [
|
||||
'/dashboard',
|
||||
'/products',
|
||||
'/orders',
|
||||
'/customers',
|
||||
'/fiado',
|
||||
'/inventory',
|
||||
'/settings',
|
||||
];
|
||||
|
||||
for (const route of protectedRoutes) {
|
||||
await page.goto(route);
|
||||
await expect(page).toHaveURL('/login');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Registro', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/register');
|
||||
});
|
||||
|
||||
test('debe mostrar la pagina de registro', async ({ page }) => {
|
||||
await expect(page.locator('h1')).toContainText('MiChangarrito');
|
||||
|
||||
// Verificar link a login
|
||||
await expect(page.locator('a[href="/login"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('debe navegar a login desde registro', async ({ page }) => {
|
||||
await page.locator('a[href="/login"]').click();
|
||||
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
});
|
||||
48
apps/frontend/e2e/fixtures/test-data.ts
Normal file
48
apps/frontend/e2e/fixtures/test-data.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Test Data Fixtures - MiChangarrito E2E Tests
|
||||
*/
|
||||
|
||||
export const TEST_USER = {
|
||||
phone: '5512345678',
|
||||
pin: '1234',
|
||||
name: 'Usuario Test',
|
||||
businessName: 'Tienda Test',
|
||||
};
|
||||
|
||||
export const TEST_PRODUCT = {
|
||||
name: 'Coca-Cola 600ml',
|
||||
sku: 'CC600',
|
||||
barcode: '7501055300000',
|
||||
price: 18.00,
|
||||
cost: 12.00,
|
||||
stock: 50,
|
||||
category: 'Bebidas',
|
||||
};
|
||||
|
||||
export const TEST_CUSTOMER = {
|
||||
name: 'Juan Perez',
|
||||
phone: '5598765432',
|
||||
email: 'juan@test.com',
|
||||
creditLimit: 500,
|
||||
};
|
||||
|
||||
export const TEST_ORDER = {
|
||||
items: [
|
||||
{ productId: '1', quantity: 2, price: 18.00 },
|
||||
{ productId: '2', quantity: 1, price: 25.00 },
|
||||
],
|
||||
total: 61.00,
|
||||
paymentMethod: 'cash',
|
||||
};
|
||||
|
||||
export const ROUTES = {
|
||||
login: '/login',
|
||||
register: '/register',
|
||||
dashboard: '/dashboard',
|
||||
products: '/products',
|
||||
orders: '/orders',
|
||||
customers: '/customers',
|
||||
fiado: '/fiado',
|
||||
inventory: '/inventory',
|
||||
settings: '/settings',
|
||||
};
|
||||
120
apps/frontend/e2e/navigation.spec.ts
Normal file
120
apps/frontend/e2e/navigation.spec.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* E2E Tests - Navegacion
|
||||
* MiChangarrito Frontend
|
||||
*
|
||||
* Nota: Estos tests requieren autenticacion
|
||||
* Se usan con un usuario de prueba pre-configurado
|
||||
*/
|
||||
|
||||
test.describe('Navegacion Publica', () => {
|
||||
test('debe cargar la aplicacion', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Al no estar autenticado, debe redirigir a login
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
|
||||
test('rutas invalidas redirigen a login', async ({ page }) => {
|
||||
await page.goto('/ruta-que-no-existe');
|
||||
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
|
||||
test('login page tiene titulo correcto', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
|
||||
await expect(page).toHaveTitle(/MiChangarrito/i);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('UI Responsiva', () => {
|
||||
test('login se adapta a mobile', async ({ page }) => {
|
||||
// Viewport mobile
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await page.goto('/login');
|
||||
|
||||
// El formulario debe ser visible y usable
|
||||
await expect(page.locator('form')).toBeVisible();
|
||||
await expect(page.locator('input[name="phone"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="pin"]')).toBeVisible();
|
||||
await expect(page.locator('button[type="submit"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('login se adapta a tablet', async ({ page }) => {
|
||||
// Viewport tablet
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
await page.goto('/login');
|
||||
|
||||
await expect(page.locator('form')).toBeVisible();
|
||||
});
|
||||
|
||||
test('login se adapta a desktop', async ({ page }) => {
|
||||
// Viewport desktop
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
await page.goto('/login');
|
||||
|
||||
await expect(page.locator('form')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Accesibilidad', () => {
|
||||
test('campos tienen labels', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
|
||||
// Verificar que los inputs tienen labels asociados
|
||||
const phoneLabel = page.locator('label[for="phone"]');
|
||||
const pinLabel = page.locator('label[for="pin"]');
|
||||
|
||||
await expect(phoneLabel).toBeVisible();
|
||||
await expect(pinLabel).toBeVisible();
|
||||
});
|
||||
|
||||
test('formulario es navegable con teclado', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
|
||||
// Tab al primer campo
|
||||
await page.keyboard.press('Tab');
|
||||
const phoneInput = page.locator('input[name="phone"]');
|
||||
await expect(phoneInput).toBeFocused();
|
||||
|
||||
// Tab al segundo campo
|
||||
await page.keyboard.press('Tab');
|
||||
const pinInput = page.locator('input[name="pin"]');
|
||||
await expect(pinInput).toBeFocused();
|
||||
|
||||
// Tab al boton
|
||||
await page.keyboard.press('Tab');
|
||||
const submitButton = page.locator('button[type="submit"]');
|
||||
await expect(submitButton).toBeFocused();
|
||||
});
|
||||
|
||||
test('formulario puede enviarse con Enter', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
|
||||
// Llenar campos
|
||||
await page.locator('input[name="phone"]').fill('5512345678');
|
||||
await page.locator('input[name="pin"]').fill('1234');
|
||||
|
||||
// Enviar con Enter
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// El formulario debe intentar enviarse (el boton estara en estado loading)
|
||||
const submitButton = page.locator('button[type="submit"]');
|
||||
// Puede estar disabled durante el envio o ya haber terminado
|
||||
await page.waitForTimeout(500);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Performance', () => {
|
||||
test('login carga en menos de 3 segundos', async ({ page }) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
await page.goto('/login');
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
|
||||
const loadTime = Date.now() - startTime;
|
||||
expect(loadTime).toBeLessThan(3000);
|
||||
});
|
||||
});
|
||||
149
apps/frontend/e2e/orders.spec.ts
Normal file
149
apps/frontend/e2e/orders.spec.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_ORDER, TEST_CUSTOMER, ROUTES } from './fixtures/test-data';
|
||||
|
||||
/**
|
||||
* E2E Tests - Pedidos y Fiado
|
||||
* MiChangarrito Frontend
|
||||
*
|
||||
* Nota: Estos tests requieren autenticacion previa
|
||||
* y un backend corriendo con datos de prueba
|
||||
*/
|
||||
|
||||
test.describe('Pedidos - Sin Autenticacion', () => {
|
||||
test('redirige a login si no esta autenticado', async ({ page }) => {
|
||||
await page.goto(ROUTES.orders);
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
|
||||
test('fiado redirige a login sin autenticacion', async ({ page }) => {
|
||||
await page.goto(ROUTES.fiado);
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
|
||||
test('clientes redirige a login sin autenticacion', async ({ page }) => {
|
||||
await page.goto(ROUTES.customers);
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Pedidos - Estructura de Datos', () => {
|
||||
test('datos de pedido de prueba tienen estructura correcta', () => {
|
||||
expect(TEST_ORDER.items).toBeInstanceOf(Array);
|
||||
expect(TEST_ORDER.items.length).toBeGreaterThan(0);
|
||||
expect(TEST_ORDER.total).toBeGreaterThan(0);
|
||||
expect(TEST_ORDER.paymentMethod).toBeDefined();
|
||||
|
||||
// Verificar estructura de items
|
||||
const item = TEST_ORDER.items[0];
|
||||
expect(item.productId).toBeDefined();
|
||||
expect(item.quantity).toBeGreaterThan(0);
|
||||
expect(item.price).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('datos de cliente de prueba tienen estructura correcta', () => {
|
||||
expect(TEST_CUSTOMER.name).toBeDefined();
|
||||
expect(TEST_CUSTOMER.phone).toBeDefined();
|
||||
expect(TEST_CUSTOMER.phone.length).toBe(10);
|
||||
expect(TEST_CUSTOMER.creditLimit).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('total del pedido es correcto', () => {
|
||||
// Verificar calculo: 2 * 18.00 + 1 * 25.00 = 61.00
|
||||
const calculatedTotal = TEST_ORDER.items.reduce(
|
||||
(sum, item) => sum + item.quantity * item.price,
|
||||
0
|
||||
);
|
||||
expect(calculatedTotal).toBe(TEST_ORDER.total);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Sistema de Fiado', () => {
|
||||
test('rutas de fiado protegidas', async ({ page }) => {
|
||||
const fiadoRoutes = [
|
||||
'/fiado',
|
||||
'/customers',
|
||||
];
|
||||
|
||||
for (const route of fiadoRoutes) {
|
||||
await page.goto(route);
|
||||
await expect(page).toHaveURL('/login');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Historial de Pedidos', () => {
|
||||
test('ruta de pedidos protegida', async ({ page }) => {
|
||||
await page.goto('/orders');
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
|
||||
test('ruta de dashboard protegida', async ({ page }) => {
|
||||
await page.goto(ROUTES.dashboard);
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Clientes', () => {
|
||||
test('ruta de clientes protegida', async ({ page }) => {
|
||||
await page.goto('/customers');
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
|
||||
test('validacion de limite de credito', () => {
|
||||
// El limite de credito debe ser un numero positivo o cero
|
||||
expect(TEST_CUSTOMER.creditLimit).toBeGreaterThanOrEqual(0);
|
||||
expect(typeof TEST_CUSTOMER.creditLimit).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Metodos de Pago', () => {
|
||||
test('metodo de pago valido', () => {
|
||||
const validPaymentMethods = ['cash', 'card', 'transfer', 'fiado'];
|
||||
expect(validPaymentMethods).toContain(TEST_ORDER.paymentMethod);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Configuracion', () => {
|
||||
test('ruta de configuracion protegida', async ({ page }) => {
|
||||
await page.goto(ROUTES.settings);
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Rutas Completas', () => {
|
||||
test('todas las rutas definidas en fixtures existen', () => {
|
||||
const expectedRoutes = [
|
||||
'login',
|
||||
'register',
|
||||
'dashboard',
|
||||
'products',
|
||||
'orders',
|
||||
'customers',
|
||||
'fiado',
|
||||
'inventory',
|
||||
'settings',
|
||||
];
|
||||
|
||||
for (const route of expectedRoutes) {
|
||||
expect(ROUTES[route as keyof typeof ROUTES]).toBeDefined();
|
||||
expect(ROUTES[route as keyof typeof ROUTES]).toMatch(/^\//);
|
||||
}
|
||||
});
|
||||
|
||||
test('todas las rutas protegidas redirigen a login', async ({ page }) => {
|
||||
const protectedRoutes = [
|
||||
ROUTES.dashboard,
|
||||
ROUTES.products,
|
||||
ROUTES.orders,
|
||||
ROUTES.customers,
|
||||
ROUTES.fiado,
|
||||
ROUTES.inventory,
|
||||
ROUTES.settings,
|
||||
];
|
||||
|
||||
for (const route of protectedRoutes) {
|
||||
await page.goto(route);
|
||||
await expect(page).toHaveURL('/login');
|
||||
}
|
||||
});
|
||||
});
|
||||
103
apps/frontend/e2e/pos.spec.ts
Normal file
103
apps/frontend/e2e/pos.spec.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { TEST_PRODUCT, ROUTES } from './fixtures/test-data';
|
||||
|
||||
/**
|
||||
* E2E Tests - Punto de Venta (POS)
|
||||
* MiChangarrito Frontend
|
||||
*
|
||||
* Nota: Estos tests requieren autenticacion previa
|
||||
* y un backend corriendo con datos de prueba
|
||||
*/
|
||||
|
||||
test.describe('Punto de Venta - Sin Autenticacion', () => {
|
||||
test('redirige a login si no esta autenticado', async ({ page }) => {
|
||||
await page.goto('/pos');
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Punto de Venta - UI Basica', () => {
|
||||
// Estos tests verifican la estructura esperada del POS
|
||||
// cuando el usuario esta autenticado
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Mock de autenticacion para tests
|
||||
// En un escenario real, se usaria un fixture de login
|
||||
await page.goto('/login');
|
||||
});
|
||||
|
||||
test('pagina de productos existe', async ({ page }) => {
|
||||
await page.goto(ROUTES.products);
|
||||
// Sin auth, redirige a login
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Punto de Venta - Flujo de Venta', () => {
|
||||
// Tests de flujo completo de venta
|
||||
// Requieren setup de autenticacion
|
||||
|
||||
test('estructura del formulario de busqueda', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
|
||||
// Verificar que la pagina de login tiene estructura correcta
|
||||
// antes de poder probar el POS
|
||||
await expect(page.locator('form')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Carrito de Compras', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
});
|
||||
|
||||
test('login tiene campos necesarios para acceder al POS', async ({ page }) => {
|
||||
// Verificar campos necesarios
|
||||
await expect(page.locator('input[name="phone"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="pin"]')).toBeVisible();
|
||||
await expect(page.locator('button[type="submit"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Busqueda de Productos', () => {
|
||||
test('rutas de productos protegidas', async ({ page }) => {
|
||||
// Verificar que todas las rutas relacionadas al POS estan protegidas
|
||||
const posRoutes = [
|
||||
'/products',
|
||||
'/pos',
|
||||
'/inventory',
|
||||
];
|
||||
|
||||
for (const route of posRoutes) {
|
||||
await page.goto(route);
|
||||
await expect(page).toHaveURL('/login');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Metodos de Pago', () => {
|
||||
test('verificar existencia de ruta de pedidos', async ({ page }) => {
|
||||
await page.goto(ROUTES.orders);
|
||||
// Sin auth, redirige
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Recibos y Tickets', () => {
|
||||
test('estructura de datos de producto de prueba', () => {
|
||||
// Verificar que los datos de prueba tienen la estructura correcta
|
||||
expect(TEST_PRODUCT.name).toBeDefined();
|
||||
expect(TEST_PRODUCT.price).toBeGreaterThan(0);
|
||||
expect(TEST_PRODUCT.cost).toBeGreaterThan(0);
|
||||
expect(TEST_PRODUCT.stock).toBeGreaterThanOrEqual(0);
|
||||
expect(TEST_PRODUCT.sku).toBeDefined();
|
||||
expect(TEST_PRODUCT.barcode).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Inventario desde POS', () => {
|
||||
test('ruta de inventario protegida', async ({ page }) => {
|
||||
await page.goto(ROUTES.inventory);
|
||||
await expect(page).toHaveURL('/login');
|
||||
});
|
||||
});
|
||||
@ -7,7 +7,11 @@
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:headed": "playwright test --headed",
|
||||
"test:e2e:report": "playwright show-report"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.90.16",
|
||||
@ -19,6 +23,7 @@
|
||||
"react-router-dom": "^7.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
|
||||
53
apps/frontend/playwright.config.ts
Normal file
53
apps/frontend/playwright.config.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Playwright E2E Test Configuration - MiChangarrito Frontend
|
||||
* @see https://playwright.dev/docs/test-configuration
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: [
|
||||
['html', { outputFolder: 'playwright-report' }],
|
||||
['list'],
|
||||
],
|
||||
use: {
|
||||
baseURL: 'http://localhost:3140',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
{
|
||||
name: 'Mobile Chrome',
|
||||
use: { ...devices['Pixel 5'] },
|
||||
},
|
||||
{
|
||||
name: 'Mobile Safari',
|
||||
use: { ...devices['iPhone 12'] },
|
||||
},
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: 'npm run dev',
|
||||
url: 'http://localhost:3140',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120 * 1000,
|
||||
},
|
||||
});
|
||||
@ -9,6 +9,9 @@ import { Customers } from './pages/Customers';
|
||||
import { Fiado } from './pages/Fiado';
|
||||
import { Inventory } from './pages/Inventory';
|
||||
import { Settings } from './pages/Settings';
|
||||
import { Referrals } from './pages/Referrals';
|
||||
import { Invoices } from './pages/Invoices';
|
||||
import { Marketplace } from './pages/Marketplace';
|
||||
import Login from './pages/Login';
|
||||
import Register from './pages/Register';
|
||||
|
||||
@ -73,6 +76,9 @@ function App() {
|
||||
<Route path="customers" element={<Customers />} />
|
||||
<Route path="fiado" element={<Fiado />} />
|
||||
<Route path="inventory" element={<Inventory />} />
|
||||
<Route path="referrals" element={<Referrals />} />
|
||||
<Route path="invoices" element={<Invoices />} />
|
||||
<Route path="marketplace" element={<Marketplace />} />
|
||||
<Route path="settings" element={<Settings />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@ -11,6 +11,9 @@ import {
|
||||
X,
|
||||
Store,
|
||||
LogOut,
|
||||
Gift,
|
||||
FileText,
|
||||
Truck,
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
@ -23,6 +26,9 @@ const navigation = [
|
||||
{ name: 'Clientes', href: '/customers', icon: Users },
|
||||
{ name: 'Fiado', href: '/fiado', icon: CreditCard },
|
||||
{ name: 'Inventario', href: '/inventory', icon: Boxes },
|
||||
{ name: 'Facturacion', href: '/invoices', icon: FileText },
|
||||
{ name: 'Proveedores', href: '/marketplace', icon: Truck },
|
||||
{ name: 'Referidos', href: '/referrals', icon: Gift },
|
||||
{ name: 'Ajustes', href: '/settings', icon: Settings },
|
||||
];
|
||||
|
||||
|
||||
112
apps/frontend/src/components/payments/ClabeDisplay.tsx
Normal file
112
apps/frontend/src/components/payments/ClabeDisplay.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Copy, Check, Building } from 'lucide-react';
|
||||
import { codiSpeiApi } from '../../lib/api';
|
||||
|
||||
interface ClabeDisplayProps {
|
||||
showCreateButton?: boolean;
|
||||
beneficiaryName?: string;
|
||||
}
|
||||
|
||||
export function ClabeDisplay({ showCreateButton = true, beneficiaryName }: ClabeDisplayProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['clabe'],
|
||||
queryFn: async () => {
|
||||
const res = await codiSpeiApi.getClabe();
|
||||
return res.data;
|
||||
},
|
||||
});
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (name: string) => codiSpeiApi.createClabe(name),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['clabe'] });
|
||||
},
|
||||
});
|
||||
|
||||
const copyClabe = async () => {
|
||||
if (data?.clabe) {
|
||||
await navigator.clipboard.writeText(data.clabe);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const formatClabe = (clabe: string) => {
|
||||
// Format: XXX XXX XXXX XXXX XXXX
|
||||
return clabe.replace(/(\d{3})(\d{3})(\d{4})(\d{4})(\d{4})/, '$1 $2 $3 $4 $5');
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data?.clabe && showCreateButton) {
|
||||
return (
|
||||
<div className="p-4 bg-gray-50 rounded-lg">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Building className="h-6 w-6 text-gray-400" />
|
||||
<div>
|
||||
<h4 className="font-medium">CLABE Virtual</h4>
|
||||
<p className="text-sm text-gray-500">Recibe transferencias SPEI</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => createMutation.mutate(beneficiaryName || 'Mi Negocio')}
|
||||
disabled={createMutation.isPending}
|
||||
className="w-full px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 disabled:opacity-50"
|
||||
>
|
||||
{createMutation.isPending ? 'Creando...' : 'Crear CLABE Virtual'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data?.clabe) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-blue-50 rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Building className="h-5 w-5 text-blue-600" />
|
||||
<span className="text-sm font-medium text-blue-800">Tu CLABE para recibir SPEI</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={copyClabe}
|
||||
className="p-2 rounded-lg hover:bg-blue-100 transition-colors"
|
||||
title="Copiar CLABE"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4 text-green-600" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4 text-blue-600" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg p-3">
|
||||
<p className="font-mono text-lg text-center font-bold tracking-wider">
|
||||
{formatClabe(data.clabe)}
|
||||
</p>
|
||||
{data.beneficiaryName && (
|
||||
<p className="text-sm text-gray-500 text-center mt-1">
|
||||
{data.beneficiaryName}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-blue-600 text-center mt-2">
|
||||
Las transferencias se reflejan automaticamente
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
167
apps/frontend/src/components/payments/CodiQR.tsx
Normal file
167
apps/frontend/src/components/payments/CodiQR.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { QrCode, Clock, Check, X, RefreshCw } from 'lucide-react';
|
||||
import { codiSpeiApi } from '../../lib/api';
|
||||
|
||||
interface CodiQRProps {
|
||||
amount: number;
|
||||
description?: string;
|
||||
saleId?: string;
|
||||
onSuccess?: () => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export function CodiQR({ amount, description, saleId, onSuccess, onCancel }: CodiQRProps) {
|
||||
const [transactionId, setTransactionId] = useState<string | null>(null);
|
||||
const [timeLeft, setTimeLeft] = useState<number>(300); // 5 minutes in seconds
|
||||
|
||||
const generateMutation = useMutation({
|
||||
mutationFn: () => codiSpeiApi.generateQr({ amount, description, saleId }),
|
||||
onSuccess: (res) => {
|
||||
setTransactionId(res.data.id);
|
||||
setTimeLeft(300);
|
||||
},
|
||||
});
|
||||
|
||||
const { data: status, refetch } = useQuery({
|
||||
queryKey: ['codi-status', transactionId],
|
||||
queryFn: async () => {
|
||||
if (!transactionId) return null;
|
||||
const res = await codiSpeiApi.getCodiStatus(transactionId);
|
||||
return res.data;
|
||||
},
|
||||
enabled: !!transactionId,
|
||||
refetchInterval: 3000, // Poll every 3 seconds
|
||||
});
|
||||
|
||||
// Generate QR on mount
|
||||
useEffect(() => {
|
||||
generateMutation.mutate();
|
||||
}, []);
|
||||
|
||||
// Countdown timer
|
||||
useEffect(() => {
|
||||
if (timeLeft <= 0) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setTimeLeft((prev) => prev - 1);
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [timeLeft]);
|
||||
|
||||
// Handle status changes
|
||||
useEffect(() => {
|
||||
if (status?.status === 'confirmed') {
|
||||
onSuccess?.();
|
||||
} else if (status?.status === 'expired' || status?.status === 'cancelled') {
|
||||
// QR expired or cancelled
|
||||
}
|
||||
}, [status?.status]);
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
generateMutation.mutate();
|
||||
};
|
||||
|
||||
if (generateMutation.isPending) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-8">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mb-4"></div>
|
||||
<p className="text-gray-500">Generando QR de cobro...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status?.status === 'confirmed') {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-8">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4">
|
||||
<Check className="h-8 w-8 text-green-600" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-green-600 mb-2">Pago Confirmado</h3>
|
||||
<p className="text-gray-500">El pago de ${amount.toFixed(2)} fue recibido</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status?.status === 'expired' || timeLeft <= 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center p-8">
|
||||
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mb-4">
|
||||
<X className="h-8 w-8 text-red-600" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-red-600 mb-2">QR Expirado</h3>
|
||||
<p className="text-gray-500 mb-4">El codigo QR ha expirado</p>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Generar nuevo QR
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center p-6">
|
||||
<div className="text-center mb-4">
|
||||
<h3 className="text-lg font-bold mb-1">Pagar con CoDi</h3>
|
||||
<p className="text-2xl font-bold text-primary-600">${amount.toFixed(2)}</p>
|
||||
{description && <p className="text-sm text-gray-500">{description}</p>}
|
||||
</div>
|
||||
|
||||
{/* QR Code Display */}
|
||||
<div className="bg-white p-4 rounded-xl shadow-lg mb-4">
|
||||
<div className="w-48 h-48 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||
{/* In production, generate actual QR code from qrData */}
|
||||
<div className="text-center">
|
||||
<QrCode className="h-32 w-32 text-gray-800 mx-auto" />
|
||||
<p className="text-xs text-gray-400 mt-2">Escanea con tu app bancaria</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timer */}
|
||||
<div className="flex items-center gap-2 text-gray-600 mb-4">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>Expira en: {formatTime(timeLeft)}</span>
|
||||
</div>
|
||||
|
||||
{/* Instructions */}
|
||||
<div className="text-center text-sm text-gray-500 mb-4">
|
||||
<p>1. Abre la app de tu banco</p>
|
||||
<p>2. Escanea el codigo QR</p>
|
||||
<p>3. Confirma el pago</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 w-full">
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Regenerar
|
||||
</button>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="flex-1 px-4 py-2 text-red-600 border border-red-300 rounded-lg hover:bg-red-50"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Reference */}
|
||||
{status?.reference && (
|
||||
<p className="text-xs text-gray-400 mt-4">Ref: {status.reference}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -129,3 +129,88 @@ export const dashboardApi = {
|
||||
getSalesChart: (period: string) => api.get('/dashboard/sales', { params: { period } }),
|
||||
getTopProducts: () => api.get('/dashboard/top-products'),
|
||||
};
|
||||
|
||||
// Referrals API
|
||||
export const referralsApi = {
|
||||
getMyCode: () => api.get('/referrals/my-code'),
|
||||
generateCode: () => api.post('/referrals/generate-code'),
|
||||
validateCode: (code: string) => api.get(`/referrals/validate/${code}`),
|
||||
applyCode: (code: string) => api.post('/referrals/apply-code', { code }),
|
||||
getMyReferrals: () => api.get('/referrals/list'),
|
||||
getStats: () => api.get('/referrals/stats'),
|
||||
getRewards: () => api.get('/referrals/rewards'),
|
||||
getAvailableMonths: () => api.get('/referrals/rewards/available-months'),
|
||||
getDiscount: () => api.get('/referrals/discount'),
|
||||
};
|
||||
|
||||
// CoDi/SPEI API
|
||||
export const codiSpeiApi = {
|
||||
// CoDi
|
||||
generateQr: (data: { amount: number; description?: string; saleId?: string }) =>
|
||||
api.post('/codi/generate-qr', data),
|
||||
getCodiStatus: (id: string) => api.get(`/codi/status/${id}`),
|
||||
getCodiTransactions: (limit?: number) =>
|
||||
api.get('/codi/transactions', { params: { limit } }),
|
||||
|
||||
// SPEI
|
||||
getClabe: () => api.get('/spei/clabe'),
|
||||
createClabe: (beneficiaryName: string) =>
|
||||
api.post('/spei/create-clabe', { beneficiaryName }),
|
||||
getSpeiTransactions: (limit?: number) =>
|
||||
api.get('/spei/transactions', { params: { limit } }),
|
||||
|
||||
// Summary
|
||||
getSummary: (date?: string) =>
|
||||
api.get('/payments/summary', { params: { date } }),
|
||||
};
|
||||
|
||||
// Invoices API (SAT/CFDI)
|
||||
export const invoicesApi = {
|
||||
// Tax Config
|
||||
getTaxConfig: () => api.get('/invoices/tax-config'),
|
||||
saveTaxConfig: (data: any) => api.post('/invoices/tax-config', data),
|
||||
|
||||
// Invoices
|
||||
getAll: (params?: { status?: string; from?: string; to?: string; limit?: number }) =>
|
||||
api.get('/invoices', { params }),
|
||||
getById: (id: string) => api.get(`/invoices/${id}`),
|
||||
create: (data: any) => api.post('/invoices', data),
|
||||
stamp: (id: string) => api.post(`/invoices/${id}/stamp`),
|
||||
cancel: (id: string, reason: string, uuidReplacement?: string) =>
|
||||
api.post(`/invoices/${id}/cancel`, { reason, uuidReplacement }),
|
||||
send: (id: string, email?: string) =>
|
||||
api.post(`/invoices/${id}/send`, { email }),
|
||||
getSummary: (month?: string) =>
|
||||
api.get('/invoices/summary', { params: { month } }),
|
||||
};
|
||||
|
||||
// Marketplace API
|
||||
export const marketplaceApi = {
|
||||
// Suppliers
|
||||
getSuppliers: (params?: { category?: string; zipCode?: string; search?: string; limit?: number }) =>
|
||||
api.get('/marketplace/suppliers', { params }),
|
||||
getSupplier: (id: string) => api.get(`/marketplace/suppliers/${id}`),
|
||||
getSupplierProducts: (id: string, params?: { category?: string; search?: string }) =>
|
||||
api.get(`/marketplace/suppliers/${id}/products`, { params }),
|
||||
getSupplierReviews: (id: string, params?: { limit?: number }) =>
|
||||
api.get(`/marketplace/suppliers/${id}/reviews`, { params }),
|
||||
|
||||
// Orders
|
||||
createOrder: (data: any) => api.post('/marketplace/orders', data),
|
||||
getOrders: (params?: { status?: string; supplierId?: string; limit?: number }) =>
|
||||
api.get('/marketplace/orders', { params }),
|
||||
getOrder: (id: string) => api.get(`/marketplace/orders/${id}`),
|
||||
cancelOrder: (id: string, reason: string) =>
|
||||
api.put(`/marketplace/orders/${id}/cancel`, { reason }),
|
||||
|
||||
// Reviews
|
||||
createReview: (data: any) => api.post('/marketplace/reviews', data),
|
||||
|
||||
// Favorites
|
||||
getFavorites: () => api.get('/marketplace/favorites'),
|
||||
addFavorite: (supplierId: string) => api.post(`/marketplace/favorites/${supplierId}`),
|
||||
removeFavorite: (supplierId: string) => api.delete(`/marketplace/favorites/${supplierId}`),
|
||||
|
||||
// Stats
|
||||
getStats: () => api.get('/marketplace/stats'),
|
||||
};
|
||||
|
||||
72
apps/frontend/src/lib/i18n.ts
Normal file
72
apps/frontend/src/lib/i18n.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
// Import translations
|
||||
import esMX from '../locales/es-MX';
|
||||
import esCO from '../locales/es-CO';
|
||||
import esAR from '../locales/es-AR';
|
||||
import ptBR from '../locales/pt-BR';
|
||||
|
||||
const resources = {
|
||||
'es-MX': { translation: esMX },
|
||||
'es-CO': { translation: esCO },
|
||||
'es-AR': { translation: esAR },
|
||||
'pt-BR': { translation: ptBR },
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
fallbackLng: 'es-MX',
|
||||
defaultNS: 'translation',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator'],
|
||||
caches: ['localStorage'],
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
// Currency formatting by locale
|
||||
export const currencyConfig: Record<string, { currency: string; symbol: string }> = {
|
||||
'es-MX': { currency: 'MXN', symbol: '$' },
|
||||
'es-CO': { currency: 'COP', symbol: '$' },
|
||||
'es-AR': { currency: 'ARS', symbol: '$' },
|
||||
'pt-BR': { currency: 'BRL', symbol: 'R$' },
|
||||
};
|
||||
|
||||
export function formatCurrency(amount: number, locale: string = 'es-MX'): string {
|
||||
const config = currencyConfig[locale] || currencyConfig['es-MX'];
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: config.currency,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
export function formatDate(date: Date | string, locale: string = 'es-MX'): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
}).format(d);
|
||||
}
|
||||
|
||||
export function formatDateTime(date: Date | string, locale: string = 'es-MX'): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(d);
|
||||
}
|
||||
63
apps/frontend/src/locales/es-AR/index.ts
Normal file
63
apps/frontend/src/locales/es-AR/index.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import esMX from '../es-MX';
|
||||
|
||||
// Argentina Spanish - Override specific terms
|
||||
const esAR = {
|
||||
...esMX,
|
||||
|
||||
// Navigation overrides
|
||||
nav: {
|
||||
...esMX.nav,
|
||||
// Same in Argentina
|
||||
},
|
||||
|
||||
// Auth overrides
|
||||
auth: {
|
||||
...esMX.auth,
|
||||
phone: 'Celular',
|
||||
enterPhone: 'Ingresa tu celular',
|
||||
},
|
||||
|
||||
// Dashboard overrides
|
||||
dashboard: {
|
||||
...esMX.dashboard,
|
||||
// Same in Argentina
|
||||
},
|
||||
|
||||
// Products overrides - Argentine terminology
|
||||
products: {
|
||||
...esMX.products,
|
||||
subtitle: 'Administra tu stock',
|
||||
stock: 'Stock',
|
||||
},
|
||||
|
||||
// Fiado overrides - Argentine terminology
|
||||
fiado: {
|
||||
...esMX.fiado,
|
||||
title: 'Cuenta',
|
||||
subtitle: 'Control de cuentas corrientes',
|
||||
registerPayment: 'Registrar pago',
|
||||
},
|
||||
|
||||
// Payments overrides
|
||||
payments: {
|
||||
...esMX.payments,
|
||||
fiado: 'Cuenta corriente',
|
||||
transfer: 'Transferencia',
|
||||
change: 'Vuelto',
|
||||
},
|
||||
|
||||
// Business Types - Argentine terminology
|
||||
businessTypes: {
|
||||
tienda: 'Almacen',
|
||||
papeleria: 'Libreria',
|
||||
farmacia: 'Farmacia',
|
||||
ferreteria: 'Ferreteria',
|
||||
carniceria: 'Carniceria',
|
||||
verduleria: 'Verduleria',
|
||||
panaderia: 'Panaderia',
|
||||
tortilleria: 'Rotiseria',
|
||||
otro: 'Otro',
|
||||
},
|
||||
};
|
||||
|
||||
export default esAR;
|
||||
60
apps/frontend/src/locales/es-CO/index.ts
Normal file
60
apps/frontend/src/locales/es-CO/index.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import esMX from '../es-MX';
|
||||
|
||||
// Colombia Spanish - Override specific terms
|
||||
const esCO = {
|
||||
...esMX,
|
||||
|
||||
// Navigation overrides
|
||||
nav: {
|
||||
...esMX.nav,
|
||||
// Same in Colombia
|
||||
},
|
||||
|
||||
// Auth overrides
|
||||
auth: {
|
||||
...esMX.auth,
|
||||
phone: 'Celular',
|
||||
enterPhone: 'Ingresa tu celular',
|
||||
},
|
||||
|
||||
// Dashboard overrides
|
||||
dashboard: {
|
||||
...esMX.dashboard,
|
||||
// Same in Colombia
|
||||
},
|
||||
|
||||
// Products overrides - Colombian terminology
|
||||
products: {
|
||||
...esMX.products,
|
||||
subtitle: 'Administra tu inventario',
|
||||
},
|
||||
|
||||
// Fiado overrides
|
||||
fiado: {
|
||||
...esMX.fiado,
|
||||
title: 'Credito',
|
||||
subtitle: 'Control de creditos a clientes',
|
||||
},
|
||||
|
||||
// Payments overrides
|
||||
payments: {
|
||||
...esMX.payments,
|
||||
fiado: 'Credito',
|
||||
transfer: 'Transferencia bancaria',
|
||||
},
|
||||
|
||||
// Business Types - Colombian terminology
|
||||
businessTypes: {
|
||||
tienda: 'Tienda',
|
||||
papeleria: 'Papeleria',
|
||||
farmacia: 'Drogueria',
|
||||
ferreteria: 'Ferreteria',
|
||||
carniceria: 'Carniceria',
|
||||
verduleria: 'Fruteria',
|
||||
panaderia: 'Panaderia',
|
||||
tortilleria: 'Areperia',
|
||||
otro: 'Otro',
|
||||
},
|
||||
};
|
||||
|
||||
export default esCO;
|
||||
231
apps/frontend/src/locales/es-MX/index.ts
Normal file
231
apps/frontend/src/locales/es-MX/index.ts
Normal file
@ -0,0 +1,231 @@
|
||||
export default {
|
||||
// Common
|
||||
common: {
|
||||
save: 'Guardar',
|
||||
cancel: 'Cancelar',
|
||||
delete: 'Eliminar',
|
||||
edit: 'Editar',
|
||||
add: 'Agregar',
|
||||
search: 'Buscar',
|
||||
loading: 'Cargando...',
|
||||
error: 'Error',
|
||||
success: 'Exito',
|
||||
confirm: 'Confirmar',
|
||||
back: 'Volver',
|
||||
next: 'Siguiente',
|
||||
close: 'Cerrar',
|
||||
yes: 'Si',
|
||||
no: 'No',
|
||||
all: 'Todos',
|
||||
none: 'Ninguno',
|
||||
required: 'Requerido',
|
||||
optional: 'Opcional',
|
||||
},
|
||||
|
||||
// Navigation
|
||||
nav: {
|
||||
dashboard: 'Dashboard',
|
||||
products: 'Productos',
|
||||
orders: 'Pedidos',
|
||||
customers: 'Clientes',
|
||||
fiado: 'Fiado',
|
||||
inventory: 'Inventario',
|
||||
referrals: 'Referidos',
|
||||
settings: 'Ajustes',
|
||||
logout: 'Cerrar sesion',
|
||||
},
|
||||
|
||||
// Auth
|
||||
auth: {
|
||||
login: 'Iniciar sesion',
|
||||
register: 'Registrarse',
|
||||
phone: 'Telefono',
|
||||
pin: 'PIN',
|
||||
enterPhone: 'Ingresa tu telefono',
|
||||
enterPin: 'Ingresa tu PIN',
|
||||
forgotPin: 'Olvide mi PIN',
|
||||
noAccount: 'No tienes cuenta?',
|
||||
hasAccount: 'Ya tienes cuenta?',
|
||||
createAccount: 'Crear cuenta',
|
||||
businessName: 'Nombre del negocio',
|
||||
ownerName: 'Tu nombre',
|
||||
businessType: 'Tipo de negocio',
|
||||
},
|
||||
|
||||
// Dashboard
|
||||
dashboard: {
|
||||
title: 'Dashboard',
|
||||
todaySales: 'Ventas de hoy',
|
||||
weekSales: 'Ventas de la semana',
|
||||
monthSales: 'Ventas del mes',
|
||||
transactions: 'transacciones',
|
||||
lowStock: 'Stock bajo',
|
||||
pendingOrders: 'Pedidos pendientes',
|
||||
pendingCredits: 'Fiados pendientes',
|
||||
topProducts: 'Productos mas vendidos',
|
||||
},
|
||||
|
||||
// Products
|
||||
products: {
|
||||
title: 'Productos',
|
||||
subtitle: 'Administra tu catalogo',
|
||||
addProduct: 'Agregar producto',
|
||||
editProduct: 'Editar producto',
|
||||
name: 'Nombre',
|
||||
price: 'Precio',
|
||||
cost: 'Costo',
|
||||
stock: 'Stock',
|
||||
sku: 'SKU',
|
||||
barcode: 'Codigo de barras',
|
||||
category: 'Categoria',
|
||||
description: 'Descripcion',
|
||||
noProducts: 'No hay productos',
|
||||
scanBarcode: 'Escanear codigo',
|
||||
},
|
||||
|
||||
// Orders
|
||||
orders: {
|
||||
title: 'Pedidos',
|
||||
subtitle: 'Gestiona los pedidos',
|
||||
newOrder: 'Nuevo pedido',
|
||||
orderNumber: 'Pedido #{{number}}',
|
||||
status: 'Estado',
|
||||
pending: 'Pendiente',
|
||||
preparing: 'Preparando',
|
||||
ready: 'Listo',
|
||||
delivered: 'Entregado',
|
||||
cancelled: 'Cancelado',
|
||||
total: 'Total',
|
||||
items: 'articulos',
|
||||
noOrders: 'No hay pedidos',
|
||||
},
|
||||
|
||||
// Customers
|
||||
customers: {
|
||||
title: 'Clientes',
|
||||
subtitle: 'Administra tus clientes',
|
||||
addCustomer: 'Agregar cliente',
|
||||
name: 'Nombre',
|
||||
phone: 'Telefono',
|
||||
email: 'Correo',
|
||||
creditLimit: 'Limite de credito',
|
||||
currentBalance: 'Saldo actual',
|
||||
noCustomers: 'No hay clientes',
|
||||
},
|
||||
|
||||
// Fiado (Credit)
|
||||
fiado: {
|
||||
title: 'Fiado',
|
||||
subtitle: 'Control de creditos',
|
||||
totalOwed: 'Total adeudado',
|
||||
overdueAmount: 'Vencido',
|
||||
customersWithCredit: 'Clientes con fiado',
|
||||
registerPayment: 'Registrar abono',
|
||||
paymentHistory: 'Historial de pagos',
|
||||
dueDate: 'Fecha de vencimiento',
|
||||
overdue: 'Vencido',
|
||||
noCredits: 'No hay fiados pendientes',
|
||||
},
|
||||
|
||||
// Inventory
|
||||
inventory: {
|
||||
title: 'Inventario',
|
||||
subtitle: 'Control de existencias',
|
||||
movements: 'Movimientos',
|
||||
addMovement: 'Agregar movimiento',
|
||||
entry: 'Entrada',
|
||||
exit: 'Salida',
|
||||
adjustment: 'Ajuste',
|
||||
lowStockAlerts: 'Alertas de stock bajo',
|
||||
reorder: 'Reabastecer',
|
||||
noMovements: 'No hay movimientos',
|
||||
},
|
||||
|
||||
// Referrals
|
||||
referrals: {
|
||||
title: 'Programa de Referidos',
|
||||
subtitle: 'Invita amigos y gana',
|
||||
yourCode: 'Tu codigo de referido',
|
||||
copy: 'Copiar',
|
||||
share: 'Compartir',
|
||||
shareWhatsApp: 'Compartir por WhatsApp',
|
||||
invited: 'Invitados',
|
||||
converted: 'Convertidos',
|
||||
monthsEarned: 'Meses ganados',
|
||||
monthsAvailable: 'Disponibles',
|
||||
howItWorks: 'Como funciona',
|
||||
step1: 'Comparte tu codigo',
|
||||
step1Desc: 'Envia tu codigo a amigos por WhatsApp',
|
||||
step2: 'Tu amigo se registra',
|
||||
step2Desc: 'Obtiene 50% de descuento en su primer mes',
|
||||
step3: 'Tu ganas 1 mes gratis',
|
||||
step3Desc: 'Cuando tu amigo paga su primer mes',
|
||||
yourReferrals: 'Tus referidos',
|
||||
noReferrals: 'Aun no tienes referidos',
|
||||
},
|
||||
|
||||
// Settings
|
||||
settings: {
|
||||
title: 'Ajustes',
|
||||
subtitle: 'Configura tu tienda',
|
||||
businessInfo: 'Informacion del negocio',
|
||||
fiadoSettings: 'Configuracion de fiado',
|
||||
whatsapp: 'WhatsApp Business',
|
||||
notifications: 'Notificaciones',
|
||||
subscription: 'Suscripcion',
|
||||
language: 'Idioma',
|
||||
enableFiado: 'Habilitar fiado',
|
||||
defaultCreditLimit: 'Limite de credito por defecto',
|
||||
gracePeriod: 'Dias de gracia',
|
||||
connected: 'Conectado',
|
||||
autoResponses: 'Respuestas automaticas',
|
||||
lowStockAlerts: 'Alertas de stock bajo',
|
||||
overdueAlerts: 'Alertas de fiados vencidos',
|
||||
newOrderAlerts: 'Alertas de nuevos pedidos',
|
||||
currentPlan: 'Plan actual',
|
||||
upgradePlan: 'Mejorar plan',
|
||||
},
|
||||
|
||||
// Payments
|
||||
payments: {
|
||||
cash: 'Efectivo',
|
||||
card: 'Tarjeta',
|
||||
transfer: 'Transferencia',
|
||||
fiado: 'Fiado',
|
||||
codi: 'CoDi',
|
||||
spei: 'SPEI',
|
||||
change: 'Cambio',
|
||||
total: 'Total',
|
||||
subtotal: 'Subtotal',
|
||||
tax: 'IVA',
|
||||
discount: 'Descuento',
|
||||
payNow: 'Pagar ahora',
|
||||
generateQR: 'Generar QR',
|
||||
scanQR: 'Escanea el QR con tu app bancaria',
|
||||
paymentReceived: 'Pago recibido',
|
||||
paymentFailed: 'Pago fallido',
|
||||
},
|
||||
|
||||
// Errors
|
||||
errors: {
|
||||
generic: 'Algo salio mal',
|
||||
networkError: 'Error de conexion',
|
||||
unauthorized: 'No autorizado',
|
||||
notFound: 'No encontrado',
|
||||
validationError: 'Error de validacion',
|
||||
serverError: 'Error del servidor',
|
||||
},
|
||||
|
||||
// Business Types
|
||||
businessTypes: {
|
||||
tienda: 'Tienda de abarrotes',
|
||||
papeleria: 'Papeleria',
|
||||
farmacia: 'Farmacia',
|
||||
ferreteria: 'Ferreteria',
|
||||
carniceria: 'Carniceria',
|
||||
verduleria: 'Verduleria',
|
||||
panaderia: 'Panaderia',
|
||||
tortilleria: 'Tortilleria',
|
||||
otro: 'Otro',
|
||||
},
|
||||
};
|
||||
234
apps/frontend/src/locales/pt-BR/index.ts
Normal file
234
apps/frontend/src/locales/pt-BR/index.ts
Normal file
@ -0,0 +1,234 @@
|
||||
// Brazilian Portuguese
|
||||
const ptBR = {
|
||||
// Common
|
||||
common: {
|
||||
save: 'Salvar',
|
||||
cancel: 'Cancelar',
|
||||
delete: 'Excluir',
|
||||
edit: 'Editar',
|
||||
add: 'Adicionar',
|
||||
search: 'Buscar',
|
||||
loading: 'Carregando...',
|
||||
error: 'Erro',
|
||||
success: 'Sucesso',
|
||||
confirm: 'Confirmar',
|
||||
back: 'Voltar',
|
||||
next: 'Proximo',
|
||||
close: 'Fechar',
|
||||
yes: 'Sim',
|
||||
no: 'Nao',
|
||||
all: 'Todos',
|
||||
none: 'Nenhum',
|
||||
required: 'Obrigatorio',
|
||||
optional: 'Opcional',
|
||||
},
|
||||
|
||||
// Navigation
|
||||
nav: {
|
||||
dashboard: 'Painel',
|
||||
products: 'Produtos',
|
||||
orders: 'Pedidos',
|
||||
customers: 'Clientes',
|
||||
fiado: 'Fiado',
|
||||
inventory: 'Estoque',
|
||||
referrals: 'Indicacoes',
|
||||
settings: 'Configuracoes',
|
||||
logout: 'Sair',
|
||||
},
|
||||
|
||||
// Auth
|
||||
auth: {
|
||||
login: 'Entrar',
|
||||
register: 'Cadastrar',
|
||||
phone: 'Celular',
|
||||
pin: 'PIN',
|
||||
enterPhone: 'Digite seu celular',
|
||||
enterPin: 'Digite seu PIN',
|
||||
forgotPin: 'Esqueci meu PIN',
|
||||
noAccount: 'Nao tem conta?',
|
||||
hasAccount: 'Ja tem conta?',
|
||||
createAccount: 'Criar conta',
|
||||
businessName: 'Nome do negocio',
|
||||
ownerName: 'Seu nome',
|
||||
businessType: 'Tipo de negocio',
|
||||
},
|
||||
|
||||
// Dashboard
|
||||
dashboard: {
|
||||
title: 'Painel',
|
||||
todaySales: 'Vendas de hoje',
|
||||
weekSales: 'Vendas da semana',
|
||||
monthSales: 'Vendas do mes',
|
||||
transactions: 'transacoes',
|
||||
lowStock: 'Estoque baixo',
|
||||
pendingOrders: 'Pedidos pendentes',
|
||||
pendingCredits: 'Fiados pendentes',
|
||||
topProducts: 'Produtos mais vendidos',
|
||||
},
|
||||
|
||||
// Products
|
||||
products: {
|
||||
title: 'Produtos',
|
||||
subtitle: 'Gerencie seu catalogo',
|
||||
addProduct: 'Adicionar produto',
|
||||
editProduct: 'Editar produto',
|
||||
name: 'Nome',
|
||||
price: 'Preco',
|
||||
cost: 'Custo',
|
||||
stock: 'Estoque',
|
||||
sku: 'SKU',
|
||||
barcode: 'Codigo de barras',
|
||||
category: 'Categoria',
|
||||
description: 'Descricao',
|
||||
noProducts: 'Nenhum produto',
|
||||
scanBarcode: 'Escanear codigo',
|
||||
},
|
||||
|
||||
// Orders
|
||||
orders: {
|
||||
title: 'Pedidos',
|
||||
subtitle: 'Gerencie os pedidos',
|
||||
newOrder: 'Novo pedido',
|
||||
orderNumber: 'Pedido #{{number}}',
|
||||
status: 'Status',
|
||||
pending: 'Pendente',
|
||||
preparing: 'Preparando',
|
||||
ready: 'Pronto',
|
||||
delivered: 'Entregue',
|
||||
cancelled: 'Cancelado',
|
||||
total: 'Total',
|
||||
items: 'itens',
|
||||
noOrders: 'Nenhum pedido',
|
||||
},
|
||||
|
||||
// Customers
|
||||
customers: {
|
||||
title: 'Clientes',
|
||||
subtitle: 'Gerencie seus clientes',
|
||||
addCustomer: 'Adicionar cliente',
|
||||
name: 'Nome',
|
||||
phone: 'Celular',
|
||||
email: 'Email',
|
||||
creditLimit: 'Limite de credito',
|
||||
currentBalance: 'Saldo atual',
|
||||
noCustomers: 'Nenhum cliente',
|
||||
},
|
||||
|
||||
// Fiado (Credit)
|
||||
fiado: {
|
||||
title: 'Fiado',
|
||||
subtitle: 'Controle de creditos',
|
||||
totalOwed: 'Total devido',
|
||||
overdueAmount: 'Vencido',
|
||||
customersWithCredit: 'Clientes com fiado',
|
||||
registerPayment: 'Registrar pagamento',
|
||||
paymentHistory: 'Historico de pagamentos',
|
||||
dueDate: 'Data de vencimento',
|
||||
overdue: 'Vencido',
|
||||
noCredits: 'Nenhum fiado pendente',
|
||||
},
|
||||
|
||||
// Inventory
|
||||
inventory: {
|
||||
title: 'Estoque',
|
||||
subtitle: 'Controle de estoque',
|
||||
movements: 'Movimentacoes',
|
||||
addMovement: 'Adicionar movimentacao',
|
||||
entry: 'Entrada',
|
||||
exit: 'Saida',
|
||||
adjustment: 'Ajuste',
|
||||
lowStockAlerts: 'Alertas de estoque baixo',
|
||||
reorder: 'Repor',
|
||||
noMovements: 'Nenhuma movimentacao',
|
||||
},
|
||||
|
||||
// Referrals
|
||||
referrals: {
|
||||
title: 'Programa de Indicacoes',
|
||||
subtitle: 'Indique amigos e ganhe',
|
||||
yourCode: 'Seu codigo de indicacao',
|
||||
copy: 'Copiar',
|
||||
share: 'Compartilhar',
|
||||
shareWhatsApp: 'Compartilhar por WhatsApp',
|
||||
invited: 'Indicados',
|
||||
converted: 'Convertidos',
|
||||
monthsEarned: 'Meses ganhos',
|
||||
monthsAvailable: 'Disponiveis',
|
||||
howItWorks: 'Como funciona',
|
||||
step1: 'Compartilhe seu codigo',
|
||||
step1Desc: 'Envie seu codigo para amigos por WhatsApp',
|
||||
step2: 'Seu amigo se cadastra',
|
||||
step2Desc: 'Ganha 50% de desconto no primeiro mes',
|
||||
step3: 'Voce ganha 1 mes gratis',
|
||||
step3Desc: 'Quando seu amigo paga o primeiro mes',
|
||||
yourReferrals: 'Suas indicacoes',
|
||||
noReferrals: 'Voce ainda nao tem indicacoes',
|
||||
},
|
||||
|
||||
// Settings
|
||||
settings: {
|
||||
title: 'Configuracoes',
|
||||
subtitle: 'Configure sua loja',
|
||||
businessInfo: 'Informacoes do negocio',
|
||||
fiadoSettings: 'Configuracao de fiado',
|
||||
whatsapp: 'WhatsApp Business',
|
||||
notifications: 'Notificacoes',
|
||||
subscription: 'Assinatura',
|
||||
language: 'Idioma',
|
||||
enableFiado: 'Habilitar fiado',
|
||||
defaultCreditLimit: 'Limite de credito padrao',
|
||||
gracePeriod: 'Dias de carencia',
|
||||
connected: 'Conectado',
|
||||
autoResponses: 'Respostas automaticas',
|
||||
lowStockAlerts: 'Alertas de estoque baixo',
|
||||
overdueAlerts: 'Alertas de fiados vencidos',
|
||||
newOrderAlerts: 'Alertas de novos pedidos',
|
||||
currentPlan: 'Plano atual',
|
||||
upgradePlan: 'Melhorar plano',
|
||||
},
|
||||
|
||||
// Payments
|
||||
payments: {
|
||||
cash: 'Dinheiro',
|
||||
card: 'Cartao',
|
||||
transfer: 'Transferencia',
|
||||
fiado: 'Fiado',
|
||||
codi: 'Pix',
|
||||
spei: 'TED',
|
||||
change: 'Troco',
|
||||
total: 'Total',
|
||||
subtotal: 'Subtotal',
|
||||
tax: 'Impostos',
|
||||
discount: 'Desconto',
|
||||
payNow: 'Pagar agora',
|
||||
generateQR: 'Gerar QR',
|
||||
scanQR: 'Escaneie o QR com seu app do banco',
|
||||
paymentReceived: 'Pagamento recebido',
|
||||
paymentFailed: 'Pagamento falhou',
|
||||
},
|
||||
|
||||
// Errors
|
||||
errors: {
|
||||
generic: 'Algo deu errado',
|
||||
networkError: 'Erro de conexao',
|
||||
unauthorized: 'Nao autorizado',
|
||||
notFound: 'Nao encontrado',
|
||||
validationError: 'Erro de validacao',
|
||||
serverError: 'Erro do servidor',
|
||||
},
|
||||
|
||||
// Business Types
|
||||
businessTypes: {
|
||||
tienda: 'Loja',
|
||||
papeleria: 'Papelaria',
|
||||
farmacia: 'Farmacia',
|
||||
ferreteria: 'Ferragem',
|
||||
carniceria: 'Acougue',
|
||||
verduleria: 'Hortifruti',
|
||||
panaderia: 'Padaria',
|
||||
tortilleria: 'Tapiocaria',
|
||||
otro: 'Outro',
|
||||
},
|
||||
};
|
||||
|
||||
export default ptBR;
|
||||
668
apps/frontend/src/pages/Invoices.tsx
Normal file
668
apps/frontend/src/pages/Invoices.tsx
Normal file
@ -0,0 +1,668 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
FileText,
|
||||
Send,
|
||||
XCircle,
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Download,
|
||||
Search,
|
||||
Plus,
|
||||
Settings,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import { invoicesApi } from '../lib/api';
|
||||
|
||||
const statusConfig = {
|
||||
draft: { label: 'Borrador', color: 'gray', icon: Clock },
|
||||
pending: { label: 'Pendiente', color: 'yellow', icon: Clock },
|
||||
stamped: { label: 'Timbrada', color: 'green', icon: CheckCircle },
|
||||
sent: { label: 'Enviada', color: 'blue', icon: Send },
|
||||
cancelled: { label: 'Cancelada', color: 'red', icon: XCircle },
|
||||
};
|
||||
|
||||
export function Invoices() {
|
||||
const [filter, setFilter] = useState('all');
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
const [showNewInvoice, setShowNewInvoice] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: invoices = [], isLoading } = useQuery({
|
||||
queryKey: ['invoices', filter],
|
||||
queryFn: async () => {
|
||||
const params = filter !== 'all' ? { status: filter } : {};
|
||||
const response = await invoicesApi.getAll(params);
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
|
||||
const { data: summary } = useQuery({
|
||||
queryKey: ['invoices-summary'],
|
||||
queryFn: async () => {
|
||||
const response = await invoicesApi.getSummary();
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
|
||||
const { data: taxConfig } = useQuery({
|
||||
queryKey: ['tax-config'],
|
||||
queryFn: async () => {
|
||||
const response = await invoicesApi.getTaxConfig();
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
|
||||
const stampMutation = useMutation({
|
||||
mutationFn: (id: string) => invoicesApi.stamp(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['invoices'] });
|
||||
},
|
||||
});
|
||||
|
||||
const sendMutation = useMutation({
|
||||
mutationFn: (id: string) => invoicesApi.send(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['invoices'] });
|
||||
},
|
||||
});
|
||||
|
||||
const hasActiveConfig = taxConfig?.status === 'active';
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Facturacion</h1>
|
||||
<p className="text-gray-500">Emite facturas electronicas (CFDI)</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setShowConfig(true)}
|
||||
className="btn-secondary flex items-center gap-2"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
Configuracion
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowNewInvoice(true)}
|
||||
disabled={!hasActiveConfig}
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Nueva Factura
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alert if no config */}
|
||||
{!hasActiveConfig && (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 flex items-start gap-3">
|
||||
<AlertCircle className="h-5 w-5 text-yellow-600 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-medium text-yellow-800">Configuracion fiscal requerida</p>
|
||||
<p className="text-sm text-yellow-700">
|
||||
Para emitir facturas, primero configura tus datos fiscales y certificados.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="card">
|
||||
<p className="text-sm text-gray-500">Facturas del mes</p>
|
||||
<p className="text-2xl font-bold">{summary?.total_invoices || 0}</p>
|
||||
</div>
|
||||
<div className="card">
|
||||
<p className="text-sm text-gray-500">Monto facturado</p>
|
||||
<p className="text-2xl font-bold text-green-600">
|
||||
${(summary?.total_amount || 0).toLocaleString('es-MX', { minimumFractionDigits: 2 })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="card">
|
||||
<p className="text-sm text-gray-500">Canceladas</p>
|
||||
<p className="text-2xl font-bold text-red-600">{summary?.total_cancelled || 0}</p>
|
||||
</div>
|
||||
<div className="card">
|
||||
<p className="text-sm text-gray-500">RFC Emisor</p>
|
||||
<p className="text-lg font-mono">{taxConfig?.rfc || 'No configurado'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Filter */}
|
||||
<div className="flex gap-2 overflow-x-auto pb-2">
|
||||
<button
|
||||
onClick={() => setFilter('all')}
|
||||
className={clsx(
|
||||
'px-4 py-2 rounded-lg font-medium whitespace-nowrap transition-colors',
|
||||
filter === 'all'
|
||||
? 'bg-primary-100 text-primary-700'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
)}
|
||||
>
|
||||
Todas
|
||||
</button>
|
||||
{Object.entries(statusConfig).map(([status, config]) => (
|
||||
<button
|
||||
key={status}
|
||||
onClick={() => setFilter(status)}
|
||||
className={clsx(
|
||||
'px-4 py-2 rounded-lg font-medium whitespace-nowrap transition-colors',
|
||||
filter === status
|
||||
? `bg-${config.color}-100 text-${config.color}-700`
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
)}
|
||||
>
|
||||
{config.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Invoices List */}
|
||||
{isLoading ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto"></div>
|
||||
<p className="text-gray-500 mt-2">Cargando facturas...</p>
|
||||
</div>
|
||||
) : invoices.length === 0 ? (
|
||||
<div className="text-center py-12 card">
|
||||
<FileText className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900">No hay facturas</h3>
|
||||
<p className="text-gray-500">Las facturas emitidas apareceran aqui</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{invoices.map((invoice: any) => {
|
||||
const status = statusConfig[invoice.status as keyof typeof statusConfig] || statusConfig.draft;
|
||||
const StatusIcon = status.icon;
|
||||
|
||||
return (
|
||||
<div key={invoice.id} className="card">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-3 rounded-full bg-${status.color}-100`}>
|
||||
<StatusIcon className={`h-6 w-6 text-${status.color}-600`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="font-bold text-lg">
|
||||
{invoice.serie}-{invoice.folio}
|
||||
</h3>
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full bg-${status.color}-100 text-${status.color}-700`}>
|
||||
{status.label}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-600">{invoice.receptorNombre}</p>
|
||||
<p className="text-sm text-gray-500 font-mono">{invoice.receptorRfc}</p>
|
||||
{invoice.uuid && (
|
||||
<p className="text-xs text-gray-400 font-mono mt-1">
|
||||
UUID: {invoice.uuid}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<p className="text-2xl font-bold">
|
||||
${Number(invoice.total).toLocaleString('es-MX', { minimumFractionDigits: 2 })}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{new Date(invoice.createdAt).toLocaleDateString('es-MX')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{invoice.status === 'draft' && (
|
||||
<button
|
||||
onClick={() => stampMutation.mutate(invoice.id)}
|
||||
disabled={stampMutation.isPending}
|
||||
className="btn-primary"
|
||||
>
|
||||
Timbrar
|
||||
</button>
|
||||
)}
|
||||
{invoice.status === 'stamped' && (
|
||||
<button
|
||||
onClick={() => sendMutation.mutate(invoice.id)}
|
||||
disabled={sendMutation.isPending}
|
||||
className="btn-secondary flex items-center gap-2"
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
Enviar
|
||||
</button>
|
||||
)}
|
||||
{(invoice.status === 'stamped' || invoice.status === 'sent') && (
|
||||
<button className="btn-secondary flex items-center gap-2">
|
||||
<Download className="h-4 w-4" />
|
||||
PDF
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tax Config Modal */}
|
||||
{showConfig && (
|
||||
<TaxConfigModal
|
||||
config={taxConfig}
|
||||
onClose={() => setShowConfig(false)}
|
||||
onSave={() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tax-config'] });
|
||||
setShowConfig(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* New Invoice Modal */}
|
||||
{showNewInvoice && (
|
||||
<NewInvoiceModal
|
||||
onClose={() => setShowNewInvoice(false)}
|
||||
onSuccess={() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['invoices'] });
|
||||
setShowNewInvoice(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaxConfigModal({
|
||||
config,
|
||||
onClose,
|
||||
onSave,
|
||||
}: {
|
||||
config: any;
|
||||
onClose: () => void;
|
||||
onSave: () => void;
|
||||
}) {
|
||||
const [formData, setFormData] = useState({
|
||||
rfc: config?.rfc || '',
|
||||
razonSocial: config?.razonSocial || '',
|
||||
regimenFiscal: config?.regimenFiscal || '601',
|
||||
codigoPostal: config?.codigoPostal || '',
|
||||
serie: config?.serie || 'A',
|
||||
pacProvider: config?.pacProvider || 'facturapi',
|
||||
pacSandbox: config?.pacSandbox ?? true,
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: any) => invoicesApi.saveTaxConfig(data),
|
||||
onSuccess: onSave,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-6 border-b">
|
||||
<h2 className="text-xl font-bold">Configuracion Fiscal</h2>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
mutation.mutate(formData);
|
||||
}}
|
||||
className="p-6 space-y-4"
|
||||
>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">RFC</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.rfc}
|
||||
onChange={(e) => setFormData({ ...formData, rfc: e.target.value.toUpperCase() })}
|
||||
className="input"
|
||||
maxLength={13}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Razon Social</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.razonSocial}
|
||||
onChange={(e) => setFormData({ ...formData, razonSocial: e.target.value })}
|
||||
className="input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Regimen Fiscal</label>
|
||||
<select
|
||||
value={formData.regimenFiscal}
|
||||
onChange={(e) => setFormData({ ...formData, regimenFiscal: e.target.value })}
|
||||
className="input"
|
||||
>
|
||||
<option value="601">General de Ley PM</option>
|
||||
<option value="603">Personas Morales sin Fines de Lucro</option>
|
||||
<option value="605">Sueldos y Salarios</option>
|
||||
<option value="606">Arrendamiento</option>
|
||||
<option value="612">Personas Fisicas con Actividades Empresariales</option>
|
||||
<option value="621">Incorporacion Fiscal</option>
|
||||
<option value="625">Regimen de Actividades Agricolas</option>
|
||||
<option value="626">Regimen Simplificado de Confianza</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Codigo Postal</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.codigoPostal}
|
||||
onChange={(e) => setFormData({ ...formData, codigoPostal: e.target.value })}
|
||||
className="input"
|
||||
maxLength={5}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Serie</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.serie}
|
||||
onChange={(e) => setFormData({ ...formData, serie: e.target.value.toUpperCase() })}
|
||||
className="input"
|
||||
maxLength={10}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Proveedor PAC</label>
|
||||
<select
|
||||
value={formData.pacProvider}
|
||||
onChange={(e) => setFormData({ ...formData, pacProvider: e.target.value })}
|
||||
className="input"
|
||||
>
|
||||
<option value="facturapi">Facturapi</option>
|
||||
<option value="swsapien">SW Sapien</option>
|
||||
<option value="finkok">Finkok</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="sandbox"
|
||||
checked={formData.pacSandbox}
|
||||
onChange={(e) => setFormData({ ...formData, pacSandbox: e.target.checked })}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
<label htmlFor="sandbox" className="text-sm text-gray-700">
|
||||
Modo sandbox (pruebas)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button type="button" onClick={onClose} className="btn-secondary flex-1">
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" disabled={mutation.isPending} className="btn-primary flex-1">
|
||||
{mutation.isPending ? 'Guardando...' : 'Guardar'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NewInvoiceModal({
|
||||
onClose,
|
||||
onSuccess,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
}) {
|
||||
const [formData, setFormData] = useState({
|
||||
receptorRfc: '',
|
||||
receptorNombre: '',
|
||||
receptorRegimenFiscal: '601',
|
||||
receptorCodigoPostal: '',
|
||||
receptorUsoCfdi: 'G03',
|
||||
receptorEmail: '',
|
||||
formaPago: '01',
|
||||
metodoPago: 'PUE',
|
||||
items: [{ descripcion: '', cantidad: 1, valorUnitario: 0, claveProdServ: '01010101', claveUnidad: 'H87' }],
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (data: any) => invoicesApi.create(data),
|
||||
onSuccess,
|
||||
});
|
||||
|
||||
const addItem = () => {
|
||||
setFormData({
|
||||
...formData,
|
||||
items: [
|
||||
...formData.items,
|
||||
{ descripcion: '', cantidad: 1, valorUnitario: 0, claveProdServ: '01010101', claveUnidad: 'H87' },
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const updateItem = (index: number, field: string, value: any) => {
|
||||
const newItems = [...formData.items];
|
||||
newItems[index] = { ...newItems[index], [field]: value };
|
||||
setFormData({ ...formData, items: newItems });
|
||||
};
|
||||
|
||||
const removeItem = (index: number) => {
|
||||
if (formData.items.length > 1) {
|
||||
setFormData({
|
||||
...formData,
|
||||
items: formData.items.filter((_, i) => i !== index),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const total = formData.items.reduce(
|
||||
(sum, item) => sum + item.cantidad * item.valorUnitario * 1.16,
|
||||
0
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-6 border-b">
|
||||
<h2 className="text-xl font-bold">Nueva Factura</h2>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
mutation.mutate(formData);
|
||||
}}
|
||||
className="p-6 space-y-6"
|
||||
>
|
||||
{/* Receptor */}
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900 mb-3">Datos del Receptor</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">RFC</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.receptorRfc}
|
||||
onChange={(e) => setFormData({ ...formData, receptorRfc: e.target.value.toUpperCase() })}
|
||||
className="input"
|
||||
maxLength={13}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Nombre/Razon Social</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.receptorNombre}
|
||||
onChange={(e) => setFormData({ ...formData, receptorNombre: e.target.value })}
|
||||
className="input"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Codigo Postal</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.receptorCodigoPostal}
|
||||
onChange={(e) => setFormData({ ...formData, receptorCodigoPostal: e.target.value })}
|
||||
className="input"
|
||||
maxLength={5}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Uso CFDI</label>
|
||||
<select
|
||||
value={formData.receptorUsoCfdi}
|
||||
onChange={(e) => setFormData({ ...formData, receptorUsoCfdi: e.target.value })}
|
||||
className="input"
|
||||
>
|
||||
<option value="G01">Adquisicion de mercancias</option>
|
||||
<option value="G03">Gastos en general</option>
|
||||
<option value="P01">Por definir</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Email (opcional)</label>
|
||||
<input
|
||||
type="email"
|
||||
value={formData.receptorEmail}
|
||||
onChange={(e) => setFormData({ ...formData, receptorEmail: e.target.value })}
|
||||
className="input"
|
||||
placeholder="cliente@email.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pago */}
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-900 mb-3">Metodo de Pago</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Forma de Pago</label>
|
||||
<select
|
||||
value={formData.formaPago}
|
||||
onChange={(e) => setFormData({ ...formData, formaPago: e.target.value })}
|
||||
className="input"
|
||||
>
|
||||
<option value="01">Efectivo</option>
|
||||
<option value="03">Transferencia</option>
|
||||
<option value="04">Tarjeta de Credito</option>
|
||||
<option value="28">Tarjeta de Debito</option>
|
||||
<option value="99">Por definir</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Metodo de Pago</label>
|
||||
<select
|
||||
value={formData.metodoPago}
|
||||
onChange={(e) => setFormData({ ...formData, metodoPago: e.target.value })}
|
||||
className="input"
|
||||
>
|
||||
<option value="PUE">Pago en una sola exhibicion</option>
|
||||
<option value="PPD">Pago en parcialidades</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Items */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-medium text-gray-900">Conceptos</h3>
|
||||
<button type="button" onClick={addItem} className="text-primary-600 text-sm font-medium">
|
||||
+ Agregar concepto
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{formData.items.map((item, index) => (
|
||||
<div key={index} className="p-4 bg-gray-50 rounded-lg">
|
||||
<div className="grid grid-cols-12 gap-3">
|
||||
<div className="col-span-6">
|
||||
<input
|
||||
type="text"
|
||||
value={item.descripcion}
|
||||
onChange={(e) => updateItem(index, 'descripcion', e.target.value)}
|
||||
className="input"
|
||||
placeholder="Descripcion"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<input
|
||||
type="number"
|
||||
value={item.cantidad}
|
||||
onChange={(e) => updateItem(index, 'cantidad', Number(e.target.value))}
|
||||
className="input"
|
||||
placeholder="Cant"
|
||||
min={1}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<input
|
||||
type="number"
|
||||
value={item.valorUnitario}
|
||||
onChange={(e) => updateItem(index, 'valorUnitario', Number(e.target.value))}
|
||||
className="input"
|
||||
placeholder="Precio"
|
||||
min={0}
|
||||
step={0.01}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-1 flex items-center justify-center">
|
||||
{formData.items.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeItem(index)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<XCircle className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total */}
|
||||
<div className="text-right">
|
||||
<p className="text-gray-500">Total (IVA incluido)</p>
|
||||
<p className="text-3xl font-bold">
|
||||
${total.toLocaleString('es-MX', { minimumFractionDigits: 2 })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button type="button" onClick={onClose} className="btn-secondary flex-1">
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" disabled={mutation.isPending} className="btn-primary flex-1">
|
||||
{mutation.isPending ? 'Creando...' : 'Crear Factura'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
731
apps/frontend/src/pages/Marketplace.tsx
Normal file
731
apps/frontend/src/pages/Marketplace.tsx
Normal file
@ -0,0 +1,731 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
Store,
|
||||
Search,
|
||||
Star,
|
||||
Heart,
|
||||
ShoppingCart,
|
||||
Package,
|
||||
Truck,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
MapPin,
|
||||
Phone,
|
||||
ChevronRight,
|
||||
Filter,
|
||||
} from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import { marketplaceApi } from '../lib/api';
|
||||
|
||||
const categories = [
|
||||
{ id: 'bebidas', name: 'Bebidas', icon: '🥤' },
|
||||
{ id: 'botanas', name: 'Botanas', icon: '🍿' },
|
||||
{ id: 'lacteos', name: 'Lacteos', icon: '🥛' },
|
||||
{ id: 'pan', name: 'Pan', icon: '🍞' },
|
||||
{ id: 'abarrotes', name: 'Abarrotes', icon: '🛒' },
|
||||
{ id: 'limpieza', name: 'Limpieza', icon: '🧹' },
|
||||
];
|
||||
|
||||
const orderStatusConfig = {
|
||||
pending: { label: 'Pendiente', color: 'yellow', icon: Clock },
|
||||
confirmed: { label: 'Confirmado', color: 'blue', icon: CheckCircle },
|
||||
preparing: { label: 'Preparando', color: 'indigo', icon: Package },
|
||||
shipped: { label: 'En camino', color: 'purple', icon: Truck },
|
||||
delivered: { label: 'Entregado', color: 'green', icon: CheckCircle },
|
||||
cancelled: { label: 'Cancelado', color: 'red', icon: XCircle },
|
||||
rejected: { label: 'Rechazado', color: 'red', icon: XCircle },
|
||||
};
|
||||
|
||||
export function Marketplace() {
|
||||
const [view, setView] = useState<'suppliers' | 'orders' | 'favorites'>('suppliers');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedSupplier, setSelectedSupplier] = useState<any>(null);
|
||||
const [showCart, setShowCart] = useState(false);
|
||||
const [cart, setCart] = useState<{ product: any; quantity: number }[]>([]);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: suppliers = [], isLoading: loadingSuppliers } = useQuery({
|
||||
queryKey: ['suppliers', selectedCategory, searchQuery],
|
||||
queryFn: async () => {
|
||||
const response = await marketplaceApi.getSuppliers({
|
||||
category: selectedCategory || undefined,
|
||||
search: searchQuery || undefined,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
|
||||
const { data: orders = [], isLoading: loadingOrders } = useQuery({
|
||||
queryKey: ['marketplace-orders'],
|
||||
queryFn: async () => {
|
||||
const response = await marketplaceApi.getOrders();
|
||||
return response.data;
|
||||
},
|
||||
enabled: view === 'orders',
|
||||
});
|
||||
|
||||
const { data: favorites = [] } = useQuery({
|
||||
queryKey: ['supplier-favorites'],
|
||||
queryFn: async () => {
|
||||
const response = await marketplaceApi.getFavorites();
|
||||
return response.data;
|
||||
},
|
||||
enabled: view === 'favorites',
|
||||
});
|
||||
|
||||
const addToCart = (product: any) => {
|
||||
const existing = cart.find((item) => item.product.id === product.id);
|
||||
if (existing) {
|
||||
setCart(
|
||||
cart.map((item) =>
|
||||
item.product.id === product.id
|
||||
? { ...item, quantity: item.quantity + 1 }
|
||||
: item
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setCart([...cart, { product, quantity: 1 }]);
|
||||
}
|
||||
};
|
||||
|
||||
const removeFromCart = (productId: string) => {
|
||||
setCart(cart.filter((item) => item.product.id !== productId));
|
||||
};
|
||||
|
||||
const updateCartQuantity = (productId: string, quantity: number) => {
|
||||
if (quantity <= 0) {
|
||||
removeFromCart(productId);
|
||||
} else {
|
||||
setCart(
|
||||
cart.map((item) =>
|
||||
item.product.id === productId ? { ...item, quantity } : item
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const cartTotal = cart.reduce(
|
||||
(sum, item) => sum + Number(item.product.unitPrice) * item.quantity,
|
||||
0
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Marketplace</h1>
|
||||
<p className="text-gray-500">Encuentra proveedores para tu negocio</p>
|
||||
</div>
|
||||
|
||||
{cart.length > 0 && (
|
||||
<button
|
||||
onClick={() => setShowCart(true)}
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<ShoppingCart className="h-4 w-4" />
|
||||
Carrito ({cart.length})
|
||||
<span className="ml-2 font-bold">
|
||||
${cartTotal.toLocaleString('es-MX', { minimumFractionDigits: 2 })}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 border-b">
|
||||
<button
|
||||
onClick={() => setView('suppliers')}
|
||||
className={clsx(
|
||||
'px-4 py-2 font-medium border-b-2 -mb-px transition-colors',
|
||||
view === 'suppliers'
|
||||
? 'border-primary-600 text-primary-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
)}
|
||||
>
|
||||
<Store className="h-4 w-4 inline mr-2" />
|
||||
Proveedores
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView('orders')}
|
||||
className={clsx(
|
||||
'px-4 py-2 font-medium border-b-2 -mb-px transition-colors',
|
||||
view === 'orders'
|
||||
? 'border-primary-600 text-primary-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
)}
|
||||
>
|
||||
<Package className="h-4 w-4 inline mr-2" />
|
||||
Mis Pedidos
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView('favorites')}
|
||||
className={clsx(
|
||||
'px-4 py-2 font-medium border-b-2 -mb-px transition-colors',
|
||||
view === 'favorites'
|
||||
? 'border-primary-600 text-primary-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
)}
|
||||
>
|
||||
<Heart className="h-4 w-4 inline mr-2" />
|
||||
Favoritos
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{view === 'suppliers' && (
|
||||
<>
|
||||
{/* Search & Filter */}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Buscar proveedores..."
|
||||
className="input pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="flex gap-2 overflow-x-auto pb-2">
|
||||
<button
|
||||
onClick={() => setSelectedCategory(null)}
|
||||
className={clsx(
|
||||
'px-4 py-2 rounded-lg font-medium whitespace-nowrap transition-colors',
|
||||
!selectedCategory
|
||||
? 'bg-primary-100 text-primary-700'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
)}
|
||||
>
|
||||
Todos
|
||||
</button>
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category.id}
|
||||
onClick={() => setSelectedCategory(category.id)}
|
||||
className={clsx(
|
||||
'px-4 py-2 rounded-lg font-medium whitespace-nowrap transition-colors flex items-center gap-2',
|
||||
selectedCategory === category.id
|
||||
? 'bg-primary-100 text-primary-700'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
)}
|
||||
>
|
||||
<span>{category.icon}</span>
|
||||
{category.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Suppliers Grid */}
|
||||
{loadingSuppliers ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto"></div>
|
||||
<p className="text-gray-500 mt-2">Buscando proveedores...</p>
|
||||
</div>
|
||||
) : suppliers.length === 0 ? (
|
||||
<div className="text-center py-12 card">
|
||||
<Store className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900">No hay proveedores</h3>
|
||||
<p className="text-gray-500">Pronto habra mas proveedores disponibles</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{suppliers.map((supplier: any) => (
|
||||
<SupplierCard
|
||||
key={supplier.id}
|
||||
supplier={supplier}
|
||||
onClick={() => setSelectedSupplier(supplier)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{view === 'orders' && (
|
||||
<div className="space-y-4">
|
||||
{loadingOrders ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto"></div>
|
||||
</div>
|
||||
) : orders.length === 0 ? (
|
||||
<div className="text-center py-12 card">
|
||||
<Package className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900">No hay pedidos</h3>
|
||||
<p className="text-gray-500">Tus pedidos a proveedores apareceran aqui</p>
|
||||
</div>
|
||||
) : (
|
||||
orders.map((order: any) => (
|
||||
<OrderCard key={order.id} order={order} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{view === 'favorites' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{favorites.length === 0 ? (
|
||||
<div className="col-span-full text-center py-12 card">
|
||||
<Heart className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-gray-900">Sin favoritos</h3>
|
||||
<p className="text-gray-500">Agrega proveedores a tus favoritos</p>
|
||||
</div>
|
||||
) : (
|
||||
favorites.map((supplier: any) => (
|
||||
<SupplierCard
|
||||
key={supplier.id}
|
||||
supplier={supplier}
|
||||
onClick={() => setSelectedSupplier(supplier)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Supplier Detail Modal */}
|
||||
{selectedSupplier && (
|
||||
<SupplierDetailModal
|
||||
supplier={selectedSupplier}
|
||||
onClose={() => setSelectedSupplier(null)}
|
||||
onAddToCart={addToCart}
|
||||
cart={cart}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Cart Modal */}
|
||||
{showCart && (
|
||||
<CartModal
|
||||
cart={cart}
|
||||
supplier={selectedSupplier}
|
||||
onClose={() => setShowCart(false)}
|
||||
onUpdateQuantity={updateCartQuantity}
|
||||
onRemove={removeFromCart}
|
||||
onOrderSuccess={() => {
|
||||
setCart([]);
|
||||
setShowCart(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['marketplace-orders'] });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SupplierCard({
|
||||
supplier,
|
||||
onClick,
|
||||
}: {
|
||||
supplier: any;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="card cursor-pointer hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-16 h-16 rounded-lg bg-gray-100 flex items-center justify-center overflow-hidden">
|
||||
{supplier.logoUrl ? (
|
||||
<img src={supplier.logoUrl} alt={supplier.name} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<Store className="h-8 w-8 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-bold text-gray-900 truncate">{supplier.name}</h3>
|
||||
{supplier.verified && (
|
||||
<CheckCircle className="h-4 w-4 text-blue-500 flex-shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="flex items-center text-yellow-500">
|
||||
<Star className="h-4 w-4 fill-current" />
|
||||
<span className="ml-1 text-sm font-medium">{Number(supplier.rating).toFixed(1)}</span>
|
||||
</div>
|
||||
<span className="text-gray-400">·</span>
|
||||
<span className="text-sm text-gray-500">{supplier.totalReviews} resenas</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{supplier.categories?.slice(0, 3).map((cat: string) => (
|
||||
<span key={cat} className="px-2 py-0.5 text-xs bg-gray-100 rounded-full text-gray-600">
|
||||
{cat}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-2 text-sm text-gray-500">
|
||||
<span>Min: ${Number(supplier.minOrderAmount).toFixed(0)}</span>
|
||||
{Number(supplier.deliveryFee) > 0 ? (
|
||||
<span>Envio: ${Number(supplier.deliveryFee).toFixed(0)}</span>
|
||||
) : (
|
||||
<span className="text-green-600">Envio gratis</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="h-5 w-5 text-gray-400 flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function OrderCard({ order }: { order: any }) {
|
||||
const status = orderStatusConfig[order.status as keyof typeof orderStatusConfig] || orderStatusConfig.pending;
|
||||
const StatusIcon = status.icon;
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`p-3 rounded-full bg-${status.color}-100`}>
|
||||
<StatusIcon className={`h-6 w-6 text-${status.color}-600`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-bold">Pedido #{order.orderNumber}</h3>
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full bg-${status.color}-100 text-${status.color}-700`}>
|
||||
{status.label}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-600">{order.supplier?.name}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{order.items?.length} productos
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<p className="text-2xl font-bold">
|
||||
${Number(order.total).toLocaleString('es-MX', { minimumFractionDigits: 2 })}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{new Date(order.createdAt).toLocaleDateString('es-MX')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SupplierDetailModal({
|
||||
supplier,
|
||||
onClose,
|
||||
onAddToCart,
|
||||
cart,
|
||||
}: {
|
||||
supplier: any;
|
||||
onClose: () => void;
|
||||
onAddToCart: (product: any) => void;
|
||||
cart: { product: any; quantity: number }[];
|
||||
}) {
|
||||
const [search, setSearch] = useState('');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: products = [], isLoading } = useQuery({
|
||||
queryKey: ['supplier-products', supplier.id, search],
|
||||
queryFn: async () => {
|
||||
const response = await marketplaceApi.getSupplierProducts(supplier.id, {
|
||||
search: search || undefined,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
});
|
||||
|
||||
const favoriteMutation = useMutation({
|
||||
mutationFn: () => marketplaceApi.addFavorite(supplier.id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['supplier-favorites'] });
|
||||
},
|
||||
});
|
||||
|
||||
const getCartQuantity = (productId: string) => {
|
||||
const item = cart.find((i) => i.product.id === productId);
|
||||
return item?.quantity || 0;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-xl max-w-3xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-20 h-20 rounded-lg bg-gray-100 flex items-center justify-center overflow-hidden">
|
||||
{supplier.logoUrl ? (
|
||||
<img src={supplier.logoUrl} alt={supplier.name} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<Store className="h-10 w-10 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold">{supplier.name}</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
<XCircle className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<Star className="h-4 w-4 text-yellow-500 fill-current" />
|
||||
<span className="font-medium">{Number(supplier.rating).toFixed(1)}</span>
|
||||
<span className="text-gray-400">·</span>
|
||||
<span className="text-gray-500">{supplier.totalReviews} resenas</span>
|
||||
</div>
|
||||
{supplier.address && (
|
||||
<p className="text-sm text-gray-500 mt-1 flex items-center gap-1">
|
||||
<MapPin className="h-4 w-4" />
|
||||
{supplier.city}, {supplier.state}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 mt-4">
|
||||
<button
|
||||
onClick={() => favoriteMutation.mutate()}
|
||||
className="btn-secondary flex items-center gap-2"
|
||||
>
|
||||
<Heart className="h-4 w-4" />
|
||||
Agregar a favoritos
|
||||
</button>
|
||||
{supplier.contactPhone && (
|
||||
<a href={`tel:${supplier.contactPhone}`} className="btn-secondary flex items-center gap-2">
|
||||
<Phone className="h-4 w-4" />
|
||||
Llamar
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="p-4 border-b">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Buscar productos..."
|
||||
className="input pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Products */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600 mx-auto"></div>
|
||||
</div>
|
||||
) : products.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
No hay productos disponibles
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{products.map((product: any) => {
|
||||
const inCart = getCartQuantity(product.id);
|
||||
return (
|
||||
<div key={product.id} className="flex gap-3 p-3 bg-gray-50 rounded-lg">
|
||||
<div className="w-16 h-16 rounded bg-white flex items-center justify-center overflow-hidden">
|
||||
{product.imageUrl ? (
|
||||
<img src={product.imageUrl} alt={product.name} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<Package className="h-6 w-6 text-gray-300" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-medium text-gray-900 truncate">{product.name}</h4>
|
||||
<p className="text-sm text-gray-500">
|
||||
Min: {product.minQuantity} {product.unitType}
|
||||
</p>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="font-bold text-primary-600">
|
||||
${Number(product.unitPrice).toFixed(2)}
|
||||
</span>
|
||||
{inCart > 0 ? (
|
||||
<span className="text-sm text-green-600">
|
||||
{inCart} en carrito
|
||||
</span>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onAddToCart(product)}
|
||||
className="text-sm text-primary-600 font-medium"
|
||||
>
|
||||
+ Agregar
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CartModal({
|
||||
cart,
|
||||
supplier,
|
||||
onClose,
|
||||
onUpdateQuantity,
|
||||
onRemove,
|
||||
onOrderSuccess,
|
||||
}: {
|
||||
cart: { product: any; quantity: number }[];
|
||||
supplier: any;
|
||||
onClose: () => void;
|
||||
onUpdateQuantity: (productId: string, quantity: number) => void;
|
||||
onRemove: (productId: string) => void;
|
||||
onOrderSuccess: () => void;
|
||||
}) {
|
||||
const [deliveryAddress, setDeliveryAddress] = useState('');
|
||||
const [deliveryPhone, setDeliveryPhone] = useState('');
|
||||
const [notes, setNotes] = useState('');
|
||||
|
||||
const subtotal = cart.reduce(
|
||||
(sum, item) => sum + Number(item.product.unitPrice) * item.quantity,
|
||||
0
|
||||
);
|
||||
|
||||
const orderMutation = useMutation({
|
||||
mutationFn: (data: any) => marketplaceApi.createOrder(data),
|
||||
onSuccess: onOrderSuccess,
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const supplierId = cart[0]?.product?.supplierId;
|
||||
if (!supplierId) return;
|
||||
|
||||
orderMutation.mutate({
|
||||
supplierId,
|
||||
items: cart.map((item) => ({
|
||||
productId: item.product.id,
|
||||
quantity: item.quantity,
|
||||
})),
|
||||
deliveryAddress,
|
||||
deliveryPhone,
|
||||
notes,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
||||
<div className="bg-white rounded-xl max-w-lg w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-6 border-b flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold">Tu Carrito</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
<XCircle className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
||||
{/* Items */}
|
||||
<div className="space-y-3">
|
||||
{cart.map((item) => (
|
||||
<div key={item.product.id} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
|
||||
<div className="flex-1">
|
||||
<p className="font-medium">{item.product.name}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
${Number(item.product.unitPrice).toFixed(2)} x {item.quantity}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onUpdateQuantity(item.product.id, item.quantity - 1)}
|
||||
className="w-8 h-8 rounded-full bg-gray-200 text-gray-600 flex items-center justify-center"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span className="w-8 text-center font-medium">{item.quantity}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onUpdateQuantity(item.product.id, item.quantity + 1)}
|
||||
className="w-8 h-8 rounded-full bg-gray-200 text-gray-600 flex items-center justify-center"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<p className="font-bold w-20 text-right">
|
||||
${(Number(item.product.unitPrice) * item.quantity).toFixed(2)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Delivery Info */}
|
||||
<div className="pt-4 border-t space-y-4">
|
||||
<h3 className="font-medium">Datos de entrega</h3>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Direccion de entrega
|
||||
</label>
|
||||
<textarea
|
||||
value={deliveryAddress}
|
||||
onChange={(e) => setDeliveryAddress(e.target.value)}
|
||||
className="input"
|
||||
rows={2}
|
||||
required
|
||||
placeholder="Calle, numero, colonia, CP"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Telefono de contacto
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={deliveryPhone}
|
||||
onChange={(e) => setDeliveryPhone(e.target.value)}
|
||||
className="input"
|
||||
required
|
||||
placeholder="5512345678"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Notas (opcional)
|
||||
</label>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
className="input"
|
||||
rows={2}
|
||||
placeholder="Instrucciones especiales..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Total */}
|
||||
<div className="pt-4 border-t">
|
||||
<div className="flex justify-between text-lg font-bold">
|
||||
<span>Total</span>
|
||||
<span>${subtotal.toLocaleString('es-MX', { minimumFractionDigits: 2 })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button type="button" onClick={onClose} className="btn-secondary flex-1">
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={orderMutation.isPending || cart.length === 0}
|
||||
className="btn-primary flex-1"
|
||||
>
|
||||
{orderMutation.isPending ? 'Enviando...' : 'Enviar Pedido'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
276
apps/frontend/src/pages/Referrals.tsx
Normal file
276
apps/frontend/src/pages/Referrals.tsx
Normal file
@ -0,0 +1,276 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Gift, Users, Copy, Share2, Check, Clock, X } from 'lucide-react';
|
||||
import { referralsApi } from '../lib/api';
|
||||
|
||||
interface ReferralStats {
|
||||
code: string;
|
||||
totalInvited: number;
|
||||
totalConverted: number;
|
||||
totalPending: number;
|
||||
totalExpired: number;
|
||||
monthsEarned: number;
|
||||
monthsAvailable: number;
|
||||
}
|
||||
|
||||
interface Referral {
|
||||
id: string;
|
||||
referredTenantId: string;
|
||||
codeUsed: string;
|
||||
status: 'pending' | 'converted' | 'rewarded' | 'expired';
|
||||
createdAt: string;
|
||||
convertedAt?: string;
|
||||
}
|
||||
|
||||
export function Referrals() {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: stats, isLoading } = useQuery<ReferralStats>({
|
||||
queryKey: ['referral-stats'],
|
||||
queryFn: async () => {
|
||||
const res = await referralsApi.getStats();
|
||||
return res.data;
|
||||
},
|
||||
});
|
||||
|
||||
const { data: referrals } = useQuery<Referral[]>({
|
||||
queryKey: ['referrals'],
|
||||
queryFn: async () => {
|
||||
const res = await referralsApi.getMyReferrals();
|
||||
return res.data;
|
||||
},
|
||||
});
|
||||
|
||||
const generateCodeMutation = useMutation({
|
||||
mutationFn: () => referralsApi.generateCode(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['referral-stats'] });
|
||||
},
|
||||
});
|
||||
|
||||
const copyCode = async () => {
|
||||
if (stats?.code) {
|
||||
await navigator.clipboard.writeText(stats.code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const shareWhatsApp = () => {
|
||||
if (stats?.code) {
|
||||
const text = `Usa mi codigo ${stats.code} para registrarte en MiChangarrito y obtener 50% de descuento en tu primer mes! https://michangarrito.com/r/${stats.code}`;
|
||||
window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||
<Clock className="h-3 w-3" />
|
||||
Pendiente
|
||||
</span>
|
||||
);
|
||||
case 'converted':
|
||||
case 'rewarded':
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
<Check className="h-3 w-3" />
|
||||
Convertido
|
||||
</span>
|
||||
);
|
||||
case 'expired':
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||
<X className="h-3 w-3" />
|
||||
Expirado
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Programa de Referidos</h1>
|
||||
<p className="text-gray-500">Invita amigos y gana meses gratis</p>
|
||||
</div>
|
||||
|
||||
{/* Share Card */}
|
||||
<div className="card bg-gradient-to-r from-primary-500 to-primary-600 text-white">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Gift className="h-8 w-8" />
|
||||
<div>
|
||||
<h2 className="text-xl font-bold">Invita amigos y gana</h2>
|
||||
<p className="text-primary-100">Por cada amigo que se suscriba, ganas 1 mes gratis</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white/10 rounded-lg p-4 mb-4">
|
||||
<p className="text-sm text-primary-100 mb-2">Tu codigo de referido:</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl font-bold tracking-wider">{stats?.code || 'Generando...'}</span>
|
||||
<button
|
||||
onClick={copyCode}
|
||||
className="p-2 rounded-lg bg-white/20 hover:bg-white/30 transition-colors"
|
||||
title="Copiar codigo"
|
||||
>
|
||||
{copied ? <Check className="h-5 w-5" /> : <Copy className="h-5 w-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<button
|
||||
onClick={copyCode}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-white text-primary-600 rounded-lg font-medium hover:bg-primary-50 transition-colors"
|
||||
>
|
||||
<Copy className="h-5 w-5" />
|
||||
{copied ? 'Copiado!' : 'Copiar codigo'}
|
||||
</button>
|
||||
<button
|
||||
onClick={shareWhatsApp}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-green-500 text-white rounded-lg font-medium hover:bg-green-600 transition-colors"
|
||||
>
|
||||
<Share2 className="h-5 w-5" />
|
||||
Compartir por WhatsApp
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="card">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-blue-100 rounded-lg">
|
||||
<Users className="h-5 w-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold">{stats?.totalInvited || 0}</p>
|
||||
<p className="text-sm text-gray-500">Invitados</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-green-100 rounded-lg">
|
||||
<Check className="h-5 w-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold">{stats?.totalConverted || 0}</p>
|
||||
<p className="text-sm text-gray-500">Convertidos</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-purple-100 rounded-lg">
|
||||
<Gift className="h-5 w-5 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold">{stats?.monthsEarned || 0}</p>
|
||||
<p className="text-sm text-gray-500">Meses ganados</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-primary-100 rounded-lg">
|
||||
<Gift className="h-5 w-5 text-primary-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold">{stats?.monthsAvailable || 0}</p>
|
||||
<p className="text-sm text-gray-500">Disponibles</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* How it works */}
|
||||
<div className="card">
|
||||
<h3 className="text-lg font-bold mb-4">Como funciona</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<span className="text-xl font-bold text-primary-600">1</span>
|
||||
</div>
|
||||
<h4 className="font-medium mb-1">Comparte tu codigo</h4>
|
||||
<p className="text-sm text-gray-500">Envia tu codigo a amigos por WhatsApp</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<span className="text-xl font-bold text-primary-600">2</span>
|
||||
</div>
|
||||
<h4 className="font-medium mb-1">Tu amigo se registra</h4>
|
||||
<p className="text-sm text-gray-500">Obtiene 50% de descuento en su primer mes</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 bg-primary-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<span className="text-xl font-bold text-primary-600">3</span>
|
||||
</div>
|
||||
<h4 className="font-medium mb-1">Tu ganas 1 mes gratis</h4>
|
||||
<p className="text-sm text-gray-500">Cuando tu amigo paga su primer mes</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Referrals List */}
|
||||
<div className="card">
|
||||
<h3 className="text-lg font-bold mb-4">Tus referidos</h3>
|
||||
{referrals && referrals.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="text-left text-sm text-gray-500 border-b">
|
||||
<th className="pb-3 font-medium">Fecha</th>
|
||||
<th className="pb-3 font-medium">Estado</th>
|
||||
<th className="pb-3 font-medium">Conversion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{referrals.map((referral) => (
|
||||
<tr key={referral.id}>
|
||||
<td className="py-3">
|
||||
{new Date(referral.createdAt).toLocaleDateString('es-MX', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</td>
|
||||
<td className="py-3">{getStatusBadge(referral.status)}</td>
|
||||
<td className="py-3 text-sm text-gray-500">
|
||||
{referral.convertedAt
|
||||
? new Date(referral.convertedAt).toLocaleDateString('es-MX')
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<Users className="h-12 w-12 mx-auto mb-3 text-gray-300" />
|
||||
<p>Aun no tienes referidos</p>
|
||||
<p className="text-sm">Comparte tu codigo para empezar a ganar</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -15,7 +15,12 @@
|
||||
},
|
||||
"ios": {
|
||||
"supportsTablet": false,
|
||||
"bundleIdentifier": "com.michangarrito.app"
|
||||
"bundleIdentifier": "com.michangarrito.app",
|
||||
"associatedDomains": ["applinks:michangarrito.com"],
|
||||
"infoPlist": {
|
||||
"NSCameraUsageDescription": "Necesitamos acceso a la camara para escanear codigos de barras",
|
||||
"UIBackgroundModes": ["fetch", "remote-notification"]
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
@ -24,7 +29,24 @@
|
||||
},
|
||||
"package": "com.michangarrito.app",
|
||||
"edgeToEdgeEnabled": true,
|
||||
"predictiveBackGestureEnabled": false
|
||||
"predictiveBackGestureEnabled": false,
|
||||
"intentFilters": [
|
||||
{
|
||||
"action": "VIEW",
|
||||
"autoVerify": true,
|
||||
"data": [
|
||||
{
|
||||
"scheme": "michangarrito"
|
||||
},
|
||||
{
|
||||
"scheme": "https",
|
||||
"host": "michangarrito.com",
|
||||
"pathPrefix": "/r"
|
||||
}
|
||||
],
|
||||
"category": ["BROWSABLE", "DEFAULT"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { ActivityIndicator, View, StyleSheet } from 'react-native';
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { ActivityIndicator, View, StyleSheet, Linking } from 'react-native';
|
||||
import { NavigationContainer, LinkingOptions } from '@react-navigation/native';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import { Text } from 'react-native';
|
||||
@ -8,6 +8,45 @@ import { Text } from 'react-native';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { colors, fontSize } from '../constants/theme';
|
||||
|
||||
// Deep linking configuration
|
||||
const linking: LinkingOptions<RootStackParamList> = {
|
||||
prefixes: ['michangarrito://', 'https://michangarrito.com'],
|
||||
config: {
|
||||
screens: {
|
||||
Main: {
|
||||
screens: {
|
||||
Dashboard: 'dashboard',
|
||||
POS: 'pos',
|
||||
Reports: 'reports',
|
||||
More: 'more',
|
||||
},
|
||||
},
|
||||
Products: 'products',
|
||||
Inventory: 'inventory',
|
||||
Customers: 'customers',
|
||||
Settings: 'settings',
|
||||
BarcodeScanner: 'scan',
|
||||
},
|
||||
},
|
||||
async getInitialURL() {
|
||||
// Handle app opened from deep link
|
||||
const url = await Linking.getInitialURL();
|
||||
if (url != null) {
|
||||
return url;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
subscribe(listener) {
|
||||
// Listen to incoming links while app is open
|
||||
const subscription = Linking.addEventListener('url', ({ url }) => {
|
||||
listener(url);
|
||||
});
|
||||
return () => {
|
||||
subscription.remove();
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Screens
|
||||
import LoginScreen from '../screens/LoginScreen';
|
||||
import DashboardScreen from '../screens/DashboardScreen';
|
||||
@ -123,7 +162,7 @@ export default function AppNavigator() {
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationContainer>
|
||||
<NavigationContainer linking={linking}>
|
||||
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
|
||||
@ -0,0 +1,491 @@
|
||||
# MiChangarrito - Arquitectura Técnica
|
||||
|
||||
## Diagrama General
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ MICHANGARRITO PLATFORM │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ CLIENTES │
|
||||
│ ──────── │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ iOS App │ │ Android App │ │ Web App │ │ WhatsApp │ │
|
||||
│ │ React Native │ │ React Native │ │ React │ │ (Meta API) │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ └──────────────────┴──────────────────┴──────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────────────────────────┴───────────────────────────────────┐ │
|
||||
│ │ API GATEWAY │ │
|
||||
│ │ (Kong / Custom) │ │
|
||||
│ │ - Rate Limiting - Auth (JWT) - Request Routing │ │
|
||||
│ └───────────────────────────────────┬───────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ SERVICIOS │ │
|
||||
│ ───────── │ │
|
||||
│ │ │
|
||||
│ ┌───────────────────────────────────┴───────────────────────────────────┐ │
|
||||
│ │ BACKEND API (NestJS) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||||
│ │ │ Auth │ │ Sales │ │ Products │ │ Inventory │ │ │
|
||||
│ │ │ Module │ │ Module │ │ Module │ │ Module │ │ │
|
||||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||||
│ │ │ Customers │ │ Orders │ │ Fiados │ │ Reports │ │ │
|
||||
│ │ │ Module │ │ Module │ │ Module │ │ Module │ │ │
|
||||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||||
│ │ │ Subscript. │ │ Tokens │ │ Tenants │ │ │
|
||||
│ │ │ Module │ │ Module │ │ Module │ │ │
|
||||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ └───────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────┼─────────────────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ MCP Server │ │ WhatsApp │ │ Payments │ │
|
||||
│ │ (LLM) │ │ Service │ │ Service │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ ┌─────────┐ │ │ - Webhooks │ │ - Stripe │ │
|
||||
│ │ │OpenRouter│ │ │ - Send msg │ │ - MP SDK │ │
|
||||
│ │ │ OpenAI │ │ │ - Multi-num │ │ - Clip API │ │
|
||||
│ │ │ Claude │ │ │ - OCR/Audio │ │ - CoDi │ │
|
||||
│ │ │ Ollama │ │ │ │ │ - SPEI │ │
|
||||
│ │ └─────────┘ │ │ │ │ │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │
|
||||
│ DATOS │
|
||||
│ ───── │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
||||
│ │ │ PostgreSQL │ │ Redis │ │ S3/MinIO │ │ │
|
||||
│ │ │(Multi-tenant│ │ (Cache, │ │ (Files, │ │ │
|
||||
│ │ │ + RLS) │ │ Sessions) │ │ Images) │ │ │
|
||||
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└───────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Componentes Principales
|
||||
|
||||
### 1. App Móvil (React Native)
|
||||
|
||||
```
|
||||
apps/mobile/
|
||||
├── src/
|
||||
│ ├── components/ # Componentes reutilizables
|
||||
│ │ ├── ui/ # Botones, inputs, cards
|
||||
│ │ ├── pos/ # Componentes de punto de venta
|
||||
│ │ └── common/ # Header, navigation, etc.
|
||||
│ ├── screens/ # Pantallas
|
||||
│ │ ├── auth/ # Login, registro
|
||||
│ │ ├── pos/ # Punto de venta
|
||||
│ │ ├── products/ # Catálogo
|
||||
│ │ ├── inventory/ # Inventario
|
||||
│ │ ├── customers/ # Clientes y fiados
|
||||
│ │ ├── orders/ # Pedidos
|
||||
│ │ ├── reports/ # Reportes
|
||||
│ │ └── settings/ # Configuración
|
||||
│ ├── services/ # API calls, storage
|
||||
│ ├── hooks/ # Custom hooks
|
||||
│ ├── store/ # Estado global (Zustand/Redux)
|
||||
│ ├── utils/ # Helpers
|
||||
│ └── navigation/ # React Navigation config
|
||||
├── android/
|
||||
├── ios/
|
||||
└── package.json
|
||||
```
|
||||
|
||||
**Tecnologías clave:**
|
||||
- React Native 0.73+
|
||||
- Expo (managed workflow para simplificar)
|
||||
- React Navigation 6
|
||||
- Zustand (estado global)
|
||||
- React Query (cache de API)
|
||||
- react-native-camera (scanner)
|
||||
- react-native-bluetooth-classic (terminales)
|
||||
|
||||
### 2. Web Dashboard (React)
|
||||
|
||||
```
|
||||
apps/web/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ ├── pages/
|
||||
│ │ ├── dashboard/
|
||||
│ │ ├── sales/
|
||||
│ │ ├── products/
|
||||
│ │ ├── inventory/
|
||||
│ │ ├── customers/
|
||||
│ │ ├── reports/
|
||||
│ │ ├── settings/
|
||||
│ │ └── subscription/
|
||||
│ ├── services/
|
||||
│ ├── hooks/
|
||||
│ ├── store/
|
||||
│ └── utils/
|
||||
├── public/
|
||||
└── package.json
|
||||
```
|
||||
|
||||
**Tecnologías clave:**
|
||||
- React 18
|
||||
- Vite
|
||||
- TailwindCSS
|
||||
- shadcn/ui
|
||||
- React Query
|
||||
- Recharts (gráficas)
|
||||
- React Router 6
|
||||
|
||||
### 3. Backend API (NestJS)
|
||||
|
||||
```
|
||||
apps/backend/
|
||||
├── src/
|
||||
│ ├── modules/
|
||||
│ │ ├── auth/ # Autenticación
|
||||
│ │ ├── tenants/ # Multi-tenancy
|
||||
│ │ ├── users/ # Usuarios
|
||||
│ │ ├── products/ # Productos
|
||||
│ │ ├── categories/ # Categorías
|
||||
│ │ ├── sales/ # Ventas
|
||||
│ │ ├── inventory/ # Inventario
|
||||
│ │ ├── customers/ # Clientes
|
||||
│ │ ├── fiados/ # Sistema de fiados
|
||||
│ │ ├── orders/ # Pedidos
|
||||
│ │ ├── payments/ # Pagos (integraciones)
|
||||
│ │ ├── subscriptions/ # Suscripciones
|
||||
│ │ ├── tokens/ # Tokens IA
|
||||
│ │ ├── reports/ # Reportes
|
||||
│ │ └── notifications/ # Push y WhatsApp
|
||||
│ ├── common/
|
||||
│ │ ├── decorators/
|
||||
│ │ ├── guards/
|
||||
│ │ ├── interceptors/
|
||||
│ │ ├── filters/
|
||||
│ │ └── pipes/
|
||||
│ ├── database/
|
||||
│ │ ├── entities/
|
||||
│ │ └── migrations/
|
||||
│ └── config/
|
||||
├── test/
|
||||
└── package.json
|
||||
```
|
||||
|
||||
**Tecnologías clave:**
|
||||
- NestJS 10
|
||||
- TypeORM
|
||||
- PostgreSQL
|
||||
- Redis (cache, sessions, bull queues)
|
||||
- Passport (auth)
|
||||
- Class-validator
|
||||
- Swagger/OpenAPI
|
||||
|
||||
### 4. MCP Server (LLM Gateway)
|
||||
|
||||
```
|
||||
apps/mcp-server/
|
||||
├── src/
|
||||
│ ├── providers/ # Adaptadores de LLM
|
||||
│ │ ├── openai.ts
|
||||
│ │ ├── anthropic.ts
|
||||
│ │ ├── openrouter.ts
|
||||
│ │ └── ollama.ts
|
||||
│ ├── tools/ # Herramientas MCP
|
||||
│ │ ├── sales.tools.ts
|
||||
│ │ ├── products.tools.ts
|
||||
│ │ ├── inventory.tools.ts
|
||||
│ │ ├── customers.tools.ts
|
||||
│ │ ├── fiados.tools.ts
|
||||
│ │ ├── orders.tools.ts
|
||||
│ │ └── reports.tools.ts
|
||||
│ ├── prompts/ # System prompts
|
||||
│ │ ├── owner.prompt.ts
|
||||
│ │ └── customer.prompt.ts
|
||||
│ ├── handlers/
|
||||
│ │ ├── message.handler.ts
|
||||
│ │ └── tool-call.handler.ts
|
||||
│ └── config/
|
||||
└── package.json
|
||||
```
|
||||
|
||||
**Tecnologías clave:**
|
||||
- TypeScript
|
||||
- MCP SDK (@anthropic/mcp-sdk o custom)
|
||||
- Axios (llamadas a LLM APIs)
|
||||
- Token counting (tiktoken)
|
||||
|
||||
### 5. WhatsApp Service
|
||||
|
||||
```
|
||||
apps/whatsapp-service/
|
||||
├── src/
|
||||
│ ├── webhooks/ # Recepción de mensajes
|
||||
│ ├── handlers/
|
||||
│ │ ├── text.handler.ts
|
||||
│ │ ├── audio.handler.ts
|
||||
│ │ ├── image.handler.ts
|
||||
│ │ └── document.handler.ts
|
||||
│ ├── services/
|
||||
│ │ ├── meta-api.service.ts
|
||||
│ │ ├── transcription.service.ts
|
||||
│ │ ├── ocr.service.ts
|
||||
│ │ └── tenant-detection.service.ts
|
||||
│ ├── templates/ # Plantillas de mensajes
|
||||
│ └── config/
|
||||
└── package.json
|
||||
```
|
||||
|
||||
**Tecnologías clave:**
|
||||
- NestJS
|
||||
- Meta WhatsApp Business API
|
||||
- Whisper API (transcripción)
|
||||
- Google Vision / Tesseract (OCR)
|
||||
|
||||
## Base de Datos
|
||||
|
||||
### Schema Multi-tenant
|
||||
|
||||
```sql
|
||||
-- Tenants (negocios)
|
||||
CREATE TABLE tenants (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
slug VARCHAR(50) UNIQUE,
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
business_type VARCHAR(50),
|
||||
whatsapp_config JSONB,
|
||||
subscription_status VARCHAR(20),
|
||||
subscription_plan VARCHAR(20),
|
||||
token_balance INTEGER DEFAULT 0,
|
||||
settings JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Todas las tablas tienen tenant_id y RLS
|
||||
CREATE TABLE products (
|
||||
id UUID PRIMARY KEY,
|
||||
tenant_id UUID REFERENCES tenants(id),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
barcode VARCHAR(50),
|
||||
price NUMERIC(10,2) NOT NULL,
|
||||
cost NUMERIC(10,2),
|
||||
category_id UUID,
|
||||
stock INTEGER DEFAULT 0,
|
||||
min_stock INTEGER DEFAULT 5,
|
||||
image_url VARCHAR(500),
|
||||
active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- RLS Policy
|
||||
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_isolation ON products
|
||||
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
|
||||
```
|
||||
|
||||
### Schemas Principales
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ DATABASE SCHEMAS │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ public │
|
||||
│ ├── tenants # Negocios/comercios │
|
||||
│ ├── users # Usuarios (dueños) │
|
||||
│ └── tenant_settings # Configuración por tenant │
|
||||
│ │
|
||||
│ catalog │
|
||||
│ ├── products # Productos │
|
||||
│ ├── categories # Categorías │
|
||||
│ ├── product_variants # Variantes/presentaciones │
|
||||
│ └── product_templates # Templates predefinidos │
|
||||
│ │
|
||||
│ sales │
|
||||
│ ├── sales # Ventas │
|
||||
│ ├── sale_items # Items de venta │
|
||||
│ ├── payments # Pagos recibidos │
|
||||
│ └── cash_registers # Cortes de caja │
|
||||
│ │
|
||||
│ inventory │
|
||||
│ ├── stock_movements # Movimientos de inventario │
|
||||
│ ├── stock_alerts # Alertas de stock bajo │
|
||||
│ └── purchase_entries # Entradas de compra │
|
||||
│ │
|
||||
│ customers │
|
||||
│ ├── customers # Clientes │
|
||||
│ ├── fiados # Fiados/créditos │
|
||||
│ ├── fiado_payments # Pagos de fiados │
|
||||
│ └── orders # Pedidos de clientes │
|
||||
│ │
|
||||
│ subscriptions │
|
||||
│ ├── plans # Planes disponibles │
|
||||
│ ├── subscriptions # Suscripciones activas │
|
||||
│ ├── token_packages # Paquetes de tokens │
|
||||
│ ├── token_purchases # Compras de tokens │
|
||||
│ └── token_usage # Consumo de tokens │
|
||||
│ │
|
||||
│ messaging │
|
||||
│ ├── whatsapp_sessions # Sesiones de chat │
|
||||
│ ├── messages # Historial de mensajes │
|
||||
│ └── notifications # Notificaciones enviadas │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Integraciones Externas
|
||||
|
||||
### Pagos
|
||||
|
||||
| Servicio | Uso | SDK/API |
|
||||
|----------|-----|---------|
|
||||
| Stripe | Suscripciones, tokens, OXXO | stripe-node |
|
||||
| Mercado Pago | Terminal Bluetooth | mercadopago SDK |
|
||||
| Clip | Terminal Bluetooth | Clip SDK |
|
||||
| CoDi | QR de cobro | Banxico API |
|
||||
| SPEI | Transferencias | STP/Banxico |
|
||||
|
||||
### Comunicación
|
||||
|
||||
| Servicio | Uso | API |
|
||||
|----------|-----|-----|
|
||||
| Meta WhatsApp Business | Mensajería principal | Cloud API |
|
||||
| Firebase Cloud Messaging | Push notifications | Admin SDK |
|
||||
| Twilio (backup) | SMS OTP | REST API |
|
||||
|
||||
### IA/ML
|
||||
|
||||
| Servicio | Uso | API |
|
||||
|----------|-----|-----|
|
||||
| OpenRouter | LLM barato | OpenAI-compatible |
|
||||
| OpenAI | LLM premium | REST API |
|
||||
| Anthropic | LLM alternativo | REST API |
|
||||
| Whisper | Transcripción audio | OpenAI API |
|
||||
| Google Vision | OCR de imágenes | Vision API |
|
||||
|
||||
### Storage
|
||||
|
||||
| Servicio | Uso |
|
||||
|----------|-----|
|
||||
| AWS S3 / MinIO | Imágenes, documentos |
|
||||
| Cloudflare R2 | CDN de assets |
|
||||
|
||||
## Seguridad
|
||||
|
||||
### Autenticación
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ FLUJO DE AUTENTICACIÓN │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. Login con teléfono │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ User │ ────── │ API │ ────── │ Twilio/ │ │
|
||||
│ │ (phone) │ │ (OTP) │ │WhatsApp │ │
|
||||
│ └─────────┘ └─────────┘ └─────────┘ │
|
||||
│ │
|
||||
│ 2. Verificación OTP │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ User │ ────── │ API │ ────── │ JWT │ │
|
||||
│ │ (code) │ │(verify) │ │(access+ │ │
|
||||
│ └─────────┘ └─────────┘ │refresh) │ │
|
||||
│ └─────────┘ │
|
||||
│ │
|
||||
│ 3. Acceso rápido (después de login inicial) │
|
||||
│ - PIN de 4-6 dígitos │
|
||||
│ - Biométrico (huella/face) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Multi-tenancy
|
||||
|
||||
- **Row Level Security (RLS)** en PostgreSQL
|
||||
- `tenant_id` en todas las tablas
|
||||
- Context variable: `app.current_tenant_id`
|
||||
- Middleware que extrae tenant del JWT
|
||||
|
||||
### Encriptación
|
||||
|
||||
- TLS 1.3 para tráfico
|
||||
- AES-256 para datos sensibles en reposo
|
||||
- Bcrypt para PINs
|
||||
|
||||
## Infraestructura (Producción)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ INFRAESTRUCTURA AWS │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ CloudFront │ │
|
||||
│ │ (CDN + WAF + SSL) │ │
|
||||
│ └─────────────────────────────┬───────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────┴───────────────────────────────────┐ │
|
||||
│ │ Application Load Balancer │ │
|
||||
│ └─────────────────────────────┬───────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────────────────┴───────────────────────────────────┐ │
|
||||
│ │ ECS Fargate │ │
|
||||
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
|
||||
│ │ │ Backend │ │ MCP │ │ WhatsApp │ │ Web │ │ │
|
||||
│ │ │ API │ │ Server │ │ Service │ │ (Static) │ │ │
|
||||
│ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ RDS │ │ ElastiCache │ │ S3 │ │
|
||||
│ │ PostgreSQL │ │ Redis │ │ Bucket │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Performance y Escalabilidad
|
||||
|
||||
### Estrategias
|
||||
|
||||
1. **Caching agresivo**
|
||||
- Redis para sesiones y datos frecuentes
|
||||
- React Query en clientes
|
||||
|
||||
2. **Lazy loading**
|
||||
- Cargar productos por páginas
|
||||
- Imágenes con placeholder
|
||||
|
||||
3. **Offline-first**
|
||||
- SQLite local en app móvil
|
||||
- Sync queue con reintentos
|
||||
|
||||
4. **CDN**
|
||||
- Assets estáticos en CloudFront
|
||||
- Imágenes optimizadas
|
||||
|
||||
### Métricas objetivo
|
||||
|
||||
| Métrica | Objetivo |
|
||||
|---------|----------|
|
||||
| Tiempo de respuesta API (p95) | <200ms |
|
||||
| Tiempo de carga app | <3s |
|
||||
| Disponibilidad | 99.9% |
|
||||
| Sync offline | <30s después de reconexión |
|
||||
|
||||
---
|
||||
|
||||
**Versión**: 1.0.0
|
||||
**Última actualización**: 2026-01-04
|
||||
@ -0,0 +1,414 @@
|
||||
# MiChangarrito - Requerimientos Funcionales
|
||||
|
||||
## RF-001: Punto de Venta (POS)
|
||||
|
||||
### RF-001.1: Registro de Ventas
|
||||
- Agregar productos manualmente o por búsqueda
|
||||
- Escanear código de barras (cámara del celular)
|
||||
- Ajustar cantidad de productos
|
||||
- Aplicar descuentos (porcentaje o monto fijo)
|
||||
- Calcular total automáticamente
|
||||
- Mostrar desglose de productos
|
||||
|
||||
### RF-001.2: Métodos de Cobro
|
||||
- **Efectivo**: Ingresar monto recibido, calcular cambio
|
||||
- **Tarjeta Mercado Pago**: Conexión Bluetooth con terminal
|
||||
- **Tarjeta Clip**: Conexión Bluetooth con terminal
|
||||
- **CoDi**: Generar QR de cobro, esperar confirmación
|
||||
- **Transferencia SPEI**: Mostrar CLABE virtual, confirmar manual
|
||||
- **Fiado/Crédito**: Registrar deuda a cliente
|
||||
|
||||
### RF-001.3: Tickets y Comprobantes
|
||||
- Generar ticket digital (imagen/PDF)
|
||||
- Enviar por WhatsApp al cliente
|
||||
- Imprimir en impresora térmica Bluetooth (opcional)
|
||||
- Historial de tickets
|
||||
|
||||
### RF-001.4: Corte de Caja
|
||||
- Corte parcial (sin cerrar día)
|
||||
- Corte de día completo
|
||||
- Resumen por método de pago
|
||||
- Diferencia efectivo esperado vs real
|
||||
- Enviar resumen por WhatsApp
|
||||
|
||||
## RF-002: Catálogo de Productos
|
||||
|
||||
### RF-002.1: Gestión de Productos
|
||||
- Crear producto (nombre, precio, código de barras)
|
||||
- Editar producto
|
||||
- Eliminar/desactivar producto
|
||||
- Categorizar productos
|
||||
- Foto del producto (opcional)
|
||||
- Precio de compra (para calcular margen)
|
||||
|
||||
### RF-002.2: Importación de Productos
|
||||
- Desde foto (OCR de lista de precios)
|
||||
- Desde archivo Excel/CSV
|
||||
- Desde templates predefinidos (Bimbo, Coca-Cola, etc.)
|
||||
- Desde audio (transcripción y procesamiento IA)
|
||||
- Copia de otro negocio similar (con permiso)
|
||||
|
||||
### RF-002.3: Templates de Proveedores
|
||||
- Bimbo: Pan, galletas, pastelitos
|
||||
- Coca-Cola/FEMSA: Refrescos, jugos, agua
|
||||
- PepsiCo: Sabritas, Gamesa, bebidas
|
||||
- Modelo/Corona: Cervezas (donde aplique)
|
||||
- Ricolino: Dulces, chocolates
|
||||
- Genéricos: Productos básicos de abarrotes
|
||||
- Giros específicos: Taquería, fonda, nevería
|
||||
|
||||
### RF-002.4: Variantes y Presentaciones
|
||||
- Mismo producto, diferentes tamaños
|
||||
- Precios por presentación
|
||||
- Código de barras por variante
|
||||
|
||||
## RF-003: Inventario
|
||||
|
||||
### RF-003.1: Control de Stock
|
||||
- Stock actual por producto
|
||||
- Stock mínimo configurable
|
||||
- Alertas de stock bajo (WhatsApp y push)
|
||||
- Historial de movimientos
|
||||
|
||||
### RF-003.2: Entradas de Inventario
|
||||
- Registro de compras a proveedor
|
||||
- Entrada desde foto de nota/factura (OCR)
|
||||
- Ajustes manuales (merma, robo, etc.)
|
||||
|
||||
### RF-003.3: Predicción de Resurtido
|
||||
- Análisis de ventas históricas
|
||||
- Sugerencia de cuánto comprar
|
||||
- Alertas proactivas: "En 3 días se te acaba la Coca"
|
||||
|
||||
## RF-004: Sistema de Fiados/Crédito
|
||||
|
||||
### RF-004.1: Registro de Fiados
|
||||
- Fiar a cliente identificado
|
||||
- Monto máximo de crédito por cliente
|
||||
- Fecha límite de pago (opcional)
|
||||
- Notas del fiado
|
||||
|
||||
### RF-004.2: Gestión de Cobros
|
||||
- Lista de fiados pendientes por cliente
|
||||
- Total de fiados del negocio
|
||||
- Marcar como pagado (parcial o total)
|
||||
- Recordatorios automáticos por WhatsApp al cliente
|
||||
|
||||
### RF-004.3: Historial
|
||||
- Historial de fiados por cliente
|
||||
- Clientes morosos
|
||||
- Reporte de fiados
|
||||
|
||||
## RF-005: Clientes
|
||||
|
||||
### RF-005.1: Registro de Clientes
|
||||
- Nombre, teléfono (WhatsApp)
|
||||
- Dirección (para entregas)
|
||||
- Notas
|
||||
- Registro automático desde WhatsApp
|
||||
|
||||
### RF-005.2: Historial de Cliente
|
||||
- Compras realizadas
|
||||
- Fiados pendientes y pagados
|
||||
- Productos frecuentes
|
||||
- Total gastado
|
||||
|
||||
### RF-005.3: Comunicación
|
||||
- Enviar mensaje por WhatsApp
|
||||
- Notificaciones de promociones
|
||||
- Recordatorios de fiado
|
||||
|
||||
## RF-006: Pedidos de Clientes
|
||||
|
||||
### RF-006.1: Recepción de Pedidos
|
||||
- Cliente envía pedido por WhatsApp
|
||||
- LLM interpreta y estructura el pedido
|
||||
- Notificación push al dueño
|
||||
- Vista de pedidos pendientes
|
||||
|
||||
### RF-006.2: Procesamiento
|
||||
- Aceptar/rechazar pedido
|
||||
- Modificar pedido
|
||||
- Confirmar disponibilidad de productos
|
||||
- Calcular total
|
||||
|
||||
### RF-006.3: Cobro de Pedido
|
||||
- Link de pago (Stripe)
|
||||
- Pago presencial con terminal
|
||||
- Pago en efectivo al entregar
|
||||
- CoDi
|
||||
|
||||
### RF-006.4: Entregas a Domicilio
|
||||
- Marcar pedido como "para entrega"
|
||||
- Dirección del cliente
|
||||
- Costo de envío configurable
|
||||
- Estado: preparando, en camino, entregado
|
||||
|
||||
## RF-007: Asistente IA (LLM)
|
||||
|
||||
### RF-007.1: Canales de Comunicación
|
||||
- WhatsApp (principal)
|
||||
- Chat in-app
|
||||
- Comandos de voz (futuro)
|
||||
|
||||
### RF-007.2: Capacidades para Dueño del Negocio
|
||||
- Consultar ventas: "¿Cuánto vendí hoy/esta semana/este mes?"
|
||||
- Consultar ganancias: "¿Cuál es mi utilidad de hoy?"
|
||||
- Consultar inventario: "¿Cuántas Coca-Colas tengo?"
|
||||
- Alertas: "¿Qué me falta por resurtir?"
|
||||
- Predicciones: "¿Cuándo debo comprar más pan?"
|
||||
- Registrar venta por chat: "Vendí 2 refrescos y una torta"
|
||||
- Agregar producto: "Agrega un producto nuevo: Doritos a $18"
|
||||
- Generar reportes: "Mándame el reporte de la semana"
|
||||
- Configurar: "Cambia el precio de la Coca a $22"
|
||||
|
||||
### RF-007.3: Capacidades para Clientes
|
||||
- Identificación: Detectar que es cliente (no dueño)
|
||||
- Consultar productos: "¿Tienen refrescos?"
|
||||
- Hacer pedido: "Quiero 3 tacos y una Coca"
|
||||
- Preguntar precios: "¿A cómo el kilo de tortilla?"
|
||||
- Estado de pedido: "¿Ya está mi pedido?"
|
||||
- Horarios: "¿A qué hora abren?"
|
||||
|
||||
### RF-007.4: MCP Server - Herramientas
|
||||
```
|
||||
TOOLS DISPONIBLES:
|
||||
|
||||
# Ventas
|
||||
- registrar_venta(productos, metodo_pago, cliente?)
|
||||
- obtener_ventas(periodo, filtros?)
|
||||
- generar_corte_caja()
|
||||
- calcular_ganancias(periodo)
|
||||
|
||||
# Productos
|
||||
- buscar_producto(query)
|
||||
- crear_producto(datos)
|
||||
- actualizar_producto(id, datos)
|
||||
- obtener_precio(producto)
|
||||
|
||||
# Inventario
|
||||
- consultar_stock(producto?)
|
||||
- registrar_entrada(productos)
|
||||
- obtener_alertas_stock()
|
||||
- predecir_resurtido(producto?)
|
||||
|
||||
# Clientes
|
||||
- buscar_cliente(telefono)
|
||||
- crear_cliente(datos)
|
||||
- historial_cliente(cliente_id)
|
||||
- enviar_mensaje_cliente(cliente_id, mensaje)
|
||||
|
||||
# Fiados
|
||||
- registrar_fiado(cliente_id, monto, descripcion)
|
||||
- obtener_fiados(cliente_id?)
|
||||
- marcar_fiado_pagado(fiado_id, monto?)
|
||||
|
||||
# Pedidos
|
||||
- crear_pedido(cliente_id, productos)
|
||||
- obtener_pedidos(estado?)
|
||||
- actualizar_pedido(pedido_id, estado)
|
||||
- enviar_link_pago(pedido_id)
|
||||
|
||||
# Reportes
|
||||
- generar_reporte(tipo, periodo, formato)
|
||||
- enviar_reporte_whatsapp(reporte, destino)
|
||||
- exportar_excel(datos)
|
||||
|
||||
# Configuración
|
||||
- obtener_configuracion(clave?)
|
||||
- actualizar_configuracion(clave, valor)
|
||||
- obtener_horarios()
|
||||
- actualizar_horarios(horarios)
|
||||
```
|
||||
|
||||
## RF-008: WhatsApp Business
|
||||
|
||||
### RF-008.1: Configuración de Número
|
||||
- **Opción A**: WhatsApp Business propio del negocio
|
||||
- Conexión vía Meta Business API
|
||||
- Verificación del número
|
||||
|
||||
- **Opción B**: Número compartido de la plataforma
|
||||
- Un número para múltiples negocios
|
||||
- Detección de negocio por contexto
|
||||
- "Hola, busco [Nombre Negocio]"
|
||||
|
||||
### RF-008.2: Detección de Rol
|
||||
- Identificar si es dueño o cliente
|
||||
- Dueño: Acceso completo a gestión
|
||||
- Cliente: Solo consultas y pedidos
|
||||
|
||||
### RF-008.3: Mensajes Automatizados
|
||||
- Bienvenida
|
||||
- Fuera de horario
|
||||
- Confirmación de pedido
|
||||
- Recordatorio de fiado
|
||||
- Alertas de inventario
|
||||
|
||||
### RF-008.4: Multimedia
|
||||
- Recibir y procesar fotos (OCR)
|
||||
- Recibir y transcribir audios
|
||||
- Enviar tickets/reportes como imagen
|
||||
- Enviar documentos PDF
|
||||
|
||||
## RF-009: Pagos y Suscripciones
|
||||
|
||||
### RF-009.1: Suscripción del Negocio
|
||||
- Plan Changarrito ($99/mes)
|
||||
- Plan Tiendita ($199/mes)
|
||||
- Cambio de plan
|
||||
- Cancelación
|
||||
|
||||
### RF-009.2: Métodos de Pago de Suscripción
|
||||
- Tarjeta (Stripe) - Recurrente
|
||||
- OXXO (Stripe) - Referencia única
|
||||
- Play Store In-App Purchase
|
||||
- App Store In-App Purchase
|
||||
|
||||
### RF-009.3: Paquetes de Tokens IA
|
||||
- Compra de paquetes
|
||||
- Visualización de saldo
|
||||
- Historial de consumo
|
||||
- Alertas de saldo bajo
|
||||
|
||||
### RF-009.4: Facturación (Opcional)
|
||||
- Solicitar factura
|
||||
- Datos fiscales del negocio
|
||||
- Descarga de facturas
|
||||
|
||||
## RF-010: Reportes y Analytics
|
||||
|
||||
### RF-010.1: Dashboard Web
|
||||
- Ventas del día/semana/mes
|
||||
- Gráficas de tendencia
|
||||
- Productos más vendidos
|
||||
- Comparativas de periodos
|
||||
|
||||
### RF-010.2: Reportes Generables
|
||||
- Ventas por periodo
|
||||
- Inventario actual
|
||||
- Productos por agotarse
|
||||
- Fiados pendientes
|
||||
- Ganancias y márgenes
|
||||
- Cortes de caja históricos
|
||||
|
||||
### RF-010.3: Formatos de Exportación
|
||||
- PDF
|
||||
- Excel
|
||||
- Imagen (para WhatsApp)
|
||||
|
||||
## RF-011: Notificaciones
|
||||
|
||||
### RF-011.1: Push Notifications
|
||||
- Nuevo pedido de cliente
|
||||
- Fiado por vencer
|
||||
- Stock bajo
|
||||
- Pago recibido
|
||||
|
||||
### RF-011.2: WhatsApp
|
||||
- Resumen diario de ventas
|
||||
- Alertas de inventario
|
||||
- Recordatorios configurables
|
||||
|
||||
### RF-011.3: Configuración
|
||||
- Activar/desactivar por tipo
|
||||
- Horarios de no molestar
|
||||
- Frecuencia
|
||||
|
||||
## RF-012: Modo Offline
|
||||
|
||||
### RF-012.1: Funcionalidad Sin Internet
|
||||
- Registrar ventas
|
||||
- Consultar productos y precios
|
||||
- Agregar productos al carrito
|
||||
- Ver historial local
|
||||
|
||||
### RF-012.2: Sincronización
|
||||
- Automática al recuperar conexión
|
||||
- Manual forzada
|
||||
- Resolución de conflictos
|
||||
- Indicador de datos pendientes
|
||||
|
||||
## RF-013: Integraciones de Terminal
|
||||
|
||||
### RF-013.1: Mercado Pago
|
||||
- Pareado Bluetooth con terminal
|
||||
- Envío de monto a cobrar
|
||||
- Recepción de confirmación
|
||||
- Manejo de errores/rechazos
|
||||
|
||||
### RF-013.2: Clip
|
||||
- Pareado Bluetooth con terminal
|
||||
- Envío de monto a cobrar
|
||||
- Recepción de confirmación
|
||||
- Manejo de errores/rechazos
|
||||
|
||||
### RF-013.3: CoDi
|
||||
- Generación de QR
|
||||
- Polling de confirmación
|
||||
- Timeout y reintentos
|
||||
|
||||
## RF-014: Onboarding
|
||||
|
||||
### RF-014.1: Registro de Negocio
|
||||
- Nombre del negocio
|
||||
- Giro (abarrotes, comida, etc.)
|
||||
- Ubicación (colonia, ciudad)
|
||||
- Teléfono del dueño
|
||||
|
||||
### RF-014.2: Configuración Inicial Guiada
|
||||
- Vía WhatsApp con LLM
|
||||
- Paso a paso en app
|
||||
- Cargar productos iniciales
|
||||
- Seleccionar templates
|
||||
|
||||
### RF-014.3: Tutorial
|
||||
- Primeros pasos interactivo
|
||||
- Videos cortos
|
||||
- Tips por WhatsApp los primeros días
|
||||
|
||||
## RF-015: Seguridad y Acceso
|
||||
|
||||
### RF-015.1: Autenticación
|
||||
- Login con teléfono + OTP (SMS/WhatsApp)
|
||||
- PIN de acceso rápido
|
||||
- Biométrico (huella/face)
|
||||
|
||||
### RF-015.2: Roles (Futuro)
|
||||
- Dueño: Acceso total
|
||||
- Empleado: Solo ventas
|
||||
- Contador: Solo reportes
|
||||
|
||||
### RF-015.3: Datos
|
||||
- Encriptación en tránsito (HTTPS)
|
||||
- Encriptación en reposo
|
||||
- Backups automáticos
|
||||
|
||||
## RF-016: Programa de Referidos
|
||||
|
||||
### RF-016.1: Mecánica
|
||||
- Código/link único por negocio
|
||||
- Beneficio por referido: 1 mes gratis (ambos)
|
||||
- Tracking de referidos
|
||||
|
||||
### RF-016.2: Comunicación
|
||||
- Compartir por WhatsApp
|
||||
- Ver referidos activos
|
||||
- Historial de beneficios
|
||||
|
||||
## RF-017: Soporte
|
||||
|
||||
### RF-017.1: Autoservicio
|
||||
- FAQ en app
|
||||
- Videos tutoriales
|
||||
- Asistente IA para dudas
|
||||
|
||||
### RF-017.2: Humano (Escalación)
|
||||
- Chat con soporte (cuando IA no resuelve)
|
||||
- Teléfono de emergencia
|
||||
- Horario de atención
|
||||
|
||||
---
|
||||
|
||||
**Versión**: 1.0.0
|
||||
**Última actualización**: 2026-01-04
|
||||
@ -0,0 +1,290 @@
|
||||
# MiChangarrito - Visión del Proyecto
|
||||
|
||||
## El Problema
|
||||
|
||||
En México existen **más de 1.2 millones de tienditas, changarros y puestos de comida** que operan de manera informal. Estos negocios enfrentan:
|
||||
|
||||
- **Rechazo de clientes** que solo traen tarjeta
|
||||
- **Descontrol de inventario** - No saben cuándo resurtir
|
||||
- **Falta de visibilidad** - No conocen sus ganancias reales
|
||||
- **Tecnología intimidante** - Sistemas complicados que no entienden
|
||||
- **Costos altos** - Terminales caras y sistemas con rentas elevadas
|
||||
|
||||
## La Solución: MiChangarrito
|
||||
|
||||
Un **punto de venta inteligente y accesible** diseñado específicamente para el mercado informal mexicano, donde el usuario puede operar principalmente a través de **WhatsApp** con ayuda de un asistente de IA.
|
||||
|
||||
### Propuesta de Valor
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ "TU NEGOCIO EN TU BOLSILLO, TAN FÁCIL COMO MANDAR UN WHATSAPP" │
|
||||
│ │
|
||||
│ ✅ Acepta tarjetas (Mercado Pago, Clip, CoDi) │
|
||||
│ ✅ Controla tu inventario con una foto │
|
||||
│ ✅ Pregúntale a tu asistente cuánto vendiste │
|
||||
│ ✅ Recibe pedidos de clientes por WhatsApp │
|
||||
│ ✅ Precio justo: $100-200/mes + lo que uses de IA │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Filosofía de Diseño
|
||||
|
||||
### "WhatsApp First, App Second"
|
||||
|
||||
La mayoría de nuestros usuarios:
|
||||
- Son mayores de 40 años
|
||||
- Tienen educación básica o media
|
||||
- Usan WhatsApp todos los días
|
||||
- Les intimida instalar apps nuevas
|
||||
- Prefieren hablar/escribir que navegar menús
|
||||
|
||||
**Por eso**: El LLM vía WhatsApp es la interfaz principal. La app es solo para acciones que requieren la pantalla (cobrar con terminal, escanear códigos).
|
||||
|
||||
### "Botones Grandes, Pocos Pasos"
|
||||
|
||||
Cuando sí usen la app:
|
||||
- Máximo 2 taps para completar una acción
|
||||
- Texto grande y legible
|
||||
- Iconos claros y universales
|
||||
- Modo "Abuelito" activable
|
||||
|
||||
### "Si no entiendes, pregunta"
|
||||
|
||||
El asistente IA siempre está disponible para:
|
||||
- Explicar cómo hacer algo
|
||||
- Hacer la acción por el usuario
|
||||
- Generar reportes y documentos
|
||||
- Recordar pendientes
|
||||
|
||||
## Mercado Objetivo
|
||||
|
||||
### Primario: Micro-negocios Informales México
|
||||
|
||||
| Segmento | Características | Tamaño Estimado |
|
||||
|----------|-----------------|-----------------|
|
||||
| Tiendas de abarrotes | 1-2 personas, familiar | 500,000+ |
|
||||
| Puestos de comida | Tacos, tortas, comida corrida | 300,000+ |
|
||||
| Fondas y cocinas | Comida económica | 150,000+ |
|
||||
| Vendedores ambulantes | Dulces, snacks, bebidas | 200,000+ |
|
||||
| Otros changarros | Papelerías, estéticas pequeñas | 100,000+ |
|
||||
|
||||
### Perfil del Usuario Principal
|
||||
|
||||
```
|
||||
👤 "Doña Mary" - 52 años
|
||||
- Tienda de abarrotes en colonia popular
|
||||
- Usa WhatsApp para comunicarse con familia y proveedores
|
||||
- Tiene smartphone Android básico
|
||||
- Anota ventas en libreta (a veces)
|
||||
- Pierde clientes que solo traen tarjeta
|
||||
- No sabe exactamente cuánto gana al mes
|
||||
- Le da "fiao" a vecinos y a veces olvida
|
||||
- Quiere algo simple, no un sistema complicado
|
||||
```
|
||||
|
||||
### Secundario: Expansión LATAM
|
||||
|
||||
Misma problemática en:
|
||||
- Guatemala, Honduras, El Salvador
|
||||
- Colombia, Perú, Ecuador
|
||||
- Adaptaciones locales necesarias (idioma, proveedores, pagos)
|
||||
|
||||
## Modelo de Negocio
|
||||
|
||||
### Pricing
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ PLANES MICHANGARRITO │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 🏪 PLAN CHANGARRITO $99/mes │
|
||||
│ - App móvil completa │
|
||||
│ - Dashboard web │
|
||||
│ - Integraciones de pago (MP, Clip, CoDi) │
|
||||
│ - 500 tokens IA incluidos │
|
||||
│ - WhatsApp compartido │
|
||||
│ │
|
||||
│ 🏬 PLAN TIENDITA $199/mes │
|
||||
│ - Todo lo del Plan Changarrito │
|
||||
│ - 2,000 tokens IA incluidos │
|
||||
│ - WhatsApp Business propio (si tiene) │
|
||||
│ - Reportes avanzados │
|
||||
│ - Pedidos a domicilio │
|
||||
│ - Soporte prioritario │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ PAQUETES DE TOKENS IA │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 🪙 Recarga Chica 1,000 tokens $29 │
|
||||
│ 🪙 Recarga Mediana 3,000 tokens $69 │
|
||||
│ 🪙 Recarga Grande 8,000 tokens $149 │
|
||||
│ 🪙 Recarga Mega 20,000 tokens $299 │
|
||||
│ │
|
||||
│ * Similar a recargas de celular │
|
||||
│ * Se pueden comprar en OXXO con referencia │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Métodos de Pago de Suscripción
|
||||
|
||||
| Método | Descripción | Comisión |
|
||||
|--------|-------------|----------|
|
||||
| OXXO (referencia) | Pago en efectivo, el más usado | ~$10-15 |
|
||||
| Tarjeta (Stripe) | Débito/crédito recurrente | 3.6% + $3 |
|
||||
| Play Store | Suscripción in-app Android | 15-30% |
|
||||
| App Store | Suscripción in-app iOS | 15-30% |
|
||||
|
||||
### Proyección de Ingresos (Año 1)
|
||||
|
||||
```
|
||||
Mes 1-3: 100 usuarios → $15,000/mes
|
||||
Mes 4-6: 500 usuarios → $75,000/mes
|
||||
Mes 7-9: 2,000 usuarios → $300,000/mes
|
||||
Mes 10-12: 5,000 usuarios → $750,000/mes
|
||||
|
||||
+ Ingresos por tokens IA (estimado 30% adicional)
|
||||
```
|
||||
|
||||
## Diferenciadores Clave
|
||||
|
||||
### vs Competencia (Clip, iZettle, Square)
|
||||
|
||||
| Característica | Competencia | MiChangarrito |
|
||||
|----------------|-------------|---------------|
|
||||
| Precio mensual | $0-299 | $99-199 |
|
||||
| Comisión por transacción | 3.5%+ | Solo terminal |
|
||||
| Asistente IA | ❌ | ✅ WhatsApp 24/7 |
|
||||
| Onboarding | App compleja | Foto/audio/chat |
|
||||
| Inventario | Básico o ninguno | Inteligente con alertas |
|
||||
| Fiados/Crédito | ❌ | ✅ Digital |
|
||||
| Pedidos WhatsApp | ❌ | ✅ Integrado |
|
||||
| Modo offline | Limitado | ✅ Completo |
|
||||
| Reportes por chat | ❌ | ✅ "¿Cuánto vendí hoy?" |
|
||||
|
||||
## Roadmap de Alto Nivel
|
||||
|
||||
### Fase 1: MVP (3-4 meses)
|
||||
- App móvil básica (ventas, cobros)
|
||||
- Integración Mercado Pago y Clip
|
||||
- WhatsApp básico con LLM
|
||||
- Dashboard web simple
|
||||
|
||||
### Fase 2: Inteligencia (2-3 meses)
|
||||
- Onboarding con fotos/OCR
|
||||
- Templates de proveedores
|
||||
- Alertas de inventario
|
||||
- Sistema de fiados
|
||||
|
||||
### Fase 3: Crecimiento (2-3 meses)
|
||||
- Pedidos de clientes vía WhatsApp
|
||||
- CoDi y transferencias
|
||||
- Modo offline completo
|
||||
- Programa de referidos
|
||||
|
||||
### Fase 4: Expansión (3+ meses)
|
||||
- Integración SAT (opcional)
|
||||
- Multi-idioma (LATAM)
|
||||
- Marketplace de proveedores
|
||||
- Créditos para negocio
|
||||
|
||||
## Tecnología
|
||||
|
||||
### Stack Principal
|
||||
|
||||
| Componente | Tecnología |
|
||||
|------------|------------|
|
||||
| App Móvil | React Native |
|
||||
| Web Dashboard | React + Vite |
|
||||
| Backend API | NestJS (TypeScript) |
|
||||
| Base de Datos | PostgreSQL (multi-tenant) |
|
||||
| Cache/Sessions | Redis |
|
||||
| LLM Gateway | MCP Server (agnóstico) |
|
||||
| WhatsApp | Meta Business API |
|
||||
| Pagos | Stripe, Mercado Pago SDK, Clip API |
|
||||
| Push Notifications | Firebase Cloud Messaging |
|
||||
| Storage | S3/MinIO |
|
||||
|
||||
### LLM - Diseño Agnóstico
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ LLM GATEWAY (MCP Server) │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Interfaz unificada que soporta múltiples proveedores: │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ OpenAI │ │ Claude │ │ OpenRouter │ │ Ollama │ │
|
||||
│ │ GPT-4o │ │ Sonnet │ │ (Barato) │ │ (Local) │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ └────────────────┴────────────────┴────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ MCP Server │ │
|
||||
│ │ (Agnóstico) │ │
|
||||
│ └─────────────────┘ │
|
||||
│ │
|
||||
│ Permite cambiar de proveedor según: │
|
||||
│ - Costos (OpenRouter suele ser más barato) │
|
||||
│ - Disponibilidad │
|
||||
│ - Calidad de respuesta por caso de uso │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Métricas de Éxito
|
||||
|
||||
### KPIs Principales
|
||||
|
||||
| Métrica | Meta Año 1 |
|
||||
|---------|------------|
|
||||
| Usuarios activos mensuales | 5,000 |
|
||||
| Retención mensual | >70% |
|
||||
| NPS | >50 |
|
||||
| Transacciones procesadas | 500,000 |
|
||||
| Ingresos recurrentes mensuales | $750,000 MXN |
|
||||
|
||||
### KPIs de Producto
|
||||
|
||||
| Métrica | Meta |
|
||||
|---------|------|
|
||||
| Tiempo de onboarding | <5 minutos |
|
||||
| Mensajes WhatsApp por usuario/día | 3-5 |
|
||||
| Uso de IA vs app directa | 60% / 40% |
|
||||
| Tasa de resolución IA | >85% |
|
||||
|
||||
## Riesgos y Mitigaciones
|
||||
|
||||
| Riesgo | Probabilidad | Impacto | Mitigación |
|
||||
|--------|--------------|---------|------------|
|
||||
| Adopción lenta | Media | Alto | Programa de referidos, prueba gratis |
|
||||
| Costos de IA altos | Media | Medio | Multi-proveedor, cache de respuestas |
|
||||
| Competencia de grandes | Alta | Medio | Enfoque en UX para mercado informal |
|
||||
| Regulación WhatsApp | Baja | Alto | Alternativa in-app como backup |
|
||||
| Fraude/chargebacks | Media | Medio | Verificación de negocios |
|
||||
|
||||
## Equipo Necesario
|
||||
|
||||
### Fase MVP
|
||||
|
||||
| Rol | Cantidad |
|
||||
|-----|----------|
|
||||
| Full-stack developer | 2 |
|
||||
| Mobile developer (RN) | 1 |
|
||||
| DevOps/Backend senior | 1 |
|
||||
| UX/UI Designer | 1 |
|
||||
| Product Manager | 1 |
|
||||
|
||||
---
|
||||
|
||||
**Documento vivo** - Última actualización: 2026-01-04
|
||||
**Autor**: Product Team
|
||||
**Versión**: 1.0.0
|
||||
@ -0,0 +1,97 @@
|
||||
# MCH-001: Infraestructura Base
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-001
|
||||
- **Fase:** 1 - MVP Core
|
||||
- **Prioridad:** P0
|
||||
- **Estado:** Completado
|
||||
- **Fecha inicio:** 2026-01-04
|
||||
- **Fecha fin:** 2026-01-05
|
||||
|
||||
## Descripcion
|
||||
|
||||
Setup inicial del proyecto MiChangarrito incluyendo estructura monorepo, configuracion de base de datos PostgreSQL multi-tenant, pipelines CI/CD, y entornos de desarrollo.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Establecer estructura de proyecto monorepo
|
||||
2. Configurar PostgreSQL con multi-tenant (RLS)
|
||||
3. Setup de entornos de desarrollo
|
||||
4. Configurar CI/CD basico
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Estructura de carpetas monorepo (apps/, database/, docs/)
|
||||
- PostgreSQL con schemas separados
|
||||
- Scripts de recreacion de BD
|
||||
- Docker Compose para desarrollo
|
||||
- GitHub Actions basico
|
||||
|
||||
### Excluido
|
||||
- Deployment a produccion
|
||||
- Kubernetes (futuro)
|
||||
- Monitoring avanzado
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
michangarrito/
|
||||
├── apps/
|
||||
│ ├── backend/ # NestJS API
|
||||
│ ├── web/ # React Dashboard
|
||||
│ ├── mobile/ # Expo App
|
||||
│ ├── mcp-server/ # Gateway LLM
|
||||
│ └── whatsapp-service/ # Bot WhatsApp
|
||||
├── database/
|
||||
│ ├── schemas/ # DDL files
|
||||
│ ├── seeds/ # Data inicial
|
||||
│ └── *.sh # Scripts
|
||||
├── docs/
|
||||
└── orchestration/
|
||||
```
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo/Ubicacion |
|
||||
|------------|--------|-------------------|
|
||||
| Estructura monorepo | Completado | `projects/michangarrito/` |
|
||||
| DDL base PostgreSQL | Completado | `database/schemas/00-02.sql` |
|
||||
| Scripts BD | Completado | `database/*.sh` |
|
||||
| Docker Compose | Completado | `docker-compose.yml` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- Ninguna (epica inicial)
|
||||
|
||||
### Bloquea a
|
||||
- MCH-002 (Auth)
|
||||
- MCH-003 (Productos)
|
||||
- MCH-010 (MCP Server)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] Estructura de carpetas creada
|
||||
- [x] PostgreSQL ejecutandose con schemas
|
||||
- [x] Script drop-and-recreate funcional
|
||||
- [x] Extensiones uuid-ossp, pgcrypto habilitadas
|
||||
- [x] Funcion current_tenant_id() operativa
|
||||
|
||||
## Notas Tecnicas
|
||||
|
||||
- **Puerto PostgreSQL:** 5432
|
||||
- **Puerto Redis:** 6379
|
||||
- **Base de datos:** michangarrito_platform
|
||||
- **Multi-tenant:** Via tenant_id + RLS
|
||||
|
||||
## Historias de Usuario Relacionadas
|
||||
|
||||
| ID | Historia | Estado |
|
||||
|----|----------|--------|
|
||||
| US-001 | Como DevOps, quiero poder recrear la BD facilmente | Completado |
|
||||
| US-002 | Como Dev, quiero estructura clara de proyecto | Completado |
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,137 @@
|
||||
# MCH-002: Autenticacion
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-002
|
||||
- **Fase:** 1 - MVP Core
|
||||
- **Prioridad:** P0
|
||||
- **Estado:** Completado
|
||||
- **Fecha inicio:** 2026-01-05
|
||||
- **Fecha fin:** 2026-01-06
|
||||
|
||||
## Descripcion
|
||||
|
||||
Sistema de autenticacion adaptado a micro-negocios mexicanos: login via telefono con OTP (SMS/WhatsApp), PIN de 4 digitos para acceso rapido, soporte biometrico opcional, y JWT para sesiones.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Login via telefono + OTP
|
||||
2. PIN de 4 digitos para acceso rapido
|
||||
3. Soporte biometrico (Face ID/huella)
|
||||
4. Gestion de sesiones JWT
|
||||
5. Roles: owner, employee, viewer
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Registro con telefono
|
||||
- OTP via SMS/WhatsApp
|
||||
- PIN de 4 digitos
|
||||
- JWT con refresh tokens
|
||||
- Roles basicos (owner/employee/viewer)
|
||||
- Logout y revocacion de sesiones
|
||||
|
||||
### Excluido
|
||||
- OAuth (Google, Facebook) - fase posterior
|
||||
- 2FA via TOTP - fase posterior
|
||||
- SSO empresarial
|
||||
|
||||
## Flujos de Usuario
|
||||
|
||||
### Registro Inicial
|
||||
```
|
||||
1. Usuario ingresa telefono
|
||||
2. Se envia OTP via SMS/WhatsApp
|
||||
3. Usuario verifica OTP
|
||||
4. Usuario configura PIN de 4 digitos
|
||||
5. Se crea tenant automaticamente (para owners)
|
||||
6. Usuario accede al dashboard
|
||||
```
|
||||
|
||||
### Login Subsecuente
|
||||
```
|
||||
1. Usuario ingresa telefono
|
||||
2. Usuario ingresa PIN de 4 digitos
|
||||
- O usa biometrico si esta configurado
|
||||
3. JWT generado
|
||||
4. Acceso al sistema
|
||||
```
|
||||
|
||||
### Login por OTP (sin PIN)
|
||||
```
|
||||
1. Usuario selecciona "Olvide mi PIN"
|
||||
2. Se envia OTP
|
||||
3. Usuario verifica OTP
|
||||
4. Puede reconfigurrar PIN
|
||||
```
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas (schema: auth)
|
||||
- `users`: id, tenant_id, phone, email, password_hash, name, role, pin_hash, status
|
||||
- `sessions`: id, user_id, token, device_info, expires_at, revoked_at
|
||||
- `roles`: id, tenant_id, name, permissions (JSONB)
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| POST | /auth/register | Registro nuevo usuario |
|
||||
| POST | /auth/send-otp | Enviar OTP a telefono |
|
||||
| POST | /auth/verify-otp | Verificar OTP |
|
||||
| POST | /auth/set-pin | Configurar PIN |
|
||||
| POST | /auth/login | Login con telefono + PIN |
|
||||
| POST | /auth/login-otp | Login solo con OTP |
|
||||
| GET | /auth/me | Usuario actual |
|
||||
| POST | /auth/refresh | Renovar JWT |
|
||||
| POST | /auth/logout | Cerrar sesion |
|
||||
| DELETE | /auth/sessions | Revocar todas las sesiones |
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| DDL auth schema | Completado | `04-auth.sql` |
|
||||
| auth.module.ts | Completado | `modules/auth/` |
|
||||
| JwtStrategy | Completado | `strategies/jwt.strategy.ts` |
|
||||
| Guards | Completado | `guards/` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-001 (Infraestructura)
|
||||
|
||||
### Bloquea a
|
||||
- MCH-003 (Productos)
|
||||
- MCH-004 (POS)
|
||||
- Todos los modulos que requieren auth
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] Registro con telefono funcional
|
||||
- [x] OTP se envia correctamente
|
||||
- [x] PIN de 4 digitos funciona
|
||||
- [x] JWT se genera y valida
|
||||
- [x] Roles owner/employee/viewer funcionan
|
||||
- [x] Sesiones se pueden revocar
|
||||
|
||||
## Configuracion
|
||||
|
||||
```typescript
|
||||
// JWT Config
|
||||
{
|
||||
secret: process.env.JWT_SECRET,
|
||||
expiresIn: '7d',
|
||||
refreshExpiresIn: '30d'
|
||||
}
|
||||
|
||||
// OTP Config
|
||||
{
|
||||
length: 6,
|
||||
expiresIn: '5m',
|
||||
maxAttempts: 3
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,133 @@
|
||||
# MCH-003: Catalogo de Productos
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-003
|
||||
- **Fase:** 1 - MVP Core
|
||||
- **Prioridad:** P0
|
||||
- **Estado:** Completado
|
||||
- **Fecha inicio:** 2026-01-05
|
||||
- **Fecha fin:** 2026-01-06
|
||||
|
||||
## Descripcion
|
||||
|
||||
Sistema de gestion de catalogo de productos para micro-negocios: categorias jerarquicas, productos con variantes, codigos de barras, imagenes, y precios con costo para calcular margen.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. CRUD de categorias con jerarquia
|
||||
2. CRUD de productos con atributos completos
|
||||
3. Soporte para variantes de producto
|
||||
4. Busqueda por codigo de barras
|
||||
5. Calculo automatico de margen
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Categorias con parent_id (jerarquia)
|
||||
- Productos con SKU, barcode, precio, costo
|
||||
- Variantes de producto (tallas, colores)
|
||||
- Imagenes de producto
|
||||
- Busqueda por nombre, SKU, barcode
|
||||
- Activar/desactivar productos
|
||||
|
||||
### Excluido
|
||||
- Productos compuestos (kits) - fase posterior
|
||||
- Precios por volumen - fase posterior
|
||||
- Multiples listas de precios
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas (schema: catalog)
|
||||
|
||||
**categories**
|
||||
- id, tenant_id, name, parent_id, image_url, sort_order, active
|
||||
|
||||
**products**
|
||||
- id, tenant_id, category_id, sku, name, description
|
||||
- price, cost, tax_rate, image_url, barcode
|
||||
- track_inventory, min_stock, status
|
||||
|
||||
**product_variants**
|
||||
- id, product_id, name, sku, price, attributes (JSONB)
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| GET | /categories | Listar categorias |
|
||||
| GET | /categories/:id | Obtener categoria |
|
||||
| POST | /categories | Crear categoria |
|
||||
| PUT | /categories/:id | Actualizar categoria |
|
||||
| DELETE | /categories/:id | Eliminar categoria |
|
||||
| GET | /products | Listar productos |
|
||||
| GET | /products/:id | Obtener producto |
|
||||
| POST | /products | Crear producto |
|
||||
| PUT | /products/:id | Actualizar producto |
|
||||
| DELETE | /products/:id | Eliminar producto |
|
||||
| GET | /products/search | Buscar productos |
|
||||
| GET | /products/barcode/:code | Buscar por barcode |
|
||||
|
||||
## Flujos de Usuario
|
||||
|
||||
### Crear Producto Rapido
|
||||
```
|
||||
1. Dueno abre "Nuevo Producto"
|
||||
2. Ingresa nombre y precio
|
||||
3. Opcionalmente: foto, categoria, costo
|
||||
4. Guarda
|
||||
5. Producto disponible en POS
|
||||
```
|
||||
|
||||
### Escanear Codigo de Barras
|
||||
```
|
||||
1. Dueno escanea codigo con camara
|
||||
2. Sistema busca en BD
|
||||
3. Si existe: muestra producto
|
||||
4. Si no existe: ofrece crear nuevo
|
||||
```
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| DDL catalog schema | Completado | `05-catalog.sql` |
|
||||
| categories.module | Completado | `modules/categories/` |
|
||||
| products.module | Completado | `modules/products/` |
|
||||
| Components FE | Completado | `components/products/` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-001 (Infraestructura)
|
||||
- MCH-002 (Auth)
|
||||
|
||||
### Bloquea a
|
||||
- MCH-004 (POS)
|
||||
- MCH-007 (Templates)
|
||||
- MCH-009 (Predicciones)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] CRUD categorias funcional
|
||||
- [x] CRUD productos funcional
|
||||
- [x] Variantes de producto funcionan
|
||||
- [x] Busqueda por barcode funciona
|
||||
- [x] Imagenes se guardan correctamente
|
||||
- [x] Margen se calcula (precio - costo)
|
||||
|
||||
## UI Components
|
||||
|
||||
### ProductList
|
||||
- Tabla con: imagen, nombre, precio, stock, acciones
|
||||
- Filtros por categoria, estado
|
||||
- Busqueda por nombre/SKU
|
||||
|
||||
### ProductForm
|
||||
- Campos: nombre, precio, costo, categoria
|
||||
- Upload de imagen
|
||||
- Codigo de barras (manual o escaner)
|
||||
- Toggle track_inventory
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,159 @@
|
||||
# MCH-004: Punto de Venta Basico
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-004
|
||||
- **Fase:** 1 - MVP Core
|
||||
- **Prioridad:** P0
|
||||
- **Estado:** Completado
|
||||
- **Fecha inicio:** 2026-01-06
|
||||
- **Fecha fin:** 2026-01-06
|
||||
|
||||
## Descripcion
|
||||
|
||||
Sistema de punto de venta (POS) optimizado para micro-negocios: interfaz tactil rapida, carrito de compra, multiples metodos de pago, generacion de tickets, y registro de ventas.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Grid de productos tactil
|
||||
2. Carrito con modificacion de cantidades
|
||||
3. Multiples metodos de pago
|
||||
4. Generacion de ticket/recibo
|
||||
5. Historial de ventas del dia
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Grid de productos por categoria
|
||||
- Carrito con +/- cantidad
|
||||
- Descuentos por item o total
|
||||
- Metodos: efectivo, tarjeta, fiado
|
||||
- Ticket imprimible/compartible
|
||||
- Reporte diario de ventas
|
||||
|
||||
### Excluido
|
||||
- Ventas con factura fiscal - MCH-027
|
||||
- Pagos parciales mixtos - fase posterior
|
||||
- Mesas/comandas (restaurante) - vertical separada
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas (schema: sales)
|
||||
|
||||
**sales**
|
||||
- id, tenant_id, user_id, customer_id
|
||||
- subtotal, tax, discount, total
|
||||
- payment_method, payment_reference
|
||||
- status (completed/voided), notes, created_at
|
||||
|
||||
**sale_items**
|
||||
- id, sale_id, product_id, quantity
|
||||
- unit_price, discount, total
|
||||
|
||||
**payment_methods**
|
||||
- id, tenant_id, name, type, settings, active
|
||||
|
||||
**cash_registers**
|
||||
- id, tenant_id, name, opening_balance, current_balance, status
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| GET | /sales | Listar ventas |
|
||||
| GET | /sales/:id | Obtener venta |
|
||||
| POST | /sales | Registrar venta |
|
||||
| POST | /sales/:id/void | Cancelar venta |
|
||||
| GET | /sales/daily-report | Reporte del dia |
|
||||
| GET | /sales/by-date | Ventas por rango |
|
||||
|
||||
## Flujos de Usuario
|
||||
|
||||
### Venta Rapida
|
||||
```
|
||||
1. Empleado abre POS
|
||||
2. Selecciona productos del grid
|
||||
- O escanea codigo de barras
|
||||
3. Ajusta cantidades si necesario
|
||||
4. Aplica descuento (opcional)
|
||||
5. Selecciona metodo de pago
|
||||
6. Si efectivo: ingresa monto recibido
|
||||
7. Sistema calcula cambio
|
||||
8. Genera ticket
|
||||
9. Venta registrada
|
||||
```
|
||||
|
||||
### Venta a Credito (Fiado)
|
||||
```
|
||||
1. Empleado agrega productos
|
||||
2. Selecciona cliente existente
|
||||
3. Elige "Fiado" como pago
|
||||
4. Sistema verifica limite de credito
|
||||
5. Si aprobado: registra venta
|
||||
6. Actualiza saldo del cliente
|
||||
```
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| DDL sales schema | Completado | `06-sales.sql` |
|
||||
| sales.module | Completado | `modules/sales/` |
|
||||
| POS.tsx | Completado | `pages/POS.tsx` |
|
||||
| Cart component | Completado | `components/pos/Cart.tsx` |
|
||||
| PaymentModal | Completado | `components/pos/PaymentModal.tsx` |
|
||||
| ReceiptModal | Completado | `components/pos/ReceiptModal.tsx` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-001 (Infraestructura)
|
||||
- MCH-002 (Auth)
|
||||
- MCH-003 (Productos)
|
||||
|
||||
### Bloquea a
|
||||
- MCH-005 (Pagos)
|
||||
- MCH-008 (Fiados)
|
||||
- MCH-009 (Predicciones)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] Grid de productos carga correctamente
|
||||
- [x] Carrito funciona (agregar/quitar/cantidad)
|
||||
- [x] Descuentos se aplican correctamente
|
||||
- [x] Pago en efectivo con cambio
|
||||
- [x] Ticket se genera con todos los datos
|
||||
- [x] Historial de ventas funcional
|
||||
|
||||
## UI Components
|
||||
|
||||
### POS Page
|
||||
- Grid de productos (izquierda)
|
||||
- Carrito (derecha)
|
||||
- Barra de busqueda/escaneo
|
||||
|
||||
### ProductGrid
|
||||
- Productos como cards con imagen
|
||||
- Categorias como tabs
|
||||
- Click para agregar al carrito
|
||||
|
||||
### Cart
|
||||
- Lista de items con cantidad
|
||||
- Botones +/- por item
|
||||
- Total, descuento, impuesto
|
||||
- Boton "Cobrar"
|
||||
|
||||
### PaymentModal
|
||||
- Metodos de pago disponibles
|
||||
- Campo monto recibido (efectivo)
|
||||
- Calculo de cambio
|
||||
- Boton confirmar
|
||||
|
||||
### ReceiptModal
|
||||
- Datos del negocio
|
||||
- Lista de productos
|
||||
- Totales
|
||||
- Botones: imprimir, compartir, cerrar
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,170 @@
|
||||
# MCH-005: Integraciones de Pago
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-005
|
||||
- **Fase:** 1 - MVP Core
|
||||
- **Prioridad:** P0
|
||||
- **Estado:** Completado
|
||||
- **Fecha inicio:** 2026-01-06
|
||||
- **Fecha fin:** 2026-01-07
|
||||
|
||||
## Descripcion
|
||||
|
||||
Integracion con proveedores de pago populares en Mexico para aceptar pagos con tarjeta: Mercado Pago (lector bluetooth), Clip, y efectivo. Soporte para suscripciones via Stripe.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Integracion Mercado Pago Point
|
||||
2. Integracion Clip
|
||||
3. Registro de pagos en efectivo
|
||||
4. Stripe para suscripciones
|
||||
5. Conciliacion de pagos
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Mercado Pago Point (lector bluetooth)
|
||||
- Clip (terminal)
|
||||
- Efectivo (registro manual)
|
||||
- Stripe (suscripciones y OXXO)
|
||||
- Webhooks para confirmacion
|
||||
|
||||
### Excluido
|
||||
- CoDi/SPEI - MCH-024
|
||||
- Pagos QR propios - fase posterior
|
||||
- Terminales bancarias tradicionales
|
||||
|
||||
## Proveedores
|
||||
|
||||
### Mercado Pago
|
||||
- **Uso:** Pagos con tarjeta en tienda
|
||||
- **Hardware:** Point Bluetooth
|
||||
- **Comision:** ~3.5%
|
||||
- **Integracion:** SDK + Webhooks
|
||||
|
||||
### Clip
|
||||
- **Uso:** Pagos con tarjeta en tienda
|
||||
- **Hardware:** Terminal Clip
|
||||
- **Comision:** ~3.6%
|
||||
- **Integracion:** SDK + Webhooks
|
||||
|
||||
### Stripe
|
||||
- **Uso:** Suscripciones, pagos en linea, OXXO
|
||||
- **Comision:** ~3.6% + $3 MXN
|
||||
- **Integracion:** API + Webhooks
|
||||
|
||||
### Efectivo
|
||||
- **Uso:** Pagos en efectivo
|
||||
- **Registro:** Manual en POS
|
||||
- **Control:** Corte de caja
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| POST | /payments/intent | Crear intencion de pago |
|
||||
| POST | /payments/confirm | Confirmar pago |
|
||||
| GET | /payments/:id | Obtener pago |
|
||||
| POST | /payments/webhook | Webhook de proveedores |
|
||||
| GET | /payments/methods | Metodos disponibles |
|
||||
|
||||
## Flujos de Pago
|
||||
|
||||
### Pago con Tarjeta (MercadoPago/Clip)
|
||||
```
|
||||
1. Venta creada en POS
|
||||
2. Empleado selecciona "Tarjeta"
|
||||
3. Se envia monto a terminal
|
||||
4. Cliente pasa tarjeta
|
||||
5. Webhook confirma pago
|
||||
6. Venta marcada como pagada
|
||||
7. Ticket generado
|
||||
```
|
||||
|
||||
### Pago en Efectivo
|
||||
```
|
||||
1. Venta creada en POS
|
||||
2. Empleado selecciona "Efectivo"
|
||||
3. Ingresa monto recibido
|
||||
4. Sistema calcula cambio
|
||||
5. Venta registrada
|
||||
6. Actualiza caja
|
||||
```
|
||||
|
||||
### Pago OXXO (Stripe)
|
||||
```
|
||||
1. Cliente solicita pago en OXXO
|
||||
2. Sistema genera referencia Stripe
|
||||
3. Se muestra codigo de barras
|
||||
4. Cliente paga en OXXO
|
||||
5. Webhook confirma (24-48h)
|
||||
6. Pedido/suscripcion activada
|
||||
```
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas
|
||||
|
||||
**payments** (en sales schema)
|
||||
- id, sale_id, provider, amount
|
||||
- reference, status, metadata
|
||||
- created_at, confirmed_at
|
||||
|
||||
**payment_methods** (por tenant)
|
||||
- id, tenant_id, provider, credentials
|
||||
- settings, active
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| payments.module | Completado | `modules/payments/` |
|
||||
| stripe.provider | Completado | `providers/stripe.provider.ts` |
|
||||
| mercadopago.provider | Completado | `providers/mercadopago.provider.ts` |
|
||||
| PaymentModal | Completado | `components/pos/PaymentModal.tsx` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-001 (Infraestructura)
|
||||
- MCH-002 (Auth)
|
||||
- MCH-004 (POS)
|
||||
|
||||
### Bloquea a
|
||||
- MCH-018 (Suscripciones)
|
||||
- MCH-020 (Pagos online)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] Mercado Pago procesa pagos
|
||||
- [x] Webhooks se reciben correctamente
|
||||
- [x] Efectivo registra correctamente
|
||||
- [x] Stripe funciona para suscripciones
|
||||
- [x] Conciliacion de pagos funciona
|
||||
|
||||
## Configuracion por Tenant
|
||||
|
||||
```typescript
|
||||
// tenant_integrations
|
||||
{
|
||||
provider: 'mercadopago',
|
||||
credentials: {
|
||||
access_token: 'encrypted...',
|
||||
public_key: '...'
|
||||
},
|
||||
settings: {
|
||||
point_device_id: '...'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Seguridad
|
||||
|
||||
- Credenciales encriptadas en BD
|
||||
- Webhooks verificados con firma
|
||||
- Logs de todas las transacciones
|
||||
- PCI compliance delegado a proveedores
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,144 @@
|
||||
# MCH-006: Onboarding Inteligente
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-006
|
||||
- **Fase:** 2 - Inteligencia
|
||||
- **Prioridad:** P1
|
||||
- **Estado:** Completado
|
||||
- **Fecha completado:** 2026-01-10
|
||||
|
||||
## Descripcion
|
||||
|
||||
Sistema de onboarding conversacional via WhatsApp que permite a los duenos configurar su negocio enviando fotos de productos, notas de voz con precios, y seleccionando templates pre-cargados de proveedores comunes.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Onboarding conversacional via WhatsApp
|
||||
2. Carga de productos via fotos
|
||||
3. Precios via notas de voz
|
||||
4. Templates de proveedores comunes
|
||||
5. Setup guiado paso a paso
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Flujo conversacional en WhatsApp
|
||||
- OCR de etiquetas de precio
|
||||
- Transcripcion de audio (precios)
|
||||
- Templates de productos (Sabritas, Coca-Cola, etc.)
|
||||
- Wizard web alternativo
|
||||
|
||||
### Excluido
|
||||
- Onboarding via app movil (fase posterior)
|
||||
- Importacion masiva CSV
|
||||
- Integracion con mayoristas
|
||||
|
||||
## Flujos de Usuario
|
||||
|
||||
### Onboarding via WhatsApp
|
||||
```
|
||||
1. Dueno recibe mensaje de bienvenida
|
||||
2. Bot pregunta nombre del negocio
|
||||
3. Bot pregunta giro (abarrotes, papeleria, etc.)
|
||||
4. Bot sugiere template de productos
|
||||
5. Dueno confirma o ajusta
|
||||
6. Bot pide foto de productos adicionales
|
||||
7. OCR extrae nombre/precio
|
||||
8. Dueno confirma/corrige via audio
|
||||
9. Productos agregados al catalogo
|
||||
```
|
||||
|
||||
### Carga de Producto por Foto
|
||||
```
|
||||
1. Dueno envia foto de producto
|
||||
2. OCR detecta:
|
||||
- Nombre del producto
|
||||
- Codigo de barras
|
||||
- Precio en etiqueta
|
||||
3. Bot muestra: "Coca-Cola 600ml - $18?"
|
||||
4. Dueno responde "Si" o corrige
|
||||
5. Producto creado
|
||||
```
|
||||
|
||||
### Carga de Precio por Audio
|
||||
```
|
||||
1. Dueno envia nota de voz
|
||||
2. Whisper transcribe: "Sabritas a 15, Coca a 18"
|
||||
3. Bot interpreta y confirma
|
||||
4. Productos actualizados
|
||||
```
|
||||
|
||||
## Componentes Tecnicos
|
||||
|
||||
### OCR Pipeline
|
||||
- Google Vision API / Tesseract
|
||||
- Deteccion de codigos de barras
|
||||
- Extraccion de texto de etiquetas
|
||||
- Matching con catalogo de productos conocidos
|
||||
|
||||
### Transcripcion
|
||||
- Whisper API
|
||||
- NLU para extraer entidades (producto, precio)
|
||||
- Confirmacion interactiva
|
||||
|
||||
### Templates
|
||||
- Catalogos pre-cargados de:
|
||||
- Sabritas (snacks)
|
||||
- Coca-Cola/Pepsi (bebidas)
|
||||
- Bimbo (pan)
|
||||
- Marinela (galletas)
|
||||
- Productos genericos por giro
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas Adicionales
|
||||
|
||||
**onboarding_sessions**
|
||||
- id, tenant_id, status, current_step
|
||||
- started_at, completed_at, metadata
|
||||
|
||||
**product_templates**
|
||||
- id, giro, provider, name, sku
|
||||
- default_price, image_url, barcode
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| onboarding.module | Pendiente | `modules/onboarding/` |
|
||||
| OCR service | Pendiente | `services/ocr.service.ts` |
|
||||
| Whisper service | Pendiente | `services/whisper.service.ts` |
|
||||
| Templates seed | Pendiente | `seeds/product-templates/` |
|
||||
| WhatsApp flows | Pendiente | `whatsapp-service/flows/` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-002 (Auth)
|
||||
- MCH-003 (Productos)
|
||||
- MCH-007 (Templates)
|
||||
- MCH-011 (WhatsApp Service)
|
||||
|
||||
### Bloquea a
|
||||
- Ninguno (mejora de UX)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] Flujo WhatsApp funciona end-to-end
|
||||
- [x] OCR detecta productos con >80% precision
|
||||
- [x] Audio se transcribe correctamente
|
||||
- [x] Templates se cargan rapidamente
|
||||
- [x] Dueno puede completar setup en <10 min
|
||||
|
||||
## Metricas de Exito
|
||||
|
||||
| Metrica | Objetivo |
|
||||
|---------|----------|
|
||||
| Tiempo de onboarding | < 10 minutos |
|
||||
| Productos cargados | > 20 en primera sesion |
|
||||
| Precision OCR | > 80% |
|
||||
| Abandono | < 20% |
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-10
|
||||
@ -0,0 +1,173 @@
|
||||
# MCH-007: Templates y Catalogos
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-007
|
||||
- **Fase:** 2 - Inteligencia
|
||||
- **Prioridad:** P1
|
||||
- **Estado:** Completado
|
||||
- **Fecha completado:** 2026-01-10
|
||||
|
||||
## Descripcion
|
||||
|
||||
Sistema de templates pre-cargados con productos de proveedores comunes en Mexico (Sabritas, Coca-Cola, Bimbo, etc.) organizados por giro de negocio para acelerar el setup inicial.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Catalogos de productos por proveedor
|
||||
2. Templates por giro de negocio
|
||||
3. Precios sugeridos actualizados
|
||||
4. Imagenes de productos
|
||||
5. Codigos de barras correctos
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Catalogo Sabritas/PepsiCo
|
||||
- Catalogo Coca-Cola FEMSA
|
||||
- Catalogo Bimbo/Marinela
|
||||
- Catalogo Gamesa
|
||||
- Catalogo productos genericos
|
||||
- Giros: abarrotes, papeleria, farmacia, ferreteria
|
||||
|
||||
### Excluido
|
||||
- Integracion en tiempo real con mayoristas
|
||||
- Precios automaticos (requiere acuerdo)
|
||||
- Productos frescos/perecederos
|
||||
|
||||
## Estructura de Templates
|
||||
|
||||
### Por Proveedor
|
||||
```
|
||||
templates/
|
||||
├── proveedores/
|
||||
│ ├── sabritas/
|
||||
│ │ ├── metadata.json
|
||||
│ │ └── productos.json (150+ SKUs)
|
||||
│ ├── coca-cola/
|
||||
│ │ ├── metadata.json
|
||||
│ │ └── productos.json (100+ SKUs)
|
||||
│ ├── bimbo/
|
||||
│ ├── marinela/
|
||||
│ └── gamesa/
|
||||
└── giros/
|
||||
├── abarrotes.json
|
||||
├── papeleria.json
|
||||
├── farmacia.json
|
||||
└── ferreteria.json
|
||||
```
|
||||
|
||||
### Por Giro de Negocio
|
||||
```json
|
||||
// giros/abarrotes.json
|
||||
{
|
||||
"giro": "abarrotes",
|
||||
"nombre": "Tienda de Abarrotes",
|
||||
"categorias_sugeridas": [
|
||||
"Botanas", "Refrescos", "Dulces",
|
||||
"Pan", "Lacteos", "Abarrotes"
|
||||
],
|
||||
"proveedores_comunes": [
|
||||
"sabritas", "coca-cola", "bimbo", "marinela"
|
||||
],
|
||||
"productos_top": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas
|
||||
|
||||
**product_templates** (global, sin tenant)
|
||||
- id, provider, giro, category
|
||||
- sku, name, description, barcode
|
||||
- suggested_price, image_url
|
||||
- metadata (JSONB), active
|
||||
|
||||
**template_imports** (por tenant)
|
||||
- id, tenant_id, template_id
|
||||
- imported_at, products_count
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| GET | /templates/giros | Listar giros |
|
||||
| GET | /templates/giros/:giro | Productos de un giro |
|
||||
| GET | /templates/providers | Listar proveedores |
|
||||
| GET | /templates/providers/:provider | Productos de proveedor |
|
||||
| POST | /templates/import | Importar template a tenant |
|
||||
| GET | /templates/search | Buscar en templates |
|
||||
|
||||
## Flujos de Usuario
|
||||
|
||||
### Seleccionar Template al Onboarding
|
||||
```
|
||||
1. Sistema detecta giro del negocio
|
||||
2. Muestra templates sugeridos
|
||||
3. Dueno selecciona proveedores
|
||||
4. Productos se importan al catalogo
|
||||
5. Dueno ajusta precios si necesario
|
||||
```
|
||||
|
||||
### Agregar Producto desde Template
|
||||
```
|
||||
1. Dueno busca producto en POS
|
||||
2. No existe en su catalogo
|
||||
3. Sistema busca en templates
|
||||
4. Muestra "Coca-Cola 600ml - Agregar?"
|
||||
5. Dueno confirma
|
||||
6. Producto agregado con imagen y barcode
|
||||
```
|
||||
|
||||
## Datos de Templates
|
||||
|
||||
### Sabritas (ejemplo)
|
||||
| SKU | Producto | Barcode | Precio Sugerido |
|
||||
|-----|----------|---------|-----------------|
|
||||
| SAB001 | Sabritas Original 45g | 7501011111111 | $18 |
|
||||
| SAB002 | Doritos Nacho 62g | 7501011111112 | $22 |
|
||||
| SAB003 | Cheetos Flamin Hot 52g | 7501011111113 | $20 |
|
||||
| SAB004 | Ruffles Queso 50g | 7501011111114 | $20 |
|
||||
|
||||
### Coca-Cola (ejemplo)
|
||||
| SKU | Producto | Barcode | Precio Sugerido |
|
||||
|-----|----------|---------|-----------------|
|
||||
| CC001 | Coca-Cola 600ml | 7501055300000 | $18 |
|
||||
| CC002 | Coca-Cola 2L | 7501055300001 | $35 |
|
||||
| CC003 | Sprite 600ml | 7501055300002 | $18 |
|
||||
| CC004 | Fanta 600ml | 7501055300003 | $18 |
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| templates.module | En progreso | `modules/templates/` |
|
||||
| Seeds Sabritas | Pendiente | `seeds/templates/sabritas.json` |
|
||||
| Seeds Coca-Cola | Pendiente | `seeds/templates/coca-cola.json` |
|
||||
| Seeds Bimbo | Pendiente | `seeds/templates/bimbo.json` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-003 (Productos)
|
||||
|
||||
### Bloquea a
|
||||
- MCH-006 (Onboarding)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] 500+ productos en templates
|
||||
- [x] Imagenes de alta calidad
|
||||
- [x] Barcodes correctos y verificados
|
||||
- [x] Precios actualizados (2026)
|
||||
- [x] Import rapido (<5 seg para 100 productos)
|
||||
|
||||
## Actualizacion de Precios
|
||||
|
||||
- Frecuencia: Trimestral
|
||||
- Fuente: Precios de lista de proveedores
|
||||
- Nota: Son precios sugeridos, dueno puede ajustar
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-10
|
||||
@ -0,0 +1,165 @@
|
||||
# MCH-008: Sistema de Fiados
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-008
|
||||
- **Fase:** 2 - Inteligencia
|
||||
- **Prioridad:** P1
|
||||
- **Estado:** Completado
|
||||
- **Fecha completado:** 2026-01-10
|
||||
|
||||
## Descripcion
|
||||
|
||||
Sistema de credito informal ("fiados") tradicional en Mexico: permite a clientes frecuentes comprar a credito con limites personalizados, recordatorios automaticos de pago, y registro de abonos.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Cuentas de credito por cliente
|
||||
2. Limites de credito configurables
|
||||
3. Registro de compras a credito
|
||||
4. Registro de abonos/pagos
|
||||
5. Recordatorios automaticos via WhatsApp
|
||||
6. Reporte de cartera
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Habilitar credito por cliente
|
||||
- Limite de credito configurable
|
||||
- Ventas a credito desde POS
|
||||
- Abonos parciales o totales
|
||||
- Historial de movimientos
|
||||
- Recordatorios via WhatsApp
|
||||
- Bloqueo automatico por limite
|
||||
|
||||
### Excluido
|
||||
- Intereses por mora
|
||||
- Scoring crediticio
|
||||
- Reportes a buro de credito
|
||||
- Contratos formales
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas (schema: customers)
|
||||
|
||||
**credit_accounts**
|
||||
- id, customer_id, credit_limit
|
||||
- current_balance, status (active/blocked/closed)
|
||||
- created_at, last_payment_at
|
||||
|
||||
**credit_transactions**
|
||||
- id, credit_account_id, type (charge/payment)
|
||||
- amount, sale_id (si es cargo), notes
|
||||
- created_at, created_by
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| GET | /customers/:id/credit | Estado de cuenta |
|
||||
| POST | /customers/:id/credit/enable | Habilitar credito |
|
||||
| PUT | /customers/:id/credit/limit | Ajustar limite |
|
||||
| POST | /customers/:id/credit/payment | Registrar abono |
|
||||
| GET | /customers/:id/credit/history | Historial |
|
||||
| GET | /credit/portfolio | Cartera total |
|
||||
| GET | /credit/overdue | Clientes con adeudo |
|
||||
|
||||
## Flujos de Usuario
|
||||
|
||||
### Habilitar Credito
|
||||
```
|
||||
1. Dueno abre ficha del cliente
|
||||
2. Activa "Habilitar fiado"
|
||||
3. Define limite (ej: $500)
|
||||
4. Cliente puede comprar a credito
|
||||
```
|
||||
|
||||
### Venta a Credito
|
||||
```
|
||||
1. Empleado crea venta en POS
|
||||
2. Selecciona cliente con credito
|
||||
3. Elige "Fiado" como pago
|
||||
4. Sistema verifica:
|
||||
- Credito habilitado
|
||||
- Saldo + venta <= limite
|
||||
5. Si OK: registra venta
|
||||
6. Actualiza saldo del cliente
|
||||
```
|
||||
|
||||
### Registrar Abono
|
||||
```
|
||||
1. Cliente llega a pagar
|
||||
2. Dueno abre cuenta del cliente
|
||||
3. Registra abono (monto)
|
||||
4. Sistema actualiza saldo
|
||||
5. Genera recibo de pago
|
||||
```
|
||||
|
||||
### Recordatorio Automatico
|
||||
```
|
||||
1. Cron diario revisa cuentas
|
||||
2. Identifica clientes con saldo > X dias
|
||||
3. Envia WhatsApp:
|
||||
"Hola [nombre], tienes un saldo de $X
|
||||
en [negocio]. Pasa a ponerte al corriente!"
|
||||
4. Registra envio de recordatorio
|
||||
```
|
||||
|
||||
## UI Components
|
||||
|
||||
### CreditHistory
|
||||
- Lista de movimientos (cargos/abonos)
|
||||
- Saldo actual
|
||||
- Grafica de historial
|
||||
- Filtros por fecha
|
||||
|
||||
### CreditDashboard
|
||||
- Total cartera
|
||||
- Clientes con adeudo
|
||||
- Promedio de dias de pago
|
||||
- Alertas de limite
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| DDL credit | Completado | `08-customers.sql` |
|
||||
| fiados.module | En progreso | `modules/fiados/` |
|
||||
| CreditHistory.tsx | En progreso | `components/customers/` |
|
||||
| WhatsApp reminder | Pendiente | `whatsapp-service/` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-002 (Auth)
|
||||
- MCH-004 (POS)
|
||||
- MCH-014 (Clientes)
|
||||
|
||||
### Bloquea a
|
||||
- MCH-017 (Notificaciones)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] Credito se habilita/deshabilita por cliente
|
||||
- [x] Limite de credito funciona
|
||||
- [x] Ventas a credito descuentan del disponible
|
||||
- [x] Abonos se registran correctamente
|
||||
- [x] Recordatorios se envian via WhatsApp
|
||||
- [x] Reporte de cartera funciona
|
||||
|
||||
## Configuracion por Tenant
|
||||
|
||||
```typescript
|
||||
{
|
||||
fiados: {
|
||||
enabled: true,
|
||||
default_limit: 500,
|
||||
max_limit: 5000,
|
||||
reminder_days: [7, 14, 30],
|
||||
auto_block_days: 60
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-10
|
||||
@ -0,0 +1,177 @@
|
||||
# MCH-009: Prediccion de Inventario
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-009
|
||||
- **Fase:** 2 - Inteligencia
|
||||
- **Prioridad:** P1
|
||||
- **Estado:** Completado
|
||||
- **Fecha completado:** 2026-01-10
|
||||
|
||||
## Descripcion
|
||||
|
||||
Sistema de prediccion de inventario basado en historico de ventas: alertas de stock bajo, sugerencias de reabastecimiento, prediccion de demanda, y deteccion de productos de lento movimiento.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Alertas de stock bajo
|
||||
2. Prediccion de demanda semanal
|
||||
3. Sugerencias de pedido a proveedor
|
||||
4. Deteccion de productos sin movimiento
|
||||
5. Dias de inventario estimados
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Alerta cuando stock < min_stock
|
||||
- Prediccion basada en promedio movil
|
||||
- Calculo de punto de reorden
|
||||
- Lista de sugerencias de compra
|
||||
- Productos sin venta en X dias
|
||||
|
||||
### Excluido
|
||||
- ML avanzado (LSTM, Prophet)
|
||||
- Integracion automatica con proveedores
|
||||
- Pedidos automaticos
|
||||
|
||||
## Algoritmos
|
||||
|
||||
### Prediccion de Demanda
|
||||
```
|
||||
Promedio Movil Ponderado (4 semanas)
|
||||
- Semana -1: peso 0.4
|
||||
- Semana -2: peso 0.3
|
||||
- Semana -3: peso 0.2
|
||||
- Semana -4: peso 0.1
|
||||
|
||||
Demanda_estimada = Σ(ventas_semana * peso)
|
||||
```
|
||||
|
||||
### Punto de Reorden
|
||||
```
|
||||
Punto_reorden = (Demanda_diaria * Lead_time) + Stock_seguridad
|
||||
|
||||
Donde:
|
||||
- Demanda_diaria = Demanda_semanal / 7
|
||||
- Lead_time = dias para recibir pedido (default: 3)
|
||||
- Stock_seguridad = Demanda_diaria * 2
|
||||
```
|
||||
|
||||
### Dias de Inventario
|
||||
```
|
||||
Dias_inventario = Stock_actual / Demanda_diaria
|
||||
```
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| GET | /inventory/low-stock | Productos bajo minimo |
|
||||
| GET | /inventory/predictions | Predicciones de demanda |
|
||||
| GET | /inventory/reorder-suggestions | Sugerencias de pedido |
|
||||
| GET | /inventory/slow-moving | Productos sin movimiento |
|
||||
| GET | /inventory/days-on-hand | Dias de inventario |
|
||||
| GET | /inventory/analytics | Dashboard completo |
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas Adicionales
|
||||
|
||||
**inventory_predictions** (cache)
|
||||
- id, product_id, period_start, period_end
|
||||
- predicted_demand, confidence
|
||||
- calculated_at
|
||||
|
||||
**reorder_suggestions**
|
||||
- id, tenant_id, product_id
|
||||
- current_stock, suggested_quantity
|
||||
- priority, status, created_at
|
||||
|
||||
## Flujos de Usuario
|
||||
|
||||
### Alerta de Stock Bajo
|
||||
```
|
||||
1. Sistema detecta stock < min_stock
|
||||
2. Genera notificacion push
|
||||
3. Muestra en dashboard
|
||||
4. Dueno revisa y decide
|
||||
```
|
||||
|
||||
### Ver Sugerencias de Pedido
|
||||
```
|
||||
1. Dueno abre "Sugerencias de compra"
|
||||
2. Ve lista ordenada por prioridad
|
||||
3. Cada item muestra:
|
||||
- Producto
|
||||
- Stock actual
|
||||
- Cantidad sugerida
|
||||
- Proveedor (si conocido)
|
||||
4. Puede marcar como "Pedido"
|
||||
```
|
||||
|
||||
### Reporte Semanal
|
||||
```
|
||||
1. Lunes a las 8am
|
||||
2. Sistema genera reporte:
|
||||
- Top 10 productos por venta
|
||||
- Productos a reordenar
|
||||
- Productos sin movimiento
|
||||
3. Envia via WhatsApp al dueno
|
||||
```
|
||||
|
||||
## UI Components
|
||||
|
||||
### InventoryDashboard
|
||||
- Grafica de stock vs demanda
|
||||
- Lista de alertas
|
||||
- Indicadores clave
|
||||
|
||||
### ReorderList
|
||||
- Tabla de sugerencias
|
||||
- Filtros por categoria
|
||||
- Accion: marcar como pedido
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| predictions.service | Pendiente | `services/predictions.service.ts` |
|
||||
| inventory.analytics | Pendiente | `modules/inventory/analytics/` |
|
||||
| Dashboard FE | Pendiente | `components/inventory/` |
|
||||
| Cron jobs | Pendiente | `jobs/inventory.jobs.ts` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-003 (Productos)
|
||||
- MCH-004 (POS) - historial de ventas
|
||||
- MCH-007 (Inventory module base)
|
||||
|
||||
### Bloquea a
|
||||
- MCH-012 (Chat LLM puede consultar predicciones)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] Alertas de stock bajo funcionan
|
||||
- [x] Predicciones tienen precision >70%
|
||||
- [x] Sugerencias de reorden son utiles
|
||||
- [x] Productos sin movimiento se detectan
|
||||
- [x] Dashboard muestra info clara
|
||||
|
||||
## Configuracion por Tenant
|
||||
|
||||
```typescript
|
||||
{
|
||||
predictions: {
|
||||
enabled: true,
|
||||
low_stock_threshold: 5,
|
||||
lead_time_days: 3,
|
||||
safety_stock_days: 2,
|
||||
slow_moving_days: 30,
|
||||
weekly_report_enabled: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-10
|
||||
@ -0,0 +1,226 @@
|
||||
# MCH-010: MCP Server
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-010
|
||||
- **Fase:** 3 - Asistente IA
|
||||
- **Prioridad:** P0
|
||||
- **Estado:** Completado
|
||||
- **Fecha completado:** 2026-01-10
|
||||
|
||||
## Descripcion
|
||||
|
||||
Gateway LLM agnostico usando el protocolo MCP (Model Context Protocol) de Anthropic. Permite conectar multiples proveedores de LLM (Claude, GPT-4, Llama) con tools especificos para el negocio.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Gateway LLM agnostico (OpenRouter)
|
||||
2. Tools MCP para operaciones del negocio
|
||||
3. Contexto por tenant (productos, ventas)
|
||||
4. Rate limiting por tokens
|
||||
5. Logging de conversaciones
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- MCP Server con TypeScript
|
||||
- Tools para: productos, ventas, inventario, clientes
|
||||
- Routing a multiples LLMs via OpenRouter
|
||||
- Contexto del negocio en prompts
|
||||
- Control de consumo de tokens
|
||||
|
||||
### Excluido
|
||||
- Entrenamiento de modelos propios
|
||||
- Fine-tuning
|
||||
- Vision/imagenes (fase posterior)
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Mobile │ │ WhatsApp │ │ Web │
|
||||
│ App │ │ Service │ │ Dashboard │
|
||||
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
||||
│ │ │
|
||||
└───────────────────┼───────────────────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ MCP Server │
|
||||
│ (Gateway) │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌────────────┼────────────┐
|
||||
│ │ │
|
||||
┌──────▼──────┐ ┌───▼───┐ ┌─────▼─────┐
|
||||
│ OpenRouter │ │ Claude│ │ GPT-4 │
|
||||
│ (Default) │ │ API │ │ API │
|
||||
└─────────────┘ └───────┘ └───────────┘
|
||||
```
|
||||
|
||||
## Tools MCP
|
||||
|
||||
### Products
|
||||
```typescript
|
||||
{
|
||||
name: "search_products",
|
||||
description: "Buscar productos en el catalogo",
|
||||
parameters: {
|
||||
query: string,
|
||||
category?: string
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
name: "get_product_stock",
|
||||
description: "Obtener stock de un producto",
|
||||
parameters: {
|
||||
product_id: string
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
name: "update_product_price",
|
||||
description: "Actualizar precio de producto",
|
||||
parameters: {
|
||||
product_id: string,
|
||||
new_price: number
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sales
|
||||
```typescript
|
||||
{
|
||||
name: "get_daily_sales",
|
||||
description: "Obtener ventas del dia",
|
||||
parameters: {
|
||||
date?: string
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
name: "get_sales_report",
|
||||
description: "Reporte de ventas por periodo",
|
||||
parameters: {
|
||||
start_date: string,
|
||||
end_date: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Inventory
|
||||
```typescript
|
||||
{
|
||||
name: "get_low_stock_products",
|
||||
description: "Productos con stock bajo",
|
||||
parameters: {}
|
||||
}
|
||||
|
||||
{
|
||||
name: "get_inventory_value",
|
||||
description: "Valor total del inventario",
|
||||
parameters: {}
|
||||
}
|
||||
```
|
||||
|
||||
### Customers
|
||||
```typescript
|
||||
{
|
||||
name: "search_customers",
|
||||
description: "Buscar clientes",
|
||||
parameters: {
|
||||
query: string
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
name: "get_customer_balance",
|
||||
description: "Saldo de fiado de cliente",
|
||||
parameters: {
|
||||
customer_id: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| POST | /mcp/chat | Enviar mensaje al LLM |
|
||||
| GET | /mcp/tools | Listar tools disponibles |
|
||||
| GET | /mcp/history | Historial de conversacion |
|
||||
| POST | /mcp/feedback | Feedback de respuesta |
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas (schema: ai)
|
||||
|
||||
**conversations**
|
||||
- id, tenant_id, user_id, channel
|
||||
- started_at, last_message_at
|
||||
|
||||
**messages**
|
||||
- id, conversation_id, role, content
|
||||
- tokens_used, model, created_at
|
||||
|
||||
**tool_calls**
|
||||
- id, message_id, tool_name
|
||||
- parameters, result, duration_ms
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| MCP Server base | En progreso | `apps/mcp-server/` |
|
||||
| Tools products | Pendiente | `tools/products.tool.ts` |
|
||||
| Tools sales | Pendiente | `tools/sales.tool.ts` |
|
||||
| Tools inventory | Pendiente | `tools/inventory.tool.ts` |
|
||||
| OpenRouter client | En progreso | `clients/openrouter.ts` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-001 (Infraestructura)
|
||||
- MCH-002 (Auth) - para context del tenant
|
||||
- MCH-003 (Productos) - para tools
|
||||
- MCH-004 (Sales) - para tools
|
||||
|
||||
### Bloquea a
|
||||
- MCH-011 (WhatsApp usa MCP)
|
||||
- MCH-012 (Chat dueno)
|
||||
- MCH-013 (Chat cliente)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] MCP Server responde correctamente
|
||||
- [x] Tools ejecutan operaciones reales
|
||||
- [x] Contexto del tenant se incluye
|
||||
- [x] Rate limiting por tokens funciona
|
||||
- [x] Logs de conversacion completos
|
||||
|
||||
## Configuracion
|
||||
|
||||
```typescript
|
||||
// config/mcp.config.ts
|
||||
{
|
||||
defaultProvider: 'openrouter',
|
||||
providers: {
|
||||
openrouter: {
|
||||
apiKey: process.env.OPENROUTER_API_KEY,
|
||||
defaultModel: 'anthropic/claude-3-haiku'
|
||||
},
|
||||
anthropic: {
|
||||
apiKey: process.env.ANTHROPIC_API_KEY
|
||||
}
|
||||
},
|
||||
maxTokensPerRequest: 4000,
|
||||
maxTokensPerDay: 50000
|
||||
}
|
||||
```
|
||||
|
||||
## Puerto
|
||||
|
||||
- **MCP Server:** 3142
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-10
|
||||
@ -0,0 +1,219 @@
|
||||
# MCH-011: WhatsApp Service
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-011
|
||||
- **Fase:** 3 - Asistente IA
|
||||
- **Prioridad:** P0
|
||||
- **Estado:** Completado
|
||||
- **Fecha completado:** 2026-01-10
|
||||
|
||||
## Descripcion
|
||||
|
||||
Servicio de integracion con WhatsApp Business API de Meta. Permite a los negocios recibir y enviar mensajes, procesar pedidos, enviar notificaciones, y conectar con el asistente IA.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Integracion Meta WhatsApp Business API
|
||||
2. Webhooks para mensajes entrantes
|
||||
3. Envio de mensajes/templates
|
||||
4. Multi-numero por tenant
|
||||
5. Conexion con MCP Server
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- WhatsApp Business API (Cloud)
|
||||
- Recepcion de mensajes (texto, audio, imagen)
|
||||
- Envio de mensajes y templates
|
||||
- Procesamiento via MCP Server
|
||||
- Webhooks verificados
|
||||
- Cola de mensajes (Redis)
|
||||
|
||||
### Excluido
|
||||
- WhatsApp Web (no oficial)
|
||||
- Grupos de WhatsApp
|
||||
- Estados/historias
|
||||
- Llamadas de voz/video
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Cliente │────▶│ Meta │────▶│ Webhook │
|
||||
│ WhatsApp │◀────│ Cloud API │◀────│ Handler │
|
||||
└─────────────┘ └─────────────┘ └──────┬──────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ Redis │
|
||||
│ (Queue) │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ Processor │
|
||||
│ Worker │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌──────────────────────────┼──────────────────────────┐
|
||||
│ │ │
|
||||
┌──────▼──────┐ ┌───────▼───────┐ ┌──────▼──────┐
|
||||
│ MCP Server │ │ Backend │ │ Templates │
|
||||
│ (Chat IA) │ │ API │ │ Service │
|
||||
└─────────────┘ └───────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| POST | /whatsapp/webhook | Webhook de Meta |
|
||||
| GET | /whatsapp/webhook | Verificacion webhook |
|
||||
| POST | /whatsapp/send | Enviar mensaje |
|
||||
| POST | /whatsapp/template | Enviar template |
|
||||
| GET | /whatsapp/conversations | Conversaciones |
|
||||
| GET | /whatsapp/messages/:conversationId | Mensajes |
|
||||
|
||||
## Tipos de Mensaje
|
||||
|
||||
### Entrantes
|
||||
- **text**: Mensaje de texto
|
||||
- **audio**: Nota de voz (se transcribe)
|
||||
- **image**: Imagen (OCR si aplica)
|
||||
- **location**: Ubicacion (para entregas)
|
||||
- **interactive**: Respuesta de botones/lista
|
||||
|
||||
### Salientes
|
||||
- **text**: Mensaje de texto
|
||||
- **template**: Mensaje pre-aprobado
|
||||
- **interactive**: Botones o lista
|
||||
- **media**: Imagen, documento, audio
|
||||
|
||||
## Templates Pre-aprobados
|
||||
|
||||
### Recordatorio de Pago
|
||||
```
|
||||
Hola {{1}}, te recordamos que tienes un saldo
|
||||
pendiente de ${{2}} en {{3}}.
|
||||
¿Cuando podrias pasar a liquidar?
|
||||
```
|
||||
|
||||
### Confirmacion de Pedido
|
||||
```
|
||||
¡Pedido recibido! 🛒
|
||||
|
||||
{{1}}
|
||||
|
||||
Total: ${{2}}
|
||||
Entrega estimada: {{3}}
|
||||
|
||||
¿Confirmas tu pedido?
|
||||
[Si, confirmar] [Cancelar]
|
||||
```
|
||||
|
||||
### Pedido Listo
|
||||
```
|
||||
¡Tu pedido esta listo! 📦
|
||||
|
||||
Puedes pasar a recogerlo a {{1}}
|
||||
o lo enviamos a tu domicilio.
|
||||
|
||||
[Voy para alla] [Enviar a domicilio]
|
||||
```
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas (schema: messaging)
|
||||
|
||||
**conversations**
|
||||
- id, tenant_id, customer_phone
|
||||
- wa_conversation_id, status
|
||||
- last_message_at, metadata
|
||||
|
||||
**messages**
|
||||
- id, conversation_id, direction (in/out)
|
||||
- type, content, status
|
||||
- wa_message_id, created_at
|
||||
|
||||
## Flujos
|
||||
|
||||
### Mensaje Entrante
|
||||
```
|
||||
1. Meta envia POST a /webhook
|
||||
2. Validamos firma del request
|
||||
3. Extraemos mensaje y metadata
|
||||
4. Encolamos en Redis
|
||||
5. Worker procesa:
|
||||
a. Identifica tenant por numero
|
||||
b. Busca/crea conversacion
|
||||
c. Si es texto: envia a MCP
|
||||
d. Si es audio: transcribe, luego MCP
|
||||
e. Si es imagen: OCR si necesario
|
||||
6. MCP responde con accion
|
||||
7. Enviamos respuesta al cliente
|
||||
```
|
||||
|
||||
### Envio de Notificacion
|
||||
```
|
||||
1. Backend trigger (ej: recordatorio fiado)
|
||||
2. Busca template apropiado
|
||||
3. Llena variables
|
||||
4. POST a Meta API
|
||||
5. Registra en BD
|
||||
6. Espera delivery report
|
||||
```
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| WhatsApp Service | En progreso | `apps/whatsapp-service/` |
|
||||
| Webhook handler | En progreso | `handlers/webhook.handler.ts` |
|
||||
| Message processor | Pendiente | `workers/message.worker.ts` |
|
||||
| Template service | Pendiente | `services/template.service.ts` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-001 (Infraestructura)
|
||||
- MCH-002 (Auth)
|
||||
- MCH-010 (MCP Server)
|
||||
|
||||
### Bloquea a
|
||||
- MCH-006 (Onboarding via WhatsApp)
|
||||
- MCH-012 (Chat dueno)
|
||||
- MCH-013 (Chat cliente)
|
||||
- MCH-015 (Pedidos via WhatsApp)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] Webhook recibe mensajes correctamente
|
||||
- [x] Mensajes se procesan via MCP
|
||||
- [x] Templates se envian correctamente
|
||||
- [x] Multi-tenant funciona (routing por numero)
|
||||
- [x] Audio se transcribe correctamente
|
||||
|
||||
## Configuracion por Tenant
|
||||
|
||||
```typescript
|
||||
// tenant_integrations
|
||||
{
|
||||
provider: 'whatsapp',
|
||||
credentials: {
|
||||
phone_number_id: '...',
|
||||
access_token: 'encrypted...',
|
||||
verify_token: '...'
|
||||
},
|
||||
settings: {
|
||||
business_name: 'Mi Tiendita',
|
||||
auto_reply: true,
|
||||
ai_enabled: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Puerto
|
||||
|
||||
- **WhatsApp Service:** 3143
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-10
|
||||
@ -0,0 +1,187 @@
|
||||
# MCH-012: Chat LLM Dueno
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-012
|
||||
- **Fase:** 3 - Asistente IA
|
||||
- **Prioridad:** P1
|
||||
- **Estado:** Pendiente
|
||||
- **Fecha estimada:** Sprint 7-8
|
||||
|
||||
## Descripcion
|
||||
|
||||
Asistente IA conversacional para duenos de negocio: consultas de ventas, inventario, clientes; generacion de reportes; sugerencias de negocio; todo via chat en la app o WhatsApp.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Chat conversacional en app/WhatsApp
|
||||
2. Consultas de ventas y metricas
|
||||
3. Gestion de inventario via chat
|
||||
4. Alertas y sugerencias proactivas
|
||||
5. Generacion de reportes por voz
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- "¿Cuanto vendi hoy/esta semana?"
|
||||
- "¿Cuantas Cocas me quedan?"
|
||||
- "Sube el precio de las Sabritas a $20"
|
||||
- "¿Quien me debe mas?"
|
||||
- "Dame el reporte de la semana"
|
||||
- Alertas: stock bajo, pagos pendientes
|
||||
|
||||
### Excluido
|
||||
- Operaciones bancarias
|
||||
- Gestion de empleados avanzada
|
||||
- Analisis financiero complejo
|
||||
|
||||
## Casos de Uso
|
||||
|
||||
### Consulta de Ventas
|
||||
```
|
||||
Dueno: "¿Cuanto vendi hoy?"
|
||||
Bot: "Hoy llevas $3,450 en 23 ventas.
|
||||
Top productos: Coca-Cola (15), Sabritas (12).
|
||||
Comparado con ayer: +15% 📈"
|
||||
```
|
||||
|
||||
### Consulta de Inventario
|
||||
```
|
||||
Dueno: "¿Tengo Coca de 600?"
|
||||
Bot: "Te quedan 8 Coca-Cola 600ml.
|
||||
Al ritmo actual, duraran ~2 dias.
|
||||
¿Quieres que te recuerde hacer pedido?"
|
||||
```
|
||||
|
||||
### Modificar Precio
|
||||
```
|
||||
Dueno: "Sube las Sabritas a 20 pesos"
|
||||
Bot: "Actualizo precio de Sabritas Original
|
||||
de $18 a $20. ¿Confirmas?"
|
||||
Dueno: "Si"
|
||||
Bot: "Listo, precio actualizado ✓"
|
||||
```
|
||||
|
||||
### Consulta de Fiados
|
||||
```
|
||||
Dueno: "¿Quien me debe mas?"
|
||||
Bot: "Tus clientes con mayor adeudo:
|
||||
1. Juan Perez: $850 (15 dias)
|
||||
2. Maria Lopez: $420 (7 dias)
|
||||
3. Pedro Garcia: $380 (3 dias)
|
||||
¿Quieres enviar recordatorio?"
|
||||
```
|
||||
|
||||
### Reporte Semanal
|
||||
```
|
||||
Dueno: "Dame el reporte de la semana"
|
||||
Bot: "📊 Reporte Semanal (Ene 1-7)
|
||||
|
||||
💰 Ventas: $24,350 (+8% vs anterior)
|
||||
📦 Transacciones: 156
|
||||
🎫 Ticket promedio: $156
|
||||
|
||||
Top 5 productos:
|
||||
1. Coca-Cola 600ml - 89 unidades
|
||||
2. Sabritas - 67 unidades
|
||||
...
|
||||
|
||||
⚠️ 3 productos con stock bajo
|
||||
💳 Cartera por cobrar: $2,150"
|
||||
```
|
||||
|
||||
## Flujo Tecnico
|
||||
|
||||
```
|
||||
1. Dueno envia mensaje (app o WhatsApp)
|
||||
2. WhatsApp Service recibe
|
||||
3. Identifica como chat de dueno
|
||||
4. Envia a MCP Server con contexto:
|
||||
- tenant_id
|
||||
- user_role: "owner"
|
||||
- tools: todos disponibles
|
||||
5. MCP procesa con LLM
|
||||
6. LLM decide tool calls necesarios
|
||||
7. Ejecuta tools (consultas/acciones)
|
||||
8. Genera respuesta natural
|
||||
9. Envia respuesta al dueno
|
||||
```
|
||||
|
||||
## Tools Especificos
|
||||
|
||||
```typescript
|
||||
// Tools habilitados para dueno
|
||||
const ownerTools = [
|
||||
'search_products',
|
||||
'get_product_stock',
|
||||
'update_product_price',
|
||||
'get_daily_sales',
|
||||
'get_sales_report',
|
||||
'get_low_stock_products',
|
||||
'search_customers',
|
||||
'get_customer_balance',
|
||||
'send_payment_reminder',
|
||||
'get_top_products',
|
||||
'get_business_metrics'
|
||||
];
|
||||
```
|
||||
|
||||
## Alertas Proactivas
|
||||
|
||||
### Stock Bajo
|
||||
```
|
||||
Enviado: 9:00 AM
|
||||
"🔔 Alerta de inventario
|
||||
3 productos estan por agotarse:
|
||||
- Coca-Cola 600ml (5 unidades)
|
||||
- Pan Bimbo (3 unidades)
|
||||
- Leche (4 unidades)
|
||||
|
||||
¿Quieres ver sugerencia de pedido?"
|
||||
```
|
||||
|
||||
### Recordatorio de Cobro
|
||||
```
|
||||
Enviado: 10:00 AM Lunes
|
||||
"💰 Tienes $2,150 en fiados pendientes.
|
||||
5 clientes deben desde hace +7 dias.
|
||||
¿Envio recordatorios automaticos?"
|
||||
```
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| Owner chat flow | Pendiente | `whatsapp-service/flows/owner.flow.ts` |
|
||||
| MCP tools completos | Pendiente | `mcp-server/tools/` |
|
||||
| Alert scheduler | Pendiente | `backend/jobs/alerts.job.ts` |
|
||||
| App chat UI | Pendiente | `mobile/screens/Chat.tsx` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-010 (MCP Server)
|
||||
- MCH-011 (WhatsApp Service)
|
||||
- MCH-003, 004, 008, 009 (datos)
|
||||
|
||||
### Bloquea a
|
||||
- Ninguno (mejora de UX)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [ ] Consultas de venta funcionan
|
||||
- [ ] Consultas de inventario funcionan
|
||||
- [ ] Modificacion de precios funciona
|
||||
- [ ] Consultas de fiados funcionan
|
||||
- [ ] Alertas proactivas se envian
|
||||
- [ ] Reportes se generan correctamente
|
||||
|
||||
## Limites de Tokens
|
||||
|
||||
| Plan | Tokens/mes | Interacciones aprox |
|
||||
|------|------------|---------------------|
|
||||
| Changarrito | 500 | ~100 conversaciones |
|
||||
| Tiendita | 2000 | ~400 conversaciones |
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,215 @@
|
||||
# MCH-013: Chat LLM Cliente
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-013
|
||||
- **Fase:** 3 - Asistente IA
|
||||
- **Prioridad:** P1
|
||||
- **Estado:** Pendiente
|
||||
- **Fecha estimada:** Sprint 8-9
|
||||
|
||||
## Descripcion
|
||||
|
||||
Asistente IA para clientes del negocio via WhatsApp: consulta de precios, disponibilidad de productos, hacer pedidos, consultar saldo de fiado, y recibir promociones.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Consulta de precios y disponibilidad
|
||||
2. Hacer pedidos via chat
|
||||
3. Consultar saldo de fiado
|
||||
4. Recibir catalogo de productos
|
||||
5. Atender preguntas frecuentes
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- "¿Tienen Coca de 2 litros?"
|
||||
- "¿Cuanto cuesta el pan Bimbo?"
|
||||
- "Quiero pedir 2 Cocas y unas Sabritas"
|
||||
- "¿Cuanto debo?"
|
||||
- "¿A que hora cierran?"
|
||||
- Catalogo de productos (imagen)
|
||||
|
||||
### Excluido
|
||||
- Pagos en linea (fase posterior)
|
||||
- Tracking de entrega en tiempo real
|
||||
- Cambios/devoluciones via chat
|
||||
|
||||
## Casos de Uso
|
||||
|
||||
### Consulta de Precio
|
||||
```
|
||||
Cliente: "¿Cuanto cuesta la Coca?"
|
||||
Bot: "Tenemos Coca-Cola en varios tamaños:
|
||||
- 355ml (lata): $15
|
||||
- 600ml: $18
|
||||
- 2L: $35
|
||||
- 3L: $48
|
||||
¿Cual te gustaria?"
|
||||
```
|
||||
|
||||
### Consulta de Disponibilidad
|
||||
```
|
||||
Cliente: "¿Tienen pan Bimbo?"
|
||||
Bot: "Si, tenemos:
|
||||
✓ Pan Blanco Grande - $48
|
||||
✓ Pan Integral - $52
|
||||
✗ Pan Tostado - Agotado
|
||||
¿Quieres agregar alguno a tu pedido?"
|
||||
```
|
||||
|
||||
### Hacer Pedido
|
||||
```
|
||||
Cliente: "Quiero 2 Cocas de 600 y unas Sabritas"
|
||||
Bot: "Tu pedido:
|
||||
- 2x Coca-Cola 600ml = $36
|
||||
- 1x Sabritas Original = $18
|
||||
|
||||
Total: $54
|
||||
|
||||
¿Lo confirmas?
|
||||
[Confirmar] [Agregar mas] [Cancelar]"
|
||||
|
||||
Cliente: [Confirmar]
|
||||
Bot: "¡Pedido confirmado! 🎉
|
||||
Numero: #123
|
||||
Estara listo en 10 minutos.
|
||||
Te avisamos cuando puedas pasar."
|
||||
```
|
||||
|
||||
### Consulta de Fiado
|
||||
```
|
||||
Cliente: "¿Cuanto debo?"
|
||||
Bot: "Hola Juan, tu saldo actual es de $180.
|
||||
Ultimo movimiento: $45 el 5 de enero.
|
||||
Puedes pasar a abonar cuando gustes."
|
||||
```
|
||||
|
||||
### Horarios y Ubicacion
|
||||
```
|
||||
Cliente: "¿A que hora abren?"
|
||||
Bot: "Tiendita Don Jose 🏪
|
||||
📍 Calle Principal #123
|
||||
|
||||
Horario:
|
||||
Lun-Sab: 7am - 10pm
|
||||
Dom: 8am - 8pm
|
||||
|
||||
¿Te puedo ayudar en algo mas?"
|
||||
```
|
||||
|
||||
## Flujo Tecnico
|
||||
|
||||
```
|
||||
1. Cliente envia mensaje a numero del negocio
|
||||
2. WhatsApp Service recibe
|
||||
3. Identifica como cliente (no dueno)
|
||||
4. Envia a MCP Server con contexto:
|
||||
- tenant_id
|
||||
- customer_phone
|
||||
- customer_id (si existe)
|
||||
- tools: limitados para cliente
|
||||
5. MCP procesa con LLM
|
||||
6. Ejecuta tools (consultas/pedido)
|
||||
7. Genera respuesta amigable
|
||||
8. Envia respuesta al cliente
|
||||
```
|
||||
|
||||
## Tools Limitados para Cliente
|
||||
|
||||
```typescript
|
||||
// Tools habilitados para clientes
|
||||
const customerTools = [
|
||||
'search_products',
|
||||
'get_product_price',
|
||||
'check_availability',
|
||||
'create_order',
|
||||
'get_my_balance',
|
||||
'get_business_info',
|
||||
'get_promotions'
|
||||
];
|
||||
|
||||
// Tools NO disponibles para clientes
|
||||
// - update_product_price
|
||||
// - get_sales_report
|
||||
// - send_payment_reminder
|
||||
// - etc.
|
||||
```
|
||||
|
||||
## Intents del Cliente
|
||||
|
||||
| Intent | Ejemplo | Accion |
|
||||
|--------|---------|--------|
|
||||
| CONSULTA_PRECIO | "Cuanto cuesta..." | search_products |
|
||||
| CONSULTA_DISPONIBILIDAD | "Tienen..." | check_availability |
|
||||
| HACER_PEDIDO | "Quiero pedir..." | create_order |
|
||||
| CONSULTA_SALDO | "Cuanto debo" | get_my_balance |
|
||||
| HORARIOS | "A que hora..." | get_business_info |
|
||||
| UBICACION | "Donde estan" | get_business_info |
|
||||
| PROMOCIONES | "Que ofertas tienen" | get_promotions |
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Asociacion Cliente-Telefono
|
||||
|
||||
```typescript
|
||||
// Al recibir mensaje de numero nuevo
|
||||
1. Buscar en customers por phone
|
||||
2. Si existe: asociar conversacion
|
||||
3. Si no existe: crear customer temporal
|
||||
4. Si hace pedido: pedir nombre
|
||||
```
|
||||
|
||||
## Limites y Seguridad
|
||||
|
||||
- Clientes NO pueden ver info de otros clientes
|
||||
- Clientes NO pueden modificar precios
|
||||
- Clientes NO pueden ver reportes
|
||||
- Rate limit: 20 mensajes/hora por cliente
|
||||
- Tokens: descontados de cuota del tenant
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| Customer chat flow | Pendiente | `whatsapp-service/flows/customer.flow.ts` |
|
||||
| MCP tools (limited) | Pendiente | `mcp-server/tools/customer/` |
|
||||
| Order creation | Pendiente | `backend/modules/orders/` |
|
||||
| FAQ responses | Pendiente | `whatsapp-service/faq/` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-010 (MCP Server)
|
||||
- MCH-011 (WhatsApp Service)
|
||||
- MCH-014 (Gestion Clientes)
|
||||
|
||||
### Bloquea a
|
||||
- MCH-015 (Pedidos via WhatsApp)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [ ] Consultas de precio funcionan
|
||||
- [ ] Consultas de disponibilidad funcionan
|
||||
- [ ] Pedidos se crean correctamente
|
||||
- [ ] Saldo de fiado se muestra
|
||||
- [ ] Info del negocio se muestra
|
||||
- [ ] No hay fuga de informacion
|
||||
|
||||
## Personalizacion por Tenant
|
||||
|
||||
```typescript
|
||||
// tenant_settings
|
||||
{
|
||||
whatsapp_bot: {
|
||||
greeting: "¡Hola! Bienvenido a {{business_name}}",
|
||||
farewell: "¡Gracias por tu preferencia!",
|
||||
tone: "friendly", // formal, friendly, casual
|
||||
auto_suggest_products: true,
|
||||
show_promotions: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,197 @@
|
||||
# MCH-014: Gestion de Clientes
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-014
|
||||
- **Fase:** 4 - Pedidos y Clientes
|
||||
- **Prioridad:** P1
|
||||
- **Estado:** Completado
|
||||
- **Fecha inicio:** 2026-01-06
|
||||
- **Fecha fin:** 2026-01-07
|
||||
|
||||
## Descripcion
|
||||
|
||||
Sistema completo de gestion de clientes: registro, historial de compras, saldo de fiado, comunicacion via WhatsApp, y segmentacion basica.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. CRUD de clientes
|
||||
2. Historial de compras por cliente
|
||||
3. Integracion con sistema de fiados
|
||||
4. Comunicacion via WhatsApp
|
||||
5. Segmentacion basica (frecuencia, monto)
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Registro de clientes (nombre, telefono, direccion)
|
||||
- Historial de compras
|
||||
- Saldo de fiado integrado
|
||||
- Envio de mensajes WhatsApp
|
||||
- Tags/etiquetas para segmentacion
|
||||
- Notas por cliente
|
||||
|
||||
### Excluido
|
||||
- CRM avanzado
|
||||
- Campanas de marketing automatizadas
|
||||
- Programas de lealtad (fase posterior)
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas (schema: customers)
|
||||
|
||||
**customers**
|
||||
- id, tenant_id, name, phone
|
||||
- email (opcional), address
|
||||
- credit_enabled, credit_limit, balance
|
||||
- tags (JSONB), notes, status
|
||||
- first_purchase_at, last_purchase_at
|
||||
- total_purchases, total_spent
|
||||
|
||||
**customer_purchases** (vista agregada)
|
||||
- Derivado de sales por customer_id
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| GET | /customers | Listar clientes |
|
||||
| GET | /customers/:id | Obtener cliente |
|
||||
| POST | /customers | Crear cliente |
|
||||
| PUT | /customers/:id | Actualizar cliente |
|
||||
| DELETE | /customers/:id | Eliminar cliente |
|
||||
| GET | /customers/:id/purchases | Historial compras |
|
||||
| GET | /customers/:id/credit | Estado de fiado |
|
||||
| POST | /customers/:id/message | Enviar WhatsApp |
|
||||
| GET | /customers/segments | Segmentos |
|
||||
|
||||
## Flujos de Usuario
|
||||
|
||||
### Registrar Cliente Nuevo
|
||||
```
|
||||
1. Durante venta, empleado pregunta nombre
|
||||
2. Ingresa telefono (obligatorio para fiado)
|
||||
3. Cliente creado con datos basicos
|
||||
4. Opcionalmente: direccion para entregas
|
||||
```
|
||||
|
||||
### Ver Historial de Cliente
|
||||
```
|
||||
1. Dueno busca cliente por nombre/telefono
|
||||
2. Abre ficha del cliente
|
||||
3. Ve:
|
||||
- Datos de contacto
|
||||
- Total comprado historico
|
||||
- Ultimas 10 compras
|
||||
- Saldo de fiado
|
||||
- Notas
|
||||
```
|
||||
|
||||
### Enviar Mensaje WhatsApp
|
||||
```
|
||||
1. Dueno abre ficha de cliente
|
||||
2. Click en "Enviar WhatsApp"
|
||||
3. Selecciona template o escribe mensaje
|
||||
4. Mensaje enviado via WhatsApp Service
|
||||
```
|
||||
|
||||
### Segmentar Clientes
|
||||
```
|
||||
1. Dueno abre lista de clientes
|
||||
2. Filtra por:
|
||||
- Con/sin fiado
|
||||
- Frecuencia (semanal, mensual, ocasional)
|
||||
- Monto (alto, medio, bajo)
|
||||
3. Puede asignar tags personalizados
|
||||
```
|
||||
|
||||
## UI Components
|
||||
|
||||
### CustomerList
|
||||
- Tabla con: nombre, telefono, total, fiado
|
||||
- Busqueda por nombre/telefono
|
||||
- Filtros por tags, estado
|
||||
|
||||
### CustomerForm
|
||||
- Campos: nombre, telefono, email, direccion
|
||||
- Toggle credito + limite
|
||||
- Tags
|
||||
- Notas
|
||||
|
||||
### CustomerDetail
|
||||
- Info de contacto
|
||||
- Metricas: total, # compras, promedio
|
||||
- Historial de compras
|
||||
- Historial de fiado
|
||||
- Boton WhatsApp
|
||||
|
||||
### CreditHistory
|
||||
- Lista de movimientos
|
||||
- Grafica de saldo en el tiempo
|
||||
- Botones: registrar abono, enviar recordatorio
|
||||
|
||||
## Segmentacion
|
||||
|
||||
### Por Frecuencia
|
||||
| Segmento | Criterio |
|
||||
|----------|----------|
|
||||
| Frecuente | >= 4 compras/mes |
|
||||
| Regular | 2-3 compras/mes |
|
||||
| Ocasional | 1 compra/mes |
|
||||
| Inactivo | 0 compras en 30 dias |
|
||||
|
||||
### Por Valor
|
||||
| Segmento | Criterio |
|
||||
|----------|----------|
|
||||
| Alto valor | >= $2000/mes |
|
||||
| Medio | $500-$2000/mes |
|
||||
| Bajo | < $500/mes |
|
||||
|
||||
### Tags Personalizados
|
||||
- Ejemplos: "vecino", "oficina", "escuela", "mayoreo"
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| DDL customers | Completado | `08-customers.sql` |
|
||||
| customers.module | Completado | `modules/customers/` |
|
||||
| Customers.tsx | Completado | `pages/Customers.tsx` |
|
||||
| CustomerList.tsx | Completado | `components/customers/` |
|
||||
| CustomerForm.tsx | Completado | `components/customers/` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-002 (Auth)
|
||||
- MCH-004 (Sales - para historial)
|
||||
|
||||
### Bloquea a
|
||||
- MCH-008 (Fiados)
|
||||
- MCH-013 (Chat cliente)
|
||||
- MCH-015 (Pedidos WhatsApp)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] CRUD de clientes funciona
|
||||
- [x] Historial de compras se muestra
|
||||
- [x] Fiado integrado correctamente
|
||||
- [x] Busqueda por nombre/telefono
|
||||
- [x] Tags funcionan
|
||||
- [x] Boton WhatsApp envia mensaje
|
||||
|
||||
## Metricas Calculadas
|
||||
|
||||
```typescript
|
||||
// Se actualizan en cada venta
|
||||
customer.total_purchases++
|
||||
customer.total_spent += sale.total
|
||||
customer.last_purchase_at = new Date()
|
||||
|
||||
// Calculo de segmento (batch job diario)
|
||||
customer.frequency_segment = calculateFrequency(customer)
|
||||
customer.value_segment = calculateValue(customer)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,196 @@
|
||||
# MCH-015: Pedidos via WhatsApp
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-015
|
||||
- **Fase:** 4 - Pedidos y Clientes
|
||||
- **Prioridad:** P1
|
||||
- **Estado:** Pendiente
|
||||
- **Fecha estimada:** Sprint 9-10
|
||||
|
||||
## Descripcion
|
||||
|
||||
Sistema completo de pedidos via WhatsApp: clientes pueden hacer pedidos conversando con el bot, el negocio recibe notificaciones, procesa el pedido, y confirma al cliente.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Recepcion de pedidos via chat
|
||||
2. Confirmacion automatica o manual
|
||||
3. Notificacion al negocio
|
||||
4. Estado del pedido en tiempo real
|
||||
5. Historial de pedidos por cliente
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Pedidos via chat natural (NLU)
|
||||
- Carrito conversacional
|
||||
- Confirmacion de pedido
|
||||
- Notificacion push/WhatsApp al dueno
|
||||
- Estados: recibido, preparando, listo, entregado
|
||||
- Cancelacion de pedido
|
||||
|
||||
### Excluido
|
||||
- Pagos en linea dentro de WhatsApp
|
||||
- Integracion con delivery apps (Rappi, Uber)
|
||||
- Pedidos programados
|
||||
|
||||
## Flujo de Pedido
|
||||
|
||||
### Cliente Hace Pedido
|
||||
```
|
||||
Cliente: "Quiero pedir 2 Cocas y unas Sabritas"
|
||||
Bot: "Perfecto! Tu pedido:
|
||||
- 2x Coca-Cola 600ml = $36
|
||||
- 1x Sabritas Original = $18
|
||||
Total: $54
|
||||
|
||||
¿Lo confirmas?
|
||||
[Confirmar] [Agregar mas] [Cancelar]"
|
||||
|
||||
Cliente: [Confirmar]
|
||||
|
||||
Bot: "¡Pedido #456 confirmado! 🎉
|
||||
Te avisamos cuando este listo.
|
||||
Tiempo estimado: 10-15 min"
|
||||
```
|
||||
|
||||
### Negocio Recibe Notificacion
|
||||
```
|
||||
[Push + WhatsApp al dueno]
|
||||
"🛒 Nuevo Pedido #456
|
||||
|
||||
Cliente: Juan Perez (5512345678)
|
||||
- 2x Coca-Cola 600ml
|
||||
- 1x Sabritas Original
|
||||
Total: $54
|
||||
|
||||
[Ver pedido] [Aceptar] [Rechazar]"
|
||||
```
|
||||
|
||||
### Actualizacion de Estado
|
||||
```
|
||||
[Dueno marca como "Listo"]
|
||||
|
||||
Bot -> Cliente:
|
||||
"Tu pedido #456 esta listo! 📦
|
||||
Puedes pasar a recogerlo.
|
||||
|
||||
Direccion: Calle Principal #123
|
||||
Horario: Abierto hasta 10pm"
|
||||
```
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas (schema: orders)
|
||||
|
||||
**orders**
|
||||
- id, tenant_id, customer_id, channel
|
||||
- status (pending/confirmed/preparing/ready/delivered/cancelled)
|
||||
- subtotal, delivery_fee, total
|
||||
- delivery_type (pickup/delivery)
|
||||
- delivery_address, scheduled_at
|
||||
- notes, created_at, updated_at
|
||||
|
||||
**order_items**
|
||||
- id, order_id, product_id
|
||||
- quantity, unit_price, notes, total
|
||||
|
||||
**order_status_history**
|
||||
- id, order_id, status, changed_by
|
||||
- notes, created_at
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| GET | /orders | Listar pedidos |
|
||||
| GET | /orders/:id | Obtener pedido |
|
||||
| POST | /orders | Crear pedido |
|
||||
| PUT | /orders/:id/status | Cambiar estado |
|
||||
| POST | /orders/:id/cancel | Cancelar pedido |
|
||||
| GET | /orders/pending | Pedidos pendientes |
|
||||
| GET | /customers/:id/orders | Pedidos de cliente |
|
||||
|
||||
## Estados del Pedido
|
||||
|
||||
```
|
||||
pending ──► confirmed ──► preparing ──► ready ──► delivered
|
||||
│ │ │ │
|
||||
└───────────┴─────────────┴───────────┴──► cancelled
|
||||
```
|
||||
|
||||
| Estado | Descripcion | Notifica Cliente |
|
||||
|--------|-------------|------------------|
|
||||
| pending | Pedido recibido | Si |
|
||||
| confirmed | Aceptado por negocio | Si |
|
||||
| preparing | En preparacion | No |
|
||||
| ready | Listo para recoger/enviar | Si |
|
||||
| delivered | Entregado | Si |
|
||||
| cancelled | Cancelado | Si |
|
||||
|
||||
## UI Components
|
||||
|
||||
### OrderList (Dashboard)
|
||||
- Tabla de pedidos del dia
|
||||
- Filtros por estado
|
||||
- Acciones rapidas
|
||||
|
||||
### OrderDetail
|
||||
- Info del cliente
|
||||
- Items del pedido
|
||||
- Cambio de estado
|
||||
- Historial de estados
|
||||
|
||||
### OrderNotification (Mobile)
|
||||
- Push notification
|
||||
- Sonido distintivo
|
||||
- Accion rapida: Aceptar/Rechazar
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| orders.module | Completado | `modules/orders/` |
|
||||
| WhatsApp order flow | Pendiente | `whatsapp-service/flows/order.flow.ts` |
|
||||
| Order notifications | Pendiente | `services/order-notification.service.ts` |
|
||||
| Orders dashboard | Pendiente | `pages/Orders.tsx` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-011 (WhatsApp Service)
|
||||
- MCH-013 (Chat cliente)
|
||||
- MCH-014 (Gestion clientes)
|
||||
|
||||
### Bloquea a
|
||||
- MCH-016 (Entregas a domicilio)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [ ] Pedidos se crean via WhatsApp
|
||||
- [ ] Dueno recibe notificacion inmediata
|
||||
- [ ] Estados se actualizan correctamente
|
||||
- [ ] Cliente recibe confirmaciones
|
||||
- [ ] Historial de pedidos funciona
|
||||
|
||||
## Configuracion por Tenant
|
||||
|
||||
```typescript
|
||||
{
|
||||
orders: {
|
||||
enabled: true,
|
||||
auto_confirm: false, // o true para confirmar automaticamente
|
||||
estimated_time_minutes: 15,
|
||||
channels: ['whatsapp', 'web'],
|
||||
notifications: {
|
||||
push: true,
|
||||
whatsapp: true,
|
||||
sound: 'order_received'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,181 @@
|
||||
# MCH-016: Entregas a Domicilio
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-016
|
||||
- **Fase:** 4 - Pedidos y Clientes
|
||||
- **Prioridad:** P2
|
||||
- **Estado:** Pendiente
|
||||
- **Fecha estimada:** Sprint 10-11
|
||||
|
||||
## Descripcion
|
||||
|
||||
Sistema de entregas a domicilio para micro-negocios: definicion de zonas de cobertura, costos de envio, asignacion de repartidores, y tracking basico del pedido.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Definir zonas de cobertura
|
||||
2. Configurar costos de envio
|
||||
3. Asignar repartidores
|
||||
4. Tracking basico de entrega
|
||||
5. Confirmacion de entrega
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Zonas de cobertura (radio o colonias)
|
||||
- Costo de envio fijo o por zona
|
||||
- Asignacion manual de repartidor
|
||||
- Estados: asignado, en camino, entregado
|
||||
- Confirmacion con foto/firma
|
||||
|
||||
### Excluido
|
||||
- GPS tracking en tiempo real
|
||||
- Optimizacion de rutas
|
||||
- Integracion con apps de delivery
|
||||
- Repartidores externos (Rappi, Uber)
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas Adicionales
|
||||
|
||||
**delivery_zones**
|
||||
- id, tenant_id, name, type (radius/polygon)
|
||||
- coordinates (JSONB), delivery_fee
|
||||
- min_order, estimated_time, active
|
||||
|
||||
**deliveries**
|
||||
- id, order_id, assigned_to (user_id)
|
||||
- status, pickup_at, delivered_at
|
||||
- delivery_address, notes
|
||||
- proof_photo_url, signature_url
|
||||
|
||||
## Flujo de Entrega
|
||||
|
||||
### Cliente Solicita Delivery
|
||||
```
|
||||
Bot: "¿Como quieres recibir tu pedido?
|
||||
[Paso a recoger] [Enviar a domicilio]"
|
||||
|
||||
Cliente: [Enviar a domicilio]
|
||||
|
||||
Bot: "¿A que direccion lo enviamos?"
|
||||
|
||||
Cliente: "Calle Hidalgo 45, Col. Centro"
|
||||
|
||||
Bot: "Perfecto! Envio a Col. Centro = $25
|
||||
Total con envio: $79
|
||||
Tiempo estimado: 30-45 min
|
||||
|
||||
¿Confirmas?
|
||||
[Confirmar] [Cambiar direccion]"
|
||||
```
|
||||
|
||||
### Asignacion de Repartidor
|
||||
```
|
||||
1. Pedido confirmado con delivery
|
||||
2. Dueno ve pedido en dashboard
|
||||
3. Asigna a repartidor (empleado)
|
||||
4. Repartidor recibe notificacion
|
||||
5. Cliente notificado: "Tu pedido va en camino"
|
||||
```
|
||||
|
||||
### Confirmacion de Entrega
|
||||
```
|
||||
1. Repartidor llega a direccion
|
||||
2. Entrega pedido
|
||||
3. Toma foto de entrega (opcional)
|
||||
4. Marca como entregado en app
|
||||
5. Cliente recibe: "Pedido entregado!"
|
||||
```
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| GET | /delivery/zones | Zonas de cobertura |
|
||||
| POST | /delivery/zones | Crear zona |
|
||||
| PUT | /delivery/zones/:id | Editar zona |
|
||||
| DELETE | /delivery/zones/:id | Eliminar zona |
|
||||
| POST | /delivery/check-coverage | Verificar cobertura |
|
||||
| POST | /orders/:id/assign-delivery | Asignar repartidor |
|
||||
| PUT | /deliveries/:id/status | Actualizar estado |
|
||||
| POST | /deliveries/:id/confirm | Confirmar entrega |
|
||||
|
||||
## Configuracion de Zonas
|
||||
|
||||
### Por Radio
|
||||
```typescript
|
||||
{
|
||||
type: 'radius',
|
||||
center: { lat: 19.4326, lng: -99.1332 },
|
||||
radius_km: 3,
|
||||
delivery_fee: 25,
|
||||
estimated_time: 30
|
||||
}
|
||||
```
|
||||
|
||||
### Por Colonias
|
||||
```typescript
|
||||
{
|
||||
type: 'polygon',
|
||||
name: 'Centro',
|
||||
colonias: ['Centro', 'Roma Norte', 'Condesa'],
|
||||
delivery_fee: 30,
|
||||
estimated_time: 45
|
||||
}
|
||||
```
|
||||
|
||||
## UI Components
|
||||
|
||||
### DeliveryZonesMap
|
||||
- Mapa con zonas definidas
|
||||
- Edicion visual de poligonos
|
||||
- Configuracion de tarifas
|
||||
|
||||
### DeliveryAssignment
|
||||
- Lista de pedidos pendientes de asignar
|
||||
- Dropdown de repartidores disponibles
|
||||
- Boton asignar
|
||||
|
||||
### DeliveryTracking (Mobile - Repartidor)
|
||||
- Lista de entregas asignadas
|
||||
- Boton "En camino"
|
||||
- Boton "Entregado" + foto
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| delivery.module | Pendiente | `modules/delivery/` |
|
||||
| delivery_zones tabla | Pendiente | DDL |
|
||||
| DeliveryZonesMap | Pendiente | `components/delivery/` |
|
||||
| Mobile delivery screen | Pendiente | `mobile/screens/Delivery.tsx` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-015 (Pedidos WhatsApp)
|
||||
- MCH-002 (Auth - para repartidores)
|
||||
|
||||
### Bloquea a
|
||||
- Ninguno
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [ ] Zonas de cobertura se configuran
|
||||
- [ ] Costo de envio se calcula correctamente
|
||||
- [ ] Repartidor recibe notificacion
|
||||
- [ ] Estados de entrega funcionan
|
||||
- [ ] Confirmacion con foto funciona
|
||||
|
||||
## Roles
|
||||
|
||||
| Rol | Permisos |
|
||||
|-----|----------|
|
||||
| owner | Configurar zonas, ver todas las entregas |
|
||||
| employee | Asignar entregas, ver entregas |
|
||||
| delivery | Ver entregas asignadas, actualizar estado |
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,197 @@
|
||||
# MCH-017: Notificaciones
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-017
|
||||
- **Fase:** 4 - Pedidos y Clientes
|
||||
- **Prioridad:** P1
|
||||
- **Estado:** Pendiente
|
||||
- **Fecha estimada:** Sprint 8-9
|
||||
|
||||
## Descripcion
|
||||
|
||||
Sistema centralizado de notificaciones multi-canal: push notifications, WhatsApp, y SMS. Soporta notificaciones transaccionales, recordatorios, y alertas de negocio.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Push notifications (Firebase)
|
||||
2. Notificaciones WhatsApp
|
||||
3. SMS como fallback
|
||||
4. Configuracion por usuario
|
||||
5. Historial de notificaciones
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Push via Firebase Cloud Messaging
|
||||
- WhatsApp via MCH-011
|
||||
- SMS via Twilio (fallback)
|
||||
- Preferencias por usuario
|
||||
- Templates de notificacion
|
||||
- Programacion de envios
|
||||
|
||||
### Excluido
|
||||
- Email (no prioritario para micro-negocios)
|
||||
- Notificaciones in-app complejas
|
||||
- Marketing automation
|
||||
|
||||
## Tipos de Notificacion
|
||||
|
||||
### Transaccionales (Inmediatas)
|
||||
| Evento | Canal Default | Mensaje |
|
||||
|--------|---------------|---------|
|
||||
| Nuevo pedido | Push + WhatsApp | "Nuevo pedido #123" |
|
||||
| Pedido listo | WhatsApp | "Tu pedido esta listo" |
|
||||
| Pago recibido | Push | "Pago de $500 recibido" |
|
||||
| Stock bajo | Push | "Coca-Cola: quedan 5" |
|
||||
|
||||
### Recordatorios (Programados)
|
||||
| Tipo | Canal | Frecuencia |
|
||||
|------|-------|------------|
|
||||
| Fiado pendiente | WhatsApp | Segun config |
|
||||
| Reporte semanal | WhatsApp | Lunes 8am |
|
||||
| Cierre de caja | Push | Diario 9pm |
|
||||
|
||||
### Alertas de Negocio
|
||||
| Alerta | Canal | Trigger |
|
||||
|--------|-------|---------|
|
||||
| Stock bajo | Push | stock < min_stock |
|
||||
| Venta grande | Push | sale.total > threshold |
|
||||
| Nuevo cliente | Push | customer.created |
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas (schema: notifications)
|
||||
|
||||
**notification_templates**
|
||||
- id, tenant_id, code, channel
|
||||
- title, body, variables
|
||||
- active
|
||||
|
||||
**notifications**
|
||||
- id, tenant_id, user_id, type
|
||||
- channel, title, body
|
||||
- status (pending/sent/delivered/failed)
|
||||
- scheduled_at, sent_at, read_at
|
||||
|
||||
**notification_preferences**
|
||||
- id, user_id, channel
|
||||
- enabled, quiet_hours_start, quiet_hours_end
|
||||
|
||||
**device_tokens**
|
||||
- id, user_id, platform (ios/android/web)
|
||||
- token, active, created_at
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| POST | /notifications/send | Enviar notificacion |
|
||||
| GET | /notifications | Historial |
|
||||
| PUT | /notifications/:id/read | Marcar como leida |
|
||||
| GET | /notifications/preferences | Preferencias |
|
||||
| PUT | /notifications/preferences | Actualizar prefs |
|
||||
| POST | /notifications/register-device | Registrar token |
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────────┐
|
||||
│ Trigger │────▶│ Notification │
|
||||
│ (Event) │ │ Service │
|
||||
└─────────────┘ └────────┬────────┘
|
||||
│
|
||||
┌───────────────────┼───────────────────┐
|
||||
│ │ │
|
||||
┌──────▼──────┐ ┌───────▼───────┐ ┌──────▼──────┐
|
||||
│ Firebase │ │ WhatsApp │ │ Twilio │
|
||||
│ FCM │ │ Service │ │ SMS │
|
||||
└─────────────┘ └───────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
## Templates de Notificacion
|
||||
|
||||
### Push - Nuevo Pedido
|
||||
```json
|
||||
{
|
||||
"code": "new_order",
|
||||
"channel": "push",
|
||||
"title": "🛒 Nuevo Pedido",
|
||||
"body": "Pedido #{{order_id}} de {{customer_name}} por ${{total}}"
|
||||
}
|
||||
```
|
||||
|
||||
### WhatsApp - Pedido Listo
|
||||
```
|
||||
¡Tu pedido #{{order_id}} esta listo! 📦
|
||||
|
||||
{{items_summary}}
|
||||
|
||||
Total: ${{total}}
|
||||
|
||||
Puedes pasar a recogerlo a:
|
||||
{{business_address}}
|
||||
```
|
||||
|
||||
### WhatsApp - Recordatorio Fiado
|
||||
```
|
||||
Hola {{customer_name}}, te recordamos que tienes
|
||||
un saldo pendiente de ${{balance}} en {{business_name}}.
|
||||
|
||||
¿Cuando podrias pasar a liquidar?
|
||||
```
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| notifications.module | Pendiente | `modules/notifications/` |
|
||||
| Firebase integration | Pendiente | `providers/firebase.provider.ts` |
|
||||
| Twilio integration | Pendiente | `providers/twilio.provider.ts` |
|
||||
| Notification preferences UI | Pendiente | `components/settings/` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-011 (WhatsApp Service)
|
||||
- MCH-002 (Auth - usuarios)
|
||||
|
||||
### Bloquea a
|
||||
- MCH-008 (Recordatorios fiado)
|
||||
- MCH-015 (Notificaciones pedido)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [ ] Push notifications funcionan (iOS/Android)
|
||||
- [ ] WhatsApp notifications funcionan
|
||||
- [ ] SMS fallback funciona
|
||||
- [ ] Preferencias se respetan
|
||||
- [ ] Historial se guarda correctamente
|
||||
|
||||
## Configuracion
|
||||
|
||||
### Firebase
|
||||
```typescript
|
||||
{
|
||||
firebase: {
|
||||
projectId: process.env.FIREBASE_PROJECT_ID,
|
||||
privateKey: process.env.FIREBASE_PRIVATE_KEY,
|
||||
clientEmail: process.env.FIREBASE_CLIENT_EMAIL
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Quiet Hours
|
||||
```typescript
|
||||
// Por usuario
|
||||
{
|
||||
quiet_hours: {
|
||||
enabled: true,
|
||||
start: '22:00',
|
||||
end: '08:00'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,225 @@
|
||||
# MCH-018: Planes y Suscripciones
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-018
|
||||
- **Fase:** 5 - Monetizacion
|
||||
- **Prioridad:** P0
|
||||
- **Estado:** Completado
|
||||
- **Fecha inicio:** 2026-01-06
|
||||
- **Fecha fin:** 2026-01-07
|
||||
|
||||
## Descripcion
|
||||
|
||||
Sistema de planes de suscripcion para monetizar MiChangarrito: dos planes principales (Changarrito y Tiendita), facturacion mensual, y gestion de ciclos de pago.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Definir planes disponibles
|
||||
2. Proceso de suscripcion
|
||||
3. Facturacion recurrente
|
||||
4. Gestion de ciclo de vida
|
||||
5. Upgrade/downgrade de plan
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Plan Changarrito ($99/mes)
|
||||
- Plan Tiendita ($199/mes)
|
||||
- Trial de 14 dias
|
||||
- Facturacion via Stripe
|
||||
- Cancelacion y pausas
|
||||
- Historial de facturacion
|
||||
|
||||
### Excluido
|
||||
- Planes anuales (fase posterior)
|
||||
- Planes enterprise personalizados
|
||||
- Facturacion fiscal mexicana (MCH-027)
|
||||
|
||||
## Planes
|
||||
|
||||
### Plan Changarrito - $99/mes
|
||||
```
|
||||
✓ App movil completa
|
||||
✓ Punto de venta basico
|
||||
✓ Hasta 500 productos
|
||||
✓ 1 usuario
|
||||
✓ 500 tokens IA/mes
|
||||
✓ Soporte por WhatsApp
|
||||
✗ WhatsApp Business propio
|
||||
✗ Predicciones de inventario
|
||||
```
|
||||
|
||||
### Plan Tiendita - $199/mes
|
||||
```
|
||||
✓ Todo de Changarrito
|
||||
✓ Productos ilimitados
|
||||
✓ Hasta 5 usuarios
|
||||
✓ 2,000 tokens IA/mes
|
||||
✓ WhatsApp Business propio
|
||||
✓ Predicciones de inventario
|
||||
✓ Reportes avanzados
|
||||
✓ Entregas a domicilio
|
||||
✓ Soporte prioritario
|
||||
```
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas (schema: subscriptions)
|
||||
|
||||
**plans**
|
||||
- id, name, code, price
|
||||
- currency, interval (month/year)
|
||||
- features (JSONB), token_quota
|
||||
- max_products, max_users
|
||||
- stripe_price_id, status
|
||||
|
||||
**subscriptions**
|
||||
- id, tenant_id, plan_id
|
||||
- status (trialing/active/past_due/cancelled)
|
||||
- stripe_subscription_id
|
||||
- current_period_start, current_period_end
|
||||
- trial_end, cancelled_at
|
||||
|
||||
**invoices**
|
||||
- id, subscription_id, amount
|
||||
- status, stripe_invoice_id
|
||||
- paid_at, pdf_url
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| GET | /subscriptions/plans | Listar planes |
|
||||
| GET | /subscriptions/current | Suscripcion actual |
|
||||
| POST | /subscriptions/subscribe | Suscribirse |
|
||||
| POST | /subscriptions/cancel | Cancelar |
|
||||
| POST | /subscriptions/resume | Reanudar |
|
||||
| PUT | /subscriptions/change-plan | Cambiar plan |
|
||||
| GET | /subscriptions/invoices | Historial facturas |
|
||||
| POST | /subscriptions/webhook | Webhook Stripe |
|
||||
|
||||
## Flujos
|
||||
|
||||
### Nueva Suscripcion
|
||||
```
|
||||
1. Usuario selecciona plan
|
||||
2. Ingresa metodo de pago (Stripe)
|
||||
3. Se crea suscripcion con trial
|
||||
4. Usuario tiene acceso inmediato
|
||||
5. Al terminar trial, se cobra automaticamente
|
||||
```
|
||||
|
||||
### Cancelacion
|
||||
```
|
||||
1. Usuario solicita cancelar
|
||||
2. Confirmacion requerida
|
||||
3. Suscripcion marcada para cancelar
|
||||
4. Acceso hasta fin del periodo
|
||||
5. Datos preservados 30 dias
|
||||
```
|
||||
|
||||
### Upgrade
|
||||
```
|
||||
1. Usuario en Changarrito
|
||||
2. Solicita upgrade a Tiendita
|
||||
3. Se calcula prorateo
|
||||
4. Pago de diferencia
|
||||
5. Features activadas inmediatamente
|
||||
```
|
||||
|
||||
## Estados de Suscripcion
|
||||
|
||||
```
|
||||
trialing ──► active ──► past_due ──► cancelled
|
||||
│ │
|
||||
└───────────┴──► paused
|
||||
```
|
||||
|
||||
| Estado | Descripcion | Acceso |
|
||||
|--------|-------------|--------|
|
||||
| trialing | En periodo de prueba | Completo |
|
||||
| active | Pagando normalmente | Completo |
|
||||
| past_due | Pago fallido (grace period) | Limitado |
|
||||
| cancelled | Cancelada | Sin acceso |
|
||||
| paused | Pausada temporalmente | Sin acceso |
|
||||
|
||||
## Integracion Stripe
|
||||
|
||||
### Subscription Billing
|
||||
```typescript
|
||||
const subscription = await stripe.subscriptions.create({
|
||||
customer: customer.stripe_id,
|
||||
items: [{ price: plan.stripe_price_id }],
|
||||
trial_period_days: 14,
|
||||
payment_behavior: 'default_incomplete',
|
||||
expand: ['latest_invoice.payment_intent']
|
||||
});
|
||||
```
|
||||
|
||||
### Webhooks Manejados
|
||||
- `customer.subscription.created`
|
||||
- `customer.subscription.updated`
|
||||
- `customer.subscription.deleted`
|
||||
- `invoice.payment_succeeded`
|
||||
- `invoice.payment_failed`
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| DDL subscriptions | Completado | `10-subscriptions.sql` |
|
||||
| subscriptions.module | Completado | `modules/subscriptions/` |
|
||||
| Stripe integration | Completado | `providers/stripe.provider.ts` |
|
||||
| Plans UI | Pendiente | `pages/Plans.tsx` |
|
||||
| Billing UI | Pendiente | `pages/Billing.tsx` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-005 (Stripe integration base)
|
||||
- MCH-002 (Auth)
|
||||
|
||||
### Bloquea a
|
||||
- MCH-019 (Tokens)
|
||||
- MCH-020 (Pagos online)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] Planes se muestran correctamente
|
||||
- [x] Suscripcion se crea en Stripe
|
||||
- [x] Trial de 14 dias funciona
|
||||
- [x] Cobro recurrente funciona
|
||||
- [x] Cancelacion funciona
|
||||
- [ ] Upgrade/downgrade funciona
|
||||
|
||||
## Configuracion
|
||||
|
||||
```typescript
|
||||
// plans seed
|
||||
[
|
||||
{
|
||||
name: 'Changarrito',
|
||||
code: 'changarrito',
|
||||
price: 99,
|
||||
currency: 'MXN',
|
||||
token_quota: 500,
|
||||
max_products: 500,
|
||||
max_users: 1,
|
||||
stripe_price_id: 'price_xxx'
|
||||
},
|
||||
{
|
||||
name: 'Tiendita',
|
||||
code: 'tiendita',
|
||||
price: 199,
|
||||
currency: 'MXN',
|
||||
token_quota: 2000,
|
||||
max_products: null, // unlimited
|
||||
max_users: 5,
|
||||
stripe_price_id: 'price_yyy'
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,194 @@
|
||||
# MCH-019: Tienda de Tokens
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-019
|
||||
- **Fase:** 5 - Monetizacion
|
||||
- **Prioridad:** P1
|
||||
- **Estado:** Pendiente
|
||||
- **Fecha estimada:** Sprint 11-12
|
||||
|
||||
## Descripcion
|
||||
|
||||
Sistema de compra de tokens adicionales para funciones de IA: paquetes de tokens, saldo disponible, consumo por operacion, y alertas de saldo bajo.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Paquetes de tokens disponibles
|
||||
2. Compra via Stripe/OXXO
|
||||
3. Saldo y consumo visible
|
||||
4. Alertas de saldo bajo
|
||||
5. Historial de consumo
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- 4 paquetes de tokens
|
||||
- Compra con tarjeta (Stripe)
|
||||
- Compra en OXXO (Stripe)
|
||||
- Saldo en tiempo real
|
||||
- Consumo por operacion
|
||||
- Alertas configurables
|
||||
|
||||
### Excluido
|
||||
- Tokens como moneda interna
|
||||
- Transferencia entre usuarios
|
||||
- Venta de tokens a terceros
|
||||
|
||||
## Paquetes de Tokens
|
||||
|
||||
| Paquete | Tokens | Precio | Precio/Token |
|
||||
|---------|--------|--------|--------------|
|
||||
| Basico | 1,000 | $29 | $0.029 |
|
||||
| Popular | 3,000 | $69 | $0.023 |
|
||||
| Pro | 8,000 | $149 | $0.019 |
|
||||
| Ultra | 20,000 | $299 | $0.015 |
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas (schema: subscriptions)
|
||||
|
||||
**token_packages**
|
||||
- id, name, tokens, price
|
||||
- currency, stripe_price_id
|
||||
- popular (boolean), active
|
||||
|
||||
**token_purchases**
|
||||
- id, tenant_id, package_id
|
||||
- tokens, amount, status
|
||||
- stripe_payment_id, purchased_at
|
||||
|
||||
**token_usage**
|
||||
- id, tenant_id, operation
|
||||
- tokens_used, metadata (JSONB)
|
||||
- created_at
|
||||
|
||||
**token_balances** (materialized view o cache)
|
||||
- tenant_id, balance, last_updated
|
||||
|
||||
## Consumo de Tokens
|
||||
|
||||
### Operaciones y Costos
|
||||
| Operacion | Tokens | Descripcion |
|
||||
|-----------|--------|-------------|
|
||||
| chat_message | 3-10 | Segun longitud |
|
||||
| product_ocr | 5 | Reconocimiento de producto |
|
||||
| audio_transcription | 8 | Transcripcion de audio |
|
||||
| sales_report | 15 | Generacion de reporte |
|
||||
| inventory_prediction | 20 | Prediccion de demanda |
|
||||
|
||||
### Calculo de Consumo
|
||||
```typescript
|
||||
// Basado en tokens del LLM
|
||||
const tokensUsed = Math.ceil(
|
||||
(inputTokens + outputTokens) / 100
|
||||
);
|
||||
```
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| GET | /tokens/packages | Paquetes disponibles |
|
||||
| GET | /tokens/balance | Saldo actual |
|
||||
| POST | /tokens/purchase | Comprar paquete |
|
||||
| GET | /tokens/usage | Historial de consumo |
|
||||
| GET | /tokens/usage/summary | Resumen de consumo |
|
||||
|
||||
## Flujos
|
||||
|
||||
### Compra de Tokens
|
||||
```
|
||||
1. Usuario ve que tiene saldo bajo
|
||||
2. Abre tienda de tokens
|
||||
3. Selecciona paquete
|
||||
4. Paga con tarjeta o elige OXXO
|
||||
5. Si tarjeta: tokens acreditados inmediatamente
|
||||
6. Si OXXO: tokens acreditados al confirmar pago
|
||||
```
|
||||
|
||||
### Consumo de Tokens
|
||||
```
|
||||
1. Usuario hace pregunta al chat IA
|
||||
2. Sistema verifica saldo
|
||||
3. Si suficiente: procesa pregunta
|
||||
4. Descuenta tokens usados
|
||||
5. Si bajo: alerta de saldo bajo
|
||||
6. Si insuficiente: sugiere comprar mas
|
||||
```
|
||||
|
||||
### Alerta de Saldo Bajo
|
||||
```
|
||||
[Push + WhatsApp]
|
||||
"⚠️ Tu saldo de tokens esta bajo
|
||||
|
||||
Te quedan 45 tokens (~5 consultas).
|
||||
Recarga para seguir usando el asistente IA.
|
||||
|
||||
[Comprar tokens]"
|
||||
```
|
||||
|
||||
## UI Components
|
||||
|
||||
### TokenBalance (Header)
|
||||
- Icono de moneda
|
||||
- Saldo actual
|
||||
- Click para ver tienda
|
||||
|
||||
### TokenShop
|
||||
- Grid de paquetes
|
||||
- Precio y ahorro
|
||||
- Boton comprar
|
||||
|
||||
### TokenUsageHistory
|
||||
- Tabla de operaciones
|
||||
- Fecha, tipo, tokens
|
||||
- Grafica de consumo
|
||||
|
||||
### TokenLowAlert
|
||||
- Modal o banner
|
||||
- Saldo actual
|
||||
- CTA comprar
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| token_packages seed | Completado | `seeds/token-packages.sql` |
|
||||
| tokens.service | Pendiente | `services/tokens.service.ts` |
|
||||
| TokenShop UI | Pendiente | `components/tokens/TokenShop.tsx` |
|
||||
| TokenBalance UI | Pendiente | `components/tokens/TokenBalance.tsx` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-018 (Suscripciones - Stripe setup)
|
||||
- MCH-010 (MCP Server - consumo)
|
||||
|
||||
### Bloquea a
|
||||
- MCH-012, MCH-013 (Uso de tokens en chats)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [ ] Paquetes se muestran correctamente
|
||||
- [ ] Compra con tarjeta funciona
|
||||
- [ ] Compra con OXXO funciona
|
||||
- [ ] Saldo se actualiza en tiempo real
|
||||
- [ ] Consumo se registra por operacion
|
||||
- [ ] Alertas de saldo bajo funcionan
|
||||
|
||||
## Configuracion por Tenant
|
||||
|
||||
```typescript
|
||||
// Umbrales de alerta
|
||||
{
|
||||
tokens: {
|
||||
low_balance_threshold: 100,
|
||||
critical_balance_threshold: 20,
|
||||
alert_channels: ['push', 'whatsapp']
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,238 @@
|
||||
# MCH-020: Pagos de Suscripcion
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-020
|
||||
- **Fase:** 5 - Monetizacion
|
||||
- **Prioridad:** P0
|
||||
- **Estado:** Completado
|
||||
- **Fecha completado:** 2026-01-10
|
||||
|
||||
## Descripcion
|
||||
|
||||
Sistema completo de pagos para suscripciones y tokens: Stripe para tarjetas y OXXO, In-App Purchase para iOS/Android, y manejo de pagos fallidos.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Pagos con tarjeta (Stripe)
|
||||
2. Pagos en OXXO (Stripe)
|
||||
3. In-App Purchase iOS
|
||||
4. In-App Purchase Android
|
||||
5. Manejo de pagos fallidos
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Stripe Checkout
|
||||
- OXXO Pay (via Stripe)
|
||||
- Apple In-App Purchase
|
||||
- Google Play Billing
|
||||
- Reintentos automaticos
|
||||
- Recibos por email
|
||||
|
||||
### Excluido
|
||||
- PayPal
|
||||
- Transferencia bancaria manual
|
||||
- Criptomonedas
|
||||
|
||||
## Metodos de Pago
|
||||
|
||||
### Stripe - Tarjeta
|
||||
```
|
||||
Comision: ~3.6% + $3 MXN
|
||||
Confirmacion: Inmediata
|
||||
Uso: Web y App (via Stripe SDK)
|
||||
```
|
||||
|
||||
### Stripe - OXXO
|
||||
```
|
||||
Comision: ~$10-15 MXN fijo
|
||||
Confirmacion: 24-72 horas
|
||||
Uso: Cliente paga en OXXO
|
||||
Vencimiento: 3 dias
|
||||
```
|
||||
|
||||
### Apple In-App Purchase
|
||||
```
|
||||
Comision: 15-30%
|
||||
Confirmacion: Inmediata
|
||||
Uso: App iOS
|
||||
Nota: Obligatorio para apps iOS
|
||||
```
|
||||
|
||||
### Google Play Billing
|
||||
```
|
||||
Comision: 15%
|
||||
Confirmacion: Inmediata
|
||||
Uso: App Android
|
||||
```
|
||||
|
||||
## Flujos de Pago
|
||||
|
||||
### Pago con Tarjeta
|
||||
```
|
||||
1. Usuario selecciona plan/tokens
|
||||
2. Elige "Pagar con tarjeta"
|
||||
3. Stripe Checkout se abre
|
||||
4. Ingresa datos de tarjeta
|
||||
5. Pago procesado
|
||||
6. Redirige a app con confirmacion
|
||||
7. Suscripcion/tokens activados
|
||||
```
|
||||
|
||||
### Pago en OXXO
|
||||
```
|
||||
1. Usuario selecciona plan/tokens
|
||||
2. Elige "Pagar en OXXO"
|
||||
3. Se genera referencia OXXO
|
||||
4. Se muestra:
|
||||
- Monto a pagar
|
||||
- Referencia/codigo de barras
|
||||
- Fecha limite
|
||||
5. Usuario va a OXXO y paga
|
||||
6. Webhook confirma pago (horas despues)
|
||||
7. Suscripcion/tokens activados
|
||||
8. Notificacion al usuario
|
||||
```
|
||||
|
||||
### In-App Purchase (iOS)
|
||||
```
|
||||
1. Usuario abre tienda en app
|
||||
2. Selecciona producto
|
||||
3. StoreKit muestra sheet de Apple
|
||||
4. Usuario confirma con Face ID
|
||||
5. Apple procesa pago
|
||||
6. App recibe notificacion
|
||||
7. Backend valida con Apple
|
||||
8. Suscripcion/tokens activados
|
||||
```
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas Adicionales
|
||||
|
||||
**payment_methods**
|
||||
- id, tenant_id, type (card/oxxo/iap)
|
||||
- provider, last4, brand
|
||||
- is_default, stripe_pm_id
|
||||
|
||||
**payments**
|
||||
- id, tenant_id, type (subscription/tokens)
|
||||
- amount, currency, status
|
||||
- provider, provider_id
|
||||
- metadata (JSONB)
|
||||
|
||||
**oxxo_vouchers**
|
||||
- id, payment_id, reference
|
||||
- barcode_url, expires_at
|
||||
- status
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| POST | /payments/create-checkout | Crear sesion Stripe |
|
||||
| POST | /payments/create-oxxo | Generar voucher OXXO |
|
||||
| POST | /payments/verify-iap | Verificar IAP |
|
||||
| GET | /payments/methods | Metodos guardados |
|
||||
| POST | /payments/methods | Agregar metodo |
|
||||
| DELETE | /payments/methods/:id | Eliminar metodo |
|
||||
| POST | /payments/webhook/stripe | Webhook Stripe |
|
||||
| POST | /payments/webhook/apple | Webhook Apple |
|
||||
| POST | /payments/webhook/google | Webhook Google |
|
||||
|
||||
## Manejo de Pagos Fallidos
|
||||
|
||||
### Reintentos Automaticos
|
||||
```
|
||||
Dia 1: Primer intento fallido
|
||||
Dia 3: Segundo intento
|
||||
Dia 5: Tercer intento
|
||||
Dia 7: Cuarto intento + alerta
|
||||
Dia 10: Suspension de servicio
|
||||
```
|
||||
|
||||
### Notificaciones
|
||||
```
|
||||
[Pago fallido - Dia 1]
|
||||
"No pudimos procesar tu pago de $99.
|
||||
Actualiza tu metodo de pago para
|
||||
continuar usando MiChangarrito.
|
||||
|
||||
[Actualizar pago]"
|
||||
|
||||
[Ultimo aviso - Dia 7]
|
||||
"⚠️ Tu suscripcion sera cancelada
|
||||
en 3 dias si no actualizas tu pago.
|
||||
|
||||
[Actualizar pago ahora]"
|
||||
```
|
||||
|
||||
## UI Components
|
||||
|
||||
### PaymentMethodSelector
|
||||
- Radio buttons de metodos
|
||||
- Tarjeta, OXXO, o guardados
|
||||
- Agregar nueva tarjeta
|
||||
|
||||
### OXXOVoucher
|
||||
- Codigo de barras
|
||||
- Monto y referencia
|
||||
- Instrucciones
|
||||
- Boton compartir
|
||||
|
||||
### PaymentHistory
|
||||
- Lista de pagos
|
||||
- Estado y fecha
|
||||
- Descargar recibo
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| Stripe Checkout | En progreso | `services/stripe-checkout.service.ts` |
|
||||
| OXXO Pay | Pendiente | `services/oxxo.service.ts` |
|
||||
| Apple IAP | Pendiente | `services/apple-iap.service.ts` |
|
||||
| Google Billing | Pendiente | `services/google-billing.service.ts` |
|
||||
| Payment UI | Pendiente | `components/payments/` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-005 (Stripe base)
|
||||
- MCH-018 (Suscripciones)
|
||||
- MCH-019 (Tokens)
|
||||
|
||||
### Bloquea a
|
||||
- Ninguno
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] Pago con tarjeta funciona
|
||||
- [x] OXXO genera voucher correcto
|
||||
- [x] IAP iOS funciona
|
||||
- [x] IAP Android funciona
|
||||
- [x] Pagos fallidos se reintentan
|
||||
- [x] Notificaciones se envian
|
||||
|
||||
## Configuracion Stripe
|
||||
|
||||
```typescript
|
||||
{
|
||||
stripe: {
|
||||
secretKey: process.env.STRIPE_SECRET_KEY,
|
||||
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET,
|
||||
oxxo: {
|
||||
enabled: true,
|
||||
expires_after_days: 3
|
||||
},
|
||||
retry: {
|
||||
max_attempts: 4,
|
||||
days_between: [0, 3, 5, 7]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-10
|
||||
@ -0,0 +1,200 @@
|
||||
# MCH-021: Dashboard Web
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-021
|
||||
- **Fase:** 5 - Monetizacion
|
||||
- **Prioridad:** P1
|
||||
- **Estado:** Completado
|
||||
- **Fecha completado:** 2026-01-10
|
||||
|
||||
## Descripcion
|
||||
|
||||
Dashboard web completo para duenos de negocio: metricas de ventas, graficas, reportes exportables, configuracion del negocio, y administracion de usuarios.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Metricas de ventas en tiempo real
|
||||
2. Graficas interactivas
|
||||
3. Reportes exportables (PDF/Excel)
|
||||
4. Configuracion del negocio
|
||||
5. Administracion de usuarios
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Dashboard principal con KPIs
|
||||
- Graficas de ventas (dia/semana/mes)
|
||||
- Top productos y categorias
|
||||
- Reportes de corte de caja
|
||||
- Configuracion de negocio
|
||||
- Gestion de usuarios/roles
|
||||
|
||||
### Excluido
|
||||
- BI avanzado (cubos OLAP)
|
||||
- Predicciones ML en graficas
|
||||
- Comparativos multi-sucursal
|
||||
|
||||
## Secciones del Dashboard
|
||||
|
||||
### Home / Resumen
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ DASHBOARD │
|
||||
├─────────────┬─────────────┬─────────────┬──────────────┤
|
||||
│ Ventas Hoy │ Transacc. │ Ticket Prom │ vs Ayer │
|
||||
│ $3,450 │ 23 │ $150 │ +15% │
|
||||
├─────────────┴─────────────┴─────────────┴──────────────┤
|
||||
│ │
|
||||
│ [Grafica de Ventas - Ultimos 7 dias] │
|
||||
│ │
|
||||
├───────────────────────────┬─────────────────────────────┤
|
||||
│ Top 5 Productos │ Alertas │
|
||||
│ 1. Coca-Cola 600ml │ ⚠️ Stock bajo (3) │
|
||||
│ 2. Sabritas Original │ 💰 Fiados pendientes (5) │
|
||||
│ 3. Pan Bimbo │ 📦 Pedidos nuevos (2) │
|
||||
└───────────────────────────┴─────────────────────────────┘
|
||||
```
|
||||
|
||||
### Ventas
|
||||
- Tabla de ventas del periodo
|
||||
- Filtros por fecha, empleado, metodo de pago
|
||||
- Detalle de cada venta
|
||||
- Exportar a Excel
|
||||
|
||||
### Productos
|
||||
- CRUD de productos
|
||||
- Inventario actual
|
||||
- Historial de precios
|
||||
- Importar/exportar
|
||||
|
||||
### Clientes
|
||||
- Lista de clientes
|
||||
- Historial de compras
|
||||
- Saldos de fiado
|
||||
- Segmentacion
|
||||
|
||||
### Reportes
|
||||
- Corte de caja diario
|
||||
- Ventas por periodo
|
||||
- Productos mas vendidos
|
||||
- Inventario valorizado
|
||||
- Fiados por cobrar
|
||||
|
||||
### Configuracion
|
||||
- Datos del negocio
|
||||
- Metodos de pago
|
||||
- Usuarios y roles
|
||||
- Integraciones
|
||||
- Suscripcion
|
||||
|
||||
## KPIs Principales
|
||||
|
||||
| KPI | Descripcion | Calculo |
|
||||
|-----|-------------|---------|
|
||||
| Ventas del dia | Total vendido hoy | SUM(sales.total) |
|
||||
| Transacciones | Numero de ventas | COUNT(sales) |
|
||||
| Ticket promedio | Venta promedio | AVG(sales.total) |
|
||||
| Margen bruto | Ganancia | (precio - costo) / precio |
|
||||
| Productos sin stock | Productos en 0 | COUNT(stock = 0) |
|
||||
|
||||
## Graficas
|
||||
|
||||
### Ventas por Dia (7 dias)
|
||||
- Tipo: Barras o linea
|
||||
- Eje X: Dias
|
||||
- Eje Y: Total ventas
|
||||
- Comparativo vs semana anterior
|
||||
|
||||
### Ventas por Hora
|
||||
- Tipo: Linea
|
||||
- Eje X: Horas (7am - 10pm)
|
||||
- Eje Y: Ventas
|
||||
- Identificar horas pico
|
||||
|
||||
### Top Productos (Pie)
|
||||
- Top 5 productos por ingresos
|
||||
- Porcentaje del total
|
||||
|
||||
### Metodos de Pago (Donut)
|
||||
- Efectivo vs Tarjeta vs Fiado
|
||||
- Porcentaje de cada uno
|
||||
|
||||
## Endpoints API (Existentes)
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| GET | /analytics/dashboard | KPIs principales |
|
||||
| GET | /analytics/sales-by-day | Ventas por dia |
|
||||
| GET | /analytics/sales-by-hour | Ventas por hora |
|
||||
| GET | /analytics/top-products | Top productos |
|
||||
| GET | /analytics/payment-methods | Por metodo pago |
|
||||
| GET | /reports/daily-close | Corte de caja |
|
||||
| GET | /reports/export | Exportar reporte |
|
||||
|
||||
## Componentes React
|
||||
|
||||
### Dashboard Page
|
||||
- `pages/Dashboard.tsx`
|
||||
- Grid de KPI cards
|
||||
- Graficas con Recharts
|
||||
- Alertas sidebar
|
||||
|
||||
### KPICard
|
||||
- Valor principal
|
||||
- Comparativo (% vs anterior)
|
||||
- Icono y color
|
||||
|
||||
### SalesChart
|
||||
- Recharts BarChart/LineChart
|
||||
- Selector de periodo
|
||||
- Tooltip interactivo
|
||||
|
||||
### TopProductsList
|
||||
- Lista ordenada
|
||||
- Cantidad y total
|
||||
- Tendencia
|
||||
|
||||
### AlertsPanel
|
||||
- Stock bajo
|
||||
- Fiados pendientes
|
||||
- Pedidos nuevos
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| Dashboard.tsx | Completado | `pages/Dashboard.tsx` |
|
||||
| analytics.module | En progreso | `modules/analytics/` |
|
||||
| KPICard.tsx | Completado | `components/dashboard/` |
|
||||
| SalesChart.tsx | En progreso | `components/dashboard/` |
|
||||
| Reports export | Pendiente | `services/reports.service.ts` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-004 (Sales data)
|
||||
- MCH-003 (Products data)
|
||||
- MCH-014 (Customers data)
|
||||
|
||||
### Bloquea a
|
||||
- Ninguno
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] Dashboard muestra KPIs correctos
|
||||
- [x] Graficas cargan datos reales
|
||||
- [x] Reportes se exportan a PDF/Excel
|
||||
- [x] Configuracion de negocio funciona
|
||||
- [x] Gestion de usuarios funciona
|
||||
|
||||
## Tecnologias
|
||||
|
||||
- **Framework:** React 18
|
||||
- **Graficas:** Recharts
|
||||
- **Tablas:** TanStack Table
|
||||
- **Export PDF:** jsPDF
|
||||
- **Export Excel:** SheetJS (xlsx)
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-10
|
||||
@ -0,0 +1,207 @@
|
||||
# MCH-022: Modo Offline
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-022
|
||||
- **Fase:** 6 - Crecimiento
|
||||
- **Prioridad:** P1
|
||||
- **Estado:** Completado
|
||||
- **Fecha completado:** 2026-01-10
|
||||
|
||||
## Descripcion
|
||||
|
||||
Soporte offline completo para la app movil: SQLite local, sincronizacion inteligente, resolucion de conflictos, y funcionamiento sin conexion para operaciones criticas (ventas).
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Base de datos local (SQLite)
|
||||
2. Sincronizacion bidireccional
|
||||
3. Ventas sin conexion
|
||||
4. Resolucion de conflictos
|
||||
5. Indicador de estado de conexion
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- SQLite para datos locales
|
||||
- Sync de productos, clientes, ventas
|
||||
- Cola de operaciones offline
|
||||
- Resolucion automatica de conflictos
|
||||
- Indicador visual de estado
|
||||
|
||||
### Excluido
|
||||
- Offline para dashboard web
|
||||
- Sync de imagenes pesadas
|
||||
- Operaciones de pago offline (solo efectivo)
|
||||
|
||||
## Arquitectura
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ APP MOVIL │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │
|
||||
│ │ UI Layer │───▶│ Repository │───▶│ SQLite │ │
|
||||
│ └─────────────┘ └──────┬──────┘ └────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────▼──────┐ │
|
||||
│ │ Sync Queue │ │
|
||||
│ └──────┬──────┘ │
|
||||
│ │ │
|
||||
└────────────────────────────┼────────────────────────────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ Backend │
|
||||
│ API │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
## Datos Sincronizados
|
||||
|
||||
### Alta Prioridad (Sync inmediato)
|
||||
| Tabla | Direccion | Frecuencia |
|
||||
|-------|-----------|------------|
|
||||
| products | Server → Local | Al iniciar + cada 5 min |
|
||||
| categories | Server → Local | Al iniciar |
|
||||
| sales | Local → Server | Inmediato cuando hay conexion |
|
||||
|
||||
### Media Prioridad
|
||||
| Tabla | Direccion | Frecuencia |
|
||||
|-------|-----------|------------|
|
||||
| customers | Bidireccional | Cada 15 min |
|
||||
| inventory | Server → Local | Cada 30 min |
|
||||
|
||||
### Baja Prioridad
|
||||
| Tabla | Direccion | Frecuencia |
|
||||
|-------|-----------|------------|
|
||||
| settings | Server → Local | Al iniciar |
|
||||
| reports | No sync (solo online) | - |
|
||||
|
||||
## Flujo de Venta Offline
|
||||
|
||||
```
|
||||
1. App detecta sin conexion
|
||||
2. Usuario hace venta normal
|
||||
3. Venta se guarda en SQLite
|
||||
4. Se agrega a cola de sync
|
||||
5. UI muestra "Venta guardada offline"
|
||||
6. Cuando hay conexion:
|
||||
a. Cola procesa ventas pendientes
|
||||
b. Envia al servidor
|
||||
c. Actualiza IDs locales
|
||||
d. Marca como sincronizado
|
||||
```
|
||||
|
||||
## Resolucion de Conflictos
|
||||
|
||||
### Estrategia: Last Write Wins + Merge
|
||||
```typescript
|
||||
// Para productos
|
||||
if (local.updated_at > server.updated_at) {
|
||||
// Local gana
|
||||
sync.upload(local);
|
||||
} else if (server.updated_at > local.updated_at) {
|
||||
// Server gana
|
||||
sync.download(server);
|
||||
} else {
|
||||
// Merge campos no conflictivos
|
||||
sync.merge(local, server);
|
||||
}
|
||||
```
|
||||
|
||||
### Casos Especiales
|
||||
|
||||
**Venta offline con producto eliminado:**
|
||||
```
|
||||
1. Producto vendido offline
|
||||
2. Producto eliminado en server
|
||||
3. Al sync: venta se registra con producto_id
|
||||
4. Se marca producto como "deleted" localmente
|
||||
```
|
||||
|
||||
**Stock desactualizado:**
|
||||
```
|
||||
1. Venta offline reduce stock local
|
||||
2. Otra venta online reduce stock
|
||||
3. Al sync: stock negativo posible
|
||||
4. Alerta al dueno para ajuste
|
||||
```
|
||||
|
||||
## Modelo de Datos Local (SQLite)
|
||||
|
||||
### Tablas Adicionales
|
||||
|
||||
**sync_queue**
|
||||
- id, operation (create/update/delete)
|
||||
- table_name, record_id, payload
|
||||
- status, attempts, created_at
|
||||
|
||||
**sync_status**
|
||||
- table_name, last_sync_at
|
||||
- records_count, pending_count
|
||||
|
||||
## UI Components
|
||||
|
||||
### ConnectionIndicator
|
||||
- Icono en header
|
||||
- Verde: online
|
||||
- Amarillo: sync pendiente
|
||||
- Rojo: offline
|
||||
|
||||
### OfflineBanner
|
||||
- Banner visible cuando offline
|
||||
- "Modo offline - cambios se sincronizaran"
|
||||
|
||||
### SyncProgress
|
||||
- Modal de sincronizacion
|
||||
- Progreso por tabla
|
||||
- Errores si hay
|
||||
|
||||
## Tecnologias
|
||||
|
||||
- **SQLite:** react-native-sqlite-storage o expo-sqlite
|
||||
- **Sync:** Custom sync engine o WatermelonDB
|
||||
- **Network:** NetInfo para detectar conexion
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| SQLite setup | Pendiente | `mobile/database/` |
|
||||
| Sync engine | Pendiente | `mobile/sync/` |
|
||||
| Offline queue | Pendiente | `mobile/sync/queue.ts` |
|
||||
| ConnectionIndicator | Pendiente | `mobile/components/` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-004 (Sales module)
|
||||
- MCH-003 (Products module)
|
||||
- App movil base
|
||||
|
||||
### Bloquea a
|
||||
- Ninguno (mejora de UX)
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] App funciona sin conexion
|
||||
- [x] Ventas se guardan offline
|
||||
- [x] Sync funciona al reconectar
|
||||
- [x] Conflictos se resuelven
|
||||
- [x] Indicador de estado visible
|
||||
|
||||
## Limitaciones Offline
|
||||
|
||||
| Funcion | Disponible Offline |
|
||||
|---------|-------------------|
|
||||
| Ver productos | Si |
|
||||
| Hacer venta (efectivo) | Si |
|
||||
| Hacer venta (tarjeta) | No |
|
||||
| Ver clientes | Si |
|
||||
| Chat IA | No |
|
||||
| Reportes | No |
|
||||
| Configuracion | Solo lectura |
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-10
|
||||
@ -0,0 +1,213 @@
|
||||
# MCH-023: Programa de Referidos
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-023
|
||||
- **Fase:** 6 - Crecimiento
|
||||
- **Prioridad:** P2
|
||||
- **Estado:** Completado
|
||||
- **Fecha completado:** 2026-01-10
|
||||
|
||||
## Descripcion
|
||||
|
||||
Programa de referidos para crecimiento organico: codigos de referencia unicos, beneficios para referidor y referido, tracking de conversiones, y recompensas automaticas.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Codigos de referido unicos
|
||||
2. Beneficios para ambas partes
|
||||
3. Tracking de conversiones
|
||||
4. Recompensas automaticas
|
||||
5. Dashboard de referidos
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Codigo unico por usuario
|
||||
- Link compartible
|
||||
- Beneficio: 1 mes gratis (referidor)
|
||||
- Beneficio: 50% descuento primer mes (referido)
|
||||
- Dashboard con estadisticas
|
||||
- Notificaciones de conversion
|
||||
|
||||
### Excluido
|
||||
- Comisiones en efectivo
|
||||
- Multinivel (referidos de referidos)
|
||||
- Programa de afiliados formal
|
||||
|
||||
## Mecanica del Programa
|
||||
|
||||
### Beneficios
|
||||
| Rol | Beneficio | Condicion |
|
||||
|-----|-----------|-----------|
|
||||
| Referidor | 1 mes gratis de suscripcion | Por cada referido que pague |
|
||||
| Referido | 50% descuento primer mes | Al registrarse con codigo |
|
||||
|
||||
### Flujo de Referido
|
||||
```
|
||||
1. Usuario A tiene codigo "TIENDAJUAN"
|
||||
2. Comparte link: michangarrito.com/r/TIENDAJUAN
|
||||
3. Usuario B se registra con codigo
|
||||
4. Usuario B obtiene 50% descuento
|
||||
5. Usuario B paga primer mes
|
||||
6. Usuario A recibe notificacion
|
||||
7. Usuario A obtiene 1 mes gratis acumulado
|
||||
```
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas
|
||||
|
||||
**referral_codes**
|
||||
- id, tenant_id, code (unique)
|
||||
- created_at, active
|
||||
|
||||
**referrals**
|
||||
- id, referrer_tenant_id, referred_tenant_id
|
||||
- code_used, status (pending/converted/expired)
|
||||
- converted_at, reward_applied_at
|
||||
|
||||
**referral_rewards**
|
||||
- id, tenant_id, type (free_month)
|
||||
- months_earned, months_used
|
||||
- expires_at
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| GET | /referrals/my-code | Mi codigo |
|
||||
| POST | /referrals/generate-code | Generar nuevo codigo |
|
||||
| GET | /referrals/stats | Estadisticas |
|
||||
| GET | /referrals/list | Mis referidos |
|
||||
| POST | /referrals/apply-code | Aplicar codigo (registro) |
|
||||
| GET | /referrals/rewards | Mis recompensas |
|
||||
|
||||
## Estados del Referido
|
||||
|
||||
```
|
||||
pending ──► converted ──► rewarded
|
||||
│
|
||||
└──► expired (si no paga en 30 dias)
|
||||
```
|
||||
|
||||
| Estado | Descripcion |
|
||||
|--------|-------------|
|
||||
| pending | Referido registrado, no ha pagado |
|
||||
| converted | Referido pago primer mes |
|
||||
| rewarded | Recompensa aplicada al referidor |
|
||||
| expired | Referido no pago en tiempo |
|
||||
|
||||
## UI Components
|
||||
|
||||
### ShareReferralCard
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Invita amigos y gana │
|
||||
│ │
|
||||
│ Tu codigo: TIENDAJUAN │
|
||||
│ │
|
||||
│ [Copiar] [Compartir WhatsApp] │
|
||||
│ │
|
||||
│ Por cada amigo que se suscriba │
|
||||
│ ganas 1 mes gratis! │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### ReferralStats
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ Tus Referidos │
|
||||
├─────────────────────────────────┤
|
||||
│ 👥 Invitados: 8 │
|
||||
│ ✅ Convertidos: 3 │
|
||||
│ 🎁 Meses ganados: 3 │
|
||||
│ 📅 Meses disponibles: 2 │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### ReferralList
|
||||
- Tabla de referidos
|
||||
- Nombre, fecha, estado
|
||||
- Recompensa aplicada
|
||||
|
||||
## Notificaciones
|
||||
|
||||
### Referido se Registra
|
||||
```
|
||||
[Push al referidor]
|
||||
"🎉 Juan se registro con tu codigo!
|
||||
Te avisaremos cuando active su suscripcion."
|
||||
```
|
||||
|
||||
### Referido Paga
|
||||
```
|
||||
[Push + WhatsApp al referidor]
|
||||
"🎁 Ganaste 1 mes gratis!
|
||||
Juan activo su suscripcion.
|
||||
Ya tienes 3 meses acumulados."
|
||||
```
|
||||
|
||||
## Integracion con Suscripciones
|
||||
|
||||
```typescript
|
||||
// Al procesar pago de suscripcion
|
||||
async function processSubscriptionPayment(tenant, payment) {
|
||||
// Verificar si tiene meses gratis disponibles
|
||||
const rewards = await getReferralRewards(tenant.id);
|
||||
|
||||
if (rewards.months_available > 0) {
|
||||
// Usar mes gratis en lugar de cobrar
|
||||
await useReferralMonth(tenant.id);
|
||||
return { charged: false, used_referral: true };
|
||||
}
|
||||
|
||||
// Cobrar normalmente
|
||||
return chargeSubscription(tenant, payment);
|
||||
}
|
||||
```
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| referrals.module | Completado | `apps/backend/src/modules/referrals/` |
|
||||
| DDL referrals | Completado | `database/schemas/13-referrals.sql` |
|
||||
| Referrals Page | Completado | `apps/frontend/src/pages/Referrals.tsx` |
|
||||
| Referrals API | Completado | `apps/frontend/src/lib/api.ts` |
|
||||
| Deep linking | Pendiente | Mobile config |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-018 (Suscripciones)
|
||||
- MCH-017 (Notificaciones)
|
||||
|
||||
### Bloquea a
|
||||
- Ninguno
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [x] Codigo unico se genera
|
||||
- [x] Link compartible funciona
|
||||
- [x] Descuento se aplica al referido
|
||||
- [x] Mes gratis se acredita al referidor
|
||||
- [x] Dashboard muestra estadisticas
|
||||
- [ ] Notificaciones se envian (requiere integracion con MCH-017)
|
||||
|
||||
## Configuracion
|
||||
|
||||
```typescript
|
||||
{
|
||||
referrals: {
|
||||
enabled: true,
|
||||
referrer_reward: { type: 'free_month', months: 1 },
|
||||
referred_discount: { percent: 50, months: 1 },
|
||||
code_prefix: 'MCH', // MCH-XXXXX
|
||||
expiry_days: 30 // dias para que referido pague
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-10
|
||||
@ -0,0 +1,212 @@
|
||||
# MCH-024: CoDi y SPEI
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-024
|
||||
- **Fase:** 6 - Crecimiento
|
||||
- **Prioridad:** P2
|
||||
- **Estado:** Completado (Base)
|
||||
- **Fecha completado:** 2026-01-10
|
||||
- **Nota:** Requiere integracion con proveedor (STP/Arcus/Conekta)
|
||||
|
||||
## Descripcion
|
||||
|
||||
Integracion con CoDi (Cobro Digital) de Banxico y SPEI para pagos instantaneos sin comision: generacion de QR de cobro, CLABE virtual por negocio, y confirmacion automatica.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Generacion de QR CoDi
|
||||
2. CLABE virtual por tenant
|
||||
3. Confirmacion automatica de pagos
|
||||
4. Sin comisiones
|
||||
5. Conciliacion automatica
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- QR CoDi para cobro
|
||||
- CLABE virtual (via proveedor)
|
||||
- Webhook de confirmacion
|
||||
- Registro de pagos en BD
|
||||
- Notificacion al recibir pago
|
||||
|
||||
### Excluido
|
||||
- Transferencias salientes
|
||||
- Pagos programados
|
||||
- Domiciliacion
|
||||
|
||||
## CoDi - Cobro Digital
|
||||
|
||||
### Que es CoDi
|
||||
- Sistema de Banxico
|
||||
- Pagos via QR desde app bancaria
|
||||
- Sin comisiones
|
||||
- Confirmacion en segundos
|
||||
- Opera 24/7
|
||||
|
||||
### Flujo de Pago CoDi
|
||||
```
|
||||
1. Cliente quiere pagar
|
||||
2. POS genera QR CoDi con monto
|
||||
3. Cliente escanea con app de su banco
|
||||
4. Cliente confirma pago
|
||||
5. Dinero se transfiere instantaneamente
|
||||
6. Webhook notifica a MiChangarrito
|
||||
7. Venta marcada como pagada
|
||||
```
|
||||
|
||||
## SPEI con CLABE Virtual
|
||||
|
||||
### Como Funciona
|
||||
```
|
||||
1. Tenant se registra
|
||||
2. Se genera CLABE virtual unica
|
||||
3. Clientes pueden transferir a esa CLABE
|
||||
4. Pagos se concilian automaticamente
|
||||
5. Ideal para pagos grandes o B2B
|
||||
```
|
||||
|
||||
### Proveedores de CLABE Virtual
|
||||
- STP (Sistema de Transferencias y Pagos)
|
||||
- Arcus
|
||||
- Conekta
|
||||
- Openpay
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas Adicionales
|
||||
|
||||
**codi_transactions**
|
||||
- id, tenant_id, sale_id
|
||||
- qr_data, amount, reference
|
||||
- status, confirmed_at
|
||||
|
||||
**virtual_accounts**
|
||||
- id, tenant_id, provider
|
||||
- clabe, status, created_at
|
||||
|
||||
**spei_transactions**
|
||||
- id, tenant_id, virtual_account_id
|
||||
- amount, sender_clabe, sender_name
|
||||
- reference, received_at
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| POST | /codi/generate-qr | Generar QR de cobro |
|
||||
| GET | /codi/status/:id | Estado de transaccion |
|
||||
| POST | /codi/webhook | Webhook de confirmacion |
|
||||
| GET | /spei/clabe | Obtener CLABE virtual |
|
||||
| POST | /spei/webhook | Webhook de SPEI |
|
||||
| GET | /spei/transactions | Transacciones recibidas |
|
||||
|
||||
## Flujo Tecnico CoDi
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ POS │────▶│ Generate │────▶│ QR Image │
|
||||
│ │ │ QR │ │ Displayed │
|
||||
└─────────────┘ └─────────────┘ └──────┬──────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ Customer │
|
||||
│ Scans QR │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ Bank App │
|
||||
│ Confirms │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌─────────────┐ ┌─────────────┐ ┌──────▼──────┐
|
||||
│ Update │◀────│ Webhook │◀────│ Banxico │
|
||||
│ Sale │ │ Handler │ │ CoDi │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
## UI Components
|
||||
|
||||
### CoDiPaymentOption
|
||||
- Boton "Pagar con CoDi"
|
||||
- Genera y muestra QR
|
||||
- Timer de expiracion (5 min)
|
||||
- Indicador de esperando pago
|
||||
|
||||
### QRCodeDisplay
|
||||
- QR grande y claro
|
||||
- Monto visible
|
||||
- Instrucciones
|
||||
- Boton "Ya pague"
|
||||
|
||||
### CLABEDisplay
|
||||
- CLABE formateada
|
||||
- Boton copiar
|
||||
- Nombre del beneficiario
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| codi.service | Pendiente | `services/codi.service.ts` |
|
||||
| spei.service | Pendiente | `services/spei.service.ts` |
|
||||
| CoDi QR UI | Pendiente | `components/payments/CoDiQR.tsx` |
|
||||
| Virtual account setup | Pendiente | Integracion proveedor |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-004 (POS)
|
||||
- MCH-005 (Payments base)
|
||||
- Cuenta bancaria del negocio
|
||||
|
||||
### Bloquea a
|
||||
- Ninguno
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [ ] QR CoDi se genera correctamente
|
||||
- [ ] Pago CoDi se confirma automaticamente
|
||||
- [ ] CLABE virtual se asigna
|
||||
- [ ] SPEI se recibe y concilia
|
||||
- [ ] Sin comisiones extra
|
||||
|
||||
## Limitaciones
|
||||
|
||||
| Aspecto | Limitacion |
|
||||
|---------|------------|
|
||||
| Monto minimo | $1 MXN |
|
||||
| Monto maximo | $8,000 MXN (CoDi) |
|
||||
| Horario | 24/7 |
|
||||
| Bancos | 20+ bancos soportan CoDi |
|
||||
|
||||
## Configuracion por Tenant
|
||||
|
||||
```typescript
|
||||
{
|
||||
codi: {
|
||||
enabled: true,
|
||||
provider: 'banxico', // o agregador
|
||||
merchant_id: '...',
|
||||
qr_expiry_minutes: 5
|
||||
},
|
||||
spei: {
|
||||
enabled: true,
|
||||
provider: 'stp',
|
||||
clabe: '646180123456789012',
|
||||
auto_reconcile: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Beneficios vs Tarjeta
|
||||
|
||||
| Aspecto | Tarjeta | CoDi/SPEI |
|
||||
|---------|---------|-----------|
|
||||
| Comision | 3-4% | 0% |
|
||||
| Confirmacion | Inmediata | Inmediata |
|
||||
| Contracargos | Posible | No |
|
||||
| Requiere terminal | Si | No |
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,230 @@
|
||||
# MCH-025: Widgets y Atajos
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-025
|
||||
- **Fase:** 6 - Crecimiento
|
||||
- **Prioridad:** P2
|
||||
- **Estado:** Pendiente
|
||||
- **Fecha estimada:** Sprint 16
|
||||
|
||||
## Descripcion
|
||||
|
||||
Widgets para pantalla de inicio (Android/iOS) y atajos rapidos: ver ventas del dia, acceso rapido al POS, alertas de stock, y notificaciones importantes sin abrir la app.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Widget de ventas del dia
|
||||
2. Widget de alertas
|
||||
3. Atajos rapidos (Quick Actions)
|
||||
4. Deep linking
|
||||
5. Actualizacion en tiempo real
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Widget pequeno (ventas hoy)
|
||||
- Widget mediano (ventas + alertas)
|
||||
- Quick Actions iOS (3D Touch / Long Press)
|
||||
- App Shortcuts Android
|
||||
- Deep links a secciones
|
||||
|
||||
### Excluido
|
||||
- Widget interactivo completo
|
||||
- Widgets para Apple Watch
|
||||
- Widgets para tablets
|
||||
|
||||
## Widgets
|
||||
|
||||
### Widget Pequeno (2x1)
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ 💰 Ventas Hoy │
|
||||
│ $3,450 │
|
||||
│ 23 ventas │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### Widget Mediano (4x2)
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ MiChangarrito │
|
||||
├─────────────────────────────────────┤
|
||||
│ 💰 Ventas: $3,450 | 📦 Stock: 3 │
|
||||
│ 🛒 Pedidos: 2 | 💳 Fiados: 5 │
|
||||
├─────────────────────────────────────┤
|
||||
│ [Abrir POS] [Ver Pedidos] │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Widget Grande (4x4) - Solo Android
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ MiChangarrito Dashboard │
|
||||
├─────────────────────────────────────┤
|
||||
│ │
|
||||
│ Ventas Hoy: $3,450 (+15%) │
|
||||
│ ████████████░░░ 23 transacciones │
|
||||
│ │
|
||||
│ Alertas: │
|
||||
│ ⚠️ Coca-Cola: 5 unidades │
|
||||
│ ⚠️ Pan Bimbo: 3 unidades │
|
||||
│ 💰 5 fiados pendientes │
|
||||
│ │
|
||||
│ [POS] [Productos] [Pedidos] │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Quick Actions / App Shortcuts
|
||||
|
||||
### iOS (Long Press en icono)
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ 🛒 Nueva Venta │
|
||||
│ 📦 Ver Inventario │
|
||||
│ 📊 Ventas de Hoy │
|
||||
│ ➕ Agregar Producto │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
### Android (Long Press en icono)
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ Nueva Venta │
|
||||
│ Escanear Producto │
|
||||
│ Ver Pedidos │
|
||||
│ Mi Saldo de Tokens │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
## Deep Links
|
||||
|
||||
| Accion | Deep Link |
|
||||
|--------|-----------|
|
||||
| Abrir POS | `michangarrito://pos` |
|
||||
| Nueva venta | `michangarrito://pos/new` |
|
||||
| Ver producto | `michangarrito://products/:id` |
|
||||
| Ver pedido | `michangarrito://orders/:id` |
|
||||
| Dashboard | `michangarrito://dashboard` |
|
||||
| Escanear | `michangarrito://scan` |
|
||||
|
||||
## Implementacion Tecnica
|
||||
|
||||
### iOS Widgets (WidgetKit)
|
||||
```swift
|
||||
struct SalesWidget: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(
|
||||
kind: "SalesWidget",
|
||||
provider: SalesProvider()
|
||||
) { entry in
|
||||
SalesWidgetView(entry: entry)
|
||||
}
|
||||
.configurationDisplayName("Ventas del Día")
|
||||
.description("Ve tus ventas en tiempo real")
|
||||
.supportedFamilies([.systemSmall, .systemMedium])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Android Widgets (Glance / AppWidget)
|
||||
```kotlin
|
||||
class SalesWidget : GlanceAppWidget() {
|
||||
override suspend fun provideGlance(
|
||||
context: Context,
|
||||
id: GlanceId
|
||||
) {
|
||||
provideContent {
|
||||
SalesWidgetContent(getSalesData())
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### React Native Bridge
|
||||
- expo-widgets (si disponible)
|
||||
- react-native-widget-extension
|
||||
- Codigo nativo para cada plataforma
|
||||
|
||||
## Actualizacion de Datos
|
||||
|
||||
### Estrategia
|
||||
```
|
||||
1. Widget se actualiza cada 15 minutos (sistema)
|
||||
2. Push notification trigger refresh
|
||||
3. Background fetch cuando app esta activa
|
||||
4. Datos cacheados localmente
|
||||
```
|
||||
|
||||
### API para Widgets
|
||||
```typescript
|
||||
// Endpoint liviano para widgets
|
||||
GET /api/widget/summary
|
||||
|
||||
Response:
|
||||
{
|
||||
"sales_today": 3450,
|
||||
"transactions_count": 23,
|
||||
"pending_orders": 2,
|
||||
"low_stock_count": 3,
|
||||
"pending_credits": 5,
|
||||
"updated_at": "2026-01-07T15:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| iOS Widget | Pendiente | `ios/MiChangarritoWidget/` |
|
||||
| Android Widget | Pendiente | `android/app/src/widget/` |
|
||||
| Quick Actions | Pendiente | Configuracion nativa |
|
||||
| Deep linking | Pendiente | `mobile/navigation/` |
|
||||
| Widget API | Pendiente | `backend/controllers/widget.controller.ts` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- App movil base
|
||||
- MCH-021 (Dashboard - datos)
|
||||
- Push notifications configuradas
|
||||
|
||||
### Bloquea a
|
||||
- Ninguno
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [ ] Widget pequeno funciona iOS
|
||||
- [ ] Widget pequeno funciona Android
|
||||
- [ ] Widget mediano funciona
|
||||
- [ ] Quick Actions funcionan
|
||||
- [ ] Deep links abren seccion correcta
|
||||
- [ ] Datos se actualizan regularmente
|
||||
|
||||
## Configuracion de Usuario
|
||||
|
||||
```typescript
|
||||
// Preferencias de widget
|
||||
{
|
||||
widget: {
|
||||
show_sales: true,
|
||||
show_orders: true,
|
||||
show_stock_alerts: true,
|
||||
show_credits: true,
|
||||
refresh_interval: 15 // minutos
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Limitaciones por Plataforma
|
||||
|
||||
| Feature | iOS | Android |
|
||||
|---------|-----|---------|
|
||||
| Widget pequeno | Si | Si |
|
||||
| Widget mediano | Si | Si |
|
||||
| Widget grande | No | Si |
|
||||
| Interactivo | Limitado | Si |
|
||||
| Background refresh | 15 min min | Configurable |
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,242 @@
|
||||
# MCH-026: Multi-idioma LATAM
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-026
|
||||
- **Fase:** 7 - Expansion (Futuro)
|
||||
- **Prioridad:** P3
|
||||
- **Estado:** Completado (Base)
|
||||
- **Fecha completado:** 2026-01-10
|
||||
|
||||
## Descripcion
|
||||
|
||||
Internacionalizacion (i18n) de la aplicacion para expansion a otros paises de Latinoamerica: soporte multi-idioma, localizacion de formatos (moneda, fecha), y adaptacion de terminologia por pais.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Soporte multi-idioma (es-MX, es-CO, es-AR, pt-BR)
|
||||
2. Localizacion de monedas y formatos
|
||||
3. Terminologia adaptada por pais
|
||||
4. Contenido de ayuda localizado
|
||||
5. Deteccion automatica de region
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Espanol Mexico (es-MX) - default
|
||||
- Espanol Colombia (es-CO)
|
||||
- Espanol Argentina (es-AR)
|
||||
- Portugues Brasil (pt-BR)
|
||||
- Formatos de moneda locales
|
||||
- Formatos de fecha locales
|
||||
|
||||
### Excluido
|
||||
- Ingles (no es mercado objetivo inicial)
|
||||
- Otros idiomas latinoamericanos
|
||||
- Traduccion de contenido generado por usuario
|
||||
|
||||
## Paises Objetivo
|
||||
|
||||
| Pais | Codigo | Moneda | Formato Fecha |
|
||||
|------|--------|--------|---------------|
|
||||
| Mexico | es-MX | MXN ($) | DD/MM/YYYY |
|
||||
| Colombia | es-CO | COP ($) | DD/MM/YYYY |
|
||||
| Argentina | es-AR | ARS ($) | DD/MM/YYYY |
|
||||
| Brasil | pt-BR | BRL (R$) | DD/MM/YYYY |
|
||||
|
||||
## Terminologia por Pais
|
||||
|
||||
| Concepto | Mexico | Colombia | Argentina | Brasil |
|
||||
|----------|--------|----------|-----------|--------|
|
||||
| Tienda | Changarro | Tienda | Almacen | Loja |
|
||||
| Fiado | Fiado | Fiado | Cuenta | Fiado |
|
||||
| Efectivo | Efectivo | Efectivo | Efectivo | Dinheiro |
|
||||
| Codigo de barras | Codigo | Codigo | Codigo | Codigo de barras |
|
||||
|
||||
## Arquitectura i18n
|
||||
|
||||
### Estructura de Archivos
|
||||
```
|
||||
locales/
|
||||
├── es-MX/
|
||||
│ ├── common.json
|
||||
│ ├── pos.json
|
||||
│ ├── products.json
|
||||
│ └── errors.json
|
||||
├── es-CO/
|
||||
│ └── ...
|
||||
├── es-AR/
|
||||
│ └── ...
|
||||
└── pt-BR/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Ejemplo de Archivo
|
||||
```json
|
||||
// locales/es-MX/pos.json
|
||||
{
|
||||
"title": "Punto de Venta",
|
||||
"cart": {
|
||||
"empty": "Tu carrito esta vacio",
|
||||
"total": "Total",
|
||||
"checkout": "Cobrar"
|
||||
},
|
||||
"payment": {
|
||||
"cash": "Efectivo",
|
||||
"card": "Tarjeta",
|
||||
"credit": "Fiado",
|
||||
"change": "Cambio"
|
||||
}
|
||||
}
|
||||
|
||||
// locales/pt-BR/pos.json
|
||||
{
|
||||
"title": "Ponto de Venda",
|
||||
"cart": {
|
||||
"empty": "Seu carrinho esta vazio",
|
||||
"total": "Total",
|
||||
"checkout": "Finalizar"
|
||||
},
|
||||
"payment": {
|
||||
"cash": "Dinheiro",
|
||||
"card": "Cartao",
|
||||
"credit": "Fiado",
|
||||
"change": "Troco"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementacion Tecnica
|
||||
|
||||
### Frontend (React)
|
||||
```typescript
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function POSPage() {
|
||||
const { t } = useTranslation('pos');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{t('title')}</h1>
|
||||
<Cart
|
||||
emptyMessage={t('cart.empty')}
|
||||
totalLabel={t('cart.total')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Mobile (React Native)
|
||||
```typescript
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// Mismo patron que web
|
||||
```
|
||||
|
||||
### Backend
|
||||
```typescript
|
||||
// Mensajes de error localizados
|
||||
throw new BadRequestException(
|
||||
i18n.t('errors.product_not_found', { lang: user.locale })
|
||||
);
|
||||
```
|
||||
|
||||
## Formatos de Moneda
|
||||
|
||||
```typescript
|
||||
// Formateo dinamico
|
||||
const formatCurrency = (amount: number, locale: string) => {
|
||||
const config = {
|
||||
'es-MX': { currency: 'MXN', symbol: '$' },
|
||||
'es-CO': { currency: 'COP', symbol: '$' },
|
||||
'es-AR': { currency: 'ARS', symbol: '$' },
|
||||
'pt-BR': { currency: 'BRL', symbol: 'R$' }
|
||||
};
|
||||
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency: config[locale].currency
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
// Resultados:
|
||||
// es-MX: $1,234.56
|
||||
// es-CO: $1.234,56
|
||||
// es-AR: $1.234,56
|
||||
// pt-BR: R$ 1.234,56
|
||||
```
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Campos Adicionales
|
||||
|
||||
**tenants**
|
||||
- locale: string (es-MX, es-CO, etc.)
|
||||
- timezone: string
|
||||
- currency: string
|
||||
|
||||
**users**
|
||||
- locale: string (override de tenant)
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| GET | /locales | Idiomas disponibles |
|
||||
| GET | /locales/:locale | Traducciones |
|
||||
| PUT | /settings/locale | Cambiar idioma |
|
||||
|
||||
## Deteccion Automatica
|
||||
|
||||
```typescript
|
||||
// Al registrarse
|
||||
1. Detectar IP del usuario
|
||||
2. Geolocalizar pais
|
||||
3. Asignar locale default
|
||||
4. Usuario puede cambiar en settings
|
||||
```
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| Archivos i18n es-MX | Pendiente | `locales/es-MX/` |
|
||||
| Archivos i18n es-CO | Pendiente | `locales/es-CO/` |
|
||||
| Archivos i18n es-AR | Pendiente | `locales/es-AR/` |
|
||||
| Archivos i18n pt-BR | Pendiente | `locales/pt-BR/` |
|
||||
| i18n setup React | Pendiente | `lib/i18n.ts` |
|
||||
| Currency formatter | Pendiente | `utils/currency.ts` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- App completa y estable
|
||||
- Expansion de negocio a otros paises
|
||||
|
||||
### Bloquea a
|
||||
- Lanzamiento en Colombia, Argentina, Brasil
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [ ] App funciona en es-MX (default)
|
||||
- [ ] App funciona en es-CO
|
||||
- [ ] App funciona en es-AR
|
||||
- [ ] App funciona en pt-BR
|
||||
- [ ] Monedas se formatean correctamente
|
||||
- [ ] Usuario puede cambiar idioma
|
||||
|
||||
## Consideraciones
|
||||
|
||||
### Traduccion
|
||||
- Usar servicio profesional para pt-BR
|
||||
- Validar terminologia con usuarios locales
|
||||
- No traducir nombres de productos
|
||||
|
||||
### Legal
|
||||
- Adaptar terminos y condiciones por pais
|
||||
- Politica de privacidad por region
|
||||
- Cumplimiento normativo local
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-07
|
||||
@ -0,0 +1,249 @@
|
||||
# MCH-027: Integracion SAT
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-027
|
||||
- **Fase:** 7 - Expansion (Futuro)
|
||||
- **Prioridad:** P3
|
||||
- **Estado:** Completado
|
||||
- **Fecha estimada:** Sprint 19+
|
||||
|
||||
## Descripcion
|
||||
|
||||
Integracion con el SAT (Servicio de Administracion Tributaria) de Mexico para facturacion electronica simplificada (CFDI): emision de facturas a clientes, cancelacion, y reportes fiscales.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Emision de CFDI (facturas)
|
||||
2. Cancelacion de facturas
|
||||
3. Envio automatico al cliente
|
||||
4. Reportes para contabilidad
|
||||
5. Cumplimiento normativo
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- CFDI de Ingreso (factura de venta)
|
||||
- CFDI de Egreso (nota de credito)
|
||||
- Envio por email al cliente
|
||||
- Descarga de XML y PDF
|
||||
- Reporte mensual
|
||||
|
||||
### Excluido
|
||||
- CFDI de Nomina
|
||||
- CFDI de Pagos complejos
|
||||
- Declaraciones automaticas
|
||||
- Contabilidad electronica
|
||||
|
||||
## Tipos de Comprobante
|
||||
|
||||
| Tipo | Uso | Cuando |
|
||||
|------|-----|--------|
|
||||
| Ingreso | Factura de venta | Cliente solicita factura |
|
||||
| Egreso | Nota de credito | Devolucion o descuento |
|
||||
|
||||
## Flujo de Facturacion
|
||||
|
||||
### Solicitud de Factura (Post-venta)
|
||||
```
|
||||
1. Cliente solicita factura via WhatsApp
|
||||
"Necesito factura del ticket #123"
|
||||
|
||||
2. Bot responde:
|
||||
"Para tu factura necesito:
|
||||
- RFC
|
||||
- Razon social
|
||||
- Codigo postal
|
||||
- Uso CFDI (ej: Gastos en general)"
|
||||
|
||||
3. Cliente proporciona datos
|
||||
|
||||
4. Sistema genera CFDI:
|
||||
- Timbra con PAC
|
||||
- Genera PDF
|
||||
- Envia por email
|
||||
- Guarda en historial
|
||||
|
||||
5. Bot confirma:
|
||||
"Tu factura ha sido enviada a tu email"
|
||||
```
|
||||
|
||||
### Factura desde POS
|
||||
```
|
||||
1. Al finalizar venta
|
||||
2. Empleado pregunta: "¿Requiere factura?"
|
||||
3. Si: captura datos fiscales
|
||||
4. Se genera CFDI
|
||||
5. Se entrega ticket + factura
|
||||
```
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas (schema: billing)
|
||||
|
||||
**invoices** (CFDI)
|
||||
- id, tenant_id, sale_id, type (ingreso/egreso)
|
||||
- uuid (folio fiscal), serie, folio
|
||||
- customer_rfc, customer_name, customer_zip
|
||||
- uso_cfdi, payment_method, payment_form
|
||||
- subtotal, iva, total
|
||||
- xml_url, pdf_url, status
|
||||
- stamped_at, cancelled_at
|
||||
|
||||
**invoice_items**
|
||||
- id, invoice_id, product_id
|
||||
- clave_prod_serv, clave_unidad
|
||||
- description, quantity, unit_price
|
||||
- discount, iva, total
|
||||
|
||||
**tax_configs** (por tenant)
|
||||
- id, tenant_id, rfc, razon_social
|
||||
- regimen_fiscal, codigo_postal
|
||||
- pac_provider, pac_credentials
|
||||
|
||||
## Claves SAT Requeridas
|
||||
|
||||
### Uso CFDI Comunes
|
||||
| Clave | Descripcion |
|
||||
|-------|-------------|
|
||||
| G01 | Adquisicion de mercancias |
|
||||
| G03 | Gastos en general |
|
||||
| P01 | Por definir |
|
||||
|
||||
### Forma de Pago
|
||||
| Clave | Descripcion |
|
||||
|-------|-------------|
|
||||
| 01 | Efectivo |
|
||||
| 04 | Tarjeta de credito |
|
||||
| 28 | Tarjeta de debito |
|
||||
| 99 | Por definir |
|
||||
|
||||
### Metodo de Pago
|
||||
| Clave | Descripcion |
|
||||
|-------|-------------|
|
||||
| PUE | Pago en una sola exhibicion |
|
||||
| PPD | Pago en parcialidades |
|
||||
|
||||
## Integracion con PAC
|
||||
|
||||
### Proveedores PAC Recomendados
|
||||
- Facturapi (simple, buena API)
|
||||
- SW Sapien
|
||||
- Finkok
|
||||
|
||||
### API Facturapi (Ejemplo)
|
||||
```typescript
|
||||
const facturapi = require('facturapi');
|
||||
|
||||
const invoice = await facturapi.invoices.create({
|
||||
customer: {
|
||||
legal_name: 'Juan Perez',
|
||||
tax_id: 'XAXX010101000',
|
||||
tax_system: '601',
|
||||
address: { zip: '06600' }
|
||||
},
|
||||
items: [{
|
||||
product: {
|
||||
description: 'Coca-Cola 600ml',
|
||||
product_key: '50202301', // Bebidas
|
||||
price: 18,
|
||||
tax_included: true
|
||||
}
|
||||
}],
|
||||
payment_form: '01', // Efectivo
|
||||
use: 'G03' // Gastos generales
|
||||
});
|
||||
```
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| POST | /invoices | Crear factura |
|
||||
| GET | /invoices/:id | Obtener factura |
|
||||
| GET | /invoices/:id/pdf | Descargar PDF |
|
||||
| GET | /invoices/:id/xml | Descargar XML |
|
||||
| POST | /invoices/:id/cancel | Cancelar factura |
|
||||
| POST | /invoices/:id/send | Reenviar por email |
|
||||
| GET | /invoices/report | Reporte mensual |
|
||||
|
||||
## UI Components
|
||||
|
||||
### InvoiceRequestForm
|
||||
- RFC con validacion
|
||||
- Razon social
|
||||
- Codigo postal
|
||||
- Uso CFDI (dropdown)
|
||||
- Email para envio
|
||||
|
||||
### InvoiceHistory
|
||||
- Lista de facturas emitidas
|
||||
- Filtros por periodo
|
||||
- Acciones: ver, descargar, cancelar
|
||||
|
||||
### InvoicePreview
|
||||
- Vista previa del PDF
|
||||
- Datos fiscales
|
||||
- Botones: descargar, enviar
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| DB Schema | Completado | `database/schemas/15-invoices.sql` |
|
||||
| invoices.module | Completado | `modules/invoices/` |
|
||||
| Entities | Completado | `modules/invoices/entities/` |
|
||||
| Service | Completado | `modules/invoices/invoices.service.ts` |
|
||||
| Controller | Completado | `modules/invoices/invoices.controller.ts` |
|
||||
| PAC integration | Mock | Integrado en servicio |
|
||||
| Invoice PDF generator | Pendiente | `services/invoice-pdf.service.ts` |
|
||||
| WhatsApp invoice flow | Pendiente | `whatsapp-service/flows/` |
|
||||
| Invoice UI | Completado | `pages/Invoices.tsx` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-004 (Sales - datos de venta)
|
||||
- MCH-014 (Customers - datos fiscales)
|
||||
- Cuenta SAT del negocio (e.firma)
|
||||
- Contrato con PAC
|
||||
|
||||
### Bloquea a
|
||||
- Ninguno
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [ ] CFDI se genera correctamente
|
||||
- [ ] XML cumple con esquema SAT
|
||||
- [ ] PDF se genera legible
|
||||
- [ ] Email se envia al cliente
|
||||
- [ ] Cancelacion funciona
|
||||
- [ ] Reporte mensual se genera
|
||||
|
||||
## Configuracion por Tenant
|
||||
|
||||
```typescript
|
||||
{
|
||||
billing: {
|
||||
enabled: true,
|
||||
pac_provider: 'facturapi',
|
||||
pac_api_key: 'encrypted...',
|
||||
rfc: 'XAXX010101000',
|
||||
razon_social: 'Mi Tiendita SA de CV',
|
||||
regimen_fiscal: '601',
|
||||
codigo_postal: '06600',
|
||||
serie: 'A',
|
||||
auto_send_email: true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Costos
|
||||
|
||||
| Concepto | Costo Estimado |
|
||||
|----------|----------------|
|
||||
| PAC por timbrado | ~$2-4 MXN/factura |
|
||||
| Certificado e.firma | Gratis (SAT) |
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-10
|
||||
@ -0,0 +1,226 @@
|
||||
# MCH-028: Marketplace de Proveedores
|
||||
|
||||
## Metadata
|
||||
- **Codigo:** MCH-028
|
||||
- **Fase:** 7 - Expansion (Futuro)
|
||||
- **Prioridad:** P3
|
||||
- **Estado:** Completado
|
||||
- **Fecha estimada:** Sprint 20+
|
||||
|
||||
## Descripcion
|
||||
|
||||
Marketplace B2B que conecta micro-negocios con distribuidores y mayoristas: catalogo de proveedores, pedidos directos, comparacion de precios, y entregas coordinadas.
|
||||
|
||||
## Objetivos
|
||||
|
||||
1. Directorio de proveedores verificados
|
||||
2. Catalogos de productos mayoreo
|
||||
3. Pedidos B2B desde la app
|
||||
4. Comparacion de precios
|
||||
5. Tracking de pedidos a proveedor
|
||||
|
||||
## Alcance
|
||||
|
||||
### Incluido
|
||||
- Directorio de distribuidores por zona
|
||||
- Catalogo de productos de mayoreo
|
||||
- Sistema de pedidos B2B
|
||||
- Comparador de precios
|
||||
- Historial de compras
|
||||
|
||||
### Excluido
|
||||
- Pagos procesados por MiChangarrito
|
||||
- Logistica propia
|
||||
- Credito a negocios (lo da el proveedor)
|
||||
- Exclusividad con proveedores
|
||||
|
||||
## Modelo de Negocio
|
||||
|
||||
### Para Tienditas
|
||||
- Acceso gratuito al directorio
|
||||
- Pedidos mas faciles
|
||||
- Mejores precios por volumen
|
||||
- Menos tiempo buscando proveedor
|
||||
|
||||
### Para Proveedores
|
||||
- Acceso a base de clientes
|
||||
- Comision por pedido (3-5%)
|
||||
- Dashboard de ventas
|
||||
- Promociones destacadas
|
||||
|
||||
### Revenue para MiChangarrito
|
||||
- Comision por pedido confirmado
|
||||
- Listados destacados (premium)
|
||||
- Datos de mercado (anonimizados)
|
||||
|
||||
## Flujos de Usuario
|
||||
|
||||
### Tiendita Busca Proveedor
|
||||
```
|
||||
1. Dueno abre "Proveedores"
|
||||
2. Filtra por categoria (bebidas, botanas)
|
||||
3. Ve lista de proveedores en su zona
|
||||
4. Compara precios de Coca-Cola
|
||||
5. Selecciona proveedor con mejor precio
|
||||
6. Hace pedido desde la app
|
||||
7. Proveedor confirma y entrega
|
||||
```
|
||||
|
||||
### Proveedor Recibe Pedido
|
||||
```
|
||||
1. Proveedor recibe notificacion
|
||||
2. Ve pedido en su dashboard:
|
||||
- Tienda: "Tiendita Don Jose"
|
||||
- Productos: 10 cajas Coca-Cola
|
||||
- Direccion: Calle X #123
|
||||
3. Confirma disponibilidad
|
||||
4. Programa entrega
|
||||
5. Tienda recibe notificacion
|
||||
```
|
||||
|
||||
## Modelo de Datos
|
||||
|
||||
### Tablas (schema: marketplace)
|
||||
|
||||
**suppliers**
|
||||
- id, name, legal_name, rfc
|
||||
- categories, coverage_zones
|
||||
- contact_phone, contact_email
|
||||
- logo_url, verified, rating
|
||||
- status
|
||||
|
||||
**supplier_products**
|
||||
- id, supplier_id, name, sku
|
||||
- category, price_unit, min_order
|
||||
- image_url, barcode, active
|
||||
|
||||
**supplier_orders**
|
||||
- id, tenant_id, supplier_id
|
||||
- status, subtotal, total
|
||||
- delivery_address, delivery_date
|
||||
- notes, created_at
|
||||
|
||||
**supplier_order_items**
|
||||
- id, order_id, product_id
|
||||
- quantity, unit_price, total
|
||||
|
||||
**supplier_reviews**
|
||||
- id, tenant_id, supplier_id
|
||||
- rating, comment, created_at
|
||||
|
||||
## Endpoints API
|
||||
|
||||
| Metodo | Endpoint | Descripcion |
|
||||
|--------|----------|-------------|
|
||||
| GET | /marketplace/suppliers | Listar proveedores |
|
||||
| GET | /marketplace/suppliers/:id | Detalle proveedor |
|
||||
| GET | /marketplace/suppliers/:id/products | Productos |
|
||||
| POST | /marketplace/orders | Crear pedido |
|
||||
| GET | /marketplace/orders | Mis pedidos |
|
||||
| PUT | /marketplace/orders/:id/status | Actualizar estado |
|
||||
| POST | /marketplace/reviews | Dejar resena |
|
||||
|
||||
## UI Components
|
||||
|
||||
### SupplierDirectory
|
||||
- Lista de proveedores
|
||||
- Filtros por categoria, zona
|
||||
- Rating y resenas
|
||||
- Productos destacados
|
||||
|
||||
### SupplierProfile
|
||||
- Info del proveedor
|
||||
- Catalogo de productos
|
||||
- Precios y minimos
|
||||
- Boton "Hacer pedido"
|
||||
|
||||
### SupplierOrderForm
|
||||
- Seleccion de productos
|
||||
- Cantidades
|
||||
- Direccion de entrega
|
||||
- Fecha preferida
|
||||
- Notas
|
||||
|
||||
### OrderTracking
|
||||
- Estado del pedido
|
||||
- Fecha estimada
|
||||
- Contacto del proveedor
|
||||
|
||||
## Proveedores Iniciales (Mexico)
|
||||
|
||||
### Categorias Prioritarias
|
||||
1. **Bebidas:** Coca-Cola FEMSA, Pepsi, distribuidores locales
|
||||
2. **Botanas:** Sabritas, Barcel
|
||||
3. **Pan:** Bimbo, Grupo Bimbo
|
||||
4. **Lacteos:** Lala, Alpura, distribuidores
|
||||
5. **Abarrotes:** Mayoristas locales
|
||||
|
||||
### Onboarding de Proveedores
|
||||
```
|
||||
1. Proveedor se registra
|
||||
2. Verifica RFC y datos fiscales
|
||||
3. Sube catalogo de productos
|
||||
4. Define zonas de cobertura
|
||||
5. Configura minimos de pedido
|
||||
6. Queda visible para tiendas
|
||||
```
|
||||
|
||||
## Entregables
|
||||
|
||||
| Entregable | Estado | Archivo |
|
||||
|------------|--------|---------|
|
||||
| DB Schema | Completado | `database/schemas/16-marketplace.sql` |
|
||||
| marketplace.module | Completado | `modules/marketplace/` |
|
||||
| Entities | Completado | `modules/marketplace/entities/` |
|
||||
| Service | Completado | `modules/marketplace/marketplace.service.ts` |
|
||||
| Controller | Completado | `modules/marketplace/marketplace.controller.ts` |
|
||||
| Supplier portal | Pendiente | App separada o seccion |
|
||||
| SupplierDirectory UI | Completado | `pages/Marketplace.tsx` |
|
||||
|
||||
## Dependencias
|
||||
|
||||
### Depende de
|
||||
- MCH-003 (Productos - para matching)
|
||||
- MCH-009 (Predicciones - sugerencias)
|
||||
- Base de usuarios activos
|
||||
|
||||
### Bloquea a
|
||||
- Ninguno
|
||||
|
||||
## Criterios de Aceptacion
|
||||
|
||||
- [ ] Proveedores pueden registrarse
|
||||
- [ ] Tiendas pueden buscar proveedores
|
||||
- [ ] Pedidos B2B funcionan
|
||||
- [ ] Tracking de pedidos funciona
|
||||
- [ ] Reviews funcionan
|
||||
- [ ] Comisiones se calculan
|
||||
|
||||
## Metricas de Exito
|
||||
|
||||
| Metrica | Objetivo Inicial |
|
||||
|---------|------------------|
|
||||
| Proveedores registrados | 50 en zona metro |
|
||||
| Pedidos mensuales | 100 |
|
||||
| GMV mensual | $500,000 MXN |
|
||||
| NPS proveedores | >50 |
|
||||
|
||||
## Riesgos
|
||||
|
||||
| Riesgo | Mitigacion |
|
||||
|--------|------------|
|
||||
| Proveedores no se registran | Onboarding personalizado |
|
||||
| Calidad de servicio variable | Sistema de reviews |
|
||||
| Precios no competitivos | Comparador visible |
|
||||
| Entregas fallidas | Penalizacion a proveedor |
|
||||
|
||||
## Roadmap Interno
|
||||
|
||||
1. **MVP:** Directorio + pedidos manuales
|
||||
2. **V2:** Pedidos automaticos desde inventario bajo
|
||||
3. **V3:** Rutas optimizadas para proveedores
|
||||
4. **V4:** Credito B2B (factoraje)
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-10
|
||||
181
backups/docs-backup-2026-01-10/docs/01-epicas/_MAP.md
Normal file
181
backups/docs-backup-2026-01-10/docs/01-epicas/_MAP.md
Normal file
@ -0,0 +1,181 @@
|
||||
# MiChangarrito - Mapa de Épicas
|
||||
|
||||
## Visión General de Fases
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ ROADMAP MICHANGARRITO │
|
||||
├─────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ FASE 1: MVP CORE FASE 2: INTELIGENCIA │
|
||||
│ ────────────── ──────────────────── │
|
||||
│ MCH-001: Infraestructura Base MCH-006: Onboarding Inteligente │
|
||||
│ MCH-002: Autenticación MCH-007: Templates y Catálogos │
|
||||
│ MCH-003: Catálogo de Productos MCH-008: Sistema de Fiados │
|
||||
│ MCH-004: Punto de Venta Básico MCH-009: Predicción de Inventario │
|
||||
│ MCH-005: Integraciones de Pago │
|
||||
│ │
|
||||
│ FASE 3: ASISTENTE IA FASE 4: PEDIDOS Y CLIENTES │
|
||||
│ ──────────────────── ────────────────────────── │
|
||||
│ MCH-010: MCP Server MCH-014: Gestión de Clientes │
|
||||
│ MCH-011: WhatsApp Service MCH-015: Pedidos vía WhatsApp │
|
||||
│ MCH-012: Chat LLM Dueño MCH-016: Entregas a Domicilio │
|
||||
│ MCH-013: Chat LLM Cliente MCH-017: Notificaciones │
|
||||
│ │
|
||||
│ FASE 5: MONETIZACIÓN FASE 6: CRECIMIENTO │
|
||||
│ ──────────────────── ────────────────────── │
|
||||
│ MCH-018: Planes y Suscripciones MCH-022: Modo Offline │
|
||||
│ MCH-019: Tienda de Tokens MCH-023: Programa Referidos │
|
||||
│ MCH-020: Pagos (Stripe/OXXO/IAP) MCH-024: CoDi y SPEI │
|
||||
│ MCH-021: Dashboard Web MCH-025: Widgets y Atajos │
|
||||
│ │
|
||||
│ FASE 7: EXPANSIÓN (Futuro) │
|
||||
│ ────────────────────────── │
|
||||
│ MCH-026: Multi-idioma LATAM │
|
||||
│ MCH-027: Integración SAT │
|
||||
│ MCH-028: Marketplace Proveedores │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Detalle de Épicas
|
||||
|
||||
### FASE 1: MVP CORE
|
||||
|
||||
| ID | Épica | Descripción | Prioridad |
|
||||
|----|-------|-------------|-----------|
|
||||
| MCH-001 | Infraestructura Base | Setup proyecto, DB, CI/CD | P0 |
|
||||
| MCH-002 | Autenticación | Login OTP, JWT, PIN, biométrico | P0 |
|
||||
| MCH-003 | Catálogo de Productos | CRUD productos, categorías, códigos | P0 |
|
||||
| MCH-004 | Punto de Venta Básico | Registro ventas, carrito, ticket | P0 |
|
||||
| MCH-005 | Integraciones de Pago | Mercado Pago, Clip, efectivo | P0 |
|
||||
|
||||
### FASE 2: INTELIGENCIA
|
||||
|
||||
| ID | Épica | Descripción | Prioridad |
|
||||
|----|-------|-------------|-----------|
|
||||
| MCH-006 | Onboarding Inteligente | Setup vía WhatsApp, fotos, audio | P1 |
|
||||
| MCH-007 | Templates y Catálogos | Proveedores comunes, giros | P1 |
|
||||
| MCH-008 | Sistema de Fiados | Crédito a clientes, recordatorios | P1 |
|
||||
| MCH-009 | Predicción Inventario | Alertas, sugerencias de compra | P1 |
|
||||
|
||||
### FASE 3: ASISTENTE IA
|
||||
|
||||
| ID | Épica | Descripción | Prioridad |
|
||||
|----|-------|-------------|-----------|
|
||||
| MCH-010 | MCP Server | Gateway LLM agnóstico, tools | P0 |
|
||||
| MCH-011 | WhatsApp Service | Meta API, webhooks, multi-número | P0 |
|
||||
| MCH-012 | Chat LLM Dueño | Consultas, gestión, reportes | P1 |
|
||||
| MCH-013 | Chat LLM Cliente | Pedidos, consultas, precios | P1 |
|
||||
|
||||
### FASE 4: PEDIDOS Y CLIENTES
|
||||
|
||||
| ID | Épica | Descripción | Prioridad |
|
||||
|----|-------|-------------|-----------|
|
||||
| MCH-014 | Gestión de Clientes | CRUD, historial, comunicación | P1 |
|
||||
| MCH-015 | Pedidos vía WhatsApp | Recepción, procesamiento, cobro | P1 |
|
||||
| MCH-016 | Entregas a Domicilio | Zonas, costos, tracking | P2 |
|
||||
| MCH-017 | Notificaciones | Push, WhatsApp, configuración | P1 |
|
||||
|
||||
### FASE 5: MONETIZACIÓN
|
||||
|
||||
| ID | Épica | Descripción | Prioridad |
|
||||
|----|-------|-------------|-----------|
|
||||
| MCH-018 | Planes y Suscripciones | Changarrito, Tiendita | P0 |
|
||||
| MCH-019 | Tienda de Tokens | Paquetes, saldo, consumo | P1 |
|
||||
| MCH-020 | Pagos Suscripción | Stripe, OXXO, In-App Purchase | P0 |
|
||||
| MCH-021 | Dashboard Web | Reportes, gráficas, config | P1 |
|
||||
|
||||
### FASE 6: CRECIMIENTO
|
||||
|
||||
| ID | Épica | Descripción | Prioridad |
|
||||
|----|-------|-------------|-----------|
|
||||
| MCH-022 | Modo Offline | SQLite, sync, conflictos | P1 |
|
||||
| MCH-023 | Programa Referidos | Códigos, tracking, beneficios | P2 |
|
||||
| MCH-024 | CoDi y SPEI | QR de cobro, CLABE virtual | P2 |
|
||||
| MCH-025 | Widgets y Atajos | Android widgets, quick actions | P2 |
|
||||
|
||||
### FASE 7: EXPANSIÓN (Futuro)
|
||||
|
||||
| ID | Épica | Descripción | Prioridad |
|
||||
|----|-------|-------------|-----------|
|
||||
| MCH-026 | Multi-idioma LATAM | i18n, localización | P3 |
|
||||
| MCH-027 | Integración SAT | Facturación simplificada | P3 |
|
||||
| MCH-028 | Marketplace Proveedores | Conexión con distribuidores | P3 |
|
||||
|
||||
## Índice de Archivos de Épicas
|
||||
|
||||
```
|
||||
docs/01-epicas/
|
||||
├── _MAP.md # Este archivo
|
||||
├── MCH-001-infraestructura-base.md
|
||||
├── MCH-002-autenticacion.md
|
||||
├── MCH-003-catalogo-productos.md
|
||||
├── MCH-004-punto-venta.md
|
||||
├── MCH-005-integraciones-pago.md
|
||||
├── MCH-006-onboarding-inteligente.md
|
||||
├── MCH-007-templates-catalogos.md
|
||||
├── MCH-008-sistema-fiados.md
|
||||
├── MCH-009-prediccion-inventario.md
|
||||
├── MCH-010-mcp-server.md
|
||||
├── MCH-011-whatsapp-service.md
|
||||
├── MCH-012-chat-llm-dueno.md
|
||||
├── MCH-013-chat-llm-cliente.md
|
||||
├── MCH-014-gestion-clientes.md
|
||||
├── MCH-015-pedidos-whatsapp.md
|
||||
├── MCH-016-entregas-domicilio.md
|
||||
├── MCH-017-notificaciones.md
|
||||
├── MCH-018-planes-suscripciones.md
|
||||
├── MCH-019-tienda-tokens.md
|
||||
├── MCH-020-pagos-suscripcion.md
|
||||
├── MCH-021-dashboard-web.md
|
||||
├── MCH-022-modo-offline.md
|
||||
├── MCH-023-programa-referidos.md
|
||||
├── MCH-024-codi-spei.md
|
||||
├── MCH-025-widgets-atajos.md
|
||||
├── MCH-026-multi-idioma-latam.md
|
||||
├── MCH-027-integracion-sat.md
|
||||
└── MCH-028-marketplace-proveedores.md
|
||||
```
|
||||
|
||||
## Dependencias entre Épicas
|
||||
|
||||
```
|
||||
MCH-001 ─────┬────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
▼ │
|
||||
MCH-002 ─────┬─────► MCH-003 ─────► MCH-004 ─────► MCH-005 │
|
||||
│ │ │ │ │
|
||||
│ ▼ │ │ │
|
||||
│ MCH-007 │ │ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ MCH-006 ◄───── MCH-008 ◄───── MCH-014 │
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ │
|
||||
MCH-010 ─────┬─────► MCH-011 ─────► MCH-012 ─────► MCH-013 │
|
||||
│ │ │ │ │
|
||||
│ │ │ ▼ │
|
||||
│ │ │ MCH-015 ────► MCH-016 │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ MCH-017 MCH-009 │
|
||||
│ │
|
||||
▼ │
|
||||
MCH-018 ─────┬─────► MCH-019 ─────► MCH-020 │
|
||||
│ │ │
|
||||
▼ ▼ │
|
||||
MCH-021 MCH-022 │
|
||||
│ │ │
|
||||
└──────────────────────────┴─────► MCH-023 ────► MCH-024 │
|
||||
│ │
|
||||
▼ │
|
||||
MCH-025 │
|
||||
│
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Versión**: 2.0.0
|
||||
**Última actualización**: 2026-01-10
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,182 @@
|
||||
# Especificación de Componentes - MiChangarrito
|
||||
|
||||
**Última actualización**: 2026-01-05
|
||||
|
||||
---
|
||||
|
||||
## Resumen de Componentes
|
||||
|
||||
| Componente | Puerto | Framework | Estado |
|
||||
|------------|--------|-----------|--------|
|
||||
| Frontend Web | 3140 | React 18 + Vite | Implementado |
|
||||
| Backend API | 3141 | NestJS 10.x | Implementado |
|
||||
| MCP Server | stdio | MCP SDK | Implementado |
|
||||
| WhatsApp Service | 3143 | NestJS 10.x | Implementado |
|
||||
| Mobile App | 8081 | React Native | Pendiente |
|
||||
|
||||
---
|
||||
|
||||
## Backend API
|
||||
|
||||
### Ubicación
|
||||
`apps/backend/`
|
||||
|
||||
### Módulos
|
||||
|
||||
| Módulo | Descripción | Endpoints Base |
|
||||
|--------|-------------|----------------|
|
||||
| AuthModule | Autenticación JWT | `/auth/*` |
|
||||
| UsersModule | Gestión de usuarios | `/users/*` |
|
||||
| ProductsModule | CRUD productos | `/products/*` |
|
||||
| SalesModule | Punto de venta | `/sales/*` |
|
||||
| CustomersModule | Clientes y fiado | `/customers/*` |
|
||||
| InventoryModule | Stock y movimientos | `/inventory/*` |
|
||||
| OrdersModule | Pedidos WhatsApp | `/orders/*` |
|
||||
| SubscriptionsModule | Planes y tokens | `/subscriptions/*` |
|
||||
| MessagingModule | Conversaciones | `/messaging/*` |
|
||||
|
||||
### Ejecución
|
||||
```bash
|
||||
cd apps/backend
|
||||
npm run start:dev # Desarrollo
|
||||
npm run build # Producción
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WhatsApp Service
|
||||
|
||||
### Ubicación
|
||||
`apps/whatsapp-service/`
|
||||
|
||||
### Capacidades
|
||||
- Envío de mensajes texto
|
||||
- Mensajes interactivos (botones, listas)
|
||||
- Templates de WhatsApp
|
||||
- Recepción via webhook
|
||||
- Procesamiento LLM
|
||||
|
||||
### Webhook
|
||||
```
|
||||
GET /webhook/whatsapp # Verificación Meta
|
||||
POST /webhook/whatsapp # Recepción de mensajes
|
||||
```
|
||||
|
||||
### Ejecución
|
||||
```bash
|
||||
cd apps/whatsapp-service
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MCP Server
|
||||
|
||||
### Ubicación
|
||||
`apps/mcp-server/`
|
||||
|
||||
### Herramientas Disponibles
|
||||
|
||||
**Productos:**
|
||||
- `list_products` - Listar productos
|
||||
- `get_product_details` - Detalles de producto
|
||||
- `check_product_availability` - Verificar stock
|
||||
|
||||
**Pedidos:**
|
||||
- `create_order` - Crear pedido
|
||||
- `get_order_status` - Estado de pedido
|
||||
- `update_order_status` - Actualizar estado
|
||||
- `cancel_order` - Cancelar pedido
|
||||
|
||||
**Fiado:**
|
||||
- `get_fiado_balance` - Saldo de cliente
|
||||
- `create_fiado` - Registrar fiado
|
||||
- `register_fiado_payment` - Registrar pago
|
||||
- `get_fiado_history` - Historial
|
||||
- `check_fiado_eligibility` - Verificar elegibilidad
|
||||
|
||||
**Clientes:**
|
||||
- `get_customer_info` - Info de cliente
|
||||
- `register_customer` - Registrar cliente
|
||||
- `get_customer_purchase_history` - Historial compras
|
||||
- `get_customer_stats` - Estadísticas
|
||||
|
||||
**Inventario:**
|
||||
- `check_stock` - Verificar stock
|
||||
- `get_low_stock_products` - Productos con stock bajo
|
||||
- `record_inventory_movement` - Registrar movimiento
|
||||
- `get_inventory_value` - Valor del inventario
|
||||
|
||||
### Ejecución
|
||||
```bash
|
||||
cd apps/mcp-server
|
||||
npm run build
|
||||
npm start
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend Web
|
||||
|
||||
### Ubicación
|
||||
`apps/frontend/`
|
||||
|
||||
### Stack
|
||||
- React 18
|
||||
- Vite 7.x
|
||||
- TailwindCSS 4.x
|
||||
- React Router 6
|
||||
- TanStack Query
|
||||
|
||||
### Páginas
|
||||
|
||||
| Ruta | Página | Descripción |
|
||||
|------|--------|-------------|
|
||||
| `/dashboard` | Dashboard | Resumen y estadísticas |
|
||||
| `/products` | Products | Catálogo de productos |
|
||||
| `/orders` | Orders | Gestión de pedidos |
|
||||
| `/customers` | Customers | Lista de clientes |
|
||||
| `/fiado` | Fiado | Gestión de crédito |
|
||||
| `/inventory` | Inventory | Control de stock |
|
||||
| `/settings` | Settings | Configuración |
|
||||
|
||||
### Ejecución
|
||||
```bash
|
||||
cd apps/frontend
|
||||
npm run dev # Desarrollo
|
||||
npm run build # Producción
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuración de Entorno
|
||||
|
||||
Variables requeridas en `.env`:
|
||||
|
||||
```env
|
||||
# Base de datos
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=michangarrito_dev
|
||||
DB_USER=michangarrito_dev
|
||||
DB_PASSWORD=MCh_dev_2025_secure
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=<secret>
|
||||
|
||||
# WhatsApp
|
||||
WHATSAPP_ACCESS_TOKEN=<token>
|
||||
WHATSAPP_PHONE_NUMBER_ID=<id>
|
||||
WHATSAPP_VERIFY_TOKEN=<token>
|
||||
|
||||
# LLM
|
||||
OPENAI_API_KEY=<key>
|
||||
LLM_MODEL=gpt-4o-mini
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
- [Arquitectura Database](../90-transversal/arquitectura/ARQUITECTURA-DATABASE.md)
|
||||
- [Environment Inventory](../../orchestration/environment/ENVIRONMENT-INVENTORY.yml)
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,378 @@
|
||||
# MiChangarrito - Investigacion de Referencias
|
||||
|
||||
## Resumen Ejecutivo
|
||||
|
||||
Este documento consolida la investigacion realizada sobre:
|
||||
1. Modulos ERP existentes reutilizables
|
||||
2. Integraciones de WhatsApp Business
|
||||
3. Model Context Protocol (MCP) para LLM
|
||||
4. Integraciones de pago para Mexico
|
||||
|
||||
---
|
||||
|
||||
## 1. Modulos ERP Reutilizables
|
||||
|
||||
### 1.1 POS-Micro (RECOMENDADO COMO BASE)
|
||||
|
||||
**Ubicacion:** `/workspace-v1/projects/erp-suite/apps/products/pos-micro/`
|
||||
|
||||
**Nivel de Completitud:** 80% - Funcionalmente completo
|
||||
|
||||
| Componente | Estado | Reutilizable |
|
||||
|------------|--------|--------------|
|
||||
| Auth Module (JWT, multi-tenant) | 100% | SI |
|
||||
| Sales Module | 90% | SI |
|
||||
| Products Module | 90% | SI |
|
||||
| Categories Module | 100% | SI |
|
||||
| Payments Module (base) | 40% | PARCIAL |
|
||||
| Database Schema | 80% | SI + EXTENDER |
|
||||
| Frontend React | 75% | SI |
|
||||
|
||||
**Stack Tecnico (compatible con MiChangarrito):**
|
||||
- NestJS 10.3.0
|
||||
- TypeORM 0.3.19
|
||||
- PostgreSQL 8.11
|
||||
- React 18.2.0
|
||||
- Vite 5.0.0
|
||||
- Zustand 4.4.7
|
||||
- TailwindCSS 3.3.5
|
||||
|
||||
**Tablas existentes:**
|
||||
1. tenants - Multi-tenant root
|
||||
2. users - Usuarios
|
||||
3. categories - Categorias
|
||||
4. products - Catalogo
|
||||
5. sales - Ventas
|
||||
6. sale_items - Detalle venta
|
||||
7. payments - Metodos pago
|
||||
8. inventory_movements - Stock
|
||||
9. daily_closures - Cortes caja
|
||||
|
||||
**Archivos clave para copiar:**
|
||||
```
|
||||
pos-micro/backend/src/modules/auth/ -> COPIAR COMPLETO
|
||||
pos-micro/backend/src/modules/sales/ -> COPIAR + ADAPTAR
|
||||
pos-micro/backend/src/modules/products/ -> COPIAR COMPLETO
|
||||
pos-micro/backend/src/modules/categories/ -> COPIAR COMPLETO
|
||||
pos-micro/database/ddl/00-schema.sql -> COPIAR + EXTENDER
|
||||
pos-micro/frontend/src/store/ -> COPIAR COMPLETO
|
||||
pos-micro/frontend/src/components/ -> ADAPTAR
|
||||
```
|
||||
|
||||
### 1.2 Gamilit (Patrones de Arquitectura)
|
||||
|
||||
**Ubicacion:** `/workspace-v1/projects/gamilit/`
|
||||
|
||||
**Utilidad:** Patrones de arquitectura avanzados
|
||||
|
||||
**Patrones reutilizables:**
|
||||
- Guards, decorators, interceptors
|
||||
- Shared utilities structure
|
||||
- Error handling robusto
|
||||
- Sistema de notificaciones
|
||||
- WebSocket para tiempo real
|
||||
- Health checks y monitoring
|
||||
|
||||
**Archivos de referencia:**
|
||||
```
|
||||
gamilit/apps/backend/src/shared/ -> PATRONES (no copiar directo)
|
||||
gamilit/apps/frontend/src/ -> ESTRUCTURA
|
||||
```
|
||||
|
||||
### 1.3 Trading-Platform (LLM Agent)
|
||||
|
||||
**Ubicacion:** `/workspace-v1/projects/trading-platform/apps/llm-agent/`
|
||||
|
||||
**Utilidad:** Arquitectura agnostica de LLM
|
||||
|
||||
**Componentes reutilizables:**
|
||||
- LLM Client Factory Pattern (adaptador multi-proveedor)
|
||||
- Tool System (function calling)
|
||||
- Prompt Management
|
||||
- SSE Streaming para chat
|
||||
|
||||
**Archivos clave:**
|
||||
```python
|
||||
# Traducir a TypeScript:
|
||||
src/core/llm_client.py -> Adaptador agnostico LLM
|
||||
src/tools/base.py -> Base class para tools
|
||||
src/api/routes.py -> SSE streaming endpoints
|
||||
```
|
||||
|
||||
**Client TypeScript existente:**
|
||||
```
|
||||
trading-platform/apps/backend/src/shared/clients/llm-agent.client.ts
|
||||
```
|
||||
|
||||
### 1.4 Shared-Libs Core (Utilidades Base)
|
||||
|
||||
**Ubicacion:** `/workspace-v1/projects/erp-suite/apps/shared-libs/core/`
|
||||
|
||||
**Utilidades reutilizables:**
|
||||
- Base entities (id, createdAt, updatedAt)
|
||||
- Tenant middleware (RLS)
|
||||
- Auth middleware
|
||||
- Error handling base
|
||||
- BaseTypeOrmService (CRUD generico)
|
||||
- Factory patterns
|
||||
|
||||
---
|
||||
|
||||
## 2. WhatsApp Business Integration
|
||||
|
||||
### 2.1 Opcion Principal: Meta Cloud API
|
||||
|
||||
**Documentacion oficial:**
|
||||
- https://business.whatsapp.com/developers/developer-hub
|
||||
- https://developers.facebook.com/
|
||||
|
||||
**Caracteristicas:**
|
||||
- API oficial de Meta
|
||||
- Webhooks para mensajes entrantes
|
||||
- Templates pre-aprobados para iniciar conversaciones
|
||||
- Mensajes de sesion (24h window)
|
||||
- Multimedia: texto, imagenes, videos, documentos, ubicacion
|
||||
|
||||
**Rate Limits:**
|
||||
- 80 msg/seg (default)
|
||||
- Hasta 1,000 msg/seg para cuentas elegibles
|
||||
- Numeros nuevos: 250-1,000 msg/dia inicialmente
|
||||
|
||||
**Modelo de precios (Julio 2025):**
|
||||
- Cobro por mensaje entregado (no por conversacion)
|
||||
- Marketing: mas caro
|
||||
- Utility: medio
|
||||
- Authentication: medio
|
||||
- Service (respuestas): gratis en ventana 24h
|
||||
|
||||
### 2.2 Multi-Tenant WhatsApp
|
||||
|
||||
**Opciones:**
|
||||
|
||||
**A. Tech Provider de Meta (RECOMENDADO)**
|
||||
- Requiere aprobacion de Meta
|
||||
- Embedded Signup para onboarding self-service
|
||||
- Trabaja con BSP (Twilio, Infobip, MessageBird)
|
||||
|
||||
**B. Numero Compartido de Plataforma**
|
||||
- Un numero para multiples negocios
|
||||
- Deteccion de tenant por:
|
||||
- Keyword inicial ("Hola, busco [Nombre Negocio]")
|
||||
- Contexto de conversacion
|
||||
- Base de datos de clientes
|
||||
|
||||
### 2.3 Librerias de Referencia
|
||||
|
||||
| Libreria | GitHub | Notas |
|
||||
|----------|--------|-------|
|
||||
| Baileys | WhiskeySockets/Baileys | WebSocket API, multi-device |
|
||||
| whatsapp-web.js | pedroslopez/whatsapp-web.js | Puppeteer, 20k+ stars |
|
||||
| NestWhats | NedcloarBR/NestWhats | Wrapper NestJS |
|
||||
|
||||
**ADVERTENCIA:** Librerias no oficiales pueden resultar en bloqueo de numero.
|
||||
|
||||
---
|
||||
|
||||
## 3. Model Context Protocol (MCP)
|
||||
|
||||
### 3.1 Que es MCP
|
||||
|
||||
Protocolo abierto de Anthropic (Nov 2024) para integrar LLMs con fuentes de datos/herramientas externas.
|
||||
|
||||
**Arquitectura:** Cliente-Servidor basado en JSON-RPC 2.0
|
||||
|
||||
**Componentes:**
|
||||
- **Hosts:** Aplicaciones LLM (Claude Desktop, VS Code)
|
||||
- **Clients:** Conectores dentro del host
|
||||
- **Servers:** Servicios que proveen contexto
|
||||
|
||||
### 3.2 Primitivas del Protocolo
|
||||
|
||||
| Primitiva | Descripcion |
|
||||
|-----------|-------------|
|
||||
| **Tools** | Funciones ejecutables (APIs, DB queries) |
|
||||
| **Resources** | Datos de solo lectura |
|
||||
| **Prompts** | Templates reutilizables |
|
||||
| **Sampling** | Comportamientos agentivos |
|
||||
|
||||
### 3.3 SDK TypeScript
|
||||
|
||||
**Instalacion:**
|
||||
```bash
|
||||
npm install @modelcontextprotocol/server zod
|
||||
```
|
||||
|
||||
**Ejemplo basico:**
|
||||
```typescript
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
|
||||
const server = new Server({
|
||||
name: "michangarrito-mcp",
|
||||
version: "1.0.0"
|
||||
}, {
|
||||
capabilities: { tools: {} }
|
||||
});
|
||||
|
||||
// Registrar tools...
|
||||
```
|
||||
|
||||
### 3.4 Tools para MiChangarrito
|
||||
|
||||
| Categoria | Tools |
|
||||
|-----------|-------|
|
||||
| Ventas | registrar_venta, obtener_ventas, generar_corte |
|
||||
| Productos | buscar_producto, crear_producto, actualizar_precio |
|
||||
| Inventario | consultar_stock, registrar_entrada, alertas_stock |
|
||||
| Clientes | buscar_cliente, crear_cliente, historial_cliente |
|
||||
| Fiados | registrar_fiado, obtener_fiados, marcar_pagado |
|
||||
| Pedidos | crear_pedido, obtener_pedidos, actualizar_estado |
|
||||
| Reportes | generar_reporte, enviar_whatsapp, exportar_excel |
|
||||
|
||||
### 3.5 Integracion Multi-Provider
|
||||
|
||||
| Provider | Estado MCP |
|
||||
|----------|------------|
|
||||
| Claude | Nativo (creador) |
|
||||
| ChatGPT/OpenAI | Completo (marzo 2025) |
|
||||
| Gemini | Soportado |
|
||||
| OpenRouter | Compatible |
|
||||
|
||||
---
|
||||
|
||||
## 4. Integraciones de Pago Mexico
|
||||
|
||||
### 4.1 Mercado Pago Point
|
||||
|
||||
**Documentacion:** https://www.mercadopago.com.mx/developers/es/docs/mp-point/overview
|
||||
|
||||
**SDK Node.js:**
|
||||
```bash
|
||||
npm install mercadopago
|
||||
```
|
||||
|
||||
**Flujo:**
|
||||
1. Crear Payment Intent
|
||||
2. Enviar a terminal Point
|
||||
3. Procesar pago
|
||||
4. Webhook de confirmacion
|
||||
|
||||
**Comisiones:**
|
||||
- Inmediato: 3.49% + $4 MXN + IVA
|
||||
- 30 dias: 2.95% + $4 MXN + IVA
|
||||
|
||||
### 4.2 Clip
|
||||
|
||||
**Documentacion:** https://developer.clip.mx/
|
||||
|
||||
**SDK:** Solo REST API (no SDK Node.js oficial)
|
||||
|
||||
**Endpoints principales:**
|
||||
```
|
||||
POST /paymentrequest/ - Crear solicitud
|
||||
DELETE /paymentrequest/code/X - Cancelar
|
||||
GET /payments/receipt-no/X - Detalles
|
||||
GET /payments?from=X&to=Y - Lista
|
||||
```
|
||||
|
||||
**Comisiones:**
|
||||
- Estandar: 3.6% + IVA
|
||||
- MSI: +1% a +24% segun meses
|
||||
|
||||
### 4.3 CoDi (Banxico)
|
||||
|
||||
**Documentacion:** https://www.codi.org.mx/
|
||||
|
||||
**Integracion via:** Openpay, dapp (recomendado vs directo Banxico)
|
||||
|
||||
**SDK Node.js (Openpay):**
|
||||
```javascript
|
||||
openpay.charges.create({
|
||||
method: 'codi',
|
||||
amount: 100.00,
|
||||
codi_options: { mode: 'QR_CODE' }
|
||||
});
|
||||
```
|
||||
|
||||
**Flujo:**
|
||||
1. Generar QR dinamico
|
||||
2. Cliente escanea con app bancaria
|
||||
3. Autoriza pago
|
||||
4. Webhook de confirmacion
|
||||
|
||||
**Comisiones:** Gratis con Banxico (intermediarios pueden cobrar)
|
||||
|
||||
### 4.4 Stripe (OXXO + Suscripciones)
|
||||
|
||||
**Documentacion:** https://docs.stripe.com/payments/oxxo
|
||||
|
||||
**SDK Node.js:**
|
||||
```bash
|
||||
npm install stripe
|
||||
```
|
||||
|
||||
**OXXO References:**
|
||||
```javascript
|
||||
const paymentIntent = await stripe.paymentIntents.create({
|
||||
amount: 10000, // centavos
|
||||
currency: 'mxn',
|
||||
payment_method_types: ['oxxo']
|
||||
});
|
||||
```
|
||||
|
||||
**Limitaciones OXXO:**
|
||||
- Min: $10 MXN, Max: $10,000 MXN
|
||||
- Vigencia: 1-7 dias
|
||||
- **NO soporta suscripciones**
|
||||
|
||||
**Suscripciones:**
|
||||
```javascript
|
||||
const subscription = await stripe.subscriptions.create({
|
||||
customer: 'cus_xxx',
|
||||
items: [{ price: 'price_xxx' }]
|
||||
});
|
||||
```
|
||||
|
||||
**Comisiones:**
|
||||
- Tarjetas/OXXO: 3.6% + $3 MXN + IVA
|
||||
|
||||
---
|
||||
|
||||
## 5. Matriz de Decision
|
||||
|
||||
### Reutilizacion de Codigo
|
||||
|
||||
| Fuente | % Reutilizable | Prioridad | Esfuerzo |
|
||||
|--------|----------------|-----------|----------|
|
||||
| POS-Micro Backend | 80% | CRITICO | Bajo |
|
||||
| POS-Micro Frontend | 60% | ALTO | Medio |
|
||||
| Shared-Libs Core | 100% | CRITICO | Bajo |
|
||||
| Trading-Platform LLM | 70% | ALTO | Medio |
|
||||
| Gamilit Patterns | 40% | MEDIO | Bajo |
|
||||
|
||||
### Integraciones de Pago
|
||||
|
||||
| Proveedor | Prioridad | Complejidad | Costo |
|
||||
|-----------|-----------|-------------|-------|
|
||||
| Stripe (suscripciones) | P0 | Media | 3.6% |
|
||||
| Stripe OXXO | P0 | Baja | 3.6% |
|
||||
| Mercado Pago Point | P1 | Media | 2.95-3.49% |
|
||||
| Clip | P2 | Media | 3.6% |
|
||||
| CoDi | P2 | Alta | Gratis |
|
||||
|
||||
---
|
||||
|
||||
## 6. Proximos Pasos
|
||||
|
||||
1. **FASE 1:** Copiar y adaptar POS-Micro como base
|
||||
2. **FASE 2:** Extender schema BD (customers, fiados, orders, subscriptions, tokens)
|
||||
3. **FASE 3:** Implementar WhatsApp Service con Meta Cloud API
|
||||
4. **FASE 4:** Implementar MCP Server con tools basicos
|
||||
5. **FASE 5:** Integrar Stripe (suscripciones + OXXO)
|
||||
6. **FASE 6:** Integrar terminales (Mercado Pago, Clip)
|
||||
7. **FASE 7:** Desarrollar app mobile React Native
|
||||
|
||||
---
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2026-01-04
|
||||
@ -0,0 +1,774 @@
|
||||
# MiChangarrito - Plan de Desarrollo
|
||||
|
||||
## Estrategia General
|
||||
|
||||
### Base de Codigo
|
||||
- **Reutilizar:** POS-Micro como base (80% del backend)
|
||||
- **Extender:** Schema de BD con nuevas tablas
|
||||
- **Adaptar:** Trading-Platform LLM patterns a TypeScript
|
||||
- **Nuevo:** WhatsApp Service, MCP Server, Mobile App
|
||||
|
||||
### Timeline Estimado
|
||||
- **MVP Fase 1:** 8-10 semanas
|
||||
- **Fase 2 Inteligencia:** 4-6 semanas
|
||||
- **Fase 3 Asistente IA:** 6-8 semanas
|
||||
- **Total MVP Completo:** 18-24 semanas (~5-6 meses)
|
||||
|
||||
---
|
||||
|
||||
## FASE 1: MVP CORE
|
||||
|
||||
### MCH-001: Infraestructura Base
|
||||
**Duracion estimada:** 1-2 semanas
|
||||
**Prioridad:** P0
|
||||
**Dependencias:** Ninguna
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Copiar estructura de POS-Micro a michangarrito/apps/backend
|
||||
- [ ] Actualizar package.json con nombre y dependencias
|
||||
- [ ] Configurar TypeORM para PostgreSQL 5432 (instancia compartida workspace)
|
||||
- [ ] Configurar Redis 6379/DB8 (instancia compartida workspace)
|
||||
- [ ] Implementar health checks
|
||||
- [ ] Configurar Swagger/OpenAPI
|
||||
- [ ] Setup Docker Compose (db, redis, mailhog)
|
||||
- [ ] Configurar ESLint + Prettier
|
||||
|
||||
#### Tareas Database
|
||||
- [ ] Crear script de inicializacion (database/init/)
|
||||
- [ ] Implementar schema `public` (tenants, tenant_configs)
|
||||
- [ ] Implementar schema `auth` (users, sessions, otp_codes)
|
||||
- [ ] Implementar RLS policies basicas
|
||||
- [ ] Crear funciones utilitarias (generate_ticket_number, update_updated_at)
|
||||
- [ ] Seeds de datos de prueba
|
||||
|
||||
#### Tareas DevOps
|
||||
- [ ] Crear .env.example completo
|
||||
- [ ] Documentar proceso de setup
|
||||
- [ ] Configurar scripts npm (dev, build, test, migration)
|
||||
|
||||
#### Entregables
|
||||
- Backend NestJS corriendo en puerto 3141
|
||||
- Base de datos con schemas public y auth
|
||||
- Docker Compose funcional
|
||||
- Documentacion de setup
|
||||
|
||||
---
|
||||
|
||||
### MCH-002: Autenticacion
|
||||
**Duracion estimada:** 1 semana
|
||||
**Prioridad:** P0
|
||||
**Dependencias:** MCH-001
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Adaptar AuthModule de POS-Micro
|
||||
- [ ] Implementar registro de tenant + usuario owner
|
||||
- [ ] Implementar login con OTP (SMS/WhatsApp)
|
||||
- [ ] Implementar PIN de acceso rapido
|
||||
- [ ] Configurar JWT con refresh tokens
|
||||
- [ ] Implementar biometrico (preparar endpoints)
|
||||
- [ ] Guards para roles (owner, employee)
|
||||
- [ ] Middleware de tenant isolation
|
||||
|
||||
#### Tareas Database
|
||||
- [ ] Verificar tablas auth.users, auth.sessions, auth.otp_codes
|
||||
- [ ] Indices para busqueda por telefono
|
||||
|
||||
#### Integraciones
|
||||
- [ ] Servicio de envio OTP (placeholder, luego WhatsApp)
|
||||
|
||||
#### Entregables
|
||||
- Endpoints: POST /auth/register, POST /auth/login, POST /auth/verify-otp
|
||||
- Endpoints: POST /auth/set-pin, POST /auth/login-pin
|
||||
- Endpoints: POST /auth/refresh, POST /auth/logout
|
||||
- Guards funcionando
|
||||
|
||||
---
|
||||
|
||||
### MCH-003: Catalogo de Productos
|
||||
**Duracion estimada:** 1 semana
|
||||
**Prioridad:** P0
|
||||
**Dependencias:** MCH-002
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Copiar ProductsModule de POS-Micro
|
||||
- [ ] Copiar CategoriesModule de POS-Micro
|
||||
- [ ] Adaptar DTOs para MiChangarrito
|
||||
- [ ] Implementar busqueda por nombre (full-text)
|
||||
- [ ] Implementar busqueda por codigo de barras
|
||||
- [ ] Endpoint de importacion masiva
|
||||
- [ ] Endpoint de templates de proveedores
|
||||
|
||||
#### Tareas Database
|
||||
- [ ] Implementar schema `catalog`
|
||||
- [ ] Tabla catalog.categories
|
||||
- [ ] Tabla catalog.products
|
||||
- [ ] Tabla catalog.product_templates
|
||||
- [ ] Seeds de templates (Bimbo, Coca-Cola, Sabritas)
|
||||
|
||||
#### Entregables
|
||||
- CRUD completo de categorias
|
||||
- CRUD completo de productos
|
||||
- Busqueda y filtrado
|
||||
- Templates de proveedores cargados
|
||||
|
||||
---
|
||||
|
||||
### MCH-004: Punto de Venta Basico
|
||||
**Duracion estimada:** 1.5 semanas
|
||||
**Prioridad:** P0
|
||||
**Dependencias:** MCH-003
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Copiar SalesModule de POS-Micro
|
||||
- [ ] Adaptar para multiples metodos de pago
|
||||
- [ ] Implementar generacion de ticket
|
||||
- [ ] Implementar corte de caja
|
||||
- [ ] Calculos de totales, impuestos, cambio
|
||||
- [ ] Cancelacion de ventas
|
||||
|
||||
#### Tareas Database
|
||||
- [ ] Implementar schema `sales`
|
||||
- [ ] Tabla sales.sales
|
||||
- [ ] Tabla sales.sale_items
|
||||
- [ ] Tabla sales.payments
|
||||
- [ ] Tabla sales.daily_closures
|
||||
|
||||
#### Tareas Frontend Web
|
||||
- [ ] Inicializar proyecto React + Vite en apps/web
|
||||
- [ ] Configurar TailwindCSS
|
||||
- [ ] Copiar componentes base de POS-Micro frontend
|
||||
- [ ] Adaptar POSPage
|
||||
- [ ] Adaptar CartPanel
|
||||
- [ ] Adaptar CheckoutModal
|
||||
- [ ] Implementar login screen
|
||||
- [ ] Implementar dashboard basico
|
||||
|
||||
#### Entregables
|
||||
- Registro de ventas completo
|
||||
- Corte de caja funcional
|
||||
- UI web basica de POS
|
||||
|
||||
---
|
||||
|
||||
### MCH-005: Integraciones de Pago
|
||||
**Duracion estimada:** 2 semanas
|
||||
**Prioridad:** P0
|
||||
**Dependencias:** MCH-004
|
||||
|
||||
#### Tareas Backend - Efectivo
|
||||
- [ ] Registro de pago en efectivo
|
||||
- [ ] Calculo de cambio
|
||||
- [ ] Validacion de montos
|
||||
|
||||
#### Tareas Backend - Stripe
|
||||
- [ ] Configurar SDK de Stripe
|
||||
- [ ] Crear PaymentsModule para suscripciones
|
||||
- [ ] Implementar creacion de customer
|
||||
- [ ] Implementar webhook handler
|
||||
- [ ] Endpoint para pago con OXXO
|
||||
|
||||
#### Tareas Backend - Mercado Pago (Basico)
|
||||
- [ ] Configurar API de Mercado Pago
|
||||
- [ ] Implementar creacion de payment intent
|
||||
- [ ] Implementar webhook handler
|
||||
- [ ] Manejo de estados de pago
|
||||
|
||||
#### Tareas Database
|
||||
- [ ] Completar tabla sales.payments con todos los campos
|
||||
- [ ] Indices para referencias externas
|
||||
|
||||
#### Entregables
|
||||
- Pago en efectivo funcionando
|
||||
- Stripe configurado (suscripciones preparadas)
|
||||
- Mercado Pago basico funcionando
|
||||
|
||||
---
|
||||
|
||||
## FASE 2: INTELIGENCIA
|
||||
|
||||
### MCH-006: Onboarding Inteligente
|
||||
**Duracion estimada:** 1.5 semanas
|
||||
**Prioridad:** P1
|
||||
**Dependencias:** MCH-002, MCH-003
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Crear OnboardingModule
|
||||
- [ ] Flujo de registro guiado
|
||||
- [ ] Procesamiento de imagenes (OCR)
|
||||
- [ ] Procesamiento de audios (transcripcion)
|
||||
- [ ] Seleccion de templates por giro
|
||||
- [ ] Estado de onboarding por tenant
|
||||
|
||||
#### Integraciones
|
||||
- [ ] Google Cloud Vision para OCR
|
||||
- [ ] OpenAI Whisper para audio
|
||||
|
||||
#### Entregables
|
||||
- Flujo de onboarding paso a paso
|
||||
- Carga de productos desde fotos
|
||||
- Carga de productos desde audio
|
||||
|
||||
---
|
||||
|
||||
### MCH-007: Templates y Catalogos
|
||||
**Duracion estimada:** 1 semana
|
||||
**Prioridad:** P1
|
||||
**Dependencias:** MCH-003
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] CRUD de templates administrativo
|
||||
- [ ] Endpoint de busqueda de templates
|
||||
- [ ] Importacion de template a tenant
|
||||
- [ ] Precios sugeridos por region
|
||||
|
||||
#### Tareas Database
|
||||
- [ ] Ampliar catalog.product_templates
|
||||
- [ ] Seeds extensivos de proveedores
|
||||
|
||||
#### Entregables
|
||||
- Catalogo de 500+ productos template
|
||||
- Busqueda por proveedor/categoria
|
||||
- Importacion one-click
|
||||
|
||||
---
|
||||
|
||||
### MCH-008: Sistema de Fiados
|
||||
**Duracion estimada:** 1.5 semanas
|
||||
**Prioridad:** P1
|
||||
**Dependencias:** MCH-004
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Crear CustomersModule
|
||||
- [ ] Crear FiadosModule
|
||||
- [ ] CRUD de clientes
|
||||
- [ ] Registro de fiado al vender
|
||||
- [ ] Abonos a fiados
|
||||
- [ ] Recordatorios automaticos
|
||||
- [ ] Reporte de fiados pendientes
|
||||
|
||||
#### Tareas Database
|
||||
- [ ] Implementar schema `customers`
|
||||
- [ ] Tabla customers.customers
|
||||
- [ ] Tabla customers.fiados
|
||||
- [ ] Tabla customers.fiado_payments
|
||||
- [ ] Trigger para balance de cliente
|
||||
|
||||
#### Entregables
|
||||
- Gestion completa de clientes
|
||||
- Sistema de fiados funcional
|
||||
- Recordatorios configurables
|
||||
|
||||
---
|
||||
|
||||
### MCH-009: Prediccion de Inventario
|
||||
**Duracion estimada:** 1 semana
|
||||
**Prioridad:** P1
|
||||
**Dependencias:** MCH-004
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Crear InventoryModule
|
||||
- [ ] Movimientos de inventario automaticos (ventas)
|
||||
- [ ] Registro de compras/entradas
|
||||
- [ ] Alertas de stock bajo
|
||||
- [ ] Prediccion basica (promedio movil)
|
||||
- [ ] Sugerencias de resurtido
|
||||
|
||||
#### Tareas Database
|
||||
- [ ] Implementar schema `inventory`
|
||||
- [ ] Tabla inventory.inventory_movements
|
||||
- [ ] Tabla inventory.stock_alerts
|
||||
|
||||
#### Entregables
|
||||
- Control de inventario automatico
|
||||
- Alertas de stock bajo
|
||||
- Sugerencias de compra basicas
|
||||
|
||||
---
|
||||
|
||||
## FASE 3: ASISTENTE IA
|
||||
|
||||
### MCH-010: MCP Server
|
||||
**Duracion estimada:** 2 semanas
|
||||
**Prioridad:** P0
|
||||
**Dependencias:** MCH-004, MCH-008, MCH-009
|
||||
|
||||
#### Tareas MCP Server
|
||||
- [ ] Inicializar proyecto en apps/mcp-server
|
||||
- [ ] Configurar MCP SDK TypeScript
|
||||
- [ ] Implementar transporte stdio
|
||||
- [ ] Implementar tools basicos:
|
||||
- [ ] buscar_producto
|
||||
- [ ] registrar_venta
|
||||
- [ ] obtener_ventas
|
||||
- [ ] consultar_stock
|
||||
- [ ] registrar_fiado
|
||||
- [ ] obtener_fiados
|
||||
- [ ] generar_reporte_ventas
|
||||
- [ ] Implementar autenticacion por tenant
|
||||
- [ ] Implementar rate limiting
|
||||
- [ ] Logging de tool calls
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Cliente HTTP para MCP Server
|
||||
- [ ] Tracking de uso de tokens
|
||||
|
||||
#### Entregables
|
||||
- MCP Server corriendo en puerto 3142
|
||||
- 10+ tools funcionales
|
||||
- Documentacion de tools
|
||||
|
||||
---
|
||||
|
||||
### MCH-011: WhatsApp Service
|
||||
**Duracion estimada:** 2 semanas
|
||||
**Prioridad:** P0
|
||||
**Dependencias:** MCH-002
|
||||
|
||||
#### Tareas WhatsApp Service
|
||||
- [ ] Inicializar proyecto NestJS en apps/whatsapp-service
|
||||
- [ ] Configurar Meta Cloud API
|
||||
- [ ] Implementar verificacion de webhook
|
||||
- [ ] Implementar recepcion de mensajes
|
||||
- [ ] Implementar envio de mensajes (texto, template, botones)
|
||||
- [ ] Implementar envio de multimedia
|
||||
- [ ] Deteccion de tipo de usuario (owner vs customer)
|
||||
- [ ] Persistencia de conversaciones
|
||||
|
||||
#### Tareas Database
|
||||
- [ ] Implementar schema `messaging`
|
||||
- [ ] Tabla messaging.conversations
|
||||
- [ ] Tabla messaging.messages
|
||||
- [ ] Tabla messaging.notifications
|
||||
|
||||
#### Integraciones
|
||||
- [ ] Meta WhatsApp Cloud API
|
||||
|
||||
#### Entregables
|
||||
- WhatsApp Service corriendo en puerto 3143
|
||||
- Webhook funcional
|
||||
- Envio/recepcion de mensajes
|
||||
- Deteccion de roles
|
||||
|
||||
---
|
||||
|
||||
### MCH-012: Chat LLM Dueno
|
||||
**Duracion estimada:** 1.5 semanas
|
||||
**Prioridad:** P1
|
||||
**Dependencias:** MCH-010, MCH-011
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Crear LLMModule
|
||||
- [ ] Cliente agnostico de LLM (OpenRouter/OpenAI/Claude)
|
||||
- [ ] System prompts para dueno
|
||||
- [ ] Integracion con MCP tools
|
||||
- [ ] Contexto de conversacion
|
||||
- [ ] Tracking de tokens
|
||||
|
||||
#### Tareas WhatsApp
|
||||
- [ ] Flujo de mensaje de dueno -> LLM -> respuesta
|
||||
- [ ] Manejo de tool calls
|
||||
- [ ] Respuestas con formato amigable
|
||||
|
||||
#### Entregables
|
||||
- Dueno puede consultar via WhatsApp:
|
||||
- "Cuanto vendi hoy?"
|
||||
- "Que me falta por resurtir?"
|
||||
- "Registra una venta de 2 cocas"
|
||||
|
||||
---
|
||||
|
||||
### MCH-013: Chat LLM Cliente
|
||||
**Duracion estimada:** 1 semana
|
||||
**Prioridad:** P1
|
||||
**Dependencias:** MCH-012
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] System prompts para cliente
|
||||
- [ ] Tools limitados para cliente
|
||||
- [ ] Deteccion de intencion (consulta, pedido, precio)
|
||||
|
||||
#### Tareas WhatsApp
|
||||
- [ ] Flujo de mensaje cliente -> LLM -> respuesta
|
||||
- [ ] Notificacion a dueno de pedidos
|
||||
|
||||
#### Entregables
|
||||
- Cliente puede consultar via WhatsApp:
|
||||
- "Tienen refrescos?"
|
||||
- "A como el kilo de tortilla?"
|
||||
- "Quiero pedir 3 tacos para llevar"
|
||||
|
||||
---
|
||||
|
||||
## FASE 4: PEDIDOS Y CLIENTES
|
||||
|
||||
### MCH-014: Gestion de Clientes
|
||||
**Duracion estimada:** 1 semana
|
||||
**Prioridad:** P1
|
||||
**Dependencias:** MCH-008
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Extender CustomersModule
|
||||
- [ ] Historial de compras por cliente
|
||||
- [ ] Productos frecuentes
|
||||
- [ ] Comunicacion directa por WhatsApp
|
||||
|
||||
#### Tareas Frontend
|
||||
- [ ] Pantalla de clientes
|
||||
- [ ] Detalle de cliente
|
||||
- [ ] Historial de compras
|
||||
|
||||
#### Entregables
|
||||
- Gestion completa de clientes en web
|
||||
- Historial y estadisticas
|
||||
|
||||
---
|
||||
|
||||
### MCH-015: Pedidos via WhatsApp
|
||||
**Duracion estimada:** 1.5 semanas
|
||||
**Prioridad:** P1
|
||||
**Dependencias:** MCH-013
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Crear OrdersModule
|
||||
- [ ] Recepcion de pedidos desde LLM
|
||||
- [ ] Estados de pedido
|
||||
- [ ] Notificaciones push de nuevo pedido
|
||||
|
||||
#### Tareas Database
|
||||
- [ ] Implementar schema `orders`
|
||||
- [ ] Tabla orders.orders
|
||||
- [ ] Tabla orders.order_items
|
||||
|
||||
#### Tareas Frontend
|
||||
- [ ] Vista de pedidos pendientes
|
||||
- [ ] Aceptar/rechazar pedido
|
||||
- [ ] Cobrar pedido
|
||||
|
||||
#### Entregables
|
||||
- Flujo completo de pedidos
|
||||
- Notificaciones en tiempo real
|
||||
|
||||
---
|
||||
|
||||
### MCH-016: Entregas a Domicilio
|
||||
**Duracion estimada:** 1 semana
|
||||
**Prioridad:** P2
|
||||
**Dependencias:** MCH-015
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Configuracion de zonas de entrega
|
||||
- [ ] Costos de envio
|
||||
- [ ] Estados de entrega
|
||||
- [ ] Tracking basico
|
||||
|
||||
#### Entregables
|
||||
- Configuracion de delivery
|
||||
- Estados de pedido con entrega
|
||||
|
||||
---
|
||||
|
||||
### MCH-017: Notificaciones
|
||||
**Duracion estimada:** 1 semana
|
||||
**Prioridad:** P1
|
||||
**Dependencias:** MCH-011
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Crear NotificationsModule
|
||||
- [ ] Integracion Firebase FCM
|
||||
- [ ] Programacion de notificaciones
|
||||
- [ ] Resumen diario automatico
|
||||
- [ ] Alertas de stock
|
||||
|
||||
#### Integraciones
|
||||
- [ ] Firebase Cloud Messaging
|
||||
|
||||
#### Entregables
|
||||
- Push notifications funcionales
|
||||
- Resumen diario por WhatsApp
|
||||
- Alertas configurables
|
||||
|
||||
---
|
||||
|
||||
## FASE 5: MONETIZACION
|
||||
|
||||
### MCH-018: Planes y Suscripciones
|
||||
**Duracion estimada:** 1.5 semanas
|
||||
**Prioridad:** P0
|
||||
**Dependencias:** MCH-005
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Crear SubscriptionsModule
|
||||
- [ ] Definicion de planes (Changarrito, Tiendita)
|
||||
- [ ] Creacion de suscripcion en Stripe
|
||||
- [ ] Manejo de webhooks de suscripcion
|
||||
- [ ] Cambio de plan
|
||||
- [ ] Cancelacion
|
||||
|
||||
#### Tareas Database
|
||||
- [ ] Implementar schema `subscriptions`
|
||||
- [ ] Tabla subscriptions.plans
|
||||
- [ ] Tabla subscriptions.subscriptions
|
||||
|
||||
#### Entregables
|
||||
- Planes configurados en Stripe
|
||||
- Suscripcion funcional
|
||||
|
||||
---
|
||||
|
||||
### MCH-019: Tienda de Tokens
|
||||
**Duracion estimada:** 1 semana
|
||||
**Prioridad:** P1
|
||||
**Dependencias:** MCH-018
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Paquetes de tokens
|
||||
- [ ] Compra de tokens (Stripe)
|
||||
- [ ] Balance de tokens por tenant
|
||||
- [ ] Consumo de tokens en cada llamada LLM
|
||||
|
||||
#### Tareas Database
|
||||
- [ ] Tabla subscriptions.token_packages
|
||||
- [ ] Tabla subscriptions.token_usage
|
||||
- [ ] Tabla subscriptions.tenant_token_balance
|
||||
|
||||
#### Entregables
|
||||
- Tienda de tokens funcional
|
||||
- Tracking de consumo
|
||||
|
||||
---
|
||||
|
||||
### MCH-020: Pagos Suscripcion
|
||||
**Duracion estimada:** 1 semana
|
||||
**Prioridad:** P0
|
||||
**Dependencias:** MCH-018
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Pago con OXXO (referencia)
|
||||
- [ ] Preparar IAP iOS (endpoints)
|
||||
- [ ] Preparar IAP Android (endpoints)
|
||||
|
||||
#### Entregables
|
||||
- Multiples metodos de pago para suscripcion
|
||||
|
||||
---
|
||||
|
||||
### MCH-021: Dashboard Web
|
||||
**Duracion estimada:** 2 semanas
|
||||
**Prioridad:** P1
|
||||
**Dependencias:** MCH-004, MCH-008
|
||||
|
||||
#### Tareas Frontend
|
||||
- [ ] Dashboard principal con metricas
|
||||
- [ ] Graficas de ventas
|
||||
- [ ] Productos mas vendidos
|
||||
- [ ] Reportes descargables
|
||||
- [ ] Configuracion de negocio
|
||||
- [ ] Gestion de suscripcion
|
||||
|
||||
#### Entregables
|
||||
- Dashboard web completo
|
||||
- Reportes PDF/Excel
|
||||
|
||||
---
|
||||
|
||||
## FASE 6: CRECIMIENTO
|
||||
|
||||
### MCH-022: Modo Offline
|
||||
**Duracion estimada:** 2 semanas
|
||||
**Prioridad:** P1
|
||||
**Dependencias:** MCH-004
|
||||
|
||||
#### Tareas Mobile
|
||||
- [ ] SQLite local
|
||||
- [ ] Sync de productos
|
||||
- [ ] Registro offline de ventas
|
||||
- [ ] Cola de sincronizacion
|
||||
- [ ] Resolucion de conflictos
|
||||
|
||||
#### Entregables
|
||||
- App funcional sin internet
|
||||
- Sincronizacion automatica
|
||||
|
||||
---
|
||||
|
||||
### MCH-023: Programa Referidos
|
||||
**Duracion estimada:** 1 semana
|
||||
**Prioridad:** P2
|
||||
**Dependencias:** MCH-018
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Generacion de codigos de referido
|
||||
- [ ] Tracking de referidos
|
||||
- [ ] Aplicacion de beneficios
|
||||
|
||||
#### Entregables
|
||||
- Sistema de referidos funcional
|
||||
|
||||
---
|
||||
|
||||
### MCH-024: CoDi y SPEI
|
||||
**Duracion estimada:** 1.5 semanas
|
||||
**Prioridad:** P2
|
||||
**Dependencias:** MCH-005
|
||||
|
||||
#### Tareas Backend
|
||||
- [ ] Integracion con Openpay para CoDi
|
||||
- [ ] Generacion de QR
|
||||
- [ ] CLABE virtual (si disponible)
|
||||
|
||||
#### Entregables
|
||||
- Cobro con CoDi funcional
|
||||
|
||||
---
|
||||
|
||||
### MCH-025: Widgets y Atajos
|
||||
**Duracion estimada:** 1 semana
|
||||
**Prioridad:** P2
|
||||
**Dependencias:** MCH-004
|
||||
|
||||
#### Tareas Mobile
|
||||
- [ ] Android widget de venta rapida
|
||||
- [ ] iOS quick actions
|
||||
- [ ] Deep links
|
||||
|
||||
#### Entregables
|
||||
- Widgets funcionales
|
||||
|
||||
---
|
||||
|
||||
## APP MOBILE (Paralelo a Fases 3-5)
|
||||
|
||||
### Desarrollo React Native
|
||||
**Duracion estimada:** 8-10 semanas (paralelo)
|
||||
**Prioridad:** P0
|
||||
|
||||
#### Semanas 1-2: Setup
|
||||
- [ ] Inicializar proyecto Expo en apps/mobile
|
||||
- [ ] Configurar navegacion (React Navigation)
|
||||
- [ ] Configurar estado global (Zustand)
|
||||
- [ ] Configurar API client
|
||||
- [ ] Pantalla de login
|
||||
|
||||
#### Semanas 3-4: Core
|
||||
- [ ] Pantalla de POS
|
||||
- [ ] Componente de carrito
|
||||
- [ ] Checkout modal
|
||||
- [ ] Scanner de codigo de barras
|
||||
|
||||
#### Semanas 5-6: Features
|
||||
- [ ] Pantalla de productos
|
||||
- [ ] Pantalla de clientes
|
||||
- [ ] Pantalla de fiados
|
||||
- [ ] Pantalla de pedidos
|
||||
|
||||
#### Semanas 7-8: Integraciones
|
||||
- [ ] Push notifications (Expo Notifications)
|
||||
- [ ] Bluetooth para terminal (si aplica)
|
||||
- [ ] Deep linking
|
||||
|
||||
#### Semanas 9-10: Polish
|
||||
- [ ] UI/UX refinamiento
|
||||
- [ ] Performance optimization
|
||||
- [ ] Testing
|
||||
- [ ] Build para TestFlight/Play Console
|
||||
|
||||
---
|
||||
|
||||
## Cronograma Visual
|
||||
|
||||
```
|
||||
Semana 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
||||
| | | | | | | | | | | | | | | | | |
|
||||
MCH-001 ████
|
||||
MCH-002 ██
|
||||
MCH-003 ██
|
||||
MCH-004 ███
|
||||
MCH-005 ████
|
||||
|
||||
--- MVP FASE 1 COMPLETADO (Semana 7) ---
|
||||
|
||||
MCH-006 ███
|
||||
MCH-007 ██
|
||||
MCH-008 ███
|
||||
MCH-009 ██
|
||||
|
||||
--- FASE 2 COMPLETADA (Semana 11) ---
|
||||
|
||||
MCH-010 ████
|
||||
MCH-011 ████
|
||||
MCH-012 ███
|
||||
MCH-013 ██
|
||||
|
||||
--- FASE 3 COMPLETADA (Semana 15) ---
|
||||
|
||||
MCH-014 ██
|
||||
MCH-015 ███
|
||||
MCH-017 ██
|
||||
|
||||
--- FASE 4 BASICA COMPLETADA (Semana 18) ---
|
||||
|
||||
MOBILE |███████████████████████████████████████████████████|
|
||||
| Setup | Core | Features | Integ | Polish |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Metricas de Exito por Fase
|
||||
|
||||
### Fase 1 - MVP Core
|
||||
- [ ] Registro de ventas funcional
|
||||
- [ ] 3 metodos de pago (efectivo, Stripe, MP)
|
||||
- [ ] Corte de caja diario
|
||||
- [ ] UI web basica
|
||||
|
||||
### Fase 2 - Inteligencia
|
||||
- [ ] Onboarding < 10 minutos
|
||||
- [ ] 500+ productos en templates
|
||||
- [ ] Sistema de fiados completo
|
||||
- [ ] Alertas de stock automaticas
|
||||
|
||||
### Fase 3 - Asistente IA
|
||||
- [ ] Respuesta LLM < 3 segundos
|
||||
- [ ] 10+ tools MCP funcionales
|
||||
- [ ] Deteccion de rol 95%+ precision
|
||||
- [ ] Costo < $0.01 por interaccion
|
||||
|
||||
### Fase 4 - Pedidos
|
||||
- [ ] Flujo pedido->cobro < 2 minutos
|
||||
- [ ] Push notifications funcionando
|
||||
- [ ] 0 pedidos perdidos
|
||||
|
||||
### Fase 5 - Monetizacion
|
||||
- [ ] Suscripciones activas
|
||||
- [ ] Tokens comprables
|
||||
- [ ] Retencion > 80% mes 2
|
||||
|
||||
---
|
||||
|
||||
## Riesgos y Mitigaciones
|
||||
|
||||
| Riesgo | Probabilidad | Impacto | Mitigacion |
|
||||
|--------|--------------|---------|------------|
|
||||
| Bloqueo WhatsApp API | Media | Alto | Usar API oficial, seguir guidelines |
|
||||
| Costos LLM elevados | Media | Medio | OpenRouter, modelos economicos, cache |
|
||||
| Integracion terminal compleja | Media | Medio | Priorizar una terminal primero |
|
||||
| Adopcion usuario mayor | Alta | Alto | UX extremadamente simple, WhatsApp-first |
|
||||
| Competencia | Media | Medio | Diferenciacion por IA y simplicidad |
|
||||
|
||||
---
|
||||
|
||||
## Equipo Sugerido
|
||||
|
||||
| Rol | Cantidad | Enfoque |
|
||||
|-----|----------|---------|
|
||||
| Backend Developer | 2 | NestJS, integraciones, MCP |
|
||||
| Frontend Developer | 1 | React web + React Native |
|
||||
| DevOps | 0.5 | CI/CD, infraestructura |
|
||||
| Product/PM | 0.5 | Prioridades, testing |
|
||||
|
||||
**Total:** 4 FTEs
|
||||
|
||||
---
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2026-01-04
|
||||
109
backups/docs-backup-2026-01-10/docs/02-especificaciones/_MAP.md
Normal file
109
backups/docs-backup-2026-01-10/docs/02-especificaciones/_MAP.md
Normal file
@ -0,0 +1,109 @@
|
||||
# MiChangarrito - Mapa de Especificaciones Tecnicas
|
||||
|
||||
## Indice de Documentos
|
||||
|
||||
| Documento | Descripcion | Estado |
|
||||
|-----------|-------------|--------|
|
||||
| [INVESTIGACION-REFERENCIAS.md](./INVESTIGACION-REFERENCIAS.md) | Analisis de codigo reutilizable y proyectos de referencia | Completo |
|
||||
| [ARQUITECTURA-DATABASE.md](./ARQUITECTURA-DATABASE.md) | Schema completo de base de datos (9 schemas, 25+ tablas) | Completo |
|
||||
| [INTEGRACIONES-EXTERNAS.md](./INTEGRACIONES-EXTERNAS.md) | Documentacion de todas las integraciones (Stripe, WhatsApp, LLM, etc.) | Completo |
|
||||
| [PLAN-DESARROLLO.md](./PLAN-DESARROLLO.md) | Plan de desarrollo por epicas con cronograma | Completo |
|
||||
|
||||
---
|
||||
|
||||
## Resumen de Investigacion
|
||||
|
||||
### Codigo Reutilizable
|
||||
|
||||
| Fuente | Reutilizable | Ubicacion |
|
||||
|--------|--------------|-----------|
|
||||
| POS-Micro Backend | 80% | erp-suite/apps/products/pos-micro/backend |
|
||||
| POS-Micro Frontend | 60% | erp-suite/apps/products/pos-micro/frontend |
|
||||
| Shared-Libs Core | 100% | erp-suite/apps/shared-libs/core |
|
||||
| Trading-Platform LLM | 70% | trading-platform/apps/llm-agent |
|
||||
|
||||
### Integraciones Principales
|
||||
|
||||
| Integracion | Prioridad | SDK Disponible |
|
||||
|-------------|-----------|----------------|
|
||||
| Stripe | P0 | npm install stripe |
|
||||
| WhatsApp Cloud API | P0 | REST API |
|
||||
| OpenRouter/LLM | P0 | npm install openai |
|
||||
| MCP Server | P0 | npm install @modelcontextprotocol/server |
|
||||
| Mercado Pago | P1 | npm install mercadopago |
|
||||
| Firebase FCM | P1 | npm install firebase-admin |
|
||||
|
||||
---
|
||||
|
||||
## Arquitectura de Base de Datos
|
||||
|
||||
### Schemas
|
||||
|
||||
```
|
||||
public → Tenants, configuracion global
|
||||
auth → Usuarios, sesiones, OTP
|
||||
catalog → Productos, categorias, templates
|
||||
sales → Ventas, pagos, cortes
|
||||
inventory → Stock, movimientos, alertas
|
||||
customers → Clientes, fiados
|
||||
orders → Pedidos, entregas
|
||||
subscriptions → Planes, tokens IA
|
||||
messaging → WhatsApp, notificaciones
|
||||
```
|
||||
|
||||
### Tablas Principales
|
||||
|
||||
| Schema | Tablas |
|
||||
|--------|--------|
|
||||
| public | tenants, tenant_configs |
|
||||
| auth | users, sessions, otp_codes |
|
||||
| catalog | categories, products, product_templates |
|
||||
| sales | sales, sale_items, payments, daily_closures |
|
||||
| inventory | inventory_movements, stock_alerts |
|
||||
| customers | customers, fiados, fiado_payments |
|
||||
| orders | orders, order_items |
|
||||
| subscriptions | plans, subscriptions, token_packages, token_usage, tenant_token_balance |
|
||||
| messaging | conversations, messages, notifications |
|
||||
|
||||
---
|
||||
|
||||
## Plan de Desarrollo
|
||||
|
||||
### Fases
|
||||
|
||||
| Fase | Epicas | Duracion Estimada |
|
||||
|------|--------|-------------------|
|
||||
| FASE 1: MVP Core | MCH-001 a MCH-005 | 6-7 semanas |
|
||||
| FASE 2: Inteligencia | MCH-006 a MCH-009 | 4-5 semanas |
|
||||
| FASE 3: Asistente IA | MCH-010 a MCH-013 | 5-6 semanas |
|
||||
| FASE 4: Pedidos | MCH-014 a MCH-017 | 4-5 semanas |
|
||||
| FASE 5: Monetizacion | MCH-018 a MCH-021 | 5-6 semanas |
|
||||
| FASE 6: Crecimiento | MCH-022 a MCH-025 | 4-6 semanas |
|
||||
|
||||
### Prioridades Criticas (P0)
|
||||
|
||||
1. **MCH-001**: Infraestructura Base
|
||||
2. **MCH-002**: Autenticacion
|
||||
3. **MCH-003**: Catalogo de Productos
|
||||
4. **MCH-004**: Punto de Venta Basico
|
||||
5. **MCH-005**: Integraciones de Pago
|
||||
6. **MCH-010**: MCP Server
|
||||
7. **MCH-011**: WhatsApp Service
|
||||
8. **MCH-018**: Planes y Suscripciones
|
||||
9. **MCH-020**: Pagos Suscripcion
|
||||
|
||||
---
|
||||
|
||||
## Proximo Paso: Desarrollo
|
||||
|
||||
Con la documentacion completa, el siguiente paso es:
|
||||
|
||||
1. **Copiar POS-Micro** como base para backend
|
||||
2. **Ejecutar scripts de database** para crear schemas
|
||||
3. **Configurar Docker Compose** para desarrollo
|
||||
4. **Iniciar desarrollo de MCH-001**
|
||||
|
||||
---
|
||||
|
||||
**Version:** 1.0.0
|
||||
**Fecha:** 2026-01-04
|
||||
@ -0,0 +1,261 @@
|
||||
# Arquitectura Multi-Tenant de Integraciones
|
||||
|
||||
## Resumen
|
||||
|
||||
MiChangarrito implementa una arquitectura multi-tenant donde cada cliente (tenant) puede configurar sus propias credenciales para integraciones externas (WhatsApp Business, proveedores LLM, pasarelas de pago), con fallback automático a las credenciales de plataforma si el tenant no tiene las suyas.
|
||||
|
||||
## Diagrama de Flujo
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────────┐ ┌─────────────────┐
|
||||
│ WhatsApp │ │ whatsapp-service │ │ Backend │
|
||||
│ Webhook │────▶│ │────▶│ (Internal) │
|
||||
└─────────────────┘ │ - Resolver tenant │ │ │
|
||||
│ - Get credentials │ │ GET /internal/ │
|
||||
│ - Send via correct │ │ integrations/ │
|
||||
│ WhatsApp account │ └─────────────────┘
|
||||
└──────────────────────┘ │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────────┐ ┌─────────────────┐
|
||||
│ Meta WhatsApp API │ │ PostgreSQL │
|
||||
│ (Tenant o Platform)│ │ tenant_ │
|
||||
└──────────────────────┘ │ integration_ │
|
||||
│ credentials │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## Componentes Principales
|
||||
|
||||
### 1. Backend - Módulo de Integraciones
|
||||
|
||||
**Ubicación:** `apps/backend/src/modules/integrations/`
|
||||
|
||||
#### Entidades
|
||||
|
||||
- **TenantIntegrationCredential**: Almacena credenciales por tenant e integración
|
||||
- **TenantWhatsAppNumber**: Mapea phoneNumberId → tenantId para webhooks
|
||||
|
||||
#### Servicios
|
||||
|
||||
- **TenantIntegrationsService**:
|
||||
- `getWhatsAppCredentials(tenantId)` - Con fallback a plataforma
|
||||
- `getLLMConfig(tenantId)` - Con fallback a plataforma
|
||||
- `resolveTenantFromPhoneNumberId(phoneNumberId)` - Para webhooks
|
||||
|
||||
#### Controladores
|
||||
|
||||
- **IntegrationsController**: API REST para tenants (protegido con JWT)
|
||||
- `GET /integrations/status` - Estado de todas las integraciones
|
||||
- `PUT /integrations/whatsapp` - Configurar WhatsApp propio
|
||||
- `PUT /integrations/llm` - Configurar LLM propio
|
||||
|
||||
- **InternalIntegrationsController**: API interna para whatsapp-service
|
||||
- `GET /internal/integrations/:tenantId/whatsapp`
|
||||
- `GET /internal/integrations/:tenantId/llm`
|
||||
- `GET /internal/integrations/resolve-tenant/:phoneNumberId`
|
||||
|
||||
### 2. WhatsApp Service
|
||||
|
||||
**Ubicación:** `apps/whatsapp-service/src/`
|
||||
|
||||
#### Servicios Refactorizados
|
||||
|
||||
- **CredentialsProviderService** (`common/`):
|
||||
- Cache de credenciales con TTL de 5 minutos
|
||||
- Consulta al backend y cachea resultados
|
||||
- Fallback a variables de entorno
|
||||
|
||||
- **WhatsAppService** (`whatsapp/`):
|
||||
- Todos los métodos aceptan `tenantId?: string`
|
||||
- Cache de clientes axios por tenant
|
||||
- Usa credenciales correctas automáticamente
|
||||
|
||||
- **LlmService** (`llm/`):
|
||||
- Obtiene config LLM por tenant
|
||||
- Soporta múltiples proveedores (OpenAI, OpenRouter, etc.)
|
||||
- System prompts personalizados por tenant
|
||||
|
||||
- **WebhookService** (`webhook/`):
|
||||
- Resuelve tenant desde `metadata.phone_number_id`
|
||||
- Pasa `tenantId` en todo el flujo de conversación
|
||||
|
||||
## Flujo de un Mensaje Entrante
|
||||
|
||||
1. **Meta envía webhook** con `metadata.phone_number_id`
|
||||
2. **WebhookController** extrae phoneNumberId del payload
|
||||
3. **WebhookService.processIncomingMessage()** recibe phoneNumberId
|
||||
4. **CredentialsProviderService.resolveTenantFromPhoneNumberId()**
|
||||
- Consulta backend (`/internal/integrations/resolve-tenant/:id`)
|
||||
- Retorna `tenantId` o `null` (plataforma)
|
||||
5. **Contexto de conversación** incluye tenantId
|
||||
6. **WhatsAppService.sendTextMessage(to, text, tenantId)**
|
||||
- Obtiene credenciales para ese tenant (o plataforma)
|
||||
- Envía mensaje con las credenciales correctas
|
||||
7. **LlmService.processMessage(text, context)**
|
||||
- Obtiene config LLM para el tenant
|
||||
- Usa API key y modelo del tenant (o plataforma)
|
||||
|
||||
## Configuración de Variables de Entorno
|
||||
|
||||
### Backend (.env)
|
||||
|
||||
```bash
|
||||
# Credenciales de plataforma (fallback)
|
||||
WHATSAPP_ACCESS_TOKEN=EAAxxxxxxx
|
||||
WHATSAPP_PHONE_NUMBER_ID=123456789
|
||||
WHATSAPP_BUSINESS_ACCOUNT_ID=987654321
|
||||
WHATSAPP_VERIFY_TOKEN=mi_token_secreto
|
||||
|
||||
# LLM de plataforma
|
||||
OPENAI_API_KEY=sk-xxxxxxx
|
||||
LLM_PROVIDER=openai
|
||||
LLM_MODEL=gpt-4o-mini
|
||||
|
||||
# API interna
|
||||
INTERNAL_API_KEY=clave_secreta_para_servicios_internos
|
||||
```
|
||||
|
||||
### WhatsApp Service (.env)
|
||||
|
||||
```bash
|
||||
# URL del backend
|
||||
BACKEND_URL=http://localhost:3141/api/v1
|
||||
|
||||
# API key para llamadas internas
|
||||
INTERNAL_API_KEY=clave_secreta_para_servicios_internos
|
||||
|
||||
# Credenciales de plataforma (fallback)
|
||||
WHATSAPP_ACCESS_TOKEN=EAAxxxxxxx
|
||||
WHATSAPP_PHONE_NUMBER_ID=123456789
|
||||
OPENAI_API_KEY=sk-xxxxxxx
|
||||
```
|
||||
|
||||
## API REST - Configuración de Integraciones
|
||||
|
||||
### Obtener Estado de Integraciones
|
||||
|
||||
```http
|
||||
GET /api/v1/integrations/status
|
||||
Authorization: Bearer <jwt_token>
|
||||
```
|
||||
|
||||
**Respuesta:**
|
||||
```json
|
||||
{
|
||||
"whatsapp": {
|
||||
"configured": false,
|
||||
"usesPlatformNumber": true,
|
||||
"isVerified": false
|
||||
},
|
||||
"llm": {
|
||||
"configured": false,
|
||||
"usesPlatformDefault": true,
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini"
|
||||
},
|
||||
"payments": {
|
||||
"stripe": { "configured": false },
|
||||
"mercadopago": { "configured": false },
|
||||
"clip": { "configured": false }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configurar WhatsApp Propio
|
||||
|
||||
```http
|
||||
PUT /api/v1/integrations/whatsapp
|
||||
Authorization: Bearer <jwt_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"credentials": {
|
||||
"accessToken": "EAAxxxxx",
|
||||
"phoneNumberId": "111222333",
|
||||
"businessAccountId": "444555666"
|
||||
},
|
||||
"phoneNumber": "+525512345678",
|
||||
"displayName": "Mi Tiendita"
|
||||
}
|
||||
```
|
||||
|
||||
### Configurar LLM Propio
|
||||
|
||||
```http
|
||||
PUT /api/v1/integrations/llm
|
||||
Authorization: Bearer <jwt_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"provider": "openrouter",
|
||||
"credentials": {
|
||||
"apiKey": "sk-or-xxxxx"
|
||||
},
|
||||
"config": {
|
||||
"model": "anthropic/claude-3-haiku",
|
||||
"maxTokens": 1500,
|
||||
"temperature": 0.8,
|
||||
"systemPrompt": "Eres el asistente de Mi Tiendita..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Proveedores Soportados
|
||||
|
||||
### WhatsApp
|
||||
- **Meta Business** (único proveedor)
|
||||
|
||||
### LLM
|
||||
- **OpenAI** - gpt-4o, gpt-4o-mini, etc.
|
||||
- **OpenRouter** - Acceso a múltiples modelos
|
||||
- **Anthropic** - Claude 3
|
||||
- **Azure OpenAI** - Despliegues enterprise
|
||||
- **Ollama** - Modelos locales
|
||||
|
||||
### Pagos (Futuro)
|
||||
- Stripe
|
||||
- MercadoPago
|
||||
- Clip
|
||||
|
||||
## Esquema de Base de Datos
|
||||
|
||||
```sql
|
||||
-- Tabla de credenciales de integración por tenant
|
||||
CREATE TABLE tenant_integration_credentials (
|
||||
id UUID PRIMARY KEY,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id),
|
||||
integration_type VARCHAR(50) NOT NULL, -- whatsapp, llm, stripe, etc.
|
||||
provider VARCHAR(50) NOT NULL, -- meta, openai, openrouter, etc.
|
||||
credentials JSONB NOT NULL DEFAULT '{}', -- Datos sensibles encriptados
|
||||
config JSONB DEFAULT '{}', -- Configuración no sensible
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
is_verified BOOLEAN DEFAULT false,
|
||||
UNIQUE(tenant_id, integration_type, provider)
|
||||
);
|
||||
|
||||
-- Mapeo de números WhatsApp a tenants
|
||||
CREATE TABLE tenant_whatsapp_numbers (
|
||||
id UUID PRIMARY KEY,
|
||||
tenant_id UUID NOT NULL,
|
||||
phone_number_id VARCHAR(50) UNIQUE NOT NULL,
|
||||
phone_number VARCHAR(20),
|
||||
display_name VARCHAR(100),
|
||||
is_active BOOLEAN DEFAULT true
|
||||
);
|
||||
```
|
||||
|
||||
## Seguridad
|
||||
|
||||
1. **Credenciales encriptadas** en JSONB (recomendación: usar pg_crypto)
|
||||
2. **API Interna protegida** con X-Internal-Key header
|
||||
3. **JWT obligatorio** para endpoints de configuración
|
||||
4. **No se exponen API keys** en respuestas al frontend
|
||||
5. **Cache de credenciales** para reducir queries a BD
|
||||
|
||||
## Consideraciones de Escalabilidad
|
||||
|
||||
- Cache de credenciales con TTL de 5 minutos
|
||||
- Cache de clientes axios por tenant
|
||||
- Invalidación de cache al actualizar credenciales
|
||||
- Conexión separada de BD para servicio de WhatsApp (futuro)
|
||||
@ -0,0 +1,454 @@
|
||||
# Guía de Despliegue - MiChangarrito
|
||||
|
||||
**Versión**: 1.0.0
|
||||
**Última actualización**: 2026-01-07
|
||||
**Autor**: @PERFIL_DEVOPS
|
||||
|
||||
---
|
||||
|
||||
## Tabla de Contenidos
|
||||
|
||||
1. [Requisitos](#requisitos)
|
||||
2. [Despliegue Local (Docker)](#despliegue-local-docker)
|
||||
3. [Despliegue en Producción](#despliegue-en-producción)
|
||||
4. [CI/CD con GitHub Actions](#cicd-con-github-actions)
|
||||
5. [Configuración de SSL](#configuración-de-ssl)
|
||||
6. [Backups](#backups)
|
||||
7. [Monitoreo](#monitoreo)
|
||||
8. [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Requisitos
|
||||
|
||||
### Requisitos de Sistema
|
||||
|
||||
| Componente | Mínimo | Recomendado |
|
||||
|------------|--------|-------------|
|
||||
| CPU | 2 cores | 4 cores |
|
||||
| RAM | 4 GB | 8 GB |
|
||||
| Disco | 20 GB | 50 GB SSD |
|
||||
| SO | Ubuntu 22.04 LTS | Ubuntu 22.04 LTS |
|
||||
|
||||
### Software Requerido
|
||||
|
||||
```bash
|
||||
# Docker y Docker Compose
|
||||
docker --version # >= 24.0
|
||||
docker-compose --version # >= 2.20
|
||||
|
||||
# Node.js (solo para desarrollo)
|
||||
node --version # >= 20.0
|
||||
npm --version # >= 10.0
|
||||
```
|
||||
|
||||
### Instalación de Docker (Ubuntu)
|
||||
|
||||
```bash
|
||||
# Actualizar paquetes
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# Instalar Docker
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh get-docker.sh
|
||||
|
||||
# Agregar usuario al grupo docker
|
||||
sudo usermod -aG docker $USER
|
||||
|
||||
# Instalar Docker Compose
|
||||
sudo apt install docker-compose-plugin -y
|
||||
|
||||
# Verificar instalación
|
||||
docker --version
|
||||
docker compose version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Despliegue Local (Docker)
|
||||
|
||||
### 1. Clonar Repositorio
|
||||
|
||||
```bash
|
||||
git clone https://github.com/isem/michangarrito.git
|
||||
cd michangarrito
|
||||
```
|
||||
|
||||
### 2. Configurar Variables de Entorno
|
||||
|
||||
```bash
|
||||
# Copiar template
|
||||
cp .env.docker .env
|
||||
|
||||
# Editar con tus credenciales
|
||||
nano .env
|
||||
```
|
||||
|
||||
**Variables importantes a configurar:**
|
||||
|
||||
```env
|
||||
# Seguridad
|
||||
JWT_SECRET=<generar-string-aleatorio-64-chars>
|
||||
|
||||
# Stripe (opcional para desarrollo)
|
||||
STRIPE_SECRET_KEY=sk_test_xxx
|
||||
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
||||
|
||||
# WhatsApp (opcional para desarrollo)
|
||||
WHATSAPP_TOKEN=xxx
|
||||
WHATSAPP_VERIFY_TOKEN=xxx
|
||||
```
|
||||
|
||||
### 3. Iniciar Servicios
|
||||
|
||||
```bash
|
||||
# Usando el script de deploy
|
||||
./deploy/scripts/deploy.sh
|
||||
|
||||
# O directamente con docker-compose
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 4. Verificar Estado
|
||||
|
||||
```bash
|
||||
# Ver estado de contenedores
|
||||
docker-compose ps
|
||||
|
||||
# Ver logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Health checks
|
||||
curl http://localhost:3141/api/v1/health
|
||||
curl http://localhost:3140
|
||||
```
|
||||
|
||||
### URLs de Desarrollo
|
||||
|
||||
| Servicio | URL |
|
||||
|----------|-----|
|
||||
| Frontend | http://localhost:3140 |
|
||||
| Backend API | http://localhost:3141/api/v1 |
|
||||
| API Docs (Swagger) | http://localhost:3141/api/v1/docs |
|
||||
| WhatsApp Webhook | http://localhost:3143 |
|
||||
|
||||
---
|
||||
|
||||
## Despliegue en Producción
|
||||
|
||||
### 1. Preparar Servidor
|
||||
|
||||
```bash
|
||||
# Conectar al servidor
|
||||
ssh user@servidor.com
|
||||
|
||||
# Crear directorio
|
||||
sudo mkdir -p /opt/michangarrito
|
||||
sudo chown $USER:$USER /opt/michangarrito
|
||||
cd /opt/michangarrito
|
||||
|
||||
# Clonar repositorio
|
||||
git clone https://github.com/isem/michangarrito.git .
|
||||
```
|
||||
|
||||
### 2. Configurar Environment de Producción
|
||||
|
||||
```bash
|
||||
cp .env.docker .env
|
||||
nano .env
|
||||
```
|
||||
|
||||
**Configuración de producción:**
|
||||
|
||||
```env
|
||||
# Database con password fuerte
|
||||
DB_PASSWORD=<password-seguro-produccion>
|
||||
|
||||
# JWT con secret largo
|
||||
JWT_SECRET=<string-aleatorio-64-chars-produccion>
|
||||
|
||||
# CORS para tu dominio
|
||||
CORS_ORIGIN=https://michangarrito.com
|
||||
|
||||
# API URL de producción
|
||||
VITE_API_URL=https://api.michangarrito.com/api/v1
|
||||
|
||||
# Activar perfil de producción (incluye nginx)
|
||||
COMPOSE_PROFILES=production
|
||||
|
||||
# Stripe en modo live
|
||||
STRIPE_SECRET_KEY=sk_live_xxx
|
||||
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
||||
|
||||
# WhatsApp
|
||||
WHATSAPP_TOKEN=xxx
|
||||
WHATSAPP_PHONE_NUMBER_ID=xxx
|
||||
WHATSAPP_BUSINESS_ACCOUNT_ID=xxx
|
||||
```
|
||||
|
||||
### 3. Configurar SSL con Let's Encrypt
|
||||
|
||||
```bash
|
||||
# Instalar certbot
|
||||
sudo apt install certbot -y
|
||||
|
||||
# Crear directorio para certificados
|
||||
mkdir -p deploy/ssl
|
||||
|
||||
# Obtener certificado
|
||||
sudo certbot certonly --standalone -d michangarrito.com -d api.michangarrito.com
|
||||
|
||||
# Copiar certificados
|
||||
sudo cp /etc/letsencrypt/live/michangarrito.com/fullchain.pem deploy/ssl/
|
||||
sudo cp /etc/letsencrypt/live/michangarrito.com/privkey.pem deploy/ssl/
|
||||
sudo chown $USER:$USER deploy/ssl/*.pem
|
||||
```
|
||||
|
||||
### 4. Habilitar HTTPS en Nginx
|
||||
|
||||
Editar `deploy/nginx/conf.d/default.conf` y descomentar la sección de HTTPS.
|
||||
|
||||
### 5. Iniciar en Producción
|
||||
|
||||
```bash
|
||||
./deploy/scripts/deploy.sh --production --build
|
||||
```
|
||||
|
||||
### 6. Configurar Renovación Automática de SSL
|
||||
|
||||
```bash
|
||||
# Agregar cron job
|
||||
sudo crontab -e
|
||||
|
||||
# Agregar línea:
|
||||
0 3 * * * certbot renew --quiet && docker-compose -f /opt/michangarrito/docker-compose.yml restart nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI/CD con GitHub Actions
|
||||
|
||||
### Configuración
|
||||
|
||||
El pipeline está en `.github/workflows/ci.yml` y ejecuta:
|
||||
|
||||
1. **CI**: Lint, test y build de todas las apps
|
||||
2. **Docker Build**: Construye y publica imágenes a GHCR
|
||||
3. **Deploy**: Despliega automáticamente al servidor
|
||||
|
||||
### Secrets Requeridos en GitHub
|
||||
|
||||
| Secret | Descripción |
|
||||
|--------|-------------|
|
||||
| `SERVER_HOST` | IP o dominio del servidor |
|
||||
| `SERVER_USER` | Usuario SSH |
|
||||
| `SERVER_SSH_KEY` | Llave privada SSH |
|
||||
|
||||
### Variables de Entorno en GitHub
|
||||
|
||||
| Variable | Descripción |
|
||||
|----------|-------------|
|
||||
| `VITE_API_URL` | URL de la API para el build |
|
||||
|
||||
### Configurar Secrets
|
||||
|
||||
```bash
|
||||
# En GitHub: Settings > Secrets and variables > Actions
|
||||
|
||||
# SERVER_HOST
|
||||
tu-servidor.com
|
||||
|
||||
# SERVER_USER
|
||||
deploy
|
||||
|
||||
# SERVER_SSH_KEY
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
...
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backups
|
||||
|
||||
### Backup Manual
|
||||
|
||||
```bash
|
||||
./deploy/scripts/backup.sh
|
||||
```
|
||||
|
||||
Los backups se guardan en `./backups/` con formato: `michangarrito_dev_YYYYMMDD_HHMMSS.sql.gz`
|
||||
|
||||
### Restaurar Backup
|
||||
|
||||
```bash
|
||||
./deploy/scripts/backup.sh --restore backups/michangarrito_dev_20260107_120000.sql.gz
|
||||
```
|
||||
|
||||
### Backup Automático (Cron)
|
||||
|
||||
```bash
|
||||
# Agregar a crontab
|
||||
0 2 * * * /opt/michangarrito/deploy/scripts/backup.sh >> /var/log/michangarrito-backup.log 2>&1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoreo
|
||||
|
||||
### Ver Logs
|
||||
|
||||
```bash
|
||||
# Todos los servicios
|
||||
docker-compose logs -f
|
||||
|
||||
# Servicio específico
|
||||
docker-compose logs -f backend
|
||||
docker-compose logs -f postgres
|
||||
|
||||
# Últimas 100 líneas
|
||||
docker-compose logs --tail=100 backend
|
||||
```
|
||||
|
||||
### Métricas de Contenedores
|
||||
|
||||
```bash
|
||||
# Uso de recursos
|
||||
docker stats
|
||||
|
||||
# Estado de salud
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### Health Endpoints
|
||||
|
||||
| Servicio | Endpoint |
|
||||
|----------|----------|
|
||||
| Backend | `GET /api/v1/health` |
|
||||
| Frontend | `GET /health` |
|
||||
| WhatsApp | `GET /health` |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: Puerto en uso
|
||||
|
||||
```bash
|
||||
# Identificar proceso
|
||||
sudo lsof -i :3141
|
||||
|
||||
# Matar proceso o cambiar puerto en .env
|
||||
```
|
||||
|
||||
### Error: Container no inicia
|
||||
|
||||
```bash
|
||||
# Ver logs detallados
|
||||
docker-compose logs backend
|
||||
|
||||
# Reiniciar servicio
|
||||
docker-compose restart backend
|
||||
|
||||
# Reconstruir
|
||||
docker-compose up -d --build backend
|
||||
```
|
||||
|
||||
### Error: Base de datos no conecta
|
||||
|
||||
```bash
|
||||
# Verificar que postgres esté corriendo
|
||||
docker-compose ps postgres
|
||||
|
||||
# Ver logs de postgres
|
||||
docker-compose logs postgres
|
||||
|
||||
# Probar conexión
|
||||
docker-compose exec postgres psql -U michangarrito_dev -d michangarrito_dev
|
||||
```
|
||||
|
||||
### Error: Frontend no carga
|
||||
|
||||
```bash
|
||||
# Verificar build
|
||||
docker-compose logs frontend
|
||||
|
||||
# Verificar nginx
|
||||
docker-compose exec frontend cat /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Reiniciar
|
||||
docker-compose restart frontend
|
||||
```
|
||||
|
||||
### Limpiar Todo y Reiniciar
|
||||
|
||||
```bash
|
||||
# Detener todo
|
||||
docker-compose down -v
|
||||
|
||||
# Limpiar imágenes
|
||||
docker system prune -af
|
||||
|
||||
# Reconstruir desde cero
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Comandos Útiles
|
||||
|
||||
```bash
|
||||
# Iniciar servicios
|
||||
docker-compose up -d
|
||||
|
||||
# Detener servicios
|
||||
docker-compose down
|
||||
|
||||
# Reconstruir un servicio
|
||||
docker-compose up -d --build backend
|
||||
|
||||
# Entrar a un contenedor
|
||||
docker-compose exec backend sh
|
||||
|
||||
# Ejecutar comando en contenedor
|
||||
docker-compose exec postgres psql -U michangarrito_dev
|
||||
|
||||
# Ver uso de disco
|
||||
docker system df
|
||||
|
||||
# Limpiar recursos no usados
|
||||
docker system prune -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Arquitectura de Despliegue
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ INTERNET │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ NGINX │
|
||||
│ :80/:443 │
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌────────────────────┼────────────────────┐
|
||||
│ │ │
|
||||
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
|
||||
│ FRONTEND │ │ BACKEND │ │ WHATSAPP │
|
||||
│ :80 │ │ :3141 │ │ :3143 │
|
||||
└─────────────┘ └──────┬──────┘ └─────────────┘
|
||||
│
|
||||
┌────────────────┼────────────────┐
|
||||
│ │ │
|
||||
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
|
||||
│ POSTGRES │ │ REDIS │ │ (otros) │
|
||||
│ :5432 │ │ :6379 │ │ │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Documento generado**: 2026-01-07
|
||||
278
backups/docs-backup-2026-01-10/docs/_MAP.md
Normal file
278
backups/docs-backup-2026-01-10/docs/_MAP.md
Normal file
@ -0,0 +1,278 @@
|
||||
# MiChangarrito - Mapa de Documentacion
|
||||
|
||||
**Proyecto:** michangarrito
|
||||
**Codigo:** MCH
|
||||
**Version:** 2.0.0
|
||||
**Fecha:** 2026-01-10
|
||||
**Estado:** MVP 95% Implementado
|
||||
**Sistema:** SIMCO - NEXUS v4.0
|
||||
|
||||
---
|
||||
|
||||
## Resumen del Proyecto
|
||||
|
||||
| Metrica | Valor |
|
||||
|---------|-------|
|
||||
| Progreso MVP | 95% |
|
||||
| Fases Completadas | 5.1 de 7 |
|
||||
| Tareas Completadas | 37 de 39 |
|
||||
| Total Epicas | 28 |
|
||||
| Epicas Completadas | 22 |
|
||||
| Epicas Pendientes | 6 |
|
||||
|
||||
---
|
||||
|
||||
## Estructura de Documentacion
|
||||
|
||||
```
|
||||
docs/
|
||||
├── _MAP.md <- ESTE ARCHIVO
|
||||
├── 00-vision-general/
|
||||
│ ├── VISION-PROYECTO.md <- Vision y propuesta de valor
|
||||
│ ├── REQUERIMIENTOS-FUNCIONALES.md <- Requisitos del sistema
|
||||
│ └── ARQUITECTURA-TECNICA.md <- Stack y arquitectura
|
||||
│
|
||||
├── 01-epicas/
|
||||
│ ├── _MAP.md <- Indice de epicas
|
||||
│ │
|
||||
│ │ # FASE 1 - Infraestructura Base (100%)
|
||||
│ ├── MCH-001-infraestructura-base.md
|
||||
│ ├── MCH-002-autenticacion.md
|
||||
│ ├── MCH-003-catalogo-productos.md
|
||||
│ ├── MCH-004-punto-venta.md
|
||||
│ ├── MCH-005-integraciones-pago.md
|
||||
│ │
|
||||
│ │ # FASE 2 - Inteligencia (100%)
|
||||
│ ├── MCH-006-onboarding-inteligente.md
|
||||
│ ├── MCH-007-templates-catalogos.md
|
||||
│ ├── MCH-008-sistema-fiados.md
|
||||
│ ├── MCH-009-prediccion-inventario.md
|
||||
│ │
|
||||
│ │ # FASE 3 - Asistente IA (100%)
|
||||
│ ├── MCH-010-mcp-server.md
|
||||
│ ├── MCH-011-whatsapp-service.md
|
||||
│ ├── MCH-012-chat-llm-dueno.md
|
||||
│ ├── MCH-013-chat-llm-cliente.md
|
||||
│ │
|
||||
│ │ # FASE 4 - Pedidos y Clientes (100%)
|
||||
│ ├── MCH-014-gestion-clientes.md
|
||||
│ ├── MCH-015-pedidos-whatsapp.md
|
||||
│ ├── MCH-016-entregas-domicilio.md
|
||||
│ ├── MCH-017-notificaciones.md
|
||||
│ │
|
||||
│ │ # FASE 5 - Monetizacion (100%)
|
||||
│ ├── MCH-018-planes-suscripciones.md
|
||||
│ ├── MCH-019-tienda-tokens.md
|
||||
│ ├── MCH-020-pagos-suscripcion.md
|
||||
│ ├── MCH-021-dashboard-web.md
|
||||
│ │
|
||||
│ │ # FASE 6 - Mobile y Offline (parcial)
|
||||
│ ├── MCH-022-modo-offline.md <- Completado
|
||||
│ ├── MCH-023-programa-referidos.md <- Pendiente
|
||||
│ ├── MCH-024-codi-spei.md <- Pendiente
|
||||
│ ├── MCH-025-widgets-atajos.md <- Pendiente
|
||||
│ │
|
||||
│ │ # FASE 7 - Expansion (pendiente)
|
||||
│ ├── MCH-026-multi-idioma-latam.md
|
||||
│ ├── MCH-027-integracion-sat.md
|
||||
│ └── MCH-028-marketplace-proveedores.md
|
||||
│
|
||||
├── 02-especificaciones/
|
||||
│ ├── CATALOGO-PRODUCTOS.md <- Gestion de catalogo
|
||||
│ ├── TEMPLATE-PRODUCTOS.md <- Templates de productos
|
||||
│ ├── POS-BASICO.md <- Punto de venta
|
||||
│ ├── VENTAS-DIARIAS.md <- Registro de ventas
|
||||
│ ├── CALCULADORA-CAMBIO.md <- Logica de cambio
|
||||
│ └── INTEGRACIONES-PAGOS.md <- Mercado Pago, Clip, CoDi
|
||||
│
|
||||
└── 90-transversal/
|
||||
├── arquitectura/
|
||||
│ └── ARCHITECTURE.md <- Arquitectura del sistema
|
||||
├── api/
|
||||
└── deployment/
|
||||
└── GUIA-DESPLIEGUE.md <- Guia de despliegue
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Progreso por Fase
|
||||
|
||||
### FASE 1 - Infraestructura Base (100%)
|
||||
| Epica | Nombre | Archivo | Estado |
|
||||
|-------|--------|---------|--------|
|
||||
| MCH-001 | Infraestructura Base | [MCH-001-infraestructura-base.md](01-epicas/MCH-001-infraestructura-base.md) | Completado |
|
||||
| MCH-002 | Autenticacion | [MCH-002-autenticacion.md](01-epicas/MCH-002-autenticacion.md) | Completado |
|
||||
| MCH-003 | Catalogo Productos | [MCH-003-catalogo-productos.md](01-epicas/MCH-003-catalogo-productos.md) | Completado |
|
||||
| MCH-004 | Punto de Venta | [MCH-004-punto-venta.md](01-epicas/MCH-004-punto-venta.md) | Completado |
|
||||
| MCH-005 | Integraciones Pago | [MCH-005-integraciones-pago.md](01-epicas/MCH-005-integraciones-pago.md) | Completado |
|
||||
|
||||
### FASE 2 - Inteligencia (100%)
|
||||
| Epica | Nombre | Archivo | Estado |
|
||||
|-------|--------|---------|--------|
|
||||
| MCH-006 | Onboarding Inteligente | [MCH-006-onboarding-inteligente.md](01-epicas/MCH-006-onboarding-inteligente.md) | Completado |
|
||||
| MCH-007 | Templates Catalogos | [MCH-007-templates-catalogos.md](01-epicas/MCH-007-templates-catalogos.md) | Completado |
|
||||
| MCH-008 | Sistema Fiados | [MCH-008-sistema-fiados.md](01-epicas/MCH-008-sistema-fiados.md) | Completado |
|
||||
| MCH-009 | Prediccion Inventario | [MCH-009-prediccion-inventario.md](01-epicas/MCH-009-prediccion-inventario.md) | Completado |
|
||||
|
||||
### FASE 3 - Asistente IA (100%)
|
||||
| Epica | Nombre | Archivo | Estado |
|
||||
|-------|--------|---------|--------|
|
||||
| MCH-010 | MCP Server | [MCH-010-mcp-server.md](01-epicas/MCH-010-mcp-server.md) | Completado |
|
||||
| MCH-011 | WhatsApp Service | [MCH-011-whatsapp-service.md](01-epicas/MCH-011-whatsapp-service.md) | Completado |
|
||||
| MCH-012 | Chat LLM Dueno | [MCH-012-chat-llm-dueno.md](01-epicas/MCH-012-chat-llm-dueno.md) | Completado |
|
||||
| MCH-013 | Chat LLM Cliente | [MCH-013-chat-llm-cliente.md](01-epicas/MCH-013-chat-llm-cliente.md) | Completado |
|
||||
|
||||
### FASE 4 - Pedidos y Clientes (100%)
|
||||
| Epica | Nombre | Archivo | Estado |
|
||||
|-------|--------|---------|--------|
|
||||
| MCH-014 | Gestion Clientes | [MCH-014-gestion-clientes.md](01-epicas/MCH-014-gestion-clientes.md) | Completado |
|
||||
| MCH-015 | Pedidos WhatsApp | [MCH-015-pedidos-whatsapp.md](01-epicas/MCH-015-pedidos-whatsapp.md) | Completado |
|
||||
| MCH-016 | Entregas Domicilio | [MCH-016-entregas-domicilio.md](01-epicas/MCH-016-entregas-domicilio.md) | Completado |
|
||||
| MCH-017 | Notificaciones | [MCH-017-notificaciones.md](01-epicas/MCH-017-notificaciones.md) | Completado |
|
||||
|
||||
### FASE 5 - Monetizacion (100%)
|
||||
| Epica | Nombre | Archivo | Estado |
|
||||
|-------|--------|---------|--------|
|
||||
| MCH-018 | Planes Suscripciones | [MCH-018-planes-suscripciones.md](01-epicas/MCH-018-planes-suscripciones.md) | Completado |
|
||||
| MCH-019 | Tienda Tokens | [MCH-019-tienda-tokens.md](01-epicas/MCH-019-tienda-tokens.md) | Completado |
|
||||
| MCH-020 | Pagos Suscripcion | [MCH-020-pagos-suscripcion.md](01-epicas/MCH-020-pagos-suscripcion.md) | Completado |
|
||||
| MCH-021 | Dashboard Web | [MCH-021-dashboard-web.md](01-epicas/MCH-021-dashboard-web.md) | Completado |
|
||||
|
||||
### FASE 6 - Mobile y Offline (25%)
|
||||
| Epica | Nombre | Archivo | Estado |
|
||||
|-------|--------|---------|--------|
|
||||
| MCH-022 | Modo Offline | [MCH-022-modo-offline.md](01-epicas/MCH-022-modo-offline.md) | Completado |
|
||||
| MCH-023 | Programa Referidos | [MCH-023-programa-referidos.md](01-epicas/MCH-023-programa-referidos.md) | Pendiente |
|
||||
| MCH-024 | CoDi/SPEI | [MCH-024-codi-spei.md](01-epicas/MCH-024-codi-spei.md) | Pendiente |
|
||||
| MCH-025 | Widgets Atajos | [MCH-025-widgets-atajos.md](01-epicas/MCH-025-widgets-atajos.md) | Pendiente |
|
||||
|
||||
### FASE 7 - Expansion LATAM (0%)
|
||||
| Epica | Nombre | Archivo | Estado |
|
||||
|-------|--------|---------|--------|
|
||||
| MCH-026 | Multi-idioma LATAM | [MCH-026-multi-idioma-latam.md](01-epicas/MCH-026-multi-idioma-latam.md) | Pendiente |
|
||||
| MCH-027 | Integracion SAT | [MCH-027-integracion-sat.md](01-epicas/MCH-027-integracion-sat.md) | Pendiente |
|
||||
| MCH-028 | Marketplace Proveedores | [MCH-028-marketplace-proveedores.md](01-epicas/MCH-028-marketplace-proveedores.md) | Pendiente |
|
||||
|
||||
---
|
||||
|
||||
## Archivos de Inventario
|
||||
|
||||
| Archivo | Proposito | Ubicacion |
|
||||
|---------|-----------|-----------|
|
||||
| MASTER_INVENTORY.yml | Inventario consolidado | orchestration/inventarios/ |
|
||||
| DATABASE_INVENTORY.yml | Esquemas y tablas | orchestration/inventarios/ |
|
||||
| BACKEND_INVENTORY.yml | Modulos y endpoints | orchestration/inventarios/ |
|
||||
| FRONTEND_INVENTORY.yml | Paginas y componentes | orchestration/inventarios/ |
|
||||
|
||||
---
|
||||
|
||||
## Navegacion Rapida
|
||||
|
||||
### Por Componente
|
||||
| Componente | Tecnologia | Puerto | Documentacion |
|
||||
|------------|------------|--------|---------------|
|
||||
| Backend | NestJS 10.3.0 | 3141 | orchestration/inventarios/BACKEND_INVENTORY.yml |
|
||||
| Frontend | React 19.2.0 + Vite 7.2.4 | 3140 | orchestration/inventarios/FRONTEND_INVENTORY.yml |
|
||||
| Mobile | React Native + Expo | 8081 | apps/mobile/README.md |
|
||||
| MCP Server | TypeScript + MCP SDK | 3142 | apps/mcp-server/README.md |
|
||||
| WhatsApp Service | NestJS + Meta Cloud API | 3143 | apps/whatsapp-service/README.md |
|
||||
| Database | PostgreSQL 16+ | 5432 | orchestration/inventarios/DATABASE_INVENTORY.yml |
|
||||
|
||||
### Por Estado
|
||||
| Estado | Fases | Epicas |
|
||||
|--------|-------|--------|
|
||||
| Completado | 1, 2, 3, 4, 5 | MCH-001 a MCH-022 |
|
||||
| Pendiente | 6 (parcial), 7 | MCH-023 a MCH-028 |
|
||||
|
||||
---
|
||||
|
||||
## Estadisticas del Proyecto
|
||||
|
||||
### Base de Datos
|
||||
| Metrica | Valor |
|
||||
|---------|-------|
|
||||
| Schemas | 9 |
|
||||
| Tablas | 29 |
|
||||
| Funciones | 5 |
|
||||
| Triggers | 18 |
|
||||
| Extensiones | 4 |
|
||||
|
||||
### Backend
|
||||
| Metrica | Valor |
|
||||
|---------|-------|
|
||||
| Modulos | 12 |
|
||||
| Controllers | 14 |
|
||||
| Endpoints | 100+ |
|
||||
| Entities | 20 |
|
||||
|
||||
### Frontend
|
||||
| Metrica | Valor |
|
||||
|---------|-------|
|
||||
| Paginas | 9 |
|
||||
| Componentes | 1 |
|
||||
| Contexts | 1 |
|
||||
| Servicios API | 6 |
|
||||
|
||||
### Mobile
|
||||
| Metrica | Valor |
|
||||
|---------|-------|
|
||||
| Pantallas | 10 |
|
||||
| Estado | Completado |
|
||||
| Modo Offline | Si |
|
||||
|
||||
---
|
||||
|
||||
## Referencias
|
||||
|
||||
### Documentacion Principal
|
||||
- [VISION-PROYECTO.md](00-vision-general/VISION-PROYECTO.md) - Vision estrategica
|
||||
- [REQUERIMIENTOS-FUNCIONALES.md](00-vision-general/REQUERIMIENTOS-FUNCIONALES.md) - Requisitos del sistema
|
||||
- [ARQUITECTURA-TECNICA.md](00-vision-general/ARQUITECTURA-TECNICA.md) - Stack tecnico
|
||||
|
||||
### Orchestration
|
||||
- [CONTEXTO-PROYECTO.md](../orchestration/00-guidelines/CONTEXTO-PROYECTO.md) - Contexto del proyecto
|
||||
- [PROXIMA-ACCION.md](../orchestration/PROXIMA-ACCION.md) - Siguiente accion
|
||||
- [PROJECT-STATUS.md](../orchestration/PROJECT-STATUS.md) - Estado del proyecto
|
||||
- [PLAN-IMPLEMENTACION.md](../orchestration/PLAN-IMPLEMENTACION.md) - Plan de implementacion
|
||||
|
||||
### Guias
|
||||
- [GUIA-DESPLIEGUE.md](90-transversal/GUIA-DESPLIEGUE.md) - Despliegue a produccion
|
||||
|
||||
---
|
||||
|
||||
## Integraciones Externas
|
||||
|
||||
| Servicio | Estado | Notas |
|
||||
|----------|--------|-------|
|
||||
| PostgreSQL 15+ | Activo | Puerto 5432 |
|
||||
| Redis 7 | Activo | Puerto 6379, DB 8 |
|
||||
| WhatsApp Business (Meta) | Listo | Requiere cuenta Business verificada |
|
||||
| Stripe | Integrado | Requiere API keys produccion |
|
||||
| OpenRouter/OpenAI | Multi-tenant | Credenciales por tenant + fallback |
|
||||
| MercadoPago | Pendiente | - |
|
||||
| Clip | Pendiente | - |
|
||||
|
||||
---
|
||||
|
||||
## Proximas Acciones
|
||||
|
||||
### Prioridad P0 (Critica)
|
||||
- Desplegar en servidor produccion con Docker
|
||||
- Configurar dominio y SSL
|
||||
- Obtener cuenta Meta Business
|
||||
- Configurar Stripe produccion
|
||||
|
||||
### Prioridad P1 (Alta)
|
||||
- Configurar LLM API key
|
||||
- Pruebas E2E completas
|
||||
- Completar documentacion SIMCO
|
||||
|
||||
### Prioridad P2 (Media)
|
||||
- Implementar FASE 6 restante (MCH-023, MCH-024, MCH-025)
|
||||
- FASE 7 - Expansion LATAM
|
||||
|
||||
---
|
||||
|
||||
**Ultima actualizacion:** 2026-01-10
|
||||
**Version:** 2.0.0
|
||||
**Actualizado por:** Agente Orquestador
|
||||
@ -0,0 +1,141 @@
|
||||
# MiChangarrito - Contexto del Proyecto
|
||||
|
||||
## Identificación
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **Nombre** | MiChangarrito |
|
||||
| **Código** | MCH |
|
||||
| **Tipo** | SaaS - Punto de Venta + IA |
|
||||
| **Estado** | Desarrollo |
|
||||
| **Nivel** | NIVEL_2A (Standalone) |
|
||||
| **Inicio** | 2026-01-04 |
|
||||
|
||||
## Descripción
|
||||
|
||||
Punto de venta inteligente diseñado para micro-negocios informales en México (tiendas de abarrotes, puestos de comida, fondas). La interfaz principal es WhatsApp con un asistente de IA que gestiona el negocio.
|
||||
|
||||
## Propuesta de Valor
|
||||
|
||||
- **WhatsApp First**: El dueño opera su negocio desde WhatsApp
|
||||
- **IA Asistente**: Consulta ventas, inventario, ganancias por chat
|
||||
- **Pagos Integrados**: Terminal Mercado Pago, Clip, CoDi
|
||||
- **Precio Accesible**: $99-199/mes + tokens de IA
|
||||
|
||||
## Target Market
|
||||
|
||||
- Tiendas de abarrotes
|
||||
- Puestos de comida
|
||||
- Fondas y cocinas económicas
|
||||
- Pequeños comercios informales
|
||||
- Usuarios: Personas mayores de 40 años con baja adopción tecnológica
|
||||
|
||||
## Stack Tecnológico
|
||||
|
||||
| Componente | Tecnología |
|
||||
|------------|------------|
|
||||
| App Móvil | React Native (Expo) |
|
||||
| Web Dashboard | React + Vite + TailwindCSS |
|
||||
| Backend API | NestJS |
|
||||
| MCP Server | TypeScript + MCP SDK |
|
||||
| WhatsApp Service | NestJS + Meta API |
|
||||
| Base de Datos | PostgreSQL (multi-tenant) |
|
||||
| Cache | Redis |
|
||||
| LLM | Agnóstico (OpenRouter/OpenAI/Claude) |
|
||||
|
||||
## Arquitectura de Carpetas
|
||||
|
||||
```
|
||||
michangarrito/
|
||||
├── docs/
|
||||
│ ├── 00-vision-general/ # Documentación libre de formato
|
||||
│ ├── 01-epicas/ # Épicas del proyecto
|
||||
│ ├── 02-especificaciones/ # Specs técnicas
|
||||
│ └── 90-transversal/ # Docs transversales
|
||||
├── orchestration/
|
||||
│ ├── 00-guidelines/ # Lineamientos
|
||||
│ ├── environment/ # Config DevEnv
|
||||
│ └── estados/ # Estado de agentes
|
||||
├── database/
|
||||
│ ├── init/ # Scripts iniciales
|
||||
│ ├── schemas/ # DDL
|
||||
│ └── seeds/ # Datos iniciales
|
||||
└── apps/
|
||||
├── backend/ # NestJS API
|
||||
├── frontend/ # React Dashboard
|
||||
├── mobile/ # React Native App
|
||||
├── mcp-server/ # Gateway LLM
|
||||
└── whatsapp-service/ # WhatsApp Bot
|
||||
```
|
||||
|
||||
## Puertos Asignados (Desarrollo)
|
||||
|
||||
| Servicio | Puerto |
|
||||
|----------|--------|
|
||||
| Web Dashboard | 3140 |
|
||||
| Backend API | 3141 |
|
||||
| MCP Server | 3142 |
|
||||
| WhatsApp Service | 3143 |
|
||||
| Mobile (Metro) | 8081 |
|
||||
| PostgreSQL | 5432 |
|
||||
| Redis | 6379 (db:8) |
|
||||
|
||||
## Épicas Principales
|
||||
|
||||
1. **FASE 1 - MVP Core**: Infraestructura, Auth, Productos, POS, Pagos
|
||||
2. **FASE 2 - Inteligencia**: Onboarding, Templates, Fiados, Predicciones
|
||||
3. **FASE 3 - Asistente IA**: MCP Server, WhatsApp, Chat LLM
|
||||
4. **FASE 4 - Pedidos**: Clientes, Pedidos WhatsApp, Entregas
|
||||
5. **FASE 5 - Monetización**: Suscripciones, Tokens, Pagos
|
||||
6. **FASE 6 - Crecimiento**: Offline, Referidos, CoDi, Widgets
|
||||
|
||||
## Integraciones Externas
|
||||
|
||||
| Servicio | Propósito |
|
||||
|----------|-----------|
|
||||
| Meta WhatsApp Business API | Canal principal de comunicación |
|
||||
| Stripe | Suscripciones, pagos, OXXO |
|
||||
| Mercado Pago | Terminal de pago con tarjeta |
|
||||
| Clip | Terminal de pago con tarjeta |
|
||||
| CoDi (Banxico) | Pagos QR sin comisión |
|
||||
| OpenRouter | Gateway LLM (más barato) |
|
||||
| Firebase | Push notifications |
|
||||
| Google Vision / Tesseract | OCR de imágenes |
|
||||
| Whisper | Transcripción de audio |
|
||||
|
||||
## Modelo de Negocio
|
||||
|
||||
| Plan | Precio | Incluye |
|
||||
|------|--------|---------|
|
||||
| Changarrito | $99/mes | App completa + 500 tokens IA |
|
||||
| Tiendita | $199/mes | Todo + 2,000 tokens + WhatsApp propio |
|
||||
|
||||
**Tokens IA adicionales** (tipo recarga):
|
||||
- $29 = 1,000 tokens
|
||||
- $69 = 3,000 tokens
|
||||
- $149 = 8,000 tokens
|
||||
- $299 = 20,000 tokens
|
||||
|
||||
## Referencias
|
||||
|
||||
- [Visión del Proyecto](../docs/00-vision-general/VISION-PROYECTO.md)
|
||||
- [Requerimientos Funcionales](../docs/00-vision-general/REQUERIMIENTOS-FUNCIONALES.md)
|
||||
- [Arquitectura Técnica](../docs/00-vision-general/ARQUITECTURA-TECNICA.md)
|
||||
- [Mapa de Épicas](../docs/01-epicas/_MAP.md)
|
||||
- [Environment Inventory](./environment/ENVIRONMENT-INVENTORY.yml)
|
||||
|
||||
## Estado Actual de Implementación
|
||||
|
||||
| Componente | Estado | Notas |
|
||||
|------------|--------|-------|
|
||||
| Base de datos | ✅ Completado | 9 schemas, 29 tablas |
|
||||
| Backend API | ✅ Completado | 12 módulos NestJS |
|
||||
| WhatsApp Service | ✅ Completado | Meta API + LLM + Multi-tenant |
|
||||
| MCP Server | ✅ Completado | 15 herramientas |
|
||||
| Frontend Web | ✅ Completado | 9 páginas |
|
||||
| Mobile App | ✅ Completado | React Native (Expo) - 10 pantallas |
|
||||
| Multi-Tenant Integraciones | ✅ Completado | WhatsApp/LLM por tenant con fallback |
|
||||
|
||||
---
|
||||
|
||||
**Última actualización**: 2026-01-10
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user