Private
Public Access
1
0

Add all files

This commit is contained in:
root
2025-11-30 11:57:22 +01:00
parent 2ee51cac4f
commit 044271db3d
1476 changed files with 142544 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Admin\Resources;
use App\Admin\Components\UserComponent;
use App\Helpers\ExtensionHelper;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource\Pages;
use Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource\RelationManagers;
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
class AffiliateResource extends Resource
{
protected static ?string $model = Affiliate::class;
protected static ?string $navigationIcon = 'ri-hand-coin-line';
protected static ?string $activeNavigationIcon = 'ri-hand-coin-fill';
protected static ?string $navigationGroup = 'Administration';
public static function form(Form $form): Form
{
$extension = ExtensionHelper::getExtension('other', 'Affiliates');
return $form
->schema([
Toggle::make('enabled')->default(true)->columnSpanFull(),
UserComponent::make('user_id'),
TextInput::make('code')
->label('Referral Code')
->required()
->minLength(5)
->maxLength(20)
->unique(ignoreRecord: true),
TextInput::make('reward')
->label('Affiliate Reward')
->helperText('Percentage of the purchase amount the affiliated user would receive as a reward.')
->placeholder($extension->config('default_reward'))
->numeric()
->minValue(0)
->maxValue(100)
->suffix('%'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('user.name')->description(function (Affiliate $affiliate) {
if (count($affiliate->earnings) <= 0) {
return null;
}
return 'Earnings - ' . implode(', ', array_map(function ($key, $value) {
return "$key: $value";
}, array_keys($affiliate->earnings), $affiliate->earnings));
}),
TextColumn::make('code')
->badge(),
TextColumn::make('visitors')
->numeric()
->sortable(),
TextColumn::make('signups')
->numeric()
->sortable(),
TextColumn::make('created_at')
->label('Signed Up')
->since()
->sortable()
->dateTimeTooltip(),
])
->filters([])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
RelationManagers\AffiliatesRelationManager::class,
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListAffiliates::route('/'),
'edit' => Pages\EditAffiliate::route('/{record}/edit'),
];
}
protected function getHeaderActions(): array
{
return [
$this->getCreateFormAction()
->formId('form'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource\Pages;
use Filament\Resources\Pages\CreateRecord;
use Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource;
class CreateAffiliate extends CreateRecord
{
protected static string $resource = AffiliateResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
use Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource;
class EditAffiliate extends EditRecord
{
protected static string $resource = AffiliateResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource\Pages;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource;
class ListAffiliates extends ListRecords
{
protected static string $resource = AffiliateResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Admin\Resources\AffiliateResource\RelationManagers;
use App\Admin\Resources\OrderResource;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Paymenter\Extensions\Others\Affiliates\Models\AffiliateOrder;
class AffiliatesRelationManager extends RelationManager
{
protected static string $relationship = 'orders';
protected static ?string $model = AffiliateOrder::class;
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('affiliate_id')
->columns([
TextColumn::make('order.id'),
TextColumn::make('earnings')->formatStateUsing(function (AffiliateOrder $affiliateOrder) {
if (count($affiliateOrder->affiliate->earnings) <= 0) {
return null;
}
return implode(', ', array_map(function ($key, $value) {
return "$key: $value";
}, array_keys($affiliateOrder->affiliate->earnings), $affiliateOrder->affiliate->earnings));
})->label('Earning'),
TextColumn::make('order.created_at')
->label('Created At')
->since()
->sortable()
->dateTimeTooltip(),
])
->filters([
//
])
->actions([
Tables\Actions\ViewAction::make()->url(fn ($record) => OrderResource::getUrl('edit', ['record' => $record->order])),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates;
use App\Classes\Extension\Extension;
use App\Events\Invoice\Paid as InvoicePaid;
use App\Events\Order\Created as OrderCreated;
use App\Events\User\Created as UserCreated;
use App\Helpers\ExtensionHelper;
use App\Models\User;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\View;
use Livewire\Livewire;
use Paymenter\Extensions\Others\Affiliates\Listeners\AssociateOrderWithAffiliate;
use Paymenter\Extensions\Others\Affiliates\Listeners\IncreamentAffiliateSignups;
use Paymenter\Extensions\Others\Affiliates\Listeners\RewardAffiliate;
use Paymenter\Extensions\Others\Affiliates\Livewire\Affiliates\Affiliate as AffiliateComponent;
use Paymenter\Extensions\Others\Affiliates\Middleware\AffiliatesMiddleware;
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
class Affiliates extends Extension
{
public function __construct(public $config = []) {}
/**
* Get all the configuration for the extension
*
* @param array $values
* @return array
*/
public function getConfig($values = [])
{
return [
[
'name' => 'default_reward',
'label' => 'Default Affiliate Reward',
'type' => 'number',
'description' => 'Percentage of the purchase amount the affiliated user would receive as a reward.',
'required' => true,
'suffix' => '%',
'validation' => 'integer|min:0|max:100',
],
[
'name' => 'cookie_max_age',
'label' => 'Referral Cookie Max-Age',
'type' => 'number',
'description' => 'Amount of days for which the referral cookie be valid. (Set 0 for infinite)',
'required' => true,
'validation' => 'integer|min:0',
],
[
'name' => 'type',
'label' => 'Affiliate Code Type',
'type' => 'select',
'default' => 'random',
'description' => 'How the affiliate would be assigned.',
'required' => true,
'options' => [
'random' => 'Random',
'custom' => 'Custom',
],
],
];
}
public function enabled()
{
// Run migrations
Artisan::call('migrate', ['--path' => 'extensions/Others/Affiliates/database/migrations/2024_12_25_075634_create_ext_affiliates_table.php', '--force' => true]);
Artisan::call('migrate', ['--path' => 'extensions/Others/Affiliates/database/migrations/2025_01_31_155928_create_ext_affiliate_orders_table.php', '--force' => true]);
}
public function disabled() {}
public function boot()
{
require __DIR__ . '/routes/web.php';
View::addNamespace('affiliates', __DIR__ . '/resources/views');
Lang::addNamespace('affiliates', __DIR__ . '/resources/lang');
Livewire::component('affiliate', AffiliateComponent::class);
User::resolveRelationUsing('affiliate', function (User $userModel) {
return $userModel->hasOne(Affiliate::class, 'user_id');
});
ExtensionHelper::registerMiddleware(AffiliatesMiddleware::class);
// Listen for UserCreated and InvoicePaid events
Event::listen(
UserCreated::class,
IncreamentAffiliateSignups::class,
);
Event::listen(
InvoicePaid::class,
RewardAffiliate::class,
);
Event::listen(
OrderCreated::class,
AssociateOrderWithAffiliate::class,
);
Event::listen('api.permissions', function () {
return [
'admin.affiliates.view' => 'View Affiliates',
'admin.affiliates.create' => 'Create Affiliates',
'admin.affiliates.update' => 'Update Affiliates',
'admin.affiliates.delete' => 'Delete Affiliates',
];
});
// Event::listen('navigation.dashboard', function ($routes) {
// dd($routes);
// return [
// 'name' => __('affiliates::affiliate.affiliate'),
// 'route' => 'affiliate.index',
// 'icon' => 'heroicon-o-banknotes',
// 'group' => 'Administration',
// ];
// });
// Hook onto account navigation
Event::listen('navigation.account', function () {
return [
'name' => __('affiliates::affiliate.affiliate'),
'route' => 'affiliate.index',
];
});
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Http\Controllers;
use App\Http\Controllers\Api\ApiController;
use Dedoc\Scramble\Attributes\Group;
use Dedoc\Scramble\Attributes\QueryParameter;
use Paymenter\Extensions\Others\Affiliates\Http\Requests\CreateAffiliateRequest;
use Paymenter\Extensions\Others\Affiliates\Http\Requests\DeleteAffiliateRequest;
use Paymenter\Extensions\Others\Affiliates\Http\Requests\GetAffiliateRequest;
use Paymenter\Extensions\Others\Affiliates\Http\Requests\GetAffiliatesRequest;
use Paymenter\Extensions\Others\Affiliates\Http\Requests\UpdateAffiliateRequest;
use Paymenter\Extensions\Others\Affiliates\Http\Resources\AffiliateResource;
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
use Spatie\QueryBuilder\QueryBuilder;
#[Group(name: 'Affiliates', weight: 1)]
class AffiliateController extends ApiController
{
protected const INCLUDES = [
'user',
'orders',
];
/**
* List Affiliates
*/
#[QueryParameter('per_page', 'How many items to show per page.', type: 'int', default: 15, example: 20)]
#[QueryParameter('page', 'Which page to show.', type: 'int', example: 2)]
public function index(GetAffiliatesRequest $request)
{
// Fetch affiliates with pagination
$affiliates = QueryBuilder::for(Affiliate::class)
->allowedFilters(['affiliate_id', 'code', 'visitors', 'reward', 'discount'])
->allowedIncludes($this->allowedIncludes(self::INCLUDES))
->allowedSorts(['id', 'code', 'visitors', 'reward', 'discount', 'created_at'])
->simplePaginate(request('per_page', 15));
// Return the affiliates as a JSON response
return AffiliateResource::collection($affiliates);
}
/**
* Create a new affiliate
*/
public function store(CreateAffiliateRequest $request)
{
// Validate and create the affiliate
$affiliate = Affiliate::create($request->validated());
// Return the created affiliate as a JSON response
return new AffiliateResource($affiliate);
}
/**
* Show a specific affiliate
*/
public function show(GetAffiliateRequest $request, Affiliate $affiliate)
{
$affiliate = QueryBuilder::for(Affiliate::class)
->allowedIncludes($this->allowedIncludes(self::INCLUDES))
->findOrFail($affiliate->id);
// Return the affiliate as a JSON response
return new AffiliateResource($affiliate);
}
/**
* Update a specific affiliate
*/
public function update(UpdateAffiliateRequest $request, Affiliate $affiliate)
{
// Validate and update the affiliate
$affiliate->update($request->validated());
// Return the updated affiliate as a JSON response
return new AffiliateResource($affiliate);
}
/**
* Delete a specific affiliate
*/
public function destroy(DeleteAffiliateRequest $request, Affiliate $affiliate)
{
// Delete the affiliate
$affiliate->delete();
return $this->returnNoContent();
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Http\Requests;
use App\Http\Requests\Api\Admin\AdminApiRequest;
class CreateAffiliateRequest extends AdminApiRequest
{
protected $permission = 'affiliates.create';
public function rules(): array
{
return [
'user_id' => 'required|exists:users,id',
'code' => 'required|string|max:255|unique:affiliates,code',
'enabled' => 'nullable|boolean',
'reward' => 'nullable|numeric|min:0|max:100',
];
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Http\Requests;
use App\Http\Requests\Api\Admin\AdminApiRequest;
class DeleteAffiliateRequest extends AdminApiRequest
{
protected $permission = 'affiliates.delete';
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Http\Requests;
use App\Http\Requests\Api\Admin\AdminApiRequest;
class GetAffiliateRequest extends AdminApiRequest
{
protected $permission = 'affiliates.view';
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Http\Requests;
use App\Http\Requests\Api\Admin\AdminApiRequest;
class GetAffiliatesRequest extends AdminApiRequest
{
protected $permission = 'affiliates.view';
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Http\Requests;
use App\Http\Requests\Api\Admin\AdminApiRequest;
class UpdateAffiliateRequest extends AdminApiRequest
{
protected $permission = 'affiliates.update';
public function rules(): array
{
return [
'user_id' => 'nullable|exists:users,id',
'code' => 'nullable|string|max:255|unique:affiliates,code,' . $this->route()->parameter('affiliate')->id,
'enabled' => 'nullable|boolean',
'reward' => 'nullable|numeric|min:0|max:100',
];
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Http\Resources;
use TiMacDonald\JsonApi\JsonApiResource;
/** @mixin \App\Models\AffiliateOrder */
class AffiliateOrderResource extends JsonApiResource
{
public $attributes = [
'id',
'order_id',
];
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Http\Resources;
use App\Http\Resources\UserResource;
use TiMacDonald\JsonApi\JsonApiResource;
/** @mixin \App\Models\Affiliate */
class AffiliateResource extends JsonApiResource
{
public $attributes = [
'id',
'code',
'enabled',
'visitors',
'reward',
'discount',
'earnings',
'updated_at',
'created_at',
];
public $relationships = [
'user' => UserResource::class,
'orders' => AffiliateOrderResource::class,
];
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Listeners;
use App\Events\Order\Created;
use Illuminate\Support\Facades\Cookie;
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
use Paymenter\Extensions\Others\Affiliates\Models\AffiliateOrder;
class AssociateOrderWithAffiliate
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(Created $event): void
{
$referral_code = Cookie::get('referred_by');
/** @var Affiliate */
$affiliate = Affiliate::where('code', $referral_code)->first();
if (!$affiliate || $affiliate->user->id === $event->order->user_id) {
return;
}
AffiliateOrder::create([
'order_id' => $event->order->id,
'affiliate_id' => $affiliate->id,
]);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Listeners;
use App\Events\User\Created;
use Illuminate\Support\Facades\Cookie;
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
class IncreamentAffiliateSignups
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(Created $event): void
{
$referral_code = Cookie::get('referred_by');
/** @var Affiliate */
$affiliate = Affiliate::where('code', $referral_code)->first();
if (!$affiliate) {
return;
}
$affiliate->increment('signups');
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Listeners;
use App\Helpers\ExtensionHelper;
use App\Models\Invoice;
use App\Models\Service;
use Illuminate\Support\Collection;
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
use Paymenter\Extensions\Others\Affiliates\Models\AffiliateOrder;
class RewardAffiliate
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(object $event): void
{
/**
* @var Invoice $invoice
*/
$invoice = $event->invoice;
if ($invoice->items()->first()->reference_type !== Service::class) {
return;
}
$order = $invoice->items()->first()->reference->order;
$referral = AffiliateOrder::where('order_id', $order->id)->first();
if (!$referral) {
return;
}
/**
* @var Affiliate $affiliate
*/
$affiliate = $referral->affiliate;
$extension = ExtensionHelper::getExtension('other', 'Affiliates');
$reward_percentage = $affiliate->reward ?: $extension->config('default_reward');
$reward_amount = $invoice->total * $reward_percentage / 100;
/**
* @var Collection
*/
$user_credits = $affiliate->user->credits;
$affiliate_credits = $user_credits->filter(function ($credit) use ($invoice) {
return $credit->currency_code === $invoice->currency_code;
})->first();
if ($affiliate_credits) {
// Add reward to credits
$affiliate->user->credits()->where('currency_code', $invoice->currency_code)->update([
'amount' => $affiliate_credits->amount + $reward_amount,
]);
} else {
// Create new credits with the invoice's currency code
$affiliate->user->credits()->create([
'amount' => $reward_amount,
'currency_code' => $invoice->currency_code,
]);
}
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Livewire\Affiliates;
use App\Helpers\ExtensionHelper;
use App\Livewire\Component;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate as AffiliateModel;
class Affiliate extends Component
{
public string $signup_type = 'custom';
public ?AffiliateModel $affiliate;
public string $referral_code = '';
public function mount()
{
$this->affiliate = Auth::user()->affiliate;
$this->signup_type = ExtensionHelper::getExtension('other', 'Affiliates')->config('type');
}
public function render()
{
return view('affiliates::index')->layoutData([
'sidebar' => true,
]);
}
public function signup()
{
$this->validate();
/**
* @var App\Models\User
*/
$user = Auth::user();
if ($user->affiliate) {
$this->notify(__('affiliates::affiliate.you-are-already-affiliated'));
return;
}
$this->affiliate = $user->affiliate()->create([
'code' => $this->signup_type === 'custom' ? $this->referral_code : Str::random(10),
'visitors' => 0,
'reward' => null,
'discount' => null,
])->refresh();
$this->notify(__('affiliates::affiliate.signup-success'));
}
public function rules()
{
return [
'referral_code' => [Rule::requiredIf($this->signup_type === 'custom'), 'alpha_num:ascii', 'unique:ext_affiliates,code', 'min:5', 'max:25'],
];
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Middleware;
use App\Helpers\ExtensionHelper;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cookie;
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
use Symfony\Component\HttpFoundation\Response;
class AffiliatesMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
// Continue if request doesn't contain `ref` query parameter
if (!request()->has('ref')) {
return $next($request);
}
$affiliate = Affiliate::where('code', request('ref'))->first();
if (!$affiliate || $affiliate->user->id === auth()->id()) {
return $next($request);
}
// Increase the visitor count if a cookie is not present already, or is not the same one
if (!Cookie::has('referred_by') || Cookie::get('referred_by') !== $affiliate->code) {
$affiliate->increment('visitors');
}
$extension = ExtensionHelper::getExtension('other', 'Affiliates');
$cookie_max_age = (int) $extension->config('cookie_max_age');
if ($cookie_max_age > 0) {
// Set affiliate cookie (and set it to expire after `$cookie_max_age` days)
Cookie::queue('referred_by', $affiliate->code, minutes: 60 * 24 * $cookie_max_age);
} else {
// Never expire
Cookie::queue(Cookie::forever('referred_by', $affiliate->code));
}
return $next($request);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Models;
use App\Models\User;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Affiliate extends Model
{
protected $table = 'ext_affiliates';
protected $fillable = [
'user_id',
'code',
'visitors',
'reward',
'discount',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function orders(): HasMany
{
return $this->hasMany(AffiliateOrder::class);
}
/**
* Get the earnings made by this affiliate
*
* @return string
*/
public function earnings(): Attribute
{
return Attribute::make(
get: function (): array {
$earnings = [];
$this->orders->each(function ($order) use (&$earnings) {
foreach ($order->earnings as $currency => $total) {
if (!isset($earnings[$currency])) {
$earnings[$currency] = 0;
}
$earnings[$currency] += $total;
}
});
return $earnings;
},
);
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Paymenter\Extensions\Others\Affiliates\Models;
use App\Helpers\ExtensionHelper;
use App\Models\Order;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AffiliateOrder extends Model
{
protected $table = 'ext_affiliate_orders';
public $timestamps = false;
protected $fillable = [
'order_id',
'affiliate_id',
];
public function affiliate(): BelongsTo
{
return $this->belongsTo(Affiliate::class);
}
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
/**
* Get the earnings made from this order
*
* @return string
*/
public function earnings(): Attribute
{
return Attribute::make(
get: function (): array {
$earnings = [];
/** @var Collection */
$invoices = $this->order->invoices;
$extension = ExtensionHelper::getExtension('other', 'Affiliates');
$reward_percentage = $this->affiliate->reward ?: $extension->config('default_reward');
$invoices->each(function ($invoice) use (&$earnings, $reward_percentage) {
if ($invoice->status !== 'paid') {
return;
}
if (!isset($earnings[$invoice->currency_code])) {
$earnings[$invoice->currency_code] = 0;
}
$earnings[$invoice->currency_code] += $invoice->total * $reward_percentage / 100;
});
foreach ($earnings as $currency => $total) {
// Round to 2 decimal places
$earnings[$currency] = round($total, 2);
}
return $earnings;
},
);
}
}

View File

@@ -0,0 +1,35 @@
<?php
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('ext_affiliates', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(User::class)->constrained()->cascadeOnDelete();
$table->string('code')->unique();
$table->boolean('enabled')->default(true);
$table->integer('reward')->unsigned()->nullable();
$table->integer('discount')->unsigned()->nullable();
$table->integer('visitors')->unsigned()->default(0);
$table->integer('signups')->unsigned()->default(0);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('affiliates');
}
};

View File

@@ -0,0 +1,30 @@
<?php
use App\Models\Order;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Paymenter\Extensions\Others\Affiliates\Models\Affiliate;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('ext_affiliate_orders', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(Affiliate::class)->constrained()->cascadeOnDelete();
$table->foreignIdFor(Order::class)->unique()->constrained()->cascadeOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('affiliate_orders');
}
};

View File

@@ -0,0 +1,19 @@
<?php
return [
'affiliate' => 'Affiliate',
'signup-for-affiliate' => 'Signup for Affiliate',
'code' => 'Code',
'visitors' => 'Visitors',
'total-visitors' => 'Total Visitors',
'signups' => 'Signups',
'total-signups' => 'Total Signups',
'earnings' => 'Earnings',
'total-earnings' => 'Total Earnings',
'your-affiliate-link' => 'Your affiliate link',
'copy' => 'Copy',
'you-havent-signed-up-yet' => 'You have not signed up for affiliates program yet!',
'you-are-already-affiliated' => 'You have already signed up for affiliate program.',
'signup-success' => 'Successfully signed up for affiliate program!',
];

View File

@@ -0,0 +1,73 @@
<div>
<x-navigation.breadcrumb />
<div class="px-2">
@isset($affiliate)
<div class="mt-4 md:grid-cols-3 grid">
<div class="flex flex-col gap-2">
<span class="text-xl font-semibold">{{ __('affiliates::affiliate.visitors') }}</span>
<span class="text-gray-500">{{ __('affiliates::affiliate.total-visitors') }}</span>
<span class="text-2xl font-semibold">{{ Number::format($affiliate->visitors) }}</span>
</div>
<div class="flex flex-col gap-2">
<span class="text-xl font-semibold">{{ __('affiliates::affiliate.signups') }}</span>
<span class="text-gray-500">{{ __('affiliates::affiliate.total-signups') }}</span>
<span class="text-2xl font-semibold">
{{ Number::format($affiliate->signups) }}
</span>
</div>
<div class="flex flex-col gap-2">
<span class="text-xl font-semibold">{{ __('affiliates::affiliate.earnings') }}</span>
<span class="text-gray-500">{{ __('affiliates::affiliate.total-earnings') }}</span>
<span class="text-2xl font-semibold">
<ul>
@foreach ($affiliate->earnings as $currency => $amount)
<li>
{{ $currency }}: {{ $amount }}
</li>
@endforeach
</ul>
</span>
</div>
<div class="col-span-3 flex flex-col mt-4">
<span class="text-xl font-semibold">{{ __('affiliates::affiliate.affiliate') }}</span>
<span class="text-gray-500">{{ __('affiliates::affiliate.your-affiliate-link') }}</span>
<div class="flex flex-row gap-2 mt-2">
<x-form.input value="{{ url('/') }}?ref={{ $affiliate->code }}" name="ref"
divClass="!mt-0" type="text" readonly />
<x-button.primary class="!w-fit" type="button"
onclick="copyToClipboard('{{ url('/?ref=' . $affiliate->code) }}')">{{ __('affiliates::affiliate.copy') }}</x-button.primary>
</div>
</div>
</div>
<script>
function copyToClipboard(textToCopy) {
const temp = document.createElement("input")
temp.type = "text"
temp.value = textToCopy
document.body.appendChild(temp)
temp.select()
document.execCommand("Copy")
document.body.removeChild(temp)
}
</script>
@else
<p class="mb-4">{{ __('affiliates::affiliate.you-havent-signed-up-yet') }}</p>
<h3 class="text-lg font-bold mb-4">{{ __('affiliates::affiliate.signup-for-affiliate') }}</h3>
<form wire:submit.prevent="signup" method="POST">
@if ($signup_type === 'custom')
<x-form.input name="referral_code" type="text" :label="__('affiliates::affiliate.code')" wire:model="referral_code"
required />
@endif
<x-button.primary type="submit" class="text-sm !w-fit mt-4">
{{ __('auth.sign_up') }}
</x-button.primary>
</form>
@endisset
</div>
</div>

View File

@@ -0,0 +1,15 @@
<?php
use Illuminate\Support\Facades\Route;
use Paymenter\Extensions\Others\Affiliates\Http\Controllers\AffiliateController;
use Paymenter\Extensions\Others\Affiliates\Livewire\Affiliates\Affiliate;
Route::group(['middleware' => ['web', 'auth']], function () {
Route::get('/account/affiliate', Affiliate::class)->name('affiliate.index');
});
Route::group(['middleware' => ['api', 'api.admin'], 'prefix' => '/api/v1/admin'], function () {
Route::apiResources([
'affiliates' => AffiliateController::class,
]);
});