Search by

mar-pod-b2b / module-b2b-demo-data

macpain

Installs a complete, realistic B2B demo dataset across every Marpod B2B Advanced module and automatically cleans it up after a configurable retention period.

Package info

gitlab.com/mar-pod-b2b/module-b2b-demo-data

Issues

Type:magento2-module

pkg:composer/mar-pod-b2b/module-b2b-demo-data

Statistics

Installs: 4

Dependents: 0

Suggesters: 0

Stars: 0

1.1.0 2026-09-13 20:41 UTC

This package is auto-updated.

Last update: 2026-09-13 20:43:45 UTC


README

Installs a complete, realistic demo dataset across every Marpod B2B Advanced module — companies, credit accounts, price lists, shared catalogs, quotes, quote templates, approval rules, budgets, company hierarchy, sales representative assignments, and procurement integration — plus a temporary backend admin account scoped only to the B2B admin menu, so a sales or solutions team can hand a prospect a working sandbox on both the storefront and the merchant admin side, without hand-seeding data or exposing the rest of the store. Installed data is tracked per run and automatically removed after a configurable retention period, so a demo instance never accumulates stale fixtures.

Status

Phase 1-6 implemented — this is the full planned installer scope. The data model (marpod_b2b_demo_data_run + marpod_b2b_demo_data_entity), the installer, the cleaner, the cron job, and the CLI commands are implemented and wired up. The installer seeds:

  • one company, one customer (as company admin with every permission), and one credit account (Phase 1);
  • one base price list with one repriced SKU, assigned to the company, and one custom shared catalog with that same SKU assigned and indexed (Phase 2);
  • a second "delegate" customer (company member, not admin), one submitted quote with one item, one approval rule group with an amount-threshold condition, and one active approval delegate from the admin customer to the delegate customer (Phase 3);
  • one pending purchase request, one hard monthly budget partially reserved against that request, and a two-level company hierarchy (an auto-created root unit plus one "Demo Branch" unit, with the delegate customer assigned to the branch and one inherited pricing override pointing at the Phase 2 price list) (Phase 4);
  • one sales representative assignment and one procurement integration session (Phase 5);
  • one restricted backend admin account, scoped to the B2B admin menu only, so a prospect can be handed real admin access without seeing the rest of the store (Phase 6 — see below).

Not yet implemented: the "Auto Reinstall After Cleanup" behavior — the config field and its default exist, but CleanupExpiredRuns does not yet act on it.

Test coverage: complete for everything implemented above. DemoDataCleaner has full PHPUnit unit coverage (every entity type's dispatch routing, reverse-of-creation-order processing, the active-run guard, and the failed-cleanup/status-transition path). DemoDataInstaller's isolated logic (the ttlDays guard, generated password shape, expiry date math, and computeRestrictedAdminResources()) is unit tested the same way; its install() orchestration itself is deliberately not unit tested, since mocking ~15 modules' repositories would only prove setters were called with values typed twice — that path, and the admin UI end to end (install, extend, and the real confirm-modal clean flow), are instead covered by tests that run against a live Magento instance: a Playwright suite for the admin UI, plus every phase's own manual CLI/browser verification recorded throughout this README. See Testing below for exactly what each suite covers and how to run it.

Phase 6: restricted backend admin

Unlike every earlier fixture (a storefront customer, or a real existing admin account borrowed for the sales-rep/session fields), Phase 6 creates a genuinely new, temporary backend admin account — real admin_user + authorization_role rows, not a storefront customer — scoped so its holder can explore the merchant/admin side of B2B (Company Management, Quotes, Pricing, Approvals, Budgets, Company Hierarchy, Procurement, ...) without seeing Sales, Catalog, Customers, System, or any other unrelated core admin section.

  • Toggle: Create Restricted Admin User in Configuration (default Yes). When off, the installer records 'Restricted backend admin: not created (disabled in configuration)' in the run's notes and skips this fixture entirely.
  • Scope is computed from the live ACL tree, not a hardcoded resource list. The new role is granted every ACL resource that is Marpod_B2bCore::main or inherits from it, except Marpod_B2bDemoData::manage and anything under it — deliberately excluded so the prospect using this account can't also install more demo runs or clean up (and thereby delete) the very account they're using. Because this walks the real ACL tree at install time (Magento\Framework\Acl\Builder), any new B2B module added later is covered automatically; nothing here needs updating when the B2B suite grows.
  • Login: username is the lowercased marker (e.g. demo-a1b2c3d4), with a random password — shown once in the run's Notes, exactly like the storefront customer/delegate credentials.
  • Verified in a real browser session, not just at the database level: logged in as a freshly installed restricted admin and confirmed (a) the left nav shows only "B2B" — no Dashboard, Sales, Catalog, Customers, Content, or System; (b) every real B2B feature screen (Companies, Pricing, Shared Catalogs, Quotes, Approvals, Budgets, Procurement, ...) is reachable; (c) "Demo Data" does not appear in the B2B submenu; (d) the account's authorization_rule rows confirm Marpod_B2bDemoData::manage is deny while the rest of the B2B tree is allow. Then, back in as a full admin, cleaned up the run through the admin grid and confirmed the admin_user row, both authorization_role rows (the role definition and its "this user has this role" assignment row — Magento models a role assignment as its own row, cascaded automatically when the role definition is deleted), and every authorization_rule row for that role were all gone.
  • Uninstall/cleanup implementation note: Magento\Authorization\Model\Role::delete() already cascades both the role's authorization_rule rows and any user-assignment rows nested under it (Magento's own resource model does this in _afterDelete()), so the cleaner only needs to delete the admin_user row and the role — no direct SQL required here, unlike the quote/ purchase-request/credit-account fixtures.

