feat(payments): track transfer match candidates
This commit is contained in:
@@ -8,6 +8,8 @@ use App\Domains\Purchase\Models\TelepagosPayment;
|
||||
use App\Domains\Purchase\Models\TelepagosQr;
|
||||
use App\Domains\Purchase\Services\CheckoutService;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
@@ -41,7 +43,6 @@ class TelepagosWebhookService
|
||||
|
||||
$paymentData = [
|
||||
'compra_id' => null,
|
||||
'matched_purchase_ids' => null,
|
||||
'cuit_buyer' => $details['data']['buyer']['cuit'] ?? $details['buyer']['cuit'] ?? null,
|
||||
'cvu_buyer' => $details['data']['buyer']['cvu'] ?? $details['buyer']['cvu'] ?? null,
|
||||
'amount' => $amount,
|
||||
@@ -73,24 +74,52 @@ class TelepagosWebhookService
|
||||
$dni = substr($cuit, 2, -1);
|
||||
|
||||
$tenantCodes = $client->tenants()->pluck('codigo');
|
||||
$purchases = Purchase::whereIn('tenant_codigo', $tenantCodes)
|
||||
->where('transfer_payer_dni', $dni)
|
||||
$eligiblePurchases = Purchase::query()
|
||||
->whereIn('tenant_codigo', $tenantCodes)
|
||||
->whereIn('status', [
|
||||
Purchase::STATUS_CREATED,
|
||||
Purchase::STATUS_PENDING_PAYMENT,
|
||||
Purchase::STATUS_IN_REVIEW,
|
||||
])
|
||||
->where('payment_method', 'transfer')
|
||||
->where('payment_method', 'transfer');
|
||||
|
||||
$purchases = (clone $eligiblePurchases)
|
||||
->where('transfer_payer_dni', $dni)
|
||||
->where('total', $amount)
|
||||
->latest()
|
||||
->get();
|
||||
|
||||
$paymentData['matched_purchase_ids'] = $purchases->pluck('id')->all();
|
||||
$compra = $purchases->count() === 1 ? $purchases->first() : null;
|
||||
|
||||
if (! $compra) {
|
||||
if ($purchases->count() > 1) {
|
||||
TelepagosPayment::create($paymentData);
|
||||
$candidatePurchases = $this->findTransferCandidates(
|
||||
$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.', [
|
||||
@@ -209,4 +238,85 @@ class TelepagosWebhookService
|
||||
{
|
||||
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');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user