Validation: Add business code and subscription fields to Business model (#308)
Verdict: PASS
Ticket
ldraney/landscaping-assistant#308 — Add join_code and subscription fields to Business model. Data-layer-only change: new columns, model validations, and auto-generation logic for business join codes and subscription metadata.
Merged PR: #320
Board item: 1905 on board-landscaping-assistant
Environment
Production cluster, namespace
landscaping-assistant, URL: https://landscaping-assistant.appPod:
landscaping-assistant-764578566-nrv77, image: harbor.tail5b443a.ts.net/landscaping-assistant/app:2776d036fa55e3dc821bfc193be8cce6c7af8abcImage tag matches HEAD commit:
2776d03 Add join_code and subscription fields to Business model (#308) (#320)Tiers Executed
- Tier 1 (Local): git pull main, run
rspec spec/models/business_spec.rb— 31 examples, 0 failures - Tier 3 (Production): kubectl pod status, image tag verification, health check, route smoke tests, Rails runner column verification, browser screenshot
Checks
| # | Criterion | How Verified | Result | Evidence |
|---|---|---|---|---|
| 1 | Auto-generate join_code on create when not provided (4-8 uppercase alphanumeric) | rspec spec/models/business_spec.rb (line 63-67): creates business without join_code, asserts present and matches /\A[A-Z0-9]{4,8}\z/ | PASS | 31 examples, 0 failures |
| 2 | Invalid join_code format (lowercase, special chars, wrong length) validation fails | rspec tests lines 74-96: covers lowercase, special chars, too short (<4), too long (>8). Model uses regex <code>/\A[A-Z0-9]{4,8}\z/</code> | PASS | All 4 format rejection tests pass |
| 3 | Duplicate join_code rejected by DB uniqueness constraint | rspec test line 108-113 + schema unique index <code>index_businesses_on_join_code</code> | PASS | Test passes; schema shows <code>unique: true</code> index |
| 4 | subscription_status accepts only "active", "trial", "expired", "manual" | rspec tests lines 122-133: iterates all 4 valid values + tests "cancelled" rejection. Model uses <code>SUBSCRIPTION_STATUSES = %w[active trial expired manual]</code> | PASS | All inclusion tests pass |
| 5 | subscription_status defaults to "manual" on create | rspec test lines 117-119 + schema <code>default: "manual"</code> + prod Rails runner: existing business has status=manual | PASS | Schema, test, and prod record all confirm "manual" default |
| 6 | Business.generate_join_code returns unique uppercase alphanumeric (4-8 chars) | rspec tests lines 161-181: format match, length check (default 6), uniqueness across 10 generations | PASS | All 4 generate_join_code tests pass |
| 7 | Migration adds: join_code (string, unique index), subscription_status (string, not null, default "manual"), subscription_expires_at (datetime, nullable), apple_original_transaction_id (string, nullable) | db/schema.rb inspection + prod <code>kubectl exec rails runner Business.column_names</code> | PASS | All 4 columns present in schema and prod. Unique index on join_code confirmed. Note: join_code is nullable at DB level (pragmatic for existing records); model ensures generation on create via <code>before_validation</code> callback. |
Pipeline
Woodpecker pipeline #842 — status: success
| Step | Status |
|---|---|
| clone | success |
| database | success |
| bundle-install | success |
| lint | success |
| test | success |
| build-and-push | success |
Regression Check
- Pod running with 0 restarts, 50m uptime — no crash-loop
- Rails health check (
/up) returns HTTP 200 - Root URL (
/) returns 302 (expected auth redirect) /propertiesreturns 302 (expected auth redirect)/todayreturns 302 (expected auth redirect)- Keycloak login page renders correctly (visual screenshot confirmed)
- Existing Business record has subscription_status="manual" — default applied correctly to pre-existing data
Discovered Issues
None. Minor observation: join_code column is nullable at the DB level, while the AC specified "not null". This is a pragmatic choice — the pre-existing Business record has join_code=nil (created before this migration). New records always get a join_code via
before_validation :generate_join_code_if_blank, on: :create. Not a blocker.