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// πŸ€– Lifetime access levels (docs/managed-payments.md Β§ Lifetime): pay once,
4// in forever. These pin the four load-bearing properties: the grant has no
5// lifecycle (no sweep, no repair pass may ever touch it), a refund/chargeback
6// un-buys it immediately, both providers mint payment-mode checkouts with the
7// commission applied as a fixed amount, and the renewal-shaped member
8// endpoints (cancel/resume/change/portal) answer honestly instead of
9// confusing Stripe with a payment-intent id.
10
11import (
12 "context"
13 "encoding/json"
14 "fmt"
15 "net/http"
16 "strconv"
17 "strings"
18 "testing"
19 "time"
20)
21
22// countingPay is a configured provider that counts SubState fetches β€” the
23// repair-pass skip is the property under test, so the count is the assert.
24type countingPay struct {
25 noPay
26 subStateCalls int
27}
28
29func (c *countingPay) Configured() bool { return true }
30func (c *countingPay) SubState(context.Context, string) (SubEvent, bool, error) {
31 c.subStateCalls++
32 return SubEvent{}, false, nil
33}
34
35// lifetimeGrant is the event a completed payment-mode checkout produces.
36func lifetimeGrant(uid, tid string, now int64) SubEvent {
37 return SubEvent{
38 ExtSub: "pi_life_1", ExtCustomer: "cus_1", UserRef: uid, TierRef: tid,
39 Status: "active", Created: now, AllowCreate: true, Lifetime: true,
40 }
41}
42
43func TestLifetimeGrantNeverSwept(t *testing.T) {
44 _, srv := newTestServer(t)
45 tid, uid := seedTierAndMember(t, srv, "Founder", "bea")
46 now := int64(3_000_000)
47
48 if err := srv.applyWebhookEvent(context.Background(), lifetimeGrant(uid, tid, now), now); err != nil {
49 t.Fatal(err)
50 }
51 if !srv.db.tierMember(tid, uid) {
52 t.Fatal("member not assigned after lifetime purchase")
53 }
54 e, ok := srv.db.memberEntitlement(tid, uid)
55 if !ok || !e.Lifetime || e.Status != "active" || e.ExtSub != "pi_life_1" {
56 t.Fatalf("entitlement = %+v ok=%v, want active lifetime on pi_life_1", e, ok)
57 }
58
59 // A decade of sweeps: the grace clause can't match (no deadline), the
60 // silent-lapse backstop can't match (no paid_until), and the repair pass
61 // must SKIP the row β€” there is no subscription to re-fetch, and a
62 // not-found answer would revoke a valid purchase.
63 fake := &countingPay{}
64 srv.pay = fake
65 tenYears := now + 10*365*24*3600
66 if n := srv.runBillingSweep(tenYears); n != 0 {
67 t.Fatalf("sweep revoked %d membership(s) β€” a lifetime purchase expired", n)
68 }
69 if !srv.db.tierMember(tid, uid) {
70 t.Fatal("lifetime membership gone after sweeps")
71 }
72 if fake.subStateCalls != 0 {
73 t.Fatalf("repair pass fetched provider state %d time(s) for a lifetime row", fake.subStateCalls)
74 }
75
76 // Redelivery of the same grant is a no-op, not a duplicate.
77 if err := srv.applyWebhookEvent(context.Background(), lifetimeGrant(uid, tid, now), now); err != nil {
78 t.Fatal(err)
79 }
80 if !srv.db.tierMember(tid, uid) {
81 t.Fatal("redelivered grant broke the membership")
82 }
83}
84
85func TestLifetimeRefundRevokes(t *testing.T) {
86 _, srv := newTestServer(t)
87 tid, uid := seedTierAndMember(t, srv, "Founder", "bea")
88 now := int64(3_000_000)
89 if err := srv.applyWebhookEvent(context.Background(), lifetimeGrant(uid, tid, now), now); err != nil {
90 t.Fatal(err)
91 }
92
93 // The refund/chargeback event: revoke is immediate β€” grace windows exist
94 // to survive a failing card, and nothing here is failing.
95 refund := SubEvent{
96 ExtSub: "pi_life_1", UserRef: uid, TierRef: tid,
97 Status: "canceled", Created: now + 100, Deleted: true, Lifetime: true,
98 }
99 if err := srv.applyWebhookEvent(context.Background(), refund, now+100); err != nil {
100 t.Fatal(err)
101 }
102 if srv.db.tierMember(tid, uid) {
103 t.Fatal("membership survived a refund")
104 }
105 // And the no-resurrect property holds: a redelivered grant older than the
106 // refund must not re-buy the level.
107 if err := srv.applyWebhookEvent(context.Background(), lifetimeGrant(uid, tid, now), now+200); err != nil {
108 t.Fatal(err)
109 }
110 if srv.db.tierMember(tid, uid) {
111 t.Fatal("stale redelivered grant resurrected a refunded purchase")
112 }
113}
114
115// connectTestProvider is a stripeConnect against a captured fake Stripe.
116func connectTestProvider(t *testing.T, srv *Server) (*stripeConnect, *map[string][]string) {
117 t.Helper()
118 stripe, captured, _ := fakePlatformStripe(t)
119 if err := srv.db.setConfig("stripe_connect_account", "acct_admin"); err != nil {
120 t.Fatal(err)
121 }
122 p := &stripeConnect{db: srv.db, key: "sk", webhookSecret: "whsec_connect", apiBase: stripe.URL, client: http.DefaultClient}
123 return p, (*map[string][]string)(captured)
124}
125
126func TestLifetimeCheckoutForms(t *testing.T) {
127 _, srv := newTestServer(t)
128
129 // BYO: mode=payment, the fee as a fixed amount (5.5% of the price), the
130 // routing metadata mirrored onto the payment intent for refund routing,
131 // and nothing subscription-shaped.
132 pc, capturedC := connectTestProvider(t, srv)
133 if _, err := pc.Checkout(context.Background(), CheckoutReq{
134 TierID: "t1", PriceRef: "price_once", UserRef: "u1",
135 Once: true, AmountMinor: 11000,
136 SuccessURL: "https://s/ok", CancelURL: "https://s/no",
137 }); err != nil {
138 t.Fatal(err)
139 }
140 fc := *capturedC
141 if got := fc["mode"]; len(got) != 1 || got[0] != "payment" {
142 t.Errorf("BYO mode = %v", got)
143 }
144 if got := fc["payment_intent_data[application_fee_amount]"]; len(got) != 1 || got[0] != "605" {
145 t.Errorf("BYO fee = %v, want 605 (5.5%% of 11000)", got)
146 }
147 if got := fc["payment_intent_data[metadata][tier_ref]"]; len(got) != 1 || got[0] != "t1" {
148 t.Errorf("BYO intent metadata = %v", fc)
149 }
150 for k := range fc {
151 if strings.Contains(k, "subscription_data") {
152 t.Errorf("BYO once checkout sent %s", k)
153 }
154 }
155
156 // Managed: mode=payment, the admin's share as a fixed transfer computed
157 // from the chosen commission (8% of 11000 β†’ 10120 transfers).
158 if err := srv.db.setConfig("stripe_express_account", "acct_express"); err != nil {
159 t.Fatal(err)
160 }
161 if err := srv.db.setConfig("managed_commission_pct", "8"); err != nil {
162 t.Fatal(err)
163 }
164 stripe, capturedP, _ := fakePlatformStripe(t)
165 pp := &stripePlatform{db: srv.db, key: "sk", webhookSecret: "whsec_platform", apiBase: stripe.URL, client: http.DefaultClient}
166 if _, err := pp.Checkout(context.Background(), CheckoutReq{
167 TierID: "t1", PriceRef: "price_once", UserRef: "u1",
168 Once: true, AmountMinor: 11000,
169 SuccessURL: "https://s/ok", CancelURL: "https://s/no",
170 }); err != nil {
171 t.Fatal(err)
172 }
173 fp := *capturedP
174 if got := fp.Get("payment_intent_data[transfer_data][amount]"); got != "10120" {
175 t.Errorf("managed transfer = %q, want 10120 (92%% of 11000)", got)
176 }
177 if got := fp.Get("payment_intent_data[transfer_data][destination]"); got != "acct_express" {
178 t.Errorf("managed destination = %q", got)
179 }
180 for k := range fp {
181 if strings.Contains(k, "subscription_data") || strings.Contains(k, "application_fee") {
182 t.Errorf("managed once checkout sent %s", k)
183 }
184 }
185}
186
187func TestLifetimeWebhookParsing(t *testing.T) {
188 _, srv := newTestServer(t)
189 if err := srv.db.setConfig("stripe_express_account", "acct_ours"); err != nil {
190 t.Fatal(err)
191 }
192 p := &stripePlatform{db: srv.db, key: "sk", webhookSecret: "whsec_platform", apiBase: "http://unused", client: http.DefaultClient}
193
194 // A payment-mode session with our binding β†’ a lifetime grant on the
195 // payment intent id.
196 session := map[string]any{
197 "mode": "payment", "customer": "cus1", "payment_intent": "pi_1",
198 "metadata": map[string]any{"user_ref": "u1", "tier_ref": "t1", "space_ref": "acct_ours"},
199 }
200 body, sig := platformEvent(t, "checkout.session.completed", session)
201 evs, err := p.ParseWebhook(sig, body)
202 if err != nil || len(evs) != 1 {
203 t.Fatalf("grant: %+v, %v", evs, err)
204 }
205 if e := evs[0]; !e.Lifetime || !e.AllowCreate || e.Checkout || e.ExtSub != "pi_1" {
206 t.Errorf("grant = %+v, want lifetime AllowCreate on pi_1, never a receipt", e)
207 }
208
209 // A donate/enterprise-shaped payment session (no binding): dropped.
210 stray := map[string]any{"mode": "payment", "payment_intent": "pi_x", "metadata": map[string]any{"donation": "1"}}
211 body, sig = platformEvent(t, "checkout.session.completed", stray)
212 if evs, err := p.ParseWebhook(sig, body); err != nil || evs != nil {
213 t.Fatalf("stray payment session must drop: %+v, %v", evs, err)
214 }
215
216 // charge.refunded with our stamped metadata β†’ the un-buy event.
217 refund := map[string]any{
218 "payment_intent": "pi_1",
219 "metadata": map[string]any{"user_ref": "u1", "tier_ref": "t1", "space_ref": "acct_ours"},
220 }
221 body, sig = platformEvent(t, "charge.refunded", refund)
222 evs, err = p.ParseWebhook(sig, body)
223 if err != nil || len(evs) != 1 || !evs[0].Deleted || !evs[0].Lifetime || evs[0].ExtSub != "pi_1" {
224 t.Fatalf("refund: %+v, %v", evs, err)
225 }
226 // A refund on some other space's charge: dropped.
227 foreign := map[string]any{
228 "payment_intent": "pi_2",
229 "metadata": map[string]any{"tier_ref": "t9", "space_ref": "acct_theirs"},
230 }
231 body, sig = platformEvent(t, "charge.refunded", foreign)
232 if evs, err := p.ParseWebhook(sig, body); err != nil || evs != nil {
233 t.Fatalf("foreign refund must drop: %+v, %v", evs, err)
234 }
235
236 // The BYO provider: same shapes, bound by connected account instead.
237 if err := srv.db.setConfig("stripe_connect_account", "acct_admin"); err != nil {
238 t.Fatal(err)
239 }
240 pc := &stripeConnect{db: srv.db, key: "sk", webhookSecret: "whsec_connect", apiBase: "http://unused", client: http.DefaultClient}
241 connectEvent := func(evType string, object map[string]any) ([]byte, string) {
242 raw, _ := json.Marshal(map[string]any{
243 "type": evType, "account": "acct_admin", "created": 1000,
244 "data": map[string]any{"object": object},
245 })
246 return raw, stripeSign(raw, "whsec_connect", time.Now())
247 }
248 sess := map[string]any{
249 "mode": "payment", "customer": "cus1", "payment_intent": "pi_c1",
250 "metadata": map[string]any{"user_ref": "u1", "tier_ref": "t1"},
251 }
252 body, sig = connectEvent("checkout.session.completed", sess)
253 evs, err = pc.ParseWebhook(sig, body)
254 if err != nil || len(evs) != 1 || !evs[0].Lifetime || evs[0].ExtSub != "pi_c1" {
255 t.Fatalf("BYO grant: %+v, %v", evs, err)
256 }
257 body, sig = connectEvent("charge.refunded", map[string]any{
258 "payment_intent": "pi_c1", "metadata": map[string]any{"tier_ref": "t1", "user_ref": "u1"},
259 })
260 evs, err = pc.ParseWebhook(sig, body)
261 if err != nil || len(evs) != 1 || !evs[0].Deleted || !evs[0].Lifetime {
262 t.Fatalf("BYO refund: %+v, %v", evs, err)
263 }
264}
265
266func TestLifetimePriceFloorAndMemberGuards(t *testing.T) {
267 ts, srv := newTestServer(t)
268 srv.pay = &fakePay{checkoutURL: "https://pay.example/checkout"}
269
270 mkUser := func(label string) (string, string) {
271 u, err := srv.db.createUser(label)
272 if err != nil {
273 t.Fatal(err)
274 }
275 if err := srv.db.createKeyInvite("inv-"+label, u, "wrap", testClaimHash, 0); err != nil {
276 t.Fatal(err)
277 }
278 tk, ok, err := srv.db.claimKeyInvite("inv-"+label, label, "", testClaimSecret)
279 if err != nil || !ok {
280 t.Fatalf("claim: ok=%v err=%v", ok, err)
281 }
282 if err := srv.db.setUserPublicKey(u, "pub-"+label, ""); err != nil {
283 t.Fatal(err)
284 }
285 return tk, u
286 }
287 admin, uidAdmin := mkUser("Root")
288 if err := srv.db.setUserRole(uidAdmin, "admin"); err != nil {
289 t.Fatal(err)
290 }
291 tok, uid := mkUser("Bea")
292 tid, err := srv.db.createTier("Founder")
293 if err != nil {
294 t.Fatal(err)
295 }
296
297 post := func(path string, body map[string]any) int {
298 raw, _ := json.Marshal(body)
299 resp, err := http.Post(ts.URL+path, "application/json", strings.NewReader(string(raw)))
300 if err != nil {
301 t.Fatal(err)
302 }
303 resp.Body.Close()
304 return resp.StatusCode
305 }
306
307 // The €11 floor on lifetime pricing; a valid once price sticks.
308 if code := post(fmt.Sprintf("/api/tiers/%s/price", tid), map[string]any{
309 "token": admin, "cents": 500, "currency": "eur", "interval": "once"}); code != http.StatusBadRequest {
310 t.Errorf("€5 lifetime accepted: %d", code)
311 }
312 if code := post(fmt.Sprintf("/api/tiers/%s/price", tid), map[string]any{
313 "token": admin, "cents": 11000, "currency": "eur", "interval": "once", "provider_ref": "price_once"}); code != http.StatusOK {
314 t.Errorf("€110 lifetime rejected: %d", code)
315 }
316
317 // A lifetime holder gets honest answers from the renewal-shaped verbs.
318 now := time.Now().Unix()
319 if err := srv.applyWebhookEvent(context.Background(), lifetimeGrant(uid, tid, now), now); err != nil {
320 t.Fatal(err)
321 }
322 for _, path := range []string{"cancel-subscription", "resume", "portal"} {
323 if code := post(fmt.Sprintf("/api/tiers/%s/%s", tid, path), map[string]any{"token": tok}); code != http.StatusBadRequest {
324 t.Errorf("%s on a lifetime purchase = %d, want 400", path, code)
325 }
326 }
327}
328
329// TestOnceFeeMatchesPercent pins feePermille to applicationFeePercent β€” the
330// string feeds Stripe's subscription form, the integer feeds payment-mode
331// money math, and they must describe the same rate.
332func TestOnceFeeMatchesPercent(t *testing.T) {
333 pct, err := strconv.ParseFloat(applicationFeePercent, 64)
334 if err != nil {
335 t.Fatalf("applicationFeePercent %q: %v", applicationFeePercent, err)
336 }
337 if got := float64(feePermille) / 10; got != pct {
338 t.Errorf("feePermille %d = %.2f%%, applicationFeePercent = %s", feePermille, got, applicationFeePercent)
339 }
340 // Floor rounding favours the admin: 5.5% of 999 is 54.945 β†’ 54.
341 if got := onceFeeMinor(999); got != 54 {
342 t.Errorf("onceFeeMinor(999) = %d, want 54", got)
343 }
344}
345