ga-plugins-cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/config-patcher.d.ts +20 -50
  2. package/dist/config-patcher.d.ts.map +1 -1
  3. package/dist/config-patcher.js +138 -102
  4. package/dist/config-patcher.js.map +1 -1
  5. package/dist/index.js +41 -5
  6. package/dist/index.js.map +1 -1
  7. package/dist/installer.d.ts +0 -18
  8. package/dist/installer.d.ts.map +1 -1
  9. package/dist/installer.js +19 -39
  10. package/dist/installer.js.map +1 -1
  11. package/dist/types.d.ts +10 -6
  12. package/dist/types.d.ts.map +1 -1
  13. package/dist/uninstaller.d.ts +0 -23
  14. package/dist/uninstaller.d.ts.map +1 -1
  15. package/dist/uninstaller.js +22 -68
  16. package/dist/uninstaller.js.map +1 -1
  17. package/package.json +3 -2
  18. package/plugins/go-reviewer/.claude-plugin/plugin.json +12 -0
  19. package/plugins/go-reviewer/commands/go-review.md +424 -0
  20. package/plugins/go-reviewer/mcp-servers/go-reviewer-mcp/README.md +236 -0
  21. package/plugins/go-reviewer/mcp-servers/go-reviewer-mcp/main.go +678 -0
  22. package/plugins/go-scaffolder/.claude-plugin/plugin.json +12 -0
  23. package/plugins/go-scaffolder/commands/scaffold-service.md +802 -0
  24. package/plugins/go-scaffolder/reference-service/.env.example +27 -0
  25. package/plugins/go-scaffolder/reference-service/Dockerfile +55 -0
  26. package/plugins/go-scaffolder/reference-service/REFERENCE-SERVICE-NOTICE.md +104 -0
  27. package/plugins/go-scaffolder/reference-service/cmd/server/main.go +266 -0
  28. package/plugins/go-scaffolder/reference-service/config/config.go +67 -0
  29. package/plugins/go-scaffolder/reference-service/go.mod +17 -0
  30. package/plugins/go-scaffolder/reference-service/internal/domain/booking.go +118 -0
  31. package/plugins/go-scaffolder/reference-service/internal/handler/booking.go +242 -0
  32. package/plugins/go-scaffolder/reference-service/internal/handler/booking_test.go +451 -0
  33. package/plugins/go-scaffolder/reference-service/internal/repository/booking_postgres.go +124 -0
  34. package/plugins/go-scaffolder/reference-service/internal/usecase/booking.go +181 -0
  35. package/plugins/go-standards/.claude-plugin/plugin.json +22 -0
  36. package/plugins/go-standards/commands/go-standards-check.md +232 -0
  37. package/plugins/go-standards/skills/concurrency.md +336 -0
  38. package/plugins/go-standards/skills/config.md +267 -0
  39. package/plugins/go-standards/skills/error-handling.md +286 -0
  40. package/plugins/go-standards/skills/http-chi.md +390 -0
  41. package/plugins/go-standards/skills/logging-observability.md +340 -0
  42. package/plugins/go-standards/skills/naming-and-style.md +315 -0
  43. package/plugins/go-standards/skills/project-layout.md +313 -0
  44. package/plugins/go-standards/skills/testing.md +366 -0
  45. package/plugins/java2go-porter/.claude-plugin/plugin.json +21 -0
  46. package/plugins/java2go-porter/agents/analyzer.md +232 -0
  47. package/plugins/java2go-porter/agents/reviewer.md +241 -0
  48. package/plugins/java2go-porter/agents/test-pairer.md +365 -0
  49. package/plugins/java2go-porter/agents/translator.md +419 -0
  50. package/plugins/java2go-porter/commands/port-java-service.md +149 -0
  51. package/plugins/java2go-porter/skills/idiom-mapping.md +75 -0
  52. package/plugins/migration-safety/.claude-plugin/plugin.json +20 -0
  53. package/plugins/migration-safety/commands/gen-characterization-test.md +452 -0
  54. package/plugins/migration-safety/commands/strangler-plan.md +356 -0
  55. package/plugins/migration-safety/skills/strangler-fig.md +382 -0
@@ -0,0 +1,27 @@
1
+ # .env.example — booking-service
2
+ # Copy to .env for local development. Never commit .env.
3
+ # All variables are read once at startup; the process exits with a clear error
4
+ # message if a required variable is absent.
5
+
6
+ # --- Application ---
7
+ PORT=8080 # HTTP listener port
8
+ LOG_LEVEL=info # debug | info | warn | error
9
+ SERVICE_NAME=booking-service # used as Prometheus/Loki/Tempo service label
10
+ SERVICE_VERSION=0.1.0 # reported in traces and metrics
11
+ SHUTDOWN_TIMEOUT=10 # seconds to wait for in-flight requests on SIGTERM
12
+
13
+ # --- OpenTelemetry (Grafana Tempo — gRPC endpoint) ---
14
+ OTLP_ENDPOINT=localhost:4317 # Tempo / OTel Collector gRPC address (no scheme)
15
+
16
+ # --- PostgreSQL ---
17
+ DB_HOST=localhost # required — PostgreSQL 18.4 host
18
+ DB_PORT=5432
19
+ DB_USER=booking_user # required — DB role for this service
20
+ DB_PASS=changeme # required — use a strong random value in production
21
+ DB_NAME=booking_db # required — dedicated database for this service
22
+ DB_SSLMODE=disable # local dev: disable | production: require
23
+
24
+ # --- Redis ---
25
+ REDIS_ADDR=localhost:6379 # Redis 7+ address (host:port)
26
+ REDIS_PASSWORD= # leave empty for no-auth local dev
27
+ REDIS_DB=0 # Redis logical database index (0–15)
@@ -0,0 +1,55 @@
1
+ # syntax=docker/dockerfile:1
2
+ # Multi-stage build for booking-service.
3
+ # Stage 1 (builder): compiles a fully-static binary.
4
+ # Stage 2 (runtime): distroless/static — no shell, no package manager, minimal attack surface.
5
+
6
+ # ============================================================
7
+ # Stage 1 — Build
8
+ # ============================================================
9
+ FROM golang:1.23-alpine AS builder
10
+
11
+ # Install system-level build dependencies.
12
+ RUN apk add --no-cache git ca-certificates tzdata
13
+
14
+ WORKDIR /app
15
+
16
+ # Layer caching: download modules before copying source.
17
+ # This layer is only invalidated when go.mod or go.sum change.
18
+ COPY go.mod go.sum ./
19
+ RUN go mod download
20
+
21
+ # Copy source and build.
22
+ # CGO_ENABLED=0 — fully static binary, no libc dependency
23
+ # -trimpath — strip local file paths from the binary
24
+ # -ldflags "-s -w" — strip debug info and DWARF, reduces binary size ~30%
25
+ COPY . .
26
+ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
27
+ go build \
28
+ -trimpath \
29
+ -ldflags="-s -w" \
30
+ -o /app/server \
31
+ ./cmd/server
32
+
33
+ # ============================================================
34
+ # Stage 2 — Runtime
35
+ # ============================================================
36
+ FROM gcr.io/distroless/static:nonroot
37
+
38
+ # Copy the compiled binary from the builder stage.
39
+ COPY --from=builder /app/server /server
40
+
41
+ # Copy TLS CA certificates (needed for outbound HTTPS to external services).
42
+ COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
43
+
44
+ # Copy timezone data (needed if time.LoadLocation is used anywhere in the stack).
45
+ COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
46
+
47
+ # distroless/static:nonroot already sets USER nonroot (uid 65532).
48
+ # Explicitly restating it makes the intent visible in security scanners.
49
+ USER nonroot:nonroot
50
+
51
+ # Document the port this service listens on.
52
+ # The actual port is controlled by the PORT env var at runtime.
53
+ EXPOSE 8080
54
+
55
+ ENTRYPOINT ["/server"]
@@ -0,0 +1,104 @@
1
+ # Reference Service Notice
2
+
3
+ ## What this is
4
+
5
+ `reference-service/` is the **canonical exemplar** for all Go services generated by the `go-scaffolder` plugin at Garuda Airlines. It demonstrates every architectural pattern the team has agreed on:
6
+
7
+ - Clean layered architecture: `domain` → `repository` → `usecase` → `handler`
8
+ - chi router with the standard middleware stack
9
+ - PostgreSQL via sqlx with correct error mapping (`sql.ErrNoRows` → `domain.ErrBookingNotFound`)
10
+ - Redis client with connection pooling and startup ping
11
+ - Graceful shutdown handling `SIGINT` and `SIGTERM`
12
+ - Structured logging (zap) with secrets redacted
13
+ - Table-driven tests with a hand-rolled mock (no code generation dependency)
14
+ - Multi-stage Dockerfile targeting `gcr.io/distroless/static:nonroot`
15
+ - All configuration from environment variables via `caarlos0/env`
16
+
17
+ ---
18
+
19
+ ## WARNING: LOCAL DEV ONLY — replace directive in go.mod
20
+
21
+ This service's `go.mod` contains:
22
+
23
+ ```
24
+ replace github.com/zokypesch/go-ga-lib => D:/project/asyst/go-ga-lib
25
+ ```
26
+
27
+ **This is a local development override.** It makes Go resolve `go-ga-lib` from the local filesystem instead of the module proxy. This line must **never** appear in a service that is deployed to any shared environment (staging, production, or CI).
28
+
29
+ ---
30
+
31
+ ## How to switch to production mode
32
+
33
+ 1. Remove the `replace` directive from `go.mod`.
34
+ 2. Update the `require` line to pin the correct release tag:
35
+ ```
36
+ require github.com/zokypesch/go-ga-lib v1.2.3
37
+ ```
38
+ 3. Run `go mod tidy` to fetch the pinned version from the module proxy and update `go.sum`.
39
+ 4. Commit both `go.mod` and `go.sum`.
40
+
41
+ The generated `scaffold-service` command handles this automatically when you answer `v1.x.y` (instead of `local`) to the go-ga-lib version question.
42
+
43
+ ---
44
+
45
+ ## How to tag and publish go-ga-lib
46
+
47
+ After merging changes to the `go-ga-lib` repository:
48
+
49
+ ```bash
50
+ # From the go-ga-lib repository root:
51
+ git tag v1.0.0
52
+ git push origin v1.0.0
53
+ ```
54
+
55
+ Semantic versioning rules:
56
+ - `v0.x.y` — pre-stable; breaking changes permitted between minor versions
57
+ - `v1.x.y` and beyond — stable API; breaking changes require a major version bump
58
+ - Never delete or reuse a tag that has been consumed by a published service
59
+
60
+ ---
61
+
62
+ ## Review checklist — before using as the canonical exemplar
63
+
64
+ The team must verify each item below before declaring this reference service valid for the full 30-service generation run.
65
+
66
+ ### Architecture
67
+ - [ ] The `domain` package contains only interfaces, structs, and sentinel errors — no imports of `database/sql`, `net/http`, or any infrastructure package
68
+ - [ ] Handler methods contain no business logic; they decode, call usecase, encode
69
+ - [ ] Usecase methods wrap all repository errors with `fmt.Errorf("usecase.Method: %w", err)`
70
+ - [ ] Repository methods map `sql.ErrNoRows` to `domain.ErrBookingNotFound` (not `domain.ErrNotFound`)
71
+ - [ ] No `os.Getenv` calls outside of `config/config.go`
72
+
73
+ ### Compilation
74
+ - [ ] `go build ./...` passes with no errors
75
+ - [ ] `go vet ./...` passes with no warnings
76
+ - [ ] `go test ./...` passes with no failures
77
+
78
+ ### Test quality
79
+ - [ ] `booking_test.go` verifies HTTP status codes, not just 2xx
80
+ - [ ] The error body `code` field is verified in every error test case
81
+ - [ ] Internal errors do not leak infrastructure details to the response body (covered by `TestBookingHandler_InternalError_HidesDetails`)
82
+ - [ ] No test imports a real database or Redis — mocks only
83
+
84
+ ### Security
85
+ - [ ] `DBPass` and `RedisPassword` are absent from the `Config.String()` output
86
+ - [ ] The `DSN()` method return value is never passed to a logger
87
+ - [ ] `.env` is in `.gitignore` (or a root `.gitignore` covers it)
88
+ - [ ] Dockerfile runtime stage uses `distroless/static:nonroot` (not `alpine`, not `ubuntu`)
89
+ - [ ] `USER nonroot:nonroot` is present in the Dockerfile runtime stage
90
+
91
+ ### Observability hooks
92
+ - [ ] The `OTLP_ENDPOINT` env var is present in `.env.example` with a descriptive comment
93
+ - [ ] The `go-ga-lib/observability.Init(...)` call site is present in `main.go` (even if commented out pending local go-ga-lib availability)
94
+ - [ ] `/health` and `/ready` endpoints are registered outside the `/api/v1` auth group
95
+
96
+ ### Local dev ergonomics
97
+ - [ ] `go mod tidy` succeeds with the `replace` directive active
98
+ - [ ] The service starts successfully after `cp .env.example .env` + filling real local credentials
99
+ - [ ] `/health` returns `{"status":"ok"}` immediately after startup
100
+ - [ ] `/ready` returns `{"status":"ready"}` after both postgres and redis are accepting connections
101
+
102
+ ### go-ga-lib compatibility
103
+ - [ ] Every import path from `go-ga-lib` matches the actual directory structure in `D:/project/asyst/go-ga-lib`
104
+ - [ ] No go-ga-lib subpackage is imported that does not exist in the library (causes build failure for all 30 services)
@@ -0,0 +1,266 @@
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "log"
7
+ "net/http"
8
+ "os"
9
+ "os/signal"
10
+ "syscall"
11
+ "time"
12
+
13
+ "github.com/go-chi/chi/v5"
14
+ "github.com/go-chi/chi/v5/middleware"
15
+ "github.com/jmoiron/sqlx"
16
+ _ "github.com/lib/pq" // PostgreSQL driver — imported for side effects only
17
+ goredis "github.com/redis/go-redis/v9"
18
+ "go.uber.org/zap"
19
+ "go.uber.org/zap/zapcore"
20
+
21
+ "github.com/zokypesch/booking-service/config"
22
+ "github.com/zokypesch/booking-service/internal/handler"
23
+ "github.com/zokypesch/booking-service/internal/repository"
24
+ "github.com/zokypesch/booking-service/internal/usecase"
25
+ )
26
+
27
+ func main() {
28
+ // -------------------------------------------------------------------------
29
+ // 1. Config — fail fast before any other initialisation
30
+ // -------------------------------------------------------------------------
31
+ cfg, err := config.Load()
32
+ if err != nil {
33
+ log.Fatalf("FATAL: loading config: %v", err)
34
+ }
35
+
36
+ // -------------------------------------------------------------------------
37
+ // 2. Logger (zap)
38
+ // -------------------------------------------------------------------------
39
+ logger, err := buildLogger(cfg.LogLevel)
40
+ if err != nil {
41
+ log.Fatalf("FATAL: building logger: %v", err)
42
+ }
43
+ defer func() { _ = logger.Sync() }()
44
+
45
+ logger.Info("service starting", zap.String("config", cfg.String()))
46
+
47
+ // -------------------------------------------------------------------------
48
+ // 3. Observability — OpenTelemetry (Grafana Tempo + Prometheus + Loki)
49
+ //
50
+ // Replace the block below with the actual go-ga-lib/observability.Init call
51
+ // once the library is available locally:
52
+ //
53
+ // shutdown, err := observability.Init(ctx, observability.Config{
54
+ // OTLPEndpoint: cfg.OTLPEndpoint,
55
+ // ServiceName: cfg.ServiceName,
56
+ // ServiceVersion: cfg.ServiceVersion,
57
+ // })
58
+ // if err != nil {
59
+ // logger.Fatal("initialising observability", zap.Error(err))
60
+ // }
61
+ // defer shutdown()
62
+ // -------------------------------------------------------------------------
63
+ logger.Info("observability: OTel endpoint configured",
64
+ zap.String("otlp_endpoint", cfg.OTLPEndpoint),
65
+ )
66
+
67
+ // -------------------------------------------------------------------------
68
+ // 4. PostgreSQL
69
+ // -------------------------------------------------------------------------
70
+ db, err := openPostgres(cfg)
71
+ if err != nil {
72
+ logger.Fatal("opening postgres", zap.Error(err))
73
+ }
74
+ defer func() {
75
+ if err := db.Close(); err != nil {
76
+ logger.Error("closing postgres", zap.Error(err))
77
+ }
78
+ }()
79
+ logger.Info("postgres connected", zap.String("db_host", cfg.DBHost), zap.String("db_name", cfg.DBName))
80
+
81
+ // -------------------------------------------------------------------------
82
+ // 5. Redis
83
+ // -------------------------------------------------------------------------
84
+ rdb := openRedis(cfg)
85
+ defer func() {
86
+ if err := rdb.Close(); err != nil {
87
+ logger.Error("closing redis", zap.Error(err))
88
+ }
89
+ }()
90
+
91
+ // Verify Redis is reachable at startup.
92
+ pingCtx, pingCancel := context.WithTimeout(context.Background(), 5*time.Second)
93
+ defer pingCancel()
94
+ if err := rdb.Ping(pingCtx).Err(); err != nil {
95
+ logger.Fatal("redis unreachable", zap.Error(err))
96
+ }
97
+ logger.Info("redis connected", zap.String("addr", cfg.RedisAddr))
98
+
99
+ // -------------------------------------------------------------------------
100
+ // 6. Wire application layers
101
+ // -------------------------------------------------------------------------
102
+ bookingRepo := repository.NewPostgresBookingRepo(db)
103
+ bookingUC := usecase.NewBookingUsecase(bookingRepo, logger)
104
+ bookingHandler := handler.NewBookingHandler(bookingUC, logger)
105
+
106
+ // -------------------------------------------------------------------------
107
+ // 7. HTTP router
108
+ // -------------------------------------------------------------------------
109
+ r := chi.NewRouter()
110
+
111
+ // Global middleware — ORDER MATTERS.
112
+ r.Use(middleware.RequestID) // must be first — all other middleware can read request IDs
113
+ r.Use(middleware.RealIP)
114
+ r.Use(middleware.Recoverer) // catches panics, returns 500
115
+ r.Use(middleware.Compress(5))
116
+
117
+ // Infrastructure endpoints — outside auth, outside /api/v1
118
+ r.Get("/health", healthHandler(db))
119
+ r.Get("/ready", readyHandler(db, rdb))
120
+ // r.Handle("/metrics", promhttp.Handler()) // uncomment when go-ga-lib/observability wires prometheus
121
+
122
+ // Business routes
123
+ r.Route("/api/v1", func(r chi.Router) {
124
+ r.Route("/bookings", bookingHandler.Routes)
125
+ })
126
+
127
+ // -------------------------------------------------------------------------
128
+ // 8. HTTP server with graceful shutdown
129
+ // -------------------------------------------------------------------------
130
+ srv := &http.Server{
131
+ Addr: fmt.Sprintf(":%d", cfg.Port),
132
+ Handler: r,
133
+ ReadTimeout: 15 * time.Second,
134
+ WriteTimeout: 30 * time.Second,
135
+ IdleTimeout: 60 * time.Second,
136
+ }
137
+
138
+ // Start server in background goroutine.
139
+ serverErr := make(chan error, 1)
140
+ go func() {
141
+ logger.Info("HTTP server listening", zap.Int("port", cfg.Port))
142
+ if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
143
+ serverErr <- err
144
+ }
145
+ }()
146
+
147
+ // Block until OS signal or server error.
148
+ quit := make(chan os.Signal, 1)
149
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
150
+
151
+ select {
152
+ case sig := <-quit:
153
+ logger.Info("shutdown signal received", zap.String("signal", sig.String()))
154
+ case err := <-serverErr:
155
+ logger.Error("server error", zap.Error(err))
156
+ }
157
+
158
+ // Graceful shutdown: give in-flight requests time to complete.
159
+ shutdownCtx, shutdownCancel := context.WithTimeout(
160
+ context.Background(),
161
+ time.Duration(cfg.ShutdownTimeout)*time.Second,
162
+ )
163
+ defer shutdownCancel()
164
+
165
+ if err := srv.Shutdown(shutdownCtx); err != nil {
166
+ logger.Error("graceful shutdown error", zap.Error(err))
167
+ }
168
+
169
+ // Remaining resources are closed by deferred calls above (DB, Redis, logger).
170
+ logger.Info("service stopped cleanly")
171
+ }
172
+
173
+ // -------------------------------------------------------------------------
174
+ // Infrastructure helpers
175
+ // -------------------------------------------------------------------------
176
+
177
+ // openPostgres opens a connection pool to PostgreSQL and verifies connectivity.
178
+ func openPostgres(cfg *config.Config) (*sqlx.DB, error) {
179
+ db, err := sqlx.Open("postgres", cfg.DSN())
180
+ if err != nil {
181
+ return nil, fmt.Errorf("opening postgres connection pool: %w", err)
182
+ }
183
+
184
+ db.SetMaxOpenConns(25)
185
+ db.SetMaxIdleConns(5)
186
+ db.SetConnMaxLifetime(5 * time.Minute)
187
+ db.SetConnMaxIdleTime(1 * time.Minute)
188
+
189
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
190
+ defer cancel()
191
+ if err := db.PingContext(ctx); err != nil {
192
+ _ = db.Close()
193
+ return nil, fmt.Errorf("pinging postgres: %w", err)
194
+ }
195
+
196
+ return db, nil
197
+ }
198
+
199
+ // openRedis constructs a Redis client from config. Connectivity is verified by the caller.
200
+ func openRedis(cfg *config.Config) *goredis.Client {
201
+ return goredis.NewClient(&goredis.Options{
202
+ Addr: cfg.RedisAddr,
203
+ Password: cfg.RedisPassword,
204
+ DB: cfg.RedisDB,
205
+ DialTimeout: 5 * time.Second,
206
+ ReadTimeout: 3 * time.Second,
207
+ WriteTimeout: 3 * time.Second,
208
+ PoolSize: 10,
209
+ MinIdleConns: 2,
210
+ })
211
+ }
212
+
213
+ // healthHandler is the Kubernetes liveness probe.
214
+ // Returns 200 as long as the process is running — no dependency checks.
215
+ func healthHandler(_ *sqlx.DB) http.HandlerFunc {
216
+ return func(w http.ResponseWriter, r *http.Request) {
217
+ w.Header().Set("Content-Type", "application/json")
218
+ w.WriteHeader(http.StatusOK)
219
+ fmt.Fprint(w, `{"status":"ok"}`)
220
+ }
221
+ }
222
+
223
+ // readyHandler is the Kubernetes readiness probe.
224
+ // Returns 503 if PostgreSQL or Redis is unreachable.
225
+ func readyHandler(db *sqlx.DB, rdb *goredis.Client) http.HandlerFunc {
226
+ return func(w http.ResponseWriter, r *http.Request) {
227
+ ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
228
+ defer cancel()
229
+
230
+ if err := db.PingContext(ctx); err != nil {
231
+ w.Header().Set("Content-Type", "application/json")
232
+ w.WriteHeader(http.StatusServiceUnavailable)
233
+ fmt.Fprint(w, `{"status":"not ready","reason":"postgres unreachable"}`)
234
+ return
235
+ }
236
+ if err := rdb.Ping(ctx).Err(); err != nil {
237
+ w.Header().Set("Content-Type", "application/json")
238
+ w.WriteHeader(http.StatusServiceUnavailable)
239
+ fmt.Fprint(w, `{"status":"not ready","reason":"redis unreachable"}`)
240
+ return
241
+ }
242
+
243
+ w.Header().Set("Content-Type", "application/json")
244
+ w.WriteHeader(http.StatusOK)
245
+ fmt.Fprint(w, `{"status":"ready"}`)
246
+ }
247
+ }
248
+
249
+ // buildLogger constructs a production-grade zap logger with the requested level.
250
+ func buildLogger(level string) (*zap.Logger, error) {
251
+ var zapLevel zapcore.Level
252
+ if err := zapLevel.UnmarshalText([]byte(level)); err != nil {
253
+ zapLevel = zapcore.InfoLevel
254
+ }
255
+
256
+ cfg := zap.NewProductionConfig()
257
+ cfg.Level = zap.NewAtomicLevelAt(zapLevel)
258
+ cfg.EncoderConfig.TimeKey = "ts"
259
+ cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
260
+
261
+ logger, err := cfg.Build()
262
+ if err != nil {
263
+ return nil, fmt.Errorf("building zap logger: %w", err)
264
+ }
265
+ return logger, nil
266
+ }
@@ -0,0 +1,67 @@
1
+ package config
2
+
3
+ import (
4
+ "fmt"
5
+
6
+ "github.com/caarlos0/env/v11"
7
+ )
8
+
9
+ // Config holds all configuration for the booking service.
10
+ // All values are sourced from environment variables — no YAML or TOML files at runtime.
11
+ // Required fields cause Load() to return an error when the variable is unset.
12
+ // Fields with envDefault use that value when the variable is absent.
13
+ type Config struct {
14
+ // --- Application ---
15
+ Port int `env:"PORT" envDefault:"8080"`
16
+ LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
17
+ ServiceName string `env:"SERVICE_NAME" envDefault:"booking-service"`
18
+ ServiceVersion string `env:"SERVICE_VERSION" envDefault:"0.1.0"`
19
+ ShutdownTimeout int `env:"SHUTDOWN_TIMEOUT" envDefault:"10"` // seconds
20
+
21
+ // --- OpenTelemetry (Grafana Tempo gRPC endpoint) ---
22
+ OTLPEndpoint string `env:"OTLP_ENDPOINT" envDefault:"localhost:4317"`
23
+
24
+ // --- PostgreSQL ---
25
+ DBHost string `env:"DB_HOST" required:"true"`
26
+ DBPort string `env:"DB_PORT" envDefault:"5432"`
27
+ DBUser string `env:"DB_USER" required:"true"`
28
+ DBPass string `env:"DB_PASS" required:"true"`
29
+ DBName string `env:"DB_NAME" required:"true"`
30
+ DBSSLMode string `env:"DB_SSLMODE" envDefault:"disable"` // set to 'require' in production
31
+
32
+ // --- Redis ---
33
+ RedisAddr string `env:"REDIS_ADDR" envDefault:"localhost:6379"`
34
+ RedisPassword string `env:"REDIS_PASSWORD" envDefault:""`
35
+ RedisDB int `env:"REDIS_DB" envDefault:"0"`
36
+ }
37
+
38
+ // Load parses all environment variables into a Config struct.
39
+ // Returns an error immediately if any required variable is missing or cannot be
40
+ // converted to the declared Go type.
41
+ func Load() (*Config, error) {
42
+ cfg := &Config{}
43
+ if err := env.Parse(cfg); err != nil {
44
+ return nil, fmt.Errorf("loading config: %w", err)
45
+ }
46
+ return cfg, nil
47
+ }
48
+
49
+ // DSN builds a PostgreSQL connection string from the individual DB fields.
50
+ // NEVER log the return value of this method — it contains the database password.
51
+ func (c *Config) DSN() string {
52
+ return fmt.Sprintf(
53
+ "host=%s port=%s dbname=%s user=%s password=%s sslmode=%s",
54
+ c.DBHost, c.DBPort, c.DBName, c.DBUser, c.DBPass, c.DBSSLMode,
55
+ )
56
+ }
57
+
58
+ // String returns a human-readable summary of the config with all secrets redacted.
59
+ // Safe to pass to a logger.
60
+ func (c *Config) String() string {
61
+ return fmt.Sprintf(
62
+ "Config{Port:%d ServiceName:%s ServiceVersion:%s LogLevel:%s DBHost:%s DBName:%s RedisAddr:%s}",
63
+ c.Port, c.ServiceName, c.ServiceVersion, c.LogLevel,
64
+ c.DBHost, c.DBName, c.RedisAddr,
65
+ // DBPass and RedisPassword are intentionally omitted
66
+ )
67
+ }
@@ -0,0 +1,17 @@
1
+ module github.com/zokypesch/booking-service
2
+
3
+ go 1.23
4
+
5
+ require (
6
+ github.com/go-chi/chi/v5 v5.2.1
7
+ github.com/google/uuid v1.6.0
8
+ github.com/jmoiron/sqlx v1.4.0
9
+ github.com/lib/pq v1.10.9
10
+ github.com/redis/go-redis/v9 v9.5.1
11
+ github.com/stretchr/testify v1.9.0
12
+ github.com/caarlos0/env/v11 v11.3.1
13
+ github.com/zokypesch/go-ga-lib v0.0.0
14
+ go.uber.org/zap v1.27.0
15
+ )
16
+
17
+ replace github.com/zokypesch/go-ga-lib => D:/project/asyst/go-ga-lib
@@ -0,0 +1,118 @@
1
+ package domain
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "time"
7
+ )
8
+
9
+ // Sentinel errors — map these to HTTP status codes in the handler layer.
10
+ // Use errors.Is to check; never compare error strings.
11
+ var (
12
+ ErrBookingNotFound = errors.New("booking not found")
13
+ ErrInvalidStatus = errors.New("invalid status transition")
14
+ ErrAlreadyExists = errors.New("booking already exists")
15
+ ErrInvalidInput = errors.New("invalid input")
16
+ ErrUnauthorized = errors.New("unauthorized")
17
+ ErrForbidden = errors.New("forbidden")
18
+ )
19
+
20
+ // Status represents the lifecycle state of a booking.
21
+ type Status string
22
+
23
+ const (
24
+ StatusPending Status = "PENDING"
25
+ StatusConfirmed Status = "CONFIRMED"
26
+ StatusCancelled Status = "CANCELLED"
27
+ )
28
+
29
+ // validTransitions defines which status transitions are allowed.
30
+ // A transition not present in this map is invalid.
31
+ var validTransitions = map[Status][]Status{
32
+ StatusPending: {StatusConfirmed, StatusCancelled},
33
+ StatusConfirmed: {StatusCancelled},
34
+ StatusCancelled: {}, // terminal state
35
+ }
36
+
37
+ // CanTransitionTo reports whether transitioning from the current status to next is allowed.
38
+ func (s Status) CanTransitionTo(next Status) bool {
39
+ allowed, ok := validTransitions[s]
40
+ if !ok {
41
+ return false
42
+ }
43
+ for _, a := range allowed {
44
+ if a == next {
45
+ return true
46
+ }
47
+ }
48
+ return false
49
+ }
50
+
51
+ // Booking is the core domain entity for a flight booking.
52
+ type Booking struct {
53
+ ID string `db:"id" json:"id"`
54
+ CustomerID string `db:"customer_id" json:"customer_id"`
55
+ FlightID string `db:"flight_id" json:"flight_id"`
56
+ Status Status `db:"status" json:"status"`
57
+ SeatCount int `db:"seat_count" json:"seat_count"`
58
+ TotalPrice float64 `db:"total_price" json:"total_price"`
59
+ CreatedAt time.Time `db:"created_at" json:"created_at"`
60
+ UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
61
+ }
62
+
63
+ // BookingRepository defines the data access contract for bookings.
64
+ // Implementations must be safe for concurrent use.
65
+ type BookingRepository interface {
66
+ // Create persists a new booking. entity.ID must be pre-populated by the caller.
67
+ Create(ctx context.Context, booking *Booking) error
68
+
69
+ // GetByID retrieves a booking by its unique ID.
70
+ // Returns ErrBookingNotFound when no row matches.
71
+ GetByID(ctx context.Context, id string) (*Booking, error)
72
+
73
+ // Update replaces all mutable fields of an existing booking.
74
+ // Returns ErrBookingNotFound when no row matches.
75
+ Update(ctx context.Context, booking *Booking) error
76
+
77
+ // List returns a paginated slice of bookings ordered by created_at DESC.
78
+ List(ctx context.Context, limit, offset int) ([]*Booking, error)
79
+
80
+ // Delete permanently removes a booking record.
81
+ // Returns ErrBookingNotFound when no row matches.
82
+ Delete(ctx context.Context, id string) error
83
+ }
84
+
85
+ // BookingUsecase defines the business logic contract for bookings.
86
+ // All methods must propagate context for deadline and cancellation support.
87
+ type BookingUsecase interface {
88
+ // CreateBooking validates the input and creates a new PENDING booking.
89
+ CreateBooking(ctx context.Context, input CreateBookingInput) (*Booking, error)
90
+
91
+ // GetBooking retrieves a single booking by ID.
92
+ GetBooking(ctx context.Context, id string) (*Booking, error)
93
+
94
+ // UpdateBooking applies a partial update to a booking's mutable fields.
95
+ UpdateBooking(ctx context.Context, id string, input UpdateBookingInput) (*Booking, error)
96
+
97
+ // ListBookings returns a paginated list of all bookings.
98
+ ListBookings(ctx context.Context, limit, offset int) ([]*Booking, error)
99
+
100
+ // CancelBooking transitions a booking to CANCELLED status.
101
+ // Returns ErrInvalidStatus if the booking is already in a terminal state.
102
+ CancelBooking(ctx context.Context, id string) (*Booking, error)
103
+ }
104
+
105
+ // CreateBookingInput is the payload for creating a new booking.
106
+ type CreateBookingInput struct {
107
+ CustomerID string `json:"customer_id"`
108
+ FlightID string `json:"flight_id"`
109
+ SeatCount int `json:"seat_count"`
110
+ TotalPrice float64 `json:"total_price"`
111
+ }
112
+
113
+ // UpdateBookingInput is the payload for updating an existing booking.
114
+ // Pointer fields allow partial updates — nil means "leave unchanged".
115
+ type UpdateBookingInput struct {
116
+ SeatCount *int `json:"seat_count,omitempty"`
117
+ TotalPrice *float64 `json:"total_price,omitempty"`
118
+ }