'invoice.payment_succeeded', 'data' => [ 'object' => array_replace_recursive([ 'id' => 'in_test_123', 'customer' => $user->stripe_id, 'subtotal' => 4900, 'tax' => 931, 'total' => 5831, 'currency' => 'eur', 'customer_name' => 'Alpha GmbH', 'customer_address' => [ 'line1' => 'Beispielweg 1', 'line2' => null, 'postal_code' => '10115', 'city' => 'Berlin', 'country' => 'DE', ], ], $overrides), ], ]; } test('a paid stripe invoice is mirrored into the STR circle', function () { $user = User::factory()->create(['stripe_id' => 'cus_test_1']); app(ProcessStripeWebhook::class)->handle(new WebhookReceived(stripeInvoicePayload($user))); $invoice = Invoice::sole(); expect($invoice->number)->toBe('STR-00001'); expect($invoice->user_id)->toBe($user->id); expect($invoice->status)->toBe(InvoiceStatus::Paid); expect($invoice->amount_cents)->toBe(4900); expect($invoice->tax_cents)->toBe(931); expect($invoice->total_cents)->toBe(5831); expect($invoice->stripe_invoice_id)->toBe('in_test_123'); expect($invoice->invoiceBillingAddress->city)->toBe('Berlin'); expect($invoice->invoiceBillingAddress->name)->toBe('Alpha GmbH'); }); test('duplicate webhook deliveries do not create a second invoice', function () { $user = User::factory()->create(['stripe_id' => 'cus_test_1']); $listener = app(ProcessStripeWebhook::class); $listener->handle(new WebhookReceived(stripeInvoicePayload($user))); $listener->handle(new WebhookReceived(stripeInvoicePayload($user))); expect(Invoice::count())->toBe(1); }); test('the snapshot falls back to the local billing address including vat id', function () { $user = User::factory()->create(['stripe_id' => 'cus_test_1']); BillingAddress::factory()->create([ 'user_id' => $user->id, 'name' => 'Lokal GmbH', 'country_code' => 'AT', 'vat_id' => 'ATU12345678', ]); app(ProcessStripeWebhook::class)->handle(new WebhookReceived(stripeInvoicePayload($user, [ 'customer_name' => null, 'customer_address' => null, ]))); $snapshot = Invoice::sole()->invoiceBillingAddress; expect($snapshot->name)->toBe('Lokal GmbH'); expect($snapshot->country_code)->toBe('AT'); expect($snapshot->vat_id)->toBe('ATU12345678'); }); test('an unknown stripe customer is skipped without an invoice', function () { User::factory()->create(['stripe_id' => 'cus_other']); app(ProcessStripeWebhook::class)->handle(new WebhookReceived(stripeInvoicePayload( new User(['name' => 'Fremd']), ['customer' => 'cus_unknown'], ))); expect(Invoice::count())->toBe(0); }); test('checkout completion marks the referenced single purchase as paid', function () { $purchase = SinglePurchase::factory()->create(); app(ProcessStripeWebhook::class)->handle(new WebhookReceived([ 'type' => 'checkout.session.completed', 'data' => [ 'object' => [ 'id' => 'cs_test_1', 'payment_intent' => 'pi_test_1', 'metadata' => ['single_purchase_id' => (string) $purchase->id], ], ], ])); $fresh = $purchase->fresh(); expect($fresh->status)->toBe(SinglePurchaseStatus::Paid); expect($fresh->paid_at)->not->toBeNull(); expect($fresh->stripe_checkout_session_id)->toBe('cs_test_1'); expect($fresh->stripe_payment_intent_id)->toBe('pi_test_1'); }); test('an already paid purchase is not touched again', function () { $purchase = SinglePurchase::factory()->paid()->create([ 'stripe_payment_intent_id' => 'pi_original', ]); app(ProcessStripeWebhook::class)->handle(new WebhookReceived([ 'type' => 'checkout.session.completed', 'data' => [ 'object' => [ 'id' => 'cs_test_2', 'payment_intent' => 'pi_new', 'metadata' => ['single_purchase_id' => (string) $purchase->id], ], ], ])); expect($purchase->fresh()->stripe_payment_intent_id)->toBe('pi_original'); }); test('the listener is wired to the cashier webhook event', function () { Event::fake(); Event::assertListening( WebhookReceived::class, ProcessStripeWebhook::class, ); });