Skip to content

Store Credit Service API

Store Credit exposes three service contracts under Hyva\StoreCredit\Api. Use them from your own modules rather than touching the ledger tables or LedgerWriter directly: they apply the same validation, attribution, and idempotency the admin screens and the REST endpoints use.

Reading and Changing a Balance

Hyva\StoreCredit\Api\BalanceManagementInterface is the service for a customer's balance. Every write returns the LedgerEntryInterface it created, so you can record the entry ID against your own records.

public function getBalance(int $customerId, int $websiteId): BalanceInterface;

public function credit(
    int $customerId,
    int $websiteId,
    float $amount,
    string $reason,
    ?string $source = null,
    ?string $idempotencyKey = null
): LedgerEntryInterface;

public function debit(
    int $customerId,
    int $websiteId,
    float $amount,
    string $reason,
    ?string $source = null,
    ?string $idempotencyKey = null
): LedgerEntryInterface;

public function adjust(
    int $customerId,
    int $websiteId,
    float $delta,
    string $reason,
    ?string $source = null
): LedgerEntryInterface;

credit() and debit() both take a positive amount and decide the direction themselves, and both reject an amount that is not greater than zero. adjust() takes a signed delta instead, which is convenient when your own code already works in signed amounts, and rejects a delta smaller than 0.0001 in either direction. All three require a non-empty $reason, which becomes the internal reason on the ledger entry.

Pass $idempotencyKey whenever the operation could be retried, for instance when your integration processes a queue that may redeliver a message. A repeated key returns the entry that already exists instead of crediting twice.

Crediting a Customer From Your Own Module

This example credits a customer as part of a loyalty payout, using the order increment ID to build a key that survives a retry.

app/code/Vendor/Loyalty/Model/CreditPayout.php
<?php
declare(strict_types=1);

namespace Vendor\Loyalty\Model;

use Hyva\StoreCredit\Api\BalanceManagementInterface;
use Hyva\StoreCredit\Model\Ledger\Source;

class CreditPayout
{
    public function __construct(private readonly BalanceManagementInterface $balanceManagement)
    {
    }

    public function payOut(int $customerId, int $websiteId, float $amount, string $orderIncrementId): void
    {
        $this->balanceManagement->credit(
            $customerId,
            $websiteId,
            $amount,
            sprintf('Loyalty payout for order %s', $orderIncrementId),
            Source::LOYALTY,
            sprintf('loyalty-payout-%s', $orderIncrementId)
        );
    }
}

Reach for one of the existing Source constants where one fits, since the source is what the admin credit history shows in its By column. Source::LOYALTY, Source::POS, Source::RMA, and Source::GIFTCARD exist precisely so integrations do not have to invent their own.

Applying Credit to a Cart

Hyva\StoreCredit\Api\QuoteBalanceManagementInterface is the cart-side service, and the one the storefront controllers and the checkout component both use.

public function apply(int $cartId): void;

public function remove(int $cartId): void;

public function getApplicable(int $cartId): float;

public function isApplied(int $cartId): bool;

getApplicable() returns how much credit this cart could actually absorb, which is the customer's balance capped at the cart total. apply() applies that amount, remove() takes it back out, and isApplied() reports the current state. None of these touch the balance: they set the store credit fields on the quote, and the debit only happens when the order is placed.

Reading History

Hyva\StoreCredit\Api\HistoryRepositoryInterface reads ledger entries.

public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface;

public function getForCustomer(int $customerId, ?int $websiteId = null): array;

getForCustomer() is the shortcut for the common case and returns an array of LedgerEntryInterface. Use getList() with search criteria when you need filtering, sorting, or paging, for example to build a report over a date range.

The Data Interfaces

Hyva\StoreCredit\Api\Data\BalanceInterface describes a balance: customer_id, website_id, amount, base_currency_code, and updated_at.

Hyva\StoreCredit\Api\Data\LedgerEntryInterface describes a single entry, with getters for entry_id, customer_id, website_id, event_type, delta, balance_after, base_currency_code, reason, reason_external, source, idempotency_key, admin_user_id, api_user, order_id, invoice_id, creditmemo_id, giftcard_id, is_customer_notified, and created_at.

Both interfaces have a di.xml preference to their implementation, so inject the interface and let Magento resolve it.

Building Entries Directly

When the three service methods are not expressive enough, for example when an entry needs an order reference, an external reason, and a notification flag all at once, build the entry yourself with Hyva\StoreCredit\Model\Ledger\LedgerEntryBuilderFactory and hand it to LedgerWriter::append().

$entry = $this->ledgerEntryBuilderFactory->create()
    ->withCustomer($customerId, $websiteId)
    ->withEvent(EventType::CREDIT_ADJUST, 25.00)
    ->withCurrency('EUR')
    ->withReason('Compensation for delayed shipment', 'Sorry for the wait, here is 25 euro of credit')
    ->withSource(Source::ADMIN)
    ->withOrderId($orderId)
    ->withCustomerNotified(true)
    ->build();

$this->ledgerWriter->append($entry);

withReason() takes the internal reason first and the optional external reason second, which is the one the customer sees. build() requires at least a customer and an event.

Exceptions to Expect

All four extend Magento\Framework\Exception\LocalizedException, so a generic handler still catches them, but catching the specific type lets you respond properly.

Exception When it is thrown
InsufficientBalanceException The write would take the balance below zero.
CurrencyMismatchException The entry's currency does not match the currency the balance is already held in.
BalanceAlreadyImportedException A CSV import row targets a customer who already has ledger history on that website.
SpendAlreadyCompensatedException A spend key that was already reverted is being re-used. The caller should raise the attempt counter and build a new key.

A refund that would return more credit than an order spent throws a plain LocalizedException, unless the entry carries the allow_over_refund flag.