# The PrepaidCredits kit — prepaid uses, spent from the pack that expires first, never below zero

> `Osysharp.PrepaidCredits` keeps prepaid uses — a class pack, a ten-visit card, machine hours — as packs of credits a person holds. A spend takes from the pack that expires first, across packs when it must, all or nothing and never below zero, once per reference; it can be undone. Staff correct, extend, end, void and move credits with a reason. Every change is a line in a ledger the holder reads, packs warn before they expire, and the value still owed is one call.

<!-- id: ui-prepaid-credits-kit · area: ui · stability: preview · html: https://osysharp.com/reference/ui/prepaid-credits-kit/ -->

## Summary        {#summary}
`use Osysharp.PrepaidCredits@0;` keeps what people paid for in advance and use one at a time. The app says who looks
after credits and who spends them for others:

```osy syntax
policy ManagesCredits => IsOwner;     // grant, correct, extend, end, void, move — and the value owed
policy SpendsCredits => IsDesk;       // spend and undo for anybody
```

Then `GrantCredits(person, 10, "Ten classes", expiresAt)` when a pack is sold, and `SpendCredits(person, 1, "Tuesday
class", reference)` when one is used.

## Signature      {#signature}
```osy syntax
virtual policy ManagesCredits => false;     virtual policy SpendsCredits => false;     virtual policy SpendsOwnCredits => false;
virtual policy ActsForCreditHolder(Principal holder) => holder == user;
app.Credits = new CreditsSetup { ExpiryNoticeDays = 7, LowBalanceAt = 0, HoldersMayTransfer = false, Unit = "credit", Units = "credits" };

CreditPack GrantCredits(Principal holder, int count, string label, DateTime? expiresAt = null, string? reference = null,
                        string scope = "", decimal unitValue = 0, string reason = "");
CreditSpend? SpendCredits(Principal holder, int amount, string reason, string reference, string scope = "", Principal? forWhom = null);
CreditSpend? SpendCredit(Principal holder, string reason, string reference, string scope = "");
bool RefundSpend(CreditSpend spend, string reason = "");
void AdjustCredits(CreditPack pack, int remaining, string reason);       void ExtendCredits(CreditPack pack, DateTime? expiresAt, string reason = "");
bool ExpireCredits(CreditPack pack, string reason = "");                 bool VoidCredits(CreditPack pack, string reason = "");
CreditPack TransferCredits(CreditPack pack, int count, Principal to, string reason = "");
int CreditsLeft(Principal holder, string scope = "");    List<CreditPack> CreditPacksOf(Principal holder);
List<CreditEntry> CreditStatementOf(Principal holder, int take = 200);  List<CreditSpend> CreditSpendsOf(Principal holder);
List<CreditPack> CreditsExpiringWithin(Principal holder, int days);     List<CreditLiability> CreditLiabilities();
```

## Description    {#description}

### Which pack does a spend take from?   {#order}
The usable pack — not voided, not ended, with credits left — that **expires first**; a pack that never expires goes
last; among equals, the oldest. A spend of several credits draws across packs in that order, and writes one ledger line
per pack it touched. It is **all or nothing**: when the holder cannot spend that many, it answers null and nothing
changes — the balance never goes below zero, whoever calls, however many spend at once.

### What makes a spend safe to repeat?   {#idempotent}
Its `reference` — a booking, a scan, a ticket. The same reference never spends twice: a repeat answers the spend it
already made. A grant with a `reference` (an order line) is the same: the same reference answers the pack it made.

### What is a scope?   {#scope}
`scope: "workshops"` keeps a pack for one purpose: only a spend naming that scope draws from it, and an unscoped spend
never touches it. `CreditsLeft(person, "workshops")` reads that purse. Osysharp.Memberships keeps each counted
membership's visits this way.

