Developer Documentation
Store Credit is built as an event-sourced ledger rather than a balance field you increment. Understanding this is important to understanding the module: why balances can be rebuilt, why retried checkouts do not double-spend, and why nothing in the history can be edited. This chapter covers the architecture first, then the extension points.
The Ledger Is the Source of Truth
Every change to a customer's store credit is a row in hyva_store_credit_ledger. Rows are only ever appended, never updated or deleted. Each row carries:
customer_idandwebsite_id- whose balance, on which website.event_type- what kind of change it was, fromHyva\StoreCredit\Model\Ledger\EventType.delta- the signed amount, negative for spends and debits.balance_after- the balance immediately after this row, which chains the ledger together and makes tampering detectable.base_currency_code- the currency the balance is held in.reasonandreason_external- the internal note and the customer-facing note.source- what caused it, fromHyva\StoreCredit\Model\Ledger\Source.idempotency_key- unique across the table, and the reason retries are safe.- Attribution and references -
admin_user_id,api_user,order_id,invoice_id,creditmemo_id,giftcard_id, andis_customer_notified.
The seven event types are CREDIT_ADJUST, DEBIT_ADJUST, SPEND, REFUND, REVERT, CONVERSION, and IMPORT. EventType classifies each as a credit or a debit and supplies its display label, so you never need to hardcode either.
The source values are admin, api, checkout, creditmemo, import, pos, giftcard, loyalty, rma, and system. Several of those exist for other Hyvä Commerce modules to write into the same ledger, which is how gift card conversions land as CONVERSION entries with source giftcard.
The Balance Is a Projection
Reading a balance by summing the ledger on every page load would not scale, so hyva_store_credit_balance holds one row per customer and website with the current amount. It is a cache with two important properties: it is written inside the same database transaction as the ledger row, and it can always be rebuilt from the ledger.
There is also a SQL view, hyva_store_credit_balance_view, which is SUM(delta) grouped by customer and website. It backs two things: seeding a projection row on the fly when one is missing, and verifying the projection against the ledger.
The combination of these is why a missing or stale projection row is a performance problem rather than a correctness problem. A spend still works when the projection row does not exist yet.
How Writes Are Made Safe
Hyva\StoreCredit\Model\Ledger\LedgerWriter is the single write path. Everything else, including the admin screens, the observers, the REST endpoints, and the importer, goes through it. It takes the balance row with SELECT ... FOR UPDATE so concurrent writes to the same balance serialize instead of racing, works to four decimal places, and compares amounts with an epsilon of 0.0001 rather than testing floats for equality.
Before writing, it refuses:
- An invalid event type, or an entry with no customer.
- A change that would take the balance below zero, with
InsufficientBalanceException. - A currency that does not match the balance's existing currency, with
CurrencyMismatchException. - A refund that would return more credit than an order actually spent, unless the caller sets the
allow_over_refundflag. - Re-use of a spend key that has already been compensated, with
SpendAlreadyCompensatedException.
Idempotency Keys
The idempotency_key column has a unique index, and Hyva\StoreCredit\Model\Ledger\IdempotencyKey builds the keys the module uses:
sc-spend-<incrementId>for an order's spend, with-<attempt>appended once a retry has happened.sc-revert-<incrementId>for putting a spend back, following the same attempt suffix.sc-refund-creditmemo-<creditmemoId>for a refund.
When a write arrives with a key that already exists, the existing entry is returned and nothing new is written. This is what makes a replayed save event or a duplicate submit-failure notification harmless. It also means the append events do not fire for a short-circuited duplicate, which is worth knowing before you hang side effects off them. See the Event Reference.
Currency and Website Scoping
A balance belongs to a customer and a website, and is held in that website's base currency. There is no conversion between websites and no combined balance. When credit is displayed or applied in a different presentation currency, Hyva\StoreCredit\Model\Total\DisplayCurrencyConverter does the base-to-display conversion in one place, so rounding happens once rather than at every call site.
Rebuilding the Balance Projection
When the projection and the ledger disagree, for example after a direct database change or an interrupted job, rebuild it:
The command replays the ledger in batches, rewrites every projection row, prunes rows the ledger no longer supports, and then runs two checks: the projection against the balance view, and each customer's balance_after chain against a recomputation. Any discrepancy is printed and the command exits with a failure status, which makes it usable as a monitoring check as well as a repair tool.
What Is in This Chapter
- Store Credit Event Reference - every event Store Credit dispatches with its payload, the Magento events it already observes, and the Magewire and browser events on the storefront.
- Store Credit Service API - the PHP service contracts, the data interfaces, the exceptions, and how to credit a customer from your own module.
- Store Credit REST API - every web API route, its ACL resource, and request examples.
- Store Credit GraphQL API - the queries and mutations for headless storefronts (optional
Hyva_StoreCreditGraphQlmodule). - Theming Store Credit - the templates, layout handles, view models, Magewire component, and Tailwind config.
Related Topics
- Store Credit Through the Order Lifecycle - the same mechanics described without the class names.
- Configuring Store Credit - the settings your code should respect.