Summary#
use Osysharp.OfferSales@1; sells offers through the providers the app lists. The app says who sells:
policy ManagesOffers => IsManager; // offers, prices, places; the takings; refunds (unless RefundsPurchases says otherwise)
policy SellsAtCounter => IsDesk; // sell in person, mark a transfer or a pay-at-the-desk receivedand hands over what was bought where it hears [On] void HandOver(PurchasePaid e) { … } — exactly once per purchase.
Signature#
virtual policy ManagesOffers => false; virtual policy SellsAtCounter => false; virtual policy RefundsPurchases => ManagesOffers;
virtual policy BuysFor(Principal holder) => holder == user;
public interface IOfferSalesRecipients { List<Principal> RecipientsFor(Principal buyer); }
app.OfferSales = new OfferSalesSetup { Providers = [ … IPaymentProvider … ], CounterPayments = [ … ICounterPayment … ],
CounterMethods = ["Cash", "Card on the terminal"], Currency = "EUR", Country = "",
HoldMinutes = 30, ManualHoldDays = 7, CounterMayDiscount = true, Seller = "" };
public entity Offer { string Key; string Name; string Description; long Price; long TaxRate; bool Active; int SortOrder;
DateTime? OnSaleFrom; DateTime? OnSaleUntil; int? Places; int? PerPerson; int MaxPerPurchase; }
PurchaseStart StartPurchase(Offer offer, string providerKey, Principal? forPerson = null, int quantity = 1, string returnUrl = "");
Purchase ConfirmPurchase(string reference, string token); string ApplyOfferSalesEvents(List<PaymentEvent> events, string method);
Purchase MarkPurchasePaid(Purchase p); Purchase CancelPurchase(Purchase p, string reason = "Cancelled.");
Purchase SellAtCounter(Offer offer, Principal holder, string method, int quantity = 1, long discount = 0, string discountReason = "");
CounterStart StartCounterPurchase(Offer offer, Principal holder, string counterKey, int quantity = 1, long discount = 0, string discountReason = "");
Purchase CheckCounterPurchase(Purchase p);
PurchaseRefund RefundPurchase(Purchase p, string reason, long amount = 0); // 0: all that is left
Takings TakingsBetween(DateOnly from, DateOnly to); string PurchasesCsv(DateOnly from, DateOnly to);Amounts are minor units (cents) throughout.
Description#
What is an offer?#
A derivable entity: an app adds its own kinds (entity GuidedTour : Offer { DateTime? StartsAt; … }) and restates a
security { } block; the base's deny — an offer off sale is hidden from the public — carries to every kind
(entity Sub : Base). Places caps how many are held or sold: a purchase holds them on OfferPlaces, one row per
offer with a concurrency check, so two buyers racing for the last place cannot both have it, and a lapse, a
cancellation or a whole refund gives them back. OfferRefusal says in a sentence why something cannot be bought now.
How does a purchase become paid — once?#
A purchase is created only by the kit's functions and moved only by its own workflow, PurchaseLife. Every report of
money — the buyer returning (ConfirmPurchase asks the provider itself), the provider's signed event
(OfferSalesPaymentEvents at a route the app declares → ApplyOfferSalesEvents), staff marking a manual payment received,
a counter sale — is deposited into that run by the purchase's reference (Correlation — finding a run by a business key). The run checks the
amount and moves to Paid once; PurchasePaid is raised on that move, so a repeated report changes nothing. A
purchase not paid within HoldMinutes (online) or ManualHoldDays (a transfer) lapses to Cancelled, its places back
on sale; a declined payment is Failed and may still be paid until then. A free offer is paid the moment it is taken.
At the counter#
SellAtCounter records money in hand by one of the app's CounterMethods and is paid at once; a discount takes a
reason, which is kept. StartCounterPurchase begins a payment on a provider's device — a card reader, a QR code
(ICounterPayment) — and CheckCounterPurchase asks it until it is paid. The sale records who sold it.
Refunds#
RefundPurchase gives back part or all of what is left. The PurchaseRefund row is written before the provider is
asked (a unique sequence per purchase), so two refunds at once cannot both ask; the provider that took the money is
asked — a device's payment through the online provider it RefundsThrough — and money taken by hand goes back by hand.
PurchaseRefunded is raised once per refund with Whole true when all of it is back; the purchase is then Refunded.
Buying for somebody else#
StartPurchase(…, forPerson) buys for another person when BuysFor(holder) says so — a parent for a child. The page
offers the people every IOfferSalesRecipients the app has answers. What was bought is handed to Purchase.For; the
buyer pays and reads it too.
Granting a pack or a membership#
With Osysharp.PrepaidCredits, Osysharp.OfferSales.PrepaidCredits arrives by itself and adds CreditPackOffer (credits,
validity, scope): paid, the pack is granted to whoever it was bought for under the purchase's reference; refunded in
full, it is voided (The PrepaidCredits kit — prepaid uses, spent from the pack that expires first, never below zero). With Osysharp.Memberships, Osysharp.OfferSales.Memberships adds
MembershipOffer (a plan): paid, the membership is sold at what was paid; refunded in full, it ends
(The Memberships kit — what lets a person in: passes, memberships and visit cards).
Which pages does it serve?#
/buy (the offers; how many, for whom, how to pay), /purchases (with each receipt), /purchases/{reference}/{token}
(where a provider sends the buyer back), /staff/counter, /staff/sales (the takings by way the money came in, every
purchase, refunds, the CSV) and /staff/offers. Each stands in the app's working shell with theme tokens only; take
one over by declaring a component of the same name, or place BuyOffers, OfferPriceList, MyPurchases,
PurchaseReceipt, CounterSale, SalesDesk or OffersDesk on a page of your own.
Whose data is kept?#
With Osysharp.Privacy, Osysharp.Privacy.OfferSales arrives by itself (Bridges — a package that arrives when an app uses the two it completes): what a person bought or had
bought for them, kept with the books on a legal obligation, counted, copied and erased when that ends.
Examples#
A café that sells a coffee card at the till#
app Cafe {
use Osysharp.Identity@0;
use Osysharp.Payments@0;
use Osysharp.Csv@0;
use Osysharp.OfferSales@1;
use Osysharp.Storage;
use Osysharp.Ui;
use Osysharp.Workflow;
model "model/**/*.osy";
}
using Osysharp.Identity;
using Osysharp.Payments;
using Osysharp.OfferSales;
[Role] enum CafeRole { Authenticator, Regular, Barista, Owner }
policy ManagesOffers => RoleGrant.Any(g => g.Member == user && g.Role == CafeRole.Owner);
policy SellsAtCounter => RoleGrant.Any(g => g.Member == user && g.Role == CafeRole.Barista);
app.OfferSales = new OfferSalesSetup {
Providers = [ new ManualPayment { Key = "tab", Label = "Pay at the till" } ],
CounterMethods = ["Cash", "Card on the terminal"], Currency = "EUR", Seller = "Corner Café",
};
/// A card of coffees — the café's own kind of offer.
entity CoffeeCard : Offer {
[Min(1)] int Coffees = 10;
security { allow read when IsAnonymous || IsAuthenticated; allow create, update when ManagesOffers; }
}
/// The stamps a regular holds — handed over when a card is paid, and once only.
entity Stamps {
[Required("Whose stamps?")] Principal Holder;
int Left = 0;
[MaxLength(64)] string Reference = "";
security { allow read when IsAuthenticated where Holder == user; }
}
[On]
void HandOverStamps(PurchasePaid e) {
var card = CoffeeCard.FirstOrDefault(c => c.Id == e.Purchase.Offer.Id);
if (card == null) { return; }
new Stamps { Holder = e.Purchase.For, Left = card!.Coffees * e.Purchase.Quantity, Reference = e.Purchase.Reference };
}See also#
- The PrepaidCredits kit — prepaid uses, spent from the pack that expires first, never below zero — a pack of credits as an offer, granted when paid
- The Memberships kit — what lets a person in: passes, memberships and visit cards — a membership plan as an offer, sold when paid
- Correlation — finding a run by a business key — how a payment finds its purchase by reference
- entity Sub : Base — an app's own kinds of offer
- The Privacy kit — erasure that keeps only what the company must or may — a person's purchases in their privacy requests
kits/offer-sales/tests/— every behaviour above as a test, the screens under--pixels