Skip to content

Add SmartTwin event subscriber skeleton#1936

Draft
pvankouteren wants to merge 18 commits into
developfrom
smart-twin-skeleton
Draft

Add SmartTwin event subscriber skeleton#1936
pvankouteren wants to merge 18 commits into
developfrom
smart-twin-skeleton

Conversation

@pvankouteren

@pvankouteren pvankouteren commented May 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Scaffolds the integration plumbing between this app and the future SmartTwin external API. No actual SmartTwin calls are made yet — this PR only wires up the events, listener and job skeletons so they can be filled in later.

  • App\Events\AccountVerified — dispatched from AccountObserver::updated() when email_verified_at transitions from null to a datetime. Covers every verification path (Fortify email link, admin-create auto-verify, API V1, SuperAdmin manual confirm).
  • App\Listeners\SmartTwinEventSubscriber — registered in AppServiceProvider::attachSubscribers(). Listens for:
    • AccountVerified → iterates the account's users/roles and dispatches the matching job per (user, role) for resident and coach.
    • Spatie\Permission\Events\RoleAttached → only acts when the underlying account is already verified (avoids double-dispatch with AccountVerified for fresh registrations); dispatches the matching job per attached role.
  • App\Jobs\SmartTwin\Out\CreateUserAccount and CreateCoachAccountShouldQueue jobs on the app_external queue, with empty handle() bodies as placeholders for the real SmartTwin API calls.

Why two triggers

AccountVerified and RoleAttached are designed to be each other's complement:

  • First-time verification of a brand-new account → AccountVerified is the trigger (RoleAttached had skipped because the account wasn't verified yet).
  • Later role mutations on an already-verified account (e.g. existing account joins a new cooperation, or a user gains an extra role) → RoleAttached is the trigger.

Because the SmartTwin API takes (user_id, role), each (User, role) pair maps to a distinct SmartTwin account — so dispatching per role rather than per account keeps idempotency concerns minimal.

Out of scope

  • Actual SmartTwin client / payloads / HTTP calls.
  • Idempotency safeguards inside the jobs (will be revisited when implementing).
  • RoleDetached handling.
  • Tests — skeleton only.

Test plan

  • Confirm the subscriber is registered (AppServiceProvider::attachSubscribers()).
  • Verify AccountVerified fires when an unverified account becomes verified (Fortify link, admin save, API V1, SuperAdmin confirm).
  • Verify RoleAttached handler skips while the account is unverified and dispatches once the account is verified.
  • Confirm the jobs land on the app_external queue.

Introduce AccountVerified event (dispatched from AccountObserver when
email_verified_at transitions from null to a datetime) and a
SmartTwinEventSubscriber that maps AccountVerified and Spatie's
RoleAttached to per-role CreateUserAccount / CreateCoachAccount jobs.
Job handlers are intentionally left empty — implementation of the
SmartTwin API calls comes in a follow-up.
@pvankouteren pvankouteren force-pushed the smart-twin-skeleton branch from baf5404 to 189b15e Compare May 20, 2026 08:20
pvankouteren and others added 17 commits May 20, 2026 08:44
Generalize UserDeleted second payload from $accountRelated to $context,
build it inline in UserService (no longer depends on EconobisService),
re-shape locally in EconobisEventSubscriber, and add SmartTwin DeleteAccount
job dispatched from SmartTwinEventSubscriber::handleUserDeleted.
Guzzle-based client with toggleable PSR-3 log middleware, X-Api-Key
header auth, and a User resource covering the Account API's register
and delete endpoints.
CreateUserAccount, CreateCoachAccount, and DeleteAccount now call
SmartTwinApi via DI. The create jobs persist the returned userId on
User.extra.smarttwin_user_id and skip when it is already set; all
three short-circuit when SmartTwin calls are disabled.
…be commands

- Add SMARTTWIN_API_SIGN_KEY and SMARTTWIN_PREVIOUS_API_SIGN_KEY (comma-separated) config values for zero-downtime key rotation
- Add SmartTwinSigned middleware checking X-Webhook-ApiKey header against current and previous sign keys
- Register POST api/v1/smarttwin route (api.v1.smarttwin.store) with SmartTwinSigned middleware
- Add SmartTwinController that logs incoming webhook JSON
- Add EventSubscription resource with subscribe/unsubscribe methods
- Add api:smarttwin:subscribe command (logs subscriptionId via Log::info)
- Add api:smarttwin:unsubscribe command (accepts subscriptionId arg or reads from config)
- Add SMARTTWIN_SUBSCRIPTION_ID config value to persist subscription ID
json_decode returns false (not null) for a JSON 'false' response body,
so the ?? [] fallback did not trigger. Use is_array() instead.
…ponse

The SmartTwin API currently returns the subscription ID under "Value"
instead of "subscriptionId" as documented. Check subscriptionId first,
fall back to Value until the API is corrected.
- Move SmartTwinSigned from Middleware/ to Middleware/Api/ for consistency
- Update bootstrap/app.php alias to new namespace
- Add App\Enums\SmartTwin\EventType with RESIDENT_SCAN_FINISHED and COACH_SCAN_FINISHED
- Add SHORT_SMARTTWIN_DOSSIER_ID constant to BuildingSettingHelper
- SmartTwinController now resolves the building from DossierId via building_settings,
  appends the webhook data payload to buildings.smarttwin_callback
…ding ID

- Job now implements ShouldBeUnique with uniqueId() returning "{EventType}_{buildingId}"
- Add buildingId as constructor parameter alongside callbackData
- Update observer and fallback cron command to pass building ID on dispatch
- Add Client::get() method for GET requests
- Add Advice resource with getAdvisorToolResults() and getQuickScanResults()
- Add SmartTwinApi::advice() returning the Advice resource
- Add SmartTwinService::processResults() stub
- Implement GetAdviceResults job: resolves endpoint via EventType enum, calls
  service, then removes the processed callback from buildings.smarttwin_callback
…rgument

- Add optional buildingId argument to api:smarttwin:get-advice-results command
  to target a single building; omit for all pending buildings (via ->when())
- Register command in console.php to run nightly at 03:00 with withoutOverlapping()
- Add @Property array|null $smarttwin_callback to Building docblock
  to fix assign.propertyType errors in controller and job
- Apply phpcbf code style fixes
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.

1 participant