### Who may spend?   {#who}
`SpendsCredits` spends for anybody. `SpendsOwnCredits` lets a holder spend their own — an online booking — and those of
the people `ActsForCreditHolder` says they act for: a parent for a child who never signs in (an account made with
Osysharp.UserAccounts' `MakeManagedAccount`). `forWhom:` records who used a credit when it is not the holder — a child on a
parent's pack. A function a kit or the app runs as the system (a workflow, a handler) may spend too.

### What do staff change, and how is it kept?   {#staff}
Every change is a line in `CreditEntry` — granted, spent, refunded, adjusted, expiry changed, expired, voided, given
away, received — with the change, the balance after it in that scope, who made it and why. `AdjustCredits` requires a
reason; nobody edits or deletes a line, and no grant opens a direct write to a balance: `Remaining` changes only inside
the kit's functions (`deny update Remaining except via …`). `TransferCredits` moves credits to another person as a pack
of theirs with the same expiry, scope and value; a holder may give their own when `HoldersMayTransfer` is on.

### What happens before and at expiry?   {#expiry}
Each pack has a life of its own: `ExpiryNoticeDays` before it expires, with credits left, `CreditsExpiringSoon` is
raised — the moment to tell its holder. At expiry what is left leaves the balance as an `Expired` line, and
`CreditsExpired` says how many. Changing the expiry moves both; an ended pack is never brought back — grant a new one.

### What is the value still owed?   {#owed}
`CreditLiabilities()` sums, per scope, the credits unspent and unexpired, their value at what each was paid
(`unitValue`), how many people hold them, and how many expire within 30 days — prepaid value the business still owes in
service.

### Which events does it raise?   {#events}
`CreditsGranted`, `CreditsSpent`, `CreditsRefunded`, `CreditsRunningLow` (at or under `LowBalanceAt`),
`CreditsRanOut`, `CreditsExpiringSoon`, `CreditsExpired`, `CreditsVoided`, `CreditsAdjusted`, `CreditsTransferred`. The
app hears one with `[On] void Heard(CreditsRanOut e) { … }` — a mail, an offer to buy another pack.

### Which pages does it serve?   {#pages}
`/credits` — the signed-in person's balance, packs about to expire, packs and statement. `/staff/credits` — the desk:
find a person, use credits, undo a use, give a pack, correct, change an expiry, move, void. `/staff/credits/owed` — the
value still owed, for those who manage credits. Each stands in the app's working shell, every colour and size a theme
token; take one over by declaring a component of the same name, or place `CreditsOverview`, `CreditsDesk` or
`CreditsOwed` on a page of your own.

### How is a pack sold?   {#selling}
Money moves through the app's payments (Osysharp.Payments, Osysharp.Shop): when an order is paid, grant the pack with
the order line as its `reference`, so a repeated notice grants nothing twice, and its price per credit as `unitValue`.

### Whose data is kept, and how does it go?   {#privacy}
What a spend or a change was for is `Personal.Content`. With Osysharp.Privacy, `Osysharp.Privacy.PrepaidCredits` —
which arrives by itself ([Bridges — a package that arrives when an app uses the two it completes](https://osysharp.com/reference/project/bridges/)) — counts, copies and erases a person's packs, spends and ledger.

## Examples       {#examples}

### A makerspace selling machine hours by the pack   {#example-makerspace}
```osy title="hours sold in packs, spent by the hour at the laser cutter, the owner keeping the books" test app=ui-prepaid-credits-kit
app Makerspace {
  use Osysharp.Identity@0;
  use Osysharp.PrepaidCredits@0;
  use Osysharp.Ui;
  use Osysharp.Workflow;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.PrepaidCredits;

[Role] enum SpaceRole { Authenticator, Maker, Host }
policy IsHost => RoleGrant.Any(g => g.Member == user && g.Role == SpaceRole.Host);
policy IsMaker => RoleGrant.Any(g => g.Member == user && g.Role == SpaceRole.Maker);
policy ManagesCredits => IsHost;
policy SpendsCredits => IsHost;
policy SpendsOwnCredits => IsMaker;

app.Credits = new CreditsSetup { ExpiryNoticeDays = 14, LowBalanceAt = 2, Unit = "hour", Units = "hours" };

entity MachineBooking {
  [Required, MaxLength(80)] string Machine;
  [Required] Principal Maker;
  [Min(1)] int Hours = 1;
  CreditSpend? PaidWith;
  security { allow read, create when IsAuthenticated; }
}

/// A maker books the laser cutter, paying in machine hours.
[Server, Authorize(IsAuthenticated)]
public string BookMachine(string machine, int hours) {
  var me = CurrentPrincipal()!;
  var spend = SpendCredits(me, hours, machine, "booking:" + Guid.NewGuid(), "machines");
  if (spend == null) { return "Not enough machine hours left."; }
  new MachineBooking { Machine = machine, Maker = me, Hours = hours, PaidWith = spend };
  return "";
}

/// Running low: offer the next pack.
[On]
void OfferMoreHours(CreditsRunningLow e) { Log.Information("{Who} has {Left} hours left", e.Holder.DisplayName, e.Left); }
```

## See also       {#see-also}
- [The Memberships kit — what lets a person in: passes, memberships and visit cards](https://osysharp.com/reference/ui/memberships-kit/) — day tickets, memberships and visit cards, built on these packs
- [deny … except via](https://osysharp.com/reference/security/except-via/) — how a balance is changed only by the kit's functions
- [What may go past the caller's own rights — and what never does](https://osysharp.com/reference/security/past-the-callers-rights/) — the desk spending for a member
- [Remind (milestone reminders)](https://osysharp.com/reference/workflow/remind/) — the clock behind the expiry notice
- [settings a package asks its app for (app settings)](https://osysharp.com/reference/config/app-settings/) — `app.Credits`
- [The Privacy kit — erasure that keeps only what the company must or may](https://osysharp.com/reference/security/privacy-kit/) — a person's credits in their privacy requests
- **`kits/prepaid-credits/tests/`** — every behaviour above as a test, the screens under `--pixels`
