Documentation
¶
Overview ¶
Package offers sells content, versions and groups. Staff and creators put an offer (terms) on a target; an engine River job syncs it to OpenRails as a product whose key and only entitlement is the target's access key, with one price under PriceKey. Prices are immutable, so a reprice is a new price; a withdrawal archives the product. It also serves free group joins and the viewer's library.
Index ¶
- Constants
- Variables
- type Amount
- type Config
- type GroupAccess
- type GroupJoin
- type LibraryItem
- type LibraryPage
- type LibraryQuery
- type ListQuery
- type Offer
- type OfferPage
- type Options
- type Put
- type Sale
- type Service
- func (s *Service) Authorize(ctx context.Context, a auth.Authenticator, actor access.Actor, ...) error
- func (s *Service) Current(ctx context.Context, a auth.Authenticator, actor access.Actor, ...) ([]*Offer, error)
- func (s *Service) GroupAccess(ctx context.Context, viewer access.Actor, groups []string) ([]GroupAccess, error)
- func (s *Service) Handler(a auth.Authenticator) (http.Handler, error)
- func (s *Service) Join(ctx context.Context, actor access.Actor, groups []string) ([]GroupJoin, error)
- func (s *Service) Joined(ctx context.Context, viewer access.Actor) ([]string, error)
- func (s *Service) Leave(ctx context.Context, actor access.Actor, groups []string) ([]GroupJoin, error)
- func (s *Service) Library(ctx context.Context, actor access.Actor, q LibraryQuery) (LibraryPage, error)
- func (s *Service) List(ctx context.Context, q ListQuery) (OfferPage, error)
- func (s *Service) Live(ctx context.Context, keys []access.Key) (map[string]Offer, error)
- func (s *Service) Put(ctx context.Context, puts []Put) ([]Offer, error)
- func (s *Service) PutTx(ctx context.Context, tx pgx.Tx, puts []Put) error
- func (s *Service) Reconcile(ctx context.Context) error
- func (s *Service) Resync(ctx context.Context, tx pgx.Tx, targets []Target) error
- func (s *Service) RiverJobs() riverhelpers.Contribution
- func (s *Service) Sales(ctx context.Context, keys []string) (map[string]Sale, error)
- func (s *Service) Sync(ctx context.Context, targets []Target) error
- func (s *Service) Withdraw(ctx context.Context, targets []Target) ([]Offer, error)
- type State
- type Target
- type TargetKind
- type Terms
Constants ¶
const PriceKey = "offer"
PriceKey names an offer's price under its product.
const ReconcileInterval = time.Minute
ReconcileInterval is how often the engine queues a sync of every offer OpenRails does not match yet.
Variables ¶
var ( ErrInvalid = errors.New("offers: invalid") ErrNotFound = errors.New("offers: not found") ErrForbidden = errors.New("offers: forbidden") ErrConflict = errors.New("offers: conflict") // ErrNotJoinable is a join of a group without a free offer. ErrNotJoinable = errors.New("offers: not free to join") )
Functions ¶
This section is empty.
Types ¶
type Amount ¶
type Amount int64
Amount is money in micros (millionths) of a currency unit. It reads and writes as a decimal: "4.99" (JSON strings or numbers, YAML scalars).
const MaxAmount Amount = 999_999_990_000
MaxAmount bounds every price: 999999.99.
func ParseAmount ¶
ParseAmount parses a non-negative decimal with at most six decimals.
func (Amount) MarshalText ¶
func (*Amount) UnmarshalJSON ¶
UnmarshalJSON takes a decimal string or number.
func (*Amount) UnmarshalText ¶
type Config ¶
type Config struct {
// Currency is the default currency: USD when empty.
Currency string `json:"currency,omitempty" yaml:"currency"`
// Minimum is the least amount per target kind; a version without its own
// takes content's. Free (0) is allowed for groups only.
Minimum map[TargetKind]Amount `json:"minimum,omitempty" yaml:"minimum"`
// PSPs are the payment providers selling every price.
PSPs []string `json:"psps,omitempty" yaml:"psps"`
}
Config is the engine's offer policy.
type GroupAccess ¶
type GroupAccess struct {
Key access.Key `json:"key"`
// Offer is the active offer as paywalls show it (Sales); nil while the
// group sells nothing.
Offer *Sale `json:"offer,omitempty"`
// Member: the viewer holds the key (bought, subscribed or joined free).
Member bool `json:"member"`
// Editor: the viewer edits the group's content (its owner, a holder of
// {kind}:content:write in its AuthKit scope), so opens it all and is
// offered nothing.
Editor bool `json:"editor,omitempty"`
// Unknown: the billing read failed, so Member is false and may be wrong.
Unknown bool `json:"unknown,omitempty"`
}
GroupAccess is a sellable group's sale as a card or page shows it: its key, its active offer and the viewer's standing.
type GroupJoin ¶
type GroupJoin struct {
GroupID string `json:"group_id"`
Key access.Key `json:"key"`
Member bool `json:"member"`
}
GroupJoin is a viewer's free membership of a group.
type LibraryItem ¶
type LibraryItem struct {
ContentID string `json:"content_id"`
Kind string `json:"kind"`
Title string `json:"title"`
Language string `json:"language,omitempty"`
// Deleted content stays readable to its buyers.
Deleted bool `json:"deleted"`
// Whole: the content key, every version. Otherwise VersionIDs are the
// versions bought.
Whole bool `json:"whole"`
VersionIDs []string `json:"version_ids"`
}
LibraryItem is one content item the viewer bought.
type LibraryPage ¶
type LibraryPage struct {
Items []LibraryItem `json:"items"`
NextCursor string `json:"next_cursor,omitempty"`
// Complete is false when the viewer holds more keys than one billing read
// returns (access.HeldLimit); the library lists those it read.
Complete bool `json:"complete"`
}
LibraryPage is one page of a library, newest content first.
type LibraryQuery ¶
type LibraryQuery struct {
// Language picks the titles; the configured languages follow.
Language string
Cursor string // the previous page's next_cursor
Limit int // 20 by default, at most 100
}
LibraryQuery pages a viewer's library.
type ListQuery ¶
type ListQuery struct {
State State // empty: any
Unsynced bool // only those OpenRails does not match yet
Cursor string
Limit int
}
ListQuery pages offers for staff.
type Offer ¶
type Offer struct {
ID string `json:"id"`
Target Target `json:"target"`
Key access.Key `json:"key"`
Terms Terms `json:"terms"`
State State `json:"state"`
// Synced: OpenRails sells it on these terms (or has taken it off sale).
Synced bool `json:"synced"`
ProductID string `json:"product_id,omitempty"`
PriceID string `json:"price_id,omitempty"`
LastError string `json:"last_error,omitempty"`
Revision int64 `json:"revision"`
UpdatedAt time.Time `json:"updated_at"`
}
Offer is one offer on a target.
type OfferPage ¶
type OfferPage struct {
Offers []Offer `json:"offers"`
NextCursor string `json:"next_cursor,omitempty"`
}
OfferPage is one page of offers, newest first.
type Options ¶
type Options struct {
Pool *pgxpool.Pool
Schema string // ContentKit's schema
Tenant string
Kinds kinds.Config
Config Config
// Billing syncs offers and grants free joins; Gate answers joins and the
// library.
Billing access.Billing
Gate *access.Gate
// Queue is the River queue of the sync jobs; contentkit_offers when empty.
Queue string
Logger *slog.Logger
}
Options configures a Service.
type Sale ¶
type Sale struct {
Key access.Key `json:"key"`
Target Target `json:"target"`
Terms Terms `json:"terms"`
PriceID string `json:"price_id,omitempty"`
}
Sale is a key's purchasable offer as a paywall shows it: its terms and the price billing checks out.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the offers module.
func (*Service) Authorize ¶
func (s *Service) Authorize(ctx context.Context, a auth.Authenticator, actor access.Actor, targets []Target) error
Authorize admits actor to change targets' offers (editors): staff, the content's creators, and editors of the user-owned group sold or owning the content.
func (*Service) Current ¶
func (s *Service) Current(ctx context.Context, a auth.Authenticator, actor access.Actor, targets []Target) ([]*Offer, error)
Current answers, per target in order, its latest offer (active, else the last withdrawn) when actor may change its offers (Authorize); nil when they may not or it never had one.
func (*Service) GroupAccess ¶
func (s *Service) GroupAccess(ctx context.Context, viewer access.Actor, groups []string) ([]GroupAccess, error)
GroupAccess answers, per group in order, its key, active offer and the viewer's standing. Internal or inactive entities return ErrNotFound. There is one offers read and at most one billing read (access.Gate.Holds). A failed billing read answers Unknown, not an error.
func (*Service) Handler ¶
Handler serves the offers routes: creator and staff offer edits, free group joins, the viewer's library and the staff offer list. a says who each request is; each request runs in access.WithMemo.
func (*Service) Join ¶
func (s *Service) Join(ctx context.Context, actor access.Actor, groups []string) ([]GroupJoin, error)
Join grants actor each group's key free, through Billing: a sellable group whose active offer is free (amount 0). Internal entities cannot be joined. A viewer already holding the key costs no grant. A group without a free offer is ErrNotJoinable.
func (*Service) Joined ¶
Joined is the public groups whose key viewer holds, bought, subscribed or joined free: the request's one entitlements read (access.Gate.Filter), at most access.HeldLimit of them. A failed read answers none.
func (*Service) Leave ¶
func (s *Service) Leave(ctx context.Context, actor access.Actor, groups []string) ([]GroupJoin, error)
Leave takes back each group's free grant from actor; a bought or subscribed membership stays.
func (*Service) Library ¶
func (s *Service) Library(ctx context.Context, actor access.Actor, q LibraryQuery) (LibraryPage, error)
Library lists the content actor holds a content or version key of: the keys under the tenant's content prefix, from the request's one billing read. Deleted content is listed too, within the current domain's hard visibility boundary.
func (*Service) Live ¶
Live answers the active offers selling keys, by key string: what a paywall (access.Decision.Sell) can sell. Tier keys are the site catalog's.
func (*Service) Put ¶
Put sets each target's offer, in one transaction: a target without an active offer gets one; new terms move the active offer to a new revision; equal terms change nothing. The target's ever_sold is set and a sync job is queued. It answers the active offers, in order.
func (*Service) Reconcile ¶
Reconcile queues a sync of every target with an offer OpenRails does not match yet: those whose job was lost, or whose sync keeps failing.
func (*Service) Resync ¶
Resync queues a sync of the active offers of targets in tx, for a change that moves what OpenRails shows (a content or group renamed); targets without one cost nothing.
func (*Service) RiverJobs ¶
func (s *Service) RiverJobs() riverhelpers.Contribution
RiverJobs contributes the offer sync worker, its queue and the periodic reconcile to the engine's helpers/river composition. It composes once; until bound, Put and Withdraw leave syncing to the reconcile.
func (*Service) Sales ¶
Sales answers, by key string, the purchasable offers selling keys (an access.Decision's Sell): active offers, a paid one once OpenRails sells it on its current terms, with its price; a free group's at once (joining syncs it). Tier keys have none.
func (*Service) Sync ¶
Sync brings OpenRails in line with each target's offers: one product per access key (created when first sold, renamed after the target's title, archived while no offer is active) and, for a paid active offer, the price of its terms; a free offer leaves no price on sale. It is idempotent: prices are keyed by their terms, so a retry changes nothing. Targets are synced one at a time under a lock of their own.
type Target ¶
type Target struct {
Kind TargetKind `json:"kind"`
ID string `json:"id"`
Content string `json:"content_id,omitempty"`
}
Target is one sold thing. Content is a version's content.
type TargetKind ¶
type TargetKind string
TargetKind is what an offer sells.
const ( ContentTarget TargetKind = "content" // every version of a content item VersionTarget TargetKind = "version" // one version GroupTarget TargetKind = "group" // a sellable group: its members and its members-only content )
type Terms ¶
type Terms struct {
// Amount is the price; "0" makes a group free to join.
Amount Amount `json:"amount"`
// Currency is an ISO 4217 code; Config.Currency when empty.
Currency string `json:"currency"`
// AccessHours is how long a purchase opens it; none is forever.
AccessHours *int `json:"access_hours,omitempty"`
// RenewHours makes a group's offer a subscription renewing this often.
RenewHours *int `json:"renew_hours,omitempty"`
}
Terms are what an offer sells for.