create(['email_verified_at' => now()]); } it('states the package, the term and every figure that will be charged', function () { $page = $this->actingAs(checkoutUser()) ->get(route('checkout', ['plan' => 'team', 'term' => 'monthly'])) ->assertOk(); $page->assertSee('Team') ->assertSee(__('order.term_monthly')) // Net, tax and gross: 179,00 net, 20 % VAT, 214,80 to pay. ->assertSee('179,00') ->assertSee('214,80') ->assertSee(__('checkout.total_today')) // And what comes after the first charge, which is not the same figure // whenever a setup fee is on the first one. Asserted in two pieces: the // formatter puts a non-breaking space before the sign, and building the // expected string by hand is a test that checks its own typing. ->assertSee(mb_substr(__('checkout.then_monthly', ['amount' => '']), 0, 6)); }); it('adds the setup fee to what is due today and to nothing after it', function () { Settings::set('company.setup_fee_cents', 9900); $page = $this->actingAs(checkoutUser()) ->get(route('checkout', ['plan' => 'team', 'term' => 'monthly'])) ->assertOk(); // 214,80 for the month plus 118,80 for the setup — both gross. $page->assertSee(__('checkout.setup_line')) // 214,80 for the month plus 118,80 for the setup. ->assertSee('333,60') // The recurring figure alone is what follows the first charge. ->assertSee('214,80'); expect(SetupFee::netCents())->toBe(9900); }); it('refuses to place the order without the terms, whatever the browser did', function () { // The button is disabled until the box is ticked, and disabling a button in // a browser refuses nothing. This is the rule. $this->actingAs(checkoutUser()) ->post(route('checkout.start'), ['plan' => 'team', 'term' => 'monthly']) ->assertSessionHasErrors('terms_accepted'); }); it('puts the acceptance under the total and over the button', function () { // The order it is read in: what you get, what it costs, what you agree to, // then the button. It used to be first, above the prices. $page = File::get(resource_path('views/livewire/checkout.blade.php')); expect(strpos($page, "__('checkout.total_today')")) ->toBeLessThan(strpos($page, "__('order.terms_label'")) ->and(strpos($page, "__('order.terms_label'")) ->toBeLessThan(strpos($page, "__('checkout.pay_cta')")); }); it('says what happens after the button, because the next screen is a stranger', function () { // Being handed to a payment provider is the moment a first-time buyer wants // to know what they are getting into. $this->actingAs(checkoutUser()) ->get(route('checkout', ['plan' => 'start'])) ->assertOk() ->assertSee(__('checkout.step_pay_title')) ->assertSee(__('checkout.step_build_title')) ->assertSee(__('checkout.step_credentials_title')) ->assertSee(__('checkout.stripe_note')); }); it('answers a package nobody sells with a sentence, not a stack trace', function () { // Reached from a retyped address, or from a package taken off sale between // the two pages. $this->actingAs(checkoutUser()) ->get(route('checkout', ['plan' => 'gibt-es-nicht'])) ->assertOk() ->assertSee(__('checkout.unknown_plan')); }); it('sends a customer who already has a contract to their billing page', function () { // A second checkout would build a second, empty cloud and bill for both. // The order page says so; a link straight into this page skips that. $user = checkoutUser(); $customer = Customer::factory()->create(['email' => $user->email, 'user_id' => $user->id]); Subscription::factory()->create(['customer_id' => $customer->id, 'status' => 'active']); $this->actingAs($user) ->get(route('checkout', ['plan' => 'team'])) ->assertOk() ->assertSee(__('order.already_customer')) ->assertDontSee(__('checkout.pay_cta')); }); it('is behind the same door as everything else', function () { // An unconfirmed address is not a billing detail to be caught at checkout: // the finished cloud's credentials are sent to it. $this->get(route('checkout', ['plan' => 'team']))->assertRedirect(); $this->actingAs(User::factory()->unverified()->create()) ->get(route('checkout', ['plan' => 'team'])) ->assertRedirect(route('verification.notice')); }); // ---- The mark in the mails ---- it('draws the mark in the mail header, not an empty orange tile', function () { // It shipped as a plain square: the ascent inside the mark lives in an SVG // the site loads, and a mail cannot — Gmail strips inline SVG, Outlook // renders with Word, and this layout carries no image by design. So the // shape is a character every mail font has. $html = (string) app(App\Services\Mail\MailPreviews::class)->make('verify-email')?->render(); // As the entity it is written with: a mail is markup, and ▲ is what // survives every client's encoding guesswork. expect($html)->toContain('▲') ->and($html)->toContain('#f97316') // And the whole name beside it, as the wordmark writes it. ->and($html)->toContain('CluPilot') ->and($html)->toContain('Cloud'); });