Skip to content

Split Bookeo and 5c SMS into modules of their own - #40

Merged
DorwardTech merged 4 commits into
claude/zone3-darwin-internal-tool-YQKKNfrom
claude/split-bookeo-sms-modules
Aug 3, 2026
Merged

Split Bookeo and 5c SMS into modules of their own#40
DorwardTech merged 4 commits into
claude/zone3-darwin-internal-tool-YQKKNfrom
claude/split-bookeo-sms-modules

Conversation

@DorwardTech

Copy link
Copy Markdown
Owner

Phase 1 of the plan. A run sheet needs Bookeo; booking reminders need SMS. Neither has anything to do with the birthday radar, and both clients lived inside it — so this extracts them before the second consumer arrives rather than after.

Module Contents
Modules/Bookeo BookeoClient, config/bookeo.php, bookeo:check, the API notes, the credential-leak tests
Modules/Sms SmsClient, config/sms.php, MobileNumber, GsmMessage, the API notes, the client tests
Modules/Crm Everything else, now declaring dependencies() => ['bookeo', 'sms']

Both are integration modules — no navigation, no settings page, no operator guide, because there's nothing there for whoever runs the venue. They still appear on /admin/modules, which is the point: when Bookeo credentials are the problem, the module list is where somebody will look.

Singletons on purpose, with a test pinning it. BookeoClient paces itself against Bookeo's rate limit through a shared cache key — two instances would each think they had the whole budget.

Behaviour-neutral by construction

  • No table moved.
  • No app_settings key renamed.
  • No env var renamed — only which config file reads it. BOOKEO_API_KEY and friends are untouched, so nothing about a deployment moves with this.

The one operator-visible change is crm:bookeo-checkbookeo:check. The guide and both READMEs moved with it, and there are no stale references left.

The deploy trap this had to handle

ModuleRegistry::sync() inserts a newly discovered module disabled, and ModuleSeeder only auto-enables a fixed list. Without intervention, the first deploy would leave CRM enabled while both modules it now depends on were off.

Nothing would visibly break — service providers register their bindings regardless of enabled state, which is deliberate so a disabled module can still be diagnosed from a shell — but the next person to toggle CRM off and on again would hit Module [crm] depends on [bookeo], which is not enabled with no idea why.

So: a migration enables them where CRM is already on, and ModuleSeeder lists them first for fresh installs (enable() refuses a module whose dependencies aren't already up).

What deliberately did not move

The whole inbound side stays in CRM — webhook receiver, deliveries table, opt-out list, reply store, both screens.

ReplyChecker matches an inbound message back to crm_sms_sends to work out which customer and child it belongs to. Moving it as-is would make Sms depend on Crm, which depends on Sms.

Breaking that needs an attribution hook — Sms asks "who is this number?", Crm answers, an unrecognised number stays unattributed (already the documented behaviour). Worth doing when a second sender exists, not before. It's written up in Modules/Sms/README.md along with the consequence in the meantime, stated plainly:

anything new that sends SMS must check the CRM module's opt-out list itself. That coupling is the reason to finish the job.

bookeo:check lost its write-back advisory for the same reason — it was reading a crm.* setting. It now reports the customer-write permission factually without naming who wants it; crm:bookeo-log-backfill --dry-run already says what its absence means for the SMS mirror.

Verification

Mechanical and wide: 19 files had an import rewritten, 5 had a config read repointed. I checked each of these came back clean —

  • no Modules\Crm reference anywhere in the two new modules
  • no stale import of the four moved classes anywhere in the repo
  • no stale config('crm.sms.*') / config('crm.bookeo.*') read
  • every changed PHP file lints
  • no duplicate test helpers (the guard added in Retry the Bookeo write-back on its own schedule, not the reply checker's #39)
  • composer validate passes and the lockfile needs no change — the root Modules\ → Modules/ PSR-4 mapping already covers the new namespaces

10 new tests in tests/Feature/Modules/IntegrationModuleTest.php cover the wiring that fails silently: both modules discovered, each client shared not transient, bindings available while the module is disabled, CRM's declared dependencies, enable refused before dependencies are up, disable refused while CRM uses them, and credentials read from the config files that moved.

vendor/ can't be installed in this environment (codeload.github.com is blocked by the proxy), so CI is the verification for the suite itself.

Next

Phase 2 — opt-out list and webhook receiver into Modules/Sms, behind the attribution hook — when you want it. That one touches the webhook URL and live settings rows, so it deserves its own PR.


Generated by Claude Code

claude added 4 commits August 3, 2026 01:30
A run sheet needs Bookeo. Booking reminders need SMS. Neither has anything
to do with the birthday radar, and both clients lived inside it — so this
extracts them before a second consumer arrives rather than after.

Modules/Bookeo  BookeoClient, config/bookeo.php, bookeo:check, the API
                notes, the credential-leak tests
Modules/Sms     SmsClient, config/sms.php, MobileNumber, GsmMessage, the
                API notes, the client tests
Modules/Crm     everything else, now declaring dependencies() on both

Integration modules, so neither has navigation, settings or an operator
guide — there is nothing there for whoever runs the venue. They still
appear on /admin/modules, which is the point: when Bookeo credentials are
the problem, the module list is where somebody will look.

Singletons on purpose, and there is a test for it. BookeoClient paces
itself against Bookeo's rate limit through a shared cache key; two
instances would each think they had the whole budget.

## Behaviour-neutral by construction

No table moved, no app_settings key was renamed, and every env var name is
unchanged — only which config file reads it. Nothing about a deployment
moves with this. `crm:bookeo-check` became `bookeo:check`, which is the
one operator-visible change, and the guide and README moved with it.

## The deploy trap

ModuleRegistry::sync() inserts a newly discovered module DISABLED, and
ModuleSeeder only auto-enables a fixed list. So the first deploy after
this would have left CRM enabled while both modules it now depends on were
off. Nothing would visibly break — providers register their bindings
regardless of enabled state, which is deliberate so a disabled module can
still be diagnosed from a shell — but the next person to toggle CRM off
and on would hit "depends on [bookeo], which is not enabled" with no idea
why. A migration enables them where CRM is already on; ModuleSeeder covers
fresh installs.

## What did not move, and why

The whole inbound side stays in CRM: the webhook receiver, the deliveries
table, the opt-out list, the reply store and both screens. ReplyChecker
matches an inbound message back to crm_sms_sends to work out whose it is,
so moving it as-is would make Sms depend on Crm, which depends on Sms.

Breaking that needs an attribution hook — Sms asks "who is this number?"
and Crm answers — which is worth doing when a second sender exists, and is
noted in Modules/Sms/README.md along with the consequence in the meantime:
anything new that sends SMS has to check CRM's opt-out list itself.

bookeo:check lost its write-back advisory for the same reason. It now
reports the customer-write permission factually, without naming who wants
it; crm:bookeo-log-backfill --dry-run already says what its absence means
for the SMS mirror.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014E51bA27LnFMK7v4YigZY1
212 failures, and the rule causing them is the one this PR adds: enable()
refuses a module whose dependencies are not already on, and nine test files
enable 'crm' directly.

Fixed in tests/Pest.php rather than nine times over, and by reading
dependencies() off the manifest rather than hardcoding ['bookeo','sms'] —
the same list would otherwise need updating by hand the day it changes.
Production solves the same problem in ModuleSeeder by ordering its list.

IntegrationModuleTest keeps calling the registry directly: it is asserting
the dependency rule itself, so going through a helper that satisfies
dependencies for it would test nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014E51bA27LnFMK7v4YigZY1
The 92 remaining failures were eight files that a repo-wide grep called
clean, because none of them ever named the class in full.

SmsClient and BookeoClient lived in Modules\Crm\app\Services alongside
RadarSender, RadarBatchBuilder, OwnerNotifier, ReplyChecker, BookeoSmsLog
and DashboardSummary. A sibling in the same namespace resolves with no
`use` statement — so when the two clients moved out, there was no old FQN
anywhere to find and rewrite, and every one of those services broke with
"Class not found" at runtime. CoworkStateParser was the same story with
MobileNumber.

That is the failure mode of a namespace move, and searching for the thing
you moved cannot see it. So there is now a test for it, with two halves
that catch opposite mistakes: one resolves every first-party `use` to a
file that must exist, the other finds a watched class used with no import
and no class of that name in the file's own namespace. The second is the
one that would have caught this.

Both were run against the tree before being written into a test, and the
second was confirmed to fail on the real breakage rather than merely
return empty — the fix script had reported "ADDED" for CoworkStateParser
without checking, and its `use` block never landed because that file had
no existing imports for the regex to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014E51bA27LnFMK7v4YigZY1
Last failure: the module went to 2.1.0 and this test asserted '2.0.0'.

Rewritten to assert what the test is actually for — that the manifest, the
rendered page and GUIDE.md all state the same version. The literal made
every release edit this test while catching nothing it claims to: a bump
that updated the manifest and forgot the guide would still have passed, so
long as somebody changed the literal too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014E51bA27LnFMK7v4YigZY1
@DorwardTech
DorwardTech marked this pull request as ready for review August 3, 2026 01:46
@DorwardTech
DorwardTech merged commit bc9f01b into claude/zone3-darwin-internal-tool-YQKKN Aug 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants