CoolCMS API

InvoiceManager extends Manager
in package
uses SaveTrait, DecimalQuantityGuardTrait

Base class of Api managers whose traits delegate to a typed `Service<TEntity>`.

Tags
extends

Table of Contents

Constants

META_PDF_ONLY_ON_CHANGE  : string = 'pdfOnlyOnChange'
Save `$meta` key: regenerate the PDF/QR only when something printed in the PDF changed (or the file is missing). Sent only by the invoice form save in the admin; without it (order and payment generators, scheduler, Api) the PDF is generated on every save.
META_SEND_EMAIL_ON_SAVE  : string = 'sendEmailOnSave'
Meta of the admin form: after the save send the invoice by e-mail when "Send by e-mail" is checked and it was not sent yet ({@see sendEmailAfterSave()}). Only the admin passes it; the schedule and the payment invoice send on their own paths, external meta is stripped.
SAVE_INFO_EMAIL  : string = 'invoiceEmail'
`saveInfo` key with the outcome of {@see META_SEND_EMAIL_ON_SAVE} (`InvoiceEmailState` value).
DELETED_IDS_META  : string = '__deletedIds'
Reserved `$meta` key with the ids of the entities being deleted, captured before the first hook.
TABLE_INVOICE_ITEMS  : string = 'cms_mod_invoicer_invoice_items'
Tables whose `quantity` becomes `decimal(8,2)` (elsewhere it stays an integer piece count).
TABLE_INVOICE_TEMPLATE_ITEMS  : string = 'cms_mod_invoicer_invoice_template_items'
INVOICE_FK  : string = 'cms_mod_invoicer_invoices_invoice_id'
META_PDF_FINGERPRINT_BEFORE  : string = '__pdfFingerprintBefore'
Internal `$meta` key: fingerprint of the printed data computed from the database before saving (`beforeSave()` to `completedSave()`).
PAYMENT_DEFAULT_BATCH  : int = 10
PAYMENT_QUEUE  : string = 'invoicer.payment.invoice'
PDF_FINGERPRINT_LINES  : array<string|int, mixed> = ['cms_mod_invoicer_invoice_items' => 'invoice_i...
Invoice lines that go into the PDF (items, shipping, payment, gifts, vouchers): table => primary key.
PDF_FINGERPRINT_SKIP_HEAD  : array<string|int, mixed> = ['pdf', 'qr_code', 'update_datetime', 'send_ema...
Header columns that are not printed in the PDF/QR (email block, file pointers, update timestamp); only these are left out of the fingerprint. Everything else (seller and customer snapshot, VAT mode, language, localization, PDF variant, sums) is included.
SETTING_MODULE  : int = 7
TABLE_INVOICES  : string = 'cms_mod_invoicer_invoices'

Properties

$service  : Service<string|int, TEntity>
Manager service; subclasses narrow the type (`@property` in the Api/Admin/Front manager).
$appContext  : AppContext
$assetFormatRegistry  : AssetFormatRegistry|null
Format processor registry for file uploads, used by {@see \App\Core\Traits\Api\Models\Managers\AssetProcessingTrait}.
$cacheTagInvalidator  : CacheTagInvalidator|null
Post-commit invalidator of the wire-payload query cache (tag bump per entity class).
$defaultSkipFields  : array<int, string>
`pdf` and `qrCode` are filled only through `updateColumn` in the PDF/QR generator (post-commit), never by an object-graph save. Skipping them in hydration keeps a normal invoice save from overwriting them with the empty value from the form.
$emailManager  : EmailManager|null
Sends the invoice e-mail after an admin save ({@see sendEmailAfterSave()}). A plain property with a default: unit tests build the manager without the constructor and nothing is sent then.
$internalCallContext  : InternalCallContext|null
Tells whether the call is an internal bridge call; the single source for reading uploaded items with `path` ({@see readUploadedItem()}).
$languageContext  : LanguageContext|null
$nonInvalidatingColumns  : array<string|int, string>
Columns whose update does not invalidate the cache: view counters (`registerEvent()` bumps the counter on every pageview). Without this every view would flush the query cache of the whole class; counters are therefore stale up to the TTL. Override per manager by redefining the property.
$paramsConfig  : Config|null
Neon parameters (`%params%`: server paths and directories), set by the decorator (`decorators.neon`) like `setAppContext()`.
$presenterContext  : PresenterContext
$settingContext  : SettingContext
$totalCount  : int|null
$useTransactionForDelete  : bool
$useTransactionForSave  : bool
$calculator  : InvoiceCalculator
$cronFailureManager  : CronFailureManager
$currencyManager  : CurrencyManager
$decimalQuantityColumns  : array<string, bool>
Whether `quantity` is a decimal column, per table. Valid for this request only (the schema does not change during a request; the alter is a maintenance step between requests).
$em  : EntityManagerInterface
$generator  : InvoiceGenerator
$invoiceService  : InvoiceService
$numberGenerator  : InvoiceNumberGenerator
$paymentManager  : PaymentManager
$paymentStatusRuleManager  : PaymentStatusRuleManager
$pdfGenerator  : InvoicePdfGenerator
$rejectedUploads  : array<int, array{name: string, reason: string}>
New uploaded items that the Api did not save during the `save()` that is running right now (type, dimensions, unreadable, …). Filled by {@see recordRejectedUpload()} and returned in `saveInfo.rejectedUploads`.

Methods

__construct()  : mixed
delete()  : Result
Deletes the entity or entities by id. Optional hooks (opt-in via `method_exists`, like {@see SaveTrait}) allow cleanup of derived or denormalized state during the delete:
deleteAll()  : Result
find()  : T|null
findAll()  : T> : list>)
findBy()  : T|null
findLatestNonCreditForOrder()  : array{invoiceId: int, number: ?string, pdf: ?string, qrCode: ?string, variableSymbol: ?string, dueDate: ?string, status: ?string, bankAccount: ?string, iban: ?string}|null
Returns the latest non-credit-note document of an order (proforma or invoice). Invoicer owns the documents, so the eshop checkout return page asks here instead of joining Invoicer tables itself.
findRelatedIds()  : array<int, int>|null
Returns the ids of target entities that already belong to the record `$parentId` through the relation collection `$property` (for example `('assetRels', 'asset')`, `('fileRels', 'file')`). Reads straight from the database, not through the Api read path (no `UoW::clear()` in the middle of save hooks).
getAppContext()  : AppContext
getDefaultSkipFields()  : array<string|int, string>
Returns keys that the generic `service->save()` must not copy onto the managed entity.
getEntityClass()  : TEntity>
Returns the root Doctrine entity class managed by this manager.
getExternalMetaAllowlist()  : array<string|int, string>
Returns the meta keys allowed through the external HTTP path ({@see \App\Core\Traits\Api\Presenters\SaveTrait::actionSave()}).
getExternalOwnerData()  : array<string, mixed>
Returns `data` keys that fill the owner from the server when an external save creates a record.
getExternalRemoteIpData()  : array<string, mixed>
Returns `data` keys that fill the caller's IP from the server when an external save creates a record; sibling of {@see getExternalOwnerData()}.
getExternalUpdateColumnsAllowlist()  : array<string|int, string>
Returns the columns the external HTTP path may change through {@see \App\Core\Traits\Api\Presenters\SaveTrait::actionUpdateColumns()}.
getGraphDataKeys()  : array<string|int, string>
Returns the keys an external client may send nested in `data`.
getLanguageContext()  : LanguageContext|null
getPresenterContext()  : PresenterContext
getServiceClass()  : class-string
Returns the fully qualified service class name, the identity for the per-service rollout allowlist of the wire-payload query cache ({@see \App\Core\Cache\Config::isServiceEnabled()}).
getSettingContext()  : SettingContext
getSettings()  : array<int, array<string, mixed>>
getTotalCount()  : int|null
initializeAllContexts()  : void
lock()  : Result
Locks an invoice explicitly (administrator action). Irreversible; an already locked invoice keeps its original time and reason (returns success, `alreadyLocked = true`).
lockedAt()  : string|null
Returns the lock time of an invoice read straight from the database (raw DBAL: no Api read, so no `UoW::clear()` in the middle of save hooks). `null` when the invoice is not locked or does not exist.
processPaidPayments()  : array{processed: int, generated: int, emailed: int, skipped: int, ignored: int, errors: list}
Periodically issues invoices from payments by the rules "payment status to invoice type" (cron `cron/invoicer/payment/generate`). Generation is separate from the payment gateway callback (no PDF/QR is built in the gateway response) and serialized (the base cron holds {@see \App\UI\Api\System\Models\Managers\CronLockManager}).
registerEvent()  : Result
save()  : Result
setAppContext()  : $this
setAssetFormatRegistry()  : static
setCacheTagInvalidator()  : static
setInternalCallContext()  : static
setLanguageContext()  : $this
setParamsConfig()  : static
setPresenterContext()  : $this
setSettingContext()  : $this
updateAllRelations()  : Result
updateColumn()  : Result
updateColumns()  : Result
Writing `emailIsSent = true` locks the invoice, whoever writes it (admin through Api `send-email`, payment generator, scheduler). This `updateColumns()` is the only place the flag is written, so another sending path cannot bypass the lock.
updateRelations()  : Result
afterSave()  : Result|null
Locks the source of a credit note or cancellation. Runs inside the save transaction, so a credit note without a locked source cannot exist (a failed lock rolls the credit note back).
beforeDelete()  : Result|null
Blocks hard delete of an invoice with a number from a billing line.
beforeSave()  : Result|null
Domain core of the invoice: authoritative recalculation of totals from the collections (client numbers are never trusted); on insert without a number it generates the number, order and `period_key`, and fills in the variable symbol.
beginRejectedUploads()  : array<int, array{name: string, reason: string}>
Starts a save ({@see save()}): the list of rejected items belongs to this save only; the outer one (a nested save of the same manager) is returned to the caller and restored after the save through {@see finishRejectedUploads()}.
bumpCacheTags()  : void
Bumps cache tags for the entity after commit: its class, the owned-cascade classes from metadata and the optional `additionalCacheTags()` of the manager.
bumpCacheTagsExplicit()  : void
Bumps the given classes explicitly (raw-DBAL paths with a cross-entity effect).
bumpCacheTagsForClass()  : void
Bumps cache tags by class name when there is no instance (bulk delete, `updateColumns()`, raw DBAL, tree operations).
classCacheTags()  : array<string|int, string>
Returns extra tags for a bump that has no entity, typically tree operations ({@see \App\Core\Traits\Api\Models\Managers\TreeTrait::treeTransaction()}) that rewrite `lft`/`rgt` on many rows.
completedSave()  : void
After the commit: PDF/QR ({@see refreshPdfAfterSave()}), then the invoice e-mail of an admin save ({@see META_SEND_EMAIL_ON_SAVE}); both best effort.
finishRejectedUploads()  : void
Ends a save: whatever {@see recordRejectedUpload()} wrote during it is added to the data of a successful result as `saveInfo.rejectedUploads` (the Api presenter passes it on to the caller, the admin builds a warning from it). A rejected item does not fail the save; the accepted files stay.
getDeletedId()  : int|null
Returns the id `$entity` had before the delete, from the snapshot in `$meta` that {@see delete()} hands to every hook. Null for an entity the snapshot does not know (not loaded by this delete, or without a simple id).
getService()  : Service<string|int, TEntity>
Returns the service backing the manager traits.
readUploadedItem()  : string|null
Reads the content of an uploaded item (`{name, data}` base64 or `{name, path}` from the dropzone) through the single reader {@see UploadedItem}.
recordRejectedUpload()  : void
Records a new uploaded item that the save skipped ({@see UploadRejection} `REASON_*`).
bumpDeleteCacheTags()  : void
Post-commit bump after a delete. With entities it goes through {@see \App\Core\Base\BaseManager::bumpCacheTags()}, which also collects `additionalCacheTags()` (Comment → CommentThread); without them it falls back to bumping the manager class itself, the original behavior.
captureDeletedIds()  : WeakMap<object, int>
Keys the ids by the loaded entity objects, not by the input ids: an input id that loaded nothing must not shift the pairing. A weak map, so the snapshot never keeps an entity alive.
columnsInvalidateCache()  : bool
deleteBatch()  : Result
Bulk delete of a list of ids as one transaction. `Service::deleteAll()` flushes per entity; in autocommit the rows deleted before a database failure would stay deleted while the caller got either an escaped `EntityManagerClosed` or an "OK" with an error text. Here the service stops at the technical failure, this wrapper rolls the whole batch back and returns the failure Result with `deletedCount = 0` and `rolledBack = true`: nothing of the batch is durable and nothing is attempted over the closed EntityManager. A domain partial success (missing ids) keeps the existing contract and is committed.
duePayments()  : array<int, Payment>
Returns payments of the status not yet invoiced for it (`invoiced_status` is null or differs from the status). Parked payments (in backoff) are skipped; ordered by id (deterministic).
findFractionalQuantity()  : string|null
Returns the first fractional quantity in the collection, or `null`. Compared with `fmod()`, not by string shape: the setter also receives `int`/`float`, and after hydration the getter returns the database spelling, which may differ ('2.5' vs '2.50').
guardFractionalQuantity()  : Result|null
Rejects the write when a line has a fractional quantity and the column is still an integer.
hasDecimalQuantityColumn()  : bool
Tells whether `quantity` of the table is already `decimal`; unknown counts as no (rejecting is safer).
isAssetPathAllowed()  : bool
Checks only entities composing {@see AssetPathTrait}; the trait is the allowlist of "relative file path" columns, other `assetPath`-like fields are untouched.
loadEntitiesForCacheTags()  : array<int, object>
Loads the entities being deleted only to find out their `additionalCacheTags()`. Fail-soft: what cannot be loaded is simply missing and the bump falls back to the class; it must never stop the delete.
lockRows()  : int
Locks rows that are not locked yet (conditional `UPDATE`; the first lock wins).
noInvoicingCurrencyIds()  : array<int, int>
Returns ids of currencies with invoicing off: `Currency.no_invoicing` or a non-monetary currency (`monetary = 0`, credits). A payment in credits (topping) never has an invoice, even if someone unticks `no_invoicing` on the currency.
pdfFingerprint()  : string|null
Returns a fingerprint of everything printed in the invoice PDF/QR: the whole header (except {@see PDF_FINGERPRINT_SKIP_HEAD}) and the content of all line kinds ({@see PDF_FINGERPRINT_LINES}), read with raw DBAL straight from the database. Called both before and after saving.
refreshPdfAfterSave()  : void
Regenerates QR and PDF after commit. Runs outside the save transaction, best effort: `SaveTrait` wraps the call in try/catch (an exception is only logged), so a generator failure must not break an already saved invoice (fail-open).
rollbackBatch()  : void
rulesByStatus()  : array<int, int>
Returns active rules grouped as payment status to invoice type (Api `findAll()` returns an array). One status maps to at most one type (unique on `payment_status`); a later row would overwrite a duplicate, which database integrity does not allow anyway.
saveWithHooks()  : Result
Runs the save itself: transaction, `beforeSave` / `afterSave` / `completedSave`, tree, cache.
sendEmailAfterSave()  : InvoiceEmailState|null
Admin save with "Send by e-mail" checked: sends the invoice (PDF attached) with its own subject and body under the admin template ({@see InvoiceEmailTemplate::Admin}, straight to the recipient) unless it was sent already.
settingBool()  : bool
settingInt()  : int
settleWithoutInvoice()  : void
Settles a payment without a document (currency with invoicing off, or a payment with no customer invoice). `ignored` counts only when the `invoiced_status` watermark was really written; an unwritten watermark is an error with backoff, otherwise the payment would silently return to the queue.
usesAssetPathTrait()  : bool

Constants

META_PDF_ONLY_ON_CHANGE

Save `$meta` key: regenerate the PDF/QR only when something printed in the PDF changed (or the file is missing). Sent only by the invoice form save in the admin; without it (order and payment generators, scheduler, Api) the PDF is generated on every save.

public string META_PDF_ONLY_ON_CHANGE = 'pdfOnlyOnChange'

filterExternalMeta drops it from the external Api (not in the allowlist).

META_SEND_EMAIL_ON_SAVE

Meta of the admin form: after the save send the invoice by e-mail when "Send by e-mail" is checked and it was not sent yet ({@see sendEmailAfterSave()}). Only the admin passes it; the schedule and the payment invoice send on their own paths, external meta is stripped.

public string META_SEND_EMAIL_ON_SAVE = 'sendEmailOnSave'

SAVE_INFO_EMAIL

`saveInfo` key with the outcome of {@see META_SEND_EMAIL_ON_SAVE} (`InvoiceEmailState` value).

public string SAVE_INFO_EMAIL = 'invoiceEmail'

DELETED_IDS_META

Reserved `$meta` key with the ids of the entities being deleted, captured before the first hook.

protected string DELETED_IDS_META = '__deletedIds'

Read it only through getDeletedId().

TABLE_INVOICE_ITEMS

Tables whose `quantity` becomes `decimal(8,2)` (elsewhere it stays an integer piece count).

protected string TABLE_INVOICE_ITEMS = 'cms_mod_invoicer_invoice_items'

TABLE_INVOICE_TEMPLATE_ITEMS

protected string TABLE_INVOICE_TEMPLATE_ITEMS = 'cms_mod_invoicer_invoice_template_items'

INVOICE_FK

private string INVOICE_FK = 'cms_mod_invoicer_invoices_invoice_id'

META_PDF_FINGERPRINT_BEFORE

Internal `$meta` key: fingerprint of the printed data computed from the database before saving (`beforeSave()` to `completedSave()`).

private string META_PDF_FINGERPRINT_BEFORE = '__pdfFingerprintBefore'

PAYMENT_QUEUE

private string PAYMENT_QUEUE = 'invoicer.payment.invoice'

PDF_FINGERPRINT_LINES

Invoice lines that go into the PDF (items, shipping, payment, gifts, vouchers): table => primary key.

private array<string|int, mixed> PDF_FINGERPRINT_LINES = ['cms_mod_invoicer_invoice_items' => 'invoice_item_id', 'cms_mod_invoicer_invoice_shippers' => 'invoice_shipper_id', 'cms_mod_invoicer_invoice_payment_methods' => 'invoice_payment_method_id', 'cms_mod_invoicer_invoice_gifts' => 'invoice_gift_id', 'cms_mod_invoicer_invoice_vouchers' => 'invoice_voucher_id']

The PK is left out of the fingerprint (it is line content, not identity) and is used for ordering.

PDF_FINGERPRINT_SKIP_HEAD

Header columns that are not printed in the PDF/QR (email block, file pointers, update timestamp); only these are left out of the fingerprint. Everything else (seller and customer snapshot, VAT mode, language, localization, PDF variant, sums) is included.

private array<string|int, mixed> PDF_FINGERPRINT_SKIP_HEAD = ['pdf', 'qr_code', 'update_datetime', 'send_email', 'email_to', 'email_subject', 'email_body', 'email_is_sent']

When in doubt, a column belongs in.

TABLE_INVOICES

private string TABLE_INVOICES = 'cms_mod_invoicer_invoices'

Properties

$service

Manager service; subclasses narrow the type (`@property` in the Api/Admin/Front manager).

public Service<string|int, TEntity> $service

$assetFormatRegistry

Format processor registry for file uploads, used by {@see \App\Core\Traits\Api\Models\Managers\AssetProcessingTrait}.

protected AssetFormatRegistry|null $assetFormatRegistry = null

Set by a decorator (decorators.neon) like setParamsConfig(): more than ten managers process assets and each has its own constructor, so constructor injection would touch all of them.

$cacheTagInvalidator

Post-commit invalidator of the wire-payload query cache (tag bump per entity class).

protected CacheTagInvalidator|null $cacheTagInvalidator = null

Set by the decorator (decorators.neon) like setParamsConfig(). Nullable and fail-open: without wiring (or without Redis) a bump is a no-op.

$defaultSkipFields

`pdf` and `qrCode` are filled only through `updateColumn` in the PDF/QR generator (post-commit), never by an object-graph save. Skipping them in hydration keeps a normal invoice save from overwriting them with the empty value from the form.

protected array<int, string> $defaultSkipFields = ['pdf', 'qrCode', 'lockedDatetime', 'lockReason']

$emailManager

Sends the invoice e-mail after an admin save ({@see sendEmailAfterSave()}). A plain property with a default: unit tests build the manager without the constructor and nothing is sent then.

protected EmailManager|null $emailManager = null

$internalCallContext

Tells whether the call is an internal bridge call; the single source for reading uploaded items with `path` ({@see readUploadedItem()}).

protected InternalCallContext|null $internalCallContext = null

Only ApiBridge::internalCall() sets it, never the payload and not the RBAC guard, so the emergency apiRbac.internalMode: off cannot disable it. Set by a decorator like setAssetFormatRegistry(). Without it (direct manager call from cron or a test) the call counts as external.

$nonInvalidatingColumns

Columns whose update does not invalidate the cache: view counters (`registerEvent()` bumps the counter on every pageview). Without this every view would flush the query cache of the whole class; counters are therefore stale up to the TTL. Override per manager by redefining the property.

protected array<string|int, string> $nonInvalidatingColumns = ['views', 'viewsList', 'viewsDetail']

Covers all view counters across modules: views (Blog Article), viewsList (list impression of Advert/Message) and viewsDetail (detail view of Advert/Message, registerEvent() in the *DetailTraits). IMPORTANT: viewsDetail must stay in the list, otherwise every open of an advert/post detail would invalidate all lists of the class.

$paramsConfig

Neon parameters (`%params%`: server paths and directories), set by the decorator (`decorators.neon`) like `setAppContext()`.

protected Config|null $paramsConfig = null

Write logic (asset directories) reads paths from here, not from getSettings() or the setting context, which may be filled by the client without server paths. The property cannot be named $config: it collides with the promoted $config in some managers.

$useTransactionForDelete

protected bool $useTransactionForDelete = true

$useTransactionForSave

protected bool $useTransactionForSave = true

$decimalQuantityColumns

Whether `quantity` is a decimal column, per table. Valid for this request only (the schema does not change during a request; the alter is a maintenance step between requests).

private static array<string, bool> $decimalQuantityColumns = []

$rejectedUploads

New uploaded items that the Api did not save during the `save()` that is running right now (type, dimensions, unreadable, …). Filled by {@see recordRejectedUpload()} and returned in `saveInfo.rejectedUploads`.

private array<int, array{name: string, reason: string}> $rejectedUploads = []

Methods

__construct()

public __construct(InvoiceService $invoiceService, InvoiceNumberGenerator $numberGenerator, InvoiceCalculator $calculator, InvoicePdfGenerator $pdfGenerator, InvoiceGenerator $generator, PaymentManager $paymentManager, PaymentStatusRuleManager $paymentStatusRuleManager, CurrencyManager $currencyManager, EntityManagerInterface $em, CronFailureManager $cronFailureManager[, EmailManager|null $emailManager = null ]) : mixed
Parameters
$invoiceService : InvoiceService
$numberGenerator : InvoiceNumberGenerator
$calculator : InvoiceCalculator
$pdfGenerator : InvoicePdfGenerator
$generator : InvoiceGenerator
$paymentManager : PaymentManager
$paymentStatusRuleManager : PaymentStatusRuleManager
$currencyManager : CurrencyManager
$em : EntityManagerInterface
$cronFailureManager : CronFailureManager
$emailManager : EmailManager|null = null

delete()

Deletes the entity or entities by id. Optional hooks (opt-in via `method_exists`, like {@see SaveTrait}) allow cleanup of derived or denormalized state during the delete:

public delete(int|array<string|int, mixed> $id[, array<string, mixed> $meta = [] ]) : Result
  • beforeDelete(array $entities, array $meta): ?Result runs before the delete (validation or capturing context while the rows exist). A failed Result means rollback and return. Its data flows into afterDelete under $meta['__beforeDelete'].
  • afterDelete(array $entities, Result $result, array $meta): ?Result runs after the delete, still in the transaction. It is only for recomputing derived state from the remaining rows (toped_until, member_until, county), idempotent and reversible. A failure rolls back the delete itself (atomicity). It is also the place for the mandatory delete of dependents through a foreign manager (comment, rating and message threads): an owner still managed in the unit of work references them, so they must go after its row, never before it in beforeDelete (collect their ids there). Additive or irreversible effects (credits, e-mails, invoices) do not belong here, only into completedDelete.
  • completedDelete(array $entities, Result $result, array $meta): void runs best effort after the commit, outside the transaction; an exception is only logged (the delete stays).
  • completedAssetCleanup(array $entities, Result $result, array $meta): void runs best effort after the commit and releases the Asset rows and files the deleted entity owned. The body is in AssetProcessingTrait; opt-in is ownedAssetIds(object $entity): array on the manager, without it the hook does not run.

IMPORTANT: the cleanup has its own name instead of completedDelete() because many Api managers define completedDelete() themselves and a class method overrides a trait method without a warning, so the cleanup would silently be missing in them.

IMPORTANT: a soft delete (deleted = 1) goes through save(), not here; assets must not be released there because the record can be restored.

$entities are the entities loaded before the delete (after the DELETE the row is gone, but the PHP object keeps the data, such as FKs for the recomputation).

IMPORTANT: the id is the exception. Doctrine nulls the id of a removed instance on flush(), so a hook must not rely on getId() once the delete ran (today only the read UoW::clear() in the service find() hands the preloaded entities over detached and keeps their id). Every hook therefore gets the ids captured before beforeDelete under the reserved key DELETED_IDS_META, keyed by the loaded entity object (an input id that loaded nothing has no entry); read it through getDeletedId(). The key is always set here, so a caller-supplied value never survives.

Without hooks it behaves exactly as before (no preloading, no transaction). NOTE: deleteAll() (bulk) and the tree deleteNode take another path and do not trigger these hooks.

Parameters
$id : int|array<string|int, mixed>
$meta : array<string, mixed> = []
Return values
Result

deleteAll()

public deleteAll([array<string|int, mixed> $filters = [] ]) : Result
Parameters
$filters : array<string|int, mixed> = []
Return values
Result

find()

public find(int $id[, string|array<string|int, mixed> $includes = [] ]) : T|null
Parameters
$id : int
$includes : string|array<string|int, mixed> = []
Return values
T|null

findAll()

public findAll([array<string|int, mixed>|null $includes = [] ][, array<string|int, mixed>|null $filters = [] ][, array<string|int, mixed>|null $orderBys = null ][, int|null $offset = null ][, int|null $limit = null ][, array<string|int, mixed>|null $columns = null ][, bool $computeTotalCount = false ]) : T> : list>)
Parameters
$includes : array<string|int, mixed>|null = []
$filters : array<string|int, mixed>|null = []
$orderBys : array<string|int, mixed>|null = null
$offset : int|null = null
$limit : int|null = null
$columns : array<string|int, mixed>|null = null
$computeTotalCount : bool = false
Return values
T> : list>) —

