Services/Integrations/ExchangeRate/ and Services/Pdf/ were both mostly Support-shaped: interfaces, abstract classes, static factories, concrete adapter drivers, DTOs, and exceptions — infrastructure that doesn't carry business logic. They only each had one real DI-injected service mixed in.
This commit applies the same Services=DI-business-logic / Support=stateless-plumbing rule we've been using throughout the reorg:
**Moved to Support/Integrations/ExchangeRate/** (7 files): ExchangeRateDriver (abstract), ExchangeRateDriverFactory (static), ExchangeRateException, and the four concrete drivers (CurrencyConverter, CurrencyFreak, CurrencyLayer, OpenExchangeRate). These are HTTP adapters over third-party currency APIs — same shape as the Hashids library wrapper classes already in Support.
**Moved to Support/Pdf/** (6 files, merging with existing Pdf utilities): PdfDriver (interface), PdfDriverFactory (static), PdfService (static facade), GotenbergPdfDriver, GotenbergPdfResponse (DTO), ResponseStream (interface). The Support/Pdf/ dir now contains the full PDF rendering subsystem — drivers + sanitizer + template/image utilities.
**Promoted to Services/ root** (the real DI services): ExchangeRateProviderService (CRUD for ExchangeRateProvider model) and FontService (font package install/download orchestration). Both are proper DI services — instance methods, model writes, HTTP side effects.
Services/Integrations/ and Services/Pdf/ are now empty and deleted. Services/ holds only DI-injected classes; Support/ holds all the plumbing.
17 files renamed (git detects 90-99% similarity), 4 consumer files updated (DriverRegistryProvider, PdfServiceProvider, ExchangeRateProviderController, FontController, GeneratesPdfTrait, test). 350 tests pass, Pint clean.
Finishes the Support/ consolidation pass: the three remaining root-level files get grouped into purpose-named subdirs, matching the shape the Pdf/, Hashids/, Update/, and Module/ subdirs took in the earlier sweep.
- BouncerDefaultScope → Support/Bouncer/ (Bouncer-specific authorization scope)
- CustomPathGenerator → Support/Media/ (Spatie MediaLibrary path generator — media config references it from config/media-library.php)
- InstallWizardAuth → Support/Setup/ (folded into the existing Setup/ subdir alongside EnvironmentManager, FilePermissionChecker, InstallUtils, RequirementsChecker — it's install-wizard-flow state)
4 consumer files updated (AppServiceProvider, LoginController, UseInstallWizardTokenAuth middleware, config/media-library.php). app/Support/ root is now completely empty of standalone PHP files except helpers.php.
ModuleInstaller has the same shape as Updater (moved in 7cf72b9f): every method is public static, no constructor, no DI, no instance state. It orchestrates marketplace operations — fetch catalog, download zip, verify checksum, unzip, copy files, run module:migrate/module:enable, dispatch install/enable events — but the orchestration itself is stateless procedural plumbing.
Emitting events and writing to the Module eloquent model doesn't make it a service; plenty of static helpers touch models. The distinguishing factor is stateless-procedural vs DI-injected-workflow, and this is clearly the former.
4 consumers updated: ModulesController, ModuleInstallationController, InstallModuleCommand, and a doc comment in config/invoiceshelf.php. 350 tests still pass.
This leaves app/Services/ with no single-file driver-less subdirs except Mail/Module/Pdf/Storage which have multiple files each. The Module/ subdir in Services is now deleted entirely — the marketplace installer moved out and there were no other files in there.
Updater is a pure static procedural class — all eight public methods (checkForUpdate, download, unzip, copyFiles, deleteFiles, cleanStaleFiles, migrateUpdate, finishUpdate) are static, there's no constructor, no DI, no instance state. It's stateless self-update plumbing, same character as the Setup/ helpers that moved to Support/ in commit 6d1816bd.
Moving it out of Services/ leaves Services/ exclusively for DI-injected classes with real business logic, which is the contract the reorg was aiming for. Update/ now lives next to Setup/ in Support/ where the other install/upgrade-time utilities live.
Only 3 consumers needed touching: UpdateController, UpdateCommand, and a reference in config/invoiceshelf.php's comment. 350 tests still pass.
The app/Services/ directory had grown into 22 flat files at the root plus 7 uneven subdirectories — finding anything required scrolling through an alphabetical mix of small CRUD services, infrastructure drivers, and install-time utilities. This commit groups services by domain, folds Backup into a new Storage namespace, and moves framework-infrastructure and install-time helpers out of Services and into Support where they belong.
New Services layout: Documents/ (Invoice, Estimate, RecurringInvoice, Payment, Expense, Transaction, DocumentItem, SerialNumber, Currency — matches the 'Documents' navigation group); Company/ (Company, Member, Invitation); Mail/ (MailConfiguration, CompanyMailConfig); Storage/ (FileDisk, plus Backup folded in). ExchangeRateProviderService moves next to its drivers in ExchangeRate/; FontService moves into Pdf/ where it belongs. CustomerService, ItemService, CustomFieldService stay at the Services root as standalone single-file domains.
Moves to Support/: Hashids/ (library wrapper — not business logic); Setup/ (one-shot install-time utilities — stateless helpers); Pdf/ (ImageUtils, PdfTemplateUtils, plus the existing PdfHtmlSanitizer consolidated into the same subdir). These are all framework infrastructure and stateless utilities — the 'service' label never really fit them.
Namespace declarations in 29 moved files updated to match new paths. 62 consumer files (controllers, other services, tests, database factories, seeders, routes, bootstrap/providers.php) have their use statements rewritten via a literal-string replacement script — no regex meant no risk of half-matching. Three Documents services needed an explicit 'use App\Services\Mail\CompanyMailConfigService' added because the same-namespace short reference they relied on no longer resolves after the split.
Verified: composer dump-autoload, 350 tests pass (850 assertions), vendor/bin/pint clean, npm run build succeeds.
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.