The Ally package provides a partner and reseller network management system that enables businesses to scale via tiered partnerships, a pre-paid distribution credit system, and partitioned client management.
- Hierarchical Partner Profiles: Detailed reseller profiles linked to system
Useraccounts for authenticated access. - Tiered Partnership Logic: Support for partner levels (e.g., Standard, Gold, Platinum) to govern custom pricing and permissions.
- Distribution Credit Wallets: Native integration with the Wallet package to manage pre-paid credits for service provisioning.
- Portfolio Isolation: Partitioned data views where resellers only manage clients they have onboarded.
- Automated Provisioning: Real-time service fulfillment (licenses, server resources) using credit balances to bypass manual payment gateways.
- Event Hooks: Hook into system events like
LowCreditEventto automate business logic when a partner's balance is low. - Low-Credit Notifications: Automated alerts via the Mail package when a partner's balance drops below a critical threshold.
Ally is a package that requires installation to initialize the partner network.
php dock package:install Ally --packagesThis will automatically:
- Run database migrations for
ally_*and related tables. - Register the
AllyServiceProvider. - Publish the configuration file.
Configuration file: App/Config/ally.php
return [
'default_tier' => 'standard',
'credit_conversion_rate' => 100, // e.g., 100 units = $1.00
'auto_provision_wallet' => true,
'low_credit_threshold' => 500, // Trigger notification at 500 units
'urls' => [
'dashboard' => 'partner/dashboard',
],
];Ally works natively with the Client package to provide portfolio isolation.
The connection is strictly maintained via the System User ID:
Ally Reseller -> User (id: 1) <- Client (owner_id: 1)
- Ally manages the partner's profile, wallet, and tier.
- Client records the end-customers belonging to that partner.
Since clients are scoped by the owner_id, you can easily fetch all clients for a specific reseller.
use Client\Client;
$user = $this->auth->user(); // The partner user
// 1. Get Reseller Profile
$reseller = Ally::findByUser($user->id);
// 2. Get Their Clients
$myClients = Client::getByReseller($user->id);
foreach ($myClients as $client) {
echo $client->name; // "Acme Corp"
}Ally enables you to build a dashboard for your partners to view their balance, tier, and performance.
In your controller, retrieve the currently authenticated reseller.
use Ally\Ally;
public function index()
{
$user = $this->auth->user();
$reseller = Ally::findByUser($user->id);
// Get stats from Analytics service
$stats = Ally::analytics()->resellerStats($reseller);
// Get transaction history from Wallet
$history = $reseller->wallet->transactions()->latest()->limit(10)->get();
return $this->asView('partner.dashboard', compact('reseller', 'stats', 'history'));
}You can then access all reseller properties directly.
// Tier and Company
echo $reseller->company_name; // "Elite Global"
echo $reseller->tier; // "gold"
// Wallet Balance (from stats or direct relationship)
echo $stats['balance']; // 5000
// OR
echo $reseller->wallet->balance;
// Transaction History
foreach ($history as $txn) {
echo "{$txn->type}: {$txn->amount} ({$txn->created_at})";
}The Ally::make() builder handles partner onboarding. You can use fluent methods for tiers.
use Ally\Ally;
$reseller = Ally::make()
->user($userId)
->company('Elite Global Partners')
->platinum() // or ->gold(), ->standard()
->create();Methods accept both Reseller IDs and objects.
// Add 50,000 credit units (Admin Action)
Ally::addCredits($reseller, 50000);
// Deduct credits for a service (Reseller Action)
if (Ally::provision($reseller, 200, 'Issue Pro License')) {
// Deducted 200 credits successfully.
}Ally is often the core of a "Channel Sales" strategy where your application is sold by third-party partners.
In this scenario, we use the calculateTierCost() helper to determine the discounted rate.
$reseller = Ally::findByUser($authId);
$basePrice = 1000; // Standard cost is 1,000 credits
// Calculate cost based on tier (Platinum: 30% off, Gold: 15% off)
$cost = $reseller->calculateTierCost($basePrice);
if (Ally::provision($reseller, $cost)) {
// Reseller paid the discounted rate.
Forge::make()->client($clientId)->create();
}Resellers often onboard multiple clients at once. Ally manages the ledger and ensures the partner has sufficient funds for the entire operation.
$clients = $request->post('clients'); // Array of client data
$licenseCost = 500;
$totalCost = count($clients) * $licenseCost;
// 1. Verify total affordability first
if ($reseller->wallet->balance < $totalCost) {
throw new Exception("Insufficient credits for bulk operation.");
}
// 2. Process batch
foreach ($clients as $data) {
Ally::provision($reseller->id, $licenseCost);
$client = Client::make()
->name($data['name'])
->reseller($reseller->user_id)
->create();
Forge::make()->client($client->id)->create();
}The Ally package automatically monitors credit health. When Ally::provision() is called and the balance falls below the threshold defined in App/Config/ally.php, a LowCreditEvent is dispatched.
The package includes a built-in SendLowCreditNotification listener that handles email alerts. You can hook onto this event for additional logic.
Hook onto the Event
You can listen to this event to automate actions, such as suspending the reseller's ability to provision or notifying an account manager.
// In a ServiceProvider or Event Listener
use Core\Event;
use Ally\Events\LowCreditEvent;
Event::listen(LowCreditEvent::class, function (LowCreditEvent $event) {
// 1. Log the event for admin review
logger('ally.log')->info("Low credit warning for {$event->reseller->company_name}: {$event->balance}");
// 2. Custom Automation: Pause high-value provisioning
$event->reseller->updateMetadata('hold_provisioning', true);
});- Reseller Experience: Receives an email (via
LowCreditNotification) alerting them to top up. - Automation: Your custom listener can preemptively pause services until the balance is restored.
You can use the Analytics Service to generate end-of-month reports for your admin dashboard. This helps identifying high-performing partners who might be eligible for a tier upgrade.
use Ally\Ally;
use Ally\Enums\ResellerTier;
use System\Helpers\DateTimeHelper;
public function monthlyReport()
{
// 1. Get top performers (explicit string formatting)
$leaders = Ally::analytics()->topResellers(
limit: 5,
start: DateTimeHelper::now()->subMonth()->toDateTimeString(),
end: DateTimeHelper::now()->toDateTimeString()
);
// 2. Get network health (total outstanding credits)
$liability = Ally::analytics()->totalCreditsDistributed();
// 3. Generate summary
foreach ($leaders as $partner) {
if ($partner['acquisitions'] > 100 && $partner['tier'] !== ResellerTier::PLATINUM->value) {
// Suggest upgrade
logger('ally.log')->info("Partner eligible for upgrade: {$partner['company']}");
}
}
return $this->asView('admin.reports.partners', compact('leaders', 'liability'));
}Tiers govern your reseller logic (pricing, discounts, access). The package provides a ResellerTier enum for standardisation:
| Tier | Value | Description |
|---|---|---|
PLATINUM |
platinum |
Highest volume partners, maximum discount. |
GOLD |
gold |
Mid-tier partners with moderate benefits. |
STANDARD |
standard |
Default tier for new resellers. |
You can use fluent helper methods for type-safe comparisons:
if ($reseller->isPlatinum()) {
// Apply VIP treatment
}| Method | Description |
|---|---|
make() |
Returns a fluent AllyBuilder for registration. |
findByUser($id) |
Locates a reseller profile by associated User ID. |
findByRefid($refid) |
Locates a reseller profile by its public reference. |
addCredits($reseller, $amt) |
Increases the credit balance of a reseller. Accepts object or ID. |
provision($reseller, $cost) |
Deducts credits and checks for low balance alerts. Accepts object or ID. |
analytics() |
Returns the AnalyticsManager service. |
| Method | Description |
|---|---|
topResellers($limit, $start, $end) |
Returns array of top partners. |
tierSnapshot($start, $end) |
Returns count of partners per tier. Supports forReseller() scoping. |
creditTrends($start, $end) |
Returns historical volume data. Supports fluent intervals and scoping. |
totalCreditsDistributed() |
Returns total sum of all wallet balances. |
resellerStats($reseller) |
Returns balance and tier stats for a specific partner. |
daily(), monthly(), yearly() |
Chainable methods to set trend aggregation interval. |
forReseller($id) |
Chainable method to scope analytics to a specific partner. |
| Method | Type | Description |
|---|---|---|
user() |
relation |
BelongsTo relationship to the User model. |
scopeActive() |
scope |
Filters for resellers with 'active' status. |
scopePlatinum() |
scope |
Filters for resellers in the Platinum tier. |
scopeGold() |
scope |
Filters for resellers in the Gold tier. |
scopeStandard() |
scope |
Filters for resellers in the Standard tier. |
isPlatinum() |
bool |
Checks if reseller is Platinum tier. |
isGold() |
bool |
Checks if reseller is Gold tier. |
isStandard() |
bool |
Checks if reseller is Standard tier. |
calculateTierCost($cost) |
int |
Returns cost after applying tier discount. |
updateMetadata($k, $v) |
bool |
Fluently updates a key in the metadata JSON. |
metadata |
attribute |
JSON data for whitelabeling (logos, etc). |
| Error/Log | Cause | Solution |
|---|---|---|
InsufficientCredits |
The cost exceeds the wallet balance. | Add credits or reduce the provision cost. |
| "Notification not sent" | Mail driver is not configured properly. | Check App/Config/mail.php settings. |
| "Wrong owner_id" | Client is linked to a different partner. | Verify attribution in the Client package. |
Ally provides a built-in AnalyticsManager with a fluent, chainable API to track reseller performance and distribution metrics.
You can track historical credit distribution trends using daily(), monthly(), or yearly() intervals.
// Monthly credit distribution volume for the last 6 months
$trends = Ally::analytics()
->monthly()
->creditTrends('2023-01-01', '2023-06-30');
// Returns ['2023-01' => 75000, '2023-02' => 82000, ...]Any analytical call can be scoped to a specific reseller. This is ideal for reseller-specific reports.
// Get a specific reseller's credit volume trends
$partnerTrends = Ally::analytics()
->forReseller($resellerId)
->daily()
->creditTrends($start, $end);Visualize the composition of your partner network.
$stats = Ally::analytics()->tierSnapshot();
/* Sample Output:
[
'platinum' => 12,
'gold' => 45,
'standard' => 120
]
*/Track the total volume of credits currently held in partner wallets.
$outstandingCredits = Ally::analytics()->totalCreditsDistributed();
// Returns (int): 2500000- Strict Wallet Modification: The
addCredits()method should never be exposed to public-facing reseller routes. It is exclusively for administrative or payment-webhook use. - Whitelabeling: Use the
metadatafield on theResellermodel to store branding information. Your UI can then check if a user belongs to a reseller and dynamically skin the dashboard. - Tier Restrictions: Beyond pricing, use tiers to control which Forge products a partner can distribute (e.g., Silver partners can only sell Basic licenses).
Ally is designed for B2B Resellers and Partners who function as distributors. They have:
- A wallet balance for pre-purchasing inventory.
- A dashboard to manage their own clients.
- Tiered pricing privileges.
Refer, on the other hand, is a lightweight Affiliate/Referral system. It is for:
- Tracking invite links (e.g.,
?ref=ben). - Rewarding users for signups (e.g., "Give $5, Get $5").
- Simple commission usage without client management.
Can they works together? Yes. You can use Refer to acquire new partners (e.g., refer a friend to become a reseller), and then use Ally to manage that partner's lifecycle. They do not share code dependencies.
In this workflow, an existing partner refers a new user who then becomes a sub-reseller.
1. Generate Invite Link
Existing partners share a Refer tracked link.
$link = Refer::link(auth()->user(), 'become-partner');
// https://app.com/register?ref=BEN1232. Handle Registration When the new user registers, capture the referral code.
public function register() {
// 1. Create System User
// Note: Use only() or explicit post() retrieval
$user = User::create($this->request->only(['name', 'email', 'password']));
// 2. Track Conversion (Refer Package)
// This logs that BEN123 referred this new user
Refer::complete($this->request->post('ref'), $user);
// 3. Onboard as Reseller (Ally Package)
$reseller = Ally::make()
->user($user)
->company($this->request->post('company_name'))
->standard()
->create();
return $this->asView('dashboard', compact('reseller'));
}3. Reward the Referrer (Optional)
You can use the ReferralCompleted event to give the referrer 1000 free credits.
Event::listen(ReferralCompleted::class, function ($event) {
if ($event->tag === 'become-partner') {
// Find the referrer's reseller profile
$referrer = Ally::findByUser($event->referrer->id);
// Give bonus credits
Ally::addCredits($referrer, 1000);
}
});