feat(payments): track transfer match candidates
This commit is contained in:
@@ -8,6 +8,7 @@ PURCHASE_CHECKOUT_EXPIRATION_MINUTES=30
|
|||||||
PURCHASE_QR_EXPIRATION_MINUTES=15
|
PURCHASE_QR_EXPIRATION_MINUTES=15
|
||||||
PURCHASE_TELEPAGOS_EXPIRATION_MINUTES=30
|
PURCHASE_TELEPAGOS_EXPIRATION_MINUTES=30
|
||||||
PURCHASE_TRANSFER_EXPIRATION_MINUTES=1440
|
PURCHASE_TRANSFER_EXPIRATION_MINUTES=1440
|
||||||
|
PURCHASE_TRANSFER_CANDIDATE_AMOUNT_TOLERANCE_PERCENTAGE=5
|
||||||
STOCK_RESERVATION_EXPIRATION_MINUTES=30
|
STOCK_RESERVATION_EXPIRATION_MINUTES=30
|
||||||
FRONTEND_URLS=http://localhost:4200
|
FRONTEND_URLS=http://localhost:4200
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use App\Domains\Purchase\Models\TelepagosPayment;
|
|||||||
use App\Domains\Purchase\Models\TelepagosQr;
|
use App\Domains\Purchase\Models\TelepagosQr;
|
||||||
use App\Domains\Purchase\Services\CheckoutService;
|
use App\Domains\Purchase\Services\CheckoutService;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
@@ -41,7 +43,6 @@ class TelepagosWebhookService
|
|||||||
|
|
||||||
$paymentData = [
|
$paymentData = [
|
||||||
'compra_id' => null,
|
'compra_id' => null,
|
||||||
'matched_purchase_ids' => null,
|
|
||||||
'cuit_buyer' => $details['data']['buyer']['cuit'] ?? $details['buyer']['cuit'] ?? null,
|
'cuit_buyer' => $details['data']['buyer']['cuit'] ?? $details['buyer']['cuit'] ?? null,
|
||||||
'cvu_buyer' => $details['data']['buyer']['cvu'] ?? $details['buyer']['cvu'] ?? null,
|
'cvu_buyer' => $details['data']['buyer']['cvu'] ?? $details['buyer']['cvu'] ?? null,
|
||||||
'amount' => $amount,
|
'amount' => $amount,
|
||||||
@@ -73,24 +74,52 @@ class TelepagosWebhookService
|
|||||||
$dni = substr($cuit, 2, -1);
|
$dni = substr($cuit, 2, -1);
|
||||||
|
|
||||||
$tenantCodes = $client->tenants()->pluck('codigo');
|
$tenantCodes = $client->tenants()->pluck('codigo');
|
||||||
$purchases = Purchase::whereIn('tenant_codigo', $tenantCodes)
|
$eligiblePurchases = Purchase::query()
|
||||||
->where('transfer_payer_dni', $dni)
|
->whereIn('tenant_codigo', $tenantCodes)
|
||||||
->whereIn('status', [
|
->whereIn('status', [
|
||||||
Purchase::STATUS_CREATED,
|
Purchase::STATUS_CREATED,
|
||||||
Purchase::STATUS_PENDING_PAYMENT,
|
Purchase::STATUS_PENDING_PAYMENT,
|
||||||
Purchase::STATUS_IN_REVIEW,
|
Purchase::STATUS_IN_REVIEW,
|
||||||
])
|
])
|
||||||
->where('payment_method', 'transfer')
|
->where('payment_method', 'transfer');
|
||||||
|
|
||||||
|
$purchases = (clone $eligiblePurchases)
|
||||||
|
->where('transfer_payer_dni', $dni)
|
||||||
->where('total', $amount)
|
->where('total', $amount)
|
||||||
->latest()
|
->latest()
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$paymentData['matched_purchase_ids'] = $purchases->pluck('id')->all();
|
|
||||||
$compra = $purchases->count() === 1 ? $purchases->first() : null;
|
$compra = $purchases->count() === 1 ? $purchases->first() : null;
|
||||||
|
|
||||||
if (! $compra) {
|
if (! $compra) {
|
||||||
if ($purchases->count() > 1) {
|
$candidatePurchases = $this->findTransferCandidates(
|
||||||
TelepagosPayment::create($paymentData);
|
$eligiblePurchases,
|
||||||
|
$dni,
|
||||||
|
$amount,
|
||||||
|
);
|
||||||
|
if ($candidatePurchases->isNotEmpty()) {
|
||||||
|
$payment = $this->storeTransferCandidates(
|
||||||
|
$paymentData,
|
||||||
|
$candidatePurchases,
|
||||||
|
$dni,
|
||||||
|
$amount,
|
||||||
|
);
|
||||||
|
|
||||||
|
Log::channel('telepagos')->info('Telepagos webhook: Transfer payment candidates found.', [
|
||||||
|
'client_code' => $client->code,
|
||||||
|
'cashin_id' => $cashinId,
|
||||||
|
'telepagos_payment_id' => $payment->id,
|
||||||
|
'amount' => $amount,
|
||||||
|
'candidate_count' => $payment->candidates->count(),
|
||||||
|
'candidates' => $payment->candidates
|
||||||
|
->map(fn ($candidate): array => [
|
||||||
|
'purchase_id' => $candidate->compra_id,
|
||||||
|
'match_reason' => $candidate->match_reason,
|
||||||
|
'amount_difference' => $candidate->amount_difference,
|
||||||
|
'confidence' => $candidate->confidence,
|
||||||
|
])
|
||||||
|
->all(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log::channel('telepagos')->warning('Telepagos webhook: Expected exactly one matching purchase.', [
|
Log::channel('telepagos')->warning('Telepagos webhook: Expected exactly one matching purchase.', [
|
||||||
@@ -209,4 +238,85 @@ class TelepagosWebhookService
|
|||||||
{
|
{
|
||||||
return number_format((float) $amount, 2, '.', '');
|
return number_format((float) $amount, 2, '.', '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Builder<Purchase> $eligiblePurchases
|
||||||
|
* @return Collection<int, Purchase>
|
||||||
|
*/
|
||||||
|
private function findTransferCandidates(Builder $eligiblePurchases, string $dni, string $amount): Collection
|
||||||
|
{
|
||||||
|
$tolerancePercentage = max(
|
||||||
|
0,
|
||||||
|
(float) config('purchase.transfer_candidate_amount_tolerance_percentage', 5),
|
||||||
|
);
|
||||||
|
$numericAmount = (float) $amount;
|
||||||
|
$tolerance = $numericAmount * ($tolerancePercentage / 100);
|
||||||
|
$minimumAmount = $this->normalizeAmount(max(0, $numericAmount - $tolerance));
|
||||||
|
$maximumAmount = $this->normalizeAmount($numericAmount + $tolerance);
|
||||||
|
|
||||||
|
return (clone $eligiblePurchases)
|
||||||
|
->where(function ($query) use ($dni, $amount, $minimumAmount, $maximumAmount): void {
|
||||||
|
$query
|
||||||
|
->where(function ($query) use ($dni, $minimumAmount, $maximumAmount): void {
|
||||||
|
$query
|
||||||
|
->where('transfer_payer_dni', $dni)
|
||||||
|
->whereBetween('total', [$minimumAmount, $maximumAmount]);
|
||||||
|
})
|
||||||
|
->orWhere(function ($query) use ($dni, $amount): void {
|
||||||
|
$query
|
||||||
|
->where('total', $amount)
|
||||||
|
->where(function ($query) use ($dni): void {
|
||||||
|
$query
|
||||||
|
->whereNull('transfer_payer_dni')
|
||||||
|
->orWhere('transfer_payer_dni', '!=', $dni);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->latest()
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $paymentData
|
||||||
|
* @param Collection<int, Purchase> $candidatePurchases
|
||||||
|
*/
|
||||||
|
private function storeTransferCandidates(
|
||||||
|
array $paymentData,
|
||||||
|
Collection $candidatePurchases,
|
||||||
|
string $dni,
|
||||||
|
string $amount,
|
||||||
|
): TelepagosPayment {
|
||||||
|
return DB::transaction(function () use ($paymentData, $candidatePurchases, $dni, $amount): TelepagosPayment {
|
||||||
|
$payment = TelepagosPayment::create($paymentData);
|
||||||
|
|
||||||
|
$payment->candidates()->createMany(
|
||||||
|
$candidatePurchases
|
||||||
|
->map(function (Purchase $purchase) use ($dni, $amount): array {
|
||||||
|
$purchaseAmount = $this->normalizeAmount($purchase->total);
|
||||||
|
$dniMatches = $purchase->transfer_payer_dni === $dni;
|
||||||
|
$amountMatches = $purchaseAmount === $amount;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'compra_id' => $purchase->id,
|
||||||
|
'dni_matches' => $dniMatches,
|
||||||
|
'amount_matches' => $amountMatches,
|
||||||
|
'payment_amount' => $amount,
|
||||||
|
'purchase_amount' => $purchaseAmount,
|
||||||
|
'amount_difference' => $this->normalizeAmount(
|
||||||
|
abs((float) $purchaseAmount - (float) $amount),
|
||||||
|
),
|
||||||
|
'match_reason' => $amountMatches
|
||||||
|
? ($dniMatches ? 'ambiguous_exact_match' : 'exact_amount_different_dni')
|
||||||
|
: 'exact_dni_near_amount',
|
||||||
|
'confidence' => $amountMatches
|
||||||
|
? ($dniMatches ? 'exact' : 'medium')
|
||||||
|
: 'high',
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->all(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $payment->load('candidates');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'compra_id',
|
'compra_id',
|
||||||
'matched_purchase_ids',
|
|
||||||
'cuit_buyer',
|
'cuit_buyer',
|
||||||
'cvu_buyer',
|
'cvu_buyer',
|
||||||
'amount',
|
'amount',
|
||||||
@@ -30,7 +30,6 @@ class TelepagosPayment extends Model
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'compra_id' => 'integer',
|
'compra_id' => 'integer',
|
||||||
'matched_purchase_ids' => 'array',
|
|
||||||
'amount' => 'decimal:2',
|
'amount' => 'decimal:2',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -42,4 +41,10 @@ class TelepagosPayment extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Purchase::class, 'compra_id');
|
return $this->belongsTo(Purchase::class, 'compra_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<TelepagosPaymentCandidate, $this> */
|
||||||
|
public function candidates(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(TelepagosPaymentCandidate::class, 'telepagos_payment_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
app/Domains/Purchase/Models/TelepagosPaymentCandidate.php
Normal file
46
app/Domains/Purchase/Models/TelepagosPaymentCandidate.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Purchase\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
#[Fillable([
|
||||||
|
'telepagos_payment_id',
|
||||||
|
'compra_id',
|
||||||
|
'dni_matches',
|
||||||
|
'amount_matches',
|
||||||
|
'payment_amount',
|
||||||
|
'purchase_amount',
|
||||||
|
'amount_difference',
|
||||||
|
'match_reason',
|
||||||
|
'confidence',
|
||||||
|
])]
|
||||||
|
class TelepagosPaymentCandidate extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'telepagos_payment_candidates';
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'telepagos_payment_id' => 'integer',
|
||||||
|
'compra_id' => 'integer',
|
||||||
|
'dni_matches' => 'boolean',
|
||||||
|
'amount_matches' => 'boolean',
|
||||||
|
'payment_amount' => 'decimal:2',
|
||||||
|
'purchase_amount' => 'decimal:2',
|
||||||
|
'amount_difference' => 'decimal:2',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function payment(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(TelepagosPayment::class, 'telepagos_payment_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function purchase(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Purchase::class, 'compra_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,4 +8,9 @@ return [
|
|||||||
'telepagos' => (int) env('PURCHASE_TELEPAGOS_EXPIRATION_MINUTES', 30),
|
'telepagos' => (int) env('PURCHASE_TELEPAGOS_EXPIRATION_MINUTES', 30),
|
||||||
'transfer' => (int) env('PURCHASE_TRANSFER_EXPIRATION_MINUTES', 1440),
|
'transfer' => (int) env('PURCHASE_TRANSFER_EXPIRATION_MINUTES', 1440),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'transfer_candidate_amount_tolerance_percentage' => (float) env(
|
||||||
|
'PURCHASE_TRANSFER_CANDIDATE_AMOUNT_TOLERANCE_PERCENTAGE',
|
||||||
|
5,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('telepagos_payment_candidates', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('telepagos_payment_id')
|
||||||
|
->constrained('telepagos_payments')
|
||||||
|
->cascadeOnDelete();
|
||||||
|
$table->foreignId('compra_id')->constrained('compras')->cascadeOnDelete();
|
||||||
|
$table->boolean('dni_matches');
|
||||||
|
$table->boolean('amount_matches');
|
||||||
|
$table->decimal('payment_amount', 10, 2);
|
||||||
|
$table->decimal('purchase_amount', 10, 2);
|
||||||
|
$table->decimal('amount_difference', 10, 2);
|
||||||
|
$table->string('match_reason');
|
||||||
|
$table->string('confidence');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(
|
||||||
|
['telepagos_payment_id', 'compra_id'],
|
||||||
|
'telepagos_payment_candidate_unique',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->migrateExistingCandidates();
|
||||||
|
|
||||||
|
Schema::table('telepagos_payments', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('matched_purchase_ids');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('telepagos_payments', function (Blueprint $table) {
|
||||||
|
$table->json('matched_purchase_ids')->nullable()->after('compra_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('telepagos_payments')
|
||||||
|
->whereNull('compra_id')
|
||||||
|
->orderBy('id')
|
||||||
|
->each(function (object $payment): void {
|
||||||
|
$candidateIds = DB::table('telepagos_payment_candidates')
|
||||||
|
->where('telepagos_payment_id', $payment->id)
|
||||||
|
->pluck('compra_id')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($candidateIds !== []) {
|
||||||
|
DB::table('telepagos_payments')
|
||||||
|
->where('id', $payment->id)
|
||||||
|
->update(['matched_purchase_ids' => json_encode($candidateIds)]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::dropIfExists('telepagos_payment_candidates');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function migrateExistingCandidates(): void
|
||||||
|
{
|
||||||
|
DB::table('telepagos_payments')
|
||||||
|
->whereNull('compra_id')
|
||||||
|
->whereNotNull('matched_purchase_ids')
|
||||||
|
->orderBy('id')
|
||||||
|
->each(function (object $payment): void {
|
||||||
|
$candidateIds = json_decode((string) $payment->matched_purchase_ids, true);
|
||||||
|
|
||||||
|
if (! is_array($candidateIds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$paymentAmount = number_format((float) $payment->amount, 2, '.', '');
|
||||||
|
$payerDni = $payment->cuit_buyer
|
||||||
|
? substr((string) $payment->cuit_buyer, 2, -1)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
foreach ($candidateIds as $candidateId) {
|
||||||
|
$purchase = DB::table('compras')->find($candidateId);
|
||||||
|
|
||||||
|
if ($purchase === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$purchaseAmount = number_format((float) $purchase->total, 2, '.', '');
|
||||||
|
$dniMatches = $payerDni !== null && $purchase->transfer_payer_dni === $payerDni;
|
||||||
|
$amountMatches = $purchaseAmount === $paymentAmount;
|
||||||
|
|
||||||
|
DB::table('telepagos_payment_candidates')->insertOrIgnore([
|
||||||
|
'telepagos_payment_id' => $payment->id,
|
||||||
|
'compra_id' => $purchase->id,
|
||||||
|
'dni_matches' => $dniMatches,
|
||||||
|
'amount_matches' => $amountMatches,
|
||||||
|
'payment_amount' => $paymentAmount,
|
||||||
|
'purchase_amount' => $purchaseAmount,
|
||||||
|
'amount_difference' => number_format(
|
||||||
|
abs((float) $purchaseAmount - (float) $paymentAmount),
|
||||||
|
2,
|
||||||
|
'.',
|
||||||
|
'',
|
||||||
|
),
|
||||||
|
'match_reason' => $this->matchReason($dniMatches, $amountMatches),
|
||||||
|
'confidence' => $this->confidence($dniMatches, $amountMatches),
|
||||||
|
'created_at' => $payment->created_at,
|
||||||
|
'updated_at' => $payment->updated_at,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function matchReason(bool $dniMatches, bool $amountMatches): string
|
||||||
|
{
|
||||||
|
if ($dniMatches && $amountMatches) {
|
||||||
|
return 'ambiguous_exact_match';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dniMatches) {
|
||||||
|
return 'exact_dni_near_amount';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($amountMatches) {
|
||||||
|
return 'exact_amount_different_dni';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'legacy_candidate';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function confidence(bool $dniMatches, bool $amountMatches): string
|
||||||
|
{
|
||||||
|
if ($dniMatches && $amountMatches) {
|
||||||
|
return 'exact';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $dniMatches ? 'high' : 'medium';
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -325,8 +325,9 @@ class TelepagosWebhookTest extends TestCase
|
|||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
$this->assertEqualsCanonicalizing(
|
$this->assertEqualsCanonicalizing(
|
||||||
[$firstPurchase->id, $secondPurchase->id, $thirdPurchase->id],
|
[$firstPurchase->id, $secondPurchase->id, $thirdPurchase->id],
|
||||||
$payment->matched_purchase_ids,
|
$payment->candidates()->pluck('compra_id')->all(),
|
||||||
);
|
);
|
||||||
|
$this->assertSame(3, $payment->candidates()->where('match_reason', 'ambiguous_exact_match')->count());
|
||||||
$this->assertDatabaseHas('compras', [
|
$this->assertDatabaseHas('compras', [
|
||||||
'id' => $firstPurchase->id,
|
'id' => $firstPurchase->id,
|
||||||
'status' => Purchase::STATUS_PENDING_PAYMENT,
|
'status' => Purchase::STATUS_PENDING_PAYMENT,
|
||||||
@@ -341,6 +342,111 @@ class TelepagosWebhookTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_transfer_webhook_records_near_amount_only_with_exact_dni_or_exact_amount_with_different_dni(): void
|
||||||
|
{
|
||||||
|
config(['purchase.transfer_candidate_amount_tolerance_percentage' => 5]);
|
||||||
|
|
||||||
|
$tenant = $this->createTenant('candidates', 'Candidates', 'candidates.com.ar');
|
||||||
|
$this->configureTelepagosIntegration($tenant);
|
||||||
|
|
||||||
|
$nearAmountVariant = $this->createVariantForTenant('candidates', 10, '52.00', 'near');
|
||||||
|
$exactAmountVariant = $this->createVariantForTenant('candidates', 10, '50.00', 'exact');
|
||||||
|
$nearAmountDifferentDniVariant = $this->createVariantForTenant('candidates', 10, '51.00', 'near-other-dni');
|
||||||
|
$outsideRangeVariant = $this->createVariantForTenant('candidates', 10, '100.00', 'outside');
|
||||||
|
|
||||||
|
$nearAmountPurchase = $this->createPendingTransferPurchase(
|
||||||
|
$tenant,
|
||||||
|
User::factory()->create()->id,
|
||||||
|
$nearAmountVariant->id,
|
||||||
|
1,
|
||||||
|
'12345678',
|
||||||
|
);
|
||||||
|
$exactAmountDifferentDniPurchase = $this->createPendingTransferPurchase(
|
||||||
|
$tenant,
|
||||||
|
User::factory()->create()->id,
|
||||||
|
$exactAmountVariant->id,
|
||||||
|
1,
|
||||||
|
'87654321',
|
||||||
|
);
|
||||||
|
$nearAmountDifferentDniPurchase = $this->createPendingTransferPurchase(
|
||||||
|
$tenant,
|
||||||
|
User::factory()->create()->id,
|
||||||
|
$nearAmountDifferentDniVariant->id,
|
||||||
|
1,
|
||||||
|
'87654321',
|
||||||
|
);
|
||||||
|
$outsideRangePurchase = $this->createPendingTransferPurchase(
|
||||||
|
$tenant,
|
||||||
|
User::factory()->create()->id,
|
||||||
|
$outsideRangeVariant->id,
|
||||||
|
1,
|
||||||
|
'12345678',
|
||||||
|
);
|
||||||
|
|
||||||
|
Http::fake([
|
||||||
|
'https://api.telepagos.com.ar/v2/auth/token' => Http::response([
|
||||||
|
'status' => 'ok',
|
||||||
|
'token' => 'test-token',
|
||||||
|
'expires_at' => now()->addHour()->toIso8601String(),
|
||||||
|
]),
|
||||||
|
'https://api.telepagos.com.ar/v2/payment/cashin/candidates' => Http::response([
|
||||||
|
'status' => 'ok',
|
||||||
|
'data' => [
|
||||||
|
'amount' => 50,
|
||||||
|
'operation_id' => 1,
|
||||||
|
'transaction_id' => 'tx-candidates',
|
||||||
|
'buyer' => ['cuit' => '20123456789'],
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->postJson('/api/webhooks/telepagos/candidates', ['id' => 'candidates'])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('status', 'success');
|
||||||
|
|
||||||
|
$payment = TelepagosPayment::query()
|
||||||
|
->where('transaction_id', 'tx-candidates')
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$this->assertNull($payment->compra_id);
|
||||||
|
$this->assertEqualsCanonicalizing([
|
||||||
|
$nearAmountPurchase->id,
|
||||||
|
$exactAmountDifferentDniPurchase->id,
|
||||||
|
], $payment->candidates()->pluck('compra_id')->all());
|
||||||
|
$this->assertDatabaseHas('telepagos_payment_candidates', [
|
||||||
|
'telepagos_payment_id' => $payment->id,
|
||||||
|
'compra_id' => $nearAmountPurchase->id,
|
||||||
|
'dni_matches' => true,
|
||||||
|
'amount_matches' => false,
|
||||||
|
'payment_amount' => 50,
|
||||||
|
'purchase_amount' => 52,
|
||||||
|
'amount_difference' => 2,
|
||||||
|
'match_reason' => 'exact_dni_near_amount',
|
||||||
|
'confidence' => 'high',
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseHas('telepagos_payment_candidates', [
|
||||||
|
'telepagos_payment_id' => $payment->id,
|
||||||
|
'compra_id' => $exactAmountDifferentDniPurchase->id,
|
||||||
|
'dni_matches' => false,
|
||||||
|
'amount_matches' => true,
|
||||||
|
'payment_amount' => 50,
|
||||||
|
'purchase_amount' => 50,
|
||||||
|
'amount_difference' => 0,
|
||||||
|
'match_reason' => 'exact_amount_different_dni',
|
||||||
|
'confidence' => 'medium',
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseMissing('telepagos_payment_candidates', [
|
||||||
|
'telepagos_payment_id' => $payment->id,
|
||||||
|
'compra_id' => $nearAmountDifferentDniPurchase->id,
|
||||||
|
]);
|
||||||
|
$this->assertDatabaseMissing('telepagos_payment_candidates', [
|
||||||
|
'telepagos_payment_id' => $payment->id,
|
||||||
|
'compra_id' => $outsideRangePurchase->id,
|
||||||
|
]);
|
||||||
|
$this->assertSame(Purchase::STATUS_PENDING_PAYMENT, $nearAmountPurchase->fresh()->status);
|
||||||
|
$this->assertSame(Purchase::STATUS_PENDING_PAYMENT, $exactAmountDifferentDniPurchase->fresh()->status);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_webhook_confirms_a_purchase_with_tickets_enabled(): void
|
public function test_webhook_confirms_a_purchase_with_tickets_enabled(): void
|
||||||
{
|
{
|
||||||
$tenant = $this->createTenant('expired-ticket', 'Expired Ticket', 'expired-ticket.com.ar');
|
$tenant = $this->createTenant('expired-ticket', 'Expired Ticket', 'expired-ticket.com.ar');
|
||||||
|
|||||||
Reference in New Issue
Block a user