From 7839842e98de87324722cae57b5870966e381e0a Mon Sep 17 00:00:00 2001 From: EOEboh Date: Mon, 3 Aug 2026 15:54:39 +0100 Subject: [PATCH 1/2] fix(ui): calm the plan cards and drop em dashes from billing copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plan cards carried a large coloured glow, shadow-[0_0_50px_-18px_rgba(20,184,166,0.55)], which read as decoration rather than hierarchy. It was also the wrong colour: a teal glow under an indigo gradient border, so the two never resolved into one accent. The gradient border and the "Most popular" badge already distinguish the Pro card, so the glow is replaced with plain neutral elevation (shadow-md shadow-black/20), matching how the rest of the app handles raised surfaces. The border gradients are toned down slightly to sit with it. Also removes em dashes from billing copy, which had crept into buttons and the renewal notice. Rewritten as ordinary sentences: "Subscribe — ₦3,500/mo" -> "Subscribe for ₦3,500/mo" "Upgrade to Pro — $7/mo" -> "Upgrade to Pro for $7/mo" "Cancellation scheduled — ..." -> "Cancellation scheduled. Access continues until ..." The non-renewal notice was one long sentence hinged on a dash; it is now three plain ones. Em dashes elsewhere in the app (terms, privacy, tour copy) are untouched. --- .../billing/ManageSubscriptionPanel.tsx | 7 +++--- ui/src/components/billing/PricingPage.tsx | 22 ++++++++++--------- ui/src/components/billing/UpgradePrompt.tsx | 2 +- ui/src/context/BillingContext.tsx | 2 +- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/ui/src/components/billing/ManageSubscriptionPanel.tsx b/ui/src/components/billing/ManageSubscriptionPanel.tsx index d84c0d5..abd3c1b 100644 --- a/ui/src/components/billing/ManageSubscriptionPanel.tsx +++ b/ui/src/components/billing/ManageSubscriptionPanel.tsx @@ -111,16 +111,15 @@ export function ManageSubscriptionPanel({ {subscription?.cancel_at_period_end && (

- Cancellation scheduled — access until {renewalDate} + Cancellation scheduled. Access continues until {renewalDate}

)} {willNotRenew && (

- This is a one-off payment, so there is nothing to cancel — access - simply ends on {renewalDate}. Renew from the billing page to extend - it. + This is a one-off payment, so there is nothing to cancel. Access + ends on {renewalDate}. Renew from the billing page to extend it.

)} diff --git a/ui/src/components/billing/PricingPage.tsx b/ui/src/components/billing/PricingPage.tsx index 2568e15..2d002de 100644 --- a/ui/src/components/billing/PricingPage.tsx +++ b/ui/src/components/billing/PricingPage.tsx @@ -105,8 +105,8 @@ function PaystackButton({ {loading ? 'Processing…' : label - ? `${label} — ${display.amount}${display.suffix}` - : `Subscribe — ${display.amount}${display.suffix}` + ? `${label} for ${display.amount}${display.suffix}` + : `Subscribe for ${display.amount}${display.suffix}` } ) @@ -206,8 +206,9 @@ export function PricingPage() { if (isPro) { return (
- {/* Gradient-border shell — 1px indigo blend + soft glow, not a heavy card */} -
+ {/* 1px gradient border carries the emphasis; the shadow is neutral + elevation, not a coloured glow. */} +
@@ -242,10 +243,10 @@ export function PricingPage() { {willNotRenew && (

- This payment covered a single period and will not renew - automatically — the method you paid with cannot be charged - again. Renew below to extend your access; the new period is - added to the time you have left. + This payment covered a single period. The method you paid + with cannot be charged again, so it will not renew + automatically. Renewing adds a new period to whatever time + you have left.

- {/* Pro card — gradient border + soft glow gives it real visual weight */} -
+ {/* Pro card: the gradient border and the badge distinguish it. A + coloured glow read as decoration rather than hierarchy. */} +
Most popular diff --git a/ui/src/components/billing/UpgradePrompt.tsx b/ui/src/components/billing/UpgradePrompt.tsx index 66b39c6..c45f146 100644 --- a/ui/src/components/billing/UpgradePrompt.tsx +++ b/ui/src/components/billing/UpgradePrompt.tsx @@ -39,7 +39,7 @@ export function UpgradePrompt({ feature, description }: Props) { disabled={loading} className="w-full py-2 rounded-lg bg-indigo-500 hover:bg-indigo-400 active:scale-[0.98] disabled:opacity-50 text-white text-xs font-medium transition-all duration-200 ease-(--ease-considered)" > - {loading ? 'Redirecting…' : `Upgrade to Pro — ${price}`} + {loading ? 'Redirecting…' : `Upgrade to Pro for ${price}`}

14-day free trial · Cancel anytime

diff --git a/ui/src/context/BillingContext.tsx b/ui/src/context/BillingContext.tsx index 2a25deb..054b504 100644 --- a/ui/src/context/BillingContext.tsx +++ b/ui/src/context/BillingContext.tsx @@ -185,7 +185,7 @@ function getPaystackConfig( amount, plan: planCode ?? '', currency: 'NGN', - label: `hookdrop Pro — ${isAnnual ? 'Annual' : 'Monthly'}`, + label: `hookdrop Pro ${isAnnual ? 'Annual' : 'Monthly'}`, // Carried onto the transaction so subscription webhooks can identify the // local user. Renewals will not have it — those resolve by customer code. metadata: { From 17b4fe75894acde66b3617e2e57c61517693a04e Mon Sep 17 00:00:00 2001 From: EOEboh Date: Mon, 3 Aug 2026 16:54:56 +0100 Subject: [PATCH 2/2] style: remove em dashes from comments, copy and log output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sweeps the remaining em dashes after the billing copy was cleaned up in the previous change. 222 replacements across 68 files, covering comments, log lines, test failure messages, CLI docs and the last few user-facing strings. Mostly rewritten as two sentences, since in almost every case the dash was introducing an independent clause. Where it introduced a dependent one ("so", "which", "over") a comma is used instead, and where it introduced a quotation a colon reads better. The mechanical pass needed three corrections afterwards, all found by reading the diff rather than trusting it: - it capitalised identifiers at what it took to be a sentence start, so comments named charge.success, order_created and subscription_updated with the wrong casing - a quoted line from Paystack's docs ended up opening a sentence in lowercase - dependent clauses became sentence fragments One em dash is deliberately kept: SettingsTokensPage uses '—' as the placeholder for an absent timestamp, which is typography rather than prose. Go build, vet and tests pass; the CLI module and the UI build pass. --- cli/.goreleaser.yaml | 2 +- cli/README.md | 8 +-- cli/cmd/listen.go | 10 ++-- cli/cmd/login.go | 6 +-- cli/cmd/login_test.go | 4 +- cli/cmd/root.go | 2 +- cli/internal/api/types.go | 2 +- cli/internal/config/config.go | 8 +-- cli/internal/forward/forwarder.go | 4 +- cli/internal/forward/forwarder_test.go | 2 +- cli/internal/output/printer.go | 4 +- cli/internal/sseclient/client.go | 8 +-- internal/billing/entitlements.go | 4 +- internal/billing/lemonsqueezy.go | 18 +++---- internal/billing/lemonsqueezy_test.go | 12 ++--- internal/billing/paystack.go | 16 +++--- internal/billing/paystack_test.go | 14 +++--- internal/billing/provider.go | 2 +- internal/email/resend.go | 2 +- internal/handler/auth.go | 4 +- internal/handler/billing.go | 48 +++++++++--------- internal/handler/billing_test.go | 50 +++++++++---------- internal/handler/health.go | 4 +- internal/handler/me.go | 2 +- internal/handler/replay.go | 2 +- internal/handler/requests.go | 2 +- internal/handler/sse.go | 12 ++--- internal/handler/tokens.go | 2 +- internal/middleware/auth.go | 2 +- internal/middleware/cors.go | 6 +-- internal/models/request.go | 8 +-- internal/replay/engine.go | 6 +-- internal/session/session.go | 2 +- internal/sse/broadcaster.go | 4 +- internal/store/api_tokens.go | 4 +- internal/store/store.go | 20 ++++---- internal/store/store_test.go | 4 +- internal/verify/verify.go | 6 +-- main.go | 14 +++--- repair.go | 10 ++-- .../billing/ManageSubscriptionPanel.tsx | 4 +- ui/src/components/billing/PricingPage.tsx | 6 +-- ui/src/components/detail/BodyViewer.tsx | 6 +-- ui/src/components/detail/MetaBar.tsx | 4 +- ui/src/components/feed/EmptyFeed.tsx | 2 +- ui/src/components/feed/FilterBar.tsx | 4 +- ui/src/components/feed/RequestItem.tsx | 2 +- ui/src/components/layout/MainPanel.tsx | 2 +- ui/src/components/layout/MobileTabBar.tsx | 2 +- ui/src/components/layout/Sidebar.tsx | 2 +- ui/src/components/onboarding/TourOverlay.tsx | 4 +- ui/src/components/onboarding/TourProvider.tsx | 2 +- ui/src/components/onboarding/tourSteps.ts | 6 +-- ui/src/components/replay/ReplayResponse.tsx | 4 +- ui/src/components/session/SessionBar.tsx | 4 +- ui/src/components/ui/EmptyState.tsx | 2 +- ui/src/components/ui/Logo.tsx | 4 +- ui/src/components/ui/VerificationBadge.tsx | 2 +- ui/src/context/BillingContext.tsx | 10 ++-- ui/src/index.css | 6 +-- ui/src/lib/analytics.ts | 2 +- ui/src/lib/json.ts | 8 +-- ui/src/pages/AuthCallbackPage.tsx | 2 +- ui/src/pages/CliAuthPage.tsx | 4 +- ui/src/pages/PrivacyPage.tsx | 4 +- ui/src/pages/SettingsTokensPage.tsx | 2 +- ui/src/pages/TermsPage.tsx | 2 +- ui/src/types/index.ts | 2 +- 68 files changed, 222 insertions(+), 222 deletions(-) diff --git a/cli/.goreleaser.yaml b/cli/.goreleaser.yaml index 1652259..7ce8a3f 100644 --- a/cli/.goreleaser.yaml +++ b/cli/.goreleaser.yaml @@ -1,5 +1,5 @@ # goreleaser config for the hookdrop CLI. -# Released locally via scripts/release.sh (see RELEASING.md) — no CI. +# Released locally via scripts/release.sh (see RELEASING.md). No CI. # The backend is deployed separately by scripts/deploy.sh and never tagged. version: 2 diff --git a/cli/README.md b/cli/README.md index 85dd1f4..add4773 100644 --- a/cli/README.md +++ b/cli/README.md @@ -14,7 +14,7 @@ hookdrop listen my-slug -f 3000 # stream webhooks + forward to localhost:30 ## Install -### curl (recommended — one line, no extra steps) +### curl (recommended, one line, no extra steps) ```sh curl -fsSL https://raw.githubusercontent.com/EOEboh/hookdrop/main/scripts/install.sh | sh @@ -41,7 +41,7 @@ fails with "Refusing to load formula … from untrusted tap".) Download the archive from [GitHub Releases](https://github.com/EOEboh/hookdrop/releases). (Note: replacing `hookdrop.exe` while a `listen` session is running will fail -on Windows — stop it first. On macOS/Linux, upgrading while running is fine; +on Windows, so stop it first. On macOS/Linux, upgrading while running is fine; the active session keeps the old binary until it exits.) ## Use @@ -69,8 +69,8 @@ Shell completions are available via `hookdrop completion bash|zsh|fish|powershel Forwarded requests carry `X-Hookdrop-Forwarded: true` and `X-Hookdrop-Original-Id` headers; hop-by-hop headers (`Host`, -`Content-Length`, …) are regenerated, everything else — including provider -signature headers — is preserved byte-for-byte. +`Content-Length`, …) are regenerated, everything else, including provider +signature headers, is preserved byte-for-byte. ## Config diff --git a/cli/cmd/listen.go b/cli/cmd/listen.go index ae1b5ea..63d8086 100644 --- a/cli/cmd/listen.go +++ b/cli/cmd/listen.go @@ -46,7 +46,7 @@ var listenCmd = &cobra.Command{ Short: "Stream webhooks live into your terminal (and forward them locally)", Long: `Streams every webhook hitting your hookdrop endpoint into the terminal, one line per event. With -f/--forward, each webhook is also re-sent to a local -server — the way the web UI's replay works — so your dev server receives traffic +server. The way the web UI's replay works, so your dev server receives traffic the hosted backend can't deliver directly. The endpoint is optional: with one named endpoint it's picked automatically, @@ -169,7 +169,7 @@ func resolveEndpoint(ctx context.Context, client *api.Client, explicit, frontend for _, e := range endpoints { slugs = append(slugs, e.Slug) } - return "", fmt.Errorf("multiple endpoints (%s) — specify one: hookdrop listen ", strings.Join(slugs, ", ")) + return "", fmt.Errorf("multiple endpoints (%s). Specify one: hookdrop listen ", strings.Join(slugs, ", ")) } return pickEndpoint(endpoints) } @@ -229,7 +229,7 @@ func runListen(ctx context.Context, apiURL, token, endpoint, forwardURL string) } printer.Line(printer.Event(&req, forwarding)) if forwarding && !fwd.Enqueue(&req) { - printer.Line(printer.Status("⚠ forward queue full — dropped " + output.ShortID(req.ID) + " (still visible in the dashboard)")) + printer.Line(printer.Status("⚠ forward queue full. Dropped " + output.ShortID(req.ID) + " (still visible in the dashboard)")) } } } @@ -251,7 +251,7 @@ func runListen(ctx context.Context, apiURL, token, endpoint, forwardURL string) printer.Line(printer.Status("stopped")) return nil case errors.Is(err, api.ErrUnauthorized): - return errors.New("your session is no longer valid — the token may have been revoked. Run 'hookdrop login' again") + return errors.New("your session is no longer valid. The token may have been revoked. Run 'hookdrop login' again") case errors.Is(err, api.ErrNotFound): return fmt.Errorf("endpoint %q not found on your account (it may have expired if it was a temporary session). Run 'hookdrop endpoints' to see yours", endpoint) case errors.Is(err, api.ErrPaymentRequired): @@ -264,7 +264,7 @@ func runListen(ctx context.Context, apiURL, token, endpoint, forwardURL string) } attempt++ jittered := delay + time.Duration(rand.Int63n(int64(delay/2+1))) - printer.Line(printer.Status(fmt.Sprintf("⟳ connection lost (%v) — reconnecting in %s (attempt %d)", err, jittered.Round(time.Second), attempt))) + printer.Line(printer.Status(fmt.Sprintf("⟳ connection lost (%v). Reconnecting in %s (attempt %d)", err, jittered.Round(time.Second), attempt))) select { case <-time.After(jittered): diff --git a/cli/cmd/login.go b/cli/cmd/login.go index a86e11b..8992da6 100644 --- a/cli/cmd/login.go +++ b/cli/cmd/login.go @@ -46,7 +46,7 @@ or --no-browser on headless machines.`, if token == "" && !loginNoBrowser { token, err = browserLogin(cmd.Context(), cfg.FrontendURL) if err != nil { - fmt.Fprintf(os.Stderr, "Browser login didn't complete (%v) — falling back to manual entry.\n", err) + fmt.Fprintf(os.Stderr, "Browser login didn't complete (%v). Falling back to manual entry.\n", err) } } if token == "" { @@ -110,7 +110,7 @@ func browserLogin(ctx context.Context, frontendURL string) (string, error) { fmt.Printf("Opening your browser to authorize the CLI…\n %s\n", authURL) if err := openBrowser(authURL); err != nil { - fmt.Fprintln(os.Stderr, "Couldn't open a browser automatically — open the URL above manually.") + fmt.Fprintln(os.Stderr, "Couldn't open a browser automatically. Open the URL above manually.") } fmt.Println("Waiting for authorization…") @@ -135,7 +135,7 @@ func waitForCallback(ctx context.Context, listener net.Listener, state string, t } w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprint(w, ` -

✓ You're logged in

Return to your terminal — you can close this tab.

+

✓ You're logged in

Return to your terminal. You can close this tab.

`) // Flush before signaling: the wait returns and closes the server as // soon as it has the token, which would race the buffered response. diff --git a/cli/cmd/login_test.go b/cli/cmd/login_test.go index 44b60d7..bffe3d0 100644 --- a/cli/cmd/login_test.go +++ b/cli/cmd/login_test.go @@ -129,12 +129,12 @@ func TestWaitForCallbackTimesOut(t *testing.T) { t.Fatal("expected timeout error") } if elapsed := time.Since(start); elapsed < 150*time.Millisecond { - t.Fatalf("returned too early (%v) — timeout not honored", elapsed) + t.Fatalf("returned too early (%v). Timeout not honored", elapsed) } } // Two listeners bound back-to-back get distinct OS-assigned ports and each -// delivers independently — demonstrating the port-0 design has no +// delivers independently. Demonstrating the port-0 design has no // fixed-port collision even with concurrent logins. func TestConcurrentCallbacksDistinctPorts(t *testing.T) { l1, l2 := listen(t), listen(t) diff --git a/cli/cmd/root.go b/cli/cmd/root.go index c5d3867..5af38a2 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -14,7 +14,7 @@ var rootCmd = &cobra.Command{ Use: "hookdrop", Short: "Stream and forward your hookdrop webhooks from the terminal", Long: `hookdrop streams webhooks captured at hookdrop.app into your terminal and -forwards each one to a local server the hosted backend can't reach directly — +forwards each one to a local server the hosted backend can't reach directly. a local webhook forwarder for developing against real webhook traffic.`, Example: ` hookdrop login # authenticate (opens your browser) hookdrop listen my-slug -f 3000 # stream + forward to localhost:3000 diff --git a/cli/internal/api/types.go b/cli/internal/api/types.go index 8de891e..7c48a6c 100644 --- a/cli/internal/api/types.go +++ b/cli/internal/api/types.go @@ -3,7 +3,7 @@ package api import "time" // Wire types mirroring the backend's JSON responses. Source of truth: -// internal/models/request.go in the backend module — keep field names and +// internal/models/request.go in the backend module. Keep field names and // JSON tags in sync (internal/ packages can't be imported across modules). // CapturedRequest is one webhook as delivered over SSE and /requests. diff --git a/cli/internal/config/config.go b/cli/internal/config/config.go index e4741e3..8c4c139 100644 --- a/cli/internal/config/config.go +++ b/cli/internal/config/config.go @@ -1,5 +1,5 @@ // Package config manages the CLI's local configuration file, which holds -// the API token — permissions are locked down and writes are atomic. +// the API token. Permissions are locked down and writes are atomic. package config import ( @@ -50,12 +50,12 @@ func Load() (*Config, error) { data, err := os.ReadFile(path) switch { case errors.Is(err, os.ErrNotExist): - // fresh install — fall through to defaults + // fresh install. Fall through to defaults case err != nil: return nil, fmt.Errorf("read config %s: %w", path, err) default: if jsonErr := json.Unmarshal(data, cfg); jsonErr != nil { - return nil, fmt.Errorf("config file %s is corrupt (%v) — run 'hookdrop login' to recreate it", path, jsonErr) + return nil, fmt.Errorf("config file %s is corrupt (%v). Run 'hookdrop login' to recreate it", path, jsonErr) } warnLoosePermissions(path) } @@ -123,7 +123,7 @@ func warnLoosePermissions(path string) { } if info.Mode().Perm()&0o077 != 0 { fmt.Fprintf(os.Stderr, - "warning: %s is readable by other users (mode %o) — consider: chmod 600 %s\n", + "warning: %s is readable by other users (mode %o). Consider: chmod 600 %s\n", path, info.Mode().Perm(), path) } } diff --git a/cli/internal/forward/forwarder.go b/cli/internal/forward/forwarder.go index 12d889a..0b70f1d 100644 --- a/cli/internal/forward/forwarder.go +++ b/cli/internal/forward/forwarder.go @@ -69,7 +69,7 @@ func (f *Forwarder) Start(ctx context.Context) { } // Enqueue adds a webhook to the delivery queue. Returns false when the -// queue is full — callers should surface the drop, never block the SSE +// queue is full. Callers should surface the drop, never block the SSE // reader on a slow local server. func (f *Forwarder) Enqueue(req *api.CapturedRequest) bool { select { @@ -115,7 +115,7 @@ func (f *Forwarder) deliver(ctx context.Context, original *api.CapturedRequest) // shouldSkipHeader filters headers that break or are meaningless when // forwarded. Parity port of shouldSkipHeader in the backend's -// internal/replay/engine.go — keep the two lists identical so behavior +// internal/replay/engine.go. Keep the two lists identical so behavior // matches whether a request is replayed from the web UI or forwarded live. func shouldSkipHeader(key string) bool { skip := map[string]bool{ diff --git a/cli/internal/forward/forwarder_test.go b/cli/internal/forward/forwarder_test.go index a2c5866..0e1c4d4 100644 --- a/cli/internal/forward/forwarder_test.go +++ b/cli/internal/forward/forwarder_test.go @@ -89,7 +89,7 @@ func TestDeliverUnreachableTarget(t *testing.T) { func TestEnqueueOverflowDropsInsteadOfBlocking(t *testing.T) { f := New("http://127.0.0.1:1", func(Result) {}) - // worker not started — queue just fills + // worker not started. Queue just fills for i := 0; i < queueSize; i++ { if !f.Enqueue(&api.CapturedRequest{}) { t.Fatalf("enqueue %d should succeed", i) diff --git a/cli/internal/output/printer.go b/cli/internal/output/printer.go index a302dd0..9f0df20 100644 --- a/cli/internal/output/printer.go +++ b/cli/internal/output/printer.go @@ -76,7 +76,7 @@ func (p *Printer) Event(req *api.CapturedRequest, showID bool) string { // ForwardResult renders the delivery outcome, indented under its event: // -// ↳ (4dbb48bd) 200 in 45ms +// ↳ (4dbb48bd) 200 in 45ms func (p *Printer) ForwardResult(res forward.Result) string { id := Colorize(p.Colors, Dim, "("+ShortID(res.Request.ID)+")") if res.Err != nil { @@ -105,7 +105,7 @@ func (p *Printer) Status(s string) string { func (p *Printer) Ready(inboxURL, forwardURL string) string { check := Colorize(p.Colors, Green, "✓") var b strings.Builder - fmt.Fprintf(&b, "%s Ready — listening on %s\n", check, Colorize(p.Colors, Bold, inboxURL)) + fmt.Fprintf(&b, "%s Ready. Listening on %s\n", check, Colorize(p.Colors, Bold, inboxURL)) if forwardURL != "" { fmt.Fprintf(&b, " → forwarding to %s\n", Colorize(p.Colors, Bold, forwardURL)) } diff --git a/cli/internal/sseclient/client.go b/cli/internal/sseclient/client.go index 53bde46..9cf0cd0 100644 --- a/cli/internal/sseclient/client.go +++ b/cli/internal/sseclient/client.go @@ -1,6 +1,6 @@ // Package sseclient is a minimal Server-Sent Events client for the hookdrop // /events stream. The format is three line types: "event:", "data:", and -// ":" comments (keepalives) — no library needed. +// ":" comments (keepalives). No library needed. package sseclient import ( @@ -41,7 +41,7 @@ func New(baseURL, token string) *Client { return &Client{ BaseURL: strings.TrimRight(baseURL, "/"), Token: token, - // No overall client timeout — the stream is long-lived. Individual + // No overall client timeout. The stream is long-lived. Individual // phases are bounded instead; mid-stream silence is the watchdog's job. http: &http.Client{ Transport: &http.Transport{ @@ -111,7 +111,7 @@ func (c *Client) Stream(ctx context.Context, identifier string, events chan<- Ev } } case line[0] == ':': - // comment/keepalive — watchdog already reset + // comment/keepalive. Watchdog already reset case bytes.HasPrefix(line, []byte("event:")): eventName = string(bytes.TrimSpace(line[len("event:"):])) case bytes.HasPrefix(line, []byte("data:")): @@ -123,7 +123,7 @@ func (c *Client) Stream(ctx context.Context, identifier string, events chan<- Ev } if watchdogFired.Load() { - return fmt.Errorf("no data for %s — connection stale", watchdogTimeout) + return fmt.Errorf("no data for %s. Connection stale", watchdogTimeout) } if ctx.Err() != nil { return ctx.Err() diff --git a/internal/billing/entitlements.go b/internal/billing/entitlements.go index 5dd8212..1dd302f 100644 --- a/internal/billing/entitlements.go +++ b/internal/billing/entitlements.go @@ -62,8 +62,8 @@ const PastDueGrace = 7 * 24 * time.Hour // IsActive reports whether a subscription is usable right now. // // The period is authoritative, not the status. Paystack does not retry a -// failed subscription charge — "when a payment attempt fails, it will not be -// attempted again" — so a lapsed subscription simply stops producing events +// failed subscription charge: "when a payment attempt fails, it will not be +// attempted again", so a lapsed subscription simply stops producing events // and sits at status "active" forever. Trusting the status alone handed those // customers Pro indefinitely. func IsActive(status string, periodEnd *time.Time) bool { diff --git a/internal/billing/lemonsqueezy.go b/internal/billing/lemonsqueezy.go index 7c8f2e1..f0aa2a9 100644 --- a/internal/billing/lemonsqueezy.go +++ b/internal/billing/lemonsqueezy.go @@ -116,7 +116,7 @@ func (p *LemonSqueezyProvider) CreateCheckout( // Lemonsqueezy uses JSON:API format. // - // store_id and variant_id are READ-ONLY response attributes — the only + // store_id and variant_id are READ-ONLY response attributes. The only // attributes accepted on create are custom_price, product_options, // checkout_options, checkout_data, preview, test_mode and expires_at. // The store and variant are addressed through relationships. @@ -142,7 +142,7 @@ func (p *LemonSqueezyProvider) CreateCheckout( }, // The free trial is configured on the variant in the - // Lemonsqueezy dashboard — there is no per-checkout trial + // Lemonsqueezy dashboard. There is no per-checkout trial // override. checkout_options.skip_trial would REMOVE it, so // it is deliberately not set here. "checkout_options": map[string]interface{}{ @@ -243,8 +243,8 @@ func (p *LemonSqueezyProvider) GetPortalURL( url := result.Data.Attributes.URLs.CustomerPortal if url == "" { - // Do not silently bounce the user back to the page they came from — - // that hides a real failure behind a no-op redirect. + // Do not silently bounce the user back to the page they came from. + // That hides a real failure behind a no-op redirect. return "", fmt.Errorf( "lemonsqueezy customer %s returned no customer_portal URL", customerID) } @@ -256,8 +256,8 @@ func (p *LemonSqueezyProvider) GetPortalURL( // Every other Lemonsqueezy event carries a DIFFERENT object shape: // order_created sends an Order (no top-level variant_id, status "paid"), // subscription_payment_* send a Subscription invoice. Parsing either as a -// subscription writes a garbage row — plan "free", status "paid" and an order -// ID in provider_sub_id — over a paying customer. +// subscription writes a garbage row. Plan "free", status "paid" and an order +// ID in provider_sub_id, over a paying customer. var lsSubscriptionEvents = map[string]bool{ "subscription_created": true, "subscription_updated": true, @@ -294,8 +294,8 @@ func (p *LemonSqueezyProvider) HandleWebhook( // Parse the event envelope. // // Note there is deliberately no first_subscription_item here: it is null - // while the subscription is on trial, and it carries no interval field — - // the interval is derived from variant_id instead. + // while the subscription is on trial, and it carries no interval field. + // The interval is derived from variant_id instead. var envelope struct { Meta struct { EventName string `json:"event_name"` @@ -329,7 +329,7 @@ func (p *LemonSqueezyProvider) HandleWebhook( } if !lsSubscriptionEvents[envelope.Meta.EventName] { - // Not a subscription event — nothing to persist. + // Not a subscription event, so there is nothing to persist. return nil, nil } diff --git a/internal/billing/lemonsqueezy_test.go b/internal/billing/lemonsqueezy_test.go index ed4f850..9e7f155 100644 --- a/internal/billing/lemonsqueezy_test.go +++ b/internal/billing/lemonsqueezy_test.go @@ -35,7 +35,7 @@ func handle(t *testing.T, payload string) (*WebhookEvent, error) { } // subscriptionPayload mirrors the shape Lemonsqueezy actually sends for -// subscription events. Note first_subscription_item is null — that is what a +// subscription events. Note first_subscription_item is null. That is what a // subscription on a free trial looks like. const trialCreatedPayload = `{ "meta": { @@ -100,7 +100,7 @@ func TestHandleWebhook_TrialSubscriptionCreated(t *testing.T) { if ev.Status != "trialing" { t.Errorf("Status = %q, want trialing", ev.Status) } - // The customer ID must be the LS customer, not the order or subscription — + // The customer ID must be the LS customer, not the order or subscription. // GetPortalURL calls GET /customers/{id} with it. if ev.CustomerID != "4210987" { t.Errorf("CustomerID = %q, want 4210987 (customer_id, not order_id)", ev.CustomerID) @@ -112,7 +112,7 @@ func TestHandleWebhook_TrialSubscriptionCreated(t *testing.T) { t.Errorf("Interval = %q, want month", ev.Interval) } if ev.TrialEnd == 0 { - t.Error("TrialEnd = 0, want the parsed trial_ends_at — a nil trial_end renders 'Trial ends soon' forever") + t.Error("TrialEnd = 0, want the parsed trial_ends_at. A nil trial_end renders 'Trial ends soon' forever") } if want := time.Date(2026, 8, 10, 12, 0, 0, 0, time.UTC).Unix(); ev.TrialEnd != want { t.Errorf("TrialEnd = %d, want %d", ev.TrialEnd, want) @@ -172,7 +172,7 @@ func TestHandleWebhook_IgnoresNonSubscriptionEvents(t *testing.T) { t.Fatalf("unexpected error: %v", err) } if ev != nil { - t.Fatalf("expected nil event, got %+v — this would corrupt the subscriptions row", ev) + t.Fatalf("expected nil event, got %+v. This would corrupt the subscriptions row", ev) } }) } @@ -193,7 +193,7 @@ func TestHandleWebhook_CancelKeepsAccessUntilEndsAt(t *testing.T) { t.Fatalf("unexpected error: %v", err) } if ev.Type == "subscription.canceled" { - t.Error("subscription_cancelled mapped to subscription.canceled — that drops the customer to free during a grace period they paid for") + t.Error("subscription_cancelled mapped to subscription.canceled. That drops the customer to free during a grace period they paid for") } if ev.Type != "subscription.updated" { t.Errorf("Type = %q, want subscription.updated", ev.Type) @@ -229,7 +229,7 @@ func TestHandleWebhook_ExpiredDowngrades(t *testing.T) { t.Errorf("Type = %q, want subscription.canceled", ev.Type) } if ev.Status != "canceled" { - t.Errorf("Status = %q, want canceled — an expired subscription reported as active keeps Pro forever", ev.Status) + t.Errorf("Status = %q, want canceled. An expired subscription reported as active keeps Pro forever", ev.Status) } } diff --git a/internal/billing/paystack.go b/internal/billing/paystack.go index fa43239..c0e94c3 100644 --- a/internal/billing/paystack.go +++ b/internal/billing/paystack.go @@ -353,7 +353,7 @@ func (p *PaystackProvider) HandleWebhook(payload []byte, signature string) (*Web }, nil case "charge.success": - // Subscription renewals arrive as charge.success — Paystack sends no + // Subscription renewals arrive as charge.success. Paystack sends no // subscription.* event when a recurring payment goes through. Without // handling it, current_period_end is written once at signup and then // silently goes stale. @@ -397,7 +397,7 @@ func (p *PaystackProvider) HandleWebhook(payload []byte, signature string) (*Web Type: "subscription.updated", CustomerID: charge.Customer.CustomerCode, // charge.success names no subscription, so this cannot set - // provider_sub_id — the user resolves by customer code. + // provider_sub_id. The user resolves by customer code. Plan: "pro", Status: "active", Currency: "ngn", @@ -414,7 +414,7 @@ func (p *PaystackProvider) HandleWebhook(payload []byte, signature string) (*Web // only notice we get that the subscription has stopped paying. // // This exists to make the downgrade prompt. It is NOT what guarantees - // correctness — IsActive expires access once the period lapses, which + // correctness. IsActive expires access once the period lapses, which // holds whether or not this event ever arrives or is shaped as // expected. Parsed defensively for that reason: only fields present in // every Paystack payload observed so far are relied on. @@ -455,7 +455,7 @@ func (p *PaystackProvider) HandleWebhook(payload []byte, signature string) (*Web Type: "subscription.updated", CustomerID: inv.Customer.CustomerCode, // The subscription code is nested and its shape is unconfirmed, so - // it is deliberately not read — processWebhookEvent preserves the + // it is deliberately not read. ProcessWebhookEvent preserves the // stored one and the user resolves by customer code. Plan: "pro", Status: "past_due", @@ -556,7 +556,7 @@ func (p *PaystackProvider) planFromCode(code string) string { // IntervalForPlanCode reports the billing interval for one of our configured // plan codes. The second return is false for any code that is not ours. // -// This is the authority on interval — the client-supplied value is not +// This is the authority on interval. The client-supplied value is not // trusted, since it drives current_period_end. func (p *PaystackProvider) IntervalForPlanCode(code string) (string, bool) { switch { @@ -588,7 +588,7 @@ type PaystackTransaction struct { // Channel is how it was paid: card, bank_transfer, ussd and so on. Channel string // Reusable reports whether the authorization can be charged again. - // Paystack subscriptions need one that can — bank transfer produces + // Paystack subscriptions need one that can. Bank transfer produces // reusable=false, so the plan is charged once and no subscription is // created at all. Reusable bool @@ -609,7 +609,7 @@ type PaystackPlanInfo struct { // paystackVerifyResponse mirrors GET /transaction/verify/{reference}. // // Plan is json.RawMessage because Paystack returns it as a plan code string on -// some transactions and as a full plan object on others — decoding it into a +// some transactions and as a full plan object on others. Decoding it into a // fixed shape aborts the whole parse mid-way, which is what originally made // legitimate payments fail verification. type paystackVerifyResponse struct { @@ -765,7 +765,7 @@ func (p *PaystackProvider) VerifyTransaction( // object, which is the only route from a customer *code* to a subscription. // // GET /subscription?customer= expects the numeric customer id, not the code, -// and silently returns zero rows when given a code — which is what made the +// and silently returns zero rows when given a code, which is what made the // old cancel path fail without complaining. type PaystackCustomerSubscription struct { SubscriptionCode string diff --git a/internal/billing/paystack_test.go b/internal/billing/paystack_test.go index 81d7757..d07350b 100644 --- a/internal/billing/paystack_test.go +++ b/internal/billing/paystack_test.go @@ -178,7 +178,7 @@ func TestVerifyTransaction_Success(t *testing.T) { t.Fatalf("unexpected error: %v", err) } if *calls != 1 { - t.Errorf("made %d requests, want 1 — a success must not retry", *calls) + t.Errorf("made %d requests, want 1. A success must not retry", *calls) } if tx.Amount != 350000 || tx.Currency != "NGN" { t.Errorf("amount/currency = %d/%s", tx.Amount, tx.Currency) @@ -206,7 +206,7 @@ func TestVerifyTransaction_FailuresAreErrors(t *testing.T) { p, calls := paystackStub(t, body, 200) tx, err := p.VerifyTransaction(context.Background(), "T_bad") if err == nil { - t.Fatalf("expected an error, got tx=%+v — this is the free-Pro escalation", tx) + t.Fatalf("expected an error, got tx=%+v. This is the free-Pro escalation", tx) } if *calls != 3 { t.Errorf("made %d attempts, want 3 (retries preserved)", *calls) @@ -290,7 +290,7 @@ func TestHandleWebhook_ReadsUserIDFromTransactionMetadata(t *testing.T) { } } -// metadata:0 must not abort the parse — the event still has to resolve by +// metadata:0 must not abort the parse. The event still has to resolve by // customer code downstream. func TestHandleWebhook_SurvivesIntegerMetadata(t *testing.T) { p := NewPaystackProvider("sk", testSecret512, PaystackPlans{ProMonthly: "PLN_monthly"}) @@ -308,7 +308,7 @@ func TestHandleWebhook_SurvivesIntegerMetadata(t *testing.T) { t.Errorf("UserID = %q, want empty", ev.UserID) } if ev.CustomerID != "CUS_1" { - t.Errorf("CustomerID = %q, want CUS_1 — the fallback key", ev.CustomerID) + t.Errorf("CustomerID = %q, want CUS_1. The fallback key", ev.CustomerID) } } @@ -347,7 +347,7 @@ func TestHandleWebhook_NotRenewFlagsCancelAtPeriodEnd(t *testing.T) { // realSubscriptionCreatePayload is a verbatim capture of what Paystack // actually sent on subscription.create (test mode, 2026-07-28), trimmed of // identifying values. Note data.plan is an OBJECT here, though it is a bare -// code string on transaction payloads — decoding it as a string failed the +// code string on transaction payloads. Decoding it as a string failed the // whole parse and dropped the event with a 400. const realSubscriptionCreatePayload = `{ "event": "subscription.create", @@ -406,7 +406,7 @@ func TestHandleWebhook_RealSubscriptionCreatePayload(t *testing.T) { // The object-shaped plan must still resolve to our plan. if ev.Plan != "pro" { - t.Errorf("Plan = %q, want pro — the object-shaped plan was not recognised", ev.Plan) + t.Errorf("Plan = %q, want pro. The object-shaped plan was not recognised", ev.Plan) } if ev.SubscriptionID != "SUB_xhhcq6g7fl194tn" { t.Errorf("SubscriptionID = %q, want the SUB_ code", ev.SubscriptionID) @@ -414,7 +414,7 @@ func TestHandleWebhook_RealSubscriptionCreatePayload(t *testing.T) { // metadata is null on subscription events, so the customer code is the // only key that can resolve the user. if ev.UserID != "" { - t.Errorf("UserID = %q, want empty — Paystack sends metadata:null here", ev.UserID) + t.Errorf("UserID = %q, want empty. Paystack sends metadata:null here", ev.UserID) } if ev.CustomerID != "CUS_mpzcgx3mniosw1j" { t.Errorf("CustomerID = %q, want the customer code", ev.CustomerID) diff --git a/internal/billing/provider.go b/internal/billing/provider.go index 5ed8822..08a5fa7 100644 --- a/internal/billing/provider.go +++ b/internal/billing/provider.go @@ -56,7 +56,7 @@ type WebhookEvent struct { // // Currency selection happens on the client (detectCurrency in // ui/src/context/BillingContext.tsx), which keeps its own timezone list -// derived from this map — keep the two in step. +// derived from this map. Keep the two in step. var PaystackCountries = map[string]bool{ "NG": true, "GH": true, "ZA": true, "KE": true, "CI": true, "RW": true, "TZ": true, "EG": true, diff --git a/internal/email/resend.go b/internal/email/resend.go index 229c775..529277d 100644 --- a/internal/email/resend.go +++ b/internal/email/resend.go @@ -69,7 +69,7 @@ func (s *Sender) SendMagicLink(toEmail, magicURL string) error { if resp.StatusCode >= 400 { // Include the body and the recipient. Resend explains its rejections - // there — reporting the bare status code made a recurring 422 + // there. Reporting the bare status code made a recurring 422 // impossible to diagnose. body, _ := io.ReadAll(io.LimitReader(resp.Body, 4<<10)) return fmt.Errorf("resend API error %d sending to %s: %s", diff --git a/internal/handler/auth.go b/internal/handler/auth.go index e2aa7e0..332ba19 100644 --- a/internal/handler/auth.go +++ b/internal/handler/auth.go @@ -22,7 +22,7 @@ type AuthHandler struct { EmailLimiter *middleware.EmailRateLimiter } -// POST /auth/request — send a magic link +// POST /auth/request. Send a magic link func (h *AuthHandler) RequestLink(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) @@ -82,7 +82,7 @@ func (h *AuthHandler) RequestLink(w http.ResponseWriter, r *http.Request) { }) } -// GET /auth/verify?token=xxx — verify token, return JWT +// GET /auth/verify?token=xxx. Verify token, return JWT func (h *AuthHandler) VerifyLink(w http.ResponseWriter, r *http.Request) { token := r.URL.Query().Get("token") if token == "" { diff --git a/internal/handler/billing.go b/internal/handler/billing.go index 479dbe8..a981ad2 100644 --- a/internal/handler/billing.go +++ b/internal/handler/billing.go @@ -185,7 +185,7 @@ func (h *BillingHandler) handleWebhook( route webhookRoute, ) { // MaxBytesReader errors on an oversized body rather than silently - // truncating it — a truncated payload would fail signature verification + // truncating it. A truncated payload would fail signature verification // and be reported as a forgery. payload, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20)) if err != nil { @@ -222,7 +222,7 @@ func (h *BillingHandler) handleWebhook( if event != nil { record.UserID = event.UserID // Scope the ordering check by subscription where there is one, else by - // customer — charge.success identifies only the customer. + // customer, charge.success identifies only the customer. record.ObjectID = event.SubscriptionID if record.ObjectID == "" { record.ObjectID = event.CustomerID @@ -237,7 +237,7 @@ func (h *BillingHandler) handleWebhook( case errors.Is(err, store.ErrDuplicateBillingEvent): // The provider retried something we already have. 200 stops the // retries; re-processing would be wasted work at best. - log.Printf("%s webhook: duplicate delivery %s (%s) — already recorded", + log.Printf("%s webhook: duplicate delivery %s (%s). Already recorded", route.provider, eventType, eventKey[:12]) writeWebhookOK(w) return @@ -275,7 +275,7 @@ func (h *BillingHandler) handleWebhook( return } } else { - log.Printf("%s webhook: %s carries no timestamp — no ordering protection", + log.Printf("%s webhook: %s carries no timestamp. No ordering protection", route.provider, eventType) } @@ -333,7 +333,7 @@ func (h *BillingHandler) processWebhookEvent( // custom_data.user_id should always be present on subscription events, but // if it ever isn't, recover the user from the subscription ID we already - // stored rather than dropping the event on the floor — a dropped + // stored rather than dropping the event on the floor. A dropped // cancellation leaves a customer on Pro forever. if userID == "" { existing, via, err := h.resolveWebhookUser(event) @@ -346,7 +346,7 @@ func (h *BillingHandler) processWebhookEvent( providerName, event.Type, event.SubscriptionID, event.CustomerID) } userID = existing.UserID - log.Printf("WARNING: %s webhook %s carried no user_id — resolved user=%s via %s", + log.Printf("WARNING: %s webhook %s carried no user_id. Resolved user=%s via %s", providerName, event.Type, userID, via) } @@ -355,7 +355,7 @@ func (h *BillingHandler) processWebhookEvent( t := time.Unix(event.PeriodEnd, 0) periodEnd = &t } else if existing, err := h.Store.GetSubscription(userID); err == nil { - // Not every event moves the period — a failed renewal reports no new + // Not every event moves the period. A failed renewal reports no new // date. The upsert writes every column, so passing nil would blank // current_period_end, and a nil period grants access indefinitely. // That would turn the expiry gate off for exactly the subscriptions it @@ -487,7 +487,7 @@ func (h *BillingHandler) VerifyPaystack(w http.ResponseWriter, r *http.Request) return } if body.Interval != "" && body.Interval != interval { - log.Printf("VerifyPaystack: client claimed interval=%q, plan %s says %q — using %q", + log.Printf("VerifyPaystack: client claimed interval=%q, plan %s says %q. Using %q", body.Interval, tx.Plan.Code, interval, interval) } @@ -498,8 +498,8 @@ func (h *BillingHandler) VerifyPaystack(w http.ResponseWriter, r *http.Request) switch { case !tx.Plan.AmountKnown: // Paystack returned a bare plan code with no plan object. The plan - // code check above already carries the weight here — attaching our - // plan code makes Paystack charge that plan's price — so accept, but + // code check above already carries the weight here. Attaching our + // plan code makes Paystack charge that plan's price, so accept, but // say so. log.Printf("WARNING: VerifyPaystack: plan %s amount unknown, cannot cross-check charge of %d (user=%s ref=%s)", tx.Plan.Code, tx.Amount, user.ID, body.Reference) @@ -536,14 +536,14 @@ func (h *BillingHandler) VerifyPaystack(w http.ResponseWriter, r *http.Request) http.Error(w, "this payment has already been redeemed", http.StatusConflict) return } - // Same user re-verifying must not fail — a double submit or a retried - // handlePaystackSuccess should not lock a paying customer out. But it must + // Same user re-verifying must not fail. A double submit or a retried + // handlePaystackSuccess should not lock a paying customer out, but it must // not be *applied* twice either: the period is extended from whatever is // left, so replaying a reference would hand out free time. // // Recording the verification claims the reference. The UNIQUE index on // event_key decides, which is stronger than comparing against - // provider_sub_id — that only remembers the most recent reference and + // provider_sub_id. That only remembers the most recent reference and // would let an older one be replayed after a renewal. claim := sha256.Sum256([]byte("paystack-verify:" + body.Reference)) claimKey := hex.EncodeToString(claim[:]) @@ -556,7 +556,7 @@ func (h *BillingHandler) VerifyPaystack(w http.ResponseWriter, r *http.Request) ObjectID: tx.CustomerCode, }); { case errors.Is(err, store.ErrDuplicateBillingEvent): - log.Printf("VerifyPaystack: reference %s already applied for user=%s — returning current state", + log.Printf("VerifyPaystack: reference %s already applied for user=%s. Returning current state", body.Reference, user.ID) current, err := h.Store.GetSubscription(user.ID) if err != nil { @@ -583,16 +583,16 @@ func (h *BillingHandler) VerifyPaystack(w http.ResponseWriter, r *http.Request) // Paystack subscriptions require an authorization that can be charged // again. A bank transfer cannot, so Paystack takes the plan amount once - // and creates no subscription — the customer has bought a single period. + // and creates no subscription. The customer has bought a single period. // They keep what they paid for; the UI has to stop calling it a // subscription. if !tx.Reusable { - log.Printf("VerifyPaystack: %s payment is not reusable — user=%s gets one period and will NOT auto-renew", + log.Printf("VerifyPaystack: %s payment is not reusable. User=%s gets one period and will NOT auto-renew", tx.Channel, user.ID) } // NGN pricing is substantially cheaper than USD, and the currency is - // chosen client-side — localStorage hookdrop_currency is enough to pick + // chosen client-side. LocalStorage hookdrop_currency is enough to pick // it. The card's country is the only server-side evidence of where the // payer really is. // @@ -601,7 +601,7 @@ func (h *BillingHandler) VerifyPaystack(w http.ResponseWriter, r *http.Request) // a worse failure than the mispricing. This makes the exposure visible so // it can be judged on evidence. if tx.CardCountry != "" && !billing.PaystackCountries[tx.CardCountry] { - log.Printf("WARNING: VerifyPaystack: NGN pricing paid with a %s card — user=%s ref=%s customer=%s payer_ip=%s (review: possible currency mispricing)", + log.Printf("WARNING: VerifyPaystack: NGN pricing paid with a %s card. User=%s ref=%s customer=%s payer_ip=%s (review: possible currency mispricing)", tx.CardCountry, user.ID, body.Reference, tx.CustomerCode, tx.PayerIP) } @@ -615,7 +615,7 @@ func (h *BillingHandler) VerifyPaystack(w http.ResponseWriter, r *http.Request) // Stack onto whatever is left rather than restarting from now. A // non-recurring subscription is renewed by paying again, and someone who // renews early should not silently lose the days they already had. - // An expired period is not carried forward — that would backdate the + // An expired period is not carried forward. That would backdate the // renewal to a date already passed. base := now if current, err := h.Store.GetSubscription(user.ID); err == nil && @@ -647,7 +647,7 @@ func (h *BillingHandler) VerifyPaystack(w http.ResponseWriter, r *http.Request) CreatedAt: now, } - log.Printf("VerifyPaystack: upserting — user=%s plan=pro status=%s customer=%s", + log.Printf("VerifyPaystack: upserting. User=%s plan=pro status=%s customer=%s", user.ID, subStatus, customerCode) if err := h.Store.UpsertSubscription(sub); err != nil { @@ -656,7 +656,7 @@ func (h *BillingHandler) VerifyPaystack(w http.ResponseWriter, r *http.Request) return } - log.Printf("VerifyPaystack: upsert SUCCESS — user=%s is now Pro (status=%s)", + log.Printf("VerifyPaystack: upsert SUCCESS. User=%s is now Pro (status=%s)", user.ID, subStatus) w.Header().Set("Content-Type", "application/json") @@ -717,7 +717,7 @@ func (h *BillingHandler) CancelSubscription(w http.ResponseWriter, r *http.Reque } if sub.CancelAtPeriodEnd { - // Already scheduled for cancellation — return success idempotently + // Already scheduled for cancellation. Return success idempotently w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "cancelled": true, @@ -747,11 +747,11 @@ func (h *BillingHandler) CancelSubscription(w http.ResponseWriter, r *http.Reque } else { // There is nothing callable. Honour the intent locally, but say // plainly that the provider was not told and is still billing. - log.Printf("WARNING: CancelSubscription: user=%s has no Paystack subscription code (provider_sub_id=%q) — cancelled locally only; Paystack was NOT notified and will keep billing", + log.Printf("WARNING: CancelSubscription: user=%s has no Paystack subscription code (provider_sub_id=%q). Cancelled locally only; Paystack was NOT notified and will keep billing", user.ID, sub.ProviderSubID) } } else { - // LemonSqueezy — use stored sub ID directly. + // LemonSqueezy. Use stored sub ID directly. // // This one is NOT best-effort: if the DELETE fails, Lemonsqueezy keeps // billing the customer. Marking them cancelled locally would show them diff --git a/internal/handler/billing_test.go b/internal/handler/billing_test.go index 4a86c01..6dc408a 100644 --- a/internal/handler/billing_test.go +++ b/internal/handler/billing_test.go @@ -158,12 +158,12 @@ func TestVerifyPaystack_RejectsUnverifiableTransaction(t *testing.T) { `{"reference":"T_fake","plan":"pro","interval":"month"}`)) if rec.Code == http.StatusOK { - t.Fatalf("got 200 — unverified transaction granted Pro") + t.Fatalf("got 200. Unverified transaction granted Pro") } sub, _ := h.Store.GetSubscription(user.ID) if sub.Plan != "free" { - t.Errorf("plan = %q, want free — nothing may be written on a failed verify", sub.Plan) + t.Errorf("plan = %q, want free. Nothing may be written on a failed verify", sub.Plan) } }) } @@ -197,7 +197,7 @@ func TestVerifyPaystack_EmailComparisonIsForgiving(t *testing.T) { `{"reference":"T_ref","plan":"pro","interval":"month"}`)) if rec.Code != http.StatusOK { - t.Fatalf("got %d, want 200 — a case/whitespace difference locked out a real customer: %s", + t.Fatalf("got %d, want 200. A case/whitespace difference locked out a real customer: %s", rec.Code, rec.Body.String()) } } @@ -241,7 +241,7 @@ func TestVerifyPaystack_RejectsReplayByAnotherUser(t *testing.T) { `{"reference":"T_ref","plan":"pro","interval":"month"}`)) if rec.Code != http.StatusConflict { - t.Fatalf("got %d, want 409 — a redeemed reference was accepted again: %s", + t.Fatalf("got %d, want 409. A redeemed reference was accepted again: %s", rec.Code, rec.Body.String()) } @@ -252,7 +252,7 @@ func TestVerifyPaystack_RejectsReplayByAnotherUser(t *testing.T) { // The rightful owner keeps their subscription. ownerSub, _ := h.Store.GetSubscription(owner.ID) if ownerSub.Plan != "pro" { - t.Errorf("owner plan = %q, want pro — the replay must not disturb them", ownerSub.Plan) + t.Errorf("owner plan = %q, want pro. The replay must not disturb them", ownerSub.Plan) } } @@ -303,7 +303,7 @@ func TestVerifyPaystack_RejectsAmountBelowPlanPrice(t *testing.T) { } // Paystack has no native free trial, so a ₦0 charge against a priced plan is -// never legitimate — it used to be treated as a trial and granted Pro. +// never legitimate. It used to be treated as a trial and granted Pro. func TestVerifyPaystack_RejectsZeroAmountCharge(t *testing.T) { h, user := newBillingTestHandler(t, verifyBody("success", "buyer@example.com", testPlanMonthly, 0, monthlyKobo)) @@ -350,7 +350,7 @@ func TestVerifyPaystack_NeverProducesATrial(t *testing.T) { sub, _ := h.Store.GetSubscription(user.ID) if sub.TrialEnd != nil { - t.Errorf("stored trial_end = %v, want nil — Paystack grants no trial", sub.TrialEnd) + t.Errorf("stored trial_end = %v, want nil. Paystack grants no trial", sub.TrialEnd) } if sub.Status != "active" { t.Errorf("stored status = %q, want active", sub.Status) @@ -372,7 +372,7 @@ func TestVerifyPaystack_IgnoresClientSuppliedInterval(t *testing.T) { sub, _ := h.Store.GetSubscription(user.ID) if sub.Interval != "month" { - t.Errorf("interval = %q, want month — the client's claim was trusted", sub.Interval) + t.Errorf("interval = %q, want month. The client's claim was trusted", sub.Interval) } // A month's access, not a year's. if sub.CurrentPeriodEnd == nil { @@ -519,7 +519,7 @@ func TestWebhook_StaleDeliveryDoesNotResurrectPro(t *testing.T) { sub, _ = h.Store.GetSubscription(user.ID) if sub.Plan != "free" { - t.Errorf("plan = %q, want free — a stale delivery resurrected Pro", sub.Plan) + t.Errorf("plan = %q, want free. A stale delivery resurrected Pro", sub.Plan) } if sub.Status != "canceled" { t.Errorf("status = %q, want canceled", sub.Status) @@ -537,7 +537,7 @@ func TestWebhook_ExpiredStatusDropsPlanToFree(t *testing.T) { sub, _ := h.Store.GetSubscription(user.ID) if sub.Plan != "free" { - t.Errorf("plan = %q, want free — subscription_updated carrying status=expired left the row inconsistent", sub.Plan) + t.Errorf("plan = %q, want free, subscription_updated carrying status=expired left the row inconsistent", sub.Plan) } } @@ -586,7 +586,7 @@ func TestWebhook_IgnoredEventRecordedButNotApplied(t *testing.T) { sub, _ := h.Store.GetSubscription(user.ID) if sub.Plan != "free" { - t.Errorf("plan = %q, want free — order_created must not touch the subscription", sub.Plan) + t.Errorf("plan = %q, want free, order_created must not touch the subscription", sub.Plan) } } @@ -658,7 +658,7 @@ func TestPaystackRenewalAdvancesPeriodWithoutLosingSubID(t *testing.T) { sub, _ := h.Store.GetSubscription(user.ID) if sub.ProviderSubID != "SUB_original" { - t.Errorf("provider_sub_id = %q, want SUB_original — the renewal blanked it", sub.ProviderSubID) + t.Errorf("provider_sub_id = %q, want SUB_original. The renewal blanked it", sub.ProviderSubID) } if sub.Plan != "pro" || sub.Status != "active" { t.Errorf("plan/status = %s/%s, want pro/active", sub.Plan, sub.Status) @@ -682,7 +682,7 @@ func TestPaystackChargeWithoutPlanIsIgnored(t *testing.T) { } sub, _ := h.Store.GetSubscription(user.ID) if sub.Plan != "free" { - t.Errorf("plan = %q, want free — a plan-less charge granted Pro", sub.Plan) + t.Errorf("plan = %q, want free. A plan-less charge granted Pro", sub.Plan) } } @@ -706,7 +706,7 @@ func TestPaystackForeignPlanChargeIgnored(t *testing.T) { } // A failed renewal must mark the subscription past_due WITHOUT blanking the -// period — a nil period grants access indefinitely, which would disable the +// period. A nil period grants access indefinitely, which would disable the // very expiry gate this event exists to trigger. func TestPaystackInvoiceFailureMarksPastDueAndKeepsPeriod(t *testing.T) { h, user := newPaystackWebhookHandler(t) @@ -742,7 +742,7 @@ func TestPaystackInvoiceFailureMarksPastDueAndKeepsPeriod(t *testing.T) { t.Errorf("status = %q, want past_due", sub.Status) } if sub.CurrentPeriodEnd == nil { - t.Fatal("current_period_end was blanked — the expiry gate can no longer fire") + t.Fatal("current_period_end was blanked. The expiry gate can no longer fire") } if !sub.CurrentPeriodEnd.Equal(period) { t.Errorf("current_period_end = %v, want %v unchanged", sub.CurrentPeriodEnd, period) @@ -766,12 +766,12 @@ func TestPaystackInvoiceSuccessIgnored(t *testing.T) { } sub, _ := h.Store.GetSubscription(user.ID) if sub.Plan != "free" { - t.Errorf("plan = %q, want free — a paid invoice must not create a subscription", sub.Plan) + t.Errorf("plan = %q, want free. A paid invoice must not create a subscription", sub.Plan) } } -// A foreign card on NGN pricing is recorded and flagged, never refused — -// blocking a real customer after charging them is worse than the mispricing. +// A foreign card on NGN pricing is recorded and flagged, never refused. +// Blocking a real customer after charging them is worse than the mispricing. func TestVerifyPaystack_ForeignCardOnNgnPricingIsAllowedNotBlocked(t *testing.T) { h, user := newBillingTestHandler(t, verifyBodyFrom("success", "buyer@example.com", testPlanMonthly, monthlyKobo, monthlyKobo, "US")) @@ -781,7 +781,7 @@ func TestVerifyPaystack_ForeignCardOnNgnPricingIsAllowedNotBlocked(t *testing.T) `{"reference":"T_ref","plan":"pro","interval":"month"}`)) if rec.Code != http.StatusOK { - t.Fatalf("got %d, want 200 — a foreign card must not be refused: %s", + t.Fatalf("got %d, want 200. A foreign card must not be refused: %s", rec.Code, rec.Body.String()) } sub, _ := h.Store.GetSubscription(user.ID) @@ -802,14 +802,14 @@ func TestVerifyPaystack_NonReusablePaymentIsGrantedButNotRecurring(t *testing.T) `{"reference":"T_ref","plan":"pro","interval":"month"}`)) if rec.Code != http.StatusOK { - t.Fatalf("got %d, want 200 — they paid, they get the period: %s", rec.Code, rec.Body.String()) + t.Fatalf("got %d, want 200. They paid, they get the period: %s", rec.Code, rec.Body.String()) } sub, _ := h.Store.GetSubscription(user.ID) if sub.Plan != "pro" || sub.Status != "active" { t.Errorf("plan/status = %s/%s, want pro/active", sub.Plan, sub.Status) } if sub.AutoRenews { - t.Error("auto_renews = true for a bank transfer — it cannot be charged again") + t.Error("auto_renews = true for a bank transfer. It cannot be charged again") } } @@ -862,7 +862,7 @@ func TestPaystackWebhookPreservesAutoRenews(t *testing.T) { } sub, _ := h.Store.GetSubscription(user.ID) if sub.AutoRenews { - t.Error("a webhook flipped auto_renews to true — it cannot see the authorization") + t.Error("a webhook flipped auto_renews to true. It cannot see the authorization") } } @@ -890,14 +890,14 @@ func TestVerifyPaystack_ReplayingAReferenceDoesNotApplyTwice(t *testing.T) { for i := 2; i <= 4; i++ { if rec := post(); rec.Code != http.StatusOK { - t.Fatalf("attempt %d: got %d, want 200 — a retry must not lock the customer out: %s", + t.Fatalf("attempt %d: got %d, want 200. A retry must not lock the customer out: %s", i, rec.Code, rec.Body.String()) } } after, _ := h.Store.GetSubscription(user.ID) if !after.CurrentPeriodEnd.Equal(firstEnd) { - t.Errorf("current_period_end moved from %v to %v — replaying a reference granted extra time", + t.Errorf("current_period_end moved from %v to %v. Replaying a reference granted extra time", firstEnd, after.CurrentPeriodEnd) } } @@ -938,7 +938,7 @@ func TestVerifyPaystack_RenewalExtendsFromTheCurrentExpiry(t *testing.T) { } } -// An expired period must not be carried forward — that would backdate the +// An expired period must not be carried forward. That would backdate the // renewal to a date already gone. func TestVerifyPaystack_RenewalAfterExpiryStartsFromNow(t *testing.T) { h, user := newBillingTestHandler(t, diff --git a/internal/handler/health.go b/internal/handler/health.go index 67cc2c2..c8148f3 100644 --- a/internal/handler/health.go +++ b/internal/handler/health.go @@ -38,8 +38,8 @@ func (h *HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") // monitors must never get a cached result if err := h.Store.Ping(); err != nil { - // Deliberately don't leak err.Error() in the response — - // internal error details have no business being public. + // Deliberately don't leak err.Error() in the response. + // Internal error details have no business being public. // Full detail goes to the server log only. resp.Status = "degraded" resp.Database = "error" diff --git a/internal/handler/me.go b/internal/handler/me.go index dafab2e..b9b3490 100644 --- a/internal/handler/me.go +++ b/internal/handler/me.go @@ -10,7 +10,7 @@ import ( ) // MeHandler returns the authenticated user's identity, plan, and limits. -// Works with both JWTs and API tokens — the CLI uses it for `hookdrop whoami` +// Works with both JWTs and API tokens. The CLI uses it for `hookdrop whoami` // and to validate a token at login. type MeHandler struct { Store *store.Store diff --git a/internal/handler/replay.go b/internal/handler/replay.go index eb747ee..7d6f434 100644 --- a/internal/handler/replay.go +++ b/internal/handler/replay.go @@ -44,7 +44,7 @@ func (h *ReplayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - // Ownership check on the captured request's session/endpoint — foreign + // Ownership check on the captured request's session/endpoint. Foreign // requests return the same 404 as missing ones. user := middleware.GetUser(r) if _, ok := h.Store.ResolveIdentifierForUser(original.SessionID, user.ID); !ok { diff --git a/internal/handler/requests.go b/internal/handler/requests.go index eb6f0d2..6a2c228 100644 --- a/internal/handler/requests.go +++ b/internal/handler/requests.go @@ -24,7 +24,7 @@ func (h *RequestsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // Resolve slug/endpoint/session to the canonical ID requests are stored - // under, and enforce ownership — foreign resources return the same 404 as + // under, and enforce ownership. Foreign resources return the same 404 as // missing ones. user := middleware.GetUser(r) sessionID, ok := h.Store.ResolveIdentifierForUser(identifier, user.ID) diff --git a/internal/handler/sse.go b/internal/handler/sse.go index b0b7599..4394a7f 100644 --- a/internal/handler/sse.go +++ b/internal/handler/sse.go @@ -23,7 +23,7 @@ func (h *SSEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { identifier = strings.Trim(identifier, "/") // Resolve slug/endpoint/session to the canonical ID the broadcaster keys - // by, and enforce ownership — foreign resources look identical to missing + // by, and enforce ownership. Foreign resources look identical to missing // ones (404, no existence leak). user := middleware.GetUser(r) sessionID, ok := h.Store.ResolveIdentifierForUser(identifier, user.ID) @@ -38,7 +38,7 @@ func (h *SSEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Connection", "keep-alive") w.Header().Set("X-Accel-Buffering", "no") // tells nginx: don't buffer this - // 3. Flush support — needed to push chunks immediately + // 3. Flush support. Needed to push chunks immediately flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "streaming not supported", http.StatusInternalServerError) @@ -48,7 +48,7 @@ func (h *SSEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // 4. Register this browser tab as a client client := &sse.Client{ SessionID: sessionID, - Send: make(chan []byte, 32), // buffered — absorbs short bursts + Send: make(chan []byte, 32), // buffered. Absorbs short bursts } h.Broadcaster.Register(client) defer h.Broadcaster.Deregister(client) @@ -57,11 +57,11 @@ func (h *SSEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "event: connected\ndata: {\"session_id\":\"%s\"}\n\n", sessionID) flusher.Flush() - // 6. Keep-alive ticker — browsers disconnect if nothing arrives for ~30s + // 6. Keep-alive ticker. Browsers disconnect if nothing arrives for ~30s ticker := time.NewTicker(20 * time.Second) defer ticker.Stop() - // 7. Main loop — wait for events or disconnection + // 7. Main loop. Wait for events or disconnection for { select { case payload := <-client.Send: @@ -70,7 +70,7 @@ func (h *SSEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { flusher.Flush() case <-ticker.C: - // SSE comment — keeps the connection alive, browsers ignore it + // SSE comment. Keeps the connection alive, browsers ignore it fmt.Fprintf(w, ": keepalive\n\n") flusher.Flush() diff --git a/internal/handler/tokens.go b/internal/handler/tokens.go index 551da52..20c54c1 100644 --- a/internal/handler/tokens.go +++ b/internal/handler/tokens.go @@ -54,7 +54,7 @@ func (h *TokensHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *TokensHandler) create(w http.ResponseWriter, r *http.Request, userID string) { if h.MintLimiter != nil && !h.MintLimiter.Allow(userID) { w.Header().Set("Retry-After", "3600") - http.Error(w, "too many tokens created — try again later", http.StatusTooManyRequests) + http.Error(w, "too many tokens created. Try again later", http.StatusTooManyRequests) return } diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go index d9bc5df..91438f0 100644 --- a/internal/middleware/auth.go +++ b/internal/middleware/auth.go @@ -14,7 +14,7 @@ type contextKey string const UserContextKey contextKey = "user" -// Auth methods — token-management routes are JWT-only so a leaked API token +// Auth methods. Token-management routes are JWT-only so a leaked API token // cannot mint or revoke tokens. const ( AuthMethodJWT = "jwt" diff --git a/internal/middleware/cors.go b/internal/middleware/cors.go index f59b93a..677bc55 100644 --- a/internal/middleware/cors.go +++ b/internal/middleware/cors.go @@ -8,10 +8,10 @@ import ( func CORS(next http.Handler, allowedOrigins string) http.Handler { if allowedOrigins == "" { - log.Fatal("CORS: ALLOWED_ORIGIN is not set — refusing to start with open CORS") + log.Fatal("CORS: ALLOWED_ORIGIN is not set. Refusing to start with open CORS") } - // Build a lookup map — supports comma-separated list of origins + // Build a lookup map. Supports comma-separated list of origins // e.g. "https://hookdrop.app,https://www.hookdrop.app" allowed := make(map[string]bool) for _, origin := range strings.Split(allowedOrigins, ",") { @@ -36,7 +36,7 @@ func CORS(next http.Handler, allowedOrigins string) http.Handler { log.Printf("CORS: rejected origin %q (allowed: %v)", origin, allowedOrigins) } - // Always handle preflight — even for rejected origins, return 204 + // Always handle preflight. Even for rejected origins, return 204 if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) diff --git a/internal/models/request.go b/internal/models/request.go index 2c2c087..45a336b 100644 --- a/internal/models/request.go +++ b/internal/models/request.go @@ -85,8 +85,8 @@ type Subscription struct { CurrentPeriodEnd *time.Time `json:"current_period_end"` TrialEnd *time.Time `json:"trial_end"` CancelAtPeriodEnd bool `json:"cancel_at_period_end"` - // AutoRenews is false when the payment method cannot be charged again — - // a Paystack bank transfer, for example. Such a subscription is a single + // AutoRenews is false when the payment method cannot be charged again. + // A Paystack bank transfer, for example. Such a subscription is a single // paid period that will simply expire, and the UI must say so. AutoRenews bool `json:"auto_renews"` Currency string `json:"currency"` @@ -107,8 +107,8 @@ type BillingEvent struct { EventType string Payload string // EventKey is the SHA-256 of the raw payload. Neither provider sends a - // unique event ID, but retries are byte-identical — they have to be, since - // the signature is computed over the raw body — so the hash is stable + // unique event ID, but retries are byte-identical. They have to be, since + // the signature is computed over the raw body, so the hash is stable // across them. EventKey string // EventAt is the provider's own timestamp for the event, used to reject diff --git a/internal/replay/engine.go b/internal/replay/engine.go index e8bb988..ffba924 100644 --- a/internal/replay/engine.go +++ b/internal/replay/engine.go @@ -20,7 +20,7 @@ func NewEngine() *Engine { return &Engine{ client: &http.Client{ Timeout: 30 * time.Second, - // Don't follow redirects — return them as-is so the + // Don't follow redirects. Return them as-is so the // developer sees exactly what their server responded CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse @@ -35,7 +35,7 @@ func (e *Engine) Replay( replayReq *models.ReplayRequest, ) (*models.ReplayResponse, error) { - // 1. Determine body — use override if provided, else original + // 1. Determine body. Use override if provided, else original bodyBytes := original.Body if replayReq.Body != "" { bodyBytes = []byte(replayReq.Body) @@ -80,7 +80,7 @@ func (e *Engine) Replay( } defer resp.Body.Close() - // 7. Read response body — cap at 1MB + // 7. Read response body. Cap at 1MB respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if err != nil { return nil, fmt.Errorf("read response: %w", err) diff --git a/internal/session/session.go b/internal/session/session.go index 137f306..a1d136e 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -21,7 +21,7 @@ func NewManager(s *store.Store) *Manager { func (m *Manager) CreateSession(userID string) (*models.Session, error) { session := &models.Session{ - ID: uuid.NewString()[:8], // short ID — easier to read in URLs + ID: uuid.NewString()[:8], // short ID. Easier to read in URLs UserID: userID, CreatedAt: time.Now().UTC(), ExpiresAt: time.Now().UTC().Add(DefaultTTL), diff --git a/internal/sse/broadcaster.go b/internal/sse/broadcaster.go index 92b431c..41a5602 100644 --- a/internal/sse/broadcaster.go +++ b/internal/sse/broadcaster.go @@ -56,7 +56,7 @@ func (b *Broadcaster) Broadcast(sessionID string, req *models.CapturedRequest) { clients, ok := b.clients[sessionID] if !ok || len(clients) == 0 { - return // no one is watching — that's fine + return // no one is watching. That's fine } payload, err := json.Marshal(req) @@ -70,7 +70,7 @@ func (b *Broadcaster) Broadcast(sessionID string, req *models.CapturedRequest) { case client.Send <- payload: // delivered default: - // client's channel is full — it's too slow, skip it + // client's channel is full. It's too slow, skip it // it will catch up via the REST history endpoint log.Printf("SSE client too slow, dropping event for session %s", sessionID) } diff --git a/internal/store/api_tokens.go b/internal/store/api_tokens.go index 689741f..fed9003 100644 --- a/internal/store/api_tokens.go +++ b/internal/store/api_tokens.go @@ -16,7 +16,7 @@ func (s *Store) CreateAPIToken(t *models.APIToken) error { return err } -// GetAPITokenByHash returns the token only if it is active — revoked or +// GetAPITokenByHash returns the token only if it is active. Revoked or // expired tokens are filtered out in SQL so callers can't misuse them. func (s *Store) GetAPITokenByHash(hash string) (*models.APIToken, error) { t := &models.APIToken{} @@ -85,7 +85,7 @@ func (s *Store) RevokeAllAPITokens(userID string) error { } // TouchAPIToken records usage, throttled to one write per 5 minutes per -// token via the WHERE clause — no in-memory state needed. +// token via the WHERE clause. No in-memory state needed. func (s *Store) TouchAPIToken(id string, now time.Time) error { _, err := s.db.Exec( `UPDATE api_tokens SET last_used_at = ? diff --git a/internal/store/store.go b/internal/store/store.go index 3bfd7ab..8d8ce8c 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -26,7 +26,7 @@ func (s *Store) Ping() error { return fmt.Errorf("connection: %w", err) } - // A real query catches issues Ping() alone misses — locked file, + // A real query catches issues Ping() alone misses. Locked file, // corrupted schema, disk full. SELECT 1 against sqlite_master is // the lightest possible real query. var result int @@ -212,7 +212,7 @@ func (s *Store) migrate() error { return err } - // Safe column additions — idempotent, safe to run on every startup + // Safe column additions. Idempotent, safe to run on every startup migrations := []struct { table string column string @@ -490,7 +490,7 @@ func (s *Store) ConsumeMagicLink(token string) (*models.User, error) { return nil, fmt.Errorf("token expired") } - // Mark as consumed — one-time use + // Mark as consumed. One-time use _, err = s.db.Exec( `UPDATE magic_links SET used = 1 WHERE id = ?`, link.ID, ) @@ -590,8 +590,8 @@ func (s *Store) IdentifierExists(id string) bool { // ResolveIdentifierForUser resolves a slug, endpoint ID, or session ID to the // canonical identifier requests are stored/broadcast under, and checks the -// user may access it. ok=false covers both "not found" and "not yours" — -// callers must respond 404 so foreign resources are indistinguishable from +// user may access it. ok=false covers both "not found" and "not yours". +// Callers must respond 404 so foreign resources are indistinguishable from // missing ones (same convention as the secrets handler). func (s *Store) ResolveIdentifierForUser(identifier, userID string) (string, bool) { if ep, err := s.GetEndpointBySlug(identifier); err == nil && ep != nil { @@ -740,7 +740,7 @@ func (s *Store) GetSubscription(userID string) (*models.Subscription, error) { // - Auto-expiry check ───────────────────────────────────────────────── // If the user cancelled and the period has passed, treat them as free // without waiting for a Paystack webhook that may never arrive. - // This is a read-time check only — nothing is written to the database. + // This is a read-time check only. Nothing is written to the database. // The webhook will eventually persist the correct state when it fires. if sub.CancelAtPeriodEnd && sub.CurrentPeriodEnd != nil && @@ -768,7 +768,7 @@ func (s *Store) GetSubscription(userID string) (*models.Subscription, error) { const BillingEventRetention = 90 * 24 * time.Hour // ErrDuplicateBillingEvent means this exact webhook payload has been seen -// before — the provider retried a delivery we already have. +// before. The provider retried a delivery we already have. var ErrDuplicateBillingEvent = errors.New("duplicate billing event") // RecordBillingEvent stores an inbound webhook before it is processed. @@ -896,7 +896,7 @@ func (s *Store) DeleteOldBillingEvents() (int64, error) { // subscription ID. Used to recover the local user when a webhook arrives // without custom_data.user_id. // -// Unlike GetSubscription this returns (nil, nil) when there is no row — the +// Unlike GetSubscription this returns (nil, nil) when there is no row. The // caller needs to tell "not found" apart from "free plan". func (s *Store) GetSubscriptionByProviderSubID(providerSubID string) (*models.Subscription, error) { return s.subscriptionBy("provider_sub_id", providerSubID) @@ -929,7 +929,7 @@ func (s *Store) ListPaystackSubscriptionsNeedingRepair() ([]*models.Subscription WHERE provider = 'paystack' AND COALESCE(provider_sub_id,'') NOT LIKE 'SUB\_%' ESCAPE '\' -- Already reconciled. A row marked non-recurring will never gain a - -- SUB_ code, because there is no subscription to find — without this + -- SUB_ code, because there is no subscription to find. Without this -- it is reported as outstanding on every run, for ever. AND COALESCE(auto_renews,1) = 1 ORDER BY created_at`) @@ -988,7 +988,7 @@ func (s *Store) MarkSubscriptionNonRecurring(id string) error { } // subscriptionBy fetches one subscription by an indexed provider column. -// column is never caller-supplied — it comes from the two wrappers above. +// column is never caller-supplied. It comes from the two wrappers above. func (s *Store) subscriptionBy(column, value string) (*models.Subscription, error) { if value == "" { return nil, nil diff --git a/internal/store/store_test.go b/internal/store/store_test.go index 7de5975..bc2ec3f 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -221,7 +221,7 @@ func TestLatestProcessedEventAt(t *testing.T) { t.Fatalf("record: %v", err) } if _, found, err := s.LatestProcessedEventAt("lemonsqueezy", "sub-1"); err != nil || found { - t.Fatalf("found = %t (err %v), want false — nothing is processed yet", found, err) + t.Fatalf("found = %t (err %v), want false. Nothing is processed yet", found, err) } if err := s.RecordBillingEvent(billingEvent("key-old", "sub-1", older)); err != nil { @@ -266,7 +266,7 @@ func TestDeleteOldBillingEvents(t *testing.T) { t.Fatalf("delete: %v", err) } if n != 1 { - t.Fatalf("deleted %d, want 1 — only the out-of-window row", n) + t.Fatalf("deleted %d, want 1. Only the out-of-window row", n) } // The fresh row survives, so its key still collides. diff --git a/internal/verify/verify.go b/internal/verify/verify.go index 24cbd1c..d29681b 100644 --- a/internal/verify/verify.go +++ b/internal/verify/verify.go @@ -80,7 +80,7 @@ func verifyStripe(req *models.CapturedRequest, secret string) Result { return Result{Status: "failed", Provider: provider, Reason: "malformed Stripe-Signature header"} } - // Reject timestamps older than 5 minutes — replay attack protection + // Reject timestamps older than 5 minutes. Replay attack protection ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return Result{Status: "failed", Provider: provider, Reason: "invalid timestamp"} @@ -89,7 +89,7 @@ func verifyStripe(req *models.CapturedRequest, secret string) Result { return Result{ Status: "failed", Provider: provider, - Reason: fmt.Sprintf("timestamp too old (%ds) — possible replay attack", time.Now().Unix()-ts), + Reason: fmt.Sprintf("timestamp too old (%ds). Possible replay attack", time.Now().Unix()-ts), } } @@ -117,7 +117,7 @@ func verifyPaystack(req *models.CapturedRequest, secret string) Result { expected := computeHMAC(sha512.New, string(req.Body), secret) match := hmac.Equal([]byte(expected), []byte(sigHeader)) - // TEMP DEBUG — remove once the Paystack verification-failure bug is resolved. + // TEMP DEBUG. Remove once the Paystack verification-failure bug is resolved. // Never logs the secret itself, only its length. log.Printf("[paystack-verify-debug] secret_len=%d received_sig=%s computed_sig=%s match=%t", len(secret), sigHeader, expected, match) diff --git a/main.go b/main.go index b605488..5e583c8 100644 --- a/main.go +++ b/main.go @@ -88,7 +88,7 @@ func main() { "LEMONSQUEEZY_VARIANT_PRO_ANNUAL": lsVariantAnnual, }) if lsTestMode { - log.Printf("billing: LEMONSQUEEZY_TEST_MODE=true — checkouts are test mode, no real charges") + log.Printf("billing: LEMONSQUEEZY_TEST_MODE=true. Checkouts are test mode, no real charges") } // ── Paystack config (African users) @@ -178,7 +178,7 @@ func main() { mux.Handle("/health", healthHandler) authRateLimit := middleware.AuthIPRateLimit(authIPLimiter) - // Auth — public + // Auth. Public mux.HandleFunc("/auth/request", authRateLimit(authHandler.RequestLink)) mux.HandleFunc("/auth/verify", authHandler.VerifyLink) @@ -189,7 +189,7 @@ func main() { mux.Handle("/billing/verify-paystack", requireAuth(http.HandlerFunc(billingHandler.VerifyPaystack))) - // Billing — authenticated + // Billing. Authenticated mux.Handle("/billing/subscription", requireAuth(http.HandlerFunc(billingHandler.GetSubscription))) mux.Handle("/billing/checkout", @@ -199,20 +199,20 @@ func main() { mux.Handle("/billing/cancel", requireAuth(http.HandlerFunc(billingHandler.CancelSubscription))) - // Account + API tokens — authenticated (token management is JWT-only, + // Account + API tokens. Authenticated (token management is JWT-only, // enforced inside TokensHandler) mux.Handle("/me", requireAuth(&handler.MeHandler{Store: st})) mux.Handle("/tokens", requireAuth(&handler.TokensHandler{Store: st, MintLimiter: tokenMintLimiter})) mux.Handle("/tokens/", requireAuth(&handler.TokensHandler{Store: st, MintLimiter: tokenMintLimiter})) - // Core — authenticated + // Core. Authenticated mux.Handle("/sessions", requireAuth(&handler.SessionHandler{Manager: mgr})) mux.Handle("/requests/", requireAuth(&handler.RequestsHandler{Store: st})) mux.Handle("/replay", requireAuth(&handler.ReplayHandler{Store: st, Engine: replayEngine})) mux.Handle("/events/", requireAuth(&handler.SSEHandler{Broadcaster: broadcaster, Store: st})) mux.Handle("/endpoints", requireAuth(&handler.EndpointsHandler{Store: st})) - // Endpoints + secrets — authenticated, routed by path shape + // Endpoints + secrets. Authenticated, routed by path shape mux.Handle("/endpoints/", requireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/secrets") { secretsHandler.ServeHTTP(w, r) @@ -221,7 +221,7 @@ func main() { } }))) - // Inbox — public but rate limited + // Inbox. Public but rate limited mux.Handle("/i/", inboxLimiter(&handler.InboxHandler{ Store: st, Broadcast: broadcaster.Broadcast, diff --git a/repair.go b/repair.go index e68c444..08b397d 100644 --- a/repair.go +++ b/repair.go @@ -16,7 +16,7 @@ import ( // // Those rows predate subscription.create being applied correctly. They cannot // be cancelled through the Paystack API, and their current_period_end was -// derived at signup and never refreshed by a renewal — so once access is gated +// derived at signup and never refreshed by a renewal, so once access is gated // on the period, they read as lapsed. // // Dry run by default: nothing is written unless apply is true. @@ -31,11 +31,11 @@ func repairPaystackSubscriptions( } if len(rows) == 0 { - log.Printf("repair: nothing to do — every Paystack row already holds a subscription code") + log.Printf("repair: nothing to do. Every Paystack row already holds a subscription code") return nil } - mode := "DRY RUN — nothing will be written" + mode := "DRY RUN. Nothing will be written" if apply { mode = "APPLYING changes" } @@ -101,9 +101,9 @@ func repairPaystackSubscriptions( } if apply { - log.Printf("repair: done — %d repaired, %d failed", repaired, failed) + log.Printf("repair: done. %d repaired, %d failed", repaired, failed) } else { - log.Printf("repair: dry run complete — %d would be repaired, %d could not be resolved", len(rows)-failed, failed) + log.Printf("repair: dry run complete. %d would be repaired, %d could not be resolved", len(rows)-failed, failed) log.Printf("repair: re-run with -apply to write these changes") } if failed > 0 { diff --git a/ui/src/components/billing/ManageSubscriptionPanel.tsx b/ui/src/components/billing/ManageSubscriptionPanel.tsx index abd3c1b..5048c2e 100644 --- a/ui/src/components/billing/ManageSubscriptionPanel.tsx +++ b/ui/src/components/billing/ManageSubscriptionPanel.tsx @@ -12,7 +12,7 @@ export function ManageSubscriptionPanel({ const { subscription, isTrialing, refetch } = useBilling() // A subscription paid for with a method that cannot be charged again has - // nothing to cancel — Paystack holds no subscription for it. Offering the + // nothing to cancel. Paystack holds no subscription for it. Offering the // button would mark the row cancelled for no reason and log that the // provider was never notified. const willNotRenew = subscription?.auto_renews === false @@ -123,7 +123,7 @@ export function ManageSubscriptionPanel({

)} - {/* Cancel — hidden once cancelled, and for one-off payments */} + {/* Cancel. Hidden once cancelled, and for one-off payments */} {!subscription?.cancel_at_period_end && !willNotRenew && (
- {/* Active filter chips — always visible when filters applied (even when collapsed) */} + {/* Active filter chips. Always visible when filters applied (even when collapsed) */} {hasActiveChips && (
{filters.method && ( diff --git a/ui/src/components/feed/RequestItem.tsx b/ui/src/components/feed/RequestItem.tsx index c639462..b3efbbb 100644 --- a/ui/src/components/feed/RequestItem.tsx +++ b/ui/src/components/feed/RequestItem.tsx @@ -18,7 +18,7 @@ export function RequestItem({ request, selected, isNew = false, onClick }: Props selected ? 'bg-indigo-500/[0.07]' : 'hover:bg-surface' } ${isNew ? 'animate-arrive' : ''}`} > - {/* Left accent bar — always present, no layout shift */} + {/* Left accent bar. Always present, no layout shift */} - {/* Tabs — hidden on mobile where MobileTabBar takes over this role */} + {/* Tabs. Hidden on mobile where MobileTabBar takes over this role */}
{(['session', 'endpoints'] as Tab[]).map(t => (