a2zwebltd/laravel-affiliate

Generic affiliate / revenue-share engine for Laravel: opt-in workflow, monthly commission closure, admin adjustments, partner-initiated payout requests, Blade+Alpine and Livewire dashboards.

Maintainers

Package info

github.com/a2zwebltd/laravel-affiliate

pkg:composer/a2zwebltd/laravel-affiliate

Statistics

Installs: 10

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.1 2026-04-30 10:49 UTC

This package is auto-updated.

Last update: 2026-04-30 10:49:32 UTC


README

Generic affiliate / revenue-share engine for Laravel โ€” opt-in workflow, monthly commission closure, admin adjustments, partner-initiated payout requests, immutable commission statements, Blade+Alpine and Livewire dashboards.

Features

  • ๐Ÿ”— Cookie + query-param attribution: First-touch or last-touch, configurable lifetime.
  • ๐Ÿ“… Monthly closure: Idempotent close-month job snapshots commission rates per row; recalculation safe.
  • ๐Ÿงพ Immutable commission statements: Auto-generated, PDF-rendered, stamped with issuing-entity snapshot.
  • ๐Ÿ’ธ Partner-initiated payouts: Self-serve payout requests with PDF invoice upload + admin approval flow.
  • โš–๏ธ Admin adjustments: Internal-only revenue corrections, applied at the partner's commission rate.
  • ๐ŸŽจ Drop-in UI: Blade + Alpine partner dashboard, plus optional Livewire forms.
  • ๐Ÿ› ๏ธ Nova-ready: Resources, actions, and a single-line NovaIntegration::resources() registration.
  • ๐ŸŒ i18n: Every user-facing string is wrapped in __() and ready for the host app's translation pipeline.

Requirements

  • PHP 8.2+
  • Laravel 12 or 13
  • barryvdh/laravel-dompdf ^3.1 (PDF statements)
  • rinvex/countries ^9.1 (country dropdown on the apply form)
  • (optional) laravel/nova ^5 โ€” admin resources & actions
  • (optional) livewire/livewire ^3 / ^4 โ€” reactive partner forms

Installation

composer require a2zwebltd/laravel-affiliate

The service provider is auto-discovered. Publish migrations and run them:

php artisan vendor:publish --tag=affiliate-migrations
php artisan migrate

Optionally publish config and views:

php artisan vendor:publish --tag=affiliate-config
php artisan vendor:publish --tag=affiliate-views

Quick Start

1. Implement RevenueResolver in your app

The package never queries your billing system directly โ€” you implement a thin adapter:

use A2ZWeb\Affiliate\Contracts\RevenueResolver;

class StripeRevenueResolver implements RevenueResolver
{
    public function revenueCentsForUserMonth(int $userId, int $year, int $month): int
    {
        // Sum realised revenue (paid invoices) for $userId in (year, month).
        return (int) Invoice::query()
            ->where('user_id', $userId)
            ->whereYear('paid_at', $year)
            ->whereMonth('paid_at', $month)
            ->sum('amount_cents');
    }
}

Bind it in config/affiliate.php:

'resolvers' => [
    'revenue' => \App\Affiliate\StripeRevenueResolver::class,
    'referred_user_info' => null, // optional
],

2. Add the affiliate concern to your User model

use A2ZWeb\Affiliate\Concerns\HasAffiliateProgram;

class User extends Authenticatable
{
    use HasAffiliateProgram;
}

This adds affiliatePartner() and affiliateLink().

3. Capture attribution on signup

In your registration controller (or a Registered listener):

use A2ZWeb\Affiliate\Services\ReferralAttributor;

app(ReferralAttributor::class)->attributeNewUser($user, $request);

The cookie is set automatically by the affiliate.capture middleware whenever a visitor lands on a page with ?aff=CODE.

4. Schedule monthly closure

In routes/console.php (or app/Console/Kernel.php):

Schedule::command('affiliate:close-month')
    ->monthlyOn(1, '02:00')
    ->onOneServer();

That's it โ€” once a partner is approved and you ship referral codes, the engine will close commissions on the 1st of each month.

Usage Examples

Manual referral attachment (Nova action)

When a referral was missed (cookie cleared, signup race, manual import), use the Attach referral action on the AffiliatePartner Nova resource:

  1. Open the partner detail page in Nova.
  2. Run Attach referral โ€” provide an email or user ID, optional attributed_at (for backdating), and tick "Recalculate" to trigger the monthly closer for past months.
  3. The action enforces the referred_user_id UNIQUE constraint and config-driven self-referral / first-touch rules.

Programmatic equivalent:

use A2ZWeb\Affiliate\Services\ReferralAttributor;
use Illuminate\Support\Carbon;

$referral = app(ReferralAttributor::class)->manuallyAttach(
    $partner,
    $referredUser,
    Carbon::parse('2026-02-15'),
);

Recalculate a single partner

php artisan affiliate:recalc-partner 1234

Re-runs MonthlyCloser for partner user 1234 โ€” picks up new referrals, adjustments, or rate overrides without touching other partners.

Issue a commission statement

Statements are generated when an admin marks a payout request as approved (or via the Generate statement for period Nova action). Once issued they are immutable โ€” corrections happen via AffiliateAdjustment rows, never by editing the statement.

use A2ZWeb\Affiliate\Services\CommissionStatementGenerator;

$statement = app(CommissionStatementGenerator::class)->generateForPartner(
    $partner,
    Carbon::parse('2026-02-01')->startOfMonth(),
    Carbon::parse('2026-02-01')->endOfMonth(),
);

Nova Integration

If laravel/nova is installed, register the resources in your NovaServiceProvider:

use A2ZWeb\Affiliate\Nova\NovaIntegration;

protected function resources(): void
{
    Nova::resources(NovaIntegration::resources());
}

protected function gates(): void
{
    // ...your existing setup...
    Nova::mainMenu(fn (Request $request) => [
        // ...
        NovaIntegration::menuSection(),
    ]);
}

Resources registered:

  • AffiliatePartner โ€” applications, status, payout details, audit trail.
  • AffiliateCommission โ€” read-only monthly rows.
  • AffiliateReferral โ€” read-only attribution log.
  • AffiliatePayoutRequest โ€” partner-initiated payouts with approval/reject/mark-paid actions.
  • AffiliateCommissionStatement (+ AffiliateCommissionStatementLine) โ€” issued statements with PDF download.
  • AffiliateAdjustment โ€” admin-only revenue corrections.

Bundled actions: Approve, Reject, Suspend, Recalculate commissions, Generate statement for period, Attach referral, Approve payout, Reject payout, Mark as paid, Issue statement, Mark statement paid, Cancel statement.

Configuration

All keys in config/affiliate.php can be overridden by environment variables. A few highlights:

Env Default Description
AFFILIATE_REVENUE_SHARE_BP 3000 Default commission rate in basis points (3000 = 30%).
AFFILIATE_MIN_REFERRED_USERS 2 Number of paying referrals required before a user can apply.
AFFILIATE_MIN_PAYOUT_CENTS 5000 Minimum balance required to request a payout.
AFFILIATE_ATTRIBUTION first_touch first_touch or last_touch.
AFFILIATE_WINDOW_MODE lifetime lifetime or windowed.
AFFILIATE_WINDOW_MONTHS 12 When windowed, how many months a referral keeps generating commissions.
AFFILIATE_COOKIE_TTL_DAYS 60 Attribution cookie lifetime.
AFFILIATE_ADMIN_EMAIL โ€” Address that receives admin notifications.
AFFILIATE_LAYOUT layouts.app Blade layout used by partner pages.
AFFILIATE_NOVA_USER_RESOURCE App\Nova\User Class used for BelongsTo user fields.

Per-partner rate overrides live on affiliate_partners.revenue_share_bp and take precedence over the global default for future commissions; historical rows keep their snapshot rate.

Architecture

Data model

users โ”€โ”€< affiliate_partners
  โ”‚            โ”‚
  โ”‚            โ”œโ”€โ”€< affiliate_referrals โ”€โ”€> users (referred)
  โ”‚            โ”œโ”€โ”€< affiliate_commissions   (one per partner+referral+month)
  โ”‚            โ”œโ”€โ”€< affiliate_adjustments   (admin-only revenue correction rows)
  โ”‚            โ””โ”€โ”€< affiliate_payout_requests
  โ”‚                     โ”‚
  โ”‚                     โ””โ”€โ”€< affiliate_commission_statements
  โ”‚                                 โ”‚
  โ”‚                                 โ””โ”€โ”€< affiliate_commission_statement_lines
  โ””โ”€โ”€< affiliate_terms_acceptances    (versioned ToS audit log)

Lifecycle

  1. Visit โ€” affiliate.capture middleware reads ?aff=CODE and sets a cookie.
  2. Signup โ€” host app calls ReferralAttributor::attributeNewUser() which writes an affiliate_referrals row (subject to UNIQUE on referred_user_id).
  3. Application โ€” qualifying user submits the apply form; AffiliatePartner row is created with status=pending.
  4. Decision โ€” admin approves/rejects via Nova; partner email is dispatched.
  5. Monthly close โ€” affiliate:close-month iterates approved partners, sums revenue from RevenueResolver, computes commission rows.
  6. Payout request โ€” partner self-serves; admin approves; MarkPayoutRequestPaid action runs the PayoutCompletionWorkflow which issues an immutable PDF statement.

Key services

Service Responsibility
ReferralAttributor Cookie/code attribution; manuallyAttach() for admin tools.
MonthlyCloser Idempotent close-month + per-partner recalc.
CommissionCalculator Per-partner-month โ†’ commission cents.
CommissionStatementGenerator / CommissionStatementIssuer Draft โ†’ issue lifecycle.
PayoutRequestService / PayoutCompletionWorkflow Approve/reject/pay flows.
EligibilityChecker "Can this user apply?" gate.
PartnerStatistics Cached KPI feed for the partner dashboard.

Routes

By default the package mounts a dashboard/affiliate route group with web,auth middleware:

Route Name Purpose
GET / affiliate.dashboard Partner dashboard (state-aware: no-partner / pending / approved / rejected / suspended).
GET /apply affiliate.apply.show Apply form.
POST /apply affiliate.apply.store Submit application.
PATCH /payout-details affiliate.payout-details.update Edit payout details after approval.
POST /payouts affiliate.payouts.store Request payout.
DELETE /payouts/{request} affiliate.payouts.cancel Cancel pending payout request.
GET /statements/{statement} affiliate.statements.show Partner-facing statement detail.
GET /statements/{statement}/download affiliate.statements.download Signed PDF download.

Disable the default routes by setting affiliate.routes.enabled = false and registering your own.

Localization

Every user-facing string passes through __() โ€” including Nova labels, notification subjects, Livewire flash messages, and the PDF statement template. The package ships no language files; the host app is expected to extract keys via its own pipeline (e.g. a translate:extract artisan command). Run your extractor over the package's src/ and resources/views/ paths to harvest all keys.

Testing

composer test

The test suite uses Pest + Orchestra Testbench and exercises the full closure lifecycle with stub revenue resolvers.

Contributing

Issues and PRs are welcome. Please open a discussion before tackling large changes โ€” the engine has a few non-obvious invariants (idempotent closure, immutable statements) that are easy to break.

Security

Found a vulnerability? Please email contact@a2zweb.co rather than opening a public issue.

Credits

License

The MIT License (MIT). See the license field in composer.json.