# My Diaspora Nexus Backend Implementation Log | Date/Time | Feature | Implementation Summary | Test Run ID | Result | Evidence Path | Notes | |---|---|---|---|---|---|---| | 2026-06-19 12:52 EAT | Project setup and health foundation | Created Laravel API project in `C:\xampp\htdocs\mdn-backend`, installed required packages, configured MySQL to use `MDN_DB_*`, added `/api/v1/health` and `/api/v1/health/database`, and verified missing-env and wrong-credential behavior. | 20260619-1248-foundation | PASS | storage/test-runs/20260619-1248-foundation/ | Live DB health passed through HTTP; no credentials written to evidence. | | 2026-06-19 12:52 EAT | Shared API infrastructure | Added standardized `ApiResponse`, global request ID middleware, API exception mapping, UUID generator, transaction manager, base repository, ownership validator, and pagination validator. | 20260619-1248-foundation | PASS | storage/test-runs/20260619-1248-foundation/ | Unknown routes return standardized JSON with `X-Request-Id`; rollback command verified failed multi-step writes persist zero rows. | | 2026-06-19 12:52 EAT | Audit Service foundation | Implemented `AuditService`, repository, DTO, event constants, health-check audit events, and `php artisan mdn:audit:test` append-only verification. | 20260619-1248-foundation | PASS | storage/test-runs/20260619-1248-foundation/ | Inserted safe `AUDIT_TEST_EVENT`; update/delete attempts were rejected by live DB immutability protection. | | 2026-06-19 13:35 EAT | Identity and Access implementation | Implemented registration, email verification challenge creation/verification, login, JWT access tokens, hashed refresh sessions, refresh, logout, bearer auth middleware, permission middleware, `/users/me`, `/authorization/check`, and `mdn:create-admin`. Added redacted Step 4 API runner at `tests/api/run-auth-step4.ps1`. | 20260619-identity-step4 | PENDING LIVE ACCEPTANCE | storage/test-runs/20260619-identity-step4/ | PHP syntax and runner parser checks passed. Live HTTP acceptance was not run because approval was rejected for persistent writes to the live identity DB; requires explicit user approval before proceeding. | | 2026-06-19 13:08 EAT | Identity safe HTTP checks | Ran actual HTTP checks that do not persist live identity records: invalid email, weak password, protected route without token, and missing refresh token. Hardened login so unverified/disabled login failures write `login_events` and audit failures before rejection. | 20260619-identity-step4-safe-http | PARTIAL PASS | storage/test-runs/20260619-identity-step4-safe-http/ | Non-mutating HTTP checks passed. Full Step 4 acceptance remains pending explicit approval for marked live identity test writes. | | 2026-06-19 13:23 EAT | Identity live HTTP acceptance | Fixed live-schema issue by letting MariaDB generate `users.email_normalized`; fixed `tests/api/db-helper.php` parameter handling; ran live HTTP tests for register, duplicate email, invalid email, weak password, verification message creation, wrong/expired/valid verification token, unverified login denial, verified login, wrong password, refresh, protected `/users/me`, RBAC denial, logout, revoked refresh, disabled login, and audit signal. | 20260619-identity-step4-live6 | PASS WITH PRIVILEGED ADMIN PENDING | storage/test-runs/20260619-identity-step4-live6/ | Core auth/RBAC live acceptance passed with marked QA records and redacted evidence. `SUPER_ADMIN` creation/access test was skipped by default and then rejected by escalation reviewer as requiring exact privileged approval. | | 2026-06-19 13:48 EAT | Identity privileged admin acceptance | With explicit approval, ran Step 4 live acceptance including `mdn:create-admin`, login as a marked QA `SUPER_ADMIN`, and protected admin endpoint access. | 20260619-identity-step4-admin2 | PASS | storage/test-runs/20260619-identity-step4-admin2/ | All Step 4 identity/RBAC cases passed through real HTTP calls with redacted evidence. QA audit/session/user records retained per guide. | | 2026-06-19 14:10 EAT | Communications Service implementation and acceptance | Implemented `CommunicationsService`, template renderer, recipient resolver, channel router, DEV_LOG email provider, provider DTOs/interfaces, template/message/delivery repositories, communication APIs, template seed command, and refactored email verification to send through Communications Service. Seeded required MVP templates and ran live HTTP acceptance for template create/duplicate/publish, send, missing variable, unknown user, forced failure, retry, auth integration, audit, and redaction signal. | 20260619-communications-step5-live1 | PASS | storage/test-runs/20260619-communications-step5-live1/ | All Step 5 cases passed through real HTTP calls with marked QA records and redacted evidence. | | 2026-06-19 15:18 EAT | Configuration and Secret Metadata Service | Implemented `ConfigRepository`, `ConfigService`, `SecretMetadataService`, config/secret metadata APIs, and non-secret default config seed command. Ran live HTTP acceptance for namespace read, unauthorized update denial, admin version update/publish, invalid value rejection, version listing, secret metadata list/create/rotate, raw secret rejection, and audit signal. | 20260619-config-step6-live1 | PASS | storage/test-runs/20260619-config-step6-live1/ | All Step 6 cases passed with marked QA records and redacted evidence; raw secret values are prohibited and not returned. | | 2026-06-19 15:25 EAT | File Storage Service | Implemented `FileService`, repositories, access policy, and file APIs for metadata registration, linking, metadata read, download URL, archive, and delete. Ran live HTTP acceptance for valid metadata, unsupported type, oversized file, one/zero/multiple/missing link targets, unauthorized/authorized metadata read, controlled download URL, archive, and sensitive access audit. | 20260619-files-step7-live1 | PASS | storage/test-runs/20260619-files-step7-live1/ | All Step 7 cases passed with marked QA records and redacted evidence; API responses expose storage-key hashes rather than raw local paths. | | 2026-06-19 15:35 EAT | Contract Service | Implemented contract template/version management, MVP renderer, generation, retrieval, acceptance, download URL, supersession, policy checks, and acceptance immutability test command. Ran live HTTP acceptance for template/version create, unpublished generation block, publish, membership contract generation, no/multiple subject validation, owner/wrong-user acceptance, idempotent acceptance, acceptance update/delete trigger rejection, authorized retrieval, and unauthorized retrieval. | 20260619-contracts-step8-live1 | PASS | storage/test-runs/20260619-contracts-step8-live1/ | All Step 8 cases passed with marked QA records and redacted evidence; rendered content stored in `generation_metadata` with PDF generation marked deferred. | | 2026-06-19 15:50 EAT | Payment and Billing Service | Implemented invoice creation, MANUAL_TEST payment intents, idempotency, admin manual payment confirmation, receipts, failed/waived/refunded statuses, allocations, ledger entries, reconciliation, owner/admin access checks, and payment communications/audit. | 20260619-payments-step9-live3 | PASS | storage/test-runs/20260619-payments-step9-live3/ | All Step 9 cases passed with marked QA records and redacted evidence; no raw card data persisted. | | 2026-06-20 10:09 EAT | Application, ISP, Review, and Vetting Services | Implemented Step 10 application workflow module with draft/update/get/status/document APIs, terms/privacy consents, submission gates, application-fee readiness, MANUAL ISP validation, review queue/assignment/start/notes, clarification request/response, vetting, and decision gates. | 20260620-applications-step10-live1 | PASS | storage/test-runs/20260620-applications-step10-live1/ | All Step 10 required API cases passed through live HTTP calls with marked QA records and redacted evidence. Approved applications do not activate memberships directly; ISP exemption did not create membership or bypass annual fee. | | 2026-06-21 18:10 EAT | Database bootstrap in this environment (Phase 0 migration prep) | This Laravel project's existing domains (Identity through Applications, Steps 1-11 above) were originally built/tested against a database that does not exist in this environment. Reverse-engineered exact schema for all 34 referenced tables from Repository/Service code, resolved 3 real table-name collisions with live legacy PHP tables via a `mdn_` connection-level table prefix (config/database.php, zero app/Domain changes), added 9 additive migrations plus append-only triggers for audit_events/contract_acceptances, added `mdn:rbac:seed-defaults` (no RBAC seeding existed anywhere in the repo), and found+fixed a real bug in the bootstrap itself (a botched generated-column migration left `users.email_normalized` permanently NULL, breaking all login). | 20260621-1800-bootstrap-step12 | PASS | storage/test-runs/20260621-1800-bootstrap-step12/ | Full live HTTP/artisan chain verified: migrate, audit immutability (both directions), payments/config/communications seeding, RBAC seeding, mdn:create-admin with real SUPER_ADMIN assignment, real JWT login, and an authenticated /users/me call -- all against the newly bootstrapped schema. This is prerequisite infrastructure for the RealEstate domain build (Phase 1 proper), not the RealEstate domain itself. | | 2026-06-22 01:35 EAT | RealEstate domain -- Deals slice (Phase 1, first migrated domain) | Built `app/Domain/RealEstate/` (Service/Repository/Policy/PartnerRepository/ReferenceGenerator/DealStage) covering Deal create, get, sequential stage-advance with due-diligence and closing-checklist gates, status changes, due-diligence task CRUD, and closing-checklist upsert -- ported from the legacy `realestate_deal_repository.php` state machine. Added 4 new tables (additive), 7 new routes under `mdn.auth`/`mdn.permission:REAL_ESTATE_MANAGE`, a `legacy` DB connection for read-only partner_id validation against the unmigrated legacy `partners` table, and extended `mdn:rbac:seed-defaults` with the new role/permission. | 20260622-0130-realestate-step13 | PASS | storage/test-runs/20260622-0130-realestate-step13/ | Verified live via manual HTTP sequence (the equivalent `tests/api/run-realestate-step13.ps1` is written and syntax-checked but not executed natively -- no PHP 8.4 on this Windows host, app runs in a php:8.4-cli Docker container here). Covered: happy-path create through full lifecycle to auto-closed_won, illegal stage jump (422), due-diligence gate (422 then 200 after completion), closing-checklist gate (422 then 200 after completion), unknown partner (404), unauthenticated request (401), invalid status value (422), row-version conflict (409), and full audit-trail completeness (16 audit rows verified directly in mdn_audit_events). Real membership row obtained via the genuine Applications/Memberships HTTP chain already verified in Steps 10-11, reused via direct repository calls for speed (not re-validating those domains' own preconditions). | | 2026-06-22 01:50 EAT | RealEstate domain -- Advisory Requests slice | Built `AdvisoryService`/`AdvisoryRepository`/`AdvisoryReferenceGenerator` (reusing `RealEstatePolicy`) for the member-facing "show interest" entry point: self-service create with ownership enforcement, admin-only status/outcome transitions, and role-scoped serialization stripping `advisor_user_id`/`session_notes` from any non-managing actor's view. Added 1 new table (additive) and 5 routes. | 20260622-0148-realestate-advisory-step14 | PASS | storage/test-runs/20260622-0148-realestate-advisory-step14/ | Verified live via manual HTTP: admin create/transition with internal fields visible, the SAME record re-fetched by the actual member via their own real login correctly omits the internal fields (proves server-side role-scoped serialization, not just admin-tooling behavior), member self-service create, ownership/permission denials (403/404), invalid-outcome (422), row-version conflict (409), both list endpoints, and full audit-trail confirmation (3 rows). | | 2026-06-22 02:02 EAT | RealEstate domain -- Recommendations slice | Built `RecommendationService`/`RecommendationRepository`/`RecommendationReferenceGenerator`, and extended `RealEstatePartnerRepository` with the ported legacy partner-recommendable gate (verified status, not suspended/archived/terminated, active signed agreement, not on active watchlist -- all checked against live legacy tables via the read-only 'legacy' connection). Added 1 table (additive) and 5 routes; member self-service respond with illegal-double-response protection, admin-only create/expire. | 20260622-0200-realestate-recommendations-step15 | PASS | storage/test-runs/20260622-0200-realestate-recommendations-step15/ | Verified live: gate correctly rejects a known non-qualifying live legacy partner with itemized reasons, accepts a known qualifying one; member's own login accepts a recommendation (status/response fields set correctly); double-response blocked (422); admin expire with reason; member denied expire (403); full audit trail confirmed (4 rows). | | 2026-06-22 02:08 EAT | RealEstate domain -- Introductions slice (pipeline-closing) | Built `IntroductionService`/`IntroductionRepository`/`IntroductionReferenceGenerator`. The hard member-consent gate is the only path that can create an introduction; on success it creates the linked Deal (reusing `RealEstateRepository`/`DealReferenceGenerator` from Step 13) in the same DB transaction and marks `commission_eligible`. Reuses the Recommendations slice's partner-eligibility gate. Added 1 table (additive) and 3 routes. | 20260622-0205-realestate-introductions-step16 | PASS | storage/test-runs/20260622-0205-realestate-introductions-step16/ | Found and fixed a real bug live (undefined-array-key 500 when introducing via recommendation_id only, no partner_id). After the fix: consent gate (422), not-yet-accepted recommendation blocked (422), full recommendation-driven introduction succeeds and the auto-created deal was independently fetched and confirmed (correct stage/owner/history), direct-partner introduction also succeeds, non-qualifying partner blocked (422) reusing the Step 15 gate, get/list endpoints verified, audit trail confirmed (2 rows). The full Advisory->Recommendation->Introduction->Deal pipeline is now proven live end-to-end. | | 2026-06-22 02:18 EAT | RealEstate domain -- Commission Invoices slice | Built `InvoiceService`/`InvoiceRepository`/`InvoiceReferenceGenerator` -- full legacy status state machine (draft/issued/sent/partially_paid/overdue/disputed/paid/cancelled/written_off) and the "no invoice before closing" gate (deal must be at/past `commission_invoice` stage with closing checklist's `commission_trigger_met` confirmed), reusing `RealEstateRepository`/`DealStage` from Step 13. Commission-amount auto-computation from listing/pricing-policy is out of scope (no Listings/Pricing domain exists yet) -- explicitly required as input instead, flagged not hidden. Added 1 table (additive) and 5 routes. | 20260622-0215-realestate-invoices-step17 | PASS | storage/test-runs/20260622-0215-realestate-invoices-step17/ | Verified live against the real closed_won deal from Step 13: gate blocks a not-yet-closed deal (422), illegal status jump blocked (422), payment blocked while draft (422), full draft->issued->sent->partially_paid->paid lifecycle works with correct balance math and paid_at timestamping, payment blocked once paid (422) and on negative amount (422), deal-owner member access confirmed via real login, admin-only create denied to member (403), full audit trail (5 rows). | | 2026-06-22 02:28 EAT | RealEstate domain -- Partner Vetting slice | Built `VettingService`/`VettingRepository` (checklist seed/update, decision recording) and extended `RealEstatePartnerRepository` with the one intentional write through the 'legacy' connection -- `recordVerificationStatus()`, mirroring legacy's exact behavior of flipping `partners.verification_status` on a vetting decision. Added 2 tables (additive) and 6 admin-only routes. | 20260622-0225-realestate-vetting-step18 | PASS | storage/test-runs/20260622-0225-realestate-vetting-step18/ | Verified live with a real before/after proof: a genuinely pending_review legacy partner was blocked from recommendation with reason `partner_not_verified`; after recording an `approved` vetting decision (confirmed live in the legacy `partners` table), the identical recommend call no longer returns that reason -- proving the Laravel-built feature genuinely controls the cross-domain gate, not just that a row got inserted. Also verified: reject-without-reason (422), invalid outcome (422), conditional-approval-without-conditions (422), admin-only access (403 to member), checklist row-version conflict (409), full audit trail (5 rows). | | 2026-06-22 02:40 EAT | RealEstate domain -- Partner Agreements slice | Built `AgreementService`/`AgreementRepository` -- full legacy transition map (draft/sent/signed/active/expired/terminated/superseded/under_review), the activation-checklist-or-override gate, and auto-supersede-other-active-agreements-per-partner. Added 1 table (additive) and 5 admin-only routes. | 20260622-0238-realestate-agreements-step19 | PASS | storage/test-runs/20260622-0238-realestate-agreements-step19/ | Found and fixed a real bug live: the Recommendations gate (Step 15) only checked the legacy agreements table, so a brand-new Laravel-created agreement didn't unlock anything. Fixed `recommendableFailureReasons()` to check both tables. After the fix: full draft->sent->signed->active lifecycle works, illegal jump blocked (422), activation blocked without checklist/override (422), the partner vetted in Step 18 is now provably fully recommendable end-to-end (zero gate failures) -- completing a real cross-step business journey. Also verified override-activation and automatic supersede-of-prior-active-agreement live, and admin-only access (403 to member). Full audit trail (9 rows). | | 2026-06-22 02:48 EAT | RealEstate domain -- Benefits/Gifts slice | Built `BenefitService`/`BenefitRepository`/`BenefitReferenceGenerator` -- full legacy transition map (promised/pending/delivered/missed/disputed/expired/cancelled/not_applicable) and the FR-BEN-010 housewarming gift calculation (1% of deal value, capped at 5000, both constants ported verbatim). Missed/disputed auto-escalation to a linked risk record (FR-BEN-006) is explicitly deferred -- Risk/Watchlist doesn't exist yet (next slice) -- flagged in code rather than forward-referencing a missing table. Added 1 table (additive) and 6 admin-only routes. | 20260622-0246-realestate-benefits-step20 | PASS | storage/test-runs/20260622-0246-realestate-benefits-step20/ | Verified live against the real closed_won deal from Step 13: housewarming calc correct (1% of real $120,000 deal_value = $1,200, under cap), ineligibility correctly blocked for a not-yet-closed deal (422), gift creation correct, invalid type (422), illegal status transition blocked (422) then a valid chain succeeds, admin-only access (403 to member), partner-scoped list, full audit trail (3 rows). | | 2026-06-22 03:02 EAT | RealEstate domain -- Risk & Watchlist slice | Built `RiskService`/`RiskRepository`/`RiskReferenceGenerator` -- watchlist add/resolve and the full risk-record status transition map. Closed two loops: `RealEstatePartnerRepository`'s watchlist gate now checks both legacy and new tables (mirroring the Step 19 Agreements fix), and Benefits' (Step 20) deferred missed/disputed auto-escalation is now wired to create a real linked risk record. Added 2 tables (additive) and 7 admin-only routes. | 20260622-0300-realestate-risk-step21 | PASS | storage/test-runs/20260622-0300-realestate-risk-step21/ | Found and fixed a real MySQL identifier-length-limit bug live (auto-generated composite index name exceeded 64 chars once prefixed) -- masked across retries by a half-succeeded prior migration attempt, diagnosed by manually isolating each table. After the fix: full watchlist add->gate-blocks->resolve->gate-clears cycle proven live (same pattern as Vetting/Agreements before it); closed-loop proof that marking a benefit "missed" auto-creates a correctly-linked risk record; risk-record transition map validated (422 on illegal jump, 200 on valid chain with resolved_at set); invalid category rejected (422); admin-only access enforced (403); full audit trail (5 rows). | | 2026-06-22 03:08 EAT | RealEstate domain -- Reviews slice | Built `ReviewService`/`ReviewRepository`/`ReviewReferenceGenerator` -- BR-023 eligibility gate (closed_won deal owned by reviewing member, one review per deal), moderation workflow, and BR-025 rating-label mapping. Wired the low-rating (<=2) approved-review auto-escalation to a linked risk record directly via `RiskService` (no deferral needed, unlike Benefits in Step 20, since Risk now exists). Added 1 table (additive, with a proactively-shortened unique-constraint name) and 5 routes. | 20260622-0306-realestate-reviews-step22 | PASS | storage/test-runs/20260622-0306-realestate-reviews-step22/ | Verified live: ineligible-deal rejection (422), the real QA member submits a genuine 1-star review on the real closed_won deal from Step 13 (rating_label correctly "Poor Performance"), duplicate-review blocked (409), approving it auto-creates a correctly-linked risk record (verified by fetching the risk-records list), invalid moderation status (422), out-of-range rating (422), full audit trail (2 rows). | | 2026-06-22 03:09 EAT | RealEstate domain -- Partner Documents slice (final slice) | Built `DocumentService`/`DocumentRepository`/`DocumentReferenceGenerator` -- reuses the existing Files domain (Step 7) for actual file storage instead of re-implementing upload handling (file_id is a plain reference to stored_files.id); full legacy status transition map and the FR-DOC-011 nearing-expiry work-queue query. Added 1 table (additive) and 6 admin-only routes. This completes all 11 RealEstate sub-resources (Steps 13-23). | 20260622-0307-realestate-documents-step23 | PASS | storage/test-runs/20260622-0307-realestate-documents-step23/ | Verified live: registered a real file via the unmodified existing Files domain, created a document referencing it (201), invalid file_id rejected (404), illegal status jump blocked (422), valid submitted->under_review->accepted chain works, nearing-expiry date-windowed query correctly returns an accepted document expiring within the window, full audit trail (6 rows). | | 2026-06-22 03:23 EAT | Membership module -- live end-to-end re-verification in this environment | Steps 1-11 (Identity/Communications/Config/Files/Contracts/Payments/Applications/Memberships) were originally built and PASS-marked in a different environment before this repo's Docker bootstrap (Step 12) existed; Applications/Memberships had only been exercised via a tinker shortcut in Step 13, not real HTTP, in this environment. Ran the full real chain end-to-end with no shortcuts: register -> verify email -> login -> application create/consent/submit -> application fee invoice+payment -> file/document attach -> review assign/start/complete -> vetting start/outcome -> decision approve -> contract template/version/publish/generate/accept -> annual fee invoice+payment -> membership activate. | 20260622-0316-membership-live-verification-step24 | PASS | storage/test-runs/20260622-0316-membership-live-verification-step24/ | Found and fixed a real, environment-breaking bug live: `POST /auth/register` 500'd on the very first attempt because `RecipientResolver` reads `users.phone_number`, a column the Step 12 schema bootstrap never created (missed because that pass only scanned UserRepository's own usage, not Communications'). This meant the entire Membership module was non-functional in this environment via the real registration path until fixed (Step 12's own register/login smoke test never caught it because `mdn:create-admin` bypasses IdentityService::register() entirely). After the fix: full chain completed successfully -- real member created, MDN-2026-000001, status ACTIVE, activation correctly idempotent on retry, 30 real audit-trail rows spanning every domain touched. | | 2026-06-22 07:10 EAT | Membership flow correction -- no self-registration; admin/system-driven account creation after annual fee payment | User correction: applicants never self-register; legacy creates the member's account and emails credentials only after the $100 annual fee is paid. Refactored Applications to use a per-application access token (`X-Application-Access-Token`) instead of a login for all pre-payment applicant actions (create/get/update/consents/submit/status/documents/clarification-responses/ISP validation), removed `POST /auth/register` entirely, and wired real account auto-provisioning (pre-verified user, `must_change_password`, generated temp password emailed) into `MembershipService::activate()`. | 20260622-0655-membership-corrected-flow-step25 | PASS | storage/test-runs/20260622-0655-membership-corrected-flow-step25/ | Found and fixed 2 real bugs live: an `access_token_hash` leak in the application API response, and `application_consents.user_id` being `NOT NULL` (blocked the first applicant-token consent call with a 500; fixed via migration `2025_06_22_001600`). Full live HTTP chain verified end-to-end with a brand-new application created with zero auth, through to a real auto-provisioned member account, a working login with the emailed temporary password (`must_change_password: true` correctly surfaced), and confirmation that the original access token is invalidated once the real account exists. `/auth/register` confirmed `404`. | | 2026-06-22 12:40 EAT | Merge missing Products domain from `mdn-backend` remote's `main` branch | User asked to check `main` for changes not yet in our branch and merge them in. `main` is a much older, pre-monorepo, pre-Step-12 snapshot with no shared git history (no merge-base) -- compared by diffing trees directly. Found `main` had one thing we didn't: the full Products domain (cross-cutting member product-request/entitlement/blocker layer for CARE/INVESTMENT/REAL_ESTATE/CONNECT/CONCIERGE/MARKETPLACE). Ported the 8 domain files and 14 routes verbatim. | 20260622-1230-products-domain-merge-step26 | PASS | storage/test-runs/20260622-1230-products-domain-merge-step26/ | Found the same class of gap as Step 12: the Products domain's tables were never reverse-engineered into this environment's schema (built/tested in an earlier environment). `products` catalogue table didn't exist at all; `product_requests`/`product_entitlements` existed but only with the narrow columns `MembershipService` itself needed; `product_request_blockers` didn't exist. Added migration `2025_06_22_001700_create_products_domain_tables.php` and ported the `mdn:products:seed-catalogue` command. Verified live: catalogue seed/list, a real product request against the real Step-25 membership with correct auto-seeded blockers (contract/payment/approval matching CARE's flags), submit correctly stays blocked while blockers are open, blockers list, and entitlements recalculate on the empty case. | | 2026-06-22 14:50 EAT | Calendly integration -- shared API client + real advisory-meeting booking on Applications | User provided a live Calendly personal access token and asked for real booking integration in the active Laravel app (not the legacy PHP monorepo), built as a global/shared service reusable by future integrations. Built `App\Infrastructure\Calendly\CalendlyClient` (stateless, no constructor deps, same tier as `UuidGenerator`/`TransactionManager`) and its first consumer, `App\Domain\Applications\AdvisoryMeetingService`, which creates a real single-use Calendly scheduling link per application and matches the booked/canceled webhook back via a `utm_content` tracking param (Calendly's scheduling-links API has no field for arbitrary metadata). | 20260622-1445-calendly-advisory-meeting-step27 | PASS | storage/test-runs/20260622-1445-calendly-advisory-meeting-step27/ | Verified against the real, live Calendly account: `POST /applications/{id}/advisory-meeting` made a real API call and returned a real single-use booking link that was confirmed to resolve (HTTP 200). Simulated realistic `invitee.created`/`invitee.canceled` webhook payloads (matching Calendly's documented schema) against `POST /webhooks/calendly` -- correctly transitioned `REQUESTED -> SCHEDULED -> CANCELED` with all Calendly fields populated, correctly recorded `webhook_signature_verified: false` (no secret configured yet -- no public URL exists to register a real subscription against), and didn't crash on unknown `utm_content` or unrelated event types. Full audit trail confirmed. Real signed webhook *delivery* from Calendly itself remains unverified until a public URL exists; the payload/signature logic is implemented against their real documented schema but not yet exercised by an actual incoming webhook. |