ShipRight is an internal operations dashboard built with Ruby on Rails 8. It centralises shipment management, providing a unified place to track, organise, and manage order lifecycle workflows.
- Staff-only authentication via Devise — no self-registration
- Order lifecycle management — explicit, validated status transitions (pending → approved → shipped → delivered / cancelled)
- Reusable audit history — polymorphic
AuditEntryrecords every status change with who did it - Simulated carrier tracking integration —
Carriers::FakeCarrierClientbehind a clean service boundary - Background job —
TrackingSyncJobsyncs tracking events asynchronously - Orders dashboard — filterable list, detail page, bulk approval
- Precise money storage — all prices and totals stored as integer cents
- Tailwind CSS — clean, responsive UI
| Concern | Solution |
|---|---|
| Language | Ruby 3.2, Rails 8.1 |
| Database | PostgreSQL |
| Authentication | Devise |
| CSS | Tailwind CSS v4 |
| Testing | RSpec, FactoryBot, Shoulda Matchers |
| Jobs | Solid Queue |
| Pagination | Pagy |
Services:
appruns Rails on port3000dbruns PostgreSQL16redisruns Redis for Sidekiq/shared app servicestestruns the RSpec suite againstship_right_test
- Ruby 3.2+
- PostgreSQL 14+
- Docker
- Docker Compose
- Node.js (for Tailwind CSS build)
- Bundler
# 1. Clone the repository
git clone <repo-url>
cd ShipRight
# 2. Install dependencies
bundle install
# 3. Set up the database
bin/rails db:create db:migrate db:seed
# 4. Start the server
bin/dev# 1. Clone the repository
git clone <repo-url>
cd ShipRight
# 2. Build and start the containers
docker compose up --build
# 3. Run database migrations and seed data inside the app container
None, the docker compose file is set to run migrations and seeds on startup. If you need to run them manually, you can execute:
docker compose exec app bin/rails db:create db:migrate db:seed
Open http://localhost:3000.
| Password | Role | |
|---|---|---|
| admin@shipright.io | password123 | Staff |
| ops@shipright.io | password123 | Staff |
app/
├── controllers/
│ ├── application_controller.rb # authenticate_user! globally
│ ├── sessions_controller.rb # Devise sessions override
│ └── dashboard/
│ └── orders_controller.rb # Index, show, transitions, bulk approve
├── models/
│ ├── user.rb # Devise + staff flag
│ ├── order.rb # Lifecycle, VALID_TRANSITIONS map
│ ├── product.rb
│ ├── order_line_item.rb
│ ├── tracking_event.rb
│ └── audit_entry.rb # Polymorphic, self.log helper
├── services/
│ ├── orders/
│ │ ├── status_transition_service.rb # Validates + applies transition + audit
│ │ └── bulk_approve_service.rb # Iterates StatusTransitionService
│ ├── carriers/
│ │ └── fake_carrier_client.rb # Simulated carrier with 5% failure rate
│ └── tracking/
│ └── sync_tracking_events_service.rb # Deduplicates + persists events
├── jobs/
│ └── tracking_sync_job.rb # Async wrapper for SyncTrackingEventsService
└── views/
├── layouts/application.html.erb # Nav + flash messages
├── devise/sessions/new.html.erb # Tailwind login page
└── dashboard/orders/
├── index.html.erb # Filterable table, bulk select
└── show.html.erb # Detail page, actions sidebar, audit log
pending ──► approved ──► shipped ──► delivered
│ │
└──────────────┴──► cancelled
All transitions go through Orders::StatusTransitionService, which:
- Validates the transition is allowed
- Updates the order status in a transaction
- Creates an
AuditEntryrecording the before/after status and the acting user
Invalid transitions return a user-friendly error message — they never raise exceptions to the controller.
Carriers::FakeCarrierClient simulates a real carrier API:
- Deterministic events based on the tracking number (reproducible)
- ~5% simulated failure rate for realistic error handling
- Returns a
TrackingResultstruct withsuccess?,events,error
To swap in a real carrier, implement the same interface (.fetch_tracking(tracking_number) → TrackingResult).
Any model can gain audit history by:
- Adding
has_many :audit_entries, as: :auditable - Calling
AuditEntry.log(auditable:, event:, user:, changes:)
All monetary values are stored as integer cents (total_cents, unit_price_cents). Helper methods (#total_dollars, #unit_price_dollars) convert for display. Never use floats for money.
# Run all specs
bundle exec rspec
# Run specific suites
bundle exec rspec spec/models
bundle exec rspec spec/services
bundle exec rspec spec/requests
bundle exec rspec spec/jobs# Run the full test suite
docker compose run --rm test
# Run a single spec file
docker compose run --rm test bash -lc "bundle exec rails db:create db:schema:load && bundle exec rspec spec/requests/dashboard/orders_spec.rb"Tests focus on behaviour:
StatusTransitionService— valid/invalid transitions, audit trailBulkApproveService— partial failuresFakeCarrierClient— success/failure simulationSyncTrackingEventsService— deduplication, missing tracking numberTrackingSyncJob— delegation + no-op for missing orders- Request specs — authentication, transitions, bulk actions
# Start with live Tailwind recompilation
bin/dev
# Reset and reseed the database
bin/rails db:reset db:seed
# Open Rails console
bin/rails console