Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
vendra-store-development
vendra-store-development
misaf/vendra
claude-code
translatable-persistence
pest-testing
filament
storefront
module-boundary
tenant-model
composer-dependencies

Create, modify, review, or test the Vendra Store module in packages/vendra-store, changing the store domain and the storefront deployment lifecycle. Use for StorefrontProvisioner, ContainerStorefrontProvisioner, StorefrontDeployment, StorefrontDeploymentStatus, StorefrontDesiredState, StorefrontRuntimeState, StorefrontReconciliationOutcome, CreateStoreAction, ProvisionStoreAction, DeleteStoreAction, DeployStoreStorefrontAction, StartStoreStorefrontAction, StopStoreStorefrontAction, RestartStoreStorefrontAction, ReconcileStoreStorefrontAction, RequestStorefrontDeploymentAction, ProvisionStorefrontJob, DestroyStorefrontJob, ReconcileStorefrontJob, CompleteStoreProvisioningJob, CompleteStoreProvisioningAction, FailStoreProvisioningAction, StorefrontSettings, StorefrontConfigurationMap, StorefrontConfigurationValidator, StorefrontContainerDefinitionFactory, StorefrontOrigins, StoreQuota, CreateStorePage, and the storefront console commands.

Install
php artisan boost:add-skill misaf/vendra

Save this content to: .claude/skills/vendra-store-development/SKILL.md

# Vendra Store

## Workflow

- Inspect `composer.json`, sibling files, and existing tests before changing the package.
- Use Laravel Boost `application-info` and `search-docs` before code changes.
- Apply `laravel-best-practices` to Laravel PHP and `pest-testing` whenever tests change.
- Keep changes inside this package's boundary and preserve its public contracts.
- Add or update focused Pest coverage, then run `php artisan test --compact --testsuite=vendra-store` from the project root.

## Translatable Persistence

- Making a persisted model field translatable is an explicit domain choice unless this package already requires it.
- Every field listed in a model's `$translatable` array must definitely use a JSON database column. Keep its model traits/casts, factories, validation, Filament locale UI, API serialization, and tests translation-aware.
- A field not listed in `$translatable` must use the appropriate scalar database type and must not use Spatie Translatable, translatable slug traits, locale switchers, translated callbacks, or translation-shaped array data.

## Vendra Transitive API Policy

- Treat a Vendra dependency intentionally exposed through the public API of a directly required Vendra platform package as part of the supported public contract of that package.
- Do not add a redundant direct Composer requirement solely because source code imports a type from that exposed dependency.
- Apply this only to Vendra platform packages listed under `require`; never extend it to `require-dev`, `suggest`, incidental implementation dependencies, or third-party packages. Removing or replacing an exposed dependency is a breaking change; keep `self.version` alignment across the Vendra package graph.

## Module Boundary

- This package owns the **ecommerce Store**: the concrete tenant model, its domains, and its storefront lifecycle. It owns *what* a storefront should be and *when* it should change; it never owns *how* a runtime is driven — that lives behind `Contracts\StorefrontProvisioner`.

## Store As Tenant

