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:
policy ManagesCredits => IsOwner; // grant, correct, extend, end, void, move — and the value owed
policy SpendsCredits => IsDesk; // spend and undo for anybodyThen GrantCredits(person, 10, "Ten classes", expiresAt) when a pack is sold, and SpendCredits(person, 1, "Tuesday class", reference) when one is used.
Signature#
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#
Which pack does a spend take from?#
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?#
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: "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?#
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?#
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?#
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?#
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?#
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?#
/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?#
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?#
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) — counts, copies and erases a person's packs, spends and ledger.
Examples#
A makerspace selling machine hours by the pack#
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#
- The Memberships kit — what lets a person in: passes, memberships and visit cards — day tickets, memberships and visit cards, built on these packs
- deny … 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 — the desk spending for a member
- Remind (milestone reminders) — the clock behind the expiry notice
- settings a package asks its app for (app settings) —
app.Credits - The Privacy kit — erasure that keeps only what the company must or may — a person's credits in their privacy requests
kits/prepaid-credits/tests/— every behaviour above as a test, the screens under--pixels