diff --git a/src/app/Documents/Receipt.php b/src/app/Documents/Receipt.php index cd3de15d..f95246d7 100644 --- a/src/app/Documents/Receipt.php +++ b/src/app/Documents/Receipt.php @@ -1,277 +1,278 @@ wallet = $wallet; $this->year = $year; $this->month = $month; } /** * Render the mail template with fake data * * @param string $type Output format ('html' or 'pdf') * * @return string HTML or PDF output */ public static function fakeRender(string $type = 'html'): string { $wallet = new Wallet(['currency' => 'CHF']); $wallet->id = \App\Utils::uuidStr(); $wallet->owner = new User(['id' => 123456789]); $receipt = new self($wallet, date('Y'), date('n')); self::$fakeMode = true; if ($type == 'pdf') { return $receipt->pdfOutput(); } elseif ($type !== 'html') { throw new \Exception("Unsupported output format"); } return $receipt->htmlOutput(); } /** * Render the receipt in HTML format. * * @return string HTML content */ public function htmlOutput(): string { return $this->build()->render(); } /** * Render the receipt in PDF format. * * @return string PDF content */ public function pdfOutput(): string { // Parse ther HTML template $html = $this->build()->render(); // Link fonts from public/fonts to storage/fonts so DomPdf can find them if (!is_link(storage_path('fonts/Roboto-Regular.ttf'))) { symlink( public_path('fonts/Roboto-Regular.ttf'), storage_path('fonts/Roboto-Regular.ttf') ); symlink( public_path('fonts/Roboto-Bold.ttf'), storage_path('fonts/Roboto-Bold.ttf') ); } // Fix font and image paths $html = str_replace('url(/fonts/', 'url(fonts/', $html); $html = str_replace('src="/', 'src="', $html); // TODO: The output file is about ~200KB, we could probably slim it down // by using separate font files with small subset of languages when // there are no Unicode characters used, e.g. only ASCII or Latin. // Load PDF generator - $pdf = \PDF::loadHTML($html)->setPaper('a4', 'portrait'); + $pdf = Pdf::loadHTML($html)->setPaper('a4', 'portrait'); return $pdf->output(); } /** * Build the document * * @return \Illuminate\View\View The template object */ protected function build() { $appName = \config('app.name'); $start = Carbon::create($this->year, $this->month, 1, 0, 0, 0); $end = $start->copy()->endOfMonth(); $month = \trans('documents.month' . intval($this->month)); $title = \trans('documents.receipt-title', ['year' => $this->year, 'month' => $month]); $company = $this->companyData(); if (self::$fakeMode) { $country = 'CH'; $customer = [ 'id' => $this->wallet->owner->id, 'wallet_id' => $this->wallet->id, 'customer' => 'Freddie Krüger
7252 Westminster Lane
Forest Hills, NY 11375', ]; $items = collect([ (object) [ 'amount' => 1234, 'updated_at' => $start->copy()->next(Carbon::MONDAY), ], (object) [ 'amount' => 10000, 'updated_at' => $start->copy()->next()->next(), ], (object) [ 'amount' => 1234, 'updated_at' => $start->copy()->next()->next()->next(Carbon::MONDAY), ], (object) [ 'amount' => 99, 'updated_at' => $start->copy()->next()->next()->next(), ], ]); } else { $customer = $this->customerData(); $country = $this->wallet->owner->getSetting('country'); $items = $this->wallet->payments() ->where('status', PaymentProvider::STATUS_PAID) ->where('updated_at', '>=', $start) ->where('updated_at', '<', $end) ->where('amount', '<>', 0) ->orderBy('updated_at') ->get(); } $vatRate = \config('app.vat.rate'); $vatCountries = explode(',', \config('app.vat.countries')); $vatCountries = array_map('strtoupper', array_map('trim', $vatCountries)); if (!$country || !in_array(strtoupper($country), $vatCountries)) { $vatRate = 0; } $totalVat = 0; $total = 0; $items = $items->map(function ($item) use (&$total, &$totalVat, $appName, $vatRate) { $amount = $item->amount; if ($vatRate > 0) { $amount = round($amount * ((100 - $vatRate) / 100)); $totalVat += $item->amount - $amount; } $total += $amount; $type = $item->type ?? null; if ($type == PaymentProvider::TYPE_REFUND) { $description = \trans('documents.receipt-refund'); } elseif ($type == PaymentProvider::TYPE_CHARGEBACK) { $description = \trans('documents.receipt-chargeback'); } else { $description = \trans('documents.receipt-item-desc', ['site' => $appName]); } return [ 'amount' => $this->wallet->money($amount), 'description' => $description, 'date' => $item->updated_at->toDateString(), ]; }); // Load the template $view = view('documents.receipt') ->with([ 'site' => $appName, 'title' => $title, 'company' => $company, 'customer' => $customer, 'items' => $items, 'subTotal' => $this->wallet->money($total), 'total' => $this->wallet->money($total + $totalVat), 'totalVat' => $this->wallet->money($totalVat), 'vatRate' => preg_replace('/([.,]00|0|[.,])$/', '', sprintf('%.2f', $vatRate)), 'vat' => $vatRate > 0, ]); return $view; } /** * Prepare customer data for the template * * @return array Customer data for the template */ protected function customerData(): array { $user = $this->wallet->owner; $name = $user->name(); $settings = $user->getSettings(['organization', 'billing_address']); $customer = trim(($settings['organization'] ?: $name) . "\n" . $settings['billing_address']); $customer = str_replace("\n", '
', htmlentities($customer)); return [ 'id' => $this->wallet->owner->id, 'wallet_id' => $this->wallet->id, 'customer' => $customer, ]; } /** * Prepare company data for the template * * @return array Company data for the template */ protected function companyData(): array { $header = \config('app.company.name') . "\n" . \config('app.company.address'); $header = str_replace("\n", '
', htmlentities($header)); $footerLineLength = 110; $footer = \config('app.company.details'); $contact = \config('app.company.email'); $logo = \config('app.company.logo'); $theme = \config('app.theme'); if ($contact) { $length = strlen($footer) + strlen($contact) + 3; $contact = htmlentities($contact); $footer .= ($length > $footerLineLength ? "\n" : ' | ') . sprintf('%s', $contact, $contact); } if ($logo && strpos($logo, '/') === false) { $logo = "/themes/$theme/images/$logo"; } return [ 'logo' => $logo ? "" : '', 'header' => $header, 'footer' => $footer, ]; } } diff --git a/src/config/app.php b/src/config/app.php index 7844da46..054f2b5c 100644 --- a/src/config/app.php +++ b/src/config/app.php @@ -1,276 +1,269 @@ env('APP_NAME', 'Laravel'), /* |-------------------------------------------------------------------------- | Application Environment |-------------------------------------------------------------------------- | | This value determines the "environment" your application is currently | running in. This may determine how you prefer to configure various | services the application utilizes. Set this in your ".env" file. | */ 'env' => env('APP_ENV', 'production'), /* |-------------------------------------------------------------------------- | Application Debug Mode |-------------------------------------------------------------------------- | | When your application is in debug mode, detailed error messages with | stack traces will be shown on every error that occurs within your | application. If disabled, a simple generic error page is shown. | */ 'debug' => env('APP_DEBUG', false), /* |-------------------------------------------------------------------------- | Application URL |-------------------------------------------------------------------------- | | This URL is used by the console to properly generate URLs when using | the Artisan command line tool. You should set this to the root of | your application so that it is used when running Artisan tasks. */ 'url' => env('APP_URL', 'http://localhost'), 'passphrase' => env('APP_PASSPHRASE', null), 'public_url' => env('APP_PUBLIC_URL', env('APP_URL', 'http://localhost')), 'asset_url' => env('ASSET_URL'), 'support_url' => env('SUPPORT_URL', null), 'support_email' => env('SUPPORT_EMAIL', null), 'webmail_url' => env('WEBMAIL_URL', null), 'theme' => env('APP_THEME', 'default'), 'tenant_id' => env('APP_TENANT_ID', null), 'currency' => \strtoupper(env('APP_CURRENCY', 'CHF')), /* |-------------------------------------------------------------------------- | Application Domain |-------------------------------------------------------------------------- | | System domain used for user signup (kolab identity) */ 'domain' => env('APP_DOMAIN', 'domain.tld'), 'website_domain' => env('APP_WEBSITE_DOMAIN', env('APP_DOMAIN', 'domain.tld')), /* |-------------------------------------------------------------------------- | Application Timezone |-------------------------------------------------------------------------- | | Here you may specify the default timezone for your application, which | will be used by the PHP date and date-time functions. We have gone | ahead and set this to a sensible default for you out of the box. | */ 'timezone' => 'UTC', /* |-------------------------------------------------------------------------- | Application Locale Configuration |-------------------------------------------------------------------------- | | The application locale determines the default locale that will be used | by the translation service provider. You are free to set this value | to any of the locales which will be supported by the application. | */ 'locale' => env('APP_LOCALE', 'en'), /* |-------------------------------------------------------------------------- | Application Fallback Locale |-------------------------------------------------------------------------- | | The fallback locale determines the locale to use when the current one | is not available. You may change the value to correspond to any of | the language folders that are provided through your application. | */ 'fallback_locale' => 'en', /* |-------------------------------------------------------------------------- | Faker Locale |-------------------------------------------------------------------------- | | This locale will be used by the Faker PHP library when generating fake | data for your database seeds. For example, this will be used to get | localized telephone numbers, street address information and more. | */ 'faker_locale' => 'en_US', /* |-------------------------------------------------------------------------- | Encryption Key |-------------------------------------------------------------------------- | | This key is used by the Illuminate encrypter service and should be set | to a random, 32 character string, otherwise these encrypted strings | will not be safe. Please do this before deploying an application! | */ 'key' => env('APP_KEY'), 'cipher' => 'AES-256-CBC', /* |-------------------------------------------------------------------------- | Autoloaded Service Providers |-------------------------------------------------------------------------- | | The service providers listed here will be automatically loaded on the | request to your application. Feel free to add your own services to | this array to grant expanded functionality to your applications. | */ 'providers' => [ /* * Laravel Framework Service Providers... */ Illuminate\Auth\AuthServiceProvider::class, Illuminate\Broadcasting\BroadcastServiceProvider::class, Illuminate\Bus\BusServiceProvider::class, Illuminate\Cache\CacheServiceProvider::class, Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, Illuminate\Cookie\CookieServiceProvider::class, Illuminate\Database\DatabaseServiceProvider::class, Illuminate\Encryption\EncryptionServiceProvider::class, Illuminate\Filesystem\FilesystemServiceProvider::class, Illuminate\Foundation\Providers\FoundationServiceProvider::class, Illuminate\Hashing\HashServiceProvider::class, Illuminate\Mail\MailServiceProvider::class, Illuminate\Notifications\NotificationServiceProvider::class, Illuminate\Pagination\PaginationServiceProvider::class, Illuminate\Pipeline\PipelineServiceProvider::class, Illuminate\Queue\QueueServiceProvider::class, Illuminate\Redis\RedisServiceProvider::class, Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, Illuminate\Session\SessionServiceProvider::class, Illuminate\Translation\TranslationServiceProvider::class, Illuminate\Validation\ValidationServiceProvider::class, Illuminate\View\ViewServiceProvider::class, - /* - * Package Service Providers... - */ - Barryvdh\DomPDF\ServiceProvider::class, - /* * Application Service Providers... */ App\Providers\AppServiceProvider::class, App\Providers\AuthServiceProvider::class, // App\Providers\BroadcastServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\HorizonServiceProvider::class, App\Providers\PassportServiceProvider::class, App\Providers\RouteServiceProvider::class, ], /* |-------------------------------------------------------------------------- | Class Aliases |-------------------------------------------------------------------------- | | This array of class aliases will be registered when this application | is started. However, feel free to register as many as you wish as | the aliases are "lazy" loaded so they don't hinder performance. | */ - 'aliases' => \Illuminate\Support\Facades\Facade::defaultAliases()->merge([ - 'PDF' => Barryvdh\DomPDF\Facade::class, - ])->toArray(), + 'aliases' => \Illuminate\Support\Facades\Facade::defaultAliases()->toArray(), 'headers' => [ 'csp' => env('APP_HEADER_CSP', ""), 'xfo' => env('APP_HEADER_XFO', ""), ], // Locations of knowledge base articles 'kb' => [ // An article about suspended accounts 'account_suspended' => env('KB_ACCOUNT_SUSPENDED'), // An article about a way to delete an owned account 'account_delete' => env('KB_ACCOUNT_DELETE'), // An article about the payment system 'payment_system' => env('KB_PAYMENT_SYSTEM'), ], 'company' => [ 'name' => env('COMPANY_NAME'), 'address' => env('COMPANY_ADDRESS'), 'details' => env('COMPANY_DETAILS'), 'email' => env('COMPANY_EMAIL'), 'logo' => env('COMPANY_LOGO'), 'footer' => env('COMPANY_FOOTER', env('COMPANY_DETAILS')), ], 'storage' => [ 'min_qty' => (int) env('STORAGE_MIN_QTY', 5), // in GB ], 'vat' => [ 'countries' => env('VAT_COUNTRIES'), 'rate' => (float) env('VAT_RATE'), ], 'password_policy' => env('PASSWORD_POLICY') ?: 'min:6,max:255', 'payment' => [ 'methods_oneoff' => env('PAYMENT_METHODS_ONEOFF', 'creditcard,paypal,banktransfer,bitcoin'), 'methods_recurring' => env('PAYMENT_METHODS_RECURRING', 'creditcard'), ], 'with_admin' => (bool) env('APP_WITH_ADMIN', false), 'with_files' => (bool) env('APP_WITH_FILES', false), 'with_reseller' => (bool) env('APP_WITH_RESELLER', false), 'with_services' => (bool) env('APP_WITH_SERVICES', false), 'signup' => [ 'email_limit' => (int) env('SIGNUP_LIMIT_EMAIL', 0), 'ip_limit' => (int) env('SIGNUP_LIMIT_IP', 0), ], 'woat_ns1' => env('WOAT_NS1', 'ns01.' . env('APP_DOMAIN')), 'woat_ns2' => env('WOAT_NS2', 'ns02.' . env('APP_DOMAIN')), 'ratelimit_whitelist' => explode(',', env('RATELIMIT_WHITELIST', '')) ];