misaf / vendra-reseller
Reseller domain and self-service panel for the Vendra platform
Requires
- php: ^8.4
- filament/filament: ^5.6.8
- illuminate/console: ^13.0
- illuminate/contracts: ^13.0
- illuminate/database: ^13.0
- illuminate/http: ^13.0
- illuminate/support: ^13.0
- misaf/laravel-email-validation: ^1.0
- misaf/vendra-localization: v1.12.3
- misaf/vendra-store: v1.12.3
- misaf/vendra-subscription: v1.12.3
- misaf/vendra-support: v1.12.3
- misaf/vendra-tenant: v1.12.3
- misaf/vendra-transaction: v1.12.3
- spatie/laravel-multitenancy: ^4.1.3
- spatie/laravel-package-tools: ^1.93.1
- spatie/laravel-sluggable: ^4.0.2
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
The reseller domain and the reseller self-service panel for Laravel. A reseller is billed for one or more stores: it holds the subscription, the plan limits are enforced against it, and its user manages its stores from its own Filament panel.
A reseller spans several tenants, so the panel runs outside the tenant middleware stack. There is no current tenant here; everything is scoped by reseller.
That scoping lives in one place — StoreResource::getEloquentQuery() — because
the table, the record actions, and global search all build on it. A user whose
reseller cannot be resolved sees nothing at all: offboarding soft-deletes the
Reseller while the user's canonical identity and membership can still sign
in, and where('reseller_id', null) means whereNull to Eloquent, which is
every store the platform owns directly.
Reseller users are canonical users (misaf/vendra-user): reseller_users is a
membership pivot linking a user to its reseller, not a user table. Identity
columns live on users; disabling or replacing a user retires memberships
while the identity — and any tenant access it holds — stays intact.
Requirements
- PHP 8.4+
- Laravel 13
- Filament 5
misaf/vendra-store,misaf/vendra-subscription,misaf/vendra-transaction,misaf/vendra-tenant,misaf/vendra-localization,misaf/vendra-userandmisaf/vendra-support
Installation
composer require misaf/vendra-reseller php artisan migrate
The resellers table and the reseller_users membership pivot are created by
migrations; the host application's config/auth.php points the reseller
guard at the platform-scoped reseller provider and the reseller
password broker, which stores its reset tokens in
reseller_password_reset_tokens so neither a tenant user sharing the email nor
the console panel can consume them. A user may
enter the panel while holding an active membership — one active membership per
reseller is enforced, and disabled history is kept as soft-deleted rows.
The panel is served on reseller.<app host>, derived from app.url — nothing
here hard-codes a host.
Usage
Creating a reseller
use Misaf\VendraReseller\Actions\CreateResellerAction; $reseller = app(CreateResellerAction::class)->execute( plan: $plan, username: 'acme', email: 'user@acme.test', password: $password, );
This creates the reseller, its first panel user (CreateResellerUserAction),
and the subscription to the given plan.
User accounts
Credential and lifecycle changes go through
UpdateResellerUserPasswordAction, UpdateResellerUserEmailAction,
SetResellerUserAccountEnabledAction, and ReplaceResellerUserAction, all
operating on the canonical User and its membership. Replacement retires the
previous membership as history — the former identity is never deleted — and
creates the new user through CreateResellerUserAction; disabling retires
the membership so panel access stops while the identity survives; email changes
also keep the reseller contact email synchronized.
Offboarding
use Misaf\VendraReseller\Actions\OffboardResellerAction; app(OffboardResellerAction::class)->execute($reseller, reason: 'Contract ended');
OffboardResellerAction is the only supported removal path. Reseller's
deleting hook throws for a reseller that was never offboarded, and
Events\ResellerOffboarded is the extension point for downstream work.
The subscriber
Models\Reseller implements SubscriptionSubscriber, so plan limits are
answered by misaf/vendra-subscription and store quotas by
Misaf\VendraStore\Support\StoreQuota — no limit arithmetic is duplicated
here.
$reseller->isSubscriptionActive(); $reseller->activeSubscription(); $reseller->subscribedUnitCount(); $reseller->suspendActiveUnits(); $reseller->reactivateSuspendedUnits(); $reseller->allows('feature-key');
Support\TransactionSubscriptionCharger implements the SubscriptionCharger
contract by posting an internal withdrawal against the payer's wallet through
misaf/vendra-transaction.
Subscription reactions
The subscription engine raises only generic lifecycle events. This package turns
them into reseller behaviour, wired in Providers\ResellerServiceProvider:
| Event | Listener |
|---|---|
SubscriptionActivated |
NotifyActivatedSubscriber |
SubscriptionCancelled |
SuspendSubscriberStores |
SubscriptionExpiringSoon |
RemindExpiringSubscriber |
SubscriptionGraceExpired |
SuspendSubscriberStores |
Add a new reaction as a listener here rather than pushing reseller knowledge into the subscription engine, and do not register these listeners again in the host application.
Commands
php artisan vendra-subscription:provision {name} {domain} {username} {email} \
[--reseller=] [--plan=] [--password=] [--if-missing] [--seed]
Provisions a store with its domain, administrator user, and role assignment. It calls
Misaf\VendraStore\Actions\ProvisionStoreAction — the reseller-specific
part is only which reseller is attached (--reseller), or created and
subscribed (--plan).
Panel
Providers\ResellerPanelServiceProvider registers the panel (guard, broker,
domain, login and registration pages, widgets). Providers\ResellerServiceProvider
registers the console command and the event listeners. The split is deliberate.
Store screens are reused, not copied: the panel's resources extend
misaf/vendra-store's CreateStorePage, StorefrontConfigurationFields
and ReplaceDomainAction, supplying the authenticated user's reseller.
Resolve the acting reseller with Filament\Concerns\InteractsWithCurrentReseller;
Http\Middleware\AddResellerToRequestJobContext carries it into queued work.
The reseller dashboard shows subscribed store usage against the plan allowance,
remaining capacity, subscription state, and counts for active, provisioning,
and failed stores. Store listings expose derived store and storefront-deployment
statuses and filters, with all queries still rooted in
StoreResource::getEloquentQuery(). Runtime administration and container details
remain console concerns and are not exposed here.
Testing
Build resellers from the package factory and users as canonical users with a
reseller_users membership ($reseller->users()->attach($user)), then assert
quota and suspension behaviour through the actions. Panel tests must not assume
a current tenant.
php artisan test --compact --testsuite=vendra-reseller
License
MIT. See LICENSE.