entities, or raw rows when $columns is set

findBy()

public findBy([array<string|int, mixed>|null $filters = [] ][, string|array<string|int, mixed> $includes = [] ]) : T|null
Parameters
$filters : array<string|int, mixed>|null = []
$includes : string|array<string|int, mixed> = []
Return values
T|null

findLatestNonCreditForOrder()

Returns the latest non-credit-note document of an order (proforma or invoice). Invoicer owns the documents, so the eshop checkout return page asks here instead of joining Invoicer tables itself.

public findLatestNonCreditForOrder(int $orderId) : array{invoiceId: int, number: ?string, pdf: ?string, qrCode: ?string, variableSymbol: ?string, dueDate: ?string, status: ?string, bankAccount: ?string, iban: ?string}|null
Parameters
$orderId : int
Return values
array{invoiceId: int, number: ?string, pdf: ?string, qrCode: ?string, variableSymbol: ?string, dueDate: ?string, status: ?string, bankAccount: ?string, iban: ?string}|null

findRelatedIds()

Returns the ids of target entities that already belong to the record `$parentId` through the relation collection `$property` (for example `('assetRels', 'asset')`, `('fileRels', 'file')`). Reads straight from the database, not through the Api read path (no `UoW::clear()` in the middle of save hooks).

public findRelatedIds(int $parentId, string $property, string $targetAssociation) : array<int, int>|null
Parameters
$parentId : int
$property : string
$targetAssociation : string
Return values
array<int, int>|null —

