feat: implement Telepagos webhook handling with request validation and service integration
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Integration\Controllers;
|
||||||
|
|
||||||
|
use App\Domains\Integration\Requests\TelepagosWebhookRequest;
|
||||||
|
use App\Domains\Integration\Services\TelepagosWebhookService;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class TelepagosWebhookController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the incoming Telepagos webhook.
|
||||||
|
*
|
||||||
|
* @param TelepagosWebhookRequest $request
|
||||||
|
* @param string $tenantCodigo
|
||||||
|
* @param TelepagosWebhookService $service
|
||||||
|
* @return JsonResponse
|
||||||
|
*/
|
||||||
|
public function handle(TelepagosWebhookRequest $request, string $tenantCodigo, TelepagosWebhookService $service): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$cashinId = $request->validated('id');
|
||||||
|
|
||||||
|
$service->handleWebhook($tenantCodigo, $cashinId);
|
||||||
|
|
||||||
|
return response()->json(['status' => 'success']);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return response()->json(['status' => 'error', 'message' => $e->getMessage()], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Domains/Integration/Requests/TelepagosWebhookRequest.php
Normal file
28
app/Domains/Integration/Requests/TelepagosWebhookRequest.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Integration\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class TelepagosWebhookRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => ['required', 'string'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
92
app/Domains/Integration/Services/TelepagosWebhookService.php
Normal file
92
app/Domains/Integration/Services/TelepagosWebhookService.php
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Integration\Services;
|
||||||
|
|
||||||
|
use App\Domains\Purchase\Models\TelepagosPayment;
|
||||||
|
use App\Domains\Purchase\Models\TelepagosQr;
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Exception;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class TelepagosWebhookService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle the Telepagos webhook notification.
|
||||||
|
*
|
||||||
|
* @param string $tenantCodigo
|
||||||
|
* @param string $cashinId
|
||||||
|
* @return void
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function handleWebhook(string $tenantCodigo, string $cashinId): void
|
||||||
|
{
|
||||||
|
$tenant = Tenant::where('codigo', $tenantCodigo)->firstOrFail();
|
||||||
|
|
||||||
|
$telepagosService = new TelepagosIntegrationService();
|
||||||
|
$telepagosService->forTenant($tenant);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$details = $telepagosService->getCashinDetails($cashinId);
|
||||||
|
|
||||||
|
// Extract the qr_order_id from the response
|
||||||
|
// The exact path depends on Telepagos response structure, assuming it's inside 'data'
|
||||||
|
// Need to handle potential nested structures based on actual API
|
||||||
|
$qrOrderId = $details['data']['qr_order_id'] ?? $details['qr_order_id'] ?? null;
|
||||||
|
$amount = $details['data']['amount'] ?? $details['amount'] ?? 0;
|
||||||
|
|
||||||
|
if (!$qrOrderId) {
|
||||||
|
Log::warning("Telepagos webhook: qr_order_id not found for cashin {$cashinId}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$telepagosQr = TelepagosQr::where('qr_order_id', $qrOrderId)->first();
|
||||||
|
|
||||||
|
if (!$telepagosQr) {
|
||||||
|
Log::warning("Telepagos webhook: QR {$qrOrderId} not found in database for cashin {$cashinId}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$compra = $telepagosQr->compra;
|
||||||
|
|
||||||
|
if (!$compra) {
|
||||||
|
Log::warning("Telepagos webhook: Purchase not found for QR {$qrOrderId}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalAmount = $compra->getTotalAmount();
|
||||||
|
|
||||||
|
// Depending on precision, we might need a looser comparison, but == should work for floats here
|
||||||
|
if ($amount != $totalAmount) {
|
||||||
|
Log::warning("Telepagos webhook: Amount mismatch. Cashin amount: {$amount}, Purchase amount: {$totalAmount}");
|
||||||
|
// In a real scenario, you might want to record a partial payment or flag it,
|
||||||
|
// but the instructions say to finalize ONLY if the amount is correct.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the TelepagosPayment and finalize the purchase in a transaction
|
||||||
|
$paymentData = [
|
||||||
|
'compra_id' => $compra->id,
|
||||||
|
'cuit_buyer' => $details['data']['cuit_buyer'] ?? $details['cuit_buyer'] ?? null,
|
||||||
|
'cvu_buyer' => $details['data']['cvu_buyer'] ?? $details['cvu_buyer'] ?? null,
|
||||||
|
'amount' => $amount,
|
||||||
|
'concept' => $details['data']['concept'] ?? $details['concept'] ?? null,
|
||||||
|
'operation' => $details['data']['operation'] ?? $details['operation'] ?? null,
|
||||||
|
'operation_id' => $details['data']['operation_id'] ?? $details['operation_id'] ?? null,
|
||||||
|
'transaction_id' => $details['data']['transaction_id'] ?? $details['transaction_id'] ?? null,
|
||||||
|
'qr_order_id' => $qrOrderId,
|
||||||
|
'link_id' => $details['data']['link_id'] ?? $details['link_id'] ?? null,
|
||||||
|
];
|
||||||
|
|
||||||
|
\Illuminate\Support\Facades\DB::transaction(function () use ($compra, $paymentData) {
|
||||||
|
TelepagosPayment::create($paymentData);
|
||||||
|
$compra->finalize();
|
||||||
|
});
|
||||||
|
|
||||||
|
Log::info("Telepagos webhook: Successfully processed cashin {$cashinId} for purchase {$compra->id}");
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Log::error("Telepagos webhook error: " . $e->getMessage());
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,3 +17,5 @@ Route::group(['prefix' => '{tenant_code}/integrations'], function () {
|
|||||||
Route::get('/{integration_code}', [TenantIntegrationController::class, 'show']);
|
Route::get('/{integration_code}', [TenantIntegrationController::class, 'show']);
|
||||||
Route::post('/{integration_code}', [TenantIntegrationController::class, 'store']);
|
Route::post('/{integration_code}', [TenantIntegrationController::class, 'store']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::post('webhooks/telepagos/{tenant_codigo}', [\App\Domains\Integration\Controllers\TelepagosWebhookController::class, 'handle']);
|
||||||
|
|||||||
@@ -57,4 +57,33 @@ class Purchase extends Model
|
|||||||
{
|
{
|
||||||
return $this->hasMany(PurchaseItem::class, 'compra_id');
|
return $this->hasMany(PurchaseItem::class, 'compra_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasOne<TelepagosQr, $this>
|
||||||
|
*/
|
||||||
|
public function telepagosQr()
|
||||||
|
{
|
||||||
|
return $this->hasOne(TelepagosQr::class, 'compra_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return HasMany<TelepagosPayment, $this>
|
||||||
|
*/
|
||||||
|
public function telepagosPayments(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(TelepagosPayment::class, 'compra_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalAmount(): float
|
||||||
|
{
|
||||||
|
return (float) $this->items()->sum('total');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function finalize(): void
|
||||||
|
{
|
||||||
|
$this->update([
|
||||||
|
'status' => 'pagado', // Or the equivalent final status in your system
|
||||||
|
'payment_status' => 'paid',
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
app/Domains/Purchase/Models/TelepagosPayment.php
Normal file
43
app/Domains/Purchase/Models/TelepagosPayment.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Purchase\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
#[Fillable([
|
||||||
|
'compra_id',
|
||||||
|
'cuit_buyer',
|
||||||
|
'cvu_buyer',
|
||||||
|
'amount',
|
||||||
|
'concept',
|
||||||
|
'operation',
|
||||||
|
'operation_id',
|
||||||
|
'transaction_id',
|
||||||
|
'qr_order_id',
|
||||||
|
'link_id',
|
||||||
|
])]
|
||||||
|
class TelepagosPayment extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'telepagos_payments';
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'compra_id' => 'integer',
|
||||||
|
'amount' => 'decimal:2',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BelongsTo<Purchase, $this>
|
||||||
|
*/
|
||||||
|
public function compra(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Purchase::class, 'compra_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
35
app/Domains/Purchase/Models/TelepagosQr.php
Normal file
35
app/Domains/Purchase/Models/TelepagosQr.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Purchase\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
#[Fillable([
|
||||||
|
'compra_id',
|
||||||
|
'qr_order_id',
|
||||||
|
'qr_code',
|
||||||
|
])]
|
||||||
|
class TelepagosQr extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'telepagos_qr';
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'compra_id' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BelongsTo<Purchase, $this>
|
||||||
|
*/
|
||||||
|
public function compra(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Purchase::class, 'compra_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('telepagos_payments', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('compra_id')->constrained('compras')->onDelete('cascade');
|
||||||
|
$table->string('cuit_buyer')->nullable();
|
||||||
|
$table->string('cvu_buyer')->nullable();
|
||||||
|
$table->decimal('amount', 10, 2);
|
||||||
|
$table->string('concept')->nullable();
|
||||||
|
$table->string('operation')->nullable();
|
||||||
|
$table->string('operation_id')->nullable();
|
||||||
|
$table->string('transaction_id')->nullable();
|
||||||
|
$table->string('qr_order_id')->nullable();
|
||||||
|
$table->string('link_id')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('telepagos_payments');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('telepagos_qr', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('compra_id')->constrained('compras')->onDelete('cascade');
|
||||||
|
$table->string('qr_order_id');
|
||||||
|
$table->text('qr_code');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('telepagos_qr');
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user