The vestigial App\Services\Module\Module static class — with its unused
\$scripts / \$styles / \$settings registries — never had any of its helpers
wired up. The new InvoiceShelf\Modules\Registry shipped from the
invoiceshelf/modules package supersedes it cleanly: same static-array surface
(\$menu, \$settings, \$scripts, \$styles), but lives outside the host app so
third-party modules can depend on it without importing v3-app internals.
Three consumers in the host app are migrated to the new namespace:
- ScriptController and StyleController (the HTTP endpoints that serve
module-registered JS/CSS assets at /modules/scripts/{name} and
/modules/styles/{name}) now look up paths via Registry::scriptFor() and
Registry::styleFor() instead of Arr::get(ModuleFacade::all*(), \$name).
Also tightens type hints — Request import + Response return type.
- resources/views/app.blade.php iterates Registry::allStyles() /
Registry::allScripts() to inject module-supplied <link>/<script> tags into
the main layout. Same Akaunting-style asset injection mechanism, just
reading from the new namespace.
Both Module and ModuleFacade are deleted — they had no remaining callers
after this migration.
Closes the audit gaps from the original font system commit. The bundled NotoSans only covered Latin/Greek/Cyrillic but the descriptions claimed Arabic, Thai and Hindi too — that was false. DejaVu Sans, the prior dompdf default, did cover Hebrew, Arabic, Armenian and Georgian, so swapping it for NotoSans had silently regressed those scripts. The Thai conditional include was also dropped from every PDF template in that commit, leaving th locales rendering boxes despite THSarabunNew still sitting in resources/static/fonts/.
Adds four on-demand Font Packages — Noto Sans Hebrew, Noto Naskh Arabic (covering Arabic, Persian, Urdu, Sorani Kurdish), Noto Sans Devanagari (Hindi, Marathi, Sanskrit, Nepali) and Sarabun (Thai) — sourced from openmaptiles/fonts and google/fonts as static TTF. Static is mandatory because dompdf's PHP-Font-Lib does not parse variable fonts. Sarabun replaces THSarabunNew as the Thai face: same designer, OFL-licensed, maintained on a stable upstream URL, and surfaces through the same install flow as every other non-Latin script. The bundled THSarabunNew TTF files and the dead app/pdf/locale/th.blade.php legacy partial are removed as part of the migration.
Unifies the bundled Noto Sans into FONT_PACKAGES as a noto-sans entry with bundled => true and files served from resources/static/fonts/ instead of storage/fonts/. FontService::isInstalled, downloadPackage, getInstalledFontFaces and getPackageStatuses honor the flag through a new packageDir() helper. The hardcoded @font-face block in the PDF partial is gone — fonts.blade.php collapses to a single getInstalledFontFaces() call so the package array is the only source of truth for every face, bundled or on-demand. Admin → Font Packages now lists Noto Sans at the top with a primary-colored Bundled pill (new settings.fonts.bundled string) alongside the existing Installed badge / Install button states.
Also fixes the misleading settings.fonts.description and settings.fonts.bundled_info copy to actually describe what ships out of the box vs. what's optional, and rebuilds the en locale chunk.
Existing accounts inherited the company language at creation time and there was no way to change UI language per user. Add a 'Default (Company Language)' entry to the language selector in UserGeneralView, persist the choice through userStore.updateUserSettings and reload the i18n bundle via window.loadLanguage. The 'default' sentinel keeps the user opted in to the company-wide setting.
Bootstrap (global.store) now syncs userForm from current_user data and resolves the active UI language as user > company > 'en'. RegisterController, InvitationRegistrationController and MemberService seed new users with language=default instead of copying the current company setting, so promoting/inviting members no longer leaks the inviter's frozen language.
Bundle Noto Sans (Regular/Bold/Italic/BoldItalic) under resources/static/fonts/ as the default PDF face — it covers Latin, Cyrillic, Greek, Arabic, Thai and Hindi out of the box, replacing the limited DejaVu Sans fallback. Move all @font-face declarations into app.pdf.partials.fonts and include it from every invoice/estimate/payment/report template, dropping per-template font-family hardcodes and the conditional Thai locale include.
Introduce FontService + FontController to download static Noto Sans CJK packages (zh, zh_CN, ja, ko) from life888888/cjk-fonts-ttf on demand. GeneratesPdfTrait::ensureFontsForLocale primes the family before rendering and the partial emits @font-face rules for installed packages so dompdf resolves them through standard CSS — no separate registerFont() instance required. Static TTFs are mandatory because dompdf's PHP-Font-Lib does not parse variable fonts (fvar/gvar tables), which is why Google Fonts' NotoSansTC[wght].ttf rendered empty boxes.
Expose status/install via /api/v1/fonts/status and /api/v1/fonts/{package}/install with matching FONTS_STATUS / FONTS_INSTALL constants in scripts-v2/api/endpoints.ts. Flip DOMPDF_ENABLE_REMOTE default to true for remote asset loading.
Major changes to the file disk subsystem:
- Each FileDisk now gets a unique Laravel disk name (disk_{id}) instead
of temp_{driver}, fixing the bug where multiple local disks with
different roots overwrote each other's config.
- Move disk registration logic from FileDisk model to FileDiskService
(registerDisk, getDiskName). Model keeps only getDecodedCredentials
and a deprecated setConfig() wrapper.
- Add Disk Assignments admin UI (File Disk tab) with three purpose
dropdowns: Media Storage, PDF Storage, Backup Storage. Stored as
settings (media_disk_id, pdf_disk_id, backup_disk_id).
- Backup tab now uses the assigned backup disk instead of a per-backup
dropdown. BackupsController refactored to use BackupService which
centralizes disk resolution. Removed stale 4-second cache.
- Add local_public disk to config/filesystems.php so system disks
are properly defined.
- Local disk roots stored relative to storage/app/ with hint text
in the admin modal explaining the convention.
- Fix BaseModal watchEffect -> watch to prevent infinite request
loops on the File Disk page.
- Fix string/number comparison for disk purpose IDs from settings.
- Add safeguards: prevent deleting disks with files, warn on
purpose change, prevent deleting system disks.
Custom fields defined on an estimate are now carried over to the
invoice when using Convert to Invoice. Uses the same pattern as
the clone method.
Fixes#282
New backend endpoint POST /invoices/{id}/convert-to-estimate that
creates a draft estimate from an invoice, copying items, taxes,
custom fields, and financial data. Frontend wired with dropdown
action, store method, and API service call.
- Fix exchange-rate service types to match actual backend response shapes
(exchangeRate array, activeProvider success/error, used currencies as strings)
- Add ExchangeRateConverter to payments, expenses, and recurring invoices
- Set currency_id from customer currency in invoice/estimate selectCustomer()
- Load globalStore.currencies in ExchangeRateConverter on mount
- Pass driver/key/driver_config params to getSupportedCurrencies in provider modal
- Fix OpenExchangeRateDriver validateConnection to use base=USD (free plan compat)
- Fix checkActiveCurrencies SQLite whereJsonContains with array values
- Remove broken currency/companyCurrency props from ExpenseCreateView, use stores
- Show base currency equivalent in document line items and totals when exchange
rate is active
- Add manifest.json generation script (scripts/generate-manifest.php)
- Add Updater::cleanStaleFiles() that removes files not in manifest
- Add /api/v1/update/clean endpoint with backward compatibility
- Add configurable update_protected_paths in config/invoiceshelf.php
- Update frontend to use clean step instead of delete step
- Add GitHub Actions release workflow triggered on version tags
- Add .github/release.yml for auto-generated changelog categories
- Update Makefile to include manifest generation and scripts directory
The CompanyInvitationMail accesses company, role, and invitedBy
relationships which weren't loaded before sending. Also wrap mail
send in try-catch so the invitation is still created even if the
mailer is misconfigured (logs a warning instead of crashing).
Redistribute methods:
- show() -> BootstrapController::currentCompany()
- store(), destroy(), userCompanies() -> Admin\CompaniesController
- transferOwnership() -> CompanySettingsController
Security fix: introduce 'owner' role for company-level admin, distinct
from 'super admin' which is now global platform admin only.
- CompanyService::setupRoles() creates 'owner' role per company
- Company creation assigns scoped 'owner' role instead of global 'super admin'
- Seeders updated to assign 'owner'
Migration renames all existing company-scoped 'super admin' roles to
'owner' and ensures every company owner has the role assigned.
Replace duplicated switch/case blocks across 4 methods with a clean
abstract driver pattern:
- ExchangeRateDriver (abstract): defines getExchangeRate(),
getSupportedCurrencies(), validateConnection()
- CurrencyFreakDriver, CurrencyLayerDriver, OpenExchangeRateDriver,
CurrencyConverterDriver: concrete implementations
- ExchangeRateDriverFactory: resolves driver name to class, with
register() method for module extensibility
Delete ExchangeRateProvidersTrait — all logic now lives in driver
classes and ExchangeRateProviderService. Adding a new exchange rate
provider only requires implementing ExchangeRateDriver and calling
ExchangeRateDriverFactory::register() in a module service provider.
Split PDFService.php (3 classes + 2 interfaces in one file) into separate
files. Move GotenbergPDFDriver from app/Services/PDFDrivers/ into
app/Services/Pdf/. Normalize casing from ALL-CAPS PDF to Pdf throughout:
facade, provider, service, driver factory, and Gotenberg driver.
Fix PaymentService using Barryvdh DomPDF facade directly instead of the
app's PDF facade (bypassed the driver factory). Report controllers also
updated to use the app facade.
Relocate all 14 files from the catch-all app/Space namespace into proper
locations: data providers to Support/Formatters, installation utilities to
Services/Installation, PDF utils to Services/Pdf, module/update classes to
Services/Module and Services/Update, SiteApi trait to Traits, and helpers
to Support.
Extract ~1,400 lines of business logic from 8 fat models (Invoice, Payment,
Estimate, RecurringInvoice, Company, Customer, Expense, User) into 9 new
service classes with constructor injection. Controllers now depend on
services instead of calling static model methods. Shared item/tax creation
logic consolidated into DocumentItemService.
- Add Administration sidebar section (super-admin only) with Companies, Users, and Global Settings pages
- Add super-admin middleware, controllers, and API routes under /api/v1/super-admin/
- Allow super-admins to manage all companies and users across tenants
- Add user impersonation with short-lived tokens, audit logging, and UI banner
- Move system-level settings (Mail, PDF, Backup, Update, File Disk) from per-company to Administration > Global Settings
- Convert save_pdf_to_disk from CompanySetting to global Setting
- Add per-company mail configuration overrides (optional, falls back to global)
- Add CompanyMailConfigService to apply company mail config before sending emails
* Convert string references to `::class`
PHP 5.5.9 adds the new static `class` property which provides the fully qualified class name. This is preferred over using strings for class names since the `class` property references are checked by PHP.
* Use Faker methods
Accessing Faker properties was deprecated in Faker 1.14.
* Convert route options to fluent methods
Laravel 8 adopts the tuple syntax for controller actions. Since the old options array is incompatible with this syntax, Shift converted them to use modern, fluent methods.
* Adopt class based routes
* Remove default `app` files
* Shift core files
* Streamline config files
* Set new `ENV` variables
* Default new `bootstrap/app.php`
* Re-register HTTP middleware
* Consolidate service providers
* Re-register service providers
* Re-register routes
* Re-register scheduled commands
* Bump Composer dependencies
* Use `<env>` tags for configuration
`<env>` tags have a lower precedence than system environment variables making it easier to overwrite PHPUnit configuration values in additional environments, such a CI.
Review this blog post for more details on configuration precedence when testing Laravel: https://jasonmccreary.me/articles/laravel-testing-configuration-precedence/
* Adopt anonymous migrations
* Rename `password_resets` table
* Convert `$casts` property to method
* Adopt Laravel type hints
* Mark base controller as `abstract`
* Remove `CreatesApplication` testing trait
* Shift cleanup
* Fix shift first issues
* Updating Rules for laravel 11, sanctum config and pint
* Fix Carbon issue on dashboard
* Temporary fix for tests while migration is issue fixed on laravel side
* Carbon needs numerical values, not strings
* Minimum php version
* Fix domain installation step not fetching the correct company_id
* Fix Role Policy wasn't properly registered
---------