feat(integrations): resolve payment context from events
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Domains\Commerce\Purchase\Controllers;
|
||||
|
||||
use App\Shared\Integration\Services\TelepagosIntegrationService;
|
||||
use App\Domains\Commerce\Purchase\Models\Purchase;
|
||||
use App\Domains\Commerce\Purchase\Requests\PaymentIntentRequest;
|
||||
use App\Domains\Commerce\Purchase\Requests\StartCheckoutRequest;
|
||||
@@ -13,6 +12,7 @@ use App\Domains\Commerce\Purchase\Services\CheckoutService;
|
||||
use App\Domains\Commerce\Purchase\Services\PurchaseStateGuard;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Shared\Integration\Services\TelepagosIntegrationService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -151,11 +151,14 @@ class PurchaseController extends Controller
|
||||
}
|
||||
|
||||
$compra->refresh();
|
||||
$compra->loadMissing('event');
|
||||
$totalAmount = (float) $compra->total;
|
||||
|
||||
if ($method === 'transfer') {
|
||||
$telepagosService = new TelepagosIntegrationService;
|
||||
$telepagosService->forTenant($tenant->codigo);
|
||||
$compra->event
|
||||
? $telepagosService->forEvent($compra->event)
|
||||
: $telepagosService->forTenant($tenant->codigo);
|
||||
|
||||
try {
|
||||
$accountInfo = $telepagosService->getAccountInfo();
|
||||
@@ -191,7 +194,9 @@ class PurchaseController extends Controller
|
||||
|
||||
if ($method === 'qr') {
|
||||
$telepagosService = new TelepagosIntegrationService;
|
||||
$telepagosService->forTenant($tenant->codigo);
|
||||
$compra->event
|
||||
? $telepagosService->forEvent($compra->event)
|
||||
: $telepagosService->forTenant($tenant->codigo);
|
||||
|
||||
try {
|
||||
Log::channel('telepagos')->info('Generating Telepagos QR.', [
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
namespace App\Shared\Integration\Services;
|
||||
|
||||
use App\Domains\Core\Client\Models\Client;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticketing\Event\Models\Event;
|
||||
use App\Shared\Integration\Models\AdminWebsiteTypeIntegration;
|
||||
use App\Shared\Integration\Models\ClientIntegration;
|
||||
use App\Shared\Integration\Models\Integration;
|
||||
use App\Shared\Integration\Models\IntegrationInstance;
|
||||
use App\Shared\Integration\Models\AdminWebsiteTypeIntegration;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use Exception;
|
||||
use Illuminate\Http\Client\PendingRequest;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
@@ -86,6 +87,17 @@ abstract class BaseIntegrationService
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function forEvent(Event $event): self
|
||||
{
|
||||
$event->loadMissing(['client', 'tenant.client']);
|
||||
$this->tenant = $event->tenant;
|
||||
$this->tenantCode = $event->tenant_code;
|
||||
$this->clientContext = $event->effectiveClient();
|
||||
$this->loadIntegration();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the integration definition and its effective instance configuration.
|
||||
*
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
namespace App\Shared\Integration\Services;
|
||||
|
||||
use App\Domains\Core\Client\Models\Client;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Core\Tenant\Models\AdminWebsiteType;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticketing\Event\Models\Event;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Mail\Factory as MailFactory;
|
||||
use Illuminate\Contracts\Mail\Mailer;
|
||||
@@ -39,14 +40,7 @@ class MailService extends BaseIntegrationService
|
||||
public function forTenant(string $tenantCode): self
|
||||
{
|
||||
parent::forTenant($tenantCode);
|
||||
|
||||
if ($this->integrationInstance) {
|
||||
$this->mailer = $this->resolveMailer();
|
||||
$this->usesInstanceMailer = true;
|
||||
} else {
|
||||
$this->mailer = $this->mailFactory->mailer();
|
||||
$this->usesInstanceMailer = false;
|
||||
}
|
||||
$this->configureMailer();
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -55,16 +49,30 @@ class MailService extends BaseIntegrationService
|
||||
{
|
||||
parent::forClient($client);
|
||||
$this->tenant = $this->clientContext?->tenants()->first();
|
||||
$this->configureMailer();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function forEvent(Event $event): self
|
||||
{
|
||||
parent::forEvent($event);
|
||||
$this->configureMailer();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function configureMailer(): void
|
||||
{
|
||||
if ($this->integrationInstance) {
|
||||
$this->mailer = $this->resolveMailer();
|
||||
$this->usesInstanceMailer = true;
|
||||
} else {
|
||||
$this->mailer = $this->mailFactory->mailer();
|
||||
$this->usesInstanceMailer = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return $this;
|
||||
$this->mailer = $this->mailFactory->mailer();
|
||||
$this->usesInstanceMailer = false;
|
||||
}
|
||||
|
||||
public function getHeaders(): array
|
||||
@@ -83,7 +91,7 @@ class MailService extends BaseIntegrationService
|
||||
array $attachments = [],
|
||||
): void {
|
||||
if (! $this->mailer || ! $this->tenant) {
|
||||
throw new Exception('MailService no está configurado. Llamá a forTenant() o forClient() primero.');
|
||||
throw new Exception('MailService no está configurado. Llamá a forTenant(), forClient() o forEvent() primero.');
|
||||
}
|
||||
|
||||
$brand ??= $this->tenant;
|
||||
@@ -160,7 +168,7 @@ class MailService extends BaseIntegrationService
|
||||
public function onSetup(): void
|
||||
{
|
||||
if (! $this->mailer || ! $this->clientContext) {
|
||||
throw new Exception('MailService no está configurado. Llamá a forTenant() o forClient() primero.');
|
||||
throw new Exception('MailService no está configurado. Llamá a forTenant(), forClient() o forEvent() primero.');
|
||||
}
|
||||
|
||||
$recipient = $this->getIntegrationSetting('MAIL_FROM_ADDRESS');
|
||||
|
||||
@@ -46,7 +46,7 @@ class TelepagosIntegrationService extends BaseIntegrationService
|
||||
public function getToken(): string
|
||||
{
|
||||
if (! $this->integrationInstance) {
|
||||
throw new Exception('Client integration is not loaded. Call forTenant() or forClient() first.');
|
||||
throw new Exception('Client integration is not loaded. Call forTenant(), forClient(), or forEvent() first.');
|
||||
}
|
||||
|
||||
$cacheKey = $this->integrationInstance->tokenCacheKey();
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
namespace App\Shared\Integration\Services;
|
||||
|
||||
use App\Domains\Core\Client\Models\Client;
|
||||
use App\Domains\Commerce\Purchase\Models\Purchase;
|
||||
use App\Domains\Commerce\Purchase\Models\TelepagosPayment;
|
||||
use App\Domains\Commerce\Purchase\Models\TelepagosQr;
|
||||
use App\Domains\Commerce\Purchase\Services\CheckoutService;
|
||||
use App\Domains\Commerce\Purchase\Services\DniDistanceService;
|
||||
use App\Domains\Core\Client\Models\Client;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
@@ -75,9 +75,30 @@ class TelepagosWebhookService
|
||||
|
||||
$dni = substr($cuit, 2, -1);
|
||||
|
||||
$tenantCodes = $client->tenants()->pluck('codigo');
|
||||
$eligiblePurchases = Purchase::query()
|
||||
->whereIn('tenant_codigo', $tenantCodes)
|
||||
->where(function (Builder $purchases) use ($client): void {
|
||||
$purchases
|
||||
->whereHas('event', function (Builder $events) use ($client): void {
|
||||
$events
|
||||
->where('client_id', $client->id)
|
||||
->orWhere(function (Builder $fallback) use ($client): void {
|
||||
$fallback
|
||||
->whereNull('client_id')
|
||||
->whereHas(
|
||||
'tenant',
|
||||
fn (Builder $tenants): Builder => $tenants->where('client_id', $client->id),
|
||||
);
|
||||
});
|
||||
})
|
||||
->orWhere(function (Builder $legacy) use ($client): void {
|
||||
$legacy
|
||||
->whereNull('event_id')
|
||||
->whereHas(
|
||||
'tenant',
|
||||
fn (Builder $tenants): Builder => $tenants->where('client_id', $client->id),
|
||||
);
|
||||
});
|
||||
})
|
||||
->whereIn('status', [
|
||||
Purchase::STATUS_CREATED,
|
||||
Purchase::STATUS_PENDING_PAYMENT,
|
||||
@@ -170,7 +191,7 @@ class TelepagosWebhookService
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $client->tenants()->where('codigo', $compra->tenant_codigo)->exists()) {
|
||||
if (! $this->purchaseBelongsToClient($compra, $client)) {
|
||||
Log::channel('telepagos')->warning('Telepagos webhook: Purchase does not belong to client.', [
|
||||
'client_code' => $client->code,
|
||||
'cashin_id' => $cashinId,
|
||||
@@ -244,6 +265,16 @@ class TelepagosWebhookService
|
||||
return number_format((float) $amount, 2, '.', '');
|
||||
}
|
||||
|
||||
private function purchaseBelongsToClient(Purchase $purchase, Client $client): bool
|
||||
{
|
||||
$purchase->loadMissing(['event.client', 'event.tenant.client', 'tenant.client']);
|
||||
|
||||
$effectiveClient = $purchase->event?->effectiveClient()
|
||||
?? $purchase->tenant?->client;
|
||||
|
||||
return $effectiveClient?->is($client) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<Purchase> $eligiblePurchases
|
||||
* @return Collection<int, Purchase>
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
namespace App\Shared\Notification\Services;
|
||||
|
||||
use App\Domains\Commerce\Purchase\Models\Purchase;
|
||||
use App\Domains\Core\Auth\Models\ResetPasswordAttempt;
|
||||
use App\Domains\Core\Auth\Models\User;
|
||||
use App\Shared\Integration\Services\MailService;
|
||||
use App\Shared\Notification\Events\PasswordResetRequested;
|
||||
use App\Domains\Commerce\Purchase\Models\Purchase;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticketing\Ticket\Models\Ticket;
|
||||
use App\Domains\Ticketing\Ticket\Services\TicketPdfService;
|
||||
use App\Domains\Ticketing\Ticket\Services\TicketPresentationResolver;
|
||||
use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver;
|
||||
use App\Shared\Integration\Services\MailService;
|
||||
use App\Shared\Notification\Events\PasswordResetRequested;
|
||||
use Closure;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -158,7 +158,7 @@ class NotificationMailService
|
||||
$context = ['purchase_id' => $purchaseId];
|
||||
|
||||
$purchase = Purchase::query()
|
||||
->with(['tenant', 'user', 'items'])
|
||||
->with(['event', 'tenant', 'user', 'items'])
|
||||
->find($purchaseId);
|
||||
|
||||
if ($purchase === null) {
|
||||
@@ -197,8 +197,7 @@ class NotificationMailService
|
||||
'mime' => 'application/pdf',
|
||||
]];
|
||||
|
||||
$this->mailService
|
||||
->forTenant($purchase->tenant_codigo)
|
||||
$this->mailForPurchase($purchase)
|
||||
->send(
|
||||
$recipient,
|
||||
"Compra confirmada - Compra #{$purchase->getKey()}",
|
||||
@@ -286,7 +285,6 @@ class NotificationMailService
|
||||
$context,
|
||||
$recipient,
|
||||
function () use (
|
||||
$tenantCode,
|
||||
$purchase,
|
||||
$recipient,
|
||||
$previousDate,
|
||||
@@ -295,8 +293,7 @@ class NotificationMailService
|
||||
): array {
|
||||
$brand = $purchase->tenant;
|
||||
|
||||
$this->mailService
|
||||
->forTenant($tenantCode)
|
||||
$this->mailForPurchase($purchase)
|
||||
->send(
|
||||
$recipient,
|
||||
"Tu evento fue reprogramado - N° de Orden #{$purchase->getKey()}",
|
||||
@@ -387,7 +384,6 @@ class NotificationMailService
|
||||
$context,
|
||||
$recipient,
|
||||
function () use (
|
||||
$tenantCode,
|
||||
$purchase,
|
||||
$recipient,
|
||||
$date,
|
||||
@@ -396,8 +392,7 @@ class NotificationMailService
|
||||
): array {
|
||||
$brand = $purchase->tenant;
|
||||
|
||||
$this->mailService
|
||||
->forTenant($tenantCode)
|
||||
$this->mailForPurchase($purchase)
|
||||
->send(
|
||||
$recipient,
|
||||
"Una fecha de tu evento fue suspendida - N° de Orden #{$purchase->getKey()}",
|
||||
@@ -421,10 +416,17 @@ class NotificationMailService
|
||||
{
|
||||
return Purchase::query()
|
||||
->where('tenant_codigo', $tenantCode)
|
||||
->with(['tenant', 'user'])
|
||||
->with(['event', 'tenant', 'user'])
|
||||
->find($purchaseId);
|
||||
}
|
||||
|
||||
private function mailForPurchase(Purchase $purchase): MailService
|
||||
{
|
||||
return $purchase->event
|
||||
? $this->mailService->forEvent($purchase->event)
|
||||
: $this->mailService->forTenant($purchase->tenant_codigo);
|
||||
}
|
||||
|
||||
private function recipientFor(Purchase $purchase): string
|
||||
{
|
||||
return (string) ($purchase->email ?: $purchase->user?->email);
|
||||
|
||||
@@ -2,20 +2,22 @@
|
||||
|
||||
namespace Tests\Feature\Integration;
|
||||
|
||||
use App\Shared\Attachable\Enums\AttachmentType;
|
||||
use App\Shared\Attachable\Models\Attachment;
|
||||
use App\Domains\Core\Auth\Models\User;
|
||||
use App\Domains\Commerce\Cart\Models\Cart;
|
||||
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Commerce\Catalog\Models\Category;
|
||||
use App\Domains\Commerce\Catalog\Models\Inventory;
|
||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||
use App\Shared\Integration\Models\Integration;
|
||||
use App\Domains\Commerce\Purchase\Models\Purchase;
|
||||
use App\Domains\Commerce\Purchase\Models\TelepagosPayment;
|
||||
use App\Domains\Commerce\Purchase\Services\CheckoutService;
|
||||
use App\Domains\Core\Auth\Models\User;
|
||||
use App\Domains\Core\Client\Models\Client;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticketing\Event\Models\Event;
|
||||
use App\Shared\Attachable\Enums\AttachmentType;
|
||||
use App\Shared\Attachable\Models\Attachment;
|
||||
use App\Shared\Integration\Models\Integration;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
@@ -227,6 +229,77 @@ class TelepagosWebhookTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_transfer_webhook_matches_the_client_configured_on_the_event(): void
|
||||
{
|
||||
$tenant = $this->createTenant('multi-event', 'Multi Event', 'multi-event.com.ar');
|
||||
$eventClient = Client::query()->create([
|
||||
'code' => 'event-client',
|
||||
'name' => 'Event Client',
|
||||
]);
|
||||
Integration::query()->create([
|
||||
'integration_code' => 'telepagos_homo',
|
||||
'name' => 'Telepagos',
|
||||
'url' => 'https://api.telepagos.com.ar',
|
||||
'integration_data_schema' => [
|
||||
'username' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
],
|
||||
]);
|
||||
$this->createClientIntegration([
|
||||
'client_id' => $eventClient->id,
|
||||
'integration_code' => 'telepagos_homo',
|
||||
'integration_data' => ['username' => 'event-user', 'password' => 'event-password'],
|
||||
]);
|
||||
$event = Event::query()->create([
|
||||
'client_id' => $eventClient->id,
|
||||
'tenant_code' => $tenant->codigo,
|
||||
'title' => 'Event with its own client',
|
||||
]);
|
||||
$tenant->update(['active_event_id' => $event->id]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$variant = $this->createVariantForTenant($tenant->codigo, 10, '50.00');
|
||||
$purchase = $this->createPendingTransferPurchase(
|
||||
$tenant,
|
||||
$user->id,
|
||||
$variant->id,
|
||||
1,
|
||||
'12345678',
|
||||
);
|
||||
|
||||
Http::fake([
|
||||
'https://api.telepagos.com.ar/v2/auth/token' => Http::response([
|
||||
'status' => 'ok',
|
||||
'token' => 'event-client-token',
|
||||
'expires_at' => now()->addHour()->toIso8601String(),
|
||||
]),
|
||||
'https://api.telepagos.com.ar/v2/payment/cashin/event-client-payment' => Http::response([
|
||||
'status' => 'ok',
|
||||
'data' => [
|
||||
'amount' => 50,
|
||||
'operation_id' => 1,
|
||||
'transaction_id' => 'tx-event-client',
|
||||
'buyer' => ['cuit' => '20123456789'],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->postJson('/api/webhooks/telepagos/event-client', [
|
||||
'id' => 'event-client-payment',
|
||||
])->assertOk()->assertJsonPath('status', 'success');
|
||||
|
||||
$this->assertSame($event->id, $purchase->event_id);
|
||||
$this->assertDatabaseHas('compras', [
|
||||
'id' => $purchase->id,
|
||||
'event_id' => $event->id,
|
||||
'status' => Purchase::STATUS_PAID,
|
||||
]);
|
||||
$this->assertDatabaseHas('telepagos_payments', [
|
||||
'compra_id' => $purchase->id,
|
||||
'transaction_id' => 'tx-event-client',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_transfer_webhook_buys_unlimited_inventory_without_reducing_real_stock(): void
|
||||
{
|
||||
$tenant = $this->createTenant('unlimited', 'Unlimited', 'unlimited.com.ar');
|
||||
|
||||
Reference in New Issue
Block a user