diff --git a/src/resources/views/documents/receipt.blade.php b/src/resources/views/documents/receipt.blade.php index 8c3d5684..ce9e9e2c 100644 --- a/src/resources/views/documents/receipt.blade.php +++ b/src/resources/views/documents/receipt.blade.php @@ -1,66 +1,66 @@

{{ $title }}

{!! $customer['customer'] !!} - {{ __('documents.account-id') }} {{ $customer['wallet_id'] }}
+ {{--{{ __('documents.account-id') }} {{ $customer['wallet_id'] }}
--}} {{ __('documents.customer-no') }} {{ $customer['id'] }}
@foreach ($items as $item) @endforeach @if ($vat) @endif
{{ __('documents.date') }} {{ __('documents.description') }} {{ __('documents.amount') }}
{{ $item['date'] }} {{ $item['description'] }} {{ $item['amount'] }}
{{ __('documents.subtotal') }} {{ $subTotal }}
{{ __('documents.vat', ['rate' => $vatRate]) }} {{ $totalVat }}
{{ __('documents.total') }} {{ $total }}
diff --git a/src/resources/vue/User/Profile.vue b/src/resources/vue/User/Profile.vue index bb92d8f8..d966f921 100644 --- a/src/resources/vue/User/Profile.vue +++ b/src/resources/vue/User/Profile.vue @@ -1,113 +1,121 @@ diff --git a/src/tests/Browser/UserProfileTest.php b/src/tests/Browser/UserProfileTest.php index 22e25b9d..bbd5d2a2 100644 --- a/src/tests/Browser/UserProfileTest.php +++ b/src/tests/Browser/UserProfileTest.php @@ -1,191 +1,194 @@ 'John', 'last_name' => 'Doe', 'currency' => 'USD', 'country' => 'US', 'billing_address' => "601 13th Street NW\nSuite 900 South\nWashington, DC 20005", 'external_email' => 'john.doe.external@gmail.com', 'phone' => '+1 509-248-1111', 'organization' => 'Kolab Developers', ]; /** * {@inheritDoc} */ public function setUp(): void { parent::setUp(); User::where('email', 'john@kolab.org')->first()->setSettings($this->profile); $this->deleteTestUser('profile-delete@kolabnow.com'); } /** * {@inheritDoc} */ public function tearDown(): void { User::where('email', 'john@kolab.org')->first()->setSettings($this->profile); $this->deleteTestUser('profile-delete@kolabnow.com'); parent::tearDown(); } /** * Test profile page (unauthenticated) */ public function testProfileUnauth(): void { // Test that the page requires authentication $this->browse(function (Browser $browser) { $browser->visit('/profile')->on(new Home()); }); } /** * Test profile page */ public function testProfile(): void { $this->browse(function (Browser $browser) { $browser->visit(new Home()) ->submitLogon('john@kolab.org', 'simple123', true) ->on(new Dashboard()) ->assertSeeIn('@links .link-profile', 'Your profile') ->click('@links .link-profile') ->on(new UserProfile()) ->assertSeeIn('#user-profile .button-delete', 'Delete account') ->whenAvailable('@form', function (Browser $browser) { + $user = User::where('email', 'john@kolab.org')->first(); // Assert form content - $browser->assertFocused('div.row:nth-child(1) input') - ->assertSeeIn('div.row:nth-child(1) label', 'First name') - ->assertValue('div.row:nth-child(1) input[type=text]', $this->profile['first_name']) - ->assertSeeIn('div.row:nth-child(2) label', 'Last name') - ->assertValue('div.row:nth-child(2) input[type=text]', $this->profile['last_name']) - ->assertSeeIn('div.row:nth-child(3) label', 'Organization') - ->assertValue('div.row:nth-child(3) input[type=text]', $this->profile['organization']) - ->assertSeeIn('div.row:nth-child(4) label', 'Phone') - ->assertValue('div.row:nth-child(4) input[type=text]', $this->profile['phone']) - ->assertSeeIn('div.row:nth-child(5) label', 'External email') - ->assertValue('div.row:nth-child(5) input[type=text]', $this->profile['external_email']) - ->assertSeeIn('div.row:nth-child(6) label', 'Address') - ->assertValue('div.row:nth-child(6) textarea', $this->profile['billing_address']) - ->assertSeeIn('div.row:nth-child(7) label', 'Country') - ->assertValue('div.row:nth-child(7) select', $this->profile['country']) - ->assertSeeIn('div.row:nth-child(8) label', 'Password') - ->assertValue('div.row:nth-child(8) input[type=password]', '') - ->assertSeeIn('div.row:nth-child(9) label', 'Confirm password') + $browser->assertFocused('div.row:nth-child(2) input') + ->assertSeeIn('div.row:nth-child(1) label', 'Customer No.') + ->assertSeeIn('div.row:nth-child(1) .form-control-plaintext', $user->id) + ->assertSeeIn('div.row:nth-child(2) label', 'First name') + ->assertValue('div.row:nth-child(2) input[type=text]', $this->profile['first_name']) + ->assertSeeIn('div.row:nth-child(3) label', 'Last name') + ->assertValue('div.row:nth-child(3) input[type=text]', $this->profile['last_name']) + ->assertSeeIn('div.row:nth-child(4) label', 'Organization') + ->assertValue('div.row:nth-child(4) input[type=text]', $this->profile['organization']) + ->assertSeeIn('div.row:nth-child(5) label', 'Phone') + ->assertValue('div.row:nth-child(5) input[type=text]', $this->profile['phone']) + ->assertSeeIn('div.row:nth-child(6) label', 'External email') + ->assertValue('div.row:nth-child(6) input[type=text]', $this->profile['external_email']) + ->assertSeeIn('div.row:nth-child(7) label', 'Address') + ->assertValue('div.row:nth-child(7) textarea', $this->profile['billing_address']) + ->assertSeeIn('div.row:nth-child(8) label', 'Country') + ->assertValue('div.row:nth-child(8) select', $this->profile['country']) + ->assertSeeIn('div.row:nth-child(9) label', 'Password') ->assertValue('div.row:nth-child(9) input[type=password]', '') + ->assertSeeIn('div.row:nth-child(10) label', 'Confirm password') + ->assertValue('div.row:nth-child(10) input[type=password]', '') ->assertSeeIn('button[type=submit]', 'Submit'); // Test form error handling $browser->type('#phone', 'aaaaaa') ->type('#external_email', 'bbbbb') ->click('button[type=submit]') ->waitFor('#phone + .invalid-feedback') ->assertSeeIn('#phone + .invalid-feedback', 'The phone format is invalid.') ->assertSeeIn( '#external_email + .invalid-feedback', 'The external email must be a valid email address.' ) ->assertFocused('#phone') ->assertToast(Toast::TYPE_ERROR, 'Form validation error') ->clearToasts(); // Clear all fields and submit // FIXME: Should any of these fields be required? $browser->vueClear('#first_name') ->vueClear('#last_name') ->vueClear('#organization') ->vueClear('#phone') ->vueClear('#external_email') ->vueClear('#billing_address') ->click('button[type=submit]') ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.'); }) // On success we're redirected to Dashboard ->on(new Dashboard()); }); } /** * Test profile of non-controller user */ public function testProfileNonController(): void { // Test acting as non-controller $this->browse(function (Browser $browser) { $browser->visit('/logout') ->visit(new Home()) ->submitLogon('jack@kolab.org', 'simple123', true) ->on(new Dashboard()) ->assertSeeIn('@links .link-profile', 'Your profile') ->click('@links .link-profile') ->on(new UserProfile()) ->assertMissing('#user-profile .button-delete') ->whenAvailable('@form', function (Browser $browser) { // TODO: decide on what fields the non-controller user should be able // to see/change }); // Test that /profile/delete page is not accessible $browser->visit('/profile/delete') ->assertErrorPage(403); }); } /** * Test profile delete page */ public function testProfileDelete(): void { $user = $this->getTestUser('profile-delete@kolabnow.com', ['password' => 'simple123']); $this->browse(function (Browser $browser) use ($user) { $browser->visit('/logout') ->on(new Home()) ->submitLogon('profile-delete@kolabnow.com', 'simple123', true) ->on(new Dashboard()) ->clearToasts() ->assertSeeIn('@links .link-profile', 'Your profile') ->click('@links .link-profile') ->on(new UserProfile()) ->click('#user-profile .button-delete') ->waitForLocation('/profile/delete') ->assertSeeIn('#user-delete .card-title', 'Delete this account?') ->assertSeeIn('#user-delete .button-cancel', 'Cancel') ->assertSeeIn('#user-delete .card-text', 'This operation is irreversible') ->assertFocused('#user-delete .button-cancel') ->click('#user-delete .button-cancel') ->waitForLocation('/profile') ->on(new UserProfile()); // Test deleting the user $browser->click('#user-profile .button-delete') ->waitForLocation('/profile/delete') ->click('#user-delete .button-delete') ->waitForLocation('/login') ->assertToast(Toast::TYPE_SUCCESS, 'User deleted successfully.'); $this->assertTrue($user->fresh()->trashed()); }); } // TODO: Test that Ned (John's "delegatee") can delete himself // TODO: Test that Ned (John's "delegatee") can/can't delete John ? } diff --git a/src/tests/Feature/Documents/ReceiptTest.php b/src/tests/Feature/Documents/ReceiptTest.php index 60e373c1..92b7d026 100644 --- a/src/tests/Feature/Documents/ReceiptTest.php +++ b/src/tests/Feature/Documents/ReceiptTest.php @@ -1,312 +1,312 @@ deleteTestUser('receipt-test@kolabnow.com'); parent::tearDown(); } /** * Test receipt HTML output (without VAT) */ public function testHtmlOutput(): void { $appName = \config('app.name'); $wallet = $this->getTestData(); $receipt = new Receipt($wallet, 2020, 5); $html = $receipt->htmlOutput(); $this->assertStringStartsWith('', $html); $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->loadHTML($html); // Title $title = $dom->getElementById('title'); $this->assertSame("Receipt for May 2020", $title->textContent); // Company name/address $header = $dom->getElementById('header'); $companyOutput = $this->getNodeContent($header->getElementsByTagName('td')[0]); $companyExpected = \config('app.company.name') . "\n" . \config('app.company.address'); $this->assertSame($companyExpected, $companyOutput); // The main table content $content = $dom->getElementById('content'); $records = $content->getElementsByTagName('tr'); $this->assertCount(5, $records); $headerCells = $records[0]->getElementsByTagName('th'); $this->assertCount(3, $headerCells); $this->assertSame('Date', $this->getNodeContent($headerCells[0])); $this->assertSame('Description', $this->getNodeContent($headerCells[1])); $this->assertSame('Amount', $this->getNodeContent($headerCells[2])); $cells = $records[1]->getElementsByTagName('td'); $this->assertCount(3, $cells); $this->assertSame('2020-05-01', $this->getNodeContent($cells[0])); $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); $this->assertSame('12,34 CHF', $this->getNodeContent($cells[2])); $cells = $records[2]->getElementsByTagName('td'); $this->assertCount(3, $cells); $this->assertSame('2020-05-10', $this->getNodeContent($cells[0])); $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); $this->assertSame('0,01 CHF', $this->getNodeContent($cells[2])); $cells = $records[3]->getElementsByTagName('td'); $this->assertCount(3, $cells); $this->assertSame('2020-05-31', $this->getNodeContent($cells[0])); $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); $this->assertSame('1,00 CHF', $this->getNodeContent($cells[2])); $summaryCells = $records[4]->getElementsByTagName('td'); $this->assertCount(2, $summaryCells); $this->assertSame('Total', $this->getNodeContent($summaryCells[0])); $this->assertSame('13,35 CHF', $this->getNodeContent($summaryCells[1])); // Customer data $customer = $dom->getElementById('customer'); $customerCells = $customer->getElementsByTagName('td'); $customerOutput = $this->getNodeContent($customerCells[0]); $customerExpected = "Firstname Lastname\nTest Unicode Straße 150\n10115 Berlin"; $this->assertSame($customerExpected, $this->getNodeContent($customerCells[0])); $customerIdents = $this->getNodeContent($customerCells[1]); - $this->assertTrue(strpos($customerIdents, "Account ID {$wallet->id}") !== false); + //$this->assertTrue(strpos($customerIdents, "Account ID {$wallet->id}") !== false); $this->assertTrue(strpos($customerIdents, "Customer No. {$wallet->owner->id}") !== false); // Company details in the footer $footer = $dom->getElementById('footer'); $footerOutput = $footer->textContent; $this->assertStringStartsWith(\config('app.company.details'), $footerOutput); $this->assertTrue(strpos($footerOutput, \config('app.company.email')) !== false); } /** * Test receipt HTML output (with VAT) */ public function testHtmlOutputVat(): void { \config(['app.vat.rate' => 7.7]); \config(['app.vat.countries' => 'ch']); $appName = \config('app.name'); $wallet = $this->getTestData('CH'); $receipt = new Receipt($wallet, 2020, 5); $html = $receipt->htmlOutput(); $this->assertStringStartsWith('', $html); $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->loadHTML($html); // The main table content $content = $dom->getElementById('content'); $records = $content->getElementsByTagName('tr'); $this->assertCount(7, $records); $cells = $records[1]->getElementsByTagName('td'); $this->assertCount(3, $cells); $this->assertSame('2020-05-01', $this->getNodeContent($cells[0])); $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); $this->assertSame('11,39 CHF', $this->getNodeContent($cells[2])); $cells = $records[2]->getElementsByTagName('td'); $this->assertCount(3, $cells); $this->assertSame('2020-05-10', $this->getNodeContent($cells[0])); $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); $this->assertSame('0,01 CHF', $this->getNodeContent($cells[2])); $cells = $records[3]->getElementsByTagName('td'); $this->assertCount(3, $cells); $this->assertSame('2020-05-31', $this->getNodeContent($cells[0])); $this->assertSame("$appName Services", $this->getNodeContent($cells[1])); $this->assertSame('0,92 CHF', $this->getNodeContent($cells[2])); $subtotalCells = $records[4]->getElementsByTagName('td'); $this->assertCount(2, $subtotalCells); $this->assertSame('Subtotal', $this->getNodeContent($subtotalCells[0])); $this->assertSame('12,32 CHF', $this->getNodeContent($subtotalCells[1])); $vatCells = $records[5]->getElementsByTagName('td'); $this->assertCount(2, $vatCells); $this->assertSame('VAT (7.7%)', $this->getNodeContent($vatCells[0])); $this->assertSame('1,03 CHF', $this->getNodeContent($vatCells[1])); $totalCells = $records[6]->getElementsByTagName('td'); $this->assertCount(2, $totalCells); $this->assertSame('Total', $this->getNodeContent($totalCells[0])); $this->assertSame('13,35 CHF', $this->getNodeContent($totalCells[1])); } /** * Test receipt PDF output */ public function testPdfOutput(): void { $wallet = $this->getTestData(); $receipt = new Receipt($wallet, 2020, 5); $pdf = $receipt->PdfOutput(); $this->assertStringStartsWith("%PDF-1.3\n", $pdf); $this->assertTrue(strlen($pdf) > 5000); // TODO: Test the content somehow } /** * Prepare data for a test * * @param string $country User country code * * @return \App\Wallet */ protected function getTestData(string $country = null): Wallet { Bus::fake(); $user = $this->getTestUser('receipt-test@kolabnow.com'); $user->setSettings([ 'first_name' => 'Firstname', 'last_name' => 'Lastname', 'billing_address' => "Test Unicode Straße 150\n10115 Berlin", 'country' => $country ]); $wallet = $user->wallets()->first(); // Create two payments out of the 2020-05 period // and three in it, plus one in the period but unpaid, // and one with amount 0 $payment = Payment::create([ 'id' => 'AAA1', 'status' => PaymentProvider::STATUS_PAID, 'type' => PaymentProvider::TYPE_ONEOFF, 'description' => 'Paid in April', 'wallet_id' => $wallet->id, 'provider' => 'stripe', 'amount' => 1111, ]); $payment->updated_at = Carbon::create(2020, 4, 30, 12, 0, 0); $payment->save(); $payment = Payment::create([ 'id' => 'AAA2', 'status' => PaymentProvider::STATUS_PAID, 'type' => PaymentProvider::TYPE_ONEOFF, 'description' => 'Paid in June', 'wallet_id' => $wallet->id, 'provider' => 'stripe', 'amount' => 2222, ]); $payment->updated_at = Carbon::create(2020, 6, 1, 0, 0, 0); $payment->save(); $payment = Payment::create([ 'id' => 'AAA3', 'status' => PaymentProvider::STATUS_PAID, 'type' => PaymentProvider::TYPE_ONEOFF, 'description' => 'Auto-Payment Setup', 'wallet_id' => $wallet->id, 'provider' => 'stripe', 'amount' => 0, ]); $payment->updated_at = Carbon::create(2020, 5, 1, 0, 0, 0); $payment->save(); $payment = Payment::create([ 'id' => 'AAA4', 'status' => PaymentProvider::STATUS_OPEN, 'type' => PaymentProvider::TYPE_ONEOFF, 'description' => 'Payment not yet paid', 'wallet_id' => $wallet->id, 'provider' => 'stripe', 'amount' => 999, ]); $payment->updated_at = Carbon::create(2020, 5, 1, 0, 0, 0); $payment->save(); // ... so we expect the last three on the receipt $payment = Payment::create([ 'id' => 'AAA5', 'status' => PaymentProvider::STATUS_PAID, 'type' => PaymentProvider::TYPE_ONEOFF, 'description' => 'Payment OK', 'wallet_id' => $wallet->id, 'provider' => 'stripe', 'amount' => 1234, ]); $payment->updated_at = Carbon::create(2020, 5, 1, 0, 0, 0); $payment->save(); $payment = Payment::create([ 'id' => 'AAA6', 'status' => PaymentProvider::STATUS_PAID, 'type' => PaymentProvider::TYPE_ONEOFF, 'description' => 'Payment OK', 'wallet_id' => $wallet->id, 'provider' => 'stripe', 'amount' => 1, ]); $payment->updated_at = Carbon::create(2020, 5, 10, 0, 0, 0); $payment->save(); $payment = Payment::create([ 'id' => 'AAA7', 'status' => PaymentProvider::STATUS_PAID, 'type' => PaymentProvider::TYPE_RECURRING, 'description' => 'Payment OK', 'wallet_id' => $wallet->id, 'provider' => 'stripe', 'amount' => 100, ]); $payment->updated_at = Carbon::create(2020, 5, 31, 23, 59, 0); $payment->save(); // Make sure some config is set so we can test it's put into the receipt if (empty(\config('app.company.name'))) { \config(['app.company.name' => 'Company Co.']); } if (empty(\config('app.company.email'))) { \config(['app.company.email' => 'email@domina.tld']); } if (empty(\config('app.company.details'))) { \config(['app.company.details' => 'VAT No. 123456789']); } if (empty(\config('app.company.address'))) { \config(['app.company.address' => "Test Street 12\n12345 Some Place"]); } return $wallet; } /** * Extract text from a HTML element replacing
with \n * * @param \DOMElement $node The HTML element * * @return string The content */ protected function getNodeContent(\DOMElement $node) { $content = []; foreach ($node->childNodes as $child) { if ($child->nodeName == 'br') { $content[] = "\n"; } else { $content[] = $child->textContent; } } return trim(implode($content)); } }