Assumption: the pricing, shared-catalog, and quote fixtures reprice/reference SKU 24-MB01 (Magento sample data's "Joust Duffle Bag"). The installer will throw NoSuchEntityException if that SKU does not exist on the target instance — see Requirements.

Cleanup notes specific to Phase 3: the quote (and its item, cascaded at the DB level) has no repository delete method, so it's removed via a direct SQL delete on marpod_b2b_quote, same as credit_account and customer. The approval rule group and approval delegate both have deleteById() on their repositories and are removed that way; the group's condition row cascades from the group's deletion.

Cleanup notes specific to Phase 4: the purchase request has no repository delete method either, so it's removed via direct SQL on marpod_b2b_purchase_request (its item, approval-step, and approval-history rows all cascade at the DB level). The budget is removed via its repository's deleteById(), which cascades the reservation row (the reservation is not recorded in the entity registry at all — its FK cascades from either the budget or the purchase request, so whichever is deleted first already removes it). The company hierarchy unit, branch, and pricing override are deliberately not recorded or explicitly deleted either: every company gets exactly one root unit automatically the moment it's saved (see Marpod\B2bCompanyHierarchy\Observer\CreateRootUnitOnCompanySavedObserver), and both the branch unit (via parent_unit_id) and the override (via unit_id) cascade transitively once the company itself is deleted at the end of cleanup. Calling HierarchyManagementInterface::deleteUnit() explicitly would in fact be wrong here — it throws if the unit still has children, which the root unit always does once a branch exists under it.

Cleanup notes and design decisions specific to Phase 5:

  • Which admin user becomes the sales representative is never hardcoded. install() already accepts an optional $adminUserId (recorded on the run for audit purposes); if given, that admin is assigned as the primary sales rep. If not given (e.g. a cron-triggered install with no admin context), the installer falls back to the lowest-user_id admin account on the instance. If somehow no admin user exists at all, the sales rep assignment is skipped entirely and the run's notes say so — the installer never fails just because this one optional fixture couldn't be created.
  • The procurement fixture deliberately stops at one session; it does not send a signed inbound message. b2b-full-stack-test-suite/fixture-setup.php goes further and also calls ProcurementMessageSignerInterface::sign() against a real webhook signing secret — but doing that requires writing marpod_b2b/b2b_procurement_integration/signing_secret, a single global config value shared by the whole instance, not scoped to a company or a demo run. Overwriting it on every install() (a normal thing to run repeatedly against a demo environment) would silently invalidate any other integration already configured against that secret, and — unlike a data row — a changed config value cannot be "recorded" in the entity registry and precisely reverted by cleanup(). A session in initiated status is enough to show the procurement integration feature exists without touching shared, non-reversible instance configuration.
  • Both new entities have deleteById() on their repositories and are removed that way — no direct SQL needed for Phase 5.

Phases 1-5 verified end-to-end this way as they were built: setup:di:compile clean, install creating and recording every entity described above, cleanup removing all of it including every cascaded child row. See the Phase 6 section above for the latest full-install verification (fifteen entity rows, including the restricted admin) and the real-browser ACL check. No automated test suite (PHPUnit unit/integration or Playwright) exists yet for this module.

Planned design

  • Hard dependency on mar-pod/b2b-advanced. Rather than treating each of the 19 B2B feature modules as an optional integration (which would require either a fragile capability-check layer or a small bridge module per feature), this module depends directly on the b2b-advanced metapackage, which already requires all of them. The installer can therefore call every feature module's service contracts directly, the same way packages/b2b-full-stack-test-suite/fixture-setup.php already does for manual testing.
  • Exact entity tracking, not heuristic lookup. fixture-teardown.php finds what to delete by matching a VAT-ID/name prefix or email domain — good enough for a manually-run script, too fragile for unattended cleanup. This module instead records every entity it creates:
    • marpod_b2b_demo_data_run — one row per install: run_id, a unique marker (e.g. DEMO-<run_id>), status (active/cleaning/cleaned/failed), created_at, expires_at, cleaned_at, the admin user who triggered it.
    • marpod_b2b_demo_data_entityrun_id, entity_type, entity_id for every row the installer created. Cleanup deletes exactly what was recorded, in dependency order (orders and credit reservations released before the credit account and company, same ordering fixture-teardown.php already uses) — never by guessing from a naming pattern.
  • Random, single-use demo credentials. Unlike the internal test fixtures (which reuse a fixed password), each install generates random passwords for its demo customer accounts, since this data may actually be shown to a prospect. Credentials are displayed once in the admin grid after install and are not otherwise stored in plain text.
  • Cron-driven cleanup. A scheduled job runs on a short interval, finds runs whose expires_at has passed, and cleans them up. An optional "auto-reinstall" setting can immediately seed a fresh run after cleanup so a demo environment is never left empty.
  • Admin-only. Every install/cleanup action requires the Marpod_B2bDemoData::manage ACL resource. This is never exposed on the storefront or through an unauthenticated endpoint — installing or deleting real data must stay behind admin authentication.

Configuration

Stores > Configuration > B2B > Configuration > Demo Data (marpod_b2b/b2b_demo_data), website-scoped:

FieldPathDefaultNotes
Retention (Days)marpod_b2b/b2b_demo_data/retention_days2How long a demo data run stays installed before automatic cleanup. Used both by the CLI/admin install action (when no explicit --ttl-days is given) and by Extend.
Auto Cleanup Enabledmarpod_b2b/b2b_demo_data/auto_cleanup_enabledYesWhen disabled, expired runs accumulate until cleaned manually or re-enabled.
Auto Reinstall After Cleanupmarpod_b2b/b2b_demo_data/auto_reinstallNoField exists; CleanupExpiredRuns does not act on it yet — see Status.
Create Restricted Admin Usermarpod_b2b/b2b_demo_data/create_admin_userYesCreates the Phase 6 backend admin account scoped to the B2B menu. When off, that fixture is skipped.

Admin UI and CLI (both implemented)

A grid at B2B > Demo Data (marpod_b2b_demo_data/run/index) lists every run — marker, status, created/expires/cleaned timestamps, dedicated Admin Login / Admin Password columns for the run's restricted backend admin (blank when create_admin_user is disabled), and the full Notes text (including every other generated login). The dedicated columns exist so the restricted admin's credentials can be copied straight from the grid and handed to a prospect, without parsing them out of the Notes column. A toolbar button installs a new run — its success message also states the admin login/password directly — and each active run's row has "Clean Now" (with a confirmation dialog, since it's destructive) and "Extend" (pushes expires_at out by the configured retention period, measured from whichever is later: now, or the run's current expiry). Both row actions and the grid data itself were verified against a live instance, including confirming the underlying data (creation, extension, and full cleanup with the correct generated credentials) matches what the grid displays.

CLI, for scripting outside the admin UI:

  • bin/magento marpod:b2b:demo-data:install [--ttl-days=N] — installs one run. Unlike the admin UI action (which passes the logged-in admin's ID), the CLI has no admin session to default to, so the sales rep fixture falls back to the lowest-user_id admin account.
  • bin/magento marpod:b2b:demo-data:cleanup [--run-id=N] — cleans up one run, or every run already past its expiry when --run-id is omitted (the same logic the cron uses).
  • marpod:b2b:demo-data:list is not implemented; use the admin grid, or inspect marpod_b2b_demo_data_run/marpod_b2b_demo_data_entity directly.

Implementation note: row/toolbar actions are plain confirm-then-GET links (matching how Magento's own core ui_component grids implement destructive row actions, e.g. CMS Block/Page delete) rather than POST forms — appropriate here because the install/clean/extend actions have no form fields to submit, just an id. Each controller implements \Magento\Framework\App\Action\HttpGetActionInterface and is protected by the Marpod_B2bDemoData::manage ACL resource; Magento's admin URL "secret key" mechanism (present on every rendered admin link, verified on every dispatch) is what actually prevents these from being triggered by a forged cross-site link — the same protection every other admin GET action in this project relies on.

Testing

Unit tests cover DemoDataCleaner fully (dispatch routing for every entity type, reverse-of- creation-order processing, the active-run guard, and the failed-cleanup/status-transition path) and the isolated, side-effect-free logic in DemoDataInstaller (the ttlDays guard, the generated password's shape, the expiry date math, and — the highest-stakes piece of this whole module — which ACL resources the restricted backend admin ends up with). DemoDataInstaller's own install() orchestration is deliberately not unit tested: it's a straight-line dispatch across roughly fifteen other modules' repositories, and mocking all of it would only assert that setters were called with values typed twice in the test — it wouldn't catch a real regression. That path is exercised instead by the live install/cleanup verification described throughout this README (run against a real Magento instance for every phase), matching how this project already treats CreditLedger::reserveForOrder()/getAvailableBalance() in module-b2b-company-credit.

Run from a Magento project where this package is installed as a path repository:

vendor/bin/phpunit -c dev/tests/unit/phpunit.xml.dist packages/module-b2b-demo-data/Test/Unit

Writing this test suite caught a real bug: DemoDataCleaner::cleanup()'s catch (\Throwable) block wrapped whatever it caught into a LocalizedException, whose $cause parameter only accepts ?\Exception — so a caught \Error (a TypeError, for instance) would itself crash while trying to report the original failure. Fixed by only passing the cause through when it's actually an \Exception.

A Playwright suite covers the admin UI end to end against a real instance: clicking "Install New Demo Data Set" and confirming the new row's Notes column shows every phase's fixture (company, customer, delegate, price list, shared catalog, quote, approval group, purchase request, budget, company hierarchy, sales rep, procurement session, and the restricted backend admin); "Extend" changing the Expires At cell; and "Clean Now" — including the confirmation dialog Magento renders for it (Magento_Ui/js/modal/confirm, a real DOM modal, not a native confirm()) — marking the row cleaned and removing its row actions. A global-teardown.js safety net force-cleans whatever run the suite created if a test fails partway through, since a freshly-installed run isn't yet past its retention period and so wouldn't be picked up by a plain, no-argument marpod:b2b:demo-data:cleanup sweep.

cd packages/module-b2b-demo-data/Test/Playwright
npm install
npx playwright install chromium
npm test

Set PLAYWRIGHT_BASE_URL when Magento is not available at http://localhost:8090.

Requirements

  • PHP 8.5
  • Magento Open Source or Adobe Commerce 2.4.9
  • mar-pod/b2b-advanced (pulls in every Marpod B2B module this installer seeds data into)
  • Magento sample data (or a catalog that otherwise defines SKU 24-MB01) — the pricing and shared-catalog fixtures reprice and assign this SKU

Installation

Run from the Magento project root:

composer require mar-pod-b2b/module-b2b-demo-data:^1.0
bin/magento module:enable Marpod_B2bDemoData
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:clean

Uninstallation

Run the cleanup command (or wait for the cron) to remove any active demo data run before removing the module — the module's own tables are dropped on uninstall, but entities it created in other modules' tables are only removed by the cleaner, not by uninstalling this module.

bin/magento marpod:b2b:demo-data:cleanup
bin/magento module:disable Marpod_B2bDemoData
composer remove mar-pod-b2b/module-b2b-demo-data
bin/magento setup:upgrade