feat(latepoint): add LatePoint booking integration - #197
Conversation
Ports the LatePoint write actions from Bit-Pi: create/update/cancel booking, create agent, create customer, create order, and create/update coupon. The free side only resolves the field map and fires bit_integrations_latepoint_*; all LatePoint model work lives in the Pro plugin. Bit-Pi's read methods (findBookingById, findAgentByEmail, findCustomerByEmail) are not ported — they are lookups rather than actions. listBundles became the bundle dropdown instead. Agent, service, location and bundle are per-flow configuration, so they are fetched dropdowns rather than field-map rows; everything the flow varies per run stays mappable. The lists are read straight from the latepoint_* tables because the Os*Model classes only autoload on LatePoint's own screens, and are held in component state so they are never serialized into flow_details. validateLatePointConf covers what the field map cannot express: the required dropdown ids, and the customer fields whose necessity depends on the selected customer option. Both the create wizard and the edit screen call it, so neither can save a flow that would fail on every run.
There was a problem hiding this comment.
Code Review
This pull request introduces a new integration for the LatePoint booking plugin, adding backend controllers, API helpers, and routes, alongside frontend configuration screens, action layouts, and field mapping components. The review feedback suggests ensuring that non-persisted configuration fields (such as latePointFields) are rebuilt on mount when editing an integration to prevent empty options or crashes. Additionally, several controller methods in LatePointController (refreshAgents, refreshServices, refreshLocations, and refreshBundles) should be declared as static since they do not access instance state and are used as static route callbacks.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| useEffect(() => { | ||
| const keys = listsForAction(mainAction) | ||
|
|
||
| if (keys.length > 0) { | ||
| refreshLatePointLists(setLists, setIsLoading, keys) | ||
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [mainAction]) |
There was a problem hiding this comment.
When editing an integration, non-persisted configuration fields like latePointFields must be rebuilt on mount from the saved action to prevent empty options and component crashes. Rebuilding latePointFields here ensures that the field mapping section renders correctly when editing an existing flow.
| useEffect(() => { | |
| const keys = listsForAction(mainAction) | |
| if (keys.length > 0) { | |
| refreshLatePointLists(setLists, setIsLoading, keys) | |
| } | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [mainAction]) | |
| useEffect(() => { | |
| if (mainAction) { | |
| const keys = listsForAction(mainAction) | |
| if (keys.length > 0) { | |
| refreshLatePointLists(setLists, setIsLoading, keys) | |
| } | |
| if (!latePointConf.latePointFields) { | |
| setLatePointConf(prevConf => | |
| create(prevConf, draftConf => { | |
| draftConf.latePointFields = fieldsByAction[mainAction] || [] | |
| }) | |
| ) | |
| } | |
| } | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [mainAction]) |
References
- When editing an integration, ensure that non-persisted configuration fields (like static fields) are rebuilt on mount from the saved action without modifying the saved field mappings, to prevent empty options and component crashes.
There was a problem hiding this comment.
Not applying — the premise does not hold here, though there is a real point underneath it.
latePointFields is persisted. It is written into conf when the action is chosen:
draftConf.latePointFields = fieldsByAction[value] || []conf is serialized into btcbi_flow.flow_details on save, so the edit screen gets it back from the database and the field-map section renders. FluentCartIntegLayout stores fluentCartFields the same way — it is the established pattern for this component family.
There is also no crash path. Both read sites are guarded:
{mainAction && latePointConf.latePointFields && ( … )}
latePointConf?.latePointFields?.filter(fld => fld.required === true) || []The genuine issue your suggestion gestures at is staleness, not absence: because the field definitions are persisted, a future release that adds a field to CreateBookingFields will not surface it for flows saved before that release. Deriving latePointFields from mainAction at render time would fix that and drop the copy from flow_details — but it changes the saved config shape and applies equally to FluentCart and the rest of the family, so it belongs in its own change rather than this PR.
| wp_send_json_success(true); | ||
| } | ||
|
|
||
| public function refreshAgents() |
There was a problem hiding this comment.
Since refreshAgents does not access any instance properties ($this) and is registered as a static route callback, it should be explicitly declared as static to maintain consistency and adhere to coding standards.
public static function refreshAgents()References
- In PHP, controller methods that do not access instance state (
$this) and are registered as static route callbacks should be explicitly declared asstaticto maintain consistency and adhere to coding standards.
There was a problem hiding this comment.
Not applying — this is a style preference rather than a defect, and it applies to all four refresh* methods on this controller.
Route::action() dispatches through reflection and supports both forms explicitly:
$response = $reflectionMethod->invoke($reflectionMethod->isStatic() ? null : new $invokeable[0](), $data);A non-static callback is instantiated and invoked correctly, so nothing is broken. There are 30 non-static refresh* methods across backend/Actions/ today, and this controller was modelled on FluentCartController, whose refresh methods are also non-static.
Static is the wider majority (64 vs 30), so this is a fair thing to standardise — but as a single sweep across the directory rather than only on the newest integration. The same suggestion was raised and declined for the same reason on #196, and I would rather LatePoint and BadgeOS stay consistent with each other than have one of each.
| wp_send_json_success($response, 200); | ||
| } | ||
|
|
||
| public function refreshServices() |
There was a problem hiding this comment.
Since refreshServices does not access any instance properties ($this) and is registered as a static route callback, it should be explicitly declared as static to maintain consistency and adhere to coding standards.
public static function refreshServices()References
- In PHP, controller methods that do not access instance state (
$this) and are registered as static route callbacks should be explicitly declared asstaticto maintain consistency and adhere to coding standards.
There was a problem hiding this comment.
Same as the refreshAgents thread above — not applying, for the same reason. Route::action() invokes non-static callbacks correctly via reflection, and this controller follows FluentCartController. Worth standardising across backend/Actions/ as one sweep rather than on this integration alone.
| wp_send_json_success($response, 200); | ||
| } | ||
|
|
||
| public function refreshLocations() |
There was a problem hiding this comment.
Since refreshLocations does not access any instance properties ($this) and is registered as a static route callback, it should be explicitly declared as static to maintain consistency and adhere to coding standards.
public static function refreshLocations()References
- In PHP, controller methods that do not access instance state (
$this) and are registered as static route callbacks should be explicitly declared asstaticto maintain consistency and adhere to coding standards.
There was a problem hiding this comment.
Same as the refreshAgents thread above — not applying, for the same reason. Route::action() invokes non-static callbacks correctly via reflection, and this controller follows FluentCartController. Worth standardising across backend/Actions/ as one sweep rather than on this integration alone.
| wp_send_json_success($response, 200); | ||
| } | ||
|
|
||
| public function refreshBundles() |
There was a problem hiding this comment.
Since refreshBundles does not access any instance properties ($this) and is registered as a static route callback, it should be explicitly declared as static to maintain consistency and adhere to coding standards.
public static function refreshBundles()References
- In PHP, controller methods that do not access instance state (
$this) and are registered as static route callbacks should be explicitly declared asstaticto maintain consistency and adhere to coding standards.
There was a problem hiding this comment.
Same as the refreshAgents thread above — not applying, for the same reason. Route::action() invokes non-static callbacks correctly via reflection, and this controller follows FluentCartController. Worth standardising across backend/Actions/ as one sweep rather than on this integration alone.
✅ WordPress Plugin Check Report
📊 ReportAll checks passed! No errors or warnings found. 🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check |
Description
Adds LatePoint as an action integration and registers it as a trigger source.
Eight write actions — create/update/cancel booking, create agent, create
customer, create order, and create/update coupon — plus the UI for configuring
them.
The free plugin resolves the field map and fires
bit_integrations_latepoint_*;all LatePoint model work lives in bit-integrations-pro.
Motivation & Context
LatePoint is a widely used WordPress appointment plugin that Bit-Pi already
integrates with, but Bit Integrations had no support for it at all. This closes
that gap so booking flows (create a booking on form submit, sync a customer to a
CRM when a booking is made) can be built here.
Bit-Pi's read methods —
findBookingById,findAgentByEmail,findCustomerByEmail— are deliberately not ported: they are lookups, notactions.
listBundlesbecame the bundle dropdown instead.Related Links: (if applicable)
Type of Change
Key Changes
Backend
LatePointControllerwith an activation check, an authorizeendpoint, and four dropdown endpoints for agents, services, locations and
bundles
RecordApiHelperdispatching the eight actions to their Pro hooks,logging every path through
LogHandler::saveLatePointtoAllTriggersNameand tocustomFormIntegrations,registering the Pro trigger with the flow builder
Frontend
integration layout, Utilities section, create and edit wizards
validateLatePointConf, shared by the create wizard and the editscreen so neither can save a flow that would fail on every run
Field mapping
are per-flow configuration rather than values that vary per run
Utilities, so they only apply when explicitly enabled
Checklist
Notes for reviewers
against a live install; verification was static only (
php -l, php-cs-fixer,ESLint, Prettier, plus hook-argument and field-key parity checks).
latePointentry intutorialLinks.js, so the tutorial boxrenders empty (degrades gracefully, same as FluentCart).
Changelog