eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3// πŸ€– enterprise.go β€” checkout for the Eleven Enterprise Edition page
4// (elevenmessenger.com/enterprise/): the satirical-but-real €1,100,000/year
5// invoice. A public, CORS-scoped endpoint that mints a Stripe Checkout
6// Session for year one; the page redirects the buyer to Stripe's hosted
7// payment page and celebrates on return.
8//
9// Deliberate shape:
10// - Its Stripe key is LCHAT_STRIPE_ENTERPRISE_KEY, separate from the space
11// billing key and with NO fallback to it β€” so the page runs against the
12// sandbox while real billing stays live, and going live is an explicit
13// operator act (swapping that one value), never a config accident.
14// - The browser calls this directly (the marketing site is static GitHub
15// Pages with no backend), so unlike every other billing route this one is
16// public: CORS pinned to the marketing origin, fixed server-side redirect
17// URLs (the client picks nothing), and a global mint rate limit.
18// - Amounts are compiled in, not client-supplied: the client sends only a
19// seat count, the intern flag, and a currency. The numerals are identical
20// in every currency β€” the elevens are load-bearing, we don't do exchange
21// rates.
22// - mode=payment (year one, one invoice). The +11%/year escalator stays
23// contractual comedy until a subscription-schedule slice earns its keep.
24
25import (
26 "encoding/json"
27 "fmt"
28 "io"
29 "log"
30 "net/http"
31 "net/url"
32 "strconv"
33 "strings"
34 "sync"
35 "time"
36)
37
38// The price list, in minor units (cents/pence β€” EUR, USD and GBP all Γ—100).
39// Base €1,100,000 minus the €121,000 only-for-you offer (11% of base, 11Β²
40// thousand); €11 a seat; the intern β€” one eleventh of an employee β€” exactly 1.
41const (
42 enterpriseBaseMinor = (1_100_000 - 121_000) * 100
43 enterpriseSeatMinor = 11 * 100
44 enterpriseInternMinor = 1 * 100
45 // The intern is only ever the .09 of the blessed 1010.09 headcount.
46 enterpriseInternSeats = 1010
47 // Stripe caps a single payment at 8 digits of the minor unit
48 // (99,999,999 β‰ˆ €999,999.99). The only-for-you offer is what carries the
49 // invoice under that cap at all; past this many seats even the offer
50 // can't save it, and Sales will call you.
51 enterpriseMaxSeats = (99_999_999 - enterpriseBaseMinor) / enterpriseSeatMinor
52)
53
54var enterpriseCurrencies = map[string]bool{"eur": true, "usd": true, "gbp": true}
55
56// enterpriseCheckout holds the page's Stripe config. nil on cloudServer =
57// the feature is off and the endpoint answers 501.
58type enterpriseCheckout struct {
59 key string // LCHAT_STRIPE_ENTERPRISE_KEY β€” sandbox until the deliberate live flip
60 apiBase string // https://api.stripe.com; tests point it at a fake
61 origin string // the marketing origin: CORS allow + redirect base
62 client *http.Client
63 limiter mintLimiter
64}
65
66func newEnterpriseCheckout(key, apiBase, origin string) *enterpriseCheckout {
67 key = strings.TrimSpace(key)
68 if key == "" {
69 return nil
70 }
71 return &enterpriseCheckout{
72 key: key,
73 apiBase: apiBase,
74 origin: strings.TrimRight(origin, "/"),
75 client: &http.Client{Timeout: 20 * time.Second},
76 limiter: mintLimiter{max: 30, window: time.Minute},
77 }
78}
79
80// mintLimiter is a global rolling-window cap on session mints β€” this endpoint
81// is public, and each hit costs a Stripe API call. Per-IP bookkeeping isn't
82// worth carrying for a page whose realistic traffic is one CFO at a time.
83type mintLimiter struct {
84 mu sync.Mutex
85 stamps []time.Time
86 max int
87 window time.Duration
88}
89
90func (l *mintLimiter) allow(now time.Time) bool {
91 l.mu.Lock()
92 defer l.mu.Unlock()
93 keep := l.stamps[:0]
94 for _, t := range l.stamps {
95 if now.Sub(t) < l.window {
96 keep = append(keep, t)
97 }
98 }
99 l.stamps = keep
100 if len(l.stamps) >= l.max {
101 return false
102 }
103 l.stamps = append(l.stamps, now)
104 return true
105}
106
107// enterpriseCORS stamps the CORS headers on every response from the endpoint.
108// The allow-origin is the single marketing origin, never *: the page is the
109// only legitimate caller, and scoping it keeps random sites from spending our
110// Stripe rate limit from their visitors' browsers.
111func (s *cloudServer) enterpriseCORS(w http.ResponseWriter) bool {
112 if s.enterprise == nil {
113 return false
114 }
115 h := w.Header()
116 h.Set("Access-Control-Allow-Origin", s.enterprise.origin)
117 h.Set("Vary", "Origin")
118 return true
119}
120
121// enterprisePreflightHandler: OPTIONS /api/enterprise/checkout β€” the CORS
122// preflight for the cross-origin JSON POST below.
123func (s *cloudServer) enterprisePreflightHandler(w http.ResponseWriter, r *http.Request) {
124 if !s.enterpriseCORS(w) {
125 w.WriteHeader(http.StatusNotImplemented)
126 return
127 }
128 h := w.Header()
129 h.Set("Access-Control-Allow-Methods", "POST")
130 h.Set("Access-Control-Allow-Headers", "Content-Type")
131 h.Set("Access-Control-Max-Age", "86400")
132 w.WriteHeader(http.StatusNoContent)
133}
134
135// enterpriseCheckoutHandler: POST /api/enterprise/checkout β€” mint the year-one
136// Checkout Session. Body: {"seats": n, "intern": bool, "currency": "eur"}.
137func (s *cloudServer) enterpriseCheckoutHandler(w http.ResponseWriter, r *http.Request) {
138 if !s.enterpriseCORS(w) {
139 writeControlErr(w, http.StatusNotImplemented, "unavailable", "enterprise checkout is not configured")
140 return
141 }
142 e := s.enterprise
143 if !e.limiter.allow(time.Now()) {
144 writeControlErr(w, http.StatusTooManyRequests, "slow_down", "the till is busy β€” try again in a minute")
145 return
146 }
147 var req struct {
148 Seats int `json:"seats"`
149 Intern bool `json:"intern"`
150 Currency string `json:"currency"`
151 }
152 if err := json.NewDecoder(io.LimitReader(r.Body, 4<<10)).Decode(&req); err != nil {
153 writeControlErr(w, http.StatusBadRequest, "bad_request", "invalid JSON body")
154 return
155 }
156 cur := strings.ToLower(strings.TrimSpace(req.Currency))
157 if !enterpriseCurrencies[cur] {
158 writeControlErr(w, http.StatusBadRequest, "bad_request", "currency must be eur, usd or gbp")
159 return
160 }
161 if req.Seats < 1 {
162 writeControlErr(w, http.StatusBadRequest, "bad_request", "at least one employee required")
163 return
164 }
165 // The intern is exactly the .09 of 1010.09 β€” any other fractional
166 // headcount was already rounded to a person by the page.
167 if req.Intern && req.Seats != enterpriseInternSeats {
168 writeControlErr(w, http.StatusBadRequest, "bad_request", "the intern only joins a 1,010-person team")
169 return
170 }
171 if req.Seats > enterpriseMaxSeats {
172 writeControlErr(w, http.StatusBadRequest, "over_stripe_cap",
173 fmt.Sprintf("Stripe cannot carry this much money in one payment β€” %s employees is the most it can hold. Sales will call you.",
174 formatCount(enterpriseMaxSeats)))
175 return
176 }
177 checkoutURL, err := e.createSession(cur, req.Seats, req.Intern)
178 if err != nil {
179 log.Printf("cloud: enterprise checkout (%s, %d seats, intern=%v): %v", cur, req.Seats, req.Intern, err)
180 writeControlErr(w, http.StatusBadGateway, "checkout_failed", "couldn't start the checkout β€” please try again")
181 return
182 }
183 writeJSON(w, http.StatusOK, map[string]any{"url": checkoutURL})
184}
185
186// createSession builds the hosted Checkout page: three price_data line items
187// (no dashboard Prices to manage), fixed redirect URLs on the marketing
188// origin, and no customer identity β€” Stripe collects what it needs.
189func (e *enterpriseCheckout) createSession(currency string, seats int, intern bool) (string, error) {
190 form := url.Values{}
191 form.Set("mode", "payment")
192 form.Set("line_items[0][price_data][currency]", currency)
193 form.Set("line_items[0][price_data][product_data][name]", "Eleven Enterprise Edition β€” year one (only-for-you offer applied)")
194 form.Set("line_items[0][price_data][unit_amount]", strconv.Itoa(enterpriseBaseMinor))
195 form.Set("line_items[0][quantity]", "1")
196 form.Set("line_items[1][price_data][currency]", currency)
197 form.Set("line_items[1][price_data][product_data][name]", "Seat β€” one employee, one year")
198 form.Set("line_items[1][price_data][unit_amount]", strconv.Itoa(enterpriseSeatMinor))
199 form.Set("line_items[1][quantity]", strconv.Itoa(seats))
200 if intern {
201 form.Set("line_items[2][price_data][currency]", currency)
202 form.Set("line_items[2][price_data][product_data][name]", "The intern (one eleventh of an employee)")
203 form.Set("line_items[2][price_data][unit_amount]", strconv.Itoa(enterpriseInternMinor))
204 form.Set("line_items[2][quantity]", "1")
205 }
206 form.Set("metadata[enterprise]", "1")
207 form.Set("metadata[seats]", strconv.Itoa(seats))
208 form.Set("metadata[intern]", strconv.FormatBool(intern))
209 form.Set("success_url", e.origin+"/enterprise/?paid=1")
210 form.Set("cancel_url", e.origin+"/enterprise/")
211
212 req, err := http.NewRequest("POST", e.apiBase+"/v1/checkout/sessions", strings.NewReader(form.Encode()))
213 if err != nil {
214 return "", err
215 }
216 req.Header.Set("Authorization", "Bearer "+e.key)
217 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
218 resp, err := e.client.Do(req)
219 if err != nil {
220 return "", err
221 }
222 defer resp.Body.Close()
223 var body struct {
224 URL string `json:"url"`
225 Error struct {
226 Message string `json:"message"`
227 } `json:"error"`
228 }
229 if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&body); err != nil {
230 return "", fmt.Errorf("stripe: HTTP %d (unparseable body)", resp.StatusCode)
231 }
232 if resp.StatusCode != http.StatusOK || body.URL == "" {
233 return "", fmt.Errorf("stripe: HTTP %d: %s", resp.StatusCode, body.Error.Message)
234 }
235 return body.URL, nil
236}
237
238// formatCount renders a count with locale-style thousands separators for the
239// one English error message that carries a number ("1,909 employees…").
240func formatCount(n int) string {
241 s := strconv.Itoa(n)
242 for i := len(s) - 3; i > 0; i -= 3 {
243 s = s[:i] + "," + s[i:]
244 }
245 return s
246}
247