fix: Resolve TypeScript build errors
- Replace class-validator with plain TypeScript interfaces in AI DTOs - Fix AIModel field names (inputCostPer1k instead of inputCostPer1m) - Fix AIUsageLog field mapping (promptTokens, completionTokens, cost) - Add 'branches', 'financial', 'sales' to MCP ToolCategory - Add branchId to McpContext interface - Create stub entities for BranchPaymentTerminal and PaymentTransaction - Create CircuitBreaker utility for payment transactions - Create database config and error classes for base.service.ts - Fix type assertions for fetch response.json() calls - Align PaymentStatus and PaymentMethod types between entities and DTOs - Add @types/pg dependency Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
dad6575a3c
commit
a0ca2bd77a
13
package-lock.json
generated
13
package-lock.json
generated
@ -33,6 +33,7 @@
|
|||||||
"@types/jsonwebtoken": "^9.0.5",
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.0",
|
||||||
|
"@types/pg": "^8.16.0",
|
||||||
"@types/uuid": "^9.0.7",
|
"@types/uuid": "^9.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||||
"@typescript-eslint/parser": "^6.14.0",
|
"@typescript-eslint/parser": "^6.14.0",
|
||||||
@ -1656,6 +1657,18 @@
|
|||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/pg": {
|
||||||
|
"version": "8.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz",
|
||||||
|
"integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"pg-protocol": "*",
|
||||||
|
"pg-types": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||||
|
|||||||
41
package.json
41
package.json
@ -15,38 +15,39 @@
|
|||||||
"db:migrate:revert": "typeorm migration:revert"
|
"db:migrate:revert": "typeorm migration:revert"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"typeorm": "^0.3.17",
|
"helmet": "^7.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"morgan": "^1.10.0",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"reflect-metadata": "^0.2.1",
|
"reflect-metadata": "^0.2.1",
|
||||||
"dotenv": "^16.3.1",
|
"typeorm": "^0.3.17",
|
||||||
"zod": "^3.22.4",
|
|
||||||
"bcryptjs": "^2.4.3",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"cors": "^2.8.5",
|
"winston": "^3.11.0",
|
||||||
"helmet": "^7.1.0",
|
"zod": "^3.22.4"
|
||||||
"compression": "^1.7.4",
|
|
||||||
"morgan": "^1.10.0",
|
|
||||||
"winston": "^3.11.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.21",
|
|
||||||
"@types/node": "^20.10.0",
|
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/jsonwebtoken": "^9.0.5",
|
|
||||||
"@types/uuid": "^9.0.7",
|
|
||||||
"@types/cors": "^2.8.17",
|
|
||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/jest": "^29.5.11",
|
||||||
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
"typescript": "^5.3.3",
|
"@types/node": "^20.10.0",
|
||||||
"ts-node-dev": "^2.0.0",
|
"@types/pg": "^8.16.0",
|
||||||
"eslint": "^8.55.0",
|
"@types/uuid": "^9.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||||
"@typescript-eslint/parser": "^6.14.0",
|
"@typescript-eslint/parser": "^6.14.0",
|
||||||
|
"eslint": "^8.55.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"@types/jest": "^29.5.11",
|
"ts-jest": "^29.1.1",
|
||||||
"ts-jest": "^29.1.1"
|
"ts-node-dev": "^2.0.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
|
|||||||
67
src/config/database.ts
Normal file
67
src/config/database.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/**
|
||||||
|
* Database Configuration and Query Helpers
|
||||||
|
* Stub implementation for base.service.ts compatibility
|
||||||
|
*/
|
||||||
|
import { Pool, PoolClient, QueryResult } from 'pg';
|
||||||
|
|
||||||
|
// Connection pool (initialized lazily)
|
||||||
|
let pool: Pool | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create database pool
|
||||||
|
*/
|
||||||
|
function getPool(): Pool {
|
||||||
|
if (!pool) {
|
||||||
|
pool = new Pool({
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_PORT || '5432', 10),
|
||||||
|
database: process.env.DB_NAME || 'mecanicas_diesel',
|
||||||
|
user: process.env.DB_USER || 'mecanicas_user',
|
||||||
|
password: process.env.DB_PASSWORD || 'mecanicas_dev_2026',
|
||||||
|
max: 20,
|
||||||
|
idleTimeoutMillis: 30000,
|
||||||
|
connectionTimeoutMillis: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a query and return rows
|
||||||
|
*/
|
||||||
|
export async function query<T = any>(sql: string, params?: any[]): Promise<T[]> {
|
||||||
|
const client = await getPool().connect();
|
||||||
|
try {
|
||||||
|
const result = await client.query(sql, params);
|
||||||
|
return result.rows as T[];
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a query and return a single row
|
||||||
|
*/
|
||||||
|
export async function queryOne<T = any>(sql: string, params?: any[]): Promise<T | null> {
|
||||||
|
const rows = await query<T>(sql, params);
|
||||||
|
return rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a client from the pool
|
||||||
|
*/
|
||||||
|
export async function getClient(): Promise<PoolClient> {
|
||||||
|
return getPool().connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the pool
|
||||||
|
*/
|
||||||
|
export async function closePool(): Promise<void> {
|
||||||
|
if (pool) {
|
||||||
|
await pool.end();
|
||||||
|
pool = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { PoolClient };
|
||||||
@ -1,133 +1,39 @@
|
|||||||
import {
|
/**
|
||||||
IsString,
|
* AI DTOs
|
||||||
IsOptional,
|
* Using plain TypeScript interfaces (Zod for runtime validation)
|
||||||
IsBoolean,
|
*/
|
||||||
IsNumber,
|
|
||||||
IsArray,
|
|
||||||
IsObject,
|
|
||||||
IsUUID,
|
|
||||||
MaxLength,
|
|
||||||
MinLength,
|
|
||||||
Min,
|
|
||||||
Max,
|
|
||||||
} from 'class-validator';
|
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// PROMPT DTOs
|
// PROMPT DTOs
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
export class CreatePromptDto {
|
export interface CreatePromptDto {
|
||||||
@IsString()
|
|
||||||
@MinLength(2)
|
|
||||||
@MaxLength(50)
|
|
||||||
code: string;
|
code: string;
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@MinLength(2)
|
|
||||||
@MaxLength(100)
|
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@MaxLength(30)
|
|
||||||
category?: string;
|
category?: string;
|
||||||
|
|
||||||
@IsString()
|
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
userPromptTemplate?: string;
|
userPromptTemplate?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
@IsString({ each: true })
|
|
||||||
variables?: string[];
|
variables?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
@Max(2)
|
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(1)
|
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
@IsString({ each: true })
|
|
||||||
stopSequences?: string[];
|
stopSequences?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsObject()
|
|
||||||
modelParameters?: Record<string, any>;
|
modelParameters?: Record<string, any>;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
@IsString({ each: true })
|
|
||||||
allowedModels?: string[];
|
allowedModels?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsObject()
|
|
||||||
metadata?: Record<string, any>;
|
metadata?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdatePromptDto {
|
export interface UpdatePromptDto {
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@MaxLength(100)
|
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@MaxLength(30)
|
|
||||||
category?: string;
|
category?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
userPromptTemplate?: string;
|
userPromptTemplate?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
@IsString({ each: true })
|
|
||||||
variables?: string[];
|
variables?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
@Max(2)
|
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(1)
|
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
@IsString({ each: true })
|
|
||||||
stopSequences?: string[];
|
stopSequences?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsObject()
|
|
||||||
modelParameters?: Record<string, any>;
|
modelParameters?: Record<string, any>;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,71 +41,23 @@ export class UpdatePromptDto {
|
|||||||
// CONVERSATION DTOs
|
// CONVERSATION DTOs
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
export class CreateConversationDto {
|
export interface CreateConversationDto {
|
||||||
@IsOptional()
|
|
||||||
@IsUUID()
|
|
||||||
modelId?: string;
|
modelId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsUUID()
|
|
||||||
promptId?: string;
|
promptId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@MaxLength(200)
|
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
@Max(2)
|
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(1)
|
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsObject()
|
|
||||||
context?: Record<string, any>;
|
context?: Record<string, any>;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsObject()
|
|
||||||
metadata?: Record<string, any>;
|
metadata?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateConversationDto {
|
export interface UpdateConversationDto {
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@MaxLength(200)
|
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
systemPrompt?: string;
|
systemPrompt?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
@Max(2)
|
|
||||||
temperature?: number;
|
temperature?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(1)
|
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsObject()
|
|
||||||
context?: Record<string, any>;
|
context?: Record<string, any>;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsObject()
|
|
||||||
metadata?: Record<string, any>;
|
metadata?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,46 +65,15 @@ export class UpdateConversationDto {
|
|||||||
// MESSAGE DTOs
|
// MESSAGE DTOs
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
export class AddMessageDto {
|
export interface AddMessageDto {
|
||||||
@IsString()
|
|
||||||
@MaxLength(20)
|
|
||||||
role: string;
|
role: string;
|
||||||
|
|
||||||
@IsString()
|
|
||||||
content: string;
|
content: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@MaxLength(50)
|
|
||||||
modelCode?: string;
|
modelCode?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
promptTokens?: number;
|
promptTokens?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
completionTokens?: number;
|
completionTokens?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
totalTokens?: number;
|
totalTokens?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@MaxLength(30)
|
|
||||||
finishReason?: string;
|
finishReason?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
latencyMs?: number;
|
latencyMs?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsObject()
|
|
||||||
metadata?: Record<string, any>;
|
metadata?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,50 +81,17 @@ export class AddMessageDto {
|
|||||||
// USAGE DTOs
|
// USAGE DTOs
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
export class LogUsageDto {
|
export interface LogUsageDto {
|
||||||
@IsOptional()
|
|
||||||
@IsUUID()
|
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsUUID()
|
|
||||||
conversationId?: string;
|
conversationId?: string;
|
||||||
|
|
||||||
@IsUUID()
|
|
||||||
modelId: string;
|
modelId: string;
|
||||||
|
|
||||||
@IsString()
|
|
||||||
@MaxLength(20)
|
|
||||||
usageType: string;
|
usageType: string;
|
||||||
|
promptTokens: number;
|
||||||
@IsNumber()
|
completionTokens: number;
|
||||||
@Min(0)
|
|
||||||
inputTokens: number;
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
outputTokens: number;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
costUsd?: number;
|
costUsd?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
latencyMs?: number;
|
latencyMs?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
wasSuccessful?: boolean;
|
wasSuccessful?: boolean;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsObject()
|
|
||||||
metadata?: Record<string, any>;
|
metadata?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,39 +99,12 @@ export class LogUsageDto {
|
|||||||
// QUOTA DTOs
|
// QUOTA DTOs
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|
||||||
export class UpdateQuotaDto {
|
export interface UpdateQuotaDto {
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
maxRequestsPerMonth?: number;
|
maxRequestsPerMonth?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
maxTokensPerMonth?: number;
|
maxTokensPerMonth?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
maxSpendPerMonth?: number;
|
maxSpendPerMonth?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
maxRequestsPerDay?: number;
|
maxRequestsPerDay?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNumber()
|
|
||||||
@Min(0)
|
|
||||||
maxTokensPerDay?: number;
|
maxTokensPerDay?: number;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
@IsString({ each: true })
|
|
||||||
allowedModels?: string[];
|
allowedModels?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsArray()
|
|
||||||
@IsString({ each: true })
|
|
||||||
blockedModels?: string[];
|
blockedModels?: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -271,9 +271,10 @@ export class RoleBasedAIService extends AIService {
|
|||||||
await this.logUsage(context.tenantId, {
|
await this.logUsage(context.tenantId, {
|
||||||
modelId: model.id,
|
modelId: model.id,
|
||||||
conversationId: conversation.id,
|
conversationId: conversation.id,
|
||||||
inputTokens: response.tokensUsed.input,
|
promptTokens: response.tokensUsed.input,
|
||||||
outputTokens: response.tokensUsed.output,
|
completionTokens: response.tokensUsed.output,
|
||||||
costUsd: this.calculateCost(model, response.tokensUsed),
|
totalTokens: response.tokensUsed.total,
|
||||||
|
cost: this.calculateCost(model, response.tokensUsed),
|
||||||
usageType: 'chat',
|
usageType: 'chat',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -372,7 +373,7 @@ export class RoleBasedAIService extends AIService {
|
|||||||
'HTTP-Referer': process.env.APP_URL || 'https://erp.local',
|
'HTTP-Referer': process.env.APP_URL || 'https://erp.local',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: model.externalId || model.code,
|
model: model.modelId || model.code,
|
||||||
messages: messages.map((m) => ({
|
messages: messages.map((m) => ({
|
||||||
role: m.role,
|
role: m.role,
|
||||||
content: m.content,
|
content: m.content,
|
||||||
@ -393,18 +394,33 @@ export class RoleBasedAIService extends AIService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json().catch(() => ({}));
|
const error = await response.json().catch(() => ({})) as { error?: { message?: string } };
|
||||||
throw new Error(error.error?.message || 'AI provider error');
|
throw new Error(error.error?.message || 'AI provider error');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json() as {
|
||||||
|
choices?: Array<{
|
||||||
|
message?: {
|
||||||
|
content?: string;
|
||||||
|
tool_calls?: Array<{
|
||||||
|
id: string;
|
||||||
|
function?: { name: string; arguments: string };
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
usage?: {
|
||||||
|
prompt_tokens?: number;
|
||||||
|
completion_tokens?: number;
|
||||||
|
total_tokens?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
const choice = data.choices?.[0];
|
const choice = data.choices?.[0];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: choice?.message?.content || '',
|
content: choice?.message?.content || '',
|
||||||
toolCalls: choice?.message?.tool_calls?.map((tc: any) => ({
|
toolCalls: choice?.message?.tool_calls?.map((tc) => ({
|
||||||
id: tc.id,
|
id: tc.id,
|
||||||
name: tc.function?.name,
|
name: tc.function?.name || '',
|
||||||
arguments: JSON.parse(tc.function?.arguments || '{}'),
|
arguments: JSON.parse(tc.function?.arguments || '{}'),
|
||||||
})),
|
})),
|
||||||
tokensUsed: {
|
tokensUsed: {
|
||||||
@ -422,8 +438,8 @@ export class RoleBasedAIService extends AIService {
|
|||||||
model: AIModel,
|
model: AIModel,
|
||||||
tokens: { input: number; output: number }
|
tokens: { input: number; output: number }
|
||||||
): number {
|
): number {
|
||||||
const inputCost = (tokens.input / 1000000) * (model.inputCostPer1m || 0);
|
const inputCost = (tokens.input / 1000) * (model.inputCostPer1k || 0);
|
||||||
const outputCost = (tokens.output / 1000000) * (model.outputCostPer1m || 0);
|
const outputCost = (tokens.output / 1000) * (model.outputCostPer1k || 0);
|
||||||
return inputCost + outputCost;
|
return inputCost + outputCost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Branch Payment Terminal Entity (Stub)
|
||||||
|
* TODO: Full implementation
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
export type TerminalProvider = 'mercadopago' | 'clip' | 'stripe_terminal';
|
||||||
|
export type HealthStatus = 'healthy' | 'degraded' | 'unhealthy' | 'unknown';
|
||||||
|
|
||||||
|
@Entity({ name: 'branch_payment_terminals', schema: 'branches' })
|
||||||
|
export class BranchPaymentTerminal {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({ name: 'branch_id', type: 'uuid' })
|
||||||
|
branchId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'terminal_provider', type: 'varchar', length: 50 })
|
||||||
|
terminalProvider: TerminalProvider;
|
||||||
|
|
||||||
|
@Column({ name: 'terminal_id', type: 'varchar', length: 100 })
|
||||||
|
terminalId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'terminal_name', type: 'varchar', length: 200, nullable: true })
|
||||||
|
terminalName: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'jsonb', default: {} })
|
||||||
|
credentials: Record<string, any>;
|
||||||
|
|
||||||
|
@Column({ name: 'is_primary', type: 'boolean', default: false })
|
||||||
|
isPrimary: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'daily_limit', type: 'decimal', precision: 12, scale: 2, nullable: true })
|
||||||
|
dailyLimit: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'transaction_limit', type: 'decimal', precision: 12, scale: 2, nullable: true })
|
||||||
|
transactionLimit: number | null;
|
||||||
|
|
||||||
|
@Column({ name: 'health_status', type: 'varchar', length: 20, default: 'unknown' })
|
||||||
|
healthStatus: HealthStatus;
|
||||||
|
|
||||||
|
@Column({ name: 'last_transaction_at', type: 'timestamptz', nullable: true })
|
||||||
|
lastTransactionAt: Date | null;
|
||||||
|
|
||||||
|
@Column({ name: 'last_health_check_at', type: 'timestamptz', nullable: true })
|
||||||
|
lastHealthCheckAt: Date | null;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ export interface McpContext {
|
|||||||
userId?: string;
|
userId?: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
conversationId?: string;
|
conversationId?: string;
|
||||||
|
branchId?: string;
|
||||||
callerType: CallerType;
|
callerType: CallerType;
|
||||||
permissions: string[];
|
permissions: string[];
|
||||||
metadata?: Record<string, any>;
|
metadata?: Record<string, any>;
|
||||||
|
|||||||
@ -39,6 +39,9 @@ export type ToolCategory =
|
|||||||
| 'orders'
|
| 'orders'
|
||||||
| 'customers'
|
| 'customers'
|
||||||
| 'fiados'
|
| 'fiados'
|
||||||
|
| 'branches'
|
||||||
|
| 'financial'
|
||||||
|
| 'sales'
|
||||||
| 'system';
|
| 'system';
|
||||||
|
|
||||||
export interface McpToolDefinition {
|
export interface McpToolDefinition {
|
||||||
|
|||||||
@ -326,6 +326,9 @@ export class SalesToolsService implements McpToolProvider {
|
|||||||
params: { period?: string },
|
params: { period?: string },
|
||||||
context: McpContext
|
context: McpContext
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
if (!context.userId) {
|
||||||
|
throw new Error('User ID is required for getMySales');
|
||||||
|
}
|
||||||
const period = (params.period || 'today') as 'today' | 'week' | 'month';
|
const period = (params.period || 'today') as 'today' | 'week' | 'month';
|
||||||
const userSales = await this.analyticsService.getUserSales(
|
const userSales = await this.analyticsService.getUserSales(
|
||||||
context.tenantId,
|
context.tenantId,
|
||||||
|
|||||||
112
src/modules/mobile/entities/payment-transaction.entity.ts
Normal file
112
src/modules/mobile/entities/payment-transaction.entity.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* Payment Transaction Entity (Stub)
|
||||||
|
* TODO: Full implementation
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
Index,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
export type PaymentSourceType = 'sale' | 'service_order' | 'fiado_payment' | 'manual';
|
||||||
|
export type PaymentMethod = 'card' | 'qr' | 'link' | 'cash' | 'bank_transfer';
|
||||||
|
export type PaymentStatus =
|
||||||
|
| 'pending'
|
||||||
|
| 'processing'
|
||||||
|
| 'approved'
|
||||||
|
| 'authorized'
|
||||||
|
| 'in_process'
|
||||||
|
| 'rejected'
|
||||||
|
| 'refunded'
|
||||||
|
| 'partially_refunded'
|
||||||
|
| 'cancelled'
|
||||||
|
| 'charged_back'
|
||||||
|
| 'completed'
|
||||||
|
| 'failed';
|
||||||
|
|
||||||
|
@Entity({ name: 'payment_transactions', schema: 'mobile' })
|
||||||
|
export class PaymentTransaction {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({ name: 'tenant_id', type: 'uuid' })
|
||||||
|
tenantId: string;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({ name: 'branch_id', type: 'uuid' })
|
||||||
|
branchId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'user_id', type: 'uuid', nullable: true })
|
||||||
|
userId: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'source_type', type: 'varchar', length: 30 })
|
||||||
|
sourceType: PaymentSourceType;
|
||||||
|
|
||||||
|
@Column({ name: 'source_id', type: 'uuid' })
|
||||||
|
sourceId: string;
|
||||||
|
|
||||||
|
@Column({ name: 'terminal_provider', type: 'varchar', length: 50 })
|
||||||
|
terminalProvider: string;
|
||||||
|
|
||||||
|
@Column({ name: 'terminal_id', type: 'varchar', length: 100 })
|
||||||
|
terminalId: string;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 12, scale: 2 })
|
||||||
|
amount: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 3, default: 'MXN' })
|
||||||
|
currency: string;
|
||||||
|
|
||||||
|
@Column({ name: 'tip_amount', type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||||
|
tipAmount: number;
|
||||||
|
|
||||||
|
@Column({ name: 'total_amount', type: 'decimal', precision: 12, scale: 2 })
|
||||||
|
totalAmount: number;
|
||||||
|
|
||||||
|
@Column({ name: 'payment_method', type: 'varchar', length: 30, default: 'card' })
|
||||||
|
paymentMethod: PaymentMethod;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column({ type: 'varchar', length: 20, default: 'pending' })
|
||||||
|
status: PaymentStatus;
|
||||||
|
|
||||||
|
@Column({ name: 'external_transaction_id', type: 'varchar', length: 255, nullable: true })
|
||||||
|
externalTransactionId: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'card_brand', type: 'varchar', length: 30, nullable: true })
|
||||||
|
cardBrand: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'card_last_four', type: 'varchar', length: 4, nullable: true })
|
||||||
|
cardLastFour: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'receipt_url', type: 'text', nullable: true })
|
||||||
|
receiptUrl: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'receipt_sent', type: 'boolean', default: false })
|
||||||
|
receiptSent: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'receipt_sent_to', type: 'varchar', length: 255, nullable: true })
|
||||||
|
receiptSentTo: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'failure_reason', type: 'text', nullable: true })
|
||||||
|
failureReason: string | null;
|
||||||
|
|
||||||
|
@Column({ name: 'provider_response', type: 'jsonb', nullable: true })
|
||||||
|
providerResponse: Record<string, any> | null;
|
||||||
|
|
||||||
|
@Column({ name: 'initiated_at', type: 'timestamptz', nullable: true })
|
||||||
|
initiatedAt: Date | null;
|
||||||
|
|
||||||
|
@Column({ name: 'completed_at', type: 'timestamptz', nullable: true })
|
||||||
|
completedAt: Date | null;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
@ -2,7 +2,9 @@
|
|||||||
* Terminal DTOs
|
* Terminal DTOs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TerminalProvider, HealthStatus } from '../../branches/entities/branch-payment-terminal.entity';
|
// Local type definitions (based on tenant-terminal-config.entity.ts)
|
||||||
|
export type TerminalProvider = 'mercadopago' | 'clip' | 'stripe_terminal';
|
||||||
|
export type HealthStatus = 'healthy' | 'degraded' | 'unhealthy' | 'unknown';
|
||||||
|
|
||||||
export class CreateTerminalDto {
|
export class CreateTerminalDto {
|
||||||
branchId: string;
|
branchId: string;
|
||||||
@ -36,12 +38,12 @@ export class TerminalResponseDto {
|
|||||||
branchId: string;
|
branchId: string;
|
||||||
terminalProvider: TerminalProvider;
|
terminalProvider: TerminalProvider;
|
||||||
terminalId: string;
|
terminalId: string;
|
||||||
terminalName?: string;
|
terminalName: string | null;
|
||||||
isPrimary: boolean;
|
isPrimary: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
dailyLimit?: number;
|
dailyLimit: number | null;
|
||||||
transactionLimit?: number;
|
transactionLimit: number | null;
|
||||||
healthStatus: HealthStatus;
|
healthStatus: HealthStatus;
|
||||||
lastTransactionAt?: Date;
|
lastTransactionAt: Date | null;
|
||||||
lastHealthCheckAt?: Date;
|
lastHealthCheckAt: Date | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,22 @@
|
|||||||
* Transaction DTOs
|
* Transaction DTOs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PaymentSourceType, PaymentMethod, PaymentStatus } from '../../mobile/entities/payment-transaction.entity';
|
// Local type definitions (based on terminal-payment.entity.ts)
|
||||||
|
export type PaymentSourceType = 'sale' | 'service_order' | 'fiado_payment' | 'manual';
|
||||||
|
export type PaymentMethod = 'card' | 'qr' | 'link' | 'cash' | 'bank_transfer';
|
||||||
|
export type PaymentStatus =
|
||||||
|
| 'pending'
|
||||||
|
| 'processing'
|
||||||
|
| 'approved'
|
||||||
|
| 'authorized'
|
||||||
|
| 'in_process'
|
||||||
|
| 'rejected'
|
||||||
|
| 'refunded'
|
||||||
|
| 'partially_refunded'
|
||||||
|
| 'cancelled'
|
||||||
|
| 'charged_back'
|
||||||
|
| 'completed'
|
||||||
|
| 'failed';
|
||||||
|
|
||||||
export class ProcessPaymentDto {
|
export class ProcessPaymentDto {
|
||||||
terminalId: string;
|
terminalId: string;
|
||||||
|
|||||||
@ -183,11 +183,11 @@ export class ClipService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json() as { message?: string };
|
||||||
throw new ClipError(error.message || 'Payment failed', response.status, error);
|
throw new ClipError(error.message || 'Payment failed', response.status, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json() as Promise<{ id: string; status: string; card?: { last_four: string; brand: string; type: string } }>;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actualizar registro local
|
// Actualizar registro local
|
||||||
@ -252,7 +252,7 @@ export class ClipService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const clipPayment = await response.json();
|
const clipPayment = await response.json() as { status: string };
|
||||||
payment.externalStatus = clipPayment.status;
|
payment.externalStatus = clipPayment.status;
|
||||||
payment.status = this.mapClipStatus(clipPayment.status);
|
payment.status = this.mapClipStatus(clipPayment.status);
|
||||||
payment.providerResponse = clipPayment;
|
payment.providerResponse = clipPayment;
|
||||||
@ -309,16 +309,16 @@ export class ClipService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json() as { message?: string };
|
||||||
throw new ClipError(error.message || 'Refund failed', response.status, error);
|
throw new ClipError(error.message || 'Refund failed', response.status, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json() as Promise<Record<string, any>>;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actualizar pago
|
// Actualizar pago
|
||||||
payment.refundedAmount = Number(payment.refundedAmount || 0) + refundAmount;
|
payment.refundedAmount = Number(payment.refundedAmount || 0) + refundAmount;
|
||||||
payment.refundReason = dto.reason;
|
payment.refundReason = dto.reason ?? null;
|
||||||
payment.refundedAt = new Date();
|
payment.refundedAt = new Date();
|
||||||
|
|
||||||
if (payment.refundedAmount >= Number(payment.amount)) {
|
if (payment.refundedAmount >= Number(payment.amount)) {
|
||||||
@ -361,7 +361,7 @@ export class ClipService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json() as { message?: string };
|
||||||
throw new ClipError(
|
throw new ClipError(
|
||||||
error.message || 'Failed to create payment link',
|
error.message || 'Failed to create payment link',
|
||||||
response.status,
|
response.status,
|
||||||
@ -369,7 +369,7 @@ export class ClipService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json() as Promise<{ url: string; id: string }>;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -170,23 +170,32 @@ export class MercadoPagoService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json() as { message?: string };
|
||||||
throw new MercadoPagoError(error.message || 'Payment failed', response.status, error);
|
throw new MercadoPagoError(error.message || 'Payment failed', response.status, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json() as Promise<{
|
||||||
|
id?: string | number;
|
||||||
|
status: string;
|
||||||
|
fee_details?: Array<{ amount: number }>;
|
||||||
|
card?: {
|
||||||
|
last_four_digits?: string;
|
||||||
|
payment_method?: { name?: string };
|
||||||
|
cardholder?: { identification?: { type?: string } };
|
||||||
|
};
|
||||||
|
}>;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actualizar registro local
|
// Actualizar registro local
|
||||||
savedPayment.externalId = mpPayment.id?.toString();
|
savedPayment.externalId = mpPayment.id?.toString() ?? null;
|
||||||
savedPayment.externalStatus = mpPayment.status;
|
savedPayment.externalStatus = mpPayment.status;
|
||||||
savedPayment.status = this.mapMPStatus(mpPayment.status);
|
savedPayment.status = this.mapMPStatus(mpPayment.status);
|
||||||
savedPayment.providerResponse = mpPayment;
|
savedPayment.providerResponse = mpPayment;
|
||||||
savedPayment.processedAt = new Date();
|
savedPayment.processedAt = new Date();
|
||||||
|
|
||||||
if (mpPayment.fee_details?.length > 0) {
|
if (mpPayment.fee_details && mpPayment.fee_details.length > 0) {
|
||||||
const totalFee = mpPayment.fee_details.reduce(
|
const totalFee = mpPayment.fee_details.reduce(
|
||||||
(sum: number, fee: any) => sum + fee.amount,
|
(sum: number, fee) => sum + fee.amount,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
savedPayment.feeAmount = totalFee;
|
savedPayment.feeAmount = totalFee;
|
||||||
@ -195,9 +204,9 @@ export class MercadoPagoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mpPayment.card) {
|
if (mpPayment.card) {
|
||||||
savedPayment.cardLastFour = mpPayment.card.last_four_digits;
|
savedPayment.cardLastFour = mpPayment.card.last_four_digits ?? null;
|
||||||
savedPayment.cardBrand = mpPayment.card.payment_method?.name;
|
savedPayment.cardBrand = mpPayment.card.payment_method?.name ?? null;
|
||||||
savedPayment.cardType = mpPayment.card.cardholder?.identification?.type;
|
savedPayment.cardType = mpPayment.card.cardholder?.identification?.type ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.paymentRepository.save(savedPayment);
|
return this.paymentRepository.save(savedPayment);
|
||||||
@ -249,7 +258,7 @@ export class MercadoPagoService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const mpPayment = await response.json();
|
const mpPayment = await response.json() as { status: string };
|
||||||
payment.externalStatus = mpPayment.status;
|
payment.externalStatus = mpPayment.status;
|
||||||
payment.status = this.mapMPStatus(mpPayment.status);
|
payment.status = this.mapMPStatus(mpPayment.status);
|
||||||
payment.providerResponse = mpPayment;
|
payment.providerResponse = mpPayment;
|
||||||
@ -304,16 +313,16 @@ export class MercadoPagoService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json() as { message?: string };
|
||||||
throw new MercadoPagoError(error.message || 'Refund failed', response.status, error);
|
throw new MercadoPagoError(error.message || 'Refund failed', response.status, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json() as Promise<Record<string, any>>;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actualizar pago
|
// Actualizar pago
|
||||||
payment.refundedAmount = Number(payment.refundedAmount || 0) + refundAmount;
|
payment.refundedAmount = Number(payment.refundedAmount || 0) + refundAmount;
|
||||||
payment.refundReason = dto.reason;
|
payment.refundReason = dto.reason ?? null;
|
||||||
payment.refundedAt = new Date();
|
payment.refundedAt = new Date();
|
||||||
|
|
||||||
if (payment.refundedAmount >= Number(payment.amount)) {
|
if (payment.refundedAmount >= Number(payment.amount)) {
|
||||||
@ -370,7 +379,7 @@ export class MercadoPagoService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.json();
|
const error = await response.json() as { message?: string };
|
||||||
throw new MercadoPagoError(
|
throw new MercadoPagoError(
|
||||||
error.message || 'Failed to create payment link',
|
error.message || 'Failed to create payment link',
|
||||||
response.status,
|
response.status,
|
||||||
@ -378,7 +387,7 @@ export class MercadoPagoService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
return response.json() as Promise<{ init_point: string; id: string }>;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -465,10 +474,17 @@ export class MercadoPagoService {
|
|||||||
|
|
||||||
if (!response.ok) return;
|
if (!response.ok) return;
|
||||||
|
|
||||||
const mpPayment = await response.json();
|
const mpPayment = await response.json() as {
|
||||||
|
status: string;
|
||||||
|
external_reference?: string;
|
||||||
|
card?: {
|
||||||
|
last_four_digits?: string;
|
||||||
|
payment_method?: { name?: string };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// Buscar pago local por external_reference o external_id
|
// Buscar pago local por external_reference o external_id
|
||||||
let payment = await this.paymentRepository.findOne({
|
const payment = await this.paymentRepository.findOne({
|
||||||
where: [
|
where: [
|
||||||
{ externalId: mpPaymentId.toString(), tenantId },
|
{ externalId: mpPaymentId.toString(), tenantId },
|
||||||
{ id: mpPayment.external_reference, tenantId },
|
{ id: mpPayment.external_reference, tenantId },
|
||||||
@ -483,8 +499,8 @@ export class MercadoPagoService {
|
|||||||
payment.processedAt = new Date();
|
payment.processedAt = new Date();
|
||||||
|
|
||||||
if (mpPayment.card) {
|
if (mpPayment.card) {
|
||||||
payment.cardLastFour = mpPayment.card.last_four_digits;
|
payment.cardLastFour = mpPayment.card.last_four_digits ?? null;
|
||||||
payment.cardBrand = mpPayment.card.payment_method?.name;
|
payment.cardBrand = mpPayment.card.payment_method?.name ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.paymentRepository.save(payment);
|
await this.paymentRepository.save(payment);
|
||||||
@ -494,7 +510,7 @@ export class MercadoPagoService {
|
|||||||
/**
|
/**
|
||||||
* Procesar webhook de reembolso
|
* Procesar webhook de reembolso
|
||||||
*/
|
*/
|
||||||
private async handleRefundWebhook(tenantId: string, refundId: string): Promise<void> {
|
private async handleRefundWebhook(tenantId: string, _refundId: string): Promise<void> {
|
||||||
// Implementación similar a handlePaymentWebhook
|
// Implementación similar a handlePaymentWebhook
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -147,12 +147,12 @@ export class TerminalsService {
|
|||||||
case 'mercadopago':
|
case 'mercadopago':
|
||||||
// Check MercadoPago terminal status
|
// Check MercadoPago terminal status
|
||||||
break;
|
break;
|
||||||
case 'stripe':
|
case 'stripe_terminal':
|
||||||
// Check Stripe terminal status
|
// Check Stripe terminal status
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
status = 'offline';
|
status = 'unhealthy';
|
||||||
message = error.message || 'Failed to connect to terminal';
|
message = error.message || 'Failed to connect to terminal';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,14 +209,14 @@ export class TerminalsService {
|
|||||||
return {
|
return {
|
||||||
id: terminal.id,
|
id: terminal.id,
|
||||||
branchId: terminal.branchId,
|
branchId: terminal.branchId,
|
||||||
terminalProvider: terminal.terminalProvider,
|
terminalProvider: terminal.terminalProvider as TerminalResponseDto['terminalProvider'],
|
||||||
terminalId: terminal.terminalId,
|
terminalId: terminal.terminalId,
|
||||||
terminalName: terminal.terminalName,
|
terminalName: terminal.terminalName,
|
||||||
isPrimary: terminal.isPrimary,
|
isPrimary: terminal.isPrimary,
|
||||||
isActive: terminal.isActive,
|
isActive: terminal.isActive,
|
||||||
dailyLimit: terminal.dailyLimit ? Number(terminal.dailyLimit) : undefined,
|
dailyLimit: terminal.dailyLimit ? Number(terminal.dailyLimit) : null,
|
||||||
transactionLimit: terminal.transactionLimit ? Number(terminal.transactionLimit) : undefined,
|
transactionLimit: terminal.transactionLimit ? Number(terminal.transactionLimit) : null,
|
||||||
healthStatus: terminal.healthStatus,
|
healthStatus: terminal.healthStatus as TerminalResponseDto['healthStatus'],
|
||||||
lastTransactionAt: terminal.lastTransactionAt,
|
lastTransactionAt: terminal.lastTransactionAt,
|
||||||
lastHealthCheckAt: terminal.lastHealthCheckAt,
|
lastHealthCheckAt: terminal.lastHealthCheckAt,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -133,7 +133,7 @@ export class TransactionsService {
|
|||||||
|
|
||||||
// Update terminal health
|
// Update terminal health
|
||||||
await this.terminalRepository.update(terminal.id, {
|
await this.terminalRepository.update(terminal.id, {
|
||||||
healthStatus: 'offline',
|
healthStatus: 'unhealthy',
|
||||||
lastHealthCheckAt: new Date(),
|
lastHealthCheckAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -336,18 +336,25 @@ export class TransactionsService {
|
|||||||
const byStatus: Record<PaymentStatus, number> = {
|
const byStatus: Record<PaymentStatus, number> = {
|
||||||
pending: 0,
|
pending: 0,
|
||||||
processing: 0,
|
processing: 0,
|
||||||
|
approved: 0,
|
||||||
|
authorized: 0,
|
||||||
|
in_process: 0,
|
||||||
|
rejected: 0,
|
||||||
|
refunded: 0,
|
||||||
|
partially_refunded: 0,
|
||||||
|
cancelled: 0,
|
||||||
|
charged_back: 0,
|
||||||
completed: 0,
|
completed: 0,
|
||||||
failed: 0,
|
failed: 0,
|
||||||
refunded: 0,
|
|
||||||
cancelled: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const byProvider: Record<string, { count: number; amount: number }> = {};
|
const byProvider: Record<string, { count: number; amount: number }> = {};
|
||||||
const byPaymentMethod: Record<PaymentMethod, number> = {
|
const byPaymentMethod: Record<PaymentMethod, number> = {
|
||||||
card: 0,
|
card: 0,
|
||||||
contactless: 0,
|
|
||||||
qr: 0,
|
qr: 0,
|
||||||
link: 0,
|
link: 0,
|
||||||
|
cash: 0,
|
||||||
|
bank_transfer: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let totalAmount = 0;
|
let totalAmount = 0;
|
||||||
|
|||||||
51
src/shared/errors/index.ts
Normal file
51
src/shared/errors/index.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Custom Error Classes
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class AppError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public statusCode: number = 500,
|
||||||
|
public code?: string
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotFoundError extends AppError {
|
||||||
|
constructor(message: string = 'Resource not found') {
|
||||||
|
super(message, 404, 'NOT_FOUND');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValidationError extends AppError {
|
||||||
|
constructor(message: string = 'Validation failed') {
|
||||||
|
super(message, 400, 'VALIDATION_ERROR');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnauthorizedError extends AppError {
|
||||||
|
constructor(message: string = 'Unauthorized') {
|
||||||
|
super(message, 401, 'UNAUTHORIZED');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ForbiddenError extends AppError {
|
||||||
|
constructor(message: string = 'Forbidden') {
|
||||||
|
super(message, 403, 'FORBIDDEN');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConflictError extends AppError {
|
||||||
|
constructor(message: string = 'Conflict') {
|
||||||
|
super(message, 409, 'CONFLICT');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InternalError extends AppError {
|
||||||
|
constructor(message: string = 'Internal server error') {
|
||||||
|
super(message, 500, 'INTERNAL_ERROR');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { query, queryOne, getClient, PoolClient } from '../../config/database.js';
|
import { query, getClient, PoolClient } from '../../config/database';
|
||||||
import { NotFoundError, ValidationError } from '../errors/index.js';
|
import { NotFoundError, ValidationError } from '../errors';
|
||||||
import { PaginationMeta } from '../types/index.js';
|
import { PaginationMeta } from '../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resultado paginado genérico
|
* Resultado paginado genérico
|
||||||
|
|||||||
@ -46,3 +46,23 @@ export interface TokenPair {
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pagination metadata
|
||||||
|
*/
|
||||||
|
export interface PaginationMeta {
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
hasNext: boolean;
|
||||||
|
hasPrev: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paginated response wrapper
|
||||||
|
*/
|
||||||
|
export interface PaginatedResponse<T> {
|
||||||
|
data: T[];
|
||||||
|
meta: PaginationMeta;
|
||||||
|
}
|
||||||
|
|||||||
116
src/shared/utils/circuit-breaker.ts
Normal file
116
src/shared/utils/circuit-breaker.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* Circuit Breaker Pattern Implementation
|
||||||
|
* Prevents cascade failures by monitoring failed requests
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface CircuitBreakerOptions {
|
||||||
|
failureThreshold: number; // Number of failures before opening circuit
|
||||||
|
halfOpenRequests: number; // Number of requests to allow in half-open state
|
||||||
|
resetTimeout: number; // Time in ms to wait before attempting half-open
|
||||||
|
}
|
||||||
|
|
||||||
|
type CircuitState = 'closed' | 'open' | 'half-open';
|
||||||
|
|
||||||
|
export class CircuitBreaker {
|
||||||
|
private state: CircuitState = 'closed';
|
||||||
|
private failureCount: number = 0;
|
||||||
|
private successCount: number = 0;
|
||||||
|
private lastFailureTime: number = 0;
|
||||||
|
private halfOpenSuccesses: number = 0;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private name: string,
|
||||||
|
private options: CircuitBreakerOptions
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a function through the circuit breaker
|
||||||
|
*/
|
||||||
|
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
|
if (this.isOpen()) {
|
||||||
|
throw new Error(`Circuit breaker ${this.name} is open`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await fn();
|
||||||
|
this.onSuccess();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
this.onFailure();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if circuit is open
|
||||||
|
*/
|
||||||
|
private isOpen(): boolean {
|
||||||
|
if (this.state === 'open') {
|
||||||
|
// Check if enough time has passed to try half-open
|
||||||
|
if (Date.now() - this.lastFailureTime >= this.options.resetTimeout) {
|
||||||
|
this.state = 'half-open';
|
||||||
|
this.halfOpenSuccesses = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle successful execution
|
||||||
|
*/
|
||||||
|
private onSuccess(): void {
|
||||||
|
if (this.state === 'half-open') {
|
||||||
|
this.halfOpenSuccesses++;
|
||||||
|
if (this.halfOpenSuccesses >= this.options.halfOpenRequests) {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.failureCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle failed execution
|
||||||
|
*/
|
||||||
|
private onFailure(): void {
|
||||||
|
this.failureCount++;
|
||||||
|
this.lastFailureTime = Date.now();
|
||||||
|
|
||||||
|
if (this.state === 'half-open' || this.failureCount >= this.options.failureThreshold) {
|
||||||
|
this.state = 'open';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset circuit breaker to closed state
|
||||||
|
*/
|
||||||
|
private reset(): void {
|
||||||
|
this.state = 'closed';
|
||||||
|
this.failureCount = 0;
|
||||||
|
this.halfOpenSuccesses = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current state
|
||||||
|
*/
|
||||||
|
getState(): CircuitState {
|
||||||
|
return this.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force open the circuit
|
||||||
|
*/
|
||||||
|
trip(): void {
|
||||||
|
this.state = 'open';
|
||||||
|
this.lastFailureTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force close the circuit
|
||||||
|
*/
|
||||||
|
close(): void {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user