141 lines
6.0 KiB
PHP
141 lines
6.0 KiB
PHP
<?php
|
|
|
|
use App\Models\Customer;
|
|
use App\Models\Subscription;
|
|
use App\Models\User;
|
|
use App\Services\Billing\SetupFee;
|
|
use App\Support\Settings;
|
|
use Illuminate\Support\Facades\File;
|
|
|
|
/**
|
|
* The order, before it is placed.
|
|
*
|
|
* There was no such page. The order page posted straight to Stripe: the last
|
|
* thing a customer saw in this product's design was a grid of packages, and the
|
|
* next was somebody else's card form. What they were buying, what it costs in
|
|
* total, what happens afterwards and what they were agreeing to were never
|
|
* stated in one place — and the terms box sat at the TOP of the package list,
|
|
* as a condition of looking at prices.
|
|
*/
|
|
function checkoutUser(): User
|
|
{
|
|
return User::factory()->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');
|
|
});
|