/home/suroeste/public_html/payments.transportessuroeste.com/src/Controllers
Edit: /home/suroeste/public_html/payments.transportessuroeste.com/src/Controllers/ApiController.php (10921B)
paymentService = new PaymentService();
}
public function setClient(array $client): void
{
$this->client = $client;
}
/**
* POST /api/v1/payments - Crear nueva transacción
*/
public function createPayment(): array
{
$data = $this->getJsonInput();
$data = SecurityMiddleware::sanitizeArray($data);
$validated = Validator::validatePayment($data);
$result = $this->paymentService->createPayment($validated, $this->client);
LogService::info('access', 'Payment created', [
'transaction_uuid' => $result['transaction_uuid'],
'client' => $this->client['name']
]);
return $this->success($result, 'Transacción creada exitosamente', 201);
}
/**
* GET /api/v1/payments/{uuid} - Consultar transacción
*/
public function getPayment(string $uuid): array
{
$result = $this->paymentService->getTransactionStatus($uuid, 'uuid');
if (!$result) {
throw new ApiException('Transacción no encontrada', 404);
}
return $this->success($result);
}
/**
* GET /api/v1/payments/ticket/{reference} - Consultar por ticket
*/
public function getPaymentByTicket(string $reference): array
{
$result = $this->paymentService->getTransactionStatus($reference, 'ticket');
if (!$result) {
throw new ApiException('Transacción no encontrada', 404);
}
return $this->success($result);
}
/**
* GET /api/v1/banks/pse - Obtener bancos PSE
*/
public function getPseBanks(): array
{
$banks = $this->paymentService->getPseBanks();
return $this->success([
'banks' => $banks,
'total' => count($banks)
], 'Bancos PSE obtenidos');
}
/**
* POST /api/v1/webhook/epayco - Webhook de ePayco (SEGURO)
* GET/POST /api/v1/callback - Callback de ePayco (SEGURO)
*
* Validaciones:
* 1. Método HTTP (GET/POST)
* 2. Campos requeridos de ePayco
* 3. IP de origen (producción)
* 4. Firma SHA256 de ePayco
* 5. Lista blanca de campos
* 6. Sanitización de valores
*/
public function handleCallback(): array
{
$clientIp = LogService::getClientIp();
// 1. Validar método HTTP
$method = $_SERVER['REQUEST_METHOD'];
if (!in_array($method, ['GET', 'POST'])) {
LogService::security('webhook_blocked', 'Método no permitido', [
'method' => $method,
'ip' => $clientIp
]);
throw new SecurityException('Método no permitido', 405);
}
// 2. Obtener datos según método
$data = $method === 'POST'
? array_merge($_POST, $this->getJsonInput())
: $_GET;
// 3. Validar que hay datos
if (empty($data)) {
LogService::security('webhook_blocked', 'Datos vacíos', ['ip' => $clientIp]);
throw new ApiException('Datos requeridos', 400);
}
// 4. Validar campos requeridos de ePayco
$requiredFields = ['x_ref_payco', 'x_transaction_id', 'x_response', 'x_amount', 'x_signature'];
$missingFields = [];
foreach ($requiredFields as $field) {
if (!isset($data[$field]) || $data[$field] === '') {
$missingFields[] = $field;
}
}
if (!empty($missingFields)) {
LogService::security('webhook_blocked', 'Campos faltantes', [
'missing' => $missingFields,
'ip' => $clientIp
]);
throw new ApiException('Campos requeridos: ' . implode(', ', $missingFields), 400);
}
// 5. Validar IP de origen (solo en producción)
if (!EPAYCO_CONFIG['test_mode']) {
if (!$this->isValidEpaycoIp($clientIp)) {
LogService::security('webhook_blocked', 'IP no autorizada', [
'ip' => $clientIp,
'ref_payco' => $data['x_ref_payco']
]);
throw new SecurityException('Origen no autorizado', 403);
}
}
// 6. Validar firma de ePayco
if (!$this->validateEpaycoSignature($data)) {
LogService::security('webhook_blocked', 'Firma inválida', [
'ref_payco' => $data['x_ref_payco'],
'ip' => $clientIp,
'signature' => substr($data['x_signature'] ?? '', 0, 20) . '...'
]);
throw new SecurityException('Firma inválida', 403);
}
// 7. Filtrar y sanitizar solo campos permitidos
$data = $this->sanitizeEpaycoData($data);
// 8. Log del webhook válido
LogService::info('webhook_received', 'Webhook ePayco válido', [
'ref_payco' => $data['x_ref_payco'],
'response' => $data['x_response'],
'amount' => $data['x_amount'],
'transaction_id' => $data['x_transaction_id'],
'ip' => $clientIp
]);
// 9. Procesar el callback
$result = $this->paymentService->processCallback($data);
return $this->success($result, 'Webhook procesado');
}
/**
* Validar firma SHA256 de ePayco
* Formato: hash('sha256', $p_cust_id_cliente.$p_key.$x_ref_payco.$x_transaction_id.$x_amount.$x_currency_code)
*/
private function validateEpaycoSignature(array $data): bool
{
// Si no hay P_KEY, omitir validación (solo desarrollo)
if (empty(EPAYCO_CONFIG['p_key'])) {
return true;
}
$signature = $data['x_signature'] ?? '';
if (empty($signature) || strlen($signature) !== 64) {
return false;
}
// Obtener componentes
$custId = $data['x_cust_id_cliente'] ?? '';
$pKey = EPAYCO_CONFIG['p_key'];
$refPayco = $data['x_ref_payco'] ?? '';
$transactionId = $data['x_transaction_id'] ?? '';
$amount = $data['x_amount'] ?? '';
$currency = $data['x_currency_code'] ?? 'COP';
// Formato de firma ePayco
$signatureString = $custId . '^' . $pKey . '^' . $refPayco . '^' . $transactionId . '^' . $amount . '^' . $currency;
$expectedSignature = hash('sha256', $signatureString);
return hash_equals($expectedSignature, $signature);
}
/**
* Validar que la IP sea de ePayco (AWS)
*/
private function isValidEpaycoIp(string $ip): bool
{
// Permitir localhost en desarrollo
if (in_array($ip, ['127.0.0.1', '::1', 'localhost'])) {
return EPAYCO_CONFIG['test_mode'];
}
// Verificar rangos de AWS donde opera ePayco
foreach (self::EPAYCO_IP_RANGES as $range) {
if (strpos($ip, $range) === 0) {
return true;
}
}
return false;
}
/**
* Sanitizar datos de ePayco con lista blanca de campos
*/
private function sanitizeEpaycoData(array $data): array
{
// Lista blanca de campos permitidos de ePayco
$allowedFields = [
'x_cust_id_cliente', 'x_ref_payco', 'x_id_factura', 'x_id_invoice',
'x_description', 'x_amount', 'x_amount_country', 'x_amount_ok',
'x_tax', 'x_amount_base', 'x_currency_code', 'x_bank_name',
'x_cardnumber', 'x_quotas', 'x_respuesta', 'x_response',
'x_approval_code', 'x_transaction_id', 'x_fecha_transaccion',
'x_transaction_date', 'x_cod_respuesta', 'x_cod_response',
'x_response_reason_text', 'x_errorcode', 'x_cod_transaction_state',
'x_transaction_state', 'x_franchise', 'x_business',
'x_customer_doctype', 'x_customer_document', 'x_customer_name',
'x_customer_lastname', 'x_customer_email', 'x_customer_phone',
'x_customer_movil', 'x_customer_ind_pais', 'x_customer_country',
'x_customer_city', 'x_customer_address', 'x_customer_ip',
'x_test_request', 'x_extra1', 'x_extra2', 'x_extra3', 'x_extra4',
'x_extra5', 'x_extra6', 'x_extra7', 'x_extra8', 'x_extra9', 'x_extra10',
'x_tax_ico', 'x_payment_date', 'x_signature', 'x_transaction_cycle',
'is_processable'
];
$sanitized = [];
foreach ($data as $key => $value) {
// Solo campos en lista blanca
if (!in_array($key, $allowedFields)) {
continue;
}
if (is_string($value)) {
// Eliminar null bytes
$value = str_replace(chr(0), '', $value);
// Limitar longitud (prevenir DoS)
$value = substr($value, 0, 500);
// Trim espacios
$value = trim($value);
}
$sanitized[$key] = $value;
}
return $sanitized;
}
/**
* GET /api/v1/health - Health check
*/
public function healthCheck(): array
{
return $this->success([
'status' => 'healthy',
'service' => APP_NAME,
'version' => APP_VERSION,
'timestamp' => date('Y-m-d H:i:s'),
'environment' => APP_ENV
]);
}
private function getJsonInput(): array
{
$input = file_get_contents('php://input');
if (empty($input)) return [];
$data = json_decode($input, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return [];
}
return $data ?? [];
}
private function success($data, string $message = 'OK', int $code = 200): array
{
return [
'success' => true,
'message' => $message,
'data' => $data,
'meta' => [
'timestamp' => date('Y-m-d\TH:i:s.uP'),
'request_id' => $_SERVER['HTTP_X_REQUEST_ID'] ?? bin2hex(random_bytes(8))
]
];
}
}