Pay with store credit
With credits-for-laravel, buyers can pay all or part of an order from a store-credit bucket denominated in the order’s currency. Map the bucket and switch the tender on:
// config/credits.php — denominate the bucket in your shop currency (one bucket per currency)
'currencies' => ['store_credit' => 'EUR'],SHOPS_ALLOW_STORE_CREDIT=true
SHOPS_STORE_CREDIT_BUCKET=store_credit
SHOPS_REFUND_TO_STORE_CREDIT=falseThe buyer model must implement credits’ RoundlyConsulting\Credits\Interfaces\Creditable and use its HasCredits trait — the trait alone is not enough: an order whose customer is not Creditable is charged in full through the gateway, with no credit applied:
use Illuminate\Foundation\Auth\User as Authenticatable;
use RoundlyConsulting\Credits\Interfaces\Creditable;
use RoundlyConsulting\Credits\Traits\HasCredits;
class User extends Authenticatable implements Creditable
{
use HasCredits;
}Applying credit
With allow_store_credit on, Shops::order($order)->charge() debits available credit before charging the gateway for the remainder ($order->gatewayAmount()). An order that credit covers in full is marked paid without a gateway charge:
use RoundlyConsulting\Money\Money;
use RoundlyConsulting\Shops\Facades\Shops;
use RoundlyConsulting\Shops\Payments\StoreCreditTender;
// Automatic: with allow_store_credit on, credit goes first and the gateway gets the rest.
$result = Shops::order($order)->charge();
// Or apply it yourself (once per order):
$owed = app(StoreCreditTender::class)->apply($order, $customer); // as much as possible
$owed = app(StoreCreditTender::class)->apply($order, $customer, Money::ofMinor(500, 'EUR')); // or cap the debit
$order->store_credit_applied; // ?Money, in the order currency
$order->gatewayAmount(); // what is still due through the gateway
app(StoreCreditTender::class)->restore($order); // ?Money — hand it back (cancel() does this)- apply() debits the smallest of the order total, the bucket balance and your cap, records it on store_credit_applied and returns what is still owed — in one transaction.
- A zero applicable amount changes nothing and returns the full total.
- A declined charge keeps none of the credit it applied — the whole charge rolls back — and canceling an order returns any credit applied to it, also credit you applied yourself with apply(). restore() hands it back on demand; a second call returns nothing.
- StoreCreditAlreadyAppliedException — credit was already applied to this order (re-checked under the order’s row lock).
- StoreCreditBucketNotDenominated — the bucket has no currency in credits.currencies.
- StoreCreditCurrencyMismatch — the bucket is in another currency. All three throw before any credit moves.
Refunding as store credit
With payment.refund_to_store_credit on, the GrantStoreCreditOnRefund listener credits the order’s whole final price back to the bucket when the order is refunded — so skip the gateway refund, or the buyer is refunded twice. It does nothing without a Creditable customer or a positive total; a bucket that is undenominated or in another currency is skipped with a Log::warning() instead of throwing, since the refund already happened.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.