null when it cannot be determined (collection mapping missing / database error)

getDefaultSkipFields()

Returns keys that the generic `service->save()` must not copy onto the managed entity.

public getDefaultSkipFields() : array<string|int, string>

The manager's own afterSave()/beforeSave() sync trait handles them through $meta, not through the object graph. Backed by an optional protected per-manager property. The write Api endpoint (SaveTrait::internalActionSave()) also uses this accessor to strip the same keys from $data before hydration, because defaultSkipFields alone does not protect hydration.

Return values
array<string|int, string>

getEntityClass()

Returns the root Doctrine entity class managed by this manager.

public getEntityClass() : TEntity>

The write Api endpoint (SaveTrait) takes it from here instead of the request, so a client cannot choose the instantiated PHP class.

Return values
TEntity>

getExternalMetaAllowlist()

Returns the meta keys allowed through the external HTTP path ({@see \App\Core\Traits\Api\Presenters\SaveTrait::actionSave()}).

public getExternalMetaAllowlist() : array<string|int, string>

The internal path (ApiBridge::internalCall() calling internalActionSave() directly, admin and front) is not limited by this allowlist and reads $meta unchanged. The default is an empty array, so external meta is dropped entirely; actionSave() is reachable through HTTP routing regardless of ApiBridge, hence the strip. Override per manager with the protected property $externalMetaAllowlist, analogous to self::getDefaultSkipFields().

