Architecture
This document describes the current technical state of StoreAccountant.
Bootstrap and Container
storeaccountant.php is only the bootstrap:
- Plugin metadata and constants.
- WooCommerce HPOS compatibility declaration for custom order tables.
StoreAccountant::PLUGIN_VERSION.StoreAccountant::PHP_VERSION, currently8.2.- Composer autoloader.
- Activation and deactivation hooks.
- Start of
StoreAccountant\Plugin.
StoreAccountant\Plugin builds the container through
StoreAccountant\ContainerBuilder and registers all services from
ContainerBuilder::HOOK_SERVICES when they implement HookRegistrarInterface.
Value objects such as ExportPeriod are intentionally not registered as
container services.
Uninstall Cleanup
Plugin deactivation and plugin uninstall are intentionally separate. Deactivation
must not remove saved StoreAccountant data. Uninstall runs through
uninstall.php and removes StoreAccountant database artifacts through cleanup
tasks registered on storeaccountant_uninstall_cleanup_task.
The built-in uninstall cleanup removes plugin settings, StoreAccountant role capabilities, queued StoreAccountant cron/action-scheduler state, saved export configurations, and saved export records. It is deliberately database-only: generated export archives below the StoreAccountant upload storage and diagnostic log files are not deleted during uninstall.
WooCommerce HPOS Compatibility
StoreAccountant declares compatibility with WooCommerce High-Performance Order
Storage (HPOS) through FeaturesUtil::declare_compatibility() for the
custom_order_tables feature. Future order-related features must remain HPOS
compatible by using WooCommerce order APIs instead of direct WordPress post or
postmeta access for order data.
Order data must be loaded through APIs such as wc_get_order(),
wc_get_orders(), WC_Order_Query, or WC_Order methods. Order metadata must
be read and written through the WooCommerce order object, for example
$order->get_meta(), $order->update_meta_data(), and $order->save().
Direct get_post_meta(), update_post_meta(), WP_Query, direct SQL, or
shop_order post assumptions are acceptable only for StoreAccountant's own
custom post types and never for WooCommerce order records.
Main Areas
src/Admin: shared WordPress admin shell code such as the top-level menu, header bar, and admin assets.src/*/Admin: feature-specific admin pages, forms, settings sections, tabs, notices, and field providers. Removing a feature namespace should also remove its admin UI.src/Export: export custom post type, export orchestration, export adapter registry, renderer registry, field provider registry, export repository, datasets, and export value objects.src/Queue: Symfony Messenger bus adapter and transport infrastructure.src/Export/Queue: export-specific queue messages, handlers, and cleanup.src/Schedule: reserved for future recurring export setup and cron-style execution. Scheduled exports are not currently offered in the admin UI and are not configured on export configuration records.src/Order/Export/Adapter: concrete order data adapters.src/Export/Filter: export source filters, filter field provider registries, period resolvers used by date-like filters, and filter selection value objects.src/Export/Field: field definitions, field values, field mappings, reusable metadata field helpers, and type-specific field providers and mutators. Current order-specific classes live belowsrc/Order/Export; the customer export lives belowsrc/Customer/Export.src/Export/Renderer: concrete export renderers such as CSV and serializer-backed formats.src/Export/Configuration: export configuration custom post type, repository, and additional configuration field providers.src/Invoice: invoice plugin integrations and invoice-related export providers. Order-facing invoice providers live belowsrc/Invoice/Export/Orderand register into the main export hooks.src/Storage: storage adapter registry, storage adapter contracts, and Flysystem-backed storage adapters.
Data Model
Accounting Exports
Saved exports use the custom post type storeacct_export.
Important meta fields:
_storeaccountant_exported_at_storeaccountant_status_storeaccountant_filters_storeaccountant_storage_engine_storeaccountant_export_adapter_storeaccountant_export_writer_storeaccountant_additional_settings_storeaccountant_order_tax_field_provider_storeaccountant_batch_size_storeaccountant_path_storeaccountant_triggered_by_storeaccountant_configuration_id_storeaccountant_download_token_storeaccountant_download_password_storeaccountant_download_password_hash_storeaccountant_total_items_storeaccountant_processed_items_storeaccountant_total_batches_storeaccountant_processed_batches_storeaccountant_failed_batches_storeaccountant_current_step_storeaccountant_error_message_storeaccountant_started_at_storeaccountant_finished_at
Export lifecycle statuses are stored in _storeaccountant_status:
queuedprocessingcompletedfailed
Exports are conceptually immutable: edit and quick-edit actions are removed,
bulk edit is removed, restoring from trash sets the post back to publish, and
native draft state labels are hidden.
Export title and row view links point to a custom read-only StoreAccountant
admin page. Its tabs are provided through
storeaccountant_export_read_tab_provider; providers decide whether they
support the current export by inspecting stored metadata such as the export
adapter ID. The built-in read tabs are Export Details and Raw Data.
When an export is permanently deleted, StoreAccountant resolves the saved storage adapter and asks it to delete the stored path. Missing or already removed files are ignored so the export record can still be deleted cleanly.
Each export run stores a random public download token and a snapshot of the effective download password. The snapshot stores both an encrypted reversible value for authorized backend reveal screens and a separate verification hash for frontend password checks. Quick exports also snapshot additional provider settings and the selected order tax field provider on the export record, because they intentionally have no configuration relation. Configuration records store their own download password snapshot; saving a configuration with an empty password field stores the current global download password on that configuration.
Export Configurations
Saved export configurations use the custom post type storeacct_config.
Important meta fields:
_storeaccountant_config_filters_storeaccountant_config_export_adapter_storeaccountant_config_export_writer_storeaccountant_config_storage_engine_storeaccountant_config_batch_size_storeaccountant_config_order_tax_field_provider_storeaccountant_config_additional_settings_storeaccountant_config_field_mapping_storeaccountant_config_download_password_storeaccountant_config_download_password_hash
Export configurations can be edited through the StoreAccountant configuration
form. They store source filters as JSON in _storeaccountant_config_filters.
Date ranges are not part of the generic configuration model; they are optional
filter settings for export types that need them. When a configuration is used
to start an export, dynamic filter settings such as Last Month are resolved
and snapshotted into the generated export's _storeaccountant_filters metadata.
They can also be deleted. When an export configuration is deleted, existing
exports are kept and retain their stored relation so the export overview can
show that the related configuration was deleted.
Saved configurations can have type-specific detail tabs. Additional tabs are
registered through storeaccountant_export_configuration_tab_provider; providers
decide whether they support the current configuration by inspecting stored
metadata such as the export adapter ID. The built-in order configuration tab is
Field Mapping; the built-in customer export has its own Field Mapping tab
using the same shared mapping repository.
Field mapping is stored as JSON in _storeaccountant_config_field_mapping. Each
mapping item stores at least field_id, enabled, label, and options.
When no mapping exists yet, all available fields are treated as enabled and use
their provider-defined labels. Newly added provider fields that are missing from
an existing mapping are also enabled by default. Stored mapping entries whose
field IDs are no longer available are ignored at render and export time. This
keeps old configurations safe when a provider is disabled or an integration is
no longer active; the stored mapping can become effective again if the provider
later becomes available.
Admin UI
StoreAccountant registers its own top-level admin menu named Accounting,
translated as Buchhaltung in German. Its first visible submenu item is
Exports, translated as Exporte in German.
StoreAccountant admin access is controlled through permission actions backed by
WordPress capabilities. The administrator role always has all StoreAccountant
permissions. Other backend roles can be assigned to individual actions through
the Permissions tab on the plugin settings page. See
Permissions for the action model, role assignment rules, and
extension examples.
StoreAccountant uses the native WordPress custom post type list tables so search, pagination, trash, and bulk actions remain available. Above those tables, StoreAccountant renders custom action areas and tabs:
- Tab
Exports: native list forstoreacct_export. - Tab
Export Configurations: native list forstoreacct_config, but without its own visible sidebar menu item. Create New Export: selector on the export overview withQuick Exportas the first option and saved export configurations below it. ChoosingQuick Exportopens the quick export form. Choosing a saved configuration shows an export name field and starts an export from that configuration after submitting a unique name.Create Export Configuration: custom admin page with a form. After saving, the user lands on the saved configuration detail page withConfigurationand any type-specific tabs such asField Mapping.
Both quick exports and exports started from a saved configuration create a new
export record. Configuration-based exports require a unique user-entered export
title. The export list shows the selected configuration name; quick exports
without a configuration relation are shown as Quick Export.
The list view polls queued and processing exports. It also contains preparatory
polling support for future scheduled export run records that store
_storeaccountant_scheduled_for; no current admin workflow creates those
records.
Filter fields are rendered by providers registered through
storeaccountant_export_filter_field_provider. Providers decide support per
export type, so an order export can require date and status filters while a
future customer export may offer no date filter at all.
Form content is displayed in white panels with a grey WordPress-style border. For native tabs, the top panel border is removed to avoid a double line.
Export Generation
StoreAccountant\Export\Exporter orchestrates export generation:
- Read the selected export adapter and saved filter selections from export meta.
- Resolve the selected export adapter through
ExportAdapterRegistry. - Resolve the export renderer through
ExportRendererRegistry. - Resolve the storage adapter through
StorageAdapterRegistry. - Pass an
ExportPayloadwith export type and filter selections to the export adapter. - Ask
ExportDatasetBuilderto build anExportDatasetfrom the export adapter. - Ask the renderer to turn that dataset into an
ExportArtifact. - Build the storage reference through
ExportStoragePathGenerator. - Ask the storage adapter to persist the generated file and attachments with a
StorageFileConfiguration. - Store the internal storage path on the export record.
The built-in order export adapter is OrderExportAdapter; it has the adapter ID
orders, loads WooCommerce orders, and provides an ExportContext, additional
tax context such as known tax rates, and record IDs. The built-in customer export adapter is
CustomerExportAdapter; it has the adapter ID customers and loads
WooCommerce customers through customer/user APIs. The built-in product export
adapter is ProductExportAdapter; it has the adapter ID products, loads
WooCommerce products through WordPress/WooCommerce product APIs, and can include
product variations as separate rows when the product variant export setting is
enabled. ExportContext carries typed
runtime data such as export_type, configuration_id, source items, and
optional adapter values readable through $context->get( 'tax_rates', [] ).
The shared field pipeline lives above concrete adapters in
ExportDatasetBuilder. It collects hookable field providers through
ExportFieldResolver and
FieldProviderRegistry; providers decide support through supports(). Values
are resolved separately through field value providers collected through
FieldValueProviderRegistry; value providers also decide support per context
and field. Field value mutators are collected through
FieldValueMutatorRegistry and run after value providers. Mutators are
chain-style: each mutator receives the FieldValue returned by the previous
mutator.
The dataset builder prepares a FieldCollection once, applies any saved field
mapping for configuration-based exports, and passes the mapped collection with
each source item through the value provider chain. Field mapping can disable
fields and override labels; records still only carry field IDs and values. This
keeps CSV, XML, XLSX, and future renderers independent from WooCommerce-specific
data collection. The bundled CSV renderer turns the dataset into a temporary CSV
file.
Order source querying uses WC_Order_Query. Runtime filters are registered
through storeaccountant_export_filter and receive the query object through an
apply() method. The built-in order_date filter applies a selected month/year
period to one WooCommerce date query field such as date_created,
date_modified, date_completed, or date_paid. The built-in order_status
filter applies selected WooCommerce order statuses. Periods used by date-like
filters are selected in the shop timezone, resolved to UTC, and converted to
timestamps for WooCommerce order query arguments.
Export attachment providers are collected through
ExportAttachmentProviderRegistry via storeaccountant_export_attachment_provider.
They run while the dataset builder still has access to each source item and add
streamed ExportAttachment files to the dataset. Renderers ignore attachments;
storage adapters receive them through StorageFileConfiguration. The bundled
local storage adapter stores attachments as additional entries inside the same
zip archive as the generated export file. Attachment providers declare a base
directory for their files and can place concrete attachments in nested
subdirectories.
Reusable tax provider contracts and built-in tax field providers live below the
top-level StoreAccountant\Tax namespace so the module can grow beyond order
exports later. The current order tax field providers live below
StoreAccountant\Tax\Field\Provider. Order-specific selection and WooCommerce
order tax rate discovery live below StoreAccountant\Order\Tax through
OrderTaxFieldProviderRegistry and OrderTaxRateResolver. Selectable tax field
providers are collected through the registry and also register into the regular
export field provider hook. The built-in extended provider creates separate
tax item, shipping, and total fields per WooCommerce tax rate. The built-in
simple provider aggregates tax values into two compact columns. Saved order
export configurations store the selected tax field provider, the admin tax field
provider field renders that selection, and OrderTaxFieldValueProvider resolves
the selected provider's values through the normal field value provider pipeline.
Field and field value providers use shared extension hooks, even though the
built-in adapter ID is plural orders:
storeaccountant_export_adapter
storeaccountant_export_attachment_provider
storeaccountant_export_configuration_form_field_provider
storeaccountant_export_configuration_tab_provider
storeaccountant_export_read_tab_provider
storeaccountant_export_filter
storeaccountant_export_filter_field_provider
storeaccountant_export_filter_period_provider
storeaccountant_export_field_provider
storeaccountant_export_field_value_provider
storeaccountant_export_field_value_mutator
storeaccountant_export_order_tax_field_provider
storeaccountant_export_renderer
storeaccountant_export_batch_size
storeaccountant_export_queue_debug_delay_seconds
storeaccountant_export_log_entry_limit
storeaccountant_export_polling_scheduled_window_seconds
storeaccountant_invoice_plugin
storeaccountant_permission_action
storeaccountant_plugin_settings_tab_provider
storeaccountant_queue_transport_provider
storeaccountant_storage_adapter
Field value mutators use the shared hook
storeaccountant_export_field_value_mutator, because their supports() method
decides whether they apply to a mapped field.
The field resolver always moves metadata-backed custom fields to the end of the available field collection before applying saved field mappings. For order exports this means order tax fields remain before custom order fields. For customer and product exports it means custom metadata fields are also appended after all fixed fields.
The shared metadata helper classes live below StoreAccountant\Export\Field\Meta.
Order, customer, and product metadata providers use these helpers to discover
scalar WooCommerce metadata keys from the current export context, create stable
field IDs with type-specific prefixes such as order_meta_, customer_meta_,
and product_meta_, and format metadata values. Entity-specific providers still
own their reserved metadata key lists because built-in fields differ.
The order dataset contains accounting-relevant order, billing, total, fee,
shipping, tax, and custom metadata fields. Invoice fields are intentionally outside the core
order field provider and live in StoreAccountant\Invoice\Export\Order\Field.
The bundled
WooCommerce PDF Invoices & Packing Slips integration registers itself through
storeaccountant_invoice_plugin with the plugin ID
woocommerce-pdf-invoices-packing-slips. Administrators enable at most one
active invoice plugin integration on the plugin settings page. The generic
invoice field provider contributes invoice_number, invoice_date, and one
typed invoice_file_name_{type} field per invoice file type, for example
invoice_file_name_pdf and invoice_file_name_xml, for the orders export
type only when an invoice plugin integration is enabled. Its value provider
reads through that enabled integration. The bundled integration reads the
invoice document API without
creating a missing invoice, with stored order metadata as fallback. It also
exposes invoice files through get_invoice_file() for workflows that need the
source document rather than a CSV-safe field value. The WooCommerce PDF Invoices
& Packing Slips integration offers PDF files and, when the plugin's EDI/XML
feature is available, XML e-invoices. Invoice file types are selected per export
configuration.
Customer exports include customer identity fields, billing and shipping address
fields, order count, total spent, date fields, and scalar custom customer
metadata fields not already covered by dedicated built-in fields. Customer date
filters use the customer creation date. The shared month/year period selector
supports all time, this month, last month, and concrete month/year selections
for order and customer exports. The customer country filter can restrict exports
by billing or shipping country; selecting all countries leaves the export
restricted to customers with an assigned country code, and selecting
Unassigned also includes customers where the selected country field is missing
or empty. Combining all countries with unassigned covers all WooCommerce
customers in the export period. The country selector only lists countries found
on WooCommerce customers with at least one order and promotes the shop base
country when available. Customer queries are intentionally not limited to the WordPress
customer role, because WooCommerce customers can also have roles such as
administrator or shop manager. Instead, customer exports include only registered
users whose WooCommerce customer record has at least one order.
Product exports include product identity fields, parent product IDs and SKUs,
catalog status, prices, tax settings, stock data, dimensions, categories, tags,
attributes, variation attributes, descriptions, and scalar custom product
metadata fields not already covered by dedicated built-in fields. Product date
filters use the product creation date and currently expose all time, this month,
and last month selections. The product variant export setting defaults to parent
products only; when enabled, WooCommerce product_variation records are loaded
as separate export rows alongside products.
Selected invoice files are written below the localized invoice attachment
directory in the generated zip archive. In English this is Invoices/{type}/;
in German it is Rechnungen/{type}/. The typed invoice_file_name_{type}
export fields list the same file names used for those attachments. Invoice
attachments are generated only for file types that are both selected in the
invoice attachment settings and enabled in the field mapping. Extended tax
columns are based on all configured WooCommerce tax rates, not only rates
present in the exported orders. Their headers follow
tax_{rate}_{tax-name-slug}_{country}_{items|shipping|total}.
If an export configuration contains old invoice mapping entries but the selected
invoice plugin integration is no longer active or no longer enabled,
InvoiceFieldProvider::supports() returns false. The invoice fields are then no
longer part of the available FieldCollection, so the field mapping repository
omits those stored entries from the form and from generated export datasets.
Manual export creation now stores the export record first and dispatches a
queued background message instead of generating the file directly inside an
admin request. The queue infrastructure lives below
StoreAccountant\Queue; export-specific queue messages and handlers live below
StoreAccountant\Export\Queue.
Export start actions dispatch through Symfony's MessageBusInterface. The
StoreAccountant bus implementation sends normal message DTOs to the configured
transport. The default free transport is an
action_scheduler://exports-style Action Scheduler transport implemented below
StoreAccountant\Queue\Transport. It uses Symfony Messenger's transport
serializer to encode and decode envelopes. When Action Scheduler is unavailable,
the same transport falls back to WordPress cron for local development and
minimal environments. Export orchestration depends on Messenger and
Symfony's handler locator rather than direct Action Scheduler calls.
The current batch implementation uses:
StartExportMessageto count source records and enqueue batches.ProcessExportBatchMessageto load deterministic order/customer slices, normalize those slices, persist temporary batch fragments, and update progress counters.FinalizeExportMessageto stream saved batch fragments into the configured renderer and persist the final export file through the selected storage adapter.
Orders and customers expose batch access through
BatchExportAdapterInterface. Batch queries remain HPOS-compatible by using
WooCommerce APIs for orders and WordPress/WooCommerce customer APIs for
customers. Temporary fragments are stored in a protected upload subdirectory and
are deleted after successful finalization or after stale processing jobs time
out. Final export records and generated files are never removed by automatic
queue cleanup.
Each export stores its own _storeaccountant_batch_size; saved export
configurations store _storeaccountant_config_batch_size and copy that value to
new exports created from the configuration. Quick
exports store the submitted batch size directly on the export record. The
minimum batch size is 10; the default batch size is 50.
Scheduled exports are not configured on export configuration records. Future scheduled export workflows should use a focused scheduling model instead of adding schedule controls back to the configuration form. Cleanup never deletes completed export records or generated final files; it only marks stale processing jobs as failed.
Storage
The bundled local storage adapter keeps the ID local and writes through
StoreAccountant\Storage\Adapter\LocalStorageAdapter. Public storage adapters
only need to provide ID, persistence, deletion, download file retrieval,
file existence checks, and readiness checks.
get_file() returns a StorageFile stream descriptor so download handling can
stay storage-agnostic.
Folder, file names, and display paths are generated before the storage adapter is called.
The local adapter receives its root and display paths through
LocalStorageConfiguration.
Local zip storage references point to the archive itself, for example
exports/{download-token}.zip. The path of the generated CSV/XML inside the
archive is carried separately while persisting the file. Remote storage
references use the same token as the object key base, for example
exports/{download-token}.csv.
The local adapter additionally implements Flysystem's adapter interface
internally so it can decorate League's ZipArchive adapter.
The local storage root is:
The local Flysystem adapter decorates League's ZipArchive adapter and stores generated export files inside the zip archive referenced by the generated storage path:
Generated export files are downloaded through the frontend
/storeaccountant/export-download/{token}/ endpoint or its query-argument
fallback. The endpoint resolves the export by token, verifies completed status,
checks the configured storage adapter, requires the export password, and then
streams the StorageFile returned by StorageAdapterInterface::get_file().
It never exposes direct upload URLs or assumes local filesystem paths.
Saved export titles are validated as unique before the export custom post type record is created. Storage file names are independent from titles and use the download token so stored paths do not reveal accounting export names.
When the storage root is created, protection files similar to WooCommerce are created by the local adapter:
.htaccessindex.html
Absolute server paths must not be displayed in admin error messages. Internal
export meta stores paths inside the storage adapter, for example
exports/accounting-may-2026.zip; admin display paths start with
wp-content/....
On plugin deactivation, the directory is deleted only when it does not contain foreign files or subdirectories. Managed protection files may be removed.