rastrillo / abono Public

Clone
git clone https://amadan.net/rastrillo/abono

Plain git — no account needed to clone.

Download

Download this file

1package abono
2
3import (
4 "html/template"
5 "strings"
6 "time"
7)
8
9// The keys of a Status, for the app to translate. Tones are status-pill's.
10const (
11 StatusActive = "abono.status.active"
12 StatusRenews = "abono.status.renews"
13 StatusCancelling = "abono.status.cancelling"
14 StatusFailing = "abono.status.failing"
15 StatusEnded = "abono.status.ended"
16 StatusComp = "abono.status.comp"
17 StatusCompUntil = "abono.status.comp_until"
18)
19
20// Status is the one line a holding reads as: a message key, a tone, and
21// the date the key's sentence mentions, when it has one.
22type Status struct {
23 Key string
24 Tone string // positive, warning, negative or neutral
25 Date *time.Time
26}
27
28// Describe is what to say of h at now. It applies the same holding rule as
29// Holdings, at the now it is given, so a stale Holding cannot say "active"
30// of access that has ended.
31//
32// active grants, no date known
33// renews active or trialing, with a period end, not cancelling: Date is the period end
34// cancelling cancel_at_period_end and granting: Date is the period end
35// failing past_due and granting: Date is the day access stops, 30 days after the
36// first unpaid period started (UnpaidSince, else PeriodStart; after the
37// period end when neither is known), or the period end if that is
38// sooner and it is cancelling
39// ended does not grant
40// comp a comp with no end
41// comp_until a comp with an end: Date is ends_at
42//
43// A past_due subscription that is also cancelling reads as failing: the
44// payment problem is what the reader can act on, and the date it gives is
45// the one access really stops on.
46func Describe(h Holding, now time.Time) Status {
47 granting, _ := h.evaluate(now)
48 if !granting {
49 return Status{Key: StatusEnded, Tone: "neutral"}
50 }
51 if h.Source == SourceComp {
52 if h.Until == nil {
53 return Status{Key: StatusComp, Tone: "positive"}
54 }
55 return Status{Key: StatusCompUntil, Tone: "positive", Date: copyTime(h.Until)}
56 }
57 switch {
58 case h.Status == "past_due":
59 // Granting means there is a stop date to give (ruling R30).
60 stops, ok := pastDueStops(h.UnpaidSince, h.PeriodStart, h.Until, h.CancelAtPeriodEnd)
61 if !ok {
62 return Status{Key: StatusEnded, Tone: "neutral"}
63 }
64 return Status{Key: StatusFailing, Tone: "warning", Date: &stops}
65 case h.CancelAtPeriodEnd:
66 return Status{Key: StatusCancelling, Tone: "warning", Date: copyTime(h.Until)}
67 case h.Until != nil:
68 return Status{Key: StatusRenews, Tone: "positive", Date: copyTime(h.Until)}
69 }
70 return Status{Key: StatusActive, Tone: "positive"}
71}
72
73func copyTime(t *time.Time) *time.Time {
74 if t == nil {
75 return nil
76 }
77 c := *t
78 return &c
79}
80
81// Describe is Describe(h, now), as a method so a template can call it:
82// {{.Holding.Describe .Now}}.
83func (h Holding) Describe(now time.Time) Status { return Describe(h, now) }
84
85// Sentence is the status's words as markup, for a template: text is the
86// key's catalog text with its {date} still in it, and dateText is what to
87// show for the date (empty shows the date as year-month-day). The date
88// goes in a <time datetime> element, so it stays machine-readable
89// whatever words the app gave it. Both strings are escaped; a status
90// with no date, or text with no {date}, is just the text.
91func (s Status) Sentence(text, dateText string) template.HTML {
92 before, after, found := strings.Cut(text, "{date}")
93 if s.Date == nil || !found {
94 return template.HTML(template.HTMLEscapeString(text))
95 }
96 iso := s.Date.Format("2006-01-02")
97 if dateText == "" {
98 dateText = iso
99 }
100 return template.HTML(template.HTMLEscapeString(before) +
101 `<time datetime="` + iso + `">` + template.HTMLEscapeString(dateText) + `</time>` +
102 template.HTMLEscapeString(after))
103}
104