Return values
array<string|int, string>

getExternalOwnerData()

Returns `data` keys that fill the owner from the server when an external save creates a record.

public getExternalOwnerData(int $userId) : array<string, mixed>

The default [] means the domain has no owner or must not be created through external save (Order uses the dedicated checkout action, User registers another way). On INSERT there is nothing to own, so the owner cannot be verified, only determined: the owner field is #[ExternalReadonly] (the strip removes it from the payload) and SaveTrait::applyExternalOwner() then fills it from the JWT, so the client value never reaches hydration.

IMPORTANT: return the key that matches the column mapping, otherwise the owner is silently not written:

  • Watchdog/Comment/Rating/Message: scalar column, ['userId' => $userId];
  • Advert (Bazaar/Comcat): mapped column, ['insertUserId' => $userId];
  • Bazaar Response/AdvertFollower: the scalar userId has an #[ORM\AttributeOverride] with insertable: false, updatable: false in the Api leaf and the column belongs to the association $user, so ['user' => ['id' => $userId]]; Comcat Response has no $user association, so there it is ['userId' => $userId];
  • Complaint: the owner is the association $user, ['user' => ['id' => $userId]].

['id' => X] is a reference that hydrateForPersist() looks up in the DB (OrmEntityHydrator::isReference()). Called only on the external path and only on INSERT; on UPDATE it would make whoever saves a foreign record its owner.

