| 1 | package abono |
| 2 | |
| 3 | import ( |
| 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. |
| 10 | const ( |
| 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. |
| 22 | type 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. |
| 46 | func 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 | |
| 73 | func 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}}. |
| 83 | func (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. |
| 91 | func (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 | |