Add ServiceRequest model with status transition validation #178
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "122-service-request-model"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
monthly_priceto the Service model and updates seeds with pricing for Mowing, Weeding, and Edging & TrimmingChanges
db/migrate/20260608020000_add_monthly_price_to_services.rb: addsmonthly_price(decimal, precision 8, scale 2) column to services tabledb/migrate/20260608020001_create_service_requests.rb: creates service_requests table with FKs to properties and crew_members, indexes on status and request_typeapp/models/service_request.rb: new model with request_type/status inclusion validations, status transition validation, helper methods (transition_to, transition_to!, terminal?, allowed_transitions)app/models/property.rb: addshas_many :service_requests, dependent: :destroyapp/models/crew_member.rb: addshas_many :service_requests, dependent: :destroydb/seeds.rb: updates service seeds with monthly prices (Mowing $35, Weeding $25, Edging & Trimming $20)db/schema.rb: auto-updated by migrationspec/models/service_request_spec.rb: comprehensive specs covering validations, associations, all valid/invalid status transitions, terminal states, and helper methodsTest Plan
bundle exec rspec: 278 examples, 0 failuresReview Checklist
Related Notes
ldraney/landscaping-assistant #122— ServiceRequest model + migrationQA Review -- PR #178
Checklist
propertiesandcrew_memberswithnull: false, proper indexes onstatusandrequest_type, correct decimal precision forprice(10,2) andmonthly_price(8,2)VALID_TRANSITIONShash matches the spec exactly: requested->{quoted,declined}, quoted->{paid,declined}, paid->{scheduled}, scheduled->{completed}, completed/declined->terminalreturn if new_record?prevents false rejections when creating with a non-default statushas_many :service_requests, dependent: :destroyon both Property and CrewMemberfind_or_create_by!+update!transition_to/transition_to!/terminal?/allowed_transitionshelpersFindings
No blocking issues found. Code is clean, well-structured, and matches every acceptance criterion from issue #122.
Minor note (non-blocking):
transition_tosets the status before callingvalid?, so on failure the object retains the invalid status in memory. This is standard Rails dirty-tracking behavior and the tests verify it. Callers shouldreloadif they need to recover from a failedtransition_tocall. No action needed -- just documenting the design choice.VERDICT: APPROVE
PR #178 Review
DOMAIN REVIEW
Tech stack: Ruby on Rails 8.1, PostgreSQL, RSpec. Reviewed against Rails conventions, ActiveRecord migration best practices, and model validation patterns.
Migration correctness (2 migrations):
20260608020000_add_monthly_price_to_services.rb: Addsdecimal, precision: 8, scale: 2-- appropriate for monthly pricing (up to $999,999.99). Nullable is correct since existing services get backfilled via seeds. Clean.20260608020001_create_service_requests.rb: Well-structured.null: falseon bothpropertyandcrew_member-- correct, every request must belong to both.statushasnull: false, default: "requested"-- correct default for the initial state.priceis nullable (precision: 10, scale: 2) -- appropriate since price is assigned at the "quoted" stage, not at creation.statusandrequest_type-- good for query filtering. FK indexes auto-created byt.references.stripe_payment_link,paid_at,completed_atall nullable -- correct for their lifecycle positions.Model (
service_request.rb):The
VALID_TRANSITIONShash cleanly encodes the state graph:This matches the spec description (6 valid forward transitions, 2 decline exits, 2 terminal states).
The
status_transition_is_validcallback correctly usesstatus_wasto check the previous value and only runs on persisted records (return if new_record?). Theif: :status_changed?guard avoids unnecessary validation on saves that don't touch status. Both are correct Rails patterns.Associations:
Property has_many :service_requests, dependent: :destroy-- correct.CrewMember has_many :service_requests, dependent: :destroy-- correct.ServiceRequest belongs_to :property/belongs_to :crew_member-- correct, matches FK constraints.Service-- correct, sinceservice_requestshas noservice_idFK.Seeds: Idempotent via
find_or_create_by!thenupdate!. Always overwrites price on re-seed, which is the correct behavior for reference data.No UI leakage: Confirmed. Only model, migration, seed, schema, and spec files touched. No controllers, views, routes, or JavaScript changed.
BLOCKERS
None.
NITS
Initial status not enforced at model level. The
status_transition_is_validcallback hasreturn if new_record?, which meansServiceRequest.create!(property: p, crew_member: c, request_type: "project", status: "completed")would succeed -- bypassing the entire state machine. The DB default handles the normalServiceRequest.create!path (status defaults to "requested"), but an explicitstatus: "completed"on create is not rejected. Consider adding a validation like:Not a blocker because the DB default covers the happy path and no controller exists yet to accept user input, but this would harden the model before a controller is added.
terminal?duplicates knowledge fromVALID_TRANSITIONS. The method hardcodes%w[completed declined]while this could be derived:VALID_TRANSITIONS.select { |_, v| v.empty? }.keys.include?(status). Low risk given the small, stable set, but the derivation would be self-maintaining if a new terminal state were added.Missing spec:
transition_todoes not persist. The spec tests thattransition_toreturns true/false and sets the status, but does not assert that it does NOT save to the database. Addingexpect(request).not_to be_changedafter a reload (orexpect(request.reload.status).to eq("requested")after a failedtransition_to) would document the non-persisting contract explicitly.Missing spec: initial status bypass. No test verifies what happens when creating a ServiceRequest with an explicit non-"requested" status. Whether you fix nit #1 or not, a spec documenting the current behavior would prevent accidental regressions.
crew_membernaming on ServiceRequest. The FK iscrew_member_id, implying the crew member assigned to handle the request. But apackage_changerequest would typically be initiated by a client (who is also a CrewMember with role "client"). The naming is technically correct given the current data model, but a comment clarifying whether this represents the requester or the assignee would help future developers. Very minor.SOP COMPLIANCE
122-service-request-modelfollows{issue-number}-{kebab-case-purpose}Closes #122, parent spike #121 referencedstripe_payment_linkis a column definition, not an actual key/secretPROCESS OBSERVATIONS
update_columnusage in specs to set up intermediate states is the correct pattern -- it bypasses validations intentionally to test transitions from arbitrary starting states.monthly_priceaddition toservicesis well-scoped here since it supports the pricing context of service requests.VERDICT: APPROVED
Solid, well-tested model-only PR. The nits (especially #1 regarding initial status enforcement) should be addressed before a controller is added in a follow-up ticket, but are not blocking for a model-layer-only change. The state machine is correctly implemented, migrations are clean, and test coverage is comprehensive.