Parameters
$userId : int

identity from the JWT (never from the request)

Return values
array<string, mixed>

getExternalRemoteIpData()

Returns `data` keys that fill the caller's IP from the server when an external save creates a record; sibling of {@see getExternalOwnerData()}.

public getExternalRemoteIpData(string $ip) : array<string, mixed>

The IP field is #[ExternalReadonly], so the external strip removes it and the client cannot forge it. Without filling, an external insert would have no IP and the "anonymous by IP" deduplication and per-IP manager limits would silently stop working. SaveTrait::applyExternalRemoteIp() fills it from IRequest::getRemoteAddress(), never from the request. The default [] means the entity carries no IP; overrides return the entity field name (['insertIp' => $ip]).

Parameters
$ip : string
Return values
array<string, mixed>

getExternalUpdateColumnsAllowlist()

Returns the columns the external HTTP path may change through {@see \App\Core\Traits\Api\Presenters\SaveTrait::actionUpdateColumns()}.

public getExternalUpdateColumnsAllowlist() : array<string|int, string>

updateColumn on a to-many relation is DELETE-all plus INSERT, so without this filter an external POST with values={userRoles: [...]} would bypass the strip/guard of the save path (privilege escalation). The internal path (ApiBridge::internalCall() calling internalActionUpdateColumns() directly) is not limited. The default is an empty array, so external updateColumns is rejected entirely. Override with the protected property $externalUpdateColumnsAllowlist.

Return values
array<string|int, string>

getGraphDataKeys()

Returns the keys an external client may send nested in `data`.

public getGraphDataKeys() : array<string|int, string>

Before hydration the write Api endpoint (SaveTrait::internalActionSave()) moves them from $data to $meta, where the manager's own afterSave()/beforeSave() sync trait reads them (the same mechanism as internal meta). When the client also sends the same key explicitly in meta, meta wins (compatibility with admin, e.g. the categoryRels collision on Advert). Backed by an optional protected per-manager property, like self::getDefaultSkipFields().

