Validation: Add business code and subscription fields to Business model (#308)

validation-308-2026-07-26 Doc

validation pass

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.app
Pod: landscaping-assistant-764578566-nrv77, image: harbor.tail5b443a.ts.net/landscaping-assistant/app:2776d036fa55e3dc821bfc193be8cce6c7af8abc
Image 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 (&lt;4), too long (&gt;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)
  • /properties returns 302 (expected auth redirect)
  • /today returns 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.