- `Models\Store` implements `Misaf\VendraTenant\Contracts\TenantContract` and extends Spatie's tenant, so **Store IS the tenant**: there is no `tenants` table, no `Store -> Tenant` relation, and no 1:1 pair. It is wired in `config/vendra-tenant.php` (`model`, `relation`) and `config/multitenancy.php` (`tenant_model`, `tenant_finder`).
- `Store::accessible()` — active, provisioning-ready, not billing-suspended — is the request-serving boundary. Both `Services\StoreDomainFinder` and the engine's resolver search options go through it; never expose or resolve an inaccessible store.
- `Services\StoreDomainFinder` is the adapter behind the engine's `Misaf\VendraTenant\Contracts\HostTenantFinder` port. Host-to-store mapping lives here, never in `misaf/vendra-tenant`.
- A registered, active `*.trycloudflare.com` domain alias also serves the admin panel of its accessible store for a temporary quick tunnel. Other storefront domains keep the normal admin host checks.
- A store has one primary domain (`is_primary`, read through `Store::primaryDomain()`); any other active domains are aliases that also resolve the store. The first domain `CreateStoreAction` creates is the primary for the life of the store: nothing replaces, demotes or removes it, and the database allows only one live primary per store.
- Add aliases through `Actions\AddStoreDomainAliasAction` (its caller normalizes and validates the domain) and remove one through `Actions\RemoveStoreDomainAliasAction` (it keeps the alias as trashed history and refuses the primary); both force a storefront redeploy, because the container's Traefik rule lists every active domain and labels cannot change in place.
- `CompleteStoreProvisioningJob` keeps only queue concerns; the checkpointed provisioning steps live in `Actions\CompleteStoreProvisioningAction` and failures go through `Actions\FailStoreProvisioningAction`, which never downgrades a ready store. Each step saves its own checkpoint instead of sharing a transaction.
- Administrator-driven availability and failed provisioning recovery use `SuspendStoreAction`, `ReactivateStoreAction`, and `RetryStoreProvisioningAction`. Redeploy/retry use their domain actions and the existing deployment job/transition rules.
- Business removal uses `OffboardStoreAction`, which requires an audit reason and soft-deletes after inactivation; restoration uses `RestoreOffboardedStoreAction`, rechecks reseller quota, and respects provisioning readiness.
- `Support\StoreTenantEntitlements` binds the support `TenantEntitlements` from the store reseller's plan (console-owned stores are unrestricted). Custom domains (outside `*.{storefront.base_domain}`, `StoreDomain::isCustom()`) need `PlanFeature::CustomDomain` at create, alias and replace; aliases also respect `PlanLimit::DomainsPerStore` under a store row lock, and `AddDomainAliasTableAction` is disabled with the refusal as a tooltip at that limit. The panels catch `EntitlementExceededException` and show its message. Do not expose force deletion as an administrator workflow. `assertCanAdd()` locks the store row before counting, so creates inside a transaction cannot race past a limit. `recordAdded()` fires `Events\StoreLimitApproached` (after commit) once an add crosses 80% or 100% of a limit.
- Deleting a store settles its storefront in `Observers\StoreObserver`, not in the caller: soft delete records `StorefrontDesiredState::Stopped` and queues `ReconcileStorefrontJob`; force delete queues `DestroyStorefrontJob`, addressed by slug because the deployment row is cascaded away first. `Actions\DeleteStoreAction` is the transactional entry point for a programmatic delete and no longer touches the runtime itself.
- Two row-ownership columns, two mechanisms. Reusable domain packages own their rows through the **neutral `tenant_id`** and `Misaf\VendraSupport\Tenancy\BelongsToTenant`. Records describing the Store itself — `store_domains`, `storefront_deployments` — carry **`store_id`** and use `Concerns\BelongsToStore` / `Scopes\StoreScope`. Never add `store_id` to a reusable package's table, and never register a `store_id` table in the support `TenantTableRegistry`.
- The billing reseller is optional and inverted: `stores.reseller_id` is a plain nullable indexed column, the reseller is fetched through `Contracts\StoreResellerResolver`, and `ProvisionStoreAction` / `CreateStorePage::resolveReseller()` take `(Model&SubscriptionSubscriber)|null`. A null reseller is a store the console owns directly, not an error.
- It depends on `misaf/laravel-docker-engine` for driver-managed Engine access and on `misaf/vendra-tenant` for the tenancy engine it plugs its `Store` into. It must **not** depend on `misaf/vendra-reseller`: a store's billing reseller is reached through `Contracts\StoreResellerResolver`, which the reseller package binds. Keep the arrow pointing reseller → store.
- Panels belong to their own packages. Put shared schemas, page bases, actions, and concerns here; put the panel-specific resource in `vendra-console` or `vendra-reseller`.

## Provisioner Contract

- `provision`, `start`, `stop`, `restart`, `destroy`, `observe`, `logs` — typed value objects on both sides, never arrays.
- Idempotence is part of the contract: provisioning twice leaves one storefront; starting a running one, stopping a stopped one, and destroying an absent one all succeed.
- `observe()` returns a `StorefrontObservation` rich enough to tell "stopped" from "running the wrong image", and must never answer "absent" for an unreachable runtime.
- Compare the inspected `STOREFRONT_CONFIG_BASE64` environment value with the saved deployment configuration. Configuration, image, and domain drift must trigger replacement before starting a stopped container; unchanged containers are started. Check the platform ownership label before reading logs too.
- `ContainerStorefrontProvisioner` builds its container through `Support\StorefrontContainerDefinitionFactory` after `Support\StorefrontConfigurationValidator` accepts the configuration. `Services\StorefrontContainerRuntime` resolves the selected Engine client through `ContainerManager`; no Docker or Podman CLI calls are allowed.