IMPORTANT: promoting a key here does not make its content safe to write as received. When the key carries a sensitive field that the server otherwise dictates (typically a payment or approval flag), the manager must overwrite or verify it server-side on the external path in beforeSave()/afterSave() before its sync trait reads it; otherwise the client sets it itself (privilege escalation). The external path is recognized by $meta['__external'] === true, which only SaveTrait::actionSave() sets (never internalActionSave() alone), so internal calls never get the marker. Example: Bazaar Advert.categoryRels is persisted through the object graph (owned collection outside $graphDataKeys); a client could mark paid itself and get a paid advert without payment, so AdvertManager::beforeSave() overwrites it through CategoryRelsPaidSanitizationTrait::sanitizeExternalCategoryRelsPaid() before flush.

Return values
array<string|int, string>

getServiceClass()

Returns the fully qualified service class name, the identity for the per-service rollout allowlist of the wire-payload query cache ({@see \App\Core\Cache\Config::isServiceEnabled()}).

public getServiceClass() : class-string

The cache is controlled at the Api presenter boundary through this identity.

Return values
class-string

getSettings()

public getSettings() : array<int, array<string, mixed>>
Return values
array<int, array<string, mixed>>

getTotalCount()

public getTotalCount() : int|null
Return values
int|null

lock()

Locks an invoice explicitly (administrator action). Irreversible; an already locked invoice keeps its original time and reason (returns success, `alreadyLocked = true`).

public lock(int $invoiceId[, string $reason = Invoice::LOCK_REASON_MANUAL ]) : Result
Parameters
$invoiceId : int
$reason : string = Invoice::LOCK_REASON_MANUAL
Return values
Result —

data: ['locked' => bool, 'alreadyLocked' => bool]

lockedAt()

Returns the lock time of an invoice read straight from the database (raw DBAL: no Api read, so no `UoW::clear()` in the middle of save hooks). `null` when the invoice is not locked or does not exist.

public lockedAt(int $invoiceId) : string|null
Parameters
$invoiceId : int
Return values
string|null

processPaidPayments()

Periodically issues invoices from payments by the rules "payment status to invoice type" (cron `cron/invoicer/payment/generate`). Generation is separate from the payment gateway callback (no PDF/QR is built in the gateway response) and serialized (the base cron holds {@see \App\UI\Api\System\Models\Managers\CronLockManager}).

public processPaidPayments([int|null $limit = null ]) : array{processed: int, generated: int, emailed: int, skipped: int, ignored: int, errors: list}

The code list PaymentStatusRule decides which invoice a status gets (one status maps to one invoice type, unique on payment_status; edited in the Setting form). For each active rule it takes a batch of payments whose current status has the rule and whose invoiced_status differs from that status (null means never generated), issues the invoice through InvoiceGenerator and moves invoiced_status to the status, so the payment is not selected again for it. Failures by the generator's Result code:

  • transient (missing seller or billing line configuration, failed save): the flag stays and the payment is retried later; generator idempotence prevents duplicates,
  • permanent (DB_NOT_FOUND: orphaned rule or credit note without a source invoice): the payment is settled (invoiced_status moves, the error is marked as permanent in stats); otherwise permanently unbillable payments would eat the batch and starve the rest.

Main switch is 7/invoice.payment.auto. generated counts issued documents (not payments); ignored counts payments in a currency with invoicing off (Currency.no_invoicing or a non-monetary currency such as credits), settled without a document and without an error. A transient failure gets a backoff (CronFailureManager) and selection skips the payment until then; without it a few payments lacking seller configuration would spin the batch.

Parameters
$limit : int|null = null
Return values
array{processed: int, generated: int, emailed: int, skipped: int, ignored: int, errors: list}

registerEvent()

public registerEvent(array<string|int, int> $ids, class-string $entityClass, string $property) : Result
Parameters
$ids : array<string|int, int>
$entityClass : class-string

entity used to translate the property into a column in the expression

$property : string
Return values
Result

save()

public save(TEntity $entity[, array<string, mixed> $meta = [] ][, array<int, string> $skipFields = [] ]) : Result
Parameters
$entity : TEntity
$meta : array<string, mixed> = []
$skipFields : array<int, string> = []
Return values
Result

setParamsConfig()

public setParamsConfig(Config $paramsConfig) : static
Parameters
$paramsConfig : Config
Return values
static

updateAllRelations()

public updateAllRelations(int|array<string|int, mixed> $entityId, string $property, array<string, mixed> $values) : Result
Parameters
$entityId : int|array<string|int, mixed>

id, or a list of ids / composite-id arrays

$property : string
$values : array<string, mixed>

relation property name => value set on every row

Return values
Result

updateColumn()

public updateColumn(int|array<string|int, mixed> $entityId, string $property, mixed $value) : Result
Parameters
$entityId : int|array<string|int, mixed>

id, or a list of ids / composite-id arrays

$property : string
$value : mixed
Return values
Result

updateColumns()

Writing `emailIsSent = true` locks the invoice, whoever writes it (admin through Api `send-email`, payment generator, scheduler). This `updateColumns()` is the only place the flag is written, so another sending path cannot bypass the lock.

public updateColumns(int|array<string|int, mixed> $entityId, array<string, mixed> $values) : Result

The lock goes into the same UPDATE as the flag (for rows not yet locked), so the flag and the lock cannot diverge. An invoice locked earlier (credit note, manual) keeps its original time and reason.

Parameters
$entityId : int|array<string|int, mixed>
$values : array<string, mixed>
Return values
Result

updateRelations()

public updateRelations(int|array<string|int, mixed> $entityId, string $property, array<string|int, object> $relationEntities) : Result
Parameters
$entityId : int|array<string|int, mixed>

id, or a list of ids / composite-id arrays

$property : string
$relationEntities : array<string|int, object>

relation entities with updated values

Return values
Result

afterSave()

Locks the source of a credit note or cancellation. Runs inside the save transaction, so a credit note without a locked source cannot exist (a failed lock rolls the credit note back).

protected afterSave(bool $isInsert, Invoice &$entity[, Result|null $result = null ][, array<string, mixed> &$meta = [] ]) : Result|null

Covers every way a credit note arises: payment cron, eshop (status change and partial credit note), manual credit note from the Invoicer admin.

Parameters
$isInsert : bool
$entity : Invoice
$result : Result|null = null
$meta : array<string, mixed> = []
Return values
Result|null

beforeDelete()

Blocks hard delete of an invoice with a number from a billing line.

protected beforeDelete(array<int, mixed> $entities[, array<string, mixed> $meta = [] ]) : Result|null

InvoiceNumberGenerator assigns MAX(order)+1, so deleting the last invoice would hand its number to the next one (two different invoices under one number in the customer's and accountant's records) and deleting any other would leave a gap in the line. An issued invoice is corrected by cancellation or credit note. An invoice without a number (half-finished import) may be deleted. Bulk deleteAll() skips hooks; Invoicer does not call it.

