Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Tests\Controller;
|
|
|
|
|
|
Add comprehensive test coverage for AttestationController, LegalController, AdminController, AccountController and AnalyticsEvent entity
- AttestationController: fix decodeAndVerifyHash to have max 3 returns, add 11 tests covering all routes (check, ventesRef, ventes) and all decodeAndVerifyHash branches (invalid base64, missing pipe, bad signature, bad JSON, valid hash with/without registered attestation), plus generateHash unit tests with unicode
- LegalController: add 6 tests for RGPD POST routes (rgpdAccess and rgpdDeletion) covering empty fields, data found, and no data found scenarios
- AdminController: add 10 tests for analytics page (all period filters + access denied) and orderTickets endpoint (single ticket PDF, multiple tickets ZIP, order not found, no tickets)
- AccountController: add 17 tests for downloadTicket (success/denied/404), resendTicket (success/denied/404), cancelTicket (success/denied/404), createAccreditation (staff/exposant/empty fields/no categories/invalid type), eventAttestation (with categories/billets/empty selection)
- AnalyticsEvent entity: new test file with 8 tests covering constructor defaults, all getters/setters, nullable fields, and fluent interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:41:18 +02:00
|
|
|
use App\Controller\AttestationController;
|
|
|
|
|
use App\Entity\Attestation;
|
|
|
|
|
use App\Entity\Event;
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
use App\Entity\Payout;
|
|
|
|
|
use App\Entity\User;
|
|
|
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
|
|
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
|
|
|
|
|
|
|
|
|
class AttestationControllerTest extends WebTestCase
|
|
|
|
|
{
|
|
|
|
|
public function testCheckWithValidPayout(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
|
|
|
|
|
$user = new User();
|
|
|
|
|
$user->setEmail('test-attest-'.uniqid().'@example.com');
|
|
|
|
|
$user->setFirstName('Test');
|
|
|
|
|
$user->setLastName('User');
|
|
|
|
|
$user->setPassword('$2y$13$hashed');
|
|
|
|
|
$em->persist($user);
|
|
|
|
|
|
|
|
|
|
$payout = new Payout();
|
|
|
|
|
$payout->setOrganizer($user);
|
|
|
|
|
$payout->setStripePayoutId('po_check_'.uniqid());
|
|
|
|
|
$payout->setStatus('paid');
|
|
|
|
|
$payout->setAmount(10000);
|
|
|
|
|
$payout->setCurrency('eur');
|
|
|
|
|
$em->persist($payout);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->request('GET', '/attestation/check/'.$payout->getStripePayoutId());
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertSelectorTextContains('body', 'Attestation authentique');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testCheckWithInvalidPayout(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$client->request('GET', '/attestation/check/po_fake_invalid');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertSelectorTextContains('body', 'Attestation introuvable');
|
|
|
|
|
}
|
Add comprehensive test coverage for AttestationController, LegalController, AdminController, AccountController and AnalyticsEvent entity
- AttestationController: fix decodeAndVerifyHash to have max 3 returns, add 11 tests covering all routes (check, ventesRef, ventes) and all decodeAndVerifyHash branches (invalid base64, missing pipe, bad signature, bad JSON, valid hash with/without registered attestation), plus generateHash unit tests with unicode
- LegalController: add 6 tests for RGPD POST routes (rgpdAccess and rgpdDeletion) covering empty fields, data found, and no data found scenarios
- AdminController: add 10 tests for analytics page (all period filters + access denied) and orderTickets endpoint (single ticket PDF, multiple tickets ZIP, order not found, no tickets)
- AccountController: add 17 tests for downloadTicket (success/denied/404), resendTicket (success/denied/404), cancelTicket (success/denied/404), createAccreditation (staff/exposant/empty fields/no categories/invalid type), eventAttestation (with categories/billets/empty selection)
- AnalyticsEvent entity: new test file with 8 tests covering constructor defaults, all getters/setters, nullable fields, and fluent interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:41:18 +02:00
|
|
|
|
|
|
|
|
public function testVentesRefWithValidReference(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
|
|
|
|
|
$user = new User();
|
|
|
|
|
$user->setEmail('test-vref-'.uniqid().'@example.com');
|
|
|
|
|
$user->setFirstName('Test');
|
|
|
|
|
$user->setLastName('User');
|
|
|
|
|
$user->setPassword('$2y$13$hashed');
|
|
|
|
|
$em->persist($user);
|
|
|
|
|
|
|
|
|
|
$event = new Event();
|
|
|
|
|
$event->setAccount($user);
|
|
|
|
|
$event->setTitle('Test Event');
|
|
|
|
|
$event->setStartAt(new \DateTimeImmutable('+1 day'));
|
|
|
|
|
$event->setEndAt(new \DateTimeImmutable('+2 days'));
|
|
|
|
|
$event->setAddress('123 Rue Test');
|
|
|
|
|
$event->setZipcode('75001');
|
|
|
|
|
$event->setCity('Paris');
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
|
|
|
|
|
$reference = 'REF-'.uniqid();
|
2026-04-01 19:34:37 +02:00
|
|
|
$payload = [
|
|
|
|
|
'ref' => $reference,
|
|
|
|
|
'event' => 'Test Event',
|
|
|
|
|
'organizer' => 'Test User',
|
|
|
|
|
'generatedAt' => '01/04/2026 10:00:00',
|
|
|
|
|
'totalSold' => 10,
|
|
|
|
|
'billets' => [],
|
|
|
|
|
'categories' => [],
|
|
|
|
|
];
|
Add comprehensive test coverage for AttestationController, LegalController, AdminController, AccountController and AnalyticsEvent entity
- AttestationController: fix decodeAndVerifyHash to have max 3 returns, add 11 tests covering all routes (check, ventesRef, ventes) and all decodeAndVerifyHash branches (invalid base64, missing pipe, bad signature, bad JSON, valid hash with/without registered attestation), plus generateHash unit tests with unicode
- LegalController: add 6 tests for RGPD POST routes (rgpdAccess and rgpdDeletion) covering empty fields, data found, and no data found scenarios
- AdminController: add 10 tests for analytics page (all period filters + access denied) and orderTickets endpoint (single ticket PDF, multiple tickets ZIP, order not found, no tickets)
- AccountController: add 17 tests for downloadTicket (success/denied/404), resendTicket (success/denied/404), cancelTicket (success/denied/404), createAccreditation (staff/exposant/empty fields/no categories/invalid type), eventAttestation (with categories/billets/empty selection)
- AnalyticsEvent entity: new test file with 8 tests covering constructor defaults, all getters/setters, nullable fields, and fluent interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:41:18 +02:00
|
|
|
|
|
|
|
|
$attestation = new Attestation($reference, 'sig_'.uniqid(), $event, $user, 10, $payload);
|
|
|
|
|
$em->persist($attestation);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$client->request('GET', '/attestation/ventes/r/'.$reference);
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testVentesRefWithInvalidReference(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$client->request('GET', '/attestation/ventes/r/INVALID_REF');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertSelectorTextContains('body', 'introuvable');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testVentesWithValidHash(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$appSecret = static::getContainer()->getParameter('kernel.secret');
|
|
|
|
|
|
|
|
|
|
$data = ['event' => 'Test Event', 'total' => 5000];
|
|
|
|
|
$hash = AttestationController::generateHash($data, $appSecret);
|
|
|
|
|
|
|
|
|
|
$client->request('GET', '/attestation/ventes/'.$hash);
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testVentesWithValidHashAndRegisteredAttestation(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
|
$appSecret = static::getContainer()->getParameter('kernel.secret');
|
|
|
|
|
|
|
|
|
|
$user = new User();
|
|
|
|
|
$user->setEmail('test-vreg-'.uniqid().'@example.com');
|
|
|
|
|
$user->setFirstName('Test');
|
|
|
|
|
$user->setLastName('User');
|
|
|
|
|
$user->setPassword('$2y$13$hashed');
|
|
|
|
|
$em->persist($user);
|
|
|
|
|
|
|
|
|
|
$event = new Event();
|
|
|
|
|
$event->setAccount($user);
|
|
|
|
|
$event->setTitle('Test Event');
|
|
|
|
|
$event->setStartAt(new \DateTimeImmutable('+1 day'));
|
|
|
|
|
$event->setEndAt(new \DateTimeImmutable('+2 days'));
|
|
|
|
|
$event->setAddress('123 Rue Test');
|
|
|
|
|
$event->setZipcode('75001');
|
|
|
|
|
$event->setCity('Paris');
|
|
|
|
|
$em->persist($event);
|
|
|
|
|
|
2026-04-01 19:34:37 +02:00
|
|
|
$ref = 'REF-'.uniqid();
|
|
|
|
|
$data = [
|
|
|
|
|
'ref' => $ref,
|
|
|
|
|
'event' => 'Registered Event',
|
|
|
|
|
'organizer' => 'Test User',
|
|
|
|
|
'generatedAt' => '01/04/2026 10:00:00',
|
|
|
|
|
'totalSold' => 20,
|
|
|
|
|
'billets' => [],
|
|
|
|
|
'categories' => [],
|
|
|
|
|
];
|
Add comprehensive test coverage for AttestationController, LegalController, AdminController, AccountController and AnalyticsEvent entity
- AttestationController: fix decodeAndVerifyHash to have max 3 returns, add 11 tests covering all routes (check, ventesRef, ventes) and all decodeAndVerifyHash branches (invalid base64, missing pipe, bad signature, bad JSON, valid hash with/without registered attestation), plus generateHash unit tests with unicode
- LegalController: add 6 tests for RGPD POST routes (rgpdAccess and rgpdDeletion) covering empty fields, data found, and no data found scenarios
- AdminController: add 10 tests for analytics page (all period filters + access denied) and orderTickets endpoint (single ticket PDF, multiple tickets ZIP, order not found, no tickets)
- AccountController: add 17 tests for downloadTicket (success/denied/404), resendTicket (success/denied/404), cancelTicket (success/denied/404), createAccreditation (staff/exposant/empty fields/no categories/invalid type), eventAttestation (with categories/billets/empty selection)
- AnalyticsEvent entity: new test file with 8 tests covering constructor defaults, all getters/setters, nullable fields, and fluent interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:41:18 +02:00
|
|
|
$json = json_encode($data, \JSON_UNESCAPED_UNICODE);
|
|
|
|
|
$signatureHash = hash_hmac('sha256', $json, $appSecret);
|
|
|
|
|
|
2026-04-01 19:34:37 +02:00
|
|
|
$attestation = new Attestation($ref, $signatureHash, $event, $user, 20, $data);
|
Add comprehensive test coverage for AttestationController, LegalController, AdminController, AccountController and AnalyticsEvent entity
- AttestationController: fix decodeAndVerifyHash to have max 3 returns, add 11 tests covering all routes (check, ventesRef, ventes) and all decodeAndVerifyHash branches (invalid base64, missing pipe, bad signature, bad JSON, valid hash with/without registered attestation), plus generateHash unit tests with unicode
- LegalController: add 6 tests for RGPD POST routes (rgpdAccess and rgpdDeletion) covering empty fields, data found, and no data found scenarios
- AdminController: add 10 tests for analytics page (all period filters + access denied) and orderTickets endpoint (single ticket PDF, multiple tickets ZIP, order not found, no tickets)
- AccountController: add 17 tests for downloadTicket (success/denied/404), resendTicket (success/denied/404), cancelTicket (success/denied/404), createAccreditation (staff/exposant/empty fields/no categories/invalid type), eventAttestation (with categories/billets/empty selection)
- AnalyticsEvent entity: new test file with 8 tests covering constructor defaults, all getters/setters, nullable fields, and fluent interface
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:41:18 +02:00
|
|
|
$em->persist($attestation);
|
|
|
|
|
$em->flush();
|
|
|
|
|
|
|
|
|
|
$hash = AttestationController::generateHash($data, $appSecret);
|
|
|
|
|
$client->request('GET', '/attestation/ventes/'.$hash);
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testVentesWithInvalidBase64(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$client->request('GET', '/attestation/ventes/!!!invalid-base64!!!');
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertSelectorTextContains('body', 'introuvable');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testVentesWithMissingPipeSeparator(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$hash = rtrim(strtr(base64_encode('no-pipe-here'), '+/', '-_'), '=');
|
|
|
|
|
|
|
|
|
|
$client->request('GET', '/attestation/ventes/'.$hash);
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertSelectorTextContains('body', 'introuvable');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testVentesWithInvalidSignature(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$json = json_encode(['event' => 'Test'], \JSON_UNESCAPED_UNICODE);
|
|
|
|
|
$hash = rtrim(strtr(base64_encode('invalidsignature|'.$json), '+/', '-_'), '=');
|
|
|
|
|
|
|
|
|
|
$client->request('GET', '/attestation/ventes/'.$hash);
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertSelectorTextContains('body', 'introuvable');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testVentesWithInvalidJsonPayload(): void
|
|
|
|
|
{
|
|
|
|
|
$client = static::createClient();
|
|
|
|
|
$appSecret = static::getContainer()->getParameter('kernel.secret');
|
|
|
|
|
|
|
|
|
|
$invalidJson = '{invalid json}';
|
|
|
|
|
$signature = hash_hmac('sha256', $invalidJson, $appSecret);
|
|
|
|
|
$hash = rtrim(strtr(base64_encode($signature.'|'.$invalidJson), '+/', '-_'), '=');
|
|
|
|
|
|
|
|
|
|
$client->request('GET', '/attestation/ventes/'.$hash);
|
|
|
|
|
|
|
|
|
|
self::assertResponseIsSuccessful();
|
|
|
|
|
self::assertSelectorTextContains('body', 'introuvable');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testGenerateHash(): void
|
|
|
|
|
{
|
|
|
|
|
$data = ['key' => 'value', 'amount' => 1000];
|
|
|
|
|
$secret = 'test-secret';
|
|
|
|
|
|
|
|
|
|
$hash = AttestationController::generateHash($data, $secret);
|
|
|
|
|
|
|
|
|
|
self::assertNotEmpty($hash);
|
|
|
|
|
self::assertMatchesRegularExpression('/^[A-Za-z0-9\-_]+$/', $hash);
|
|
|
|
|
|
|
|
|
|
$decoded = base64_decode(strtr($hash, '-_', '+/'), true);
|
|
|
|
|
self::assertNotFalse($decoded);
|
|
|
|
|
|
|
|
|
|
$parts = explode('|', $decoded, 2);
|
|
|
|
|
self::assertCount(2, $parts);
|
|
|
|
|
|
|
|
|
|
[$signature, $jsonPayload] = $parts;
|
|
|
|
|
$expectedSignature = hash_hmac('sha256', $jsonPayload, $secret);
|
|
|
|
|
self::assertTrue(hash_equals($expectedSignature, $signature));
|
|
|
|
|
self::assertSame($data, json_decode($jsonPayload, true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testGenerateHashWithUnicodeData(): void
|
|
|
|
|
{
|
|
|
|
|
$data = ['name' => 'Événement été', 'city' => 'Zürich'];
|
|
|
|
|
$secret = 'test-secret';
|
|
|
|
|
|
|
|
|
|
$hash = AttestationController::generateHash($data, $secret);
|
|
|
|
|
$decoded = base64_decode(strtr($hash, '-_', '+/'), true);
|
|
|
|
|
$parts = explode('|', $decoded, 2);
|
|
|
|
|
|
|
|
|
|
self::assertStringContainsString('Événement été', $parts[1]);
|
|
|
|
|
self::assertStringContainsString('Zürich', $parts[1]);
|
|
|
|
|
}
|
Add payouts, PDF attestations, sub-accounts, and webhook improvements
Payout system:
- Create Payout entity (stripePayoutId, status, amount, currency, destination, arrivalDate)
- Webhook handles payout.created/updated/paid/failed/canceled with email notification
- Payout list in /mon-compte virements tab with status badges
- PDF attestation on paid payouts with email attachment
PDF attestation:
- dompdf with DejaVu Sans font, yellow-orange gradient background
- Orange centered title bar, E-Cosplay logo, emitter/beneficiary info blocks
- QR code linking to /attestation/check/{payoutId} for authenticity verification
- Public verification page: shows payout details if valid, error if altered
- Legal disclaimer and CGV reference
- Button visible only when status is paid, opens in new tab
Sub-accounts:
- Add parentOrganizer (self-referencing ManyToOne) and subAccountPermissions (JSON) to User
- Permissions: scanner (validate tickets), events (CRUD), tickets (free invitations)
- Create sub-account with random password, send email with credentials
- Edit page with name/email/permissions checkboxes
- Delete with confirmation
- hasPermission() helper method
Account improvements:
- Block entire page for unapproved organizers with validation pending message
- Display stripeStatus in Stripe Connect banners
- Remove test payout button
Webhook v2 Connect events:
- v2.core.account.created/updated/closed → update stripeStatus
- capability_status_updated → sync charges/payouts enabled from capabilities
- PayoutPdfService for reusable PDF generation
Migrations: stripeStatus, Payout table, sub-account fields, drop pdfPath
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 23:49:48 +01:00
|
|
|
}
|