We are creating a full Store Locator Plus accounting dashboard to track both the SaaS platform as well as WordPress plugin sales.
Sales Data
The primary source of truth for sales data will come from Stripe. Any references to PayPal payments or accounts can be considered legacy information and can be left out of the accounting panel.
The payment module holds the Stripe API module and related code for payment system communications.
User Interface
Stage 1 : Accounting Overview Scaffolding
Accounting Sidebar Menu
A new sidebar menu should be created in WordPress for super admin users only. \MySLP::create_network_admin_menu should be used to create and attach the menu. Follow the design pattern for the configure menu option, code snippet follows:
Unlike the main configure menu page that is rendered via \MySLP::render_configuration_page the new accounting module should load and render a React PHP helper class. We want this interface to be primarily React driven.
The WordPress PHP React Helper Class
Follow the \MySLP_Customer_Profile class for guidance on implementing a React user interface within WordPress. I suggest creating a new class MySLP_Accounting in wp-content/plugins/myslp-dashboard/include/accounting.
The React AccountingPanel Component
The React components that \MySLP_Accounting loads with the help of the wp-scripts node package should live in wp-content/plugins/myslp-dashboard/src/accounting. This directory should have a block.json and an accounting.tsx file.
block.json should probably look like this:
{
"script": "file:profile.js"
}
The accounting.tsx TypeScript should contain the React accounting component. This new React component should be named AccountingPanel. It should follow the UX design pattern of the ProfilePanel component in wp-content/plugins/myslp-dashboard/src/profile/profile.tsx
The initial user interface will only have a single submenu (SLPTabsBar Tabs components) for the main page which should be labelled “Overview” which is already selected and open. It will render the AccountingOverviewPanel component noted below.
AccountingOverviewPanel Component
A supporting AccountingOverviewPanel React component should be created. If the WordPress wp-scripts supports it, this can go in the same directory as the AccountingPanel component : if not place it in wp-content/plugins/myslp-dashboard/src/accounting_overview
This component should be a React Dashboard that shows overall accounting metrics. To start with we will want to show the total number current active accounts and total amount of payments from those active accounts. The ManageCustomers React component (wp-content/plugins/myslp-dashboard/src/manage_customers/manage_customers.tsx) displays this data in the AdminHeader children via a pair of Chips. Use the same REST endpoints to fetch this data and display it.
Here is a screen shot of the Material UI Dashboard Template that is referenced.
For now we need the cards for:
Accounts in place of Users with the active count below. We will add a data table for historical trends and a graph below it in the future. Clicking on the user count in the account card should redirect the UX to load the Manage Customers page at /wp-admin/network/admin.php?page=customer_list
Payments in place of Conversions with the total payment amount below. This too will eventually have trends stored in a data table that we will create later.
AI Summary
Junie Pro Summary
Completed setup and implementation prep for the accounting dashboard work in myslp-dashboard, including running required start-revision on branch accounting.
• Implemented Stage 1 accounting UI scaffold: new Accounting admin page, React entrypoint, and overview metrics/cards with click-through to Manage Customers.
• Added new files: include/accounting/MySLP_Accounting.php, src/accounting/accounting.tsx, and src/accounting/block.json.
• Updated integration points in include/MySLP.php and constants/version metadata in myslp-dashboard.php and package.json.
• Validation status: lint checks for new files passed (only a non-blocking TS import style warning noted earlier), diff checks passed, and you confirmed npm install + npm run build succeeded locally with generated build/accounting/accounting.js and build/accounting/accounting.asset.php present.
• Environment note: Junie shell still lacks local php tooling and direct GitHub auth; you requested follow-up guidance on PAT-based auth after task completion.
Junie Release Notes • Scope: No new product-code changes were made after the previously summarized accounting dashboard work in myslp-dashboard. • Skills/Environment updates this session: Reloaded skills via Vendors/JetBrains/relink_skills.sh; available skills now include /wordpress-release-notes. • Code impact since last implementation summary: None. • Build/Test status: No additional build or test runs were performed after the earlier confirmed successful npm install and npm run build for myslp-dashboard. Included Previously Completed Work (for continuity) • Added Accounting admin page scaffold and React entry (include/accounting/MySLP_Accounting.php, src/accounting/accounting.tsx, src/accounting/block.json). • Wired menu/render integration in include/MySLP.php and version bump artifacts from start-revision (myslp-dashboard.php, package.json).
The Settings Interface architecture drives most of the legacy Store Locator Plus admin interfaces. These are the interfaces customers interact with most often, especially on the SaaS platform. It is the main component of the SLP Dashboard for SaaS.
It was originally written to manage the UI surfaces that present the myriad of settings (options) available to customers. It presents the “dials the users turn” to change how their store locator maps and directory presentations are configured. It grew to cover other admin UI interfaces including the Locations Interface.
Outputs the HTML for settings pages, including the location details page (it uses the “settings” page construct). The output all ends up in a HTML element: <div class=’dashboard-wrapper’>…</div>.
General layout:
<div class=’dashboard-wrapper’>
<header id=”dashboard-header” class=”dashboard-header”> = React blue bar header
// -- include the asset file to get the WordPress Scripts defined dependencies and version ID
$asset = include SLPLUS_PLUGINDIR . 'build/slp_adminheader/script.asset.php';
wp_enqueue_script( 'slp_adminheader', SLPLUS_PLUGINURL . '/build/slp_adminheader/script.js', $asset['dependencies'], $asset['version'], true );
wp_add_inline_script( 'slp_adminheader', 'const slpReact = ' . wp_json_encode( $this->get_vars_for_react() ) . ';', 'before' );
Rendered via the React component at wp-content/plugins/store-locator-plus/src/components/AdminHeader.js
In the current version the header element DOES NOT contain the sub navbar.
It is rendered via SLP_Settings::sublevel_navbar deep inside the <div class=’dashboard-wrapper’>…</div> element.
React Update To Render Subnavbar
The goal is to render the \SLP_Settings::sublevel_navbar HTML output using React instead of the current PHP and HTML implementation.
The AdminHeader at wp-content/plugins/store-locator-plus/src/components/AdminHeader.js renders the top-of-page header with the page name and some interactive icon buttons.
The task is to add a submenu below the Toolbar that is comprised of the sections currently residing in $this->sections in the \SLP_Settings::sublevel_navbar method. Using the $titleText ($section->name) within. The wp_kses_post function current used to set $titleText is not necessary as the $section->name is already considered "clean".
The best way to get the variables into React so they are accessible in JavaScript is to modify the \SLP_Base_ReactObject::get_vars_for_react method. Instead of attempting to override or extend the base \SLP_Base_ReactObject::get_vars_for_react method, add a filter in \SLP_Settings for slp_react_vars. That will extend the react variable array via this return method on the end of the get_vars_for_react method:
apply_filters( 'slp_react_vars', $defaultVars );
You will find a good reference implementation of the slp_react_vars filter via:
\Customer_Profile_Site_Info::initialize - adds the filter for that class
\Customer_Profile_Site_Info::extendReactVars - extends the available JavaScript variables for React
In our case we are not extending MySLP variables, so the SLP_Settings::extendReactVars method should be more like this:
$vars['SLP']['sections'][] = array(
'name' => $section->name,
'div_id' => ! empty( $section->div_id ) ? $section->div_id : $section->slug,
'link_id' => "wpcsl-option-{$div_id}"
);
This should be set up by looping through $this->sections in a new extendReactVars method in SLP_Settings.
This should create a horizontal navbar that shows and reveals each section by the div id. This may require further refinement as the current system uses outdated JavaScript and jQuery to hide and reveal subsequent divs by using the div IDS as noted in the link_id property. For now we will assume that if React renders the components correctly the pre-existing JavaScript will take over.
Initial Results
Works on locations page. Does not work on Settings page. Spacing is not well defined, needs more space between menu items.
The locations page is a bastardized combination of the SLP_Settings class and WP_List_Table.
The top-of-page header appears to be handled by SLP_Settings. SLP_Settings extends SLP_Base_ReactObject To start rendering the page is calls \SLP_Base_ReactObject::render with these attributes: – $scriptHandle = ‘slp_settings’ – $scriptFilebase = ‘slp_settings’ This methodology ends up render the SLPSettingsPanel React component (wp-content/plugins/store-locator-plus/src/slp_settings/slp_settings.tsx)
That means the page header is rendering using the Reactionary standard <AdminHeader> for the page. Currently SLP_Settings is only leverage the pageName property, but it also supports sections as a property which is the submenu.
With the 2606.26.01 release the Settings system uses the React AdminHeader menu.
\SLP_Settings::render_settings_page
$this->sections may have is_topmenu set, this indicates the top level menus in the app
Locations Card (Defunct?)
This may no longer be used: \SLP_Admin_Locations::create_location_details_box
SLP_Admin_Locations
extends WP_List_Table – this is already going to be a shit show. The WP_List_Table interface is from 1902. Maybe before that. It is awful.
Referenced by:
\MySLP_Admin::do_admin_startup()
Setting the screen property , probably not needed: SLP_Admin_Locations::get_instance()->screen = ‘store-locator-plus_page_slp_manage_locations’;
\SLP_Power_Pages_Admin::extend_location_edit()
Only if Store Pages is active, add an edit section to show an “edit Store Page” link
\SLP_Power_Pages_Admin::set_page_actions()
Add the recreate store page action hyperlink under each location on the table
\SLP_Actions::save_screen_options()
To save the screen options, which is currently only the page (table rows) length.
\SLP_Admin_Locations_Add::
initialize
extended_data_block – for extended data fields
\SLP_Admin_UI::renderLocationsPage
To render the UI
\SLP_Location_LoadFromWP::import
To only report back any URL the person puts in is invalid.
This is called from \SLP_Admin_Locations_Actions::process_actions when the current_action is load_from_wp
This is setup from \SLP_Admin_Locations_Load
\SLP_Settings_manage_locations_table::display
A shim for $this->settings->add_ItemToGroup( array( ‘group_params’ => $group_params, ‘type’ => ‘manage_locations_table’, ) );
SLP_Admin_Locations
Hooks & Filters Fired
slp_locations_manage_bulkactions : creates the list of bulk action dropdown items
SLP_Power_Admin_Locations
SLP_Experience_Admin
SLP_Power_Pages_Admin
slp_locations_manage_filters : The filter location drop down items
SLP_Power_Admin
slp_locations_manage_cssclass : Adds extra CSS classes
SLP_Experience_Admin
slp_column_data : Change the data rendered in a column
slp_manage_locations_actionbar_ui : modify the action bar on top of the table
slp_modify_admin_locations_script_data : modify the script data for the JavaScript on the page
slp_manage_location_columns
pls_manage_location_where
slp_manage_locations_buttons
slp_build_locations_panels
Not used:
slp_invalid_highlight
slp_edit_location_change_extended_data_info
slp_manage_expanded_location_columns
Screen for WP_List_Table
This is messing up the UI. Let’s see if we can drop this, start peeling away layers to simplify this interface as we work toward a React version.
locations_per_page
Tracked via \SLPlus::$userMetaKeys
Stored in user meta via \SLP_AJAX::update_slp_user_meta
\SLP_Actions::init sets a filter on set-screen-option … it calls…
\SLP_Actions::save_screen_options … which only runs if the current page is slp_manage_locations… if so it calls…
\SLP_Admin_Locations::save_screen_options
Which returns the value of locations_per_page
Best option is to make this a React interface for page length / pagination
homeUrl most likely comes from the PHP class \MySLP_Manage_Customers::extendReactVars set to WordPress get_home_url()
REST Backend
SaaS App backend via MySLP Dashboard plugin.
— Fetching Customers PHP method \MySLP_REST_API::register_routes defines the registered routes for WordPress. register_rest_route( $this->myslp_namespace, ‘/customers’,…) Calls the PHP method \MySLP_REST_API::get_customers
Location count is coming from $this->myslp->User->location_count
Root Cause Theory
This appears to be using a meta_query to fetch the user location data. This is NOT accurate.
In some cases the MySLP_User object does not have a location_count user_meta property set. If that is the case, it should call \SLP_Location_Manager::get_location_count for that user and store the result with
User Location Count Architecture
\MySLP_User::__get
Fetched from user_meta with the location_count property. This is likely where the AI decided to make this a source of truth for location counts.
case 'location_count':
case 'mapview_count':
$this->__get( 'user_meta' );
$this->$property = (int) ( $this->user_meta[ $property ][0] ?? 0 );
break;
\MySLP_REST_API::get_location_count_for_user
Currently unused anywhere in the project. This would ensure the app switched to the user’s blog and set_database_meta() then called: \SLP_Location_Manager::get_location_count
\SLP_Location_Manager::get_location_count
This is the original method from the legacy app code to fetch location counts. It queries the custom SLP database that is added for every user to get the count of records. It comes from the linchpin Store Locator Plus base plugin.
With the SaaS dashboard there is a Manage | Customers option that displays the customer list. It is using a default WordPress table style presentation that has been modified by one of the SaaS plugins, most likely MySLP Dashboard (myslp-dashboard). I would like to make improvements to this interface.
This issue comes up for older accounts where their wp_options table has their theme set to twentytwelve. If these accounts time out it does NOT flush the cookie (shit WordPress design) and when you re-visit the SaaS dashboard site (staging or production) you get the error message noted above.
In JavaScript console on the Location Details page: Description
[Error] Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it’s running React 17. Learn more: https://reactjs.org/link/switch-to-createroot printWarning (react-dom.js:73) error (react-dom.js:47) render (react-dom.js:29680) (anonymous function) (script.js:101:67958) Global Code (script.js:101:68032)
Git repositories that support projects needing Docker images and containers should follow this directory standard. Any project that uses vendor tools or apps should store support data in a similar subdirectory structure. Notes here are relative to the Repository Root.
Docker Files
Composer
The Docker compose file should end up here: ./Vendors/Docker/Composers
This is for files the build and launch containers.
Images
This is for stuff that builds images used to launch custom containers.
The files that support building images should end up here: ./Vendors/Docker/Images Dockerfile or Dockerfile-openclaw may live here.
Supporting files for building images go in these directories: ./Vendors/Docker/Images/Files For files that are put in the SLP SaaS repo that support image building, public files. This often contains subdirectories for files to copy from the host (stored in the SLP_SaaS repo) to the guest. For example: ./apache/sites-avaiable/000-default.conf – for a Docker container with Apache websites ./php/docker-php-ext-redix.ini – for a Docker container with PHP and Redis support ./ssl/_wildcard.storelocatorplus.com+2.pem – for an SSL cert Things in Files/* in this Docker folder are often copied via Dockerfile to build the image
./Vendors/Docker/Images/Secrets It can contains supporting secret files that do not get commited to the repo. For example: do-not-commit-codebuild-vars.env This is for environment variables needed for Code Build on AWS with values set like AWS_DEFAULT_REGION=us-east-1 The README instructions would say something like “copy ./Images/Secrets-Examples/codebuild-vars.env to ./Images/Secrets/do-not-commit-codebuild-vars.env
SLP_SaaS_Docker_Directory_Standard.md
# SLP SaaS — Docker Directory Standard
This document captures the current **directory layout conventions** for Docker-related assets used with the **SLP SaaS** project, as described by Lance.
## Project roots (MBP)
- **SLP SaaS repo root**
- `/Users/lancecleveland/phpStorm Projects/SLP_SaaS`
- **Testing area (under SLP_SaaS)**
- `/Users/lancecleveland/phpStorm Projects/SLP_SaaS/Testing/`
- **myslp-cypress repo (local checkout)**
- `/Users/lancecleveland/phpStorm Projects/SLP_SaaS/Testing/myslp-cypress`
## Canonical Docker directory structure (within myslp-cypress)
All Docker-related standards below are relative to:
- `/Users/lancecleveland/phpStorm Projects/SLP_SaaS/Testing/myslp-cypress/Vendors/Docker/`
### 1) Compose files
**Docker Compose files** should live here:
- `Vendors/Docker/Composers/`
Example (full path):
- `/Users/lancecleveland/phpStorm Projects/SLP_SaaS/Testing/myslp-cypress/Vendors/Docker/Composers`
Notes:
- This is the standard landing zone for any new compose setup (e.g., an E2E Eddie/OpenClaw + Cypress compose).
### 2) Image build definitions
**Files that support building images** should live here:
- `Vendors/Docker/Images/`
Example (full path):
- `/Users/lancecleveland/phpStorm Projects/SLP_SaaS/Testing/myslp-cypress/Vendors/Docker/Images`
Notes:
- Dockerfiles may live here, including variants like `Dockerfile` or `Dockerfile-openclaw`.
### 3) Public build-support files (committed)
**Supporting files used during image builds** (intended to be committed) should live here:
- `Vendors/Docker/Images/Files/`
Example (full path):
- `/Users/lancecleveland/phpStorm Projects/SLP_SaaS/Testing/myslp-cypress/Vendors/Docker/Images/Files`
Conventions:
- This directory often contains subdirectories that mirror container filesystem targets.
- Contents are typically copied into an image via `Dockerfile` using `COPY ...`.
Examples of typical contents:
- `./apache/sites-available/000-default.conf` — Apache site config
- `./php/docker-php-ext-redix.ini` — PHP extension/config file (example)
- `./ssl/_wildcard.storelocatorplus.com+2.pem` — SSL cert material (example)
### 4) Secrets (NOT committed)
**Supporting secret files** (not committed to the repo) should live here:
- `Vendors/Docker/Images/Secrets/`
Example (full path):
- `/Users/lancecleveland/phpStorm Projects/SLP_SaaS/Testing/myslp-cypress/Vendors/Docker/Images/Secrets`
Conventions:
- This may contain env var files or other sensitive build/runtime inputs.
- Example secret file:
- `do-not-commit-codebuild-vars.env`
Example usage pattern to document in READMEs:
- “Copy `./Images/Secrets-Examples/codebuild-vars.env` to `./Images/Secrets/do-not-commit-codebuild-vars.env` and edit values (e.g., `AWS_DEFAULT_REGION=us-east-1`).”
### 5) Secrets examples (committed templates)
**Example secret files** (templates that *are* committed) should live here:
- `Vendors/Docker/Images/Secrets-Examples/`
Example (full path):
- `/Users/lancecleveland/phpStorm Projects/SLP_SaaS/Testing/myslp-cypress/Vendors/Docker/Images/Secrets-Examples`
Purpose:
- Provide safe-to-commit starter files that developers can copy into `Secrets/`.
## Recommended README conventions (optional)
When adding a new compose or image:
- Put the compose YAML in `Vendors/Docker/Composers/`.
- Put the Dockerfile(s) in `Vendors/Docker/Images/`.
- Put committed build inputs in `Vendors/Docker/Images/Files/`.
- Put local-only secrets in `Vendors/Docker/Images/Secrets/`.
- Put example secrets in `Vendors/Docker/Images/Secrets-Examples/`.
## Source
Captured from Lance Cleveland’s notes in Slack (2026-03-21).
I had to update the stripe connection which meant rewriting how subscription processing is managed including cancellations.
So for the customer mcampbell_at_tnwebtech_dot_com (830.828) this is what I have as the status: original subscription 24th last renewed feb 24th cancelled mar 6th stripe charged them mar 6th AND marked it cancelled april 6th it should have marked the 24th subscription to cancel on mar 24th
Current status according to Stripe: they have set their account to cancel on April 5th Current subscription: *z8ku is deleted on Stripe now meaning it will not auto-renew
Please ensure that is what they want.
From the Stripe history: The original subscription *9h4z Started Oct 24 2023 Cancelled via SLP Dashboard on Mar 6th 2026 at 1:05AM (server time) Was set to stop providing SLP maps on Mar 24th 2026 They then renewed (created the new subscription) Mar 6th at 1:08AM (server time) This is set to expire on April 5th 2026
Yes, we have an issue with RENEW If the prior subscription is still active (*9h4z in this case) it should set the new subscription (renewal) to start when that ends (Mar 24th 2026 06:21AM server time) The bug is that is started immediately , thus the new 6th to 5th dates That is OK for renewals that happen AFTER the maps are disabled (most users) but in this unique situation it needs to be addressed.
Flexible Recommended: Provides accurate and predictable billing behavior and new capabilities. To access these improvements, which are only available in flexible billing mode, you must create new subscriptions with flexible billing mode or migrate your existing subscriptions.
*Classic: Uses the existing Stripe subscription behavior. This setting is maintained for backward compatibility with older integrations.
AI Resolution Assistance
Prompt
@Amelia -
In the MySLP Payments module (WordPress/wp-content/plugins/myslp-payments) there is an issue related to renewing cancelled subscriptions.
__
Make a note of this as general knowledge about the Store Locator Plus Saas Application:
- local (https://local.storelocatorplus.com) and staging (https://staging.storelocatorplus.com) servers may be using outdated data sets
- local and staging servers employ the Stripe TEST environment and related keys
- the production server uses live keys
- NEVER run tests against live Stripe customer data using live keys even on the staging or local servers
__
The following scenario is a real-world situation which played out on the production version of the SaaS application.
We have an issue with RENEW subscription.
- If the prior subscription is still active (sub_1O4dzSBvHKfBw2LGsKZp9h4z in this case) it should set the new subscription (sub_1T7rZBBvHKfBw2LGODU6z8ku) to start when the still-active subscription ends (Mar 24th 2026 06:21AM server time).
- The bug is that the new subscription started immediately setting a March 6th start date when an April 5th end date.
- The new subscription should have started on March 24th 2026 at 6:21AM.
- If the prior subscription is past the end (cancel_at) date, only then should the renewal start immediately. That was not the case in this situation.
Meta data about the customer and their interaction with the application:
customer: mcampbell@tnwebtech.com
Current status according to Stripe: they have set their account to cancel on April 5th
Current subscription: *z8ku is deleted on Stripe now meaning it will not auto-renew
Please ensure that is what they want.
From the Stripe history:
The original subscription *9h4z
Started Oct 24 2023
Cancelled via SLP Dashboard on Mar 6th 2026 at 1:05AM (server time)
Was set to stop providing SLP maps on Mar 24th 2026
They then renewed (created the new subscription) Mar 6th at 1:08AM (server time)
This is set to expire on April 5th 2026
AI Fix
in \stripe\MySLP_Stripe_Payments::renew_subscription add the trial_end argument.
// If the old subscription still has remaining paid time, defer the new
// subscription so it starts when the old period ends.
$prior_period_end = $this->subscription->current_period_end ?? null;
if ( $prior_period_end && $prior_period_end > time() ) {
$args['trial_end'] = $prior_period_end;
}
try {
$this->subscription = Subscription::create( $args );
E2E Testing
Write a new E2E Test Specification "subscription_renewals".
The first test in the specification needs to test "Can renew a subscription before it has expired".
This is a corner case with some specific requirements.
- Login as a user with a current active subscription
- Go to My Profile and look for the current subscription ID, remember this value
- Go to My Profile and cancel the subscription
-- The current Stripe subscription ID should be posted and marked in Stripe as canceled
-- The current subscription should have a cancellation date at the end of the current period
- Go to My Profile and renew the subscription
-- This should create a new Stripe subscription ID
-- The new Stripe subscription ID should start when the current period ends
-- The new Stripe subscription should NOT start at the date/time of the renewal
-- The new Stripe subscription should be set to renew in a month (current period ends a month later)