Parameters
$entities : array<int, mixed>
$meta : array<string, mixed> = []
Return values
Result|null

beforeSave()

Domain core of the invoice: authoritative recalculation of totals from the collections (client numbers are never trusted); on insert without a number it generates the number, order and `period_key`, and fills in the variable symbol.

protected beforeSave(bool $isInsert, Invoice &$entity[, array<string, mixed> &$meta = [] ]) : Result|null

IMPORTANT: no find()/findAll() here (so no GetAllTrait::clearDoctrine()). At this point (beforeSave() runs after hydrateForPersist()) the collections are already a merged managed object graph; the recalculation and the generator work either purely in memory (InvoiceCalculator) or with raw DBAL (InvoiceNumberGenerator), so no reference reattach is needed. Loading anything would clear the graph being saved.

Parameters
$isInsert : bool
$entity : Invoice
$meta : array<string, mixed> = []
Return values
Result|null

beginRejectedUploads()

Starts a save ({@see save()}): the list of rejected items belongs to this save only; the outer one (a nested save of the same manager) is returned to the caller and restored after the save through {@see finishRejectedUploads()}.

protected beginRejectedUploads() : array<int, array{name: string, reason: string}>
Return values
array<int, array{name: string, reason: string}> —

the outer save's list

bumpCacheTags()

Bumps cache tags for the entity after commit: its class, the owned-cascade classes from metadata and the optional `additionalCacheTags()` of the manager.

protected bumpCacheTags(object $entity) : void

Fail-open (the invalidator swallows exceptions). Called from the manager Save/Delete/Tree traits after the commit.

Parameters
$entity : object

bumpCacheTagsExplicit()

Bumps the given classes explicitly (raw-DBAL paths with a cross-entity effect).

protected bumpCacheTagsExplicit(array<string|int, string> $classes) : void
Parameters
$classes : array<string|int, string>

bumpCacheTagsForClass()

Bumps cache tags by class name when there is no instance (bulk delete, `updateColumns()`, raw DBAL, tree operations).

protected bumpCacheTagsForClass(class-string $rootClass[, array<string|int, string> $extraTags = [] ]) : void

Bumps the root class and its owned-cascade classes.

Parameters
$rootClass : class-string
$extraTags : array<string|int, string> = []

classCacheTags()

Returns extra tags for a bump that has no entity, typically tree operations ({@see \App\Core\Traits\Api\Models\Managers\TreeTrait::treeTransaction()}) that rewrite `lft`/`rgt` on many rows.

protected classCacheTags() : array<string|int, string>

IMPORTANT: additionalCacheTags(object) receives an entity, so a class-level bump cannot use it; without this hook a delete would lose the cross-entity tag (Comment to CommentThread). A manager whose cross-entity tag does not depend on a particular entity returns it here.

NOTE: unlike additionalCacheTags(), this hook is declared (with an empty default) instead of being an opt-in through method_exists(); method_exists() is always true in a manager that has the method, which phpstan reports as alreadyNarrowedType.

Return values
array<string|int, string>

completedSave()

After the commit: PDF/QR ({@see refreshPdfAfterSave()}), then the invoice e-mail of an admin save ({@see META_SEND_EMAIL_ON_SAVE}); both best effort.

protected completedSave(bool $isInsert, Invoice $entity, Result $result[, array<string, mixed> $meta = [] ]) : void
Parameters
$isInsert : bool
$entity : Invoice
$result : Result
$meta : array<string, mixed> = []

finishRejectedUploads()

Ends a save: whatever {@see recordRejectedUpload()} wrote during it is added to the data of a successful result as `saveInfo.rejectedUploads` (the Api presenter passes it on to the caller, the admin builds a warning from it). A rejected item does not fail the save; the accepted files stay.

protected finishRejectedUploads(Result|null $result, array<int, array{name: string, reason: string}> $outer) : void
Parameters
$result : Result|null
$outer : array<int, array{name: string, reason: string}>

list from beginRejectedUploads()

getDeletedId()

Returns the id `$entity` had before the delete, from the snapshot in `$meta` that {@see delete()} hands to every hook. Null for an entity the snapshot does not know (not loaded by this delete, or without a simple id).

protected getDeletedId(array<string, mixed> $meta, object $entity) : int|null
Parameters
$meta : array<string, mixed>
$entity : object
Return values
int|null

getService()

Returns the service backing the manager traits.

protected getService() : Service<string|int, TEntity>
Return values
Service<string|int, TEntity>

readUploadedItem()

Reads the content of an uploaded item (`{name, data}` base64 or `{name, path}` from the dropzone) through the single reader {@see UploadedItem}.

protected readUploadedItem(array<string|int, mixed> $item[, bool $strictBase64 = true ][, int|null $maxBytes = null ]) : string|null

Call it right before processing and release the result before the next item, so that files never meet in memory. A path is accepted only during an internal bridge call ($internalCallContext) and only inside the dropzone root. A rejected path is logged to the auth channel and null is returned; callers treat it like unreadable data (skip the item or refuse the save).

Parameters
$item : array<string|int, mixed>
$strictBase64 : bool = true

false for callers that decode loosely (base64_decode($x))

$maxBytes : int|null = null

size limit; for path it is checked before reading

Return values
string|null

recordRejectedUpload()

Records a new uploaded item that the save skipped ({@see UploadRejection} `REASON_*`).

protected recordRejectedUpload(mixed $name, string $reason) : void

Called by the places that drop an item (processOrderedAssets(), advert attachments, product set gallery).

Parameters
$name : mixed
$reason : string

bumpDeleteCacheTags()

Post-commit bump after a delete. With entities it goes through {@see \App\Core\Base\BaseManager::bumpCacheTags()}, which also collects `additionalCacheTags()` (Comment → CommentThread); without them it falls back to bumping the manager class itself, the original behavior.

private bumpDeleteCacheTags(array<int, object> $entities) : void
Parameters
$entities : array<int, object>

captureDeletedIds()

Keys the ids by the loaded entity objects, not by the input ids: an input id that loaded nothing must not shift the pairing. A weak map, so the snapshot never keeps an entity alive.

private captureDeletedIds(array<int, object> $entities) : WeakMap<object, int>
Parameters
$entities : array<int, object>
Return values
WeakMap<object, int>

columnsInvalidateCache()

private columnsInvalidateCache(array<int, string> $columns) : bool
Parameters
$columns : array<int, string>
Return values
bool

deleteBatch()

Bulk delete of a list of ids as one transaction. `Service::deleteAll()` flushes per entity; in autocommit the rows deleted before a database failure would stay deleted while the caller got either an escaped `EntityManagerClosed` or an "OK" with an error text. Here the service stops at the technical failure, this wrapper rolls the whole batch back and returns the failure Result with `deletedCount = 0` and `rolledBack = true`: nothing of the batch is durable and nothing is attempted over the closed EntityManager. A domain partial success (missing ids) keeps the existing contract and is committed.

private deleteBatch(array<int, int|string|array<string|int, mixed>> $ids) : Result

Transaction ownership: the wrapper only opens its own level; inside a caller's transaction a rollback here marks the caller's transaction rollback-only, so its final commit rolls the whole scope back (TransactionManager counter contract, the same as the hooked delete() path).

Parameters
$ids : array<int, int|string|array<string|int, mixed>>
Return values
Result

duePayments()

Returns payments of the status not yet invoiced for it (`invoiced_status` is null or differs from the status). Parked payments (in backoff) are skipped; ordered by id (deterministic).

private duePayments(int $status, int $limit[, array<int, int> $parkedIds = [] ]) : array<int, Payment>
Parameters
$status : int
$limit : int
$parkedIds : array<int, int> = []
Return values
array<int, Payment>

findFractionalQuantity()

Returns the first fractional quantity in the collection, or `null`. Compared with `fmod()`, not by string shape: the setter also receives `int`/`float`, and after hydration the getter returns the database spelling, which may differ ('2.5' vs '2.50').

private findFractionalQuantity(iterable<string|int, mixed> $lines) : string|null
Parameters
$lines : iterable<string|int, mixed>
Return values
string|null

guardFractionalQuantity()

Rejects the write when a line has a fractional quantity and the column is still an integer.

private guardFractionalQuantity(Connection $connection, string $table, iterable<string|int, mixed> $lines) : Result|null

Returns null when all is well (all quantities whole, or the column already altered).

Parameters
$connection : Connection
$table : string
$lines : iterable<string|int, mixed>

item lines of the invoice or template

Return values
Result|null

hasDecimalQuantityColumn()

Tells whether `quantity` of the table is already `decimal`; unknown counts as no (rejecting is safer).

private hasDecimalQuantityColumn(Connection $connection, string $table) : bool
Parameters
$connection : Connection
$table : string
Return values
bool

isAssetPathAllowed()

Checks only entities composing {@see AssetPathTrait}; the trait is the allowlist of "relative file path" columns, other `assetPath`-like fields are untouched.

private isAssetPathAllowed(BaseEntity $entity) : bool
Parameters
$entity : BaseEntity
Return values
bool

loadEntitiesForCacheTags()

Loads the entities being deleted only to find out their `additionalCacheTags()`. Fail-soft: what cannot be loaded is simply missing and the bump falls back to the class; it must never stop the delete.

private loadEntitiesForCacheTags(int|array<string|int, mixed> $id) : array<int, object>
Parameters
$id : int|array<string|int, mixed>
Return values
array<int, object>

lockRows()

Locks rows that are not locked yet (conditional `UPDATE`; the first lock wins).

private lockRows(array<int, int> $invoiceIds, string $reason) : int
Parameters
$invoiceIds : array<int, int>
$reason : string
Return values
int —

number of newly locked rows

noInvoicingCurrencyIds()

Returns ids of currencies with invoicing off: `Currency.no_invoicing` or a non-monetary currency (`monetary = 0`, credits). A payment in credits (topping) never has an invoice, even if someone unticks `no_invoicing` on the currency.

private noInvoicingCurrencyIds() : array<int, int>
Return values
array<int, int>

pdfFingerprint()

Returns a fingerprint of everything printed in the invoice PDF/QR: the whole header (except {@see PDF_FINGERPRINT_SKIP_HEAD}) and the content of all line kinds ({@see PDF_FINGERPRINT_LINES}), read with raw DBAL straight from the database. Called both before and after saving.

private pdfFingerprint(int $invoiceId) : string|null

null means the fingerprint could not be computed (database error, missing invoice), and the caller then generates.

Parameters
$invoiceId : int
Return values
string|null

refreshPdfAfterSave()

Regenerates QR and PDF after commit. Runs outside the save transaction, best effort: `SaveTrait` wraps the call in try/catch (an exception is only logged), so a generator failure must not break an already saved invoice (fail-open).

private refreshPdfAfterSave(bool $isInsert, Invoice $entity, array<string, mixed> $meta) : void

pdf and qrCode are in $defaultSkipFields, so a normal object-graph save skips them; this updateColumns() is their only writer (direct UPDATE after commit, no hooks). null is written too: regeneration first wipes the invoice folder, so the old path would point to a deleted file after a failure; null is a consistent state.

Parameters
$isInsert : bool
$entity : Invoice
$meta : array<string, mixed>

rulesByStatus()

Returns active rules grouped as payment status to invoice type (Api `findAll()` returns an array). One status maps to at most one type (unique on `payment_status`); a later row would overwrite a duplicate, which database integrity does not allow anyway.

private rulesByStatus() : array<int, int>
Return values
array<int, int>

saveWithHooks()

Runs the save itself: transaction, `beforeSave` / `afterSave` / `completedSave`, tree, cache.

private saveWithHooks(TEntity $entity, array<string, mixed> $meta, array<int, string> $skipFields) : Result
Parameters
$entity : TEntity
$meta : array<string, mixed>
$skipFields : array<int, string>
Return values
Result

sendEmailAfterSave()

Admin save with "Send by e-mail" checked: sends the invoice (PDF attached) with its own subject and body under the admin template ({@see InvoiceEmailTemplate::Admin}, straight to the recipient) unless it was sent already.

private sendEmailAfterSave(Invoice $entity) : InvoiceEmailState|null

Fail-open: the invoice stays saved when the e-mail fails; the outcome goes to saveInfo. Reads the stored row (the form does not carry emailIsSent, and the PDF was just written).

Parameters
$entity : Invoice
Return values
InvoiceEmailState|null

settingBool()

private settingBool(string $key) : bool
Parameters
$key : string
Return values
bool

settingInt()

private settingInt(string $key, int $default) : int
Parameters
$key : string
$default : int
Return values
int

settleWithoutInvoice()

Settles a payment without a document (currency with invoicing off, or a payment with no customer invoice). `ignored` counts only when the `invoiced_status` watermark was really written; an unwritten watermark is an error with backoff, otherwise the payment would silently return to the queue.

private settleWithoutInvoice(int $paymentId, int $status, array{processed: int, generated: int, emailed: int, skipped: int, ignored: int, errors: list} &$stats) : void
Parameters
$paymentId : int
$status : int
$stats : array{processed: int, generated: int, emailed: int, skipped: int, ignored: int, errors: list}

usesAssetPathTrait()

private static usesAssetPathTrait(string $class) : bool
Parameters
$class : string
Return values
bool
On this page

Search results