Conversation
…n, remove queue-specific fields
… item Removes queue-specific Shop methods (latest_completed_item_tag, create_default_item_tags!, reset!, full_reload_entire_page) and replaces the after_create callback with create_sample_item_tag, which generates one generic 'Sample' ItemTag for first-run UX. Cascades caller cleanup: - shops_controller#reset action removed - delete :reset route removed - ShopPolicy#reset? removed - lib/tasks/shop.rake (only task was create_default_item_tags) deleted
- Sort by (position, name) instead of queue_number - Strip already_completed cache-purge hack from #complete - Rename #reset action to #idle (matches AASM event); route updated - Strong params: permit name, description, position, state
Removes Shop#complete_tag! and ItemTag#reset! (both carried queue-era '_tag' residue and 'reset' semantics). Controller actions now call AASM's generated complete!/idle! events directly with completed_by / completed_at set inline. This keeps method names domain-agnostic so nativeapptemplate-agent's identifier rename (e.g. ItemTag to Todo) doesn't need to rewrite method bodies.
ItemTagSerializer: - Drop queue_number, scan_state, customer_read_at, already_completed - Add name, description, position ShopSerializer: - Drop scanned_item_tags_count (referenced removed scan_state) - Drop display_shop_server_path (display route removed)
- Remove references to senior_manager?/junior_manager?/senior_member?/junior_member? predicates - Both admin and member can perform all ItemTag operations (collaborative model) - Rename reset? to idle? to match controller action Note: member? predicate doesn't yet exist; defined when AccountsShopkeeper::ROLES is updated in Step 13.
Permissions: 6 generic CRUD primitives — create_shops, update_shops, delete_shops, update_organizations, invitation, read_data. Removes 5 queue-specific perms (manage_tags, write_info_to_tags, reset_all_tags, complete_or_reset_tags, show_tag_info). Roles: 2 tiers — admin, member. Removes 5 queue-operator tiers (senior_manager, junior_manager, senior_member, junior_member, guest). Mappings (collaborative SaaS / Notion-style): - admin: all 6 permissions - member: create_shops, update_shops, delete_shops, read_data (no update_organizations or invitation) Applied identically to all 4 envs (development, test, staging, production). A fresh db rebuild + db:seed_fu is required to drop orphan rows from prior fixture loads.
AccountsShopkeeper::ROLES = [:admin, :member]. AccountsInvitation inherits via constant aliasing, so both models drop the 4 queue-era intermediate roles (senior_manager, junior_manager, senior_member, junior_member) and 'guest'. Madmin resources (AccountsShopkeeperResource, AccountsInvitationResource) updated to expose only :admin and :member attributes. Rolified concern is generic (iterates ROLES) so no change needed there. Existing rows' 'roles' jsonb column may carry orphan keys for removed roles — these are silently ignored since the model only reads ROLES.
- Rename 'number tag(s)' → 'item tag(s)' in user-visible strings (aligns with substrate v2 UI label rename in overview §2.4) - Replace activerecord.attributes.item_tag.queue_number with name / description / position - Drop queue_number format/uniqueness error keys (validations gone) No removed permission/role tag refs existed in en.yml; no ja.yml present.
- ItemTagTest: rewrite around name/description/position; drop scan_tag!/complete_tag!/reset!/scan!/unscan! tests and queue_number format/uniqueness coverage; assert names may duplicate across and within shops - ShopTest: replace default-item-tags / queue_number / latest_completed_item_tag / Shop#reset! tests with create_sample_item_tag success path and a stubbed-failure path covering the rescue branch - AccountsShopkeeperTest: collapse per-tier permission/role-helper tests to admin and member only - AccountsInvitationTest, AccountTest, RoleTest, ShopkeeperTest: rename junior_member/guest references to member
- ItemTagSerializerTest: drop scan_state/customer_read_at/already_completed assertions; switch the complete-flow test to the inlined controller pattern (set completed_by/at then call AASM complete!) - ShopSerializerTest: drop scanned_item_tags_count and display_shop_server_path tests (attributes removed) - AccountSerializerTest, AccountsShopkeeperSerializerTest, AccountsInvitationSerializerTest: rename old-tier role refs to member
- ItemTagPolicyTest: collapse per-tier tests to admin+member; rename reset? cases to idle? - ShopPolicyTest: drop reset? tests (action removed in Step 7) - PermissionPolicyTest: replace guest with admin/member coverage - AccountPolicyTest, AccountsShopkeeperPolicyTest, AccountsInvitationPolicyTest: rename old-tier role refs to member
- ItemTagsControllerTest: switch payloads from queue_number to name; drop invalid-format and already_completed cases; rename reset to idle; cover the duplicate-name-allowed case - ShopsControllerTest: drop the reset action test (action removed) - BaseControllerTest: rewire the validation-error case to use blank name instead of duplicate queue_number - Accounts/AccountsInvitations/AccountsShopkeepers/Permissions/ RegistrationsControllerTest: rename old-tier role refs to member
- Drop the extra blank line at end of shops_controller_test.rb class body (Layout/EmptyLinesAroundClassBody, leftover from Step 7 reset action removal) - Refresh brakeman.ignore: AccountsShopkeeper::ROLES collapse changed the expanded permit() literal, so the warning fingerprint shifted. Same intentional rationale (admin-gated role assignment); only the resolved role list and fingerprint differ.
README.md:
- Drop the 'real-time page updates for Number Tags Webpage' qualifier on Turbo
- Rename 'Number Tags (ItemTags)' to 'Item Tags' in the features list
CLAUDE.md:
- Correct the ItemTag description: name has no uniqueness constraint;
describe the columns (name/description/position) and binary state instead
docs/openapi.yaml (-90/+31):
- ItemTag schema: drop queue_number/scan_state/customer_read_at/already_completed,
add name (required)/description/position
- Shop schema: drop scanned_item_tags_count and display_shop_server_path
- /permissions meta: drop maximum_queue_number_length
- Roles: drop senior_manager/junior_manager/senior_member/junior_member/guest,
add member (5 sites: AccountsShopkeeper attrs/update, AccountsInvitation
attrs/create/update)
- Endpoints: remove DELETE /shops/{shopId}/reset; rename
PATCH /item_tags/{itemTagId}/reset → /idle (resetItemTag → idleItemTag)
- ItemTag create/update request bodies switched to name+description+position+state
Also delete:
- docs/pagination-item-tags.md (pre-implementation notes; pagy is now wired in
the controller, doc is stale)
- app/views/layouts/display.html.erb (orphaned after Step 3 display
namespace removal — no controllers reference this layout)
Step 17 residual sweep caught references the per-step grep missed: - config/settings.yml: drop maximum_queue_number_length and the item_tag.default_count / default_queue_number_length keys (only consumed by the removed format validator and create_default_item_tags!) - PermissionsController#index meta: drop maximum_queue_number_length (clients no longer need to enforce the queue-format length cap) - permissions_controller_test: drop the matching assertion
Without an explicit position, the sample row gets NULL, which Postgres sorts last by default — surprising for a single-row first-run state and fragile once a client adds more items without managing position themselves. Setting position 1 gives the sample a definite slot and keeps the (:position, :name) sort meaningful for clients that don't implement reordering.
Both auto-clicker controllers were used only by the display namespace queue-board views (now deleted in Step 3). The eager loader in controllers/index.js needs no change — it just won't find them. Stimulus itself is now essentially dead weight; defer the wider JS-stack removal to a follow-up sweep alongside Turbo.
Without this, clients that don't manage position end up with NULL positions on every item, the (shop_id, position) index goes unused, and ordering between user-added items falls to the :name tiebreaker. before_create callback no-ops when position was explicitly supplied, so Shop's create_sample_item_tag (position: 1) and clients that send their own positions are unaffected. Lighter than acts_as_list (no gem, no decrement-on-delete machinery — that stays out per phase 1 pitfall #7); this is just a sensible append-to-end default.
The substrate v2 design (overview §2.1 table) grants create_shops, update_shops, and delete_shops to both admin and member tiers. Pre-refactor policy was stricter than the design called for: - create? required owner? (so even non-owner admins couldn't create) - update? required admin? (so members couldn't update) - destroy? delegated to create? (so only owner could destroy) Aligned with the design and with ItemTagPolicy's pattern. Tests rewritten — two old assertions inverted (non-owner admin can now create; member can now update), and two read-only true-for-all tests specialised into admin and member variants for symmetry.
Assigns sequential position values per shop, ordered by created_at, starting from current max+1, skipping callbacks/validations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without it, deliver_later jobs (e.g. invitation emails) sit in the queue unprocessed since no worker runs in development. The Puma plugin (config/puma.rb) already activates when this env var is set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bin/setup covers the dev-environment refresh flow; bin/update was a legacy Rails generator artifact and was no longer referenced. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixture-only role redesign (c42249e) left production with stale data: - accounts_shopkeepers / accounts_invitations jsonb roles still keyed on senior_manager, junior_manager, senior_member, junior_member, guest - orphan rows in roles, permissions, roles_permissions for the dropped tiers and queue-specific perms (manage_tags, write_info_to_tags, reset_all_tags, complete_or_reset_tags, show_tag_info) The task folds non-admin keys to member, preserves admin, and deletes orphan role/permission/roles_permission rows in a single transaction. Idempotent — safe to re-run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Substrate v2 drops POST /scan and GET /scan_customer; the only Universal Link rule in the manifest was /scan/*?item_tag_id=…, which no longer resolves. No remaining iOS Universal Link routes in v2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
API-only Rails app — no Turbo runtime in use. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the "switch organization" hint — Free client v2 has no Organizations tab, so the guidance is misleading. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers ErrorsController, AdminUser, the static
ShopkeeperAuth::{ConfirmationResults,ResetPasswords} pages,
ShopkeeperMailer, and Shopkeeper::NotificationMailer (invited,
confirmation_instructions, reset_password_instructions).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ErrorsController: extract shared render_error helper for the duplicated JSON/HTML branching - RegistrationsController: render_*_error hooks now delegate to a single private render_resource_error to remove the triple copy - Account/Shop/ItemTag/AccountsShopkeeper: rename `the_limit_count` local to plain `limit` - AccountsInvitation#set_token: collapse the unique-token loop to use `break` as the assignment value - Shopkeeper: drop two commented-out send_devise_notification lines superseded by the custom mailer Behavior preserved; full test suite passes (393 runs, 792 assertions). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Rate-limit shopkeeper sign-up to 5/IP/hour Mass account creation is currently only constrained by the broad 300/IP/5min rack-attack req/ip cap. Add an endpoint-specific limit on POST /shopkeeper_auth using Rails 8's built-in ActionController::RateLimiting: 5 requests per IP per hour. When exceeded, render 429 with a localized JSON error. Switch the test cache from :null_store to :memory_store so the limiter's counters can persist within a request sequence (:null_store no-ops increments). Clear Rails.cache in the standard test setup so throttle state doesn't leak between tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Tune sign-up rate limit to 10/3min (match jumpstart-pro reference) Aligns with the reference codebase (jumpstart-pro-rails uses `to: 10, within: 3.minutes` on user sign-up). The earlier 5/1.hour was overly strict for legit users — a confused user with bad password rules or autofill misfires could exhaust the quota and get locked out for an hour. 10/3min absorbs realistic retry flows while still constraining bots; per-IP limits are not the primary defense against rotating-IP attackers anyway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#49) * Fix two latent bugs in permissions and sign-in flows PermissionsController#index compared `confirmed_*_version < current_*_version`, where `current_version` returns nil if no row is flagged current. `string < nil` raises ArgumentError → 500. Add a `version_outdated?` helper that treats a missing current version as "nothing to update". ShopkeeperAuth::SessionsController#create unconditionally assigned `request.headers["source"]` to current_platform and called `save!(validate: false)`. Sign-ins without the source header overwrote the user's stored platform with nil, bypassing the presence/inclusion validation. Now skip the assignment when the header is blank. Adds regression tests for both paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Revert PermissionsController nil-version guard Per review: a missing current PrivacyVersion/TermsVersion is a server-side data integrity problem (no row published as current). Crashing loud is preferable to silently telling the client "you're up to date" — the latter would mask the data issue and mislead clients. Keeps the SessionsController fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Require 'source' header on shopkeeper sign-in Per direction: the source header (ios/android) is mandatory. Reject the request with 401 when missing instead of skipping the current_platform update — silently signing the user in without the header would let API tools accumulate sessions that lack platform attribution and bypass the presence/inclusion validation on Shopkeeper. Adds the locale key and updates the regression test for the blank-header path. Updates the existing "no params" test to send the header so the bad_credentials assertion remains the path under test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Make 'source' header optional on shopkeeper sign-in Reverts the earlier "require source header" form. Anti-mass-signup is now handled at the right layer by the sign-up rate_limit introduced in PR #50, so the sign-in header has no security job left. current_platform is informational metadata; rejecting sign-ins on missing metadata is too aggressive — it breaks non-mobile callers (curl, CI, integration tools, future web client) without a real benefit. Skip the current_platform update when the header is blank: the existing stored value is preserved (instead of being nuked to nil by the original buggy code path). Drop the missing_source locale key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consolidates the rack-attack `logins/ip` and `logins/email`
throttles onto the controller using Rails 8's
ActionController::RateLimiting. Rules now sit next to the action
they guard, mirroring the sign-up `rate_limit` shipped earlier.
The general `req/ip` cap stays in rack-attack since `rate_limit`
is per-controller and middleware is the right layer for an
app-wide flood cap.
The email throttle uses `if: -> { params[:email].present? }` to
preserve the original "skip when no email" behavior — without
this, all email-less requests would share a single counter.
Adds an integration test covering both the IP-keyed throttle
(same IP, different emails) and the email-keyed throttle (same
email, rotating IPs).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verified-unreferenced removals (every grep below ran across app/, lib/, config/ and matched only the definition site): - gem "jbuilder" — no .jbuilder views, no JBuilder usage; all responses go through jsonapi-serializer - gem "inline_svg" — no inline_svg helper calls anywhere - locale `forgot_your_password` — no callers - locale `send_me_reset_password_instructions` — no callers - locale `api.shopkeeper.accounts.owner_required` — no callers - locale `api.shopkeeper.accounts.admin_required` — no callers Kept `validate_sign_up_params` / `validate_account_update_params` overrides in RegistrationsController despite looking unused — the parent class wires them up via `before_action` (verified in the gem source). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…54) Per docs-private/SUBSTRATE-NAMING-NORMALIZATION.md, all three substrates should use the canonical NATIVEAPPTEMPLATE_ stem so the agent's existing NativeAppTemplate -> <slug-pascal> rename pair handles every product-name occurrence. The Rails repo has no boot/config references to these env vars - only doc references that point users at the iOS/Android values - so update them for parity. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Routine bundle update: bigdecimal, bootsnap, erb, ffi, irb, json, minitest, net-imap, nokogiri, pagy, parallel, parser, propshaft, puma, regexp_parser, rubocop, rubocop-ast, tailwindcss-ruby. Tests, rubocop, brakeman, bundler-audit all clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dadachi
added a commit
that referenced
this pull request
May 2, 2026
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1 of the substrate v2 transformation — turns this Rails API from a queue-specific template (NFC/QR/scan flows, A001-style queue numbers, 7-tier queue-operator roles) into a generic single-resource CRUD substrate (
Shop→ItemTagwithname/description/positionand a binary state) thatnativeapptemplate-agentcan rename and re-skin for any domain.Design context:
docs/nativeapptemplate-substrate-v2-overview.md. Step-by-step plan:docs/phase1-rails-api.md.Scope: 21 commits, 82 files, +397 / −1686 lines. No DB-data migration required — existing
rolesjsonb keys for removed roles are silently ignored.What changed
ItemTagschemaqueue_number→name(NOT NULL, no uniqueness constraint)description(text, nullable),position(integer, nullable), composite index on(shop_id, position)scan_state,customer_read_at,already_completed, plus the(shop_id, queue_number)unique indexModels
ItemTag: droppedenum :scan_state+ AASM block,scan_tag!,complete_tag!,reset!, queue-format/uniqueness validations, turbo_stream callbacks; kept AASM:state(complete/idleevents) and thelimit_countvalidationShop: droppedlatest_completed_item_tag,create_default_item_tags!(A001-A010 generator),reset!,full_reload_entire_page. Newafter_create :create_sample_item_tagcreates exactly one "Sample" ItemTag for first-run UX (rescue-logged so failure doesn't block Shop creation)AccountsShopkeeper::ROLES:[:admin, :senior_manager, :junior_manager, :senior_member, :junior_member, :guest]→[:admin, :member]API surface
POST /scan,GET /scan_customer, the entiredisplay/namespace,DELETE /shops/:id/reset, app root route + static controllerPATCH /item_tags/:id/reset→/idle(matches AASM event name; clearer separation from "delete")POST /shops/:id/item_tagsaccepts{ "item_tag": { "name", "description", "position", "state" } };nameis the only required fieldRoles & Permissions
create_shopsupdate_shopsdelete_shopsupdate_organizationsinvitationread_dataCollaborative SaaS model (Notion/Linear/Trello-style): both tiers can freely CRUD resources; only admin can manage org and invite members. Rationale in overview §6.10–6.11.
ItemTagPolicyresolves to Shop permissions — no separate ItemTag permissions exist.State machine
AASM :statewithcomplete(idled → completed) andidle(idled/completed → idled) events. Controller actions inline the state transitions andcompleted_by/completed_atwrites — no*_tag!wrapper methods on the model, so the agent's identifier rename (e.g.ItemTag→Todo) doesn't have to rewrite method bodies.Cascading cleanup
lib/tasks/shop.rake(only task wascreate_default_item_tags)app/views/layouts/display.html.erbdocs/pagination-item-tags.md(pagy is wired in the controller, doc was pre-implementation notes)docs/openapi.yaml) regenerated to match (-90/+31)permit(*ROLES)line gets a new fingerprint when the constant changes; same intentional rationale)Test plan
bin/rails test→ 358 runs, 705 assertions, 0 failures, 0 errorsbin/ci→ all 6 steps green (Setup, Style: Ruby, Security: Gem audit, Security: Brakeman, Tests: Rails, Tests: Seeds)POST /api/v1/shopkeeper/shops/:id/item_tagswith{ "item_tag": { "name": "Buy milk" } }→ 201, response usesnamenotqueue_numberPATCH /api/v1/shopkeeper/item_tags/:id/complete→ state becomescompleted,completed_atsetPATCH /api/v1/shopkeeper/item_tags/:id/idle→ state back toidled,completed_atclearedDELETE /api/v1/shopkeeper/item_tags/:id→ 200queue_number→namerename, removed scan/QR screens, role UI collapse to 2 tiers,resetroute →idlerename (these are Phase 2–5 of the substrate v2 plan)Rollback
Pre-refactor state preserved at tag
v1.0.0-with-nfcand branchv1-with-nfc. To revert: