Files
BarangaySystem/ai-docs/dictionary.md
2026-06-06 18:43:00 +08:00

83 KiB
Raw Permalink Blame History

Dictionary

Database Standards

Required Fields

  • created_by: Every table should have this field, linked to the user ID of the creator.
  • updated_by: Every table should have this field, linked to the user ID of the last updater.
  • is_active: A boolean field, typically set to true by default, used for soft-deactivation or status tracking.
  • hashkey: string (300), unique. A unique identifier for every record, typically generated by App\Listeners\ModelSavingListener using Str::uuid() . Str::random(100).

Audit Fields & Eloquent Events

  • Audit Fields: created_by and updated_by are critical for audit trails across all modules.
  • ModelSavingListener: app/Listeners/ModelSavingListener.php automatically populates hashkey, created_by, and updated_by for Eloquent models during the Saving event.
  • Raw DB Bypass: Using DB::table(...)->insert() or update() bypasses Eloquent events and listeners.
  • Manual Requirement: When using raw DB queries for performance (e.g., in PosController.php), you MUST manually include created_by or updated_by (using Auth::id()) and updated_at/created_at (using now()) in the data array to maintain data integrity and audit trails.

UI Standards

  • Buttons and Inputs: Prefer .rounded-pill for a modern, premium look unless otherwise specified.
  • Form Spacing: Use d-flex gap-2 or mb-3 to ensure interactive elements are not too close, especially on mobile views.
  • Form Controls: Standardize on form-control and form-select with rounded-pill.
  • Empty States: When a list or dataset is empty, provide a centered, clear message (e.g., "No records found") and an icon (using fad or fas with text-muted and opacity-2) to guide the user.
  • Avatars: For user profile pictures, if a custom photo is not provided (photourl), use https://ui-avatars.com/api/ with the user's name as a dynamic fallback. This ensures a premium, personalized look even without an uploaded image. Avoid relying on local static placeholders like avatar.png.
  • Glassmorphism: Use backdrop-filter: blur(15px); and background-color: rgba(var(--bg-card-rgb), 0.7); for floating headers and overlay components. A utility class .glass-card is available in app.js for consistent application.
  • Dark Mode Surface Isolation: For a modern "seamless" look, avoid forced backgrounds on navigation items and service buttons (li, a). Instead, use subtle drop-shadows and icon contrast filters (brightness, contrast) to maintain hierarchy.
  • Scoped Dark Mode: When fixing dark mode in Vue components with <style scoped>, use :global(.dark-mode) .selector to ensure styles are correctly applied when the dark-mode class is on the body.
  • Theme Variables: Use global CSS variables defined in app.js (e.g., --bg-primary, --bg-card, --text-primary, --accent-color) for consistent theme-aware styling.
  • Administrative Icon Standards: For high-density administrative dashboards (like UltimateConsole.vue), standardize on the following Font Awesome sizes for visual hierarchy:
    • Navigation Icons: fa-lg to provide better target recognition.
    • Dashboard Stat Cards: fa-2x to fill the visual container and provide focus.
    • Primary Action Buttons: fa-lg (e.g., fas fa-paper-plane fa-lg) within buttons for a premium feel.
    • Main Dashboard Logo: fa-4x for a bold, centered focus on primary consoles.
    • Bulk Tool Icons: fa-2x to accentuate the tool's category and provide clear differentiation.
    • Table Actions: fa-lg for critical row-level controls (Download, Delete, Edit).
  • Dark Mode Scoped Fixes: Use :global(.dark-mode) .selector or theme variables (e.g., var(--bg-primary)) to override hardcoded colors in Vue <style scoped> blocks. Remove text-dark and bg-white classes where theming is required.
  • Ultimate Console Premium Header: The administrative header uses a deep, multi-stop radial gradient (#020617 base) with mesh highlights and intensified glassmorphism for stats pills (badge-pill-premium). This ensures a "Power User" feel that distinguishes administrative zones from standard user interfaces.
  • Header Action Buttons: Use .btn-glass-premium for auxiliary header actions (e.g., Refresh, Filter) within colored headers. This style uses backdrop blur and subtle transparency for a sophisticated, modern look without clashing with the header background.
  • Responsive Table Actions:
    • Container: Use d-flex flex-wrap gap-1 instead of btn-group in table action columns to prevent horizontal scrolling.
    • Text Labels: Use .d-none .d-lg-inline for text labels within buttons (e.g., <span class="d-none d-lg-inline">Manage</span>) to save space on smaller screens while keeping the icon visible.
    • Min Width: Apply style="min-width: 150px;" (or appropriate value) to the container to ensure enough space for at least a few icons without excessive wrapping.
  • Sticky Headers: For configuration-heavy or high-density administrative pages (e.g., SystemSettings.vue, UltimateConsole.vue), use sticky-top on the primary header. This ensures critical actions like "Save" or "Back" remain accessible while scrolling through long forms. CRITICAL: Because the application uses a fixed TopHeader, you MUST provide an explicit top offset (e.g., top: 66px; or top: var(--header-height, 66px);) and a high z-index (e.g., 1020) to ensure the sticky element sticks below the global navigation bar rather than being hidden behind it. Adjust sidebar top values accordingly (e.g., 13rem to 18rem depending on header height) to prevent overlap.

Layout & Responsiveness

  • Max Width: The application uses a dynamic maximum width for the main layout, controlled by the CSS variable --layout-max-width.
  • Default Width: 1440px (Modern Desktop/Tablet Landscape).
  • Mobile Behavior: On screens smaller than the max width, the layout automatically scales to 100% width.
  • Centering: The body and fixed elements (TopHeader, BottomNav) are centered using margin: 0 auto.
  • Full Width Mode: The .is-full-width class on the body overrides the max-width to none, allowing components like the POS to utilize the entire screen.
  • Implementation: Managed globally in resources/js/app.js via an injected <style> tag, and supported by :global overrides in scoped components.

Migration Standards (Hypervel/Hyperf)

  • DO NOT use Illuminate classes in migrations. This project runs on Hypervel (Hyperf + Swoole), not Laravel. Using Illuminate\Database\Migrations\Migration, Illuminate\Database\Schema\Blueprint, or Illuminate\Support\Facades\Schema will cause fatal class-not-found errors at runtime.
  • Correct imports for all migrations:
    use Hyperf\Database\Schema\Blueprint;
    use Hypervel\Database\Migrations\Migration;
    use Hypervel\Support\Facades\Schema;
    
  • Why: Hypervel wraps Hyperf's database layer. The Illuminate namespace does not exist in this project. The Hypervel Migration class provides proper coroutine-context-aware connection resolution via ApplicationContext.
  • Commands: php artisan migrate, php artisan migrate:rollback, php artisan migrate:fresh, etc. work identically to Laravel.

Development Requirements

1. API / Web Route Permissions

  • For every new function exposed through an API or web route:
    • Add a corresponding permission in UserPermissions and UserActions.
    • Ensure the permission is validated before granting access to the function.

Route file scope (important)

  • routes/api.php is intentionally minimal. It only registers: IndexController@index, RemoteLogoutController@logout, and Market\ActivityController (/activity/recent, /activity/search).
  • Products, users, stores, POS, cart, cooperatives, admin, settings, landing pages, etc. are ALL defined in routes/web.php — not api.php. Despite paths like /api/pos/..., /api/public/..., or /admin/users/list appearing throughout the codebase, these are web routes (Inertia/session-auth based), not REST API routes.
  • When searching for product/user/store endpoints, grep routes/web.php first. Do not assume /api/* paths live in routes/api.php.

2. Vue Routing

  • For every new page:

    • Add a corresponding entry in VueRouteMap.
    • Ensure the route is properly registered and accessible via the application's routing system.
  • Pages are using vue not blade. Blade pages are legacy code and must not be modified but can be a source of investigation

  • Direct Page Access: Pages such as view or edit must include one of the following in the URL:

    • h=<HASH_KEY>
    • e=<ENCODED_PAYLOAD>
    • URL Format: /page-name--h:HASHKEY or /page-name--e:PAYLOAD
    • Product Context: When a product is accessed from a store context (e.g., in BuyViewProductMarket or ManageProductAdmin), use an encoded payload (--e:) containing product_hashkey and store_hashkey to ensure context persistence and allow direct URL access.
  • This ensures that:

    • Pages can be accessed directly via URL.
    • Functionality remains intact even without navigation state.

POS Access Keys

  • Terminal Name: A descriptive name for a POS terminal (e.g., "Counter 1").
  • Access Key: A unique key used to authenticate a POS terminal without a traditional login.
  • Target File: resources/js/Pages/PosAccessKeys.vue - Main management interface for POS access keys.
  • Store Hash: The hashkey of the store associated with the access key.
  • Store Name: Displayed in PosMain.vue, retrieved from posStore.activeSession.store.name or posStore.todayStats.store_name.

Sharing Protocols

  • Web Share API: Used for sharing content (text, URLs) via the device's native sharing dialog. Supported on most modern mobile browsers.

Market Enums

  • TransactionFlow: Categorizes transactions into INCOME (1), EXPENSE (-1), or NEUTRAL (0).
  • ProductTransactionType: Defines specific transaction events (e.g., ONLINE_SALE, PURCHASE) which map to a TransactionFlow.
  • Add Transaction Page: resources/js/Pages/AddTransaction.vue - Interface for manually recording global or store-specific transactions.

Customer Management

  • Table: cst (Customers)
  • Key Fields: hashkey, name, store_id, user_id.
  • Suggestions: Handled in PosMain.vue via /api/pos/get-customers. Employs a Stale-While-Revalidate pattern: immediate results from Pinia/LocalStorage cache while background fetching latest from the server.
  • Session Lifecycle: Transactions are completed in confirmComplete. Upon success, the session is reset, but the terminal identity (pos_access_key and captured storeHash) is preserved. A new session is then automatically started via posStore.startNewSession to ensure a seamless "customer-after-customer" checkout flow. To support this, pos_sessions allows multiple records per pos_access_key (removed unique constraint).
  • Quick Select: Displays the last 3 recently used customers as quick-pick buttons when the customer name input is empty/focused. This provides immediate selection without searching.
  • Cache: Persisted in localStorage under pos_cached_customers.

Routing Architecture

  • VueRouteMap: app/Http/Controllers/Support/VueRouteMap.php - Centralized mapping for Vue components to routes.
  • SPA Routing: Catch-all route in web.php maps to VueRouteMap::handleSpa().
  • Navigation: Uses useNavigate composable. Pagenames should match Vue component names (PascalCase).
  • Initial State: app.js initializes history state on load using window.history.replaceState to ensure back/forward navigation works for the entry page.
  • Prop Extraction: The backend automatically extracts hash/payload tokens and provides them as target, hashkey, and id props to the rendered Vue component.
  • URL Flexibility: The SPA router (VueRouteMap::handleSpa) is robust against hyphen and case discrepancies in the URL slug. It automatically matches unhyphenated slugs (e.g., /viewstoremarket) and misaligned casing to their correct components (e.g., ViewStoreMarket) defined in the map.
  • Common Mappings:
    • /user-list -> UserList.vue
    • /manage-products -> ManageProductsAdmin.vue
    • /manage-product-admin -> ManageProductAdmin.vue

Missing/Future Modules

  • Shipments: ShipmentList.vue - For logistics and product tracking. Handled by ShipmentController.php.
  • Farmer Management: FarmerProfileEdit.vue - For registering farmer profiles. Handled by FarmerController.php.
  • Verification Dashboard: VerificationDashboard.vue - For admin review of farmer profiles. Handled by FarmerController.php.

Groups & Associations

  • Module: Groups

  • Tables: groups, group_members

  • Architecture: A generic container for various member organizations.

  • Group Types: Dynamic (COOPERATIVE, ASSOCIATION, NGO, etc.) defined in system_settings under group_types.

  • Key Fields: hashkey, name, type, description.

  • Membership: Linked via group_members pivot table with role and membership_type.

  • Property Management: ListProperties.vue - Interface for managing properties.

  • Referrals: ListReferrals.vue - Interface for managing property referrals and leads.

  • Accounting: Handles financial tracking and reporting.

    • Tables: accounts, account_transactions
    • Controller: AccountingController.php
    • Pages:
      • AccountingDashboard.vue (/accounting-dashboard) - Tree and Leaf views.
      • ListReports.vue (/list-reports) - Transaction list and summaries.
    • Endpoints:
      • POST /admin/accounting/reports - Fetches recent transactions.
      • POST /admin/accounting/tree - Hierarchical account tree.
      • POST /admin/accounting/leaf - Flat list of leaf accounts.

Server Configuration Notes

  • Nginx Headers: Large JWT tokens and complex data trees (like accounting) require increased header and proxy buffers in Nginx.
    • large_client_header_buffers 4 32k;
    • proxy_buffer_size 32k;
    • proxy_buffers 8 64k;
  • POST Requests: Production environments (Nginx/WAF) may reject empty POST bodies with a 400 error. A global Axios interceptor in app.js ensures all POST requests carry at least an empty object {}.

Shipment & Logistics

  • Module: Shipment
  • Key Fields: hashkey, transaction_id, courier_id, status, tracking_number.
  • Statuses: PENDING, PICKED_UP, IN_TRANSIT, DELIVERED, FAILED, RETURNED.
  • Target Files: ai-docs/modules/shipment_module.md

Farmer Management

  • Module: FarmerProfile
  • Key Fields: hashkey, user_id, organization_id, verification_status.
  • Verification Statuses: UNVERIFIED, PENDING, VERIFIED, REJECTED.
  • Target Files: ai-docs/modules/farmer_management_module.md

Redis Configuration

  • Connection: REDIS_HOST, REDIS_PORT, REDIS_DB.
  • Authentication: REDIS_AUTH or REDIS_PASSWORD.
  • Support: Empty passwords are supported by setting REDIS_AUTH= in .env. The configuration uses (?: null) logic to handle empty strings correctly.
  • Config Files: config/database.php and config/autoload/redis.php.

Query Caching (Redis)

  • Helper: App\Http\Controllers\Helpers\CacheHelper.php
  • Key Format: querycache:[queryhash]++++[parameterhash]
  • Methods:
    • get_cache($query, $params = []): Fetches from cache via Cache facade. Supports SQL strings or Builder objects.
    • set_cache($query, $data, $params = [], $ttl = 86400): Sets in cache via Cache facade inside a fire-and-forget coroutine.
    • erase_cache($query, $params = []): Deletes the specific cache key using Cache::forget.
  • Implementation: Uses Hypervel\Support\Facades\Cache for better integration with the application's caching layer.
  • Async Logic: set_cache uses Hyperf\Coroutine\Coroutine::create to return responses immediately without waiting for cache network I/O.
  • TTL: Default is 1 day (86400 seconds).

User Settings & UI Persistence

  • Composable: resources/js/composables/useUserSettings.js
  • Controller: app/Http/Controllers/Market/UserSettingsController.php
  • Frontend Sync: Handled via uiStore.syncSettings(settings) which is called after userStore.fetchCurrentUser() in app.js.
  • Purpose: Dynamically store and retrieve user-specific UI preferences in the users.settings JSON column.
  • Key Settings:
    • pos_layout: landscape or regular. Controls the split-screen behavior in PosMain.vue.
    • dark_mode: Boolean. Globally toggles the application's dark theme by applying the .dark-mode class to the body.
    • pos_sound_effects: (Future) Boolean to toggle audible feedback during scanning.

Global Theme & Dark Mode

  • Class: .dark-mode - Applied to the <body> element.
  • Persistence: Saved in the users.settings JSON column. Included in initial user data fetch via AccountSettingsPageController@listDetails.
  • Implementation: Handled via state in uiStore and a global reactive watch in app.js with { immediate: true }.
  • UI Toggle: Located in AccountSettings.vue, calls /UserSettings/Update.
  • Color Overrides: Global text color overrides for dark mode are defined in app.js and apply to standard typography tags (h1-h6, span, p, label, i) and generic div elements, ensuring uniform color contrast across the application.

POS & Terminal Management (Advanced)

  • "Start New Session" requires store context: startNewSessionSilently (in usePosSession.js) guards against calling the API when both storeHash and access_key are absent. When neither is available (e.g. navigating to /pos-main without a URL hash or stored key), it sets posStore.error and returns false instead of hitting /api/pos/start. PosController::startSession now returns 422 (not 404) when no store can be resolved, with a descriptive message. posStore.error is surfaced in the cart area of PosMain.vue.
  • Landscape Mode: Optimized for tablet/desktop usage with a split product/cart view.
  • QR/Barcode Scanner: Integrated via html5-qrcode in PosMain.vue. Supports scanning product hashkeys, barcodes, or store-specific QR codes.
  • Session Auto-Start & Access Keys: POS sessions are automatically restarted after completion. To prevent authorization failures, the system distinguishes between permanent terminal access keys (PK- prefix, stored in pos_access_keys) and temporary session-specific random tokens. The frontend (pos.js) ensures only valid terminal keys are persisted to localStorage (via PosMain.vue), and the backend (PosController.php) now allows authenticated users to start a session by specifying a store_hash even if an invalid or unrecognized access_key is present in the request payload. This prevents 401 errors caused by session-specific tokens being used as terminal credentials.
  • Clear All: A "Clear All Items" feature is available in PosMain.vue and pos.js which performs a bulk deletion of all transactions in the current session via /api/pos/clear. This includes a database update and a session archive entry for auditing.

File Uploads

  • Route: POST /File/Upload/{category}FilesMainController::UploadFilefromRequest (app/Http/Controllers/FilesMainController.php).
  • Permission scoping: gated by canUploadCategory($category) — allows anyone with UploadAllFiles (ULTIMATE / SUPER_OPERATOR). For ProductMarket, also allows roles with CreateProductForOwnStore or AddProducttoOwnStore via ProductPermissions::isActionAllowed (covers STORE_OWNER, STORE_MANAGER, OPERATOR). All other categories require UploadAllFiles.
  • Frontend: resources/js/composables/useFileUpload.js posts to /File/Upload/${category}. The CreateProductStoreOwner wizard uses category: 'ProductMarket'.

RBAC & Permissions Logic

  • Core Controller: app/Http/Controllers/Helpers/Permissions/UserPermissions.php
  • Actions Enum: app/Enums/UserActions.php (Currently defines 103 user actions).
  • Default Permissions: ULTIMATE users are automatically granted all actions defined in UserActions::cases().
  • Roles Method: UserPermissions::roles() defines the default permission mapping per user type.
  • Roles with No Target User: UserPermissions::$RoleswithNoTargetUser is a collection for actions that don't target a specific user (e.g., ViewAllUserTypes, ViewShipments, UltimateConsole).
  • User Types: ULTIMATE (all access), SUPER_OPERATOR (most management), OPERATOR (limited operational access), COORDINATOR, SUPPLIER_OVERSEER, WHOLESALE_BUYER, SUPPLIER, STORE_OWNER, STORE_MANAGER, USER, RIDER, AUDIT, POS_TERMINAL.
  • Allowed User Types (Creation Hierarchy): Defined in UserPermissions::UserTypeService::getAllowedUserTypes().
    • ULTIMATE: Can manage ALL types.
  • SUPER_OPERATOR: management of all entities except ULTIMATE accounts. Includes global store and POS key management. Now includes ViewAccountingReports and ManageAccounting to allow access to the Accounting Dashboard.
  • OPERATOR: management of multiple stores, farmers, and logistics (Shipments/POS Reports). Now includes global store and product management, POS access keys, and accounting management (ViewAccountingReports, ManageAccounting).
    • STORE_OWNER: Can manage STORE_MANAGER, RIDER, POS_TERMINAL.
    • RIDER: Has no default management permissions.

User Management

  • Create User:
    • Validation: All required fields (mobile_number, username, name, fullname, type, password) must show real-time validation status.
    • Field Conflicts: Conflicts (e.g., "Mobile number already taken" or "Username already taken") must be checked via API and displayed as inline error messages.
    • Type Selection: Filtered based on the current user's acct_type via UserTypeService::getAllowedUserTypes().
    • UX: The "Create Account" button should remain visible but disabled until all requirements are met. A "Remaining Requirements" summary box should appear if the user has started filling the form but missed some fields.
  • Table: users
  • Controller: app/Http/Controllers/UserManagement/CreateUserControllerUltimate.php
  • Page: resources/js/Pages/CreateUser.vue
  • User List API: /admin/users/list returns an object { success: true, users: [...] } containing all visible descendants of the current user.

Cooperative & User Profile

  • UserInfo: Detailed personal information for users (firstname, lastname, bank details, addresses). Linked to users via user_id.
    • Table: user_infos
    • Edit Page: resources/js/Pages/UserInfoEdit.vue
  • Navigation Fix (April 2026): Accessing the profile without a target prop now defaults to the currently logged-in user's profile by fetching the hashkey from the Pinia store.
  • Cooperative: An organization of type COOPERATIVE. Users are linked via cooperative_members. Data is stored in the organizations table.
    • Key Fields: registration_number (CDA COR), cin (Cooperative Identification Number), tin (BIR TIN), cooperative_type, cooperative_category, registration_date, contact_person, contact_number, contact_email, compliance_status.
    • Types: Credit, Consumer, Producer, Marketing, Service, Multi-purpose, Advocacy, Agrarian Reform, Cooperative Bank, Dairy, Education, Electric, Financial Service, Fishermen, Health Service, Housing, Insurance, Transport, Water Service, Workers.
    • Categories: Micro (Assets <= P3M), Small (Assets > P3M to P15M), Medium (Assets > P15M to P100M), Large (Assets > P100M).
    • Member Table: cooperative_members
    • List Page: resources/js/Pages/CooperativeList.vue
    • Create Page: resources/js/Pages/CreateCooperative.vue
    • Detail Page: resources/js/Pages/CooperativeDetail.vue
    • Model: app/Models/Market/Organization.php
    • Controller: app/Http/Controllers/Market/CooperativeController.php

Home Store Owner Dashboard

  • Component: resources/js/Pages/Fragments/Home/HomeStoreOwner.vue - Used by Home.vue for STORE_OWNER accounts.
  • Balance Card Stats: transactions_today_no (count) and cash_flow_today_php (signed SUM(amount * flow) for today, scoped to stores where the user is owner or in store_managers). Plus my_stores_no (count of stores owned by the current user).
  • Backend: Stats are added to GET /home-data (routes/web.php), scoped by store ownership for non-Big3.
  • Buttons: Create Store (opens a yes/no chooser modal — "Quick Create" calls POST /Store/AutoCreate, "Custom Create" navigates to CreateStore.vue with store-owner-only fields hidden), Import Products (BatchAddProducts), New Product (CreateProductStoreOwner — wizard-style flow, see below), My Products (ManageProductsAdmin — backend listProducts_Admin already scopes non-Big3 to products created by the user + descendants), POS Keys (PosAccessKeys).
  • Store Owner Create Product Flow (CreateProductStoreOwner.vue): 4-step wizard tailored for store owners (replaces CreateProductUltimate on the Store Owner home). On mount, fetches /Admin/Stores/Selectable; if empty, opens a yesNoModal directing the user to CreateStore and blocks. Step 1: live fuzzy search via /Products/Admin/FuzzySearch; user picks an existing global product, OR clicks "My product is not listed — Create new" to enter Step 2-new. Step 2 (existing): description override (per-store). Step 2 (new): full global product form (name/desc/category/subcategory/price/unit/available/barcode/photos). Step 3: multi-select stores from the selectable list. Step 4: per-store rows with editable price and available (defaults seeded from global). Submit: for "new" mode, first POST /Products/Admin/New/ with TargetStore = first selected store (required because store owners lack CreateProductGlobal), then iterate every selected store and POST /Products/AssignToStore/ passing target, TargetStore, price, available, description. AssignProductToOwnStore now upserts the pivot (updateExistingPivot if already attached, else attach) and accepts an optional description field written to the prd_str pivot.
  • Auto-Create Store: StoreController@autoCreatePOST /Store/AutoCreate. Quick path used by the Store Owner home's "Quick Create" choice. Creates a store with sensible defaults and self-assigns owner/manager + a store_managers row.
  • CreateStore for STORE_OWNER:
    • Hidden fields: Store Owner dropdown, Internal Remarks, Linked Cooperatives, Store Status. Owner is forced to the current user; status forced to active; remarks cleared.
    • Additional Store Managers list: only shown when descendants of the current user with acct_type = STORE_MANAGER exist. Backend (StoreController@store) re-validates that each submitted manager hashkey is both a descendant AND a STORE_MANAGER — anything else is silently dropped.
    • Self-assignment: STORE_OWNER is automatically inserted into store_managers for the new store, in addition to owner_id/manager_id.
    • Name uniqueness: stores.name is validated unique globally. Violations return 422 with message "A store with this name already exists. Please choose a different name."
    • RBAC: only Big3 + STORE_OWNER may hit POST /Store/New; STORE_MANAGER cannot create (only edit assigned stores).
  • User list filter (POST /admin/user/list/numbers/hash): accepts an optional type (string or array of acct_type values, e.g. "store manager") to restrict the descendants-only result set. Response now includes acct_type per row.
  • Route Map Adjustments: /manage-stores, /batch-add-products, and /manage-products now allow store owner (and store manager for /manage-stores and /manage-products) so the Store Owner home buttons land on accessible pages.
  • /create-product-store-owner route: mapped in VueRouteMap.phpCreateProductStoreOwner, allowed for ult, super operator, operator, store owner, store manager. Required so direct URL-bar access / page reload works (in-app navigation via useNavigate push-states this URL).
  • Add Products to Store flow (AddProductsToStore.vue): two-step wizard at /add-products-to-store--h:<store_hashkey> (allowed for Big3 + store owner + store manager). Step 1: browse every active global product via POST /Products/GlobalList (ProductController@listGlobalProductsForPicker — returns all is_active=true products regardless of created_by, since picking a global product to list in your own store requires visibility into the full catalog). Multi-select card grid + free-text search over name/category/subcategory. Step 2: editable table of selected products with per-row price and available, seeded from the global defaults, plus a "Bulk apply" panel that sets every row's price or availability at once. Submit iterates POST /Products/AssignToStore/ per row (existing upsert endpoint — see L266). Entry points: (1) CreateStore.vue after POST /Store/New success — navigates here with the new store's hashkey instead of ListStores; (2) HomeStoreOwner.vue Quick Create branch after POST /Store/AutoCreate; (3) ManageStoresAdmin.vue per-row green + "Add Products" action (visible when canModifyStore(store)).

Home Ultimate Dashboard (Revised)

  • Component: resources/js/Pages/Fragments/Home/HomeUltimate.vue

  • Style: Maintains the "Classic" BukidBounty look with a single ServiceButtonGrid for primary services and a SideTextButtonList for secondary management actions.

  • Icons: Standardized to use micons (/assets/micons/) for better dark mode visibility and modern aesthetics for key services like Products (box-seam.svg), Verification (patch-check.svg), Cooperatives (people.svg), and Announce (megaphone.svg).

  • Organization:

    • Primary Grid: Contains the most visual and frequently used administrative tools (Users, Stores, products, Transactions, Reports, Market, Shipments, etc.).
    • Management & Tools List: Text-based list for specific onboarding tasks (New User/Store/Product) and personal/social features (Profile, Referrals, Properties, Credit Transfer).
  • Data Integration: Connected to live counts for Users, Active Stores, Pending Orders, and System Balance via the updated /home-data endpoint.

    Product Management

  • Manage Product Admin: resources/js/Pages/ManageProductAdmin.vue - Central console for managing a specific product's global and store-specific data.

  • Update Product Modal: resources/js/Components/Market/UpdateProductModal.vue - A unified modal interface for editing product details, including pricing overrides for specific stores.

  • Create Product Flow (CreateProductUltimate.vue): On submit, runs a fuzzy-name dedup check via POST /Products/Admin/FuzzySearch (ProductController@fuzzySearchByName, name-only LIKE + SOUNDEX + similar_text scoring, threshold ≥ 45). If matches exist, a modal lists them with "Import to Store" (calls /Products/AssignToStore/ with the user's typed price/available). Otherwise — or after "None of these — Create new" — a store-picker modal always opens (per product spec) and the chosen store is sent as TargetStore to /Products/Admin/New/.

  • .bb-modal* styles: shared modal styles (.bb-modal-backdrop, .bb-modal, .bb-modal-header/body/footer/close) are defined as scoped styles inside CreateProductUltimate.vue and BatchAddProducts.vue. They are NOT global — any new page using <div class="bb-modal-backdrop"> must duplicate the CSS in its own scoped block or the modal will render invisibly.

  • Batch Add Products (BatchAddProducts.vuePOST /admin/batch/productsBatchController@batchCreateProducts): Big-3 only. Top-of-page target-store picker (/Admin/Stores/Selectable). Each leaf is independent; default mode creates a new global product. "Pick existing" button per leaf opens a fuzzy-search modal (reuses /Products/Admin/FuzzySearch) to link a global product instead — if the search returns zero matches, the picker is closed and a Warning modal is shown. Payload per leaf: source: 'new' | 'existing'. For existing, only product_hash, price, available, description are sent; backend attaches the global product to the target store via the prd_str pivot, falling back to the global's price/description when the leaf's value is blank or 0. For new, the global product is created and — when a target store is picked — also attached to that store with the same price/stock/description. Pivot table prd_str supports available, price, description, is_active etc. (see Store::products() withPivot).

  • Store-scoped product creation permission: UserActions::CreateProductForOwnStore (granted to STORE_OWNER, STORE_MANAGER, OPERATOR, SUPER_OPERATOR). ProductController@createNew_Admin allows the request when the caller has either CreateProductGlobal (Big3) or CreateProductForOwnStore + a TargetStore they own/manage; the existing per-store ownership check at the top of createNew_Admin enforces the latter.

  • Selectable Stores Endpoint: POST /Admin/Stores/SelectableStoreController@listSelectableStoresForAddingProduct. Non-Big3 query was widened from descendants-only to include self + descendants + store_managers pivot, so a fresh store owner sees their own store(s).

  • Manage Listings Modal (ManageProductsAdmin.vue store icon): opens a per-product modal listing the user's selectable stores. For each store it shows a "Listed" / "Not listed" badge based on POST /Products/AssignedStores/ (ProductController@getAssignedStoresForProduct, returns store hashkeys the product is attached to). Each row has a List or Unlist action; List runs the existing price/availability prompt → POST /Products/AssignToStore/; Unlist confirms then calls POST /Products/UnassignFromStore/ProductController@RemoveProductFromStore. RemoveProductFromStore was relaxed from the Big3-only RemoveProductfromAnyStore permission to ownership-aware checks mirroring AssignProductToOwnStore (Big3, owner, legacy manager, store_managers pivot member, or descendant of any of those).

  • Payload Keys:

    • product_hashkey: Used in URL payloads to identify a specific product.
    • store_hashkey: Used in URL payloads to identify the store context for a product.

    Ultimate Console

  • Component: resources/js/Pages/UltimateConsole.vue - Powerful administrative interface for ULTIMATE accounts.

  • Roles: Access restricted via ultimate middleware and UserActions::UltimateConsole.

  • Key Features:

    • Raw SQL Query execution (UltimateQueryModal.vue).
    • System-wide maintenance mode toggle (Redis-backed).
    • Global broadcast messaging (Redis-backed).
    • Batch operations for users, transactions, and store management.
    • Database clearing/flushing for specific tables.
    • System statistics and artisan command execution.
  • Backend: app/Http/Controllers/Market/UltimateController.php

  • Migrate Endpoint: POST /admin/ultimate/migrateUltimateController@runMigrate. Runs php artisan migrate --force (non-interactive, prod-safe). Auth: auth + ultimate middleware. Returns { success, output }.

  • Flush Endpoint: POST /admin/ultimate/flushUltimateController@flushData. Body { target }. Valid targets: transactions, pos_sessions, stores (truncates str + prd_str + store_managers), products (truncates prd_items + prd_str), cooperatives (truncates cooperative_* child tables + deletes organizations where type='COOPERATIVE'), carts (truncates cart_items + carts), farmer_profiles, cache (Redis flushDB). UI lives in the "Dangerous Zone" tab via flushOptions in UltimateConsole.vue.

  • Composable: resources/js/composables/useUltimate.js - Contains methods like getStats (to retrieve system metrics), toggleMaintenance, sendGlobalMessage, and runCommand (for Artisan calls). Ensure methods are called exactly as defined (e.g., getStats instead of fetchStats).

    POS (Point of Sale)

  • Main Interface: resources/js/Pages/PosMain.vue

  • Buttons: In dark mode, quantity adjustment buttons (btn-light) and the delete button (btn-soft-danger) should use theme-aware glassmorphism or deep charcoal backgrounds to avoid a "stark white" or "quirky" appearance.

  • Security: Raw queries are restricted to SELECT, SHOW, and DESCRIBE for data retrieval; others use standard DB statements. Artisan commands are whitelisted.

  • Dark Mode Overlays: Global overlays such as the .loading-overlay in application-layout.blade.php must be dark-mode aware. Ensure background: rgba(var(--bg-card-rgb), 0.7) or themed variables are used instead of hardcoded white backgrounds (rgba(255, 255, 255, 0.7)).

  • Hardcoded Backgrounds: Avoid using bg-white or bg-light classes directly in Vue templates for components that should persist in dark mode (like modals in PosMain.vue). Use themed CSS variables or :global(.dark-mode) overrides.

  • listProductsData store resolution: Must use sequential if (!$targetStore) guards, NOT if/elseif. A stale access_key in localStorage (e.g., a revoked key or session-specific token) must not block the store_hash/session_hash fallbacks. Priority order: access_key → session_hash → store_hash. Bug pattern: using elseif causes all global products to be shown when access_key is present but unrecognized.

  • Session Composable: resources/js/composables/Market/usePosSession.js - Isolates POS initialization, session loading, and transaction completion logic. Use this in PosMain.vue and related components to ensure stable terminal identity and prevent logic fragmentation.

  • Session Hashkey Integrity: The backend PosController.php must NOT truncate hashkeys at 36 characters. The hashkey field is 300 characters and can contain long uniquely generated identifiers. Incorrect truncation leads to 404 "Session not found" errors.

  • Performance Optimization: To reduce perceived latency in local and production environments, the POS API uses Request Consolidation. The addItem and removeItem endpoints return the full current session object (with transactions and products) instead of just the single modified record. This eliminates the need for the frontend to make a second loadSession call, halving the total network delay for cart actions. Additionally, the backend implements High-Efficiency SQL Paths:

    • Joined Lookups: Product pricing and store overrides are fetched in a single combined query to reduce database round-trips.
    • Aggregate Operations: Session totals are calculated using database-side SUM() aggregates rather than memory-heavy PHP collection mapping.
    • Lean Archiving: The PosSessionArchive logic only captures raw session attributes (getAttributes()) rather than nested relationship trees, which significantly reduces JSON serialization overhead and database write bloat.
    • Column Projection: Eager loads are restricted to only necessary columns (select(['id', 'hashkey', ...])) to minimize the JSON payload size returned in the response.

User Notes

  • Composable: resources/js/composables/useUserNotes.js
  • Controller: app/Http/Controllers/Pages/AccountSettingsPageController.php
  • Mechanics: Fetches from /user/note/content. Also updated in real-time via SSE in useSessionGuard.js.
  • Security: Requires web and auth middleware to ensure session persistence and prevent 302 redirects to login on AJAX calls.
  • Handling: Frontend should strictly ignore HTML responses (which indicate redirects) to avoid displaying login page source code in the notes modal.

Database Backups

  • Table: db_backups
  • Model: App\Models\DbBackup
  • Key Fields: hashkey, file_content_hashkey, filename, size_in_bytes.
  • Logic: Backups are generated as .sql files, compressed to .7z Ultra, and then stored as base64 in the file_content table. The db_backups table tracks these records. Temporary .sql and .7z files are deleted from the filesystem immediately after being saved to the database to prevent persistence on disk.
  • Exclusion: The db_backups table is excluded from the SQL dump to prevent recursive growth.
  • UI: Managed in resources/js/Pages/UltimateConsole.vue under the "Backups" tab.
  • Controller: app/Http/Controllers/Market/UltimateController.php

POS History

  • Component: resources/js/Pages/PosHistory.vue - Standalone page for viewing a store's POS transaction history.
  • Route: /pos-history--h:STORE_HASH (Mapped in VueRouteMap.php).
  • Logic: Uses PosHistoryList component and posStore for paginated data retrieval.
  • Navigation: In PosMain.vue, clicking the "Today: X txns" count navigates to this page.
  • Permissions: Restricted to ult, super operator, operator, store owner, and store manager.

Cart Module

  • Table: carts, cart_items
  • Models: App\Models\Market\Cart, App\Models\Market\CartItem
  • Controller: App\Http\Controllers\Market\CartController
  • Page: resources/js/Pages/CartProductMarket.vue
  • Logic: Users have a single active cart. Cart items are linked to products from prd_items.
  • API Routes: /cart/get, /cart/add, /cart/update, /cart/remove, /cart/clear.

Global System Settings

  • Model: App\Models\SystemSetting.php - Stores key-value configurations with caching.
  • Page: resources/js/Pages/SystemSettings.vue - Admin interface for managing branding and system flags.
  • Helper: App\Support\SystemSettingsHelper.php - Facade-like access for Blade and Controllers.
  • Branding: Includes app_name, app_logo, app_description, footer_text, and primary_color.
  • Dynamic Branding (Frontend):
    • Pattern: Use uiStore.appName in Vue templates instead of hardcoded strings like "BukidBounty".
    • Components: Home.vue, Login.vue, AccountSettings.vue, and HomePublic.vue use this pattern.
    • Tab Title: Automatically managed via uiStore.pageTitle, which defaults to appName.
  • Inertia Shared Data:
    • Controller: App\Http\Controllers\Support\Inertia.php - Automatically includes systemSettings in every Inertia::render call.
    • Synchronization: app.js populates the uiStore from initialPage.props.systemSettings immediately upon mounting. This eliminates the "flash" of default branding before the API background refresh completes.

Common Modals & UI Components

  • BaseModal: resources/js/Components/Core/BaseModal.vue - Standardized modal structure with header, body, and footer slots. Supports v-model for visibility.
  • ConfirmModal: resources/js/Components/Core/ConfirmModal.vue - Specialized modal for action confirmations (Maintenance toggle, Backups, Flushes). Supports danger, warning, and info variants.
  • UltimateQueryModal: resources/js/Components/Ultimate/UltimateQueryModal.vue - Advanced SQL console for ULTIMATE accounts, refactored to use BaseModal and theme-aware styling.
  • FileImage: resources/js/Components/Core/FileImage.vue — emits a single <img> with @error fallback. The :src prop is forwarded as-is, so callers must pass a string URL. photourl from product endpoints may be string | string[] | null; passing photourl[0] when photourl is a string yields a 1-char URL that 404s. Use a safe accessor like const firstPhoto = (v) => Array.isArray(v) ? (v[0] || '') : (v || ''); before binding.
  • FileList::resolvedUrl(): app/Models/FileList.php (table file_list) returns cdn_url if non-empty, else /RequestData/File/{hashkey}. str.photourl is a JSON array of file_list hashkeys — raw hashkeys are NOT renderable URLs. StoreController@viewStoreDetails resolves them into $store->resolved_photos (array of usable URLs, cdn_url preferred); ViewStoreMarket.vue reads store.resolved_photos for its avatar/banner/photo-gallery modal.
  • CardSimple: resources/js/Components/Core/CardSimple.vue — wrapper with is-premium defaulting to true, which adds a translucent background and a translateY(-2px) lift on hover. For non-interactive panels (page headers, search bars, info boxes), pass :is-premium="false" to disable the hover lift, otherwise the surrounding card jitters when the cursor moves over it.

Cooperative Chapter Officer/Member Features

  • User types: COOP_OFFICER ('coop officer'), COOP_MEMBER ('coop member') in app/Enums/UserTypes.php. Home fragments: resources/js/Pages/Fragments/Home/HomeCoopOfficer.vue, HomeCoopMember.vue (wired in Home.vue).
  • Controller: app/Http/Controllers/Support/ChapterController.php — methods getOrgChart, getOfficerScope, memberSearch, assignOfficer, createChapter, publicGetChapter, publicRegisterToChapter.
  • Routes (routes/web.php): authed POST /Chapters/OrgChart, /Chapters/Officer/Scope, /Chapters/Members/Search, /Chapters/Officer/Assign, /Chapters/Create; public GET /api/public/chapter/{hkey}, POST /api/public/chapter/register.
  • Composable: resources/js/composables/useChapters.jsfetchOrgChart, fetchOfficerScope, searchMembers, assignOfficer, createChapter.
  • SPA pages (VueRouteMap, module 'cooperatives'): /chapter-org-chart, /coop-member-search, /create-coop-user, /assign-chapter-officer, /create-chapter, /register-chapter (public; decodes base64 payload prop {coop_hash,chapter_hash}).
  • CHILD_LEVELS: national→region→province→[city|municipal]→barangay. chapters.level enum has NO 'municipal' (peers in code only). Level checks use in_array($level,['city','municipal']).
  • assignOfficer MOVES a member (deactivate parent ChapterMember, create child officer row, upgrade COOP_MEMBER→COOP_OFFICER). memberSearch returns ONLY {name, role, chapter_name}. HomeCoopMember shows officer names + member count only (never fellow member names).
  • home-data: COOP_OFFICER/COOP_MEMBER branches set $props['props']['chapter_info'] + stats chapter_member_count/child_chapter_count/new_members_7d.
  • Duplicate-check endpoints: authed POST /admin/user/number/exists (mobile_number), POST /admin/user/username/exists (username) → {exists}; public POST /api/public/user/check-mobile.

Account Type Definitions (RBAC)

  • ULTIMATE: full system access, bypasses all hierarchy checks.
  • SUPER_OPERATOR: management of all entities except ULTIMATE accounts. Includes global store and POS key management.
  • OPERATOR: management of multiple stores, farmers, and logistics (Shipments/POS Reports). Now includes global store and product management, and POS access keys.
  • COORDINATOR: focused on logistics, organizations, and farm management.
  • RIDER: delivery and shipment status updates only; blocked from user management.
  • POS_TERMINAL: point-of-sale operations, viewing POS reports, and customer lists.
  • AUDIT: read-only view of all system data (Global Reports, Transactions, Accounting, User Lists).
  • ANY_USER: Standard system user with no administrative or logistics privileges.

Page Access Controls

  • Setting Key: disabled_pages (stored as JSON in system_settings).
  • Logic: Enforced at both the backend (VueRouteMap.php) and the frontend navigation layer (useNavigate.js).
  • Management: Controlled via the "Page Controls" tab in UltimateConsole.vue.
  • Backend API: SystemSettingsController@getPublicSettings exposes the list to the frontend, and SystemSettingsController@update handles saving toggled states.
  • Sync Mechanism:
    • Initial Load: app.js calls uiStore.refreshSettings() on startup.
    • Background Refresh: app.js runs a background interval calling uiStore.refreshSettings() every 1 minute to stay in sync.
    • Pre-navigation Refresh: useNavigate.js automatically triggers a refresh if settings are "stale" (older than 5 minutes) before evaluating the access guard. Known Issue: This threshold may cause a delay when a page is enabled/disabled until a reload or the 5-minute window expires.
    • Sync Strategy: Admin tools (e.g., UltimateConsole.vue) should manually trigger uiStore.refreshSettings() or syncSettings() after updating disabled_pages to ensure immediate session-wide consistency.
    • Case-Insensitivity: All disabled page checks are case-insensitive to ensure reliable blocking regardless of component naming conventions.
  • Affected Files:
    • resources/js/composables/Core/useNavigate.js - Global navigation guard.
    • resources/js/stores/ui.js - Reactive store for disabledPages and lastSynced timestamp.
    • app/Http/Controllers/Support/VueRouteMap.php - Backend enforcement for direct URL access; implements isUserAllowed logic using allowedUserTypes key.
    • app/Http/Controllers/Helpers/Permissions/UserPermissions.php - Core RBAC engine mapping roles to UserActions enums.
    • app/Http/Controllers/Admin/SystemSettingsController.php - API bridge for public setting retrieval.

System vs Account Settings

  • Account Settings: AccountSettings.vue - User-specific profile and security settings (password, photo, basic info).
  • Management: Controlled via the "Page Controls" tab in UltimateConsole.vue.
  • Global System Settings: SystemSettings.vue - Administrative configuration for the entire platform (branding, maintenance, page controls).
  • Navigation: In HomeUltimate.vue and other navigation menus, "Global System Settings" must point to the SystemSettings pagename, while "My Personal Profile" or "Account" points to AccountSettings.

User Additional Details

  • Settings Field: JSON column in users table.
  • Cooperatives: Stored in settings.cooperatives as an array of organization hashkeys.
  • Composable: resources/js/composables/useUserAdditionalDetails.js
  • Controller: app/Http/Controllers/UserManagement/UserAdditionalDetailsController.php
  • Functionality: Allows users to join/leave cooperatives via settings, and provides quick search by cooperative membership.
  • Sync: Automatically synced in CooperativeController::joinCooperative to maintain consistency between cooperative_members table and user settings.

SSE & Real-time Integration

  • Controller: app/Http/Controllers/Support/SSEController.php
  • Mechanism: Uses Hypervel\Coroutine\Parallel for concurrent fetching of multiple data streams (Stats, Customers, Inventory, Settings) to minimize latency.
  • Pulse Interval: 5 seconds.
  • Syncing Logic:
    • Customers: Employs a First-Fetch vs Delta strategy. Upon initial connection (or first fetch), the top 20 most recent customers are pushed to the client to "preload" search suggestions. Subsequent pulses only push records with updated_at > lastSync (deltas).
    • Products (Marketplace): Precaches the full active product list for the global marketplace on the first fetch (isFirstFetch). Subsequent ticks send detailed product updates (price, stock, descriptions) as inventory_deltas.
    • Inventory: Product inventory updates (stock/price changes) are pushed as deltas based on the lastSync timestamp across all stores owned/managed by the user.
  • Frontend Integration:
    • Composable: resources/js/composables/Core/useSessionGuard.js acts as the primary consumer and dispatches data to Pinia stores.
    • Stores: pos.js (syncFromSSE), product.js (syncFromSSE), and ui.js (syncDisabledPages) handle the reactive merge of incoming data into the UI state.
    • Caching: The product.js store maintains a detailsCache (Object/Map) of full product details to enable instant page transitions in the Marketplace and Product Detail views.

Session Management & PWA

  • Multi-tab Logout: Synchronized via both SSE (isloggedin: false payload) and a localStorage storage event (logout_event key). When any tab logs out, it clears localStorage and sets logout_event, which triggers an immediate redirect to /login in all other open tabs via the useSessionGuard composable.
  • SSE Connection: Handled in resources/js/composables/Core/useSessionGuard.js. It maintains a persistent EventSource connection to /sse/stream for real-time updates and session monitoring. The backend SSEController explicitly sends an isloggedin: false message before closing the stream when a session is lost.
  • Service Worker Registration: Managed in resources/js/app.js. To ensure persistent SSE and background features, the service worker should NOT be unregistered upon landing on the login page; automatic unregistration logic in Login.vue was removed to prevent "service worker deleted" issues.
  • Polling Fallback: If SSE is unavailable, the system automatically falls back to a Web Worker (session-guard-worker.js) or main-thread interval polling to ensure session integrity.
  • Observability: Console logs with the prefix [SessionGuard] provide observability into connection status, version changes, and logout events.
    • Session Locking (SSE): Long-running SSE stream loops in PHP can lock the session associated with the request by default. This prevents concurrent requests from the same session (like logout, login, or any other middleware-protected route) from processing until the SSE thread yields. To avoid this "blocking" behavior, call session()->save() at the start of the loop in SSEController to release the lock immediately.

User Management

  • Create User: resources/js/Pages/CreateUser.vue - Main interface for creating new users.
  • Controller: app/Http/Controllers/UserManagement/CreateUserControllerUltimate.php
  • Required Fields:
    • mobile_number: Unique, format 09XXXXXXXXX.
    • username: Unique, required for all users.
    • name: Required (Display Name).
    • password: Min 6 characters.
    • type: User Type (via UserTypes enum).
  • Validation: When the user clicks "Create User Account", a modal appears if any required fields are missing or invalid, listing them clearly with badges and a description of the requirements. This replaces the previous inline badge system.
  • Hierarchy: Users can be assigned a parentuid (parent) to create a multi-level marketing or organizational hierarchy. The listAllUsersforParentSelectHTML method in the controller filters the parent list to only show the current user and their descendants, ensuring data isolation.

Build & Deployment Standards

  • Requirement: After any changes to the frontend (Vue components, assets) or backend logic, run the build and restart process to ensure the latest changes are active.
  • Build Process:
    • Primary: npm run build (on host, if node/npm are installed).
    • Containerized Fallback: If npm is missing on the host, use a specialized Node container to build directly into the host filesystem: docker run --rm -v "$(pwd)":/var/app -w /var/app node:latest /bin/sh -c "npm install && npm run build"
  • Vite Version: The project uses Vite 7, which requires Node.js >= 22.12.0 or >= 20.19.0.
  • Container Restart: After building assets, restart the main application container to refresh the Swoole service and pick up the new build: docker restart bukidbountyapp
  • Volume Mapping: The application container mounts the current directory to /var/app, so build assets generated on the host are immediately visible to the container upon restart.
  • Branch Synchronization: The main branch is periodically updated to match the experimental branch to promote stable experimental features to production. This is done via git checkout main && git reset --hard experimental.

RBAC & Session Hardening Best Practices

  • User Type Listing (Dropdown Fix): The CreateUserControllerUltimate@listAllUserTypesforSelectHTML method must handle UserTypes enums directly. Avoid using UserTypes::from($currentUser->acct_type) if the acct_type attribute is already cast to the enum in the User model, as this will trigger a TypeError.
  • SPA Route Protection: All sensitive management pages (e.g., /create-user) MUST be explicitly defined in App\Http\Controllers\Support\VueRouteMap::$routes with an allowedUserTypes array. If a route is not mapped, it defaults to allowing any authenticated user via the handleSpa catch-all, which creates a security vulnerability.
  • Session State Isolation: To prevent "Role Leaking" (where a new user sees the previous user's dashboard fragments or cached roles), the Login.vue component must execute sessionStorage.clear() within its onMounted hook. This ensures a clean slate for every new authentication attempt.
  • Dashboard Fragmentation: Dashboard fragments in resources/js/Pages/Fragments/Home/ (e.g., HomeShared.vue, HomeOperator.vue) should implement computed filtering for action buttons based on UserTypes to ensure that unauthorized actions are not visible in the UI, even if the user has navigated to the dashboard correctly.

Expanded Member Profile (Cooperative)

  • Table: user_infos
  • Personal Details: firstname, middlename, lastname, suffix, gender, dob.
  • Dynamic Context: priority_sector (links to priority_sectors in system settings).
  • Social Accounts: messenger_id, viber_number, tiktok_username.
  • Address Components: region, province, city, barangay. Detailed address (House No, Street, Zip, Country) is stored in the addresses JSON column.
  • Family & Education: civil_status, children_count, dependent_count, education_level, course, school, year_last_attended.
  • Employment & Livelihood: livelihood_source, last_company, employer_name, last_position, occupation, last_employment_year.
  • Financial Details: monthly_income (decimal), bank_account_no (string).
  • Government Info: tin, philhealth_id, gov_id, id_type, id_number, beneficiary_type.
  • Emergency Contact: emergency_contact_name, emergency_contact_address, emergency_contact_phone, emergency_contact_relation, emergency_contact_user_id.
  • Linking Logic: The system automatically links emergency contacts by matching their phone number with an existing user's mobile_number in UserInfoController@updateUserInfo.
  • Virtual Attributes: age (calculated on the fly from dob).

Public Cooperative Registration

  • Route (Frontend): /register-coop--h:HASHKEYRegisterCoop.vue (loginRequired: false).
  • Public API: GET /api/public/cooperative/{hkey} returns coop info (no auth). POST /api/public/cooperative/register creates a new USER account and registers them as a MEMBER.
  • Controller Methods: CooperativeController@publicGetCooperative and CooperativeController@publicRegisterMember.
  • Parent Assignment: Uses the cooperative's created_by user as parent, falling back to first COORDINATOR, then first user.
  • Share Button: In CooperativeDetail.vue → "Share Register Link" button uses the Web Share API on mobile (native dialog) and falls back to clipboard copy on desktop.

Cooperative Membership Details

  • Table: cooperative_members
  • Key Fields: membership_type, membership_level, officer_position, officer_level, concurrent_position, concurrent_level, cooperative_name_alt, cooperative_position, year_beginning.
  • Management: Membership details can be updated via /Cooperatives/Member/Update.
  • Self-Service Registration: Users can register themselves as members via the CooperativeMemberRegister.vue page (route: /cooperative-member-register--h:COOP_HASH).
  • Registration Logic: Handled by CooperativeController@registerMember. It automatically links the user to the organization and updates the settings.cooperatives array in the users table for session/preference synchronization.
  • Access Control: Controlled by UserActions::JoinCooperative. Standard users, operators, and coordinators have this permission by default.
  • UI Integration: A "Register as Member" button appears on the CooperativeDetail.vue page if the user is not yet a member. After registration, the button is hidden and the user appears in the members list.

RBAC & Hierarchy Concepts

  • Big 3: Refers to ULTIMATE, SUPER_OPERATOR, and OPERATOR account types. They have global management privileges, bypass standard organizational hierarchy checks, and are the only roles permitted to edit global product details (categories, base prices, etc.) in ManageProductAdmin.vue.
  • Hierarchy Management: Management actions (Listing, Viewing, Editing) for stores and users are typically restricted to direct or indirect parents (ancestors). "Big 3" roles are immune to this restriction when accessing stores.
  • Store Managers: Users assigned via the store_managers table who are granted delegated management access to a specific store.
  • Multi-Manager System: Replaces the legacy single-manager field (manager_id) with a searchable, multi-select interface in CreateStore.vue and EditStoreUltimate.vue, allowing multiple users to be assigned as managers for a single store.
  • Context-Aware Product Locking: A UI pattern in ManageProductAdmin.vue that hides or disables global product fields for non-"Big 3" users, while still allowing them to manage store-specific overrides if they are an owner or manager.

POS Access Control (Hardened)

  • Helper: UserPermissions::isUserAllowedAccessToStore($user, $store) - Centralized authorization check for POS/Reports access.
  • Logic:
    • ULTIMATE users have global access.
    • Owners and Managers of the store have access.
    • Any ancestor of the Owner or Manager has access.
    • Children (e.g., POS_TERMINAL, RIDER) of the Owner or Manager have access.
  • Enforcement: Applied to PosController methods: startSession, getSession, getPosSessions, getTodayStats, and getCustomers.
  • Ancillary Helper: UserPermissions::isAncestorOf($ancestor, $descendant) - Recursively checks user hierarchy via parentuid.

UI Audit Results (April 2026)

  • Navigation: "My Personal Profile" now explicitly passes the current user's hashkey in HomeUltimate.vue.
  • Connectivity: SSE connection refused (ERR_CONNECTION_REFUSED) is a known issue being investigated at the container/proxy level.
  • Functional Checks:
    • UserInfoEdit: Fixed hanging by ensuring loader always closes and defaults to current user.
    • Create/Manage Store: Confirmed functional with correct route mapping.
    • Ultimate Console: Operational with full monitoring and setting controls.

Remote Development & Tunneling

  • Script: scripts/tunnel.sh - Standardized script for creating a Cloudflare Tunnel to localhost:9522.
  • Port: 9522 (Host) maps to 9501 (Container) for the bukidapp service.
  • Usage:
    • Temporary URL: ./scripts/tunnel.sh (generates a *.trycloudflare.com link).
    • Persistent Tunnel: ./scripts/tunnel.sh <YOUR_TUNNEL_TOKEN>.
  • Implementation: Uses Docker cloudflare/cloudflared with --network host to expose the local development port online for testing and external access.

Dokploy Deployment

  • Guide: dokploy-deployment-guide.md (repo root) — Full step-by-step deployment guide.
  • Network: All services (app, MySQL, DragonflyDB) must join dokploy-network (external: true) for DNS resolution.
  • Hostname Resolution:
    • Swarm services (Dokploy-managed databases like MySQL): Use the service name as hostname (e.g., prodservers-prodsqlmain-kv3yph). Find via docker service ls.
    • Compose services (like DragonflyDB): Use the full container name as hostname (e.g., prodservers-dragonflydb-rnfaje-dragonflydb-1). Find via docker ps --format '{{.Names}}'.
  • Common Error — Redis DNS Lookup Failed: Caused by REDIS_HOST pointing to a container name that is not on the same Docker network. Fix by adding dokploy-network to the DragonflyDB compose and using the correct container name.
  • Port Exposure: Do NOT use host port mapping (e.g., 9522:9501) in docker-compose.yml for Dokploy. Expose only the internal port (9501); Traefik handles external routing via auto-injected labels.
  • Variants: Multiple app instances (prod, beta, test) can share the same MySQL server and DragonflyDB by using different DB_DATABASE names and REDIS_DB numbers (015).

Landing Page Management

  • Table: landing_pages
  • Model: App\Models\LandingPage
  • Key Fields: title, html_content, description, hashkey, is_active.
  • Active Constraint: Only one landing page can be is_active=true at a time, enforced at the application level via LandingPage::setAsActive().
  • Controller: App\Http\Controllers\Admin\LandingPageController
  • Editor Page: resources/js/Pages/LandingPageEditor.vue — Full CRUD interface with HTML editor, live preview, template snippets (hero/features/CTA), and card-based gallery view.
  • Guest Rendering: HomePublic.vue fetches the active landing page via /api/public/landing-page and renders the HTML for unauthenticated visitors. Falls back to the default homepage if no active landing page exists.
  • Access Control: Landing page management is restricted to ULTIMATE, SUPER_OPERATOR, and COORDINATOR roles.
  • RBAC: UserActions::ManageLandingPages permission added to $RoleswithNoTargetUser.
  • Route Map: Registered as LandingPageEditor in VueRouteMap.php.
  • Public API: GET /api/public/landing-page (no auth) returns the active landing page HTML content.
  • Admin APIs:
    • POST /admin/landing-pages/list — List all landing pages.
    • POST /admin/landing-pages/show — Get a single landing page by hashkey.
    • POST /admin/landing-pages/store — Create or update (if hashkey provided).
    • POST /admin/landing-pages/set-active — Set a page as active (deactivates all others).
    • POST /admin/landing-pages/deactivate-all — Deactivate all pages (revert to default homepage).
    • POST /admin/landing-pages/delete — Delete a landing page.

CDN Asset Pipeline (jsDelivr / obj-vault-3a)

  • CDN Repo: telemagnadon/obj-vault-3a on GitHub, served via jsDelivr.
  • Pinned Tag: Configured in config/cdn.php (base) and public/sw.js (CDN_SHA). Current: v2026.05.14-vendor (SHA 927de981da85).
  • Asset Path Convention: a/<sha256-12char>.<ext>. Extension is .bin by default to obscure content type. Whitelist that preserves original extension: .css, .js, .mjs, .json, .svg.
  • SVG Critical: jsDelivr serves all files with Content-Type: application/octet-stream + X-Content-Type-Options: nosniff. Browsers will render PNG/JPG inside <img> from octet-stream (image decoder sniffs bytes anyway), but SVG requires image/svg+xml<img src="*.bin"> containing SVG markup silently fails to render. Therefore SVGs MUST be stored as a/<hash>.svg so jsDelivr serves them with the correct MIME.
  • Pipeline Skill: cdn-asset-pipeline (in ~/.claude/skills/) handles sync. After updating extension whitelist, all SVG .bin files must be re-synced to .svg and orphaned .bin files removed.
  • CSS Relative URLs Break on CDN: CSS files served from CDN use the flat a/<hash>.<ext> path, so any relative url(...) inside them (e.g. @font-face src: url('icomoon.ttf')) resolves to a/icomoon.ttf and 404s. Fix: rewrite any internal asset references in CSS to absolute jsDelivr URLs pointing at their published hashed .bin/.svg. Known offender: public/assets/vendor/dist/alt-theme/icons-alipay.css (icomoon @font-face). When breaking this, top-bar back/filter icons and bottom-nav home icon (any .icon-* class from icomoon) silently render blank.
  • Icomoon Font Path: public/assets/vendor/dist/alt-theme/icomoon.ttf → CDN a/c99ad0580805.bin. Reference absolutely in icons-alipay.css so font loads regardless of where the CSS is hosted.
  • Icomoon Font Broken on jsDelivr (May 2026): The .ttf is published as a/c99ad0580805.bin and served as application/octet-stream with X-Content-Type-Options: nosniff. Chrome/Firefox refuse to use it as a font, silently breaking every .icon-* class. Same MIME-sniff problem the SVG migration solved. Resolution: don't use icomoon .icon-* classes in Vue components — use Font Awesome (fas fa-*, already loaded). Mappings used: icon-home → fa-home, icon-left → fa-chevron-left, icon-right → fa-chevron-right, icon-copy2 → fa-copy, icon-filter → fa-sliders-h. If icomoon is ever needed again, either add .ttf/.woff/.woff2/.eot/.otf to the CDN pipeline's preserve-extension whitelist and re-publish, or self-host the font under /public so Nginx sets the correct MIME.

Service Worker & Caching

  • Hashed Assets: Files under /build/assets/ (generated by Vite) include hashes in their names.
  • Caching Strategy: These files use a Cache-First strategy since their content is immutable for a given filename.
  • Expiration: Cached for 6 months (180 * 24 * 60 * 60 seconds) with a maximum of 1000 entries.
  • Implementation: Configured in public/sw.js via Workbox registerRoute.

Infrastructure & Docker

  • Main Service: bukidapp - PHP Swoole/Hypervel application service.

    • Container Name: Usually bukidbountyapp-bukidapp-1 (or bukidbountyapp-nginx-1 for the entry point).
  • Port: Internal 9501, exposed to the host/nginx.

  • Reverse Proxy: Nginx acts as the entry point and reverse proxy, configured in docker/nginx/default.conf.

  • Static Asset Delivery: Nginx serves files from /public directly (CSS, JS, images) to minimize latency and bypass the PHP/Swoole stack for static requests.

  • Docker Compose: Managed via docker-compose-local.yml (development) and docker-compose.yml (production).

  • Service Dependency: The nginx service depends on bukidapp.

  • Networking:

    • Uses dokploy-network (external) for Traefik routing and cross-service communication.
    • Uses app-internal (bridge) as an isolated network for nginx→bukidapp communication to avoid DNS collisions.
    • DNS Collision Warning: On dokploy-network, multiple compose projects may share the same service name bukidapp. Nginx must NOT resolve bukidapp directly on this shared network. Instead, use a unique network alias (swoole-upstream) on the app-internal network.
    • Upstream Alias: The bukidapp service declares aliases: [swoole-upstream] on app-internal, and docker/nginx/default.conf targets server swoole-upstream:9501.
  • Priority Sectors: Stored as a JSON array in system_settings under the key priority_sectors. Defines groups eligible for specific assistance and priority (e.g., FISHERMEN, SENIOR, PWD). Managed via the Global System Settings editor.

  • Group Types: Stored as a JSON array in system_settings under the key group_types. Defines the categories of organizations (e.g., COOPERATIVE, ASSOCIATION).

  • Address Fields: Stored as a JSON array in system_settings under the key default_address_fields. Defines the expected keys in the user_infos.addresses JSON (e.g., house_no, street, zipcode).

  • JSON Setting Editor: A dynamic editor in SystemSettings.vue that supports array-based configurations with tag/badge management.

  • Dokploy Traefik Routing:

    • Dokploy auto-injects Traefik labels on the first service by default.
    • Domain Target: In Dokploy UI → Domains tab, the service name MUST be set to nginx (not bukidapp), so Traefik routes domain → nginx:80 → bukidapp:9501.
    • Traefik uses Docker labels (not DNS) to find the target container, so the nginx service MUST be on dokploy-network for Traefik to reach it.
  • Local vs Production Volumes:

    • Local (docker-compose-local.yml): Uses bind mount .:/var/app for live code reloading during development. Nginx mounts ./public:/var/app/public directly.
    • Production (docker-compose.yml): Does NOT use bind mounts on bukidapp. The image is self-contained — composer install and npm install run during docker build, and the resulting vendor/ and node_modules/ are baked into the image.
    • Shared Build Volume (Production): A named volume public_build shares Vite-built assets from bukidapp to nginx. bukidapp copies /var/app/public/ to /var/app/public-shared/ (the shared volume) on startup via the command sh -c "cp -r /var/app/public/. /var/app/public-shared/ && exec php artisan serve". Nginx mounts this volume at /var/app/public.
    • Why not bind mount in prod?: The host's ./public directory (from the repo clone) does NOT contain Vite build output — those are generated inside the bukidapp container during docker build. A bind mount would hide the built assets.
  • Dockerignore: .dockerignore excludes vendor/, node_modules/, .git, and .env* to prevent host directories from overwriting dependencies installed during image build via COPY . ..

Module Enable/Disable System

  • Purpose: Allows enabling or disabling entire application modules (POS, Products, Accounting, etc.) via environment variables without code changes.
  • Config: config/modules.php - Central registry of modules and their associated MODULE_<NAME>_ENABLED environment variables.
  • Master Toggle: MODULES_SYSTEM_ENABLED - If set to false, all module-specific checks are bypassed (everything is enabled). Defaults to true.
  • Helper: App\Support\ModuleHelper::isEnabled($moduleKey) - Used to check module status in code.
  • Middleware: module:<key> (e.g., module:pos) - Applied to routes in web.php to block API/web access to disabled modules with a 403 response.
  • SPA Routing: VueRouteMap.php - Each SPA route can be mapped to a module using the 'module' key. The router (handleSpa and registerRoutes) automatically blocks access and redirects to / if the module is disabled.
  • Frontend Sync: Module states are exposed via SystemSettingsController@getPublicSettings under the module_states key. This allows the frontend to reactively hide navigation links or UI elements for disabled modules.

QRPH / EMV Payment QR

  • Standard: QRPH is the Philippine QR payment standard based on EMVCo Merchant-Presented QR Code Specification (TLV encoding: 2-char tag + 2-char decimal length + value).
  • Decoder: app/Http/Controllers/Helpers/QrphDecoder.php — parses raw QRPH strings into merchant name, network, account, validity (CRC-16/CCITT-FALSE), initiation method, currency.
  • Storage: QRPH payment code is stored in system_settings under key qrph_payment_code.
  • Action: UserActions::ManageQrphPaymentCode (manageqrphpaymentcode) — ULTIMATE-only via route middleware; listed in $RoleswithNoTargetUser.
  • Routes:
    • POST /Financial/Qrph/Get — any authenticated user; returns raw string + decoded info for top-up display.
    • POST /Financial/Qrph/Setultimate middleware; saves QRPH code to system_settings.
    • POST /Financial/Qrph/Decodeultimate middleware; decode-only preview (no save).
  • JS Composable: resources/js/composables/useQrph.jsparseTlv, injectAmount (inserts EMV field 54 + recalculates CRC-16/CCITT-FALSE), generateQrDataUrl (renders QR via qrcode npm library — no external API).
  • Amount injection: In MyWallet.vue QRPH tab, selecting an amount rebuilds the QRPH string client-side with the amount embedded, then renders a new QR — member's banking app (GoTyme, GCash, Maya, etc.) auto-fills the amount.
  • GoTyme / InstaPay GUIDs: QrphDecoder.php recognises com.gotyme, ph.ppmi, ph.bsp.qrph, com.p2pqrph (and all major PH banks). Raw GUID is shown as fallback for unrecognised networks.
  • Top-Up UI: MyWallet.vue top-up modal "Scan to Pay" tab: generates QR client-side with optional amount embedded; falls back to stored static image if no amount set.
  • Admin UI: AccountSettings.vue shows a "Payment QR Code (QRPH)" card only for ULTIMATE accounts; drop-zone decodes QR image locally via Html5Qrcode.scanFile, uploads the image file, and shows Static/Dynamic badge.

Known Bug Patterns & Fixes

RBAC — Action Permission vs Page Access Mismatch

  • VueRouteMap allowedUserTypes controls SPA page access; UserPermissions::roles() controls individual API action access. These must stay in sync.
  • Example: OPERATOR listed in /list-properties allowedUserTypes, but ViewProperties/ViewReferrals were missing from UserTypes::OPERATOR->value in roles() → caused 403 on the POST /admin/properties/list API.
  • Fix: always add the required UserActions::* to roles() whenever a role is added to allowedUserTypes for a page.

SPA Catch-all — Undefined $moduleKey

  • VueRouteMap::handleSpa() uses $moduleKey on line ~530 but only assigns it inside the if/else match blocks. If the path matches no known route, $moduleKey is undefined → potential 500.
  • Fix: initialize $moduleKey = null; before the if ($routeSettings) block.
  • /bukidbountyapp path is now registered in VueRouteMap as an alias of Home (no login required) to prevent it hitting the catch-all fallback.

useUserNotes — Unauthenticated fetch

  • TopHeader.vue calls fetchNotes() in onMounted unconditionally, firing GET /user/note/content (an auth-protected endpoint) before the user logs in.
  • Fix: guard the call with if (userStore.isLoggedIn) in TopHeader, and add the same guard inside useUserNotes::fetchNotes() itself as a safety net.

POS Access Key — STORE_OWNER / STORE_MANAGER missing create/delete/toggle permissions

  • /pos-access-keys VueRouteMap allows store owner and store manager, but roles() only gave them ViewPosAccessKeys. Calls to create/delete/toggle keys returned 403.
  • Fix: add CreatePosAccessKey, DeletePosAccessKey, TogglePosAccessKey to both STORE_OWNER and STORE_MANAGER in UserPermissions::roles().
  • Default State: All modules default to true (enabled) if the environment variable is missing.

Database Table Names (canonical)

Always reference these exact table names in raw SQL, unique:/exists: validation rules, DB::table(...), joins, and migrations. Several tables use abbreviated names — using the human/plural form (e.g. stores, products) causes Table not found SQL errors that Hypervel surfaces as a generic 400 with empty body.

Market / Commerce

  • strStores (model: App\Models\Market\Store)
  • prd_itemsProducts (model: App\Models\Market\Product)
  • prd_strProduct↔Store pivot (per-store price, stock, description, is_active)
  • prd_trxProduct Transactions (model: ProductTransaction). Fields used by sales aggregation: product_id, store_id, quantity, created_at. ProductController::viewProductDetails aggregates sold_today (global) and store_sold_today (per-store, when a store hash is provided) by summing quantity filtered with whereDate('created_at', today()). Surfaced in BuyViewProductMarket.vue POS scan card alongside a printPosCode() button (opens a small window with QR + barcode and auto-window.print()/close).
  • prd_trx_sesProduct Transaction Sessions (model: ProductTransactionSession)
  • prd_trx_ses_arcProduct Transaction Sessions Archive (model: ProductTransactionSessionArchive)
  • cstCustomers (model: App\Models\Market\Customer)
  • carts — Carts
  • cart_items — Cart Items
  • store_managers — Store ↔ Manager User pivot (multi-manager system)
  • couriers — Couriers
  • shipments — Shipments
  • pos_access_keys — POS Access Keys
  • pos_sessions — POS Sessions
  • pos_sessions_archive — Archived POS Sessions
  • pos_transactions — POS Transactions

Organizations / Cooperatives

  • organizations — Organizations (cooperatives, associations, etc.)
  • main_organizations — Top-level / parent organization records
  • org_strOrganization↔Store pivot (cooperative-store linkage)
  • cooperative_members — Cooperative Members
  • cooperative_documents — Cooperative Documents
  • cooperative_resolutions — Cooperative Resolutions
  • cooperative_votes — Cooperative Votes
  • chapters — Chapters
  • chapter_members — Chapter Members
  • groups — Groups
  • group_members — Group Members
  • farmer_profiles — Farmer Profiles

Users / Auth / Accounting

  • users — Users
  • user_infos — User Info (addresses JSON, profile extras)
  • personal_access_tokens — API personal access tokens
  • accounts — Accounting accounts
  • account_transactions — Account Transactions
  • member_ledgers — Member Ledgers
  • global_transactions — Global Transactions (cross-domain ledger)

Properties / Referrals

  • properties — Properties
  • referrals — Referrals
  • referral_keys — Referral Keys

Files / System

  • file_list — File List (uploaded file index)
  • file_content — File Content (binary/blob storage)
  • landing_pages — Landing Pages
  • announcements — Announcements
  • system_settings — System Settings (key/value, JSON values supported)
  • db_backups — Database Backup records
  • logs — Generic Logs
  • table_logs — Per-table audit logs (model: App\Models\Generic\TableLog)

Performance / Load-Testing API

  • Purpose: Token-gated REST endpoints for hammering the box from a client machine over curl — exercise creation paths (users/stores/products) and the POS hot path while capturing per-op timing in milliseconds.
  • Controller: app/Http/Controllers/Market/PerformanceController.php (acts as an ULTIMATE system user; pick a specific actor via env PERF_ACTOR_HASH).
  • Routes (declared in routes/api.php, true API routes — NOT in web.php):
    • GET /api/perf/ping — token check / health.
    • POST /api/perf/seed/users — body {count, type?, parent_hash?, prefix?}.
    • POST /api/perf/seed/stores — body {count, owner_hash?, category?, prefix?}.
    • POST /api/perf/seed/products — body {count, target_store_hash?, attach_to_store?, prefix?}.
    • POST /api/perf/seed/batch — body {users?, stores?, products?, type?, category?, prefix?} runs all three, returns per-phase ms.
    • POST /api/perf/pos/simulate — body {store_hash, items?, cycles?, complete?} runs N end-to-end POS cycles (open session → bulk insert N line items via raw pos_transactions insert → optional complete). Reports avg/min/max cycle_ms plus per-cycle open_ms/add_items_ms/complete_ms.
  • Auth: header X-Perf-Token: <env PERF_API_TOKEN> (also accepted as ?token= query/body). If env unset, every endpoint 403s — feature is off by default.
  • Limits: each seeder is clamped to 1..1000 per call; POS sim is clamped to ≤200 items per cycle and ≤100 cycles per call.

Rule of thumb

  • Plural English names (users, accounts, shipments, organizations, properties, couriers, carts, groups, chapters, announcements, landing_pages, system_settings, personal_access_tokens) → safe to assume the table name matches.
  • Abbreviated tables (str, cst, prd_items, prd_str, prd_trx, prd_trx_ses, prd_trx_ses_arc, org_str) → never guess — use the canonical name from this list.
  • When writing unique:<table>,<col> or exists:<table>,<col> rules, look up the model's protected ?string $table value rather than pluralizing the model class name.