mirror of
https://github.com/rjNemo/payit
synced 2026-06-06 02:16:40 +00:00
feat: scaffold payit skeleton
This commit is contained in:
parent
810a49aa4e
commit
17db8bb165
10 changed files with 696 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
*cache
|
||||
|
|
@ -11,6 +11,9 @@ PayIt is a Go-based Stripe integration demo. Use this guide to deliver focused c
|
|||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
- Prefer the Makefile workflow for local checks; these commands set local caches so they work in sandboxed environments.
|
||||
- After each code change run formatting, linting, and test suites via `make fmt lint test` to ensure consistency, coverage visibility, and to prevent cached results. If the Makefile is missing, create it before coding.
|
||||
- After verifying changes, create a conventional commit (e.g., `feat: add checkout handler`) so the history captures intent and scope.
|
||||
- `go run ./cmd/payit` — start the local server, loading configuration from `.env.local` when present.
|
||||
- `go build ./...` — ensure every package compiles prior to opening a PR.
|
||||
- `go test ./...` — execute the full unit suite; add `-run TestStripe` to focus on Stripe-specific tests during iteration.
|
||||
|
|
|
|||
27
Makefile
Normal file
27
Makefile
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
SHELL := /bin/bash
|
||||
GOFILES := $(shell find . -path './.modcache' -prune -o -path './.cache' -prune -o -name '*.go' -print)
|
||||
GOCACHE ?= $(CURDIR)/.cache
|
||||
GOMODCACHE ?= $(CURDIR)/.modcache
|
||||
GOFLAGS ?= -count=1
|
||||
|
||||
.PHONY: fmt lint test clean tidy
|
||||
|
||||
fmt:
|
||||
@echo "Formatting Go files"
|
||||
@gofmt -w $(GOFILES)
|
||||
|
||||
lint:
|
||||
@echo "Running linters"
|
||||
@GOCACHE=$(GOCACHE) GOMODCACHE=$(GOMODCACHE) go vet ./...
|
||||
@GOCACHE=$(GOCACHE) GOMODCACHE=$(GOMODCACHE) golangci-lint run --timeout=5m
|
||||
|
||||
test:
|
||||
@echo "Running tests with coverage"
|
||||
@GOCACHE=$(GOCACHE) GOMODCACHE=$(GOMODCACHE) go test $(GOFLAGS) -cover ./...
|
||||
|
||||
clean:
|
||||
@rm -rf $(GOCACHE) $(GOMODCACHE)
|
||||
|
||||
tidy:
|
||||
@echo "Tidying go.mod"
|
||||
@GOCACHE=$(GOCACHE) GOMODCACHE=$(GOMODCACHE) go mod tidy
|
||||
55
cmd/payit/main.go
Normal file
55
cmd/payit/main.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/rjNemo/payit/config"
|
||||
"github.com/rjNemo/payit/internal/web"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load configuration: %v", err)
|
||||
}
|
||||
|
||||
handler := web.NewServer(cfg)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: handler,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
log.Printf("Starting PayIt server on %s", srv.Addr)
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- srv.ListenAndServe()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("Shutting down server...")
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
log.Fatalf("server shutdown failed: %v", err)
|
||||
}
|
||||
log.Println("Server stopped cleanly")
|
||||
case err := <-errCh:
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("server error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
149
config/config.go
Normal file
149
config/config.go
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ProductConfig holds metadata for the single demo product.
|
||||
type ProductConfig struct {
|
||||
Name string
|
||||
Description string
|
||||
PriceCents int64
|
||||
Currency string
|
||||
SuccessURL string
|
||||
CancelURL string
|
||||
}
|
||||
|
||||
// Config aggregates all runtime configuration required by the server.
|
||||
type Config struct {
|
||||
StripeSecretKey string
|
||||
StripePublishableKey string
|
||||
Product ProductConfig
|
||||
}
|
||||
|
||||
// Load reads configuration from environment variables, optionally sourcing
|
||||
// a .env.local file when present.
|
||||
func Load() (Config, error) {
|
||||
_ = loadDotEnv()
|
||||
|
||||
priceRaw := strings.TrimSpace(os.Getenv("PAYIT_PRODUCT_PRICE_CENTS"))
|
||||
cfg := Config{
|
||||
StripeSecretKey: os.Getenv("PAYIT_STRIPE_SECRET_KEY"),
|
||||
StripePublishableKey: os.Getenv("PAYIT_STRIPE_PUBLISHABLE_KEY"),
|
||||
Product: ProductConfig{
|
||||
Name: os.Getenv("PAYIT_PRODUCT_NAME"),
|
||||
Description: os.Getenv("PAYIT_PRODUCT_DESCRIPTION"),
|
||||
Currency: os.Getenv("PAYIT_PRODUCT_CURRENCY"),
|
||||
SuccessURL: os.Getenv("PAYIT_PRODUCT_SUCCESS_URL"),
|
||||
CancelURL: os.Getenv("PAYIT_PRODUCT_CANCEL_URL"),
|
||||
},
|
||||
}
|
||||
|
||||
if missing := validate(cfg, priceRaw); len(missing) > 0 {
|
||||
return Config{}, fmt.Errorf("missing required environment variables: %s", strings.Join(missing, ", "))
|
||||
}
|
||||
|
||||
price, err := parsePrice(priceRaw)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
cfg.Product.PriceCents = price
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func loadDotEnv() error {
|
||||
filename := ".env.local"
|
||||
relPaths := []string{
|
||||
filename,
|
||||
filepath.Join("..", filename),
|
||||
}
|
||||
|
||||
for _, path := range relPaths {
|
||||
if err := applyEnvFile(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyEnvFile(path string) (retErr error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if cerr := file.Close(); retErr == nil && cerr != nil {
|
||||
retErr = cerr
|
||||
}
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
_ = os.Setenv(key, value)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parsePrice(value string) (int64, error) {
|
||||
price, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("PAYIT_PRODUCT_PRICE_CENTS must be a positive integer: %w", err)
|
||||
}
|
||||
if price <= 0 {
|
||||
return 0, fmt.Errorf("PAYIT_PRODUCT_PRICE_CENTS must be a positive integer")
|
||||
}
|
||||
return price, nil
|
||||
}
|
||||
|
||||
func validate(cfg Config, priceRaw string) []string {
|
||||
missing := make([]string, 0)
|
||||
if cfg.StripeSecretKey == "" {
|
||||
missing = append(missing, "PAYIT_STRIPE_SECRET_KEY")
|
||||
}
|
||||
if cfg.StripePublishableKey == "" {
|
||||
missing = append(missing, "PAYIT_STRIPE_PUBLISHABLE_KEY")
|
||||
}
|
||||
if cfg.Product.Name == "" {
|
||||
missing = append(missing, "PAYIT_PRODUCT_NAME")
|
||||
}
|
||||
if cfg.Product.Description == "" {
|
||||
missing = append(missing, "PAYIT_PRODUCT_DESCRIPTION")
|
||||
}
|
||||
if priceRaw == "" {
|
||||
missing = append(missing, "PAYIT_PRODUCT_PRICE_CENTS")
|
||||
}
|
||||
if cfg.Product.Currency == "" {
|
||||
missing = append(missing, "PAYIT_PRODUCT_CURRENCY")
|
||||
}
|
||||
if cfg.Product.SuccessURL == "" {
|
||||
missing = append(missing, "PAYIT_PRODUCT_SUCCESS_URL")
|
||||
}
|
||||
if cfg.Product.CancelURL == "" {
|
||||
missing = append(missing, "PAYIT_PRODUCT_CANCEL_URL")
|
||||
}
|
||||
|
||||
return missing
|
||||
}
|
||||
77
config/config_test.go
Normal file
77
config/config_test.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadSuccess(t *testing.T) {
|
||||
t.Setenv("PAYIT_STRIPE_SECRET_KEY", "sk_test")
|
||||
t.Setenv("PAYIT_STRIPE_PUBLISHABLE_KEY", "pk_test")
|
||||
t.Setenv("PAYIT_PRODUCT_NAME", "Demo product")
|
||||
t.Setenv("PAYIT_PRODUCT_DESCRIPTION", "Great product")
|
||||
t.Setenv("PAYIT_PRODUCT_PRICE_CENTS", "2500")
|
||||
t.Setenv("PAYIT_PRODUCT_CURRENCY", "usd")
|
||||
t.Setenv("PAYIT_PRODUCT_SUCCESS_URL", "https://example.com/success")
|
||||
t.Setenv("PAYIT_PRODUCT_CANCEL_URL", "https://example.com/cancel")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Product.PriceCents != 2500 {
|
||||
t.Fatalf("expected price 2500, got %d", cfg.Product.PriceCents)
|
||||
}
|
||||
if cfg.Product.Name != "Demo product" {
|
||||
t.Fatalf("unexpected product name: %s", cfg.Product.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadMissingMandatoryVariables(t *testing.T) {
|
||||
clearAllEnv(t)
|
||||
|
||||
_, err := Load()
|
||||
if err == nil {
|
||||
t.Fatal("expected error when required variables are missing")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "missing required environment variables") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadInvalidPrice(t *testing.T) {
|
||||
t.Setenv("PAYIT_STRIPE_SECRET_KEY", "sk_test")
|
||||
t.Setenv("PAYIT_STRIPE_PUBLISHABLE_KEY", "pk_test")
|
||||
t.Setenv("PAYIT_PRODUCT_NAME", "Demo product")
|
||||
t.Setenv("PAYIT_PRODUCT_DESCRIPTION", "Great product")
|
||||
t.Setenv("PAYIT_PRODUCT_PRICE_CENTS", "-1")
|
||||
t.Setenv("PAYIT_PRODUCT_CURRENCY", "usd")
|
||||
t.Setenv("PAYIT_PRODUCT_SUCCESS_URL", "https://example.com/success")
|
||||
t.Setenv("PAYIT_PRODUCT_CANCEL_URL", "https://example.com/cancel")
|
||||
|
||||
_, err := Load()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid price")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "PAYIT_PRODUCT_PRICE_CENTS") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func clearAllEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
envs := []string{
|
||||
"PAYIT_STRIPE_SECRET_KEY",
|
||||
"PAYIT_STRIPE_PUBLISHABLE_KEY",
|
||||
"PAYIT_PRODUCT_NAME",
|
||||
"PAYIT_PRODUCT_DESCRIPTION",
|
||||
"PAYIT_PRODUCT_PRICE_CENTS",
|
||||
"PAYIT_PRODUCT_CURRENCY",
|
||||
"PAYIT_PRODUCT_SUCCESS_URL",
|
||||
"PAYIT_PRODUCT_CANCEL_URL",
|
||||
}
|
||||
for _, env := range envs {
|
||||
t.Setenv(env, "")
|
||||
}
|
||||
}
|
||||
28
internal/web/server.go
Normal file
28
internal/web/server.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/rjNemo/payit/config"
|
||||
)
|
||||
|
||||
// Handler aggregates dependencies required by HTTP handlers.
|
||||
type Handler struct {
|
||||
cfg config.Config
|
||||
}
|
||||
|
||||
// NewServer constructs the root HTTP handler. The initial implementation only
|
||||
// exposes placeholder routes; later phases will wire Stripe-backed handlers and
|
||||
// templates.
|
||||
func NewServer(cfg config.Config) http.Handler {
|
||||
h := &Handler{cfg: cfg}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", h.notImplemented)
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func (h *Handler) notImplemented(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "PayIt demo coming soon", http.StatusNotImplemented)
|
||||
}
|
||||
5
thoughts/2025-09-27-impl-plan-todo.md
Normal file
5
thoughts/2025-09-27-impl-plan-todo.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Implementation TODOs - Stripe Checkout Demo
|
||||
- [x] Phase 1: Project skeleton & configuration
|
||||
- [ ] Phase 2: Stripe checkout backend
|
||||
- [ ] Phase 3: Static frontend & routing
|
||||
- [ ] Phase 4: Testing & developer experience
|
||||
230
thoughts/shared/plans/2025-09-27-stripe-checkout-demo.md
Normal file
230
thoughts/shared/plans/2025-09-27-stripe-checkout-demo.md
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
# Stripe Checkout Demo Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
Implement a Go-based Stripe Checkout demo that serves a static landing page for a single hardcoded product and uses Stripe-hosted checkout to process payments, aligning with the previously captured research.
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
- Repository currently contains only `go.mod`, `README.md`, and `AGENTS.md`; there is no executable application code or directory structure yet (`README.md:1-8`, `AGENTS.md:5-27`).
|
||||
- No configuration handling, HTTP server, or Stripe integration exists; we must introduce all components from scratch.
|
||||
- Research document `thoughts/shared/research/2025-09-27-stripe-integration-demo.md` outlines recommended architecture, folder layout, and open questions.
|
||||
|
||||
## Desired End State
|
||||
|
||||
Deliver a runnable Go service (`cmd/payit/main.go`) that serves a static product page, exposes a `/api/checkout` endpoint to create a Stripe Checkout Session for a single hardcoded product, and documents setup/testing steps. The codebase should follow the structure in `AGENTS.md`, with automated tests covering the Stripe service wrapper and HTTP handler behavior.
|
||||
|
||||
### Key Discoveries
|
||||
|
||||
- `thoughts/shared/research/2025-09-27-stripe-integration-demo.md:21-55` – Defines target architecture for server, Stripe layer, assets, and testing.
|
||||
- `AGENTS.md:5-18` – Specifies preferred directory layout (`cmd/`, `internal/`, `web/`, `config/`) and formatting expectations.
|
||||
- `AGENTS.md:26-27` – Emphasizes proper handling of Stripe secrets via environment variables.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Supporting multiple or dynamic products; only a single hardcoded demo product is required.
|
||||
- Implementing Stripe webhooks or fulfillment workflows; success relies on Stripe redirect pages.
|
||||
- Adding auxiliary tooling such as `hack/spec_metadata.sh` or deployment scripts.
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
Build the application in four incremental phases: establish project skeleton and configuration, implement Stripe Checkout backend logic, wire static frontend assets with routing, and finish with testing plus documentation updates. Each phase will introduce isolated packages, enabling focused testing and straightforward iteration.
|
||||
|
||||
## Phase 1: Project Skeleton & Configuration
|
||||
|
||||
### Overview
|
||||
|
||||
Create the application structure, entrypoint, and configuration loader for environment variables.
|
||||
|
||||
### Changes Required
|
||||
|
||||
**File**: `go.mod`
|
||||
**Changes**: Add required module dependencies (`github.com/stripe/stripe-go/v78`, optional `github.com/joho/godotenv` if used) and tidy module.
|
||||
|
||||
**File**: `cmd/payit/main.go` (new)
|
||||
**Changes**: Bootstrap configuration, initialize logger, construct HTTP server (delegated to `internal/web`).
|
||||
|
||||
**File**: `config/config.go` (new)
|
||||
**Changes**: Define `Config` struct holding Stripe secret key, publishable key, and product metadata (name, price, currency, success/cancel URLs). Load values from environment (with optional `.env.local` support) and validate presence.
|
||||
|
||||
**File**: `config/config_test.go` (new)
|
||||
**Changes**: Unit tests ensuring configuration loading validates required fields and default behaviors.
|
||||
|
||||
**File**: Directory scaffolding (`internal/stripe/`, `internal/web/`, `web/templates/`, `web/static/`, `testdata/`)
|
||||
**Changes**: Create empty placeholder files or README stubs as needed so later phases can populate them.
|
||||
|
||||
```go
|
||||
// config/config.go
|
||||
package config
|
||||
|
||||
type ProductConfig struct {
|
||||
Name string
|
||||
Description string
|
||||
PriceCents int64
|
||||
Currency string
|
||||
SuccessURL string
|
||||
CancelURL string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
StripeSecretKey string
|
||||
StripePublishableKey string
|
||||
Product ProductConfig
|
||||
}
|
||||
```
|
||||
|
||||
### Success Criteria
|
||||
|
||||
#### Automated Verification
|
||||
|
||||
- [x] `go fmt ./...`
|
||||
- [x] `go build ./...`
|
||||
|
||||
#### Manual Verification
|
||||
|
||||
- [x] Application starts with placeholder server: `go run ./cmd/payit` *(bind restricted in sandbox; confirmed startup log before failure)*
|
||||
- [x] Missing environment variables cause a clear startup error
|
||||
|
||||
## Phase 2: Stripe Checkout Backend
|
||||
|
||||
### Overview
|
||||
|
||||
Implement the Stripe client wrapper, checkout session creator, and API handler.
|
||||
|
||||
### Changes Required
|
||||
|
||||
**File**: `internal/stripe/client.go` (new)
|
||||
**Changes**: Wrap Stripe SDK, expose interface for session creation, configure Stripe API key.
|
||||
|
||||
**File**: `internal/stripe/types.go` (new)
|
||||
**Changes**: Define request/response structs (e.g., `CheckoutSessionRequest`, `CheckoutSessionResult`).
|
||||
|
||||
**File**: `internal/stripe/client_test.go` (new)
|
||||
**Changes**: Table-driven tests using a fake Stripe API client to validate payload construction.
|
||||
|
||||
**File**: `internal/web/handlers.go` (new)
|
||||
**Changes**: Implement `/api/checkout` HTTP handler accepting POST, calling Stripe service, returning JSON (session ID / URL) with proper error handling.
|
||||
|
||||
**File**: `internal/web/server.go` (new)
|
||||
**Changes**: Build `http.Handler` wiring API routes and injecting Stripe service.
|
||||
|
||||
**File**: `internal/web/handlers_test.go` (new)
|
||||
**Changes**: Use `httptest` with a mocked Stripe service to assert status codes, response payloads, and error cases.
|
||||
|
||||
```go
|
||||
// internal/web/handlers.go
|
||||
func (h *Handler) CreateCheckoutSession(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
session, err := h.CheckoutService.CreateSession(ctx)
|
||||
if err != nil {
|
||||
http.Error(w, "checkout session failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(session)
|
||||
}
|
||||
```
|
||||
|
||||
### Success Criteria
|
||||
|
||||
#### Automated Verification
|
||||
|
||||
- [ ] `go test ./internal/stripe`
|
||||
- [ ] `go test ./internal/web`
|
||||
|
||||
#### Manual Verification
|
||||
|
||||
- [ ] `curl -X POST http://localhost:8080/api/checkout` returns session payload with mock keys
|
||||
- [ ] Error cases logged and surfaced with 500 response when Stripe call fails
|
||||
|
||||
## Phase 3: Static Frontend & Routing
|
||||
|
||||
### Overview
|
||||
|
||||
Implement landing page, static assets, and route wiring for root and assets.
|
||||
|
||||
### Changes Required
|
||||
|
||||
**File**: `web/templates/index.html` (new)
|
||||
**Changes**: HTML page describing the product with “Buy Now” button invoking JavaScript to POST to `/api/checkout` and redirect to returned `url`.
|
||||
|
||||
**File**: `web/static/styles.css` (new)
|
||||
**Changes**: Minimal styling for the product page.
|
||||
|
||||
**File**: `web/static/app.js` (new)
|
||||
**Changes**: JS `fetch` call to `/api/checkout`, handles response, redirects or displays error.
|
||||
|
||||
**File**: `internal/web/server.go`
|
||||
**Changes**: Serve template on `/`, static assets via `http.FileServer`, inject publishable key into template data if needed.
|
||||
|
||||
**File**: `internal/web/templates.go` (new)
|
||||
**Changes**: Helper to parse templates and render with provided data (publishable key, product info).
|
||||
|
||||
**File**: `internal/web/server_test.go` (new)
|
||||
**Changes**: Verify root route renders successfully and static assets are served.
|
||||
|
||||
### Success Criteria
|
||||
|
||||
#### Automated Verification
|
||||
|
||||
- [ ] `go test ./internal/web`
|
||||
|
||||
#### Manual Verification
|
||||
|
||||
- [ ] Visit `http://localhost:8080/` and confirm product page renders with publishable key available to JS
|
||||
- [ ] Clicking “Buy Now” redirects to Stripe Checkout when using valid keys
|
||||
|
||||
## Phase 4: Testing & Developer Experience
|
||||
|
||||
### Overview
|
||||
|
||||
Finalize tests, documentation, and developer onboarding.
|
||||
|
||||
### Changes Required
|
||||
|
||||
**File**: `README.md`
|
||||
**Changes**: Add setup instructions (env vars, running server, creating test mode keys), testing commands, and manual verification steps.
|
||||
|
||||
**File**: `.env.example` (new)
|
||||
**Changes**: Document expected environment variables (`PAYIT_STRIPE_SECRET_KEY`, `PAYIT_STRIPE_PUBLISHABLE_KEY`, `PAYIT_PRODUCT_NAME`, etc.).
|
||||
|
||||
**File**: `Makefile` (new, optional)
|
||||
**Changes**: Provide convenience targets (`make run`, `make test`) if deemed helpful for onboarding.
|
||||
|
||||
**File**: `internal/stripe/mock.go` (new)
|
||||
**Changes**: Provide simple mock implementation used in tests to avoid Stripe network calls.
|
||||
|
||||
**File**: `thoughts/shared/plans/2025-09-27-stripe-checkout-demo.md`
|
||||
**Changes**: Update checkboxes for completed phases as work progresses.
|
||||
|
||||
### Success Criteria
|
||||
|
||||
#### Automated Verification
|
||||
|
||||
- [x] `go fmt ./...`
|
||||
- [ ] `go test ./...`
|
||||
- [x] `go build ./...`
|
||||
|
||||
#### Manual Verification
|
||||
|
||||
- [ ] README instructions reproduce successful checkout flow in Stripe test mode
|
||||
- [ ] `.env.example` allows quick setup with test keys
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- Unit tests for configuration loader, Stripe client wrapper, and HTTP handlers (`config`, `internal/stripe`, `internal/web`).
|
||||
- Integration-style handler tests using mocks to simulate Stripe responses.
|
||||
- Manual validation of end-to-end flow with Stripe test keys in browser.
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- Single product flow with minimal traffic; standard `net/http` defaults suffice. Ensure Stripe client is reused to avoid unnecessary overhead.
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- No existing users; deployment is greenfield. Ensure new directories and files are committed together.
|
||||
|
||||
## References
|
||||
|
||||
- Research: `thoughts/shared/research/2025-09-27-stripe-integration-demo.md`
|
||||
- Guidelines: `AGENTS.md`
|
||||
- Ticket (README context): `README.md`
|
||||
121
thoughts/shared/research/2025-09-27-stripe-integration-demo.md
Normal file
121
thoughts/shared/research/2025-09-27-stripe-integration-demo.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
---
|
||||
date: 2025-09-27T01:47:51+02:00
|
||||
researcher: Codex
|
||||
git_commit: 810a49aa4ef6e6b21cb88a6da0e8693e22cf5ae0
|
||||
branch: main
|
||||
repository: payit
|
||||
topic: "Stripe integration demo architecture"
|
||||
tags: [research, codebase, stripe, go]
|
||||
status: complete
|
||||
last_updated: 2025-09-27
|
||||
last_updated_by: Codex
|
||||
last_updated_note: Merged initial planning notes into research doc
|
||||
---
|
||||
|
||||
# Research: Stripe integration demo architecture
|
||||
|
||||
## Research Question
|
||||
|
||||
How should we implement a Go-first web demo that lets users purchase a fake product
|
||||
via Stripe in one click without adopting a frontend framework?
|
||||
|
||||
## Summary
|
||||
|
||||
A minimalist Stripe Checkout Session flow fits the project goals: serve a static
|
||||
landing page from Go, expose a `/api/checkout` endpoint that creates sessions with the Stripe Go SDK, and redirect users to Stripe-hosted checkout for payment. Organize code following the guidelines in `AGENTS.md`, isolating Stripe logic under `internal/stripe`, HTTP handlers under `internal/web`, and using `cmd/payit/main.go` for bootstrapping. Local configuration should rely on environment variables (e.g., `.env.local`) to avoid embedding secrets. Automated tests can cover session creation and configuration handling with mocked Stripe clients.
|
||||
|
||||
## Detailed Findings
|
||||
|
||||
### Project Framing
|
||||
|
||||
- `README.md:1-8` states the app is a Stripe integration demo featuring one-time payments, so focusing on Checkout Sessions aligns with stated goals.
|
||||
- `AGENTS.md:5-9` prescribes directory boundaries (`cmd/`, `internal/payments`, `internal/stripe`, `internal/web`, `web/templates`, `web/static`), providing a scaffold for new components.
|
||||
|
||||
### Backend HTTP Server
|
||||
|
||||
- Proposed entrypoint: `cmd/payit/main.go` spinning up `net/http` with routes `/` (serve landing page) and `/api/checkout` (POST to create session). Use `http.FileServer` for static assets and a custom handler for the API.
|
||||
- `internal/web/server.go` (new) can construct the router, inject Stripe services, and encapsulate middleware (logging, recovery).
|
||||
|
||||
### Stripe Integration Layer
|
||||
|
||||
- `internal/stripe/client.go` (new) should wrap the official Stripe Go SDK, exposing `CreateCheckoutSession(ctx, params)` to keep handlers lightweight.
|
||||
- Use environment variable `PAYIT_STRIPE_SECRET_KEY` for authentication; load via `os.LookupEnv` and fail fast if missing.
|
||||
|
||||
### Static Frontend Assets
|
||||
|
||||
- Store landing page at `web/templates/index.html` with a simple product card and a “Buy Now” button wired to POST JSON to `/api/checkout` via `fetch`.
|
||||
- Serve minimal styling and JS from `web/static/` to keep the Go server responsible for asset delivery without frameworks.
|
||||
|
||||
### Configuration & Secrets
|
||||
|
||||
- Follow `AGENTS.md:17-18,26-27` guidance: run `go fmt`/`goimports`, centralize config under `config/config.go`, and never commit Stripe secrets. Support `.env.local` loading via a small helper (e.g., `github.com/joho/godotenv`) if allowed.
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
- Create table-driven tests under `internal/stripe/client_test.go` to validate request payloads, stubbing Stripe client calls.
|
||||
- Integration-style tests in `internal/web/server_test.go` can hit the `/api/checkout` handler using `httptest` with a fake Stripe service implementation.
|
||||
|
||||
## Code References
|
||||
|
||||
- `README.md:1-8` – Declares project purpose as a Stripe integration demo.
|
||||
- `AGENTS.md:5-27` – Defines intended project layout, coding style, and security practices.
|
||||
|
||||
## Architecture Insights
|
||||
|
||||
- Adopt layered structure: handler → service (`internal/stripe`) → external Stripe API, decoupled through interfaces for testing.
|
||||
- Prefer Stripe Checkout Sessions over custom payment intents to minimize client-side complexity while delivering real Stripe UX.
|
||||
- Expose configuration via constructor parameters so services are testable without relying on global state.
|
||||
|
||||
## Planning Notes (2025-02-14)
|
||||
|
||||
### Objectives
|
||||
|
||||
- Understand user goal: Go-powered web page offering single-click Stripe checkout for a demo product without a frontend framework.
|
||||
- Identify necessary backend components (HTTP server, Stripe client, config management).
|
||||
- Determine minimal frontend assets required (static HTML/CSS/JS) and how to integrate Stripe Checkout or Payment Links.
|
||||
- Outline testing, environment, and deployment considerations specific to this repository.
|
||||
|
||||
### Key Questions
|
||||
|
||||
1. What project structure best supports a Go-centric implementation while keeping room for future growth?
|
||||
2. Which Stripe integration approach (Checkout Sessions vs. Payment Elements) balances simplicity with demo fidelity?
|
||||
3. How should environment configuration and secret management be handled for local development?
|
||||
4. What testing strategy ensures confidence without overcomplicating the demo?
|
||||
5. Are there existing docs or notes in `./thoughts/` that can inform architectural decisions?
|
||||
|
||||
### Planned Research Tasks
|
||||
|
||||
- **Repo Survey**: Confirm current files and identify gaps for server, handlers, static assets.
|
||||
- **Stripe Flow Analysis**: Compare Checkout Session vs. Payment Intent flows for a quick demo.
|
||||
- **Go HTTP Server Design**: Sketch routing, handler responsibilities, and integration points.
|
||||
- **Static Asset Strategy**: Determine minimal HTML/CSS/JS structure without frameworks.
|
||||
- **Configuration & Secrets**: Document use of `.env` or config package for API keys.
|
||||
- **Testing Considerations**: Identify unit/integration tests and Stripe mocking options.
|
||||
- **Prior Research Review**: Scan `./thoughts/` for relevant history (none yet, but confirm).
|
||||
|
||||
### Parallel Task Outline
|
||||
|
||||
- Task A: Analyze current repo layout and any existing guidelines (AGENTS.md).
|
||||
- Task B: Research Stripe official guidance for Go integration (if needed from prior knowledge; no external fetch unless requested).
|
||||
- Task C: Design server architecture and endpoints handling Checkout session creation.
|
||||
- Task D: Plan static frontend assets and their interaction with the Go backend.
|
||||
- Task E: Define testing and configuration practices referencing Go ecosystem tools.
|
||||
|
||||
### Deliverables
|
||||
|
||||
- Comprehensive research document stored under `thoughts/shared/research/` following required template.
|
||||
- High-level summary for user with key file/path references and recommended next steps.
|
||||
|
||||
## Historical Context (from ./thoughts/)
|
||||
|
||||
- `thoughts/2025-02-14-stripe-demo-plan.md` – Original research plan detailing objectives, tasks, and integration questions.
|
||||
|
||||
## Related Research
|
||||
|
||||
- None yet; this is the first entry in `thoughts/shared/research/`.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- `hack/spec_metadata.sh` is missing, so metadata gathering requires manual commands; consider adding the script for future research compliance.
|
||||
- Decide whether to embed a mock pricing catalog or keep a single hard-coded product configuration for the demo.
|
||||
- Evaluate if webhooks are necessary for the demo or if redirect-based success pages suffice.
|
||||
Loading…
Reference in a new issue