Search by

misaf / vendra-reseller

misaf

Reseller domain and self-service panel for the Vendra platform

Package info

github.com/misaf/vendra-reseller

Type:vendra-module

pkg:composer/misaf/vendra-reseller

Statistics

Installs: 0

Dependents: 3

Suggesters: 0

Stars: 1

Open Issues: 0

v1.12.3 2026-08-22 21:15 UTC

This package is auto-updated.

Last update: 2026-09-22 18:13:36 UTC


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. Panel access already requires an active, non-offboarded reseller, but the guard stays explicit because where('reseller_id', null) means whereNull to Eloquent, which is every store the platform owns directly.

Each reseller has exactly one main account: resellers.user_id (required, unique) points at a canonical tenantless user (misaf/vendra-user, tenant_id null). Identity columns live on users; replacing the account repoints user_id while the former 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-user and misaf/vendra-support

Registration and provisioning validate credentials through vendra-user’s Support\UserRules::username() and password(). Username rules require 3–12 letters, numbers, dashes, or underscores; registration additionally requires ASCII. Password strength follows the application default policy, for a supplied --password and for the generated one alike.

Installation

composer require misaf/vendra-reseller
php artisan vendor:publish --tag=vendra-reseller-migrations
php artisan migrate

The published migration creates the resellers table; the host application's config/auth.php points the reseller guard at the tenantless 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 only while it is the main account of an active reseller: deactivating the reseller (SetResellerActiveAction) is how its account is locked out, and an offboarded reseller grants nothing.

The panel is served on vendra-reseller.domain, which the panel provider reads with Config::string(). The config file defaults it to the reseller. subdomain of APP_URL's host, and VENDRA_RESELLER_DOMAIN overrides it — nothing here hard-codes or re-derives a host. Because the value is resolved when config loads rather than per call, changing app.url at runtime does not move the panel.

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 main account (through vendra-user's CreateUserAction), the reseller pointing at it, and the subscription to the given plan.

User accounts

Password changes go through vendra-user's UpdateUserPasswordAction, which handles tenant-less users. Email changes go through UpdateResellerUserEmailAction, and ReplaceResellerUserAction creates a new main account and repoints the reseller to it — the former identity is never deleted. There is no separate account disable: deactivate the reseller.

A reseller stores no name, description, slug, or contact email of its own: it is identified by its main account's username (Reseller::displayName(), or Reseller::displayNames() for option lists), and subscription notifications go to that account.

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.

Activation

use Misaf\VendraReseller\Actions\SetResellerActiveAction;

app(SetResellerActiveAction::class)->execute($reseller, active: false);

SetResellerActiveAction is the supported way to flip active. An inactive reseller cannot create stores; an offboarded reseller cannot be reactivated.

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->canHoldUnits();
$reseller->activeSubscription();
$reseller->subscribedUnitCount();
$reseller->allows('feature-key');

Support\ResellerStoreSuspender implements the subscription package's SubscriptionUnitSuspender contract: it suspends or reactivates a reseller's stores through SuspendStoreForBillingAction or ReactivateStoreForBillingAction, so each storefront stops or starts with it.

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-reseller:provision-store {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, by id or by its user's username), 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 ReplaceDomainTableAction, 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 (Filament\Pages\Dashboard) lists its widgets in a fixed order: GettingStarted walks a new reseller from subscribing to a live storefront and disappears once one is live; PlanSummary shows the plan, its renewal or trial end (warning a week ahead), store usage against the allowance, and stores suspended for billing; StoresNeedingAttention lists stores still provisioning, failed, or with a failed storefront, with the recorded reason; LatestStores lists the newest stores. Store counts come from Misaf\VendraStore\Support\StoreStatusCounts in one grouped query. 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, which creates the main account; pass your own with Reseller::factory()->for($user) or repoint one with $reseller->user()->associate($user)->save(), 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.