Documentation
¶
Overview ¶
Package domain (piece ⑧) holds the entities and business rules of Exhale. It is pure Go: no database, no HTTP, no channels.
Index ¶
- Constants
- Variables
- func BadBillingRule(r engine.Rule, err error) error
- func BadDate(what Word, s string) error
- func BadPrice(amount, currency string) error
- func BadRule(interval int, unit string, err error) error
- func CanTransition(from, to ChargeStatus) bool
- func Capitalize(s string) string
- func Conflict(format string, args ...any) error
- func Fail(kind error, format string, args ...any) error
- func HasPrefix(id, prefix string) bool
- func HasTag(tags []string, tag string) bool
- func Invalid(format string, args ...any) error
- func Missing(format string, args ...any) error
- func NewID(prefix string) string
- func NormalizeTags(tags []string) []string
- func NotFound(format string, args ...any) error
- func SameTag(a, b string) bool
- func ValidateConfirmRepeat(times, everyDays int) error
- func ValidateTag(t string) error
- func WebKey(format string) string
- type AltPrice
- type Category
- type Charge
- type ChargeStatus
- type Details
- type EditScope
- type Error
- type Field
- type Household
- type Member
- type PaymentMethod
- type PaymentMethodKind
- type PriceChange
- type ReminderPolicy
- type Renewal
- type Role
- type Saved
- type Share
- type Subscription
- func (s Subscription) CancelDeadline(d engine.Date) engine.Date
- func (s Subscription) InTrial(d engine.Date) bool
- func (s Subscription) ManualRenewal() bool
- func (s Subscription) PriceOn(d engine.Date) money.Money
- func (s Subscription) Validate() error
- func (s Subscription) VisibleTo(memberID string) bool
- type SubscriptionStatus
- type Text
- type Usage
- type Visibility
- type Wire
- type Word
Constants ¶
const ( DefaultConfirmTimes = 3 DefaultConfirmEveryDays = 1 MaxConfirmTimes = 10 MaxConfirmEveryDays = 30 )
The unanswered "was it charged?" question is asked again every day, up to three times in all, unless the household chooses otherwise.
const ( PrefixHousehold = "hh" PrefixMember = "mem" PrefixSubscription = "sub" PrefixCharge = "chg" PrefixPaymentMethod = "pm" PrefixCategory = "cat" PrefixEvent = "evt" PrefixFlow = "flw" PrefixOutbox = "out" )
ID prefixes make identifiers self-describing for humans and AI agents ("sub_…" is a subscription, "chg_…" a charge).
const ( MaxTags = 20 MaxTagLength = 40 )
Tags are free labels, many per subscription ("work", "family", "to review"). They group across categories; totals stay by category, so a subscription is never counted twice.
Variables ¶
var DefaultCategories = []string{"Streaming", "Music", "Software", "Cloud & Dev", "AI", "News", "Gaming", "Health & Fitness", "Utilities", "Other"}
DefaultCategories are seeded for new households.
var ErrConflict = errors.New("conflict")
ErrConflict is returned on concurrent modification.
var ErrNotFound = errors.New("not found")
ErrNotFound is returned when an entity does not exist (or is not visible).
var ErrValidation = errors.New("validation failed")
ErrValidation wraps every business-rule violation.
Functions ¶
func BadBillingRule ¶
BadBillingRule says why the engine refused r, a rule that may use a weekday of the month or a weekend shift (err from r.Validate).
func CanTransition ¶
func CanTransition(from, to ChargeStatus) bool
CanTransition reports whether a charge may go from one status to another. Every transition back to pending exists so that undo is always possible.
func Capitalize ¶
Capitalize upper-cases the first letter, for a sentence shown alone.
func Missing ¶
Missing is a not-found error whose text says it all, for example "subscription sub_x: not found".
func NormalizeTags ¶
NormalizeTags trims each tag, collapses inner spaces, drops empty ones and duplicates (case-insensitive, the first spelling wins) and keeps the order.
func ValidateConfirmRepeat ¶
ValidateConfirmRepeat checks how often an unanswered question repeats.
func ValidateTag ¶
ValidateTag checks one tag. Commas are not allowed: they separate tags in CSV files and in chat.
Types ¶
type AltPrice ¶
type AltPrice struct {
Price money.Money `json:"price"`
// Unit is the period the price covers: month or year (every 1 unit).
Unit engine.Unit `json:"unit" enum:"month,year"`
}
AltPrice is another price the provider offers for the same service, e.g. "also 99 € per year" for a monthly plan. Exhale only uses it to show what switching would save; it never guesses one.
type Category ¶
type Category struct {
ID string `json:"id"`
HouseholdID string `json:"householdId"`
Name string `json:"name"`
Color string `json:"color,omitempty"`
}
Category groups subscriptions for statistics.
type Charge ¶
type Charge struct {
ID string `json:"id"`
SubscriptionID string `json:"subscriptionId"`
OriginalDate engine.Date `json:"originalDate"`
DueDate engine.Date `json:"dueDate"`
Amount money.Money `json:"amount"`
ChargedAmount money.Money `json:"chargedAmount,omitzero"` // what the bank actually took, any currency
Status ChargeStatus `json:"status"`
ResolvedAt *time.Time `json:"resolvedAt,omitempty"`
Note string `json:"note,omitempty"`
// Decision records that the member chose to keep this renewal ("keep"),
// so Exhale stops asking before the cancellation deadline.
Decision string `json:"decision,omitempty"`
UpdatedAt time.Time `json:"updatedAt"`
Version int `json:"version"`
}
Charge is one occurrence of a subscription's schedule (ADR 0003: charges are events with state). It is identified by (SubscriptionID, OriginalDate); DueDate may move ("this charge only").
func (*Charge) Transition ¶
func (c *Charge) Transition(to ChargeStatus, at time.Time) error
Transition moves the charge to a new status.
type ChargeStatus ¶
type ChargeStatus string
ChargeStatus is the state of one concrete charge. "Delayed" is not a status: it is a pending charge whose DueDate differs from OriginalDate.
const ( ChargePending ChargeStatus = "pending" ChargePaid ChargeStatus = "paid" ChargeFailed ChargeStatus = "failed" ChargeSkipped ChargeStatus = "skipped" )
type Details ¶
type Details struct {
// StartedOn is when the member first subscribed (may be long before the
// first charge Exhale knows about).
StartedOn engine.Date `json:"startedOn,omitzero"`
// TrialEndsOn is the day a free trial turns into the first paid charge.
TrialEndsOn engine.Date `json:"trialEndsOn,omitzero"`
// NoticeDays is how many days before a renewal the provider must be told
// to cancel (0: cancel any time before the charge).
NoticeDays int `json:"noticeDays,omitempty"`
Renewal Renewal `json:"renewal,omitempty"`
// CancelURL is where the subscription is cancelled or managed.
CancelURL string `json:"cancelUrl,omitempty"`
// Account is the login the subscription lives under ("ana@work.com").
Account string `json:"account,omitempty"`
Shares []Share `json:"shares,omitempty"`
// NextPrice is a scheduled price change.
NextPrice *PriceChange `json:"nextPrice,omitempty"`
Usage *Usage `json:"usage,omitempty"`
// Tags are free labels (tags.go).
Tags []string `json:"tags,omitempty"`
// AltPrice is another price the provider offers, e.g. yearly (tags.go).
AltPrice *AltPrice `json:"altPrice,omitempty"`
}
Details are the contract facts of a subscription beyond price and schedule. They decide which dates matter: the end of a free trial, the last day to cancel, the day something expires.
type EditScope ¶
type EditScope string
EditScope says whether an edit applies to one charge or to the series.
func DefaultScope ¶
DefaultScope returns the pre-selected scope for an edit (the user is always asked; this only saves a tap in the common case): a moved date is usually a one-off delay, while price and payment-method changes are permanent.
type Error ¶
Error is a failure people can be shown in their language. Kind is the sentinel errors.Is matches: ErrValidation, ErrNotFound, ErrConflict, or another one such as app.ErrForbidden.
type Household ¶
type Household struct {
ID string `json:"id"`
Name string `json:"name"`
Currency string `json:"currency"` // main currency, ISO 4217
Timezone string `json:"timezone"` // IANA name, e.g. "Europe/Madrid"
Locale string `json:"locale"` // e.g. "es", "en"
// Default time of day for reminders, in Timezone (09:00 by default).
ReminderHour int `json:"reminderHour"`
ReminderMinute int `json:"reminderMinute"`
// Payday is the day of the month the household gets paid (0: not set;
// 31 means the last day of shorter months).
Payday int `json:"payday,omitempty"`
// An unanswered "was it charged?" is asked ConfirmTimes times in all,
// every ConfirmEveryDays days. New subscriptions copy it.
ConfirmTimes int `json:"confirmTimes"`
ConfirmEveryDays int `json:"confirmEveryDays"`
}
Household is one Exhale instance's family or group (ADR 0006).
type Member ¶
type Member struct {
ID string `json:"id"`
HouseholdID string `json:"householdId"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
Role Role `json:"role"`
// Locale is the language this person chose; empty: the household's.
Locale string `json:"locale,omitempty"`
}
Member is a person in the household.
type PaymentMethod ¶
type PaymentMethod struct {
ID string `json:"id"`
HouseholdID string `json:"householdId"`
Name string `json:"name"` // "Revolut", "Visa"
Kind PaymentMethodKind `json:"kind"`
Last4 string `json:"last4,omitempty"`
ExpiresOn engine.Date `json:"expiresOn,omitzero"` // last valid day
OwnerID string `json:"ownerId,omitempty"`
}
PaymentMethod is a card, account or wallet that pays subscriptions.
type PaymentMethodKind ¶
type PaymentMethodKind string
PaymentMethodKind classifies payment methods.
const ( KindCard PaymentMethodKind = "card" KindBank PaymentMethodKind = "bank" KindPayPal PaymentMethodKind = "paypal" KindOther PaymentMethodKind = "other" )
type PriceChange ¶
PriceChange is a price that starts applying on a known date.
type ReminderPolicy ¶
type ReminderPolicy struct {
DaysBefore []int `json:"daysBefore"` // e.g. [3, 0]: three days before and on the day
Hour int `json:"hour"` // local time of day in the household zone
Minute int `json:"minute"`
// ConfirmAfterDays asks "was it charged?" this many days after the due
// date; < 0 disables the question.
ConfirmAfterDays int `json:"confirmAfterDays"`
// ConfirmTimes is how many times "was it charged?" is asked while nobody
// answers (1: once), ConfirmEveryDays the days between two asks.
ConfirmTimes int `json:"confirmTimes"`
ConfirmEveryDays int `json:"confirmEveryDays"`
}
ReminderPolicy says when to remind about each charge.
func DefaultReminderPolicy ¶
func DefaultReminderPolicy() ReminderPolicy
DefaultReminderPolicy: three days before and on the day at 09:00, then ask for confirmation one day after.
func (ReminderPolicy) ConfirmAsks ¶
func (p ReminderPolicy) ConfirmAsks(due engine.Date) []engine.Date
ConfirmAsks returns the dates "was it charged?" is asked for a charge due on due: the first ask and its repeats.
func (ReminderPolicy) Validate ¶
func (p ReminderPolicy) Validate() error
Validate checks the policy.
type Saved ¶
type Saved struct {
F string `json:"f"`
A []savedArg `json:"a,omitempty"`
}
Saved is a Text in a form that survives a round trip through JSON, for texts kept in the database (a proposal waiting for confirmation).
type Share ¶
type Share struct {
// and 1 split two thirds and one third.
Weight int `json:"weight"`
}
Share is the part of a subscription's cost one member carries.
type Subscription ¶
type Subscription struct {
ID string `json:"id"`
HouseholdID string `json:"householdId"`
Name string `json:"name"`
Price money.Money `json:"price"`
Schedule engine.Schedule `json:"schedule"`
CategoryID string `json:"categoryId,omitempty"`
PaymentMethodID string `json:"paymentMethodId,omitempty"`
PayerID string `json:"payerId,omitempty"`
Visibility Visibility `json:"visibility"`
Status SubscriptionStatus `json:"status"`
URL string `json:"url,omitempty"`
Notes string `json:"notes,omitempty"`
AutoConfirm bool `json:"autoConfirm"` // mark charges paid automatically on the due date
Reminders ReminderPolicy `json:"reminders"`
Details
CreatedBy string `json:"createdBy,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Version int `json:"version"` // optimistic locking
}
Subscription is a recurring payment.
func (Subscription) CancelDeadline ¶
func (s Subscription) CancelDeadline(d engine.Date) engine.Date
CancelDeadline returns the last day to cancel before the charge due on day d.
func (Subscription) InTrial ¶
func (s Subscription) InTrial(d engine.Date) bool
InTrial reports whether the free trial is still running on day d.
func (Subscription) ManualRenewal ¶
func (s Subscription) ManualRenewal() bool
ManualRenewal reports whether the subscription expires unless renewed.
func (Subscription) PriceOn ¶
func (s Subscription) PriceOn(d engine.Date) money.Money
PriceOn returns the price of a charge scheduled on day d.
func (Subscription) Validate ¶
func (s Subscription) Validate() error
Validate checks the subscription's own fields.
func (Subscription) VisibleTo ¶
func (s Subscription) VisibleTo(memberID string) bool
VisibleTo reports whether member m may see the subscription.
type SubscriptionStatus ¶
type SubscriptionStatus string
SubscriptionStatus is the lifecycle state of a subscription.
const ( StatusActive SubscriptionStatus = "active" StatusPaused SubscriptionStatus = "paused" StatusCancelled SubscriptionStatus = "cancelled" )
type Text ¶
Text is a sentence: Format is the English text with fmt verbs and the key of its translations; Args fill the verbs. An argument that is a Word, a Text or an error carrying a Text is translated too.
func TextOf ¶
TextOf returns what an error says to people, if it was written with one of the constructors above (possibly wrapped).
func (Text) MarshalJSON ¶
MarshalJSON sends the web's form.
func (Text) Render ¶
Render writes the sentence, looking every format and word up with translate (nil: English).
type Visibility ¶
type Visibility string
Visibility of a subscription inside the household.
const ( VisibilityPrivate Visibility = "private" )