## Deployment Lifecycle

- `RequestStorefrontDeploymentAction` → `ProvisionStorefrontJob` → `StorefrontDeployment`. Reconciliation and retry go through `StorefrontDeploymentDispatchCommand`; do not add a second dispatch path.
- `CreateStorePage` skips the request only for explicit `create_storefront=false`. The console form exposes this through `StorefrontConfigurationFields::creationToggle()`; the reseller form remains mandatory. Both collect only image and slug and deploy with sample storefront details. The tenant administrator replaces contact and business details on the admin Storefront settings page.
- `StorefrontImage` records the approved immutable image reference and optional notes. New deployments may select only active catalog entries; existing deployments keep using their selected entry after it is disabled. Never reintroduce global `STOREFRONT_IMAGE` configuration.
- Write status only via `markProcessing()`, `markReady()`, `markRequested()`, `markFailed()`. `Enums\StorefrontDeploymentStatus::transitions()` is the transition table and `InvalidStorefrontTransitionException` is the rejection.
- A failing attempt with retries left stays `Processing`; only `ProvisionStorefrontJob::failed()` writes `Failed`.
- `ProvisionStorefrontJob` runs on its own `storefronts` queue, served by the single worker that holds a runtime socket. Do not move it onto a shared queue.
- Panel observation/log reads use `Support\StorefrontRuntimeSnapshots`, which throttles collection through `RecordStorefrontRuntimeSnapshotJob` on the same worker. Only `RecordStorefrontRuntimeSnapshotAction` accesses the provisioner. Central-cache snapshots contain plain arrays, expire after 30 seconds, and are keyed by deployment intent; synchronous queue connections must never run panel reads inline.

## Configuration

- Read infrastructure values in `config/vendra-store.php` only through the injected `Support\StorefrontSettings`. Keep it bound with `bind()` so config changes are picked up on the next resolve.
- Whether the platform is creating stores at all is `Settings\StoreCreationSettings` (group `store_creation`, `global` repository), read directly as `->open`. Console and reseller store creation both read it, so the shared rule lives below both panels.
- Runtime differences (Docker vs. a Podman compatibility socket, log driver, health-check behaviour) are configuration, never a branch. Do not add runtime sniffing.

## Testing

- Use the shared `fakeDockerEngine()` transport helper, which registers a test driver through `ContainerManager`; domain tests must not require Docker or Podman.
- Cover each lifecycle transition, including the illegal ones the enum rejects, and the retry path that must not mark a deployment `Failed` early.
- Cover the tenancy side too: Store acting as the current tenant, host resolution, `store_id` scoping of store domains, and the billing port defaulting to `Support\NullStoreResellerResolver`.
- Keep the architecture rules in `tests/ArchTest.php` asserting this package does not use `Misaf\VendraReseller` or `Misaf\VendraConsole`, and that actions, jobs and models stay behind the provisioning port.

## Filament

- Resources with a cluster live in `src/Filament/Clusters/Resources/`; resources without one live in `src/Filament/Resources/`. This package currently ships pages, schemas, actions, and concerns rather than resources.

Vendra Store

Workflow

  • Inspect composer.json, sibling files, and existing tests before changing the package.
  • Use Laravel Boost application-info and search-docs before code changes.
  • Apply laravel-best-practices to Laravel PHP and pest-testing whenever tests change.
  • Keep changes inside this package's boundary and preserve its public contracts.
  • Add or update focused Pest coverage, then run php artisan test --compact --testsuite=vendra-store from the project root.

Translatable Persistence

  • Making a persisted model field translatable is an explicit domain choice unless this package already requires it.
  • Every field listed in a model's $translatable array must definitely use a JSON database column. Keep its model traits/casts, factories, validation, Filament locale UI, API serialization, and tests translation-aware.
  • A field not listed in $translatable must use the appropriate scalar database type and must not use Spatie Translatable, translatable slug traits, locale switchers, translated callbacks, or translation-shaped array data.

Vendra Transitive API Policy

  • Treat a Vendra dependency intentionally exposed through the public API of a directly required Vendra platform package as part of the supported public contract of that package.
  • Do not add a redundant direct Composer requirement solely because source code imports a type from that exposed dependency.
  • Apply this only to Vendra platform packages listed under require; never extend it to require-dev, suggest, incidental implementation dependencies, or third-party packages. Removing or replacing an exposed dependency is a breaking change; keep self.version alignment across the Vendra package graph.

Module Boundary

  • This package owns the ecommerce Store: the concrete tenant model, its domains, and its storefront lifecycle. It owns what a storefront should be and when it should change; it never owns how a runtime is driven — that lives behind Contracts\StorefrontProvisioner.

Store As Tenant

  • Models\Store implements Misaf\VendraTenant\Contracts\TenantContract and extends Spatie's tenant, so Store IS the tenant: there is no tenants table, no Store -> Tenant relation, and no 1:1 pair. It is wired in config/vendra-tenant.php (model, relation) and config/multitenancy.php (tenant_model, tenant_finder).
  • Store::accessible() — active, provisioning-ready, not billing-suspended — is the request-serving boundary. Both Services\StoreDomainFinder and the engine's resolver search options go through it; never expose or resolve an inaccessible store.
  • Services\StoreDomainFinder is the adapter behind the engine's Misaf\VendraTenant\Contracts\HostTenantFinder port. Host-to-store mapping lives here, never in misaf/vendra-tenant.
  • A registered, active *.trycloudflare.com domain alias also serves the admin panel of its accessible store for a temporary quick tunnel. Other storefront domains keep the normal admin host checks.
  • A store has one primary domain (is_primary, read through Store::primaryDomain()); any other active domains are aliases that also resolve the store. The first domain CreateStoreAction creates is the primary for the life of the store: nothing replaces, demotes or removes it, and the database allows only one live primary per store.
  • Add aliases through Actions\AddStoreDomainAliasAction (its caller normalizes and validates the domain) and remove one through Actions\RemoveStoreDomainAliasAction (it keeps the alias as trashed history and refuses the primary); both force a storefront redeploy, because the container's Traefik rule lists every active domain and labels cannot change in place.
  • CompleteStoreProvisioningJob keeps only queue concerns; the checkpointed provisioning steps live in Actions\CompleteStoreProvisioningAction and failures go through Actions\FailStoreProvisioningAction, which never downgrades a ready store. Each step saves its own checkpoint instead of sharing a transaction.
  • Administrator-driven availability and failed provisioning recovery use SuspendStoreAction, ReactivateStoreAction, and RetryStoreProvisioningAction. Redeploy/retry use their domain actions and the existing deployment job/transition rules.
  • Business removal uses OffboardStoreAction, which requires an audit reason and soft-deletes after inactivation; restoration uses RestoreOffboardedStoreAction, rechecks reseller quota, and respects provisioning readiness.
  • Support\StoreTenantEntitlements binds the support TenantEntitlements from the store reseller's plan (console-owned stores are unrestricted). Custom domains (outside *.{storefront.base_domain}, StoreDomain::isCustom()) need PlanFeature::CustomDomain at create, alias and replace; aliases also respect PlanLimit::DomainsPerStore under a store row lock, and AddDomainAliasTableAction is disabled with the refusal as a tooltip at that limit. The panels catch EntitlementExceededException and show its message. Do not expose force deletion as an administrator workflow. assertCanAdd() locks the store row before counting, so creates inside a transaction cannot race past a limit. recordAdded() fires Events\StoreLimitApproached (after commit) once an add crosses 80% or 100% of a limit.
  • Deleting a store settles its storefront in Observers\StoreObserver, not in the caller: soft delete records StorefrontDesiredState::Stopped and queues ReconcileStorefrontJob; force delete queues DestroyStorefrontJob, addressed by slug because the deployment row is cascaded away first. Actions\DeleteStoreAction is the transactional entry point for a programmatic delete and no longer touches the runtime itself.
  • Two row-ownership columns, two mechanisms. Reusable domain packages own their rows through the neutral tenant_id and Misaf\VendraSupport\Tenancy\BelongsToTenant. Records describing the Store itself — store_domains, storefront_deployments — carry store_id and use Concerns\BelongsToStore / Scopes\StoreScope. Never add store_id to a reusable package's table, and never register a store_id table in the support TenantTableRegistry.
  • The billing reseller is optional and inverted: stores.reseller_id is a plain nullable indexed column, the reseller is fetched through Contracts\StoreResellerResolver, and ProvisionStoreAction / CreateStorePage::resolveReseller() take (Model&SubscriptionSubscriber)|null. A null reseller is a store the console owns directly, not an error.
  • It depends on misaf/laravel-docker-engine for driver-managed Engine access and on misaf/vendra-tenant for the tenancy engine it plugs its Store into. It must not depend on misaf/vendra-reseller: a store's billing reseller is reached through Contracts\StoreResellerResolver, which the reseller package binds. Keep the arrow pointing reseller → store.
  • Panels belong to their own packages. Put shared schemas, page bases, actions, and concerns here; put the panel-specific resource in vendra-console or vendra-reseller.

Provisioner Contract

  • provision, start, stop, restart, destroy, observe, logs — typed value objects on both sides, never arrays.
  • Idempotence is part of the contract: provisioning twice leaves one storefront; starting a running one, stopping a stopped one, and destroying an absent one all succeed.
  • observe() returns a StorefrontObservation rich enough to tell "stopped" from "running the wrong image", and must never answer "absent" for an unreachable runtime.
  • Compare the inspected STOREFRONT_CONFIG_BASE64 environment value with the saved deployment configuration. Configuration, image, and domain drift must trigger replacement before starting a stopped container; unchanged containers are started. Check the platform ownership label before reading logs too.
  • ContainerStorefrontProvisioner builds its container through Support\StorefrontContainerDefinitionFactory after Support\StorefrontConfigurationValidator accepts the configuration. Services\StorefrontContainerRuntime resolves the selected Engine client through ContainerManager; no Docker or Podman CLI calls are allowed.

Deployment Lifecycle

  • RequestStorefrontDeploymentAction → ProvisionStorefrontJob → StorefrontDeployment. Reconciliation and retry go through StorefrontDeploymentDispatchCommand; do not add a second dispatch path.
  • CreateStorePage skips the request only for explicit create_storefront=false. The console form exposes this through StorefrontConfigurationFields::creationToggle(); the reseller form remains mandatory. Both collect only image and slug and deploy with sample storefront details. The tenant administrator replaces contact and business details on the admin Storefront settings page.
  • StorefrontImage records the approved immutable image reference and optional notes. New deployments may select only active catalog entries; existing deployments keep using their selected entry after it is disabled. Never reintroduce global STOREFRONT_IMAGE configuration.
  • Write status only via markProcessing(), markReady(), markRequested(), markFailed(). Enums\StorefrontDeploymentStatus::transitions() is the transition table and InvalidStorefrontTransitionException is the rejection.
  • A failing attempt with retries left stays Processing; only ProvisionStorefrontJob::failed() writes Failed.
  • ProvisionStorefrontJob runs on its own storefronts queue, served by the single worker that holds a runtime socket. Do not move it onto a shared queue.
  • Panel observation/log reads use Support\StorefrontRuntimeSnapshots, which throttles collection through RecordStorefrontRuntimeSnapshotJob on the same worker. Only RecordStorefrontRuntimeSnapshotAction accesses the provisioner. Central-cache snapshots contain plain arrays, expire after 30 seconds, and are keyed by deployment intent; synchronous queue connections must never run panel reads inline.

Configuration

  • Read infrastructure values in config/vendra-store.php only through the injected Support\StorefrontSettings. Keep it bound with bind() so config changes are picked up on the next resolve.
  • Whether the platform is creating stores at all is Settings\StoreCreationSettings (group store_creation, global repository), read directly as ->open. Console and reseller store creation both read it, so the shared rule lives below both panels.
  • Runtime differences (Docker vs. a Podman compatibility socket, log driver, health-check behaviour) are configuration, never a branch. Do not add runtime sniffing.

Testing

  • Use the shared fakeDockerEngine() transport helper, which registers a test driver through ContainerManager; domain tests must not require Docker or Podman.
  • Cover each lifecycle transition, including the illegal ones the enum rejects, and the retry path that must not mark a deployment Failed early.
  • Cover the tenancy side too: Store acting as the current tenant, host resolution, store_id scoping of store domains, and the billing port defaulting to Support\NullStoreResellerResolver.
  • Keep the architecture rules in tests/ArchTest.php asserting this package does not use Misaf\VendraReseller or Misaf\VendraConsole, and that actions, jobs and models stay behind the provisioning port.

Filament

  • Resources with a cluster live in src/Filament/Clusters/Resources/; resources without one live in src/Filament/Resources/. This package currently ships pages, schemas, actions, and concerns rather than resources.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle