Rezervácie
Rezervácia (hold) vyhradí kredity v skupine bez zápisu do knihy pohybov. Znižuje to, čo sa dá minúť, kým ju strhnete — celú, alebo čiastočne so zvyškom uvoľneným —, uvoľníte, alebo kým neexpiruje. Typicky ide o pokladňu, ktorá kredity rezervuje počas platby, alebo o volanie API, ktoré si vopred rezervuje odhadovanú cenu.
Zapnutie rezervácií
Rezervácie potrebujú tabuľku credit_holds (pozrite Inštalácia) a sú vypnuté, kým ich nezapnete — dovtedy ochrana pri odpise tabuľku rezervácií nikdy nečíta:
CREDITS_HOLDS=on
# optional: holds created without their own expiry live this long
CREDITS_HOLD_EXPIRE_AFTER_MINUTES=30Rezervácia, strhnutie, uvoľnenie
use RoundlyConsulting\Credits\Facades\Credits;
$hold = Credits::for($user)->hold(30, now()->addMinutes(15), 'Order #42', ['order' => 42]);
Credits::for($user)->balance(); // 100 — the ledger is untouched
Credits::for($user)->held(); // 30
Credits::for($user)->available(); // 70 — what a debit or another hold can still take
$hold->capture(); // debit all 30 (one ledger row), or…
$hold->capture(12); // …debit 12 and release the other 18, or…
$hold->release(); // …free all of it, nothing written to the ledgerPloché metódy fasády prijímajú CreditHoldData a samotnú rezerváciu; capture() a release() na scope rezerváciu iného vlastníka odmietnu:
use RoundlyConsulting\Credits\DataTransferObjects\CreditHoldData;
use RoundlyConsulting\Credits\Facades\Credits;
$hold = Credits::hold($user, new CreditHoldData(amount: 30, expiresAt: now()->addMinutes(15), bucket: 'points'));
Credits::capture($hold, 20, 'Order #42'); // the debit row (-20); the other 10 are released
Credits::release($hold); // throws CreditHoldNotActiveException — already captured
Credits::for($user)->bucket('points')->release($otherHold); // ForeignCreditHoldException for another owner's hold
Credits::held($user, 'points'); // int
Credits::available($user, 'points'); // int
Credits::for($user)->hasAvailable(50); // boolSkratky v traite (nie sú súčasťou rozhrania Creditable):
$hold = $user->holdCredits(30, now()->addMinutes(15), bucket: 'points');
$user->heldCredits('points'); // 30
$user->availableCredits('points'); // balance minus active holds
$user->hasAvailableCredits(50, 'points');$amount musí byť kladné a $expiresAt v budúcnosti (InvalidArgumentException); suma strhnutia musí byť 1..$hold->amount a popis aj meta sa predvolene preberú z rezervácie. $hold->capture() a $hold->release() idú cez manažéra, takže ich fake zaznamená, a odovzdaná rezervácia sa aktualizuje na mieste (status, captured_amount, credit_id, resolved_at).
Pravidlá
- Dostupné = zostatok − aktívne rezervácie, pre každú skupinu. Aktívna rezervácia je v stave held a jej expires_at ešte neuplynulo. balance(), has() aj balance v CreditsModified zostávajú zostatkom knihy pohybov.
- Rezervácia sa stráži ako odpis: pokiaľ nie je povolené prečerpanie (na scope alebo cez credits.allow_overdraft), nesmie dostať dostupný zostatok pod credits.minimum_balance — InsufficientCreditsException, ktorej $available je dostupný zostatok.
- Odpisy rešpektujú rezervácie: pri zapnutých rezerváciách rozhoduje strážený odpis podľa dostupného zostatku. Odpis s povoleným prečerpaním môže rezervované kredity minúť aj tak.
- Strhnutie sa kvôli minimálnemu zostatku nikdy neodmietne — rezervácia kredity už vyhradila. Zapíše jeden odpis do knihy pohybov, na ktorý odkazuje credit_id rezervácie.
- Každá rezervácia sa vyrieši raz: strhnutie či uvoľnenie rezervácie, ktorá už bola strhnutá, uvoľnená, expirovala (upratovaním alebo len uplynutím expires_at) alebo bola zmazaná, vyhodí CreditHoldNotActiveException so stavom, v ktorom ju našlo.
- Expirácia platí okamžite, upratovanie je len evidencia: rezervácia prestane kredity vyhradzovať v momente, keď uplynie expires_at; upratovanie ju označí ako expired a spustí CreditHoldExpired.
- Rezervácie žijú na spojení knihy pohybov — spojení modelu credits.model, inak vlastníka — vedľa záznamov knihy pohybov a zámku vlastníka.
Odolnosť voči súbehu
Každá rezervácia, strhnutie, uvoľnenie aj expirácia vezme zámok vlastníka (ten istý riadok credit_locks, ktorý zvyšuje odpis) a všetko, podľa čoho rozhoduje, znova načíta pod zámkami riadkov, v jednej transakcii so svojím zápisom:
| Súbeh | Výsledok |
|---|---|
| Dve rezervácie, ktoré spolu prekročia dostupný zostatok | druhá počká a potom sa odmietne (InsufficientCreditsException) |
| Rezervácia a odpis tých istých kreditov | odmietne sa tá operácia, ktorá sa potvrdí ako druhá |
| Dve strhnutia jednej rezervácie | jeden odpis; druhé dostane CreditHoldNotActiveException (captured) |
| Strhnutie a upratovanie expirovaných rezervácií | vyhrá strhnutie (upratovanie ho preskočí) alebo upratovanie (strhnutie dostane expired) — nikdy oboje |
| Strhnutie a uvoľnenie | jedno vyhrá; druhé dostane CreditHoldNotActiveException |
Na Postgres pri REPEATABLE READ / SERIALIZABLE čakajúca transakcia zlyhá so SQLSTATE 40001 a zopakuje sa s novým snapshotom, presne ako pri odpisoch.
Upratovanie expirovaných rezervácií
Naplánujte upratovanie — príkaz alebo job do fronty:
// routes/console.php
use Illuminate\Support\Facades\Schedule;
use RoundlyConsulting\Credits\Jobs\ExpireCreditHolds;
Schedule::command('credits:expire-holds')->everyMinute()->withoutOverlapping();
// or queued:
Schedule::job(new ExpireCreditHolds)->everyMinute();- credits:expire-holds {--connection=} vypíše, koľko rezervácií expiroval; pri vypnutých rezerváciách to oznámi a skončí úspešne.
- Credits::expireHolds(?$now, ?$connection) je to isté upratovanie a vráti počet; $now uprace stav k danému okamihu. Rezervácie číta po 100 na spojení modelu knihy pohybov, inak na predvolenom — ak žijú inde, zadajte $connection.
- Bezpečné aj na viacerých serveroch naraz: každá rezervácia expiruje najviac raz, pod zámkom svojho vlastníka. Expiruje aj rezervácia, ktorej vlastník bol zmazaný.
Tabuľka credit_holds
| Stĺpec | Typ | Poznámka |
|---|---|---|
id | per primary_key_type | |
creditable_type / creditable_id | morph pair, id per key_type | Indexované. |
bucket | string | |
amount | bigInteger | Rezervovaná suma. |
status | string(16) | held, captured, released alebo expired. |
captured_amount | bigInteger, nullable | |
credit_id | per primary_key_type, nullable | Záznam strhnutia v knihe pohybov. |
description / meta | string / json, nullable | |
expires_at / resolved_at | timestamp, nullable | Uložené v app.timezone, porovnávané ako presné okamihy. |
created_at / updated_at / deleted_at | timestamps, soft deletes | Zmazaná rezervácia sa už nezapočítava. |
Indexy: (creditable_type, creditable_id, bucket, status) a (status, expires_at). CreditHold má factory na priame vytvorenie rezervácií.
Prejavte lásku k open source
Tento balík je zadarmo pod licenciou MIT. Ak vám šetrí čas, jednorazový príspevok alebo členstvo na Patreone nám pomôže ho ďalej udržiavať, testovať a dokumentovať.
Ďalšie spôsoby podpory vrátane kryptomienOdoslaním daru súhlasíte s našimi podmienkami prijímania darov.
Chcete to zabudovať do svojho produktu?
Naše balíky integrujeme do zákazkových Laravel a AI riešení. Napíšte nám, na čom pracujete, a ozveme sa do 48 hodín.