engine

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 11, 2026 License: AGPL-3.0 Imports: 4 Imported by: 0

Documentation

Overview

Package engine is the billing time engine (piece ⑨): civil dates and billing-style recurrence. It is pure: no I/O, no clock, no time zones except when converting a Date to an instant.

Index

Constants

This section is empty.

Variables

View Source
var ErrInvalidDate = errors.New("engine: invalid date")

ErrInvalidDate is returned when a year/month/day triple does not exist.

View Source
var ErrInvalidRule = errors.New("engine: invalid rule")

ErrInvalidRule is returned for rules that cannot produce charges.

View Source
var ErrInvalidSchedule = errors.New("engine: invalid schedule")

ErrInvalidSchedule is returned for schedules that cannot be evaluated.

Functions

func DaysIn

func DaysIn(y int, m time.Month) int

DaysIn returns the number of days in month m of year y.

Types

type Date

type Date struct {
	// contains filtered or unexported fields
}

Date is a civil calendar date (year, month, day) with no time and no time zone. A charge happens on a Date; a reminder happens at an instant.

The zero value is not a valid date; use IsZero to detect it.

func DateOf

func DateOf(t time.Time) Date

DateOf returns the civil date of t in t's location.

func MustDate

func MustDate(y int, m time.Month, d int) Date

MustDate is like NewDate but panics on invalid input. Use it for constants and tests.

func NewDate

func NewDate(y int, m time.Month, d int) (Date, error)

NewDate returns the date y-m-d, or ErrInvalidDate if it does not exist.

func ParseDate

func ParseDate(s string) (Date, error)

ParseDate parses an ISO 8601 calendar date (YYYY-MM-DD).

func WeekdayOfMonth

func WeekdayOfMonth(y int, m time.Month, wd time.Weekday, n int) Date

WeekdayOfMonth returns the n-th weekday wd of a month (n = 1 to 4), or the last one when n is -1.

func (Date) AddDays

func (d Date) AddDays(n int) Date

AddDays returns the date n days after d (n may be negative).

func (Date) After

func (d Date) After(other Date) bool

func (Date) At

func (d Date) At(hour, min int, loc *time.Location) time.Time

At returns the instant at hour:min on this date in loc. Wall-clock times that do not exist because of DST are normalized by the time package.

func (Date) Before

func (d Date) Before(other Date) bool

func (Date) Compare

func (d Date) Compare(other Date) int

Compare returns -1, 0 or +1 depending on whether d is before, equal to or after other.

func (Date) Day

func (d Date) Day() int

func (Date) DaysUntil

func (d Date) DaysUntil(other Date) int

DaysUntil returns the number of days from d to other (negative if other is before d).

func (Date) IsZero

func (d Date) IsZero() bool

func (Date) MarshalText

func (d Date) MarshalText() ([]byte, error)

MarshalText implements encoding.TextMarshaler (used by JSON).

func (Date) Month

func (d Date) Month() time.Month

func (*Date) Scan

func (d *Date) Scan(src any) error

Scan implements sql.Scanner.

func (Date) String

func (d Date) String() string

String formats the date as YYYY-MM-DD.

func (*Date) UnmarshalText

func (d *Date) UnmarshalText(b []byte) error

UnmarshalText implements encoding.TextUnmarshaler.

func (Date) Value

func (d Date) Value() (driver.Value, error)

Value implements driver.Valuer: dates are stored as TEXT (YYYY-MM-DD).

func (Date) Weekday

func (d Date) Weekday() time.Weekday

Weekday returns the day of the week of d.

func (Date) Year

func (d Date) Year() int

type DayRule

type DayRule string

DayRule says which day of the month a monthly or yearly rule charges on. It is read from the anchor, so a rule has a single source of truth.

const (
	// DayOfMonth charges on the anchor's day number, or on the last day of
	// shorter months (the default).
	DayOfMonth DayRule = ""
	// NthWeekday charges on the anchor's weekday in the anchor's week of the
	// month: anchored on the third Wednesday, it is always the third
	// Wednesday. Only the 1st to 4th exist in every month.
	NthWeekday DayRule = "weekday"
	// LastWeekday charges on the last of the anchor's weekday in the month
	// ("the last Friday"). The anchor must be in the last 7 days of its month.
	LastWeekday DayRule = "last-weekday"
)

type Rule

type Rule struct {
	Interval int     `json:"interval"`
	Unit     Unit    `json:"unit"`
	Anchor   Date    `json:"anchor"`
	DayRule  DayRule `json:"dayRule,omitempty"`
	Shift    Shift   `json:"weekendShift,omitempty"`
}

Rule is a billing recurrence: a charge every Interval Units, starting at Anchor.

Billing semantics (as Stripe, Apple and Google bill):

  • every occurrence is computed from the anchor, never from the previous occurrence, so there is no drift;
  • when the anchor day does not exist in a month, the charge falls on the last day of that month (31 Jan → 28/29 Feb → 31 Mar).

Monthly and yearly rules can instead follow a weekday of the month (the third Wednesday, the last Friday; see DayRule). Any rule can move charges that fall on a weekend to the next or previous business day (Shift). The shift moves the due date only: the next charge is still computed from the anchor, so a shifted charge never moves the ones after it.

func (Rule) NextOnOrAfter

func (r Rule) NextOnOrAfter(d Date) (Date, int)

NextOnOrAfter returns the first charge date on or after d and its index.

func (Rule) Nominal

func (r Rule) Nominal(n int) Date

Nominal returns the n-th date the rule is anchored on, before any weekend shift (n = 0 is the anchor). Every nominal date is computed from the anchor, never from the previous charge.

func (Rule) Occurrence

func (r Rule) Occurrence(n int) Date

Occurrence returns the n-th charge date of the rule (n = 0 is the first charge): the nominal date moved off the weekend when the rule says so. It panics if n < 0 or if the rule is invalid; call Validate first.

func (Rule) Reanchor

func (r Rule) Reanchor(anchor Date) Rule

Reanchor returns the same kind of rule anchored on a new date, for "from now on" changes. A weekday rule keeps following a weekday of the month: the new anchor's (the last one when it is in the last week and the rule was "last", or when it is a fifth weekday).

func (Rule) Validate

func (r Rule) Validate() error

Validate reports whether the rule can produce charges.

func (Rule) Week

func (r Rule) Week() int

Week returns which weekday of the month the rule charges on: 1 to 4 for NthWeekday, -1 for LastWeekday and 0 for DayOfMonth.

type Schedule

type Schedule struct {
	Versions []Version `json:"versions"`
	// Until, if set, is the last date on which a charge may happen
	// (inclusive). Used for end dates and cancellation at period end.
	Until Date `json:"until,omitzero"`
	// MaxCharges, if > 0, limits the total number of charges (contracts with
	// a fixed number of payments).
	MaxCharges int `json:"maxCharges,omitempty"`
}

Schedule is the full billing timeline of a subscription: one or more rule versions plus optional limits. Past charges keep the rule they were created under; "this and following" edits append a new version.

func NewSchedule

func NewSchedule(r Rule) (Schedule, error)

NewSchedule returns a schedule with a single rule, in effect from its first charge (or its anchor, when a weekend shift moves the first charge later).

func (Schedule) Between

func (s Schedule) Between(from, to Date) []Date

Between returns all charge dates d with from <= d <= to, in order.

func (Schedule) ChangeFrom

func (s Schedule) ChangeFrom(at, newAnchor Date, interval int, unit Unit) (Schedule, error)

ChangeFrom implements "this and following" edits. The charge that was due on `at` (and every later one) is replaced by a new rule anchored on newAnchor. Charges before min(at, newAnchor) keep their previous rule.

func (Schedule) ChangeRule

func (s Schedule) ChangeRule(at Date, r Rule) (Schedule, error)

ChangeRule is ChangeFrom with a whole rule (day rule and weekend shift included): the charge due on `at` and every later one follow r. Charges before min(at, r's first charge) keep their previous rule.

func (Schedule) Current

func (s Schedule) Current(d Date) Rule

Current returns the rule in effect on date d.

func (Schedule) First

func (s Schedule) First() Date

First returns the first charge date.

func (Schedule) NextOnOrAfter

func (s Schedule) NextOnOrAfter(d Date) (Date, bool)

NextOnOrAfter returns the first charge date on or after d, if any.

func (Schedule) NominalOf

func (s Schedule) NominalOf(d Date) (Date, bool)

NominalOf returns the date the charge due on d is anchored on, before any weekend shift. It reports false when no charge of the schedule is due on d.

func (Schedule) Upcoming

func (s Schedule) Upcoming(d Date, n int) []Date

Upcoming returns at most n charge dates on or after d.

func (Schedule) Validate

func (s Schedule) Validate() error

Validate checks every version and their ordering.

type Shift

type Shift string

Shift moves a charge that falls on a weekend to a business day. Only Saturdays and Sundays count: there are no holiday calendars.

const (
	NoShift             Shift = ""
	NextBusinessDay     Shift = "next"
	PreviousBusinessDay Shift = "previous"
)

func (Shift) Apply

func (s Shift) Apply(d Date) Date

Apply returns d moved off the weekend in the direction of the shift.

type Unit

type Unit string

Unit is the period unit of a billing rule.

const (
	Day   Unit = "day"
	Week  Unit = "week"
	Month Unit = "month"
	Year  Unit = "year"
)

func (Unit) Valid

func (u Unit) Valid() bool

Valid reports whether u is a known unit.

type Version

type Version struct {
	From Date `json:"from"`
	Rule Rule `json:"rule"`
}

Version is a rule in effect from a given date (inclusive) until the From of the next version (exclusive).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL