mirror of
https://gitlab.com/CIEFWorldwideSdnBhd/exchange-2.0.git
synced 2026-08-19 04:23:55 +00:00
Merge branch 'affiliate-program' of https://gitlab.com/CIEFWorldwideSdnBhd/exchange-2.0 into vapor/staging
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\General\Eloquent\Filters;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class AffiliateSearch implements Filter
|
||||
{
|
||||
|
||||
/**
|
||||
* @param Builder $builder
|
||||
* @param $value
|
||||
* @return Builder|mixed
|
||||
*/
|
||||
public static function apply(Builder $builder, $value)
|
||||
{
|
||||
// Sanitize input to prevent SQL injection
|
||||
// Remove any characters that could be used for SQL injection
|
||||
$sanitized = preg_replace('/[^a-zA-Z0-9\s\-_]/', '', $value);
|
||||
|
||||
// Trim whitespace
|
||||
$sanitized = trim($sanitized);
|
||||
|
||||
// If sanitized value is empty, return builder without filtering
|
||||
if (empty($sanitized)) {
|
||||
return $builder;
|
||||
}
|
||||
|
||||
return $builder->where(function ($query) use ($sanitized) {
|
||||
$query->where('campaign_name', 'LIKE', '%' . $sanitized . '%')
|
||||
->orWhere('code', 'LIKE', '%' . $sanitized . '%')
|
||||
->orWhere('campaign_description', 'LIKE', '%' . $sanitized . '%');
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Classes\Modules\Vouchers\Processors\CreateVoucherProcessor;
|
||||
use App\Classes\Modules\Companies\DataTransferObjects\EmploymentObject;
|
||||
use App\Classes\Modules\PerfexCRM\DataTransferObjects\CreateLeadPerfexCRMObject;
|
||||
use App\Classes\Modules\Rewards\Services\CreatesUserReward;
|
||||
use App\Classes\Modules\Affiliate\Services\TracksAffiliateRegistration;
|
||||
use App\Classes\ValueObjects\Constants\ApprovalStatus;
|
||||
use App\Classes\ValueObjects\Constants\BusinessType;
|
||||
use App\Classes\ValueObjects\Constants\CompanyType;
|
||||
@@ -82,6 +83,9 @@ class CreateCustomerLogic extends AbstractControllerLogic
|
||||
/** @var CreatesUserReward */
|
||||
private $createsUserReward;
|
||||
|
||||
/** @var TracksAffiliateRegistration */
|
||||
private $tracksAffiliateRegistration;
|
||||
|
||||
/**
|
||||
* CreateCustomerLogic constructor.
|
||||
* @param CreateUserProcessor $createUserProcessor
|
||||
@@ -96,9 +100,10 @@ class CreateCustomerLogic extends AbstractControllerLogic
|
||||
* @param NewCustomerToVoucherifyProcessor $newCustomerToVoucherifyProcessor
|
||||
* @param CreateVoucherProcessor $createVoucherProcessor
|
||||
* @param CreatesUserReward $createsUserReward
|
||||
* @param TracksAffiliateRegistration $tracksAffiliateRegistration
|
||||
*/
|
||||
public function __construct(CreateUserProcessor $createUserProcessor, CreateCompanyProcessor $createCompanyProcessor, CreateContactProcessor $createContactProcessor, AssignEmployeeProcessor $assignEmployeeProcessor, AssignSegmentProcessor $assignSegmentProcessor, AuthenticationProcessor $authenticationProcessor, GenerateEmailVerificationAttemptProcessor $generateEmailVerificationAttemptProcessor,
|
||||
CreatesSeasonalSegment $createsSeasonalSegment, CheckMilestonesForRewardProcessor $checkMilestonesForRewardProcessor, NewCustomerToVoucherifyProcessor $newCustomerToVoucherifyProcessor, CreateVoucherProcessor $createVoucherProcessor, CreatesUserReward $createsUserReward)
|
||||
CreatesSeasonalSegment $createsSeasonalSegment, CheckMilestonesForRewardProcessor $checkMilestonesForRewardProcessor, NewCustomerToVoucherifyProcessor $newCustomerToVoucherifyProcessor, CreateVoucherProcessor $createVoucherProcessor, CreatesUserReward $createsUserReward, TracksAffiliateRegistration $tracksAffiliateRegistration)
|
||||
{
|
||||
$this->createUserProcessor = $createUserProcessor;
|
||||
$this->createCompanyProcessor = $createCompanyProcessor;
|
||||
@@ -112,6 +117,7 @@ class CreateCustomerLogic extends AbstractControllerLogic
|
||||
$this->newCustomerToVoucherifyProcessor = $newCustomerToVoucherifyProcessor;
|
||||
$this->createVoucherProcessor = $createVoucherProcessor;
|
||||
$this->createsUserReward = $createsUserReward;
|
||||
$this->tracksAffiliateRegistration = $tracksAffiliateRegistration;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,6 +183,9 @@ class CreateCustomerLogic extends AbstractControllerLogic
|
||||
}
|
||||
}
|
||||
|
||||
// Track affiliate registration if user came from affiliate link
|
||||
$this->tracksAffiliateRegistration->execute($user, $request->get('tracking'));
|
||||
|
||||
return $this->response($this->authenticationProcessor->execute($request, false));
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\ControllersLogic;
|
||||
|
||||
use App\Classes\Modules\Affiliate\Services\CreatesAffiliate;
|
||||
use App\Classes\Modules\Affiliate\DataTransferObjects\AffiliateObject;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CreateAffiliateLogic
|
||||
{
|
||||
/** @var CreatesAffiliate */
|
||||
private $createsAffiliate;
|
||||
|
||||
/**
|
||||
* @param CreatesAffiliate $createsAffiliate
|
||||
*/
|
||||
public function __construct(CreatesAffiliate $createsAffiliate)
|
||||
{
|
||||
$this->createsAffiliate = $createsAffiliate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function execute(Request $request): JsonResponse
|
||||
{
|
||||
$code = $request->input('code');
|
||||
$code = $code !== null ? (string) $code : '';
|
||||
|
||||
$isActive = $request->input('is_active', true);
|
||||
// Convert string 'true'/'false' to boolean if needed
|
||||
if (is_string($isActive)) {
|
||||
$isActive = filter_var($isActive, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
$isActive = (bool) $isActive;
|
||||
|
||||
$object = new AffiliateObject(
|
||||
$code,
|
||||
$request->input('campaign_name'),
|
||||
$request->input('campaign_description'),
|
||||
$isActive
|
||||
);
|
||||
|
||||
$affiliate = $this->createsAffiliate->execute($object);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Affiliate code created successfully',
|
||||
'data' => $affiliate
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\ControllersLogic;
|
||||
|
||||
use App\Classes\Modules\Affiliate\Services\DeletesAffiliate;
|
||||
use App\Models\Affiliate;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class DeleteAffiliateLogic
|
||||
{
|
||||
/** @var DeletesAffiliate */
|
||||
private $deletesAffiliate;
|
||||
|
||||
/**
|
||||
* @param DeletesAffiliate $deletesAffiliate
|
||||
*/
|
||||
public function __construct(DeletesAffiliate $deletesAffiliate)
|
||||
{
|
||||
$this->deletesAffiliate = $deletesAffiliate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $id
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function execute(int $id): JsonResponse
|
||||
{
|
||||
$affiliate = Affiliate::findOrFail($id);
|
||||
$this->deletesAffiliate->execute($affiliate);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Affiliate code deleted successfully'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\ControllersLogic;
|
||||
|
||||
use App\Classes\Modules\Affiliate\Services\GetsAffiliateSettings;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class GetAffiliateSettingsLogic
|
||||
{
|
||||
/** @var GetsAffiliateSettings */
|
||||
private $getsAffiliateSettings;
|
||||
|
||||
/**
|
||||
* @param GetsAffiliateSettings $getsAffiliateSettings
|
||||
*/
|
||||
public function __construct(GetsAffiliateSettings $getsAffiliateSettings)
|
||||
{
|
||||
$this->getsAffiliateSettings = $getsAffiliateSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function execute(Request $request): JsonResponse
|
||||
{
|
||||
$settings = $this->getsAffiliateSettings->execute();
|
||||
|
||||
return response()->json($settings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\ControllersLogic;
|
||||
|
||||
use App\Classes\General\Abstracts\AbstractControllerLogic;
|
||||
use App\Classes\Modules\Affiliate\Services\ListsAffiliates;
|
||||
use App\Http\Resources\AffiliateResource;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ListAffiliatesLogic extends AbstractControllerLogic
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function notification(): array {
|
||||
return [
|
||||
'title' => 'Retrieved Affiliates',
|
||||
'message' => 'You have successfully retrieved a list of Affiliates'
|
||||
];
|
||||
}
|
||||
|
||||
/** @var ListsAffiliates */
|
||||
private $listsAffiliates;
|
||||
|
||||
/**
|
||||
* @param ListsAffiliates $listsAffiliates
|
||||
*/
|
||||
public function __construct(ListsAffiliates $listsAffiliates)
|
||||
{
|
||||
$this->listsAffiliates = $listsAffiliates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function logic(Request $request): JsonResponse
|
||||
{
|
||||
$query = $this->listsAffiliates->execute($this->listsAffiliates->deserializeFilters($request->input('filters')));
|
||||
|
||||
return $this->collectionResponse(AffiliateResource::collection($query));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\ControllersLogic;
|
||||
|
||||
use App\Classes\Modules\Affiliate\Services\UpdatesAffiliate;
|
||||
use App\Classes\Modules\Affiliate\DataTransferObjects\AffiliateObject;
|
||||
use App\Models\Affiliate;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UpdateAffiliateLogic
|
||||
{
|
||||
/** @var UpdatesAffiliate */
|
||||
private $updatesAffiliate;
|
||||
|
||||
/**
|
||||
* @param UpdatesAffiliate $updatesAffiliate
|
||||
*/
|
||||
public function __construct(UpdatesAffiliate $updatesAffiliate)
|
||||
{
|
||||
$this->updatesAffiliate = $updatesAffiliate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function execute(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$affiliate = Affiliate::findOrFail($id);
|
||||
|
||||
$isActive = $request->input('is_active', $affiliate->is_active);
|
||||
// Convert string 'true'/'false' to boolean if needed
|
||||
if (is_string($isActive)) {
|
||||
$isActive = filter_var($isActive, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
$isActive = (bool) $isActive;
|
||||
|
||||
$object = new AffiliateObject(
|
||||
$affiliate->code, // Keep existing code
|
||||
$request->input('campaign_name'),
|
||||
$request->input('campaign_description'),
|
||||
$isActive
|
||||
);
|
||||
|
||||
$affiliate = $this->updatesAffiliate->execute($affiliate, $object);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Affiliate code updated successfully',
|
||||
'data' => $affiliate
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\ControllersLogic;
|
||||
|
||||
use App\Classes\Modules\Affiliate\Services\UpdatesAffiliateSettings;
|
||||
use App\Classes\Modules\Affiliate\DataTransferObjects\AffiliateSettingsObject;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UpdateAffiliateSettingsLogic
|
||||
{
|
||||
/** @var UpdatesAffiliateSettings */
|
||||
private $updatesAffiliateSettings;
|
||||
|
||||
/**
|
||||
* @param UpdatesAffiliateSettings $updatesAffiliateSettings
|
||||
*/
|
||||
public function __construct(UpdatesAffiliateSettings $updatesAffiliateSettings)
|
||||
{
|
||||
$this->updatesAffiliateSettings = $updatesAffiliateSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function execute(Request $request): JsonResponse
|
||||
{
|
||||
$object = new AffiliateSettingsObject(
|
||||
$request->input('code_binding_days', 30)
|
||||
);
|
||||
|
||||
$settings = $this->updatesAffiliateSettings->execute($object);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Affiliate settings updated successfully',
|
||||
'data' => $settings
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\DataTransferObjects;
|
||||
|
||||
use App\Classes\General\Interfaces\DataTransferObject;
|
||||
|
||||
class AffiliateObject implements DataTransferObject
|
||||
{
|
||||
/** @var string | null */
|
||||
private ?string $code;
|
||||
|
||||
/** @var string */
|
||||
private $campaignName;
|
||||
|
||||
/** @var string|null */
|
||||
private $campaignDescription;
|
||||
|
||||
/** @var bool */
|
||||
private $isActive;
|
||||
|
||||
/**
|
||||
* AffiliateObject constructor.
|
||||
* @param string $code
|
||||
* @param string $campaignName
|
||||
* @param string|null $campaignDescription
|
||||
* @param bool $isActive
|
||||
*/
|
||||
public function __construct(string $code = '', string $campaignName, ?string $campaignDescription = null, bool $isActive = true)
|
||||
{
|
||||
$this->code = $code;
|
||||
$this->campaignName = $campaignName;
|
||||
$this->campaignDescription = $campaignDescription;
|
||||
$this->isActive = $isActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCode(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCampaignName(): string
|
||||
{
|
||||
return $this->campaignName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCampaignDescription(): ?string
|
||||
{
|
||||
return $this->campaignDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsActive(): bool
|
||||
{
|
||||
return $this->isActive;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\DataTransferObjects;
|
||||
|
||||
use App\Classes\General\Interfaces\DataTransferObject;
|
||||
|
||||
class AffiliateSettingsObject implements DataTransferObject
|
||||
{
|
||||
/** @var int */
|
||||
private $codeBindingDays;
|
||||
|
||||
/**
|
||||
* AffiliateSettingsObject constructor.
|
||||
* @param int $codeBindingDays
|
||||
*/
|
||||
public function __construct(int $codeBindingDays)
|
||||
{
|
||||
$this->codeBindingDays = $codeBindingDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getCodeBindingDays(): int
|
||||
{
|
||||
return $this->codeBindingDays;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\Services;
|
||||
|
||||
use App\Classes\General\Eloquent\AbstractUpdateRecord;
|
||||
use App\Classes\Modules\Affiliate\DataTransferObjects\AffiliateObject;
|
||||
use App\Models\Affiliate;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CreatesAffiliate extends AbstractUpdateRecord
|
||||
{
|
||||
/**
|
||||
* @param AffiliateObject $object
|
||||
* @return \Illuminate\Database\Eloquent\Model
|
||||
* @throws \App\Classes\Exceptions\MalformedRequestException
|
||||
*/
|
||||
public function execute(AffiliateObject $object)
|
||||
{
|
||||
$code = $object->getCode();
|
||||
|
||||
// Auto-generate code if not provided
|
||||
if (empty($code)) {
|
||||
$code = $this->generateUniqueCode();
|
||||
} else {
|
||||
// Validate uniqueness of user-provided code (including soft-deleted records)
|
||||
$existingAffiliate = Affiliate::where('code', $code)
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
|
||||
if ($existingAffiliate) {
|
||||
throw new \App\Classes\Exceptions\MalformedRequestException('Affiliate code already exists. Please choose a different code.');
|
||||
}
|
||||
}
|
||||
|
||||
$model = new Affiliate();
|
||||
$model->code = $code;
|
||||
$model->campaign_name = $object->getCampaignName();
|
||||
$model->campaign_description = $object->getCampaignDescription();
|
||||
$model->is_active = $object->getIsActive();
|
||||
$model->created_by = Auth::id();
|
||||
$model->clicks_count = 0;
|
||||
$model->registrations_count = 0;
|
||||
$model->orders_count = 0;
|
||||
|
||||
return $this->handler($model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique affiliate code
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function generateUniqueCode(): string
|
||||
{
|
||||
$maxAttempts = 10;
|
||||
$attempts = 0;
|
||||
|
||||
do {
|
||||
// Increased from 12 to 16 characters for better uniqueness
|
||||
$code = Str::random(16);
|
||||
$attempts++;
|
||||
|
||||
if ($attempts >= $maxAttempts) {
|
||||
// Fallback to UUID if we can't generate unique code
|
||||
$code = strtoupper(Str::substr(str_replace('-', '', Str::uuid()), 0, 16));
|
||||
break;
|
||||
}
|
||||
} while (Affiliate::where('code', $code)->exists());
|
||||
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\Services;
|
||||
|
||||
use App\Models\Affiliate;
|
||||
|
||||
class DeletesAffiliate
|
||||
{
|
||||
/**
|
||||
* @param Affiliate $affiliate
|
||||
* @return bool
|
||||
*/
|
||||
public function execute(Affiliate $affiliate): bool
|
||||
{
|
||||
return $affiliate->delete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\Services;
|
||||
|
||||
use App\Models\KeyValuePair;
|
||||
|
||||
class GetsAffiliateSettings
|
||||
{
|
||||
const SETTING_KEY = 'affiliate_code_binding_days';
|
||||
const DEFAULT_VALUE = 30;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function execute(): array
|
||||
{
|
||||
$keyValuePair = KeyValuePair::where('key', self::SETTING_KEY)
|
||||
->whereNull('owner_type')
|
||||
->whereNull('owner_id')
|
||||
->first();
|
||||
|
||||
$codeBindingDays = $keyValuePair ? (int) $keyValuePair->value : self::DEFAULT_VALUE;
|
||||
|
||||
return [
|
||||
'code_binding_days' => $codeBindingDays
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\Services;
|
||||
|
||||
use App\Classes\General\Eloquent\AbstractListRecord;
|
||||
use App\Models\Affiliate;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ListsAffiliates extends AbstractListRecord
|
||||
{
|
||||
/** @var Affiliate */
|
||||
private $repository;
|
||||
|
||||
/**
|
||||
* ListsAffiliates constructor.
|
||||
* @param Affiliate $repository
|
||||
*/
|
||||
public function __construct(Affiliate $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Builder
|
||||
*/
|
||||
function getRepository(): Builder
|
||||
{
|
||||
return $this->repository->newQuery()->with('creator')->orderBy('created_at', 'desc');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\Services;
|
||||
|
||||
use App\Models\Affiliate;
|
||||
use Illuminate\Support\Facades\Cookie;
|
||||
|
||||
class TracksAffiliateClick
|
||||
{
|
||||
const COOKIE_NAME = 'affiliate_tracking_code';
|
||||
const COOKIE_EXPIRY_DAYS = 30; // Default, can be overridden by settings
|
||||
|
||||
/**
|
||||
* Track a click on an affiliate link
|
||||
*
|
||||
* @param string $code
|
||||
* @return Affiliate|null
|
||||
*/
|
||||
public function execute(string $code): ?Affiliate
|
||||
{
|
||||
// Validate code format and length
|
||||
if (!$this->isValidCode($code)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$affiliate = Affiliate::where('code', $code)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if (!$affiliate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Increment clicks count
|
||||
$affiliate->increment('clicks_count');
|
||||
|
||||
// Get binding days from settings
|
||||
$bindingDays = $this->getBindingDays();
|
||||
|
||||
// Store in cookie for later use during registration
|
||||
// Set secure cookie flags for security
|
||||
Cookie::queue(
|
||||
cookie(
|
||||
self::COOKIE_NAME,
|
||||
$code,
|
||||
$bindingDays * 24 * 60, // Convert days to minutes
|
||||
'/', // path
|
||||
null, // domain (null = current domain)
|
||||
config('session.secure', false), // secure (HTTPS only)
|
||||
true, // httpOnly (prevent JavaScript access)
|
||||
false, // raw
|
||||
config('session.same_site', 'lax') // sameSite
|
||||
)
|
||||
);
|
||||
|
||||
return $affiliate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate affiliate code format
|
||||
*
|
||||
* @param string $code
|
||||
* @return bool
|
||||
*/
|
||||
private function isValidCode(string $code): bool
|
||||
{
|
||||
// Check length (max 255 chars based on database schema)
|
||||
if (strlen($code) > 255 || strlen($code) < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only allow alphanumeric characters, hyphens, and underscores
|
||||
return (bool) preg_match('/^[a-zA-Z0-9_-]+$/', $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get code binding days from settings
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function getBindingDays(): int
|
||||
{
|
||||
$settingsService = new GetsAffiliateSettings();
|
||||
$settings = $settingsService->execute();
|
||||
return $settings['code_binding_days'] ?? self::COOKIE_EXPIRY_DAYS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\Services;
|
||||
|
||||
use App\Models\Affiliate;
|
||||
use App\Models\Booking;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TracksAffiliateOrder
|
||||
{
|
||||
/**
|
||||
* Track when a user with an affiliate code creates an order
|
||||
*
|
||||
* @param Booking $booking
|
||||
* @return Affiliate|null
|
||||
*/
|
||||
public function execute(Booking $booking): ?Affiliate
|
||||
{
|
||||
try {
|
||||
return DB::transaction(function () use ($booking) {
|
||||
$user = $booking->company->employees()->first();
|
||||
|
||||
if (!$user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get user's affiliate
|
||||
$userAffiliate = \App\Models\UserAffiliate::where('user_id', $user->id)
|
||||
->whereNotNull('registered_at')
|
||||
->first();
|
||||
|
||||
if (!$userAffiliate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Lock the affiliate record to prevent race conditions
|
||||
$affiliate = Affiliate::where('id', $userAffiliate->affiliate_id)
|
||||
->where('is_active', true)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (!$affiliate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if this booking has already been counted
|
||||
// We'll use a KeyValuePair to track which bookings have been counted
|
||||
$kvpKey = 'affiliate_order_tracked_' . $affiliate->id;
|
||||
|
||||
$existingKvp = $booking->attributesKVP()
|
||||
->where('key', $kvpKey)
|
||||
->exists();
|
||||
|
||||
if ($existingKvp) {
|
||||
return $affiliate; // Already counted
|
||||
}
|
||||
|
||||
// Mark this booking as tracked
|
||||
$keyValuePairObject = new \App\Classes\Modules\Accounts\DataTransferObjects\KeyValuePairObject(
|
||||
$kvpKey,
|
||||
'1'
|
||||
);
|
||||
|
||||
$createsKeyValuePair = app()->make(\App\Classes\Modules\Accounts\Services\CreatesKeyValuePair::class);
|
||||
$createsKeyValuePair->execute($booking, $keyValuePairObject);
|
||||
|
||||
// Increment orders count atomically
|
||||
$affiliate->increment('orders_count');
|
||||
|
||||
return $affiliate;
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to track affiliate order', [
|
||||
'booking_id' => $booking->id,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\Services;
|
||||
|
||||
use App\Models\Affiliate;
|
||||
use App\Models\User;
|
||||
use App\Models\UserAffiliate;
|
||||
use Illuminate\Support\Facades\Cookie;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TracksAffiliateRegistration
|
||||
{
|
||||
/**
|
||||
* Track when a user registers with an affiliate code
|
||||
*
|
||||
* @param User $user
|
||||
* @return Affiliate|null
|
||||
*/
|
||||
public function execute(User $user, string $code): ?Affiliate
|
||||
{
|
||||
if (!$code || !$this->isValidCode($code)) {
|
||||
Log::info('Code is not valid', ['code' => $code]);
|
||||
return null;
|
||||
}
|
||||
|
||||
$affiliate = Affiliate::where('code', $code)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if (!$affiliate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if user is already linked to this affiliate
|
||||
$userAffiliate = UserAffiliate::where('user_id', $user->id)
|
||||
->where('affiliate_id', $affiliate->id)
|
||||
->first();
|
||||
|
||||
if (!$userAffiliate) {
|
||||
// Create new link
|
||||
$userAffiliate = new UserAffiliate();
|
||||
$userAffiliate->user_id = $user->id;
|
||||
$userAffiliate->affiliate_id = $affiliate->id;
|
||||
$userAffiliate->clicked_at = now(); // Approximate, we don't track exact click time
|
||||
$userAffiliate->registered_at = now();
|
||||
$userAffiliate->save();
|
||||
|
||||
// Increment registrations count
|
||||
$affiliate->increment('registrations_count');
|
||||
} else {
|
||||
// Update registration time if not set
|
||||
if (!$userAffiliate->registered_at) {
|
||||
$userAffiliate->registered_at = now();
|
||||
$userAffiliate->save();
|
||||
$affiliate->increment('registrations_count');
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the cookie after registration
|
||||
Cookie::queue(Cookie::forget(TracksAffiliateClick::COOKIE_NAME));
|
||||
|
||||
return $affiliate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate affiliate code format
|
||||
*
|
||||
* @param string $code
|
||||
* @return bool
|
||||
*/
|
||||
private function isValidCode(string $code): bool
|
||||
{
|
||||
// Check length (max 255 chars based on database schema)
|
||||
if (strlen($code) > 255 || strlen($code) < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only allow alphanumeric characters, hyphens, and underscores
|
||||
return (bool) preg_match('/^[a-zA-Z0-9_-]+$/', $code);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\Services;
|
||||
|
||||
use App\Classes\General\Eloquent\AbstractUpdateRecord;
|
||||
use App\Classes\Modules\Affiliate\DataTransferObjects\AffiliateObject;
|
||||
use App\Models\Affiliate;
|
||||
|
||||
class UpdatesAffiliate extends AbstractUpdateRecord
|
||||
{
|
||||
/**
|
||||
* @param Affiliate $model
|
||||
* @param AffiliateObject $object
|
||||
* @return \Illuminate\Database\Eloquent\Model
|
||||
* @throws \App\Classes\Exceptions\MalformedRequestException
|
||||
*/
|
||||
public function execute(Affiliate $model, AffiliateObject $object)
|
||||
{
|
||||
// Don't update code if it already exists
|
||||
$model->campaign_name = $object->getCampaignName();
|
||||
$model->campaign_description = $object->getCampaignDescription();
|
||||
$model->is_active = $object->getIsActive();
|
||||
|
||||
return $this->handler($model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes\Modules\Affiliate\Services;
|
||||
|
||||
use App\Classes\Modules\Affiliate\DataTransferObjects\AffiliateSettingsObject;
|
||||
use App\Models\KeyValuePair;
|
||||
|
||||
class UpdatesAffiliateSettings
|
||||
{
|
||||
const SETTING_KEY = 'affiliate_code_binding_days';
|
||||
|
||||
/**
|
||||
* @param AffiliateSettingsObject $object
|
||||
* @return array
|
||||
*/
|
||||
public function execute(AffiliateSettingsObject $object): array
|
||||
{
|
||||
$keyValuePair = KeyValuePair::where('key', self::SETTING_KEY)
|
||||
->whereNull('owner_type')
|
||||
->whereNull('owner_id')
|
||||
->first();
|
||||
|
||||
if ($keyValuePair) {
|
||||
$keyValuePair->value = (string) $object->getCodeBindingDays();
|
||||
$keyValuePair->save();
|
||||
} else {
|
||||
$keyValuePair = new KeyValuePair();
|
||||
$keyValuePair->key = self::SETTING_KEY;
|
||||
$keyValuePair->value = (string) $object->getCodeBindingDays();
|
||||
$keyValuePair->owner_type = null;
|
||||
$keyValuePair->owner_id = null;
|
||||
$keyValuePair->save();
|
||||
}
|
||||
|
||||
return [
|
||||
'code_binding_days' => $object->getCodeBindingDays()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use App\Classes\Modules\Vouchers\DataTransferObjects\CreateVoucherifyOrderObject
|
||||
use App\Classes\Modules\Vouchers\Services\Voucherify\CreatesVoucherifyOrder;
|
||||
use App\Classes\Modules\Vouchers\Services\CreatesVoucherEntityMapping;
|
||||
use App\Classes\Modules\Vouchers\DataTransferObjects\VoucherEntityObject;
|
||||
use App\Classes\Modules\Affiliate\Services\TracksAffiliateOrder;
|
||||
use App\Classes\ValueObjects\Constants\BookingAttributeNames;
|
||||
use App\Classes\ValueObjects\Constants\VoucherifyEntityType;
|
||||
use App\Http\Resources\BookingResource;
|
||||
@@ -60,6 +61,9 @@ class CreateBookingLogic extends AbstractControllerLogic
|
||||
/** @var CreatesVoucherEntityMapping */
|
||||
private $createsVoucherEntityMapping;
|
||||
|
||||
/** @var TracksAffiliateOrder */
|
||||
private $tracksAffiliateOrder;
|
||||
|
||||
/**
|
||||
* CreateBookingLogic constructor.
|
||||
* @param CanCreateBooking $canCreateBooking
|
||||
@@ -70,8 +74,9 @@ class CreateBookingLogic extends AbstractControllerLogic
|
||||
* @param CheckMilestoneForRewardProcessor $checkMilestonesForRewardProcessor
|
||||
* @param CreatesVoucherifyOrder $createsVoucherifyOrder
|
||||
* @param CreatesVoucherEntityMapping $createsVoucherEntityMapping
|
||||
* @param TracksAffiliateOrder $tracksAffiliateOrder
|
||||
*/
|
||||
public function __construct(CanCreateBooking $canCreateBooking, CreatesBooking $createsBooking, GeneratesBookingMarking $generatesBookingMarking, FetchesCompany $fetchesCompany, BookingToPerfexCRMProcessor $bookingToPerfexCRMProcessor, CheckMilestonesForRewardProcessor $checkMilestonesForRewardProcessor, CreatesVoucherifyOrder $createsVoucherifyOrder, CreatesVoucherEntityMapping $createsVoucherEntityMapping)
|
||||
public function __construct(CanCreateBooking $canCreateBooking, CreatesBooking $createsBooking, GeneratesBookingMarking $generatesBookingMarking, FetchesCompany $fetchesCompany, BookingToPerfexCRMProcessor $bookingToPerfexCRMProcessor, CheckMilestonesForRewardProcessor $checkMilestonesForRewardProcessor, CreatesVoucherifyOrder $createsVoucherifyOrder, CreatesVoucherEntityMapping $createsVoucherEntityMapping, TracksAffiliateOrder $tracksAffiliateOrder)
|
||||
{
|
||||
$this->canCreateBooking = $canCreateBooking;
|
||||
$this->createsBooking = $createsBooking;
|
||||
@@ -81,6 +86,7 @@ class CreateBookingLogic extends AbstractControllerLogic
|
||||
$this->checkMilestonesForRewardProcessor = $checkMilestonesForRewardProcessor;
|
||||
$this->createsVoucherifyOrder = $createsVoucherifyOrder;
|
||||
$this->createsVoucherEntityMapping = $createsVoucherEntityMapping;
|
||||
$this->tracksAffiliateOrder = $tracksAffiliateOrder;
|
||||
}
|
||||
|
||||
|
||||
@@ -147,6 +153,9 @@ class CreateBookingLogic extends AbstractControllerLogic
|
||||
$this->recordVoucherifyOrderInfo($voucherify_order_id, $booking);
|
||||
$this->recordVoucherifyCustomerInfo($voucherify_customer_id, $user);
|
||||
|
||||
// Track affiliate order if user has an affiliate code
|
||||
$this->tracksAffiliateOrder->execute($booking);
|
||||
|
||||
//cief todo: case study 5 voucherify
|
||||
// $user = $company->employees()->first();
|
||||
// $this->checkMilestonesForRewardProcessor->execute($user, [Milestones::MILESTONE_5]);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Affiliate;
|
||||
|
||||
use App\Classes\Modules\Affiliate\ControllersLogic\CreateAffiliateLogic;
|
||||
use App\Http\Requests\CreateAffiliateRequest;
|
||||
use App\Models\Affiliate;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class CreateAffiliateController
|
||||
{
|
||||
/**
|
||||
* @param CreateAffiliateRequest $request
|
||||
* @param CreateAffiliateLogic $logic
|
||||
* @return JsonResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function create(CreateAffiliateRequest $request, CreateAffiliateLogic $logic): JsonResponse {
|
||||
$this->authorize('create', Affiliate::class);
|
||||
return $logic->execute($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize a given action for the current user.
|
||||
*
|
||||
* @param mixed $ability
|
||||
* @param mixed|array $arguments
|
||||
* @return \Illuminate\Auth\Access\Response
|
||||
*
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function authorize($ability, $arguments = [])
|
||||
{
|
||||
return app(\Illuminate\Contracts\Auth\Access\Gate::class)->authorize($ability, $arguments);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Affiliate;
|
||||
|
||||
use App\Classes\Modules\Affiliate\ControllersLogic\DeleteAffiliateLogic;
|
||||
use App\Models\Affiliate;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class DeleteAffiliateController
|
||||
{
|
||||
/**
|
||||
* @param int $id
|
||||
* @param DeleteAffiliateLogic $logic
|
||||
* @return JsonResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function delete(int $id, DeleteAffiliateLogic $logic): JsonResponse {
|
||||
$affiliate = Affiliate::findOrFail($id);
|
||||
$this->authorize('delete', $affiliate);
|
||||
return $logic->execute($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize a given action for the current user.
|
||||
*
|
||||
* @param mixed $ability
|
||||
* @param mixed|array $arguments
|
||||
* @return \Illuminate\Auth\Access\Response
|
||||
*
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function authorize($ability, $arguments = [])
|
||||
{
|
||||
return app(\Illuminate\Contracts\Auth\Access\Gate::class)->authorize($ability, $arguments);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Affiliate;
|
||||
|
||||
use App\Classes\Modules\Affiliate\ControllersLogic\GetAffiliateSettingsLogic;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class GetAffiliateSettingsController
|
||||
{
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param GetAffiliateSettingsLogic $logic
|
||||
* @return JsonResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function get(Request $request, GetAffiliateSettingsLogic $logic): JsonResponse {
|
||||
Gate::authorize('manageSettings', \App\Models\Affiliate::class);
|
||||
return $logic->execute($request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Affiliate;
|
||||
|
||||
use App\Classes\Modules\Affiliate\ControllersLogic\ListAffiliatesLogic;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class ListAffiliatesController
|
||||
{
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param ListAffiliatesLogic $logic
|
||||
* @return JsonResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function list(Request $request, ListAffiliatesLogic $logic): JsonResponse {
|
||||
Gate::authorize('viewAny', \App\Models\Affiliate::class);
|
||||
return $logic->execute($request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Affiliate;
|
||||
|
||||
use App\Classes\Modules\Affiliate\ControllersLogic\UpdateAffiliateLogic;
|
||||
use App\Http\Requests\UpdateAffiliateRequest;
|
||||
use App\Models\Affiliate;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class UpdateAffiliateController
|
||||
{
|
||||
/**
|
||||
* @param UpdateAffiliateRequest $request
|
||||
* @param int $id
|
||||
* @param UpdateAffiliateLogic $logic
|
||||
* @return JsonResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function update(UpdateAffiliateRequest $request, int $id, UpdateAffiliateLogic $logic): JsonResponse {
|
||||
$affiliate = Affiliate::findOrFail($id);
|
||||
$this->authorize('update', $affiliate);
|
||||
return $logic->execute($request, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authorize a given action for the current user.
|
||||
*
|
||||
* @param mixed $ability
|
||||
* @param mixed|array $arguments
|
||||
* @return \Illuminate\Auth\Access\Response
|
||||
*
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function authorize($ability, $arguments = [])
|
||||
{
|
||||
return app(\Illuminate\Contracts\Auth\Access\Gate::class)->authorize($ability, $arguments);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Affiliate;
|
||||
|
||||
use App\Classes\Modules\Affiliate\ControllersLogic\UpdateAffiliateSettingsLogic;
|
||||
use App\Http\Requests\UpdateAffiliateSettingsRequest;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
class UpdateAffiliateSettingsController
|
||||
{
|
||||
/**
|
||||
* @param UpdateAffiliateSettingsRequest $request
|
||||
* @param UpdateAffiliateSettingsLogic $logic
|
||||
* @return JsonResponse
|
||||
* @throws \Illuminate\Auth\Access\AuthorizationException
|
||||
*/
|
||||
public function update(UpdateAffiliateSettingsRequest $request, UpdateAffiliateSettingsLogic $logic): JsonResponse {
|
||||
// Check if user has permission to manage affiliate settings
|
||||
Gate::authorize('manageSettings', \App\Models\Affiliate::class);
|
||||
return $logic->execute($request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ namespace App\Http\Middleware;
|
||||
|
||||
use App\Providers\RouteServiceProvider;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Tymon\JWTAuth\Facades\JWTAuth;
|
||||
|
||||
class RedirectIfAuthenticated
|
||||
{
|
||||
@@ -18,10 +20,48 @@ class RedirectIfAuthenticated
|
||||
*/
|
||||
public function handle($request, Closure $next, $guard = null)
|
||||
{
|
||||
// Check session-based authentication
|
||||
if (Auth::guard($guard)->check()) {
|
||||
return redirect(RouteServiceProvider::HOME);
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
|
||||
// Check JWT authentication (since tokens are in localStorage, check cookies/headers)
|
||||
try {
|
||||
$token =
|
||||
$this->getBearerToken($request) ??
|
||||
$request->get('token') ??
|
||||
$request->cookie('access_token') ??
|
||||
$request->cookie('user-token');
|
||||
|
||||
if ($token) {
|
||||
try {
|
||||
JWTAuth::setToken($token);
|
||||
$user = JWTAuth::authenticate();
|
||||
if ($user) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Token invalid, continue
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// JWT check failed, continue
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Bearer token from Authorization header
|
||||
*/
|
||||
private function getBearerToken(Request $request): ?string
|
||||
{
|
||||
if (!$request->hasHeader('Authorization')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return preg_match('/Bearer\s+(.*)$/i', $request->header('Authorization'), $matches)
|
||||
? $matches[1]
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\{Log, Auth, Session};
|
||||
use Tymon\JWTAuth\Facades\JWTAuth;
|
||||
use App\Classes\Modules\Affiliate\Services\TracksAffiliateClick;
|
||||
|
||||
class TrackAffiliateClick
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$isAuthenticated = $this->checkWebAuth() || $this->checkJwtAuth($request);
|
||||
|
||||
if (!$isAuthenticated && $request->filled('tracking')) {
|
||||
$trackingCode = $this->validateTrackingCode($request->get('tracking'));
|
||||
if ($trackingCode) {
|
||||
$this->trackClick($trackingCode);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and sanitize tracking code
|
||||
*
|
||||
* @param mixed $code
|
||||
* @return string|null
|
||||
*/
|
||||
private function validateTrackingCode($code): ?string
|
||||
{
|
||||
if (!is_string($code)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Trim whitespace
|
||||
$code = trim($code);
|
||||
|
||||
// Check length (max 255 chars based on database schema)
|
||||
if (strlen($code) > 255 || strlen($code) < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only allow alphanumeric characters, hyphens, and underscores
|
||||
// This matches typical affiliate code formats
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $code)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated via WEB session
|
||||
*/
|
||||
private function checkWebAuth(): bool
|
||||
{
|
||||
try {
|
||||
if (Auth::guard('web')->check()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// fallback session check
|
||||
foreach (Session::all() as $key => $value) {
|
||||
if (strpos($key, 'login_web_') === 0 && !empty($value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('AffiliateClickMiddleware: Web auth error', ['error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated via JWT token
|
||||
*/
|
||||
private function checkJwtAuth(Request $request): bool
|
||||
{
|
||||
try {
|
||||
$token =
|
||||
$this->getBearerToken($request) ??
|
||||
$request->get('token') ??
|
||||
$request->cookie('access_token') ??
|
||||
$request->cookie('user-token');
|
||||
|
||||
if (!$token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
JWTAuth::setToken($token);
|
||||
$user = JWTAuth::authenticate();
|
||||
|
||||
if ($user) {
|
||||
return true;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::debug('AffiliateClickMiddleware: JWT invalid', ['error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Bearer token
|
||||
*/
|
||||
private function getBearerToken(Request $request): ?string
|
||||
{
|
||||
if (!$request->hasHeader('Authorization')) return null;
|
||||
|
||||
return preg_match('/Bearer\s+(.*)$/i', $request->header('Authorization'), $m)
|
||||
? $m[1]
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track affiliate click once per session
|
||||
*/
|
||||
private function trackClick(string $trackingCode): void
|
||||
{
|
||||
// Use a single session key with an array to prevent session bloat
|
||||
$trackedClicks = Session::get('affiliate_clicks_tracked', []);
|
||||
|
||||
// Check if this code has already been tracked in this session
|
||||
if (in_array($trackingCode, $trackedClicks)) {
|
||||
return; // Already tracked
|
||||
}
|
||||
|
||||
// Execute tracking
|
||||
app(TracksAffiliateClick::class)->execute($trackingCode);
|
||||
|
||||
// Add to tracked list
|
||||
$trackedClicks[] = $trackingCode;
|
||||
|
||||
// Limit array size to prevent DoS (keep only last 10 clicks)
|
||||
if (count($trackedClicks) > 10) {
|
||||
$trackedClicks = array_slice($trackedClicks, -10);
|
||||
}
|
||||
|
||||
Session::put('affiliate_clicks_tracked', $trackedClicks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CreateAffiliateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
// Authorization will be handled by middleware and policy
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'code' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'min:3',
|
||||
'max:255',
|
||||
'regex:/^[a-zA-Z0-9_-]+$/',
|
||||
'unique:affiliates,code,NULL,id,deleted_at,NULL'
|
||||
],
|
||||
'campaign_name' => 'required|string|max:255',
|
||||
'campaign_description' => 'nullable|string|max:65535',
|
||||
'is_active' => 'nullable|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom error messages for validation rules.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'code.regex' => 'The affiliate code may only contain letters, numbers, hyphens, and underscores.',
|
||||
'code.unique' => 'This affiliate code is already in use.',
|
||||
'code.min' => 'The affiliate code must be at least 3 characters.',
|
||||
'campaign_name.required' => 'A campaign name is required.',
|
||||
'campaign_name.max' => 'The campaign name cannot exceed 255 characters.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateAffiliateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'campaign_name' => 'required|string|max:255',
|
||||
'campaign_description' => 'nullable|string|max:65535',
|
||||
'is_active' => 'nullable|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom error messages for validation rules.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'campaign_name.required' => 'A campaign name is required.',
|
||||
'campaign_name.max' => 'The campaign name cannot exceed 255 characters.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateAffiliateSettingsRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'code_binding_days' => 'required|integer|min:1|max:365',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom error messages for validation rules.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
'code_binding_days.required' => 'The code binding days setting is required.',
|
||||
'code_binding_days.integer' => 'The code binding days must be a number.',
|
||||
'code_binding_days.min' => 'The code binding days must be at least 1 day.',
|
||||
'code_binding_days.max' => 'The code binding days cannot exceed 365 days.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class AffiliateResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'code' => $this->code,
|
||||
'campaign_name' => $this->campaign_name,
|
||||
'campaign_description' => $this->campaign_description,
|
||||
'is_active' => (bool) $this->is_active,
|
||||
'created_by' => $this->created_by,
|
||||
'clicks_count' => $this->clicks_count,
|
||||
'registrations_count' => $this->registrations_count,
|
||||
'orders_count' => $this->orders_count,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Affiliate extends AbstractModel
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'affiliates';
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
protected $fillable = [
|
||||
'code',
|
||||
'campaign_name',
|
||||
'campaign_description',
|
||||
'is_active',
|
||||
'created_by',
|
||||
'clicks_count',
|
||||
'registrations_count',
|
||||
'orders_count'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'clicks_count' => 'integer',
|
||||
'registrations_count' => 'integer',
|
||||
'orders_count' => 'integer'
|
||||
];
|
||||
|
||||
/**
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'user_affiliates', 'affiliate_id', 'user_id')
|
||||
->withPivot('clicked_at', 'registered_at')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the affiliate tracking link
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTrackingLinkAttribute(): string
|
||||
{
|
||||
$baseUrl = config('app.url');
|
||||
return $baseUrl . '/signup?tracking=' . $this->code;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,4 +120,14 @@ class User extends AbstractModel implements
|
||||
{
|
||||
return $this->morphMany(KeyValuePair::class, 'owner');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsToMany
|
||||
*/
|
||||
public function affiliates(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Affiliate::class, 'user_affiliates', 'user_id', 'affiliate_id')
|
||||
->withPivot('clicked_at', 'registered_at')
|
||||
->withTimestamps();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserAffiliate extends AbstractModel
|
||||
{
|
||||
protected $table = 'user_affiliates';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'affiliate_id',
|
||||
'clicked_at',
|
||||
'registered_at'
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'clicked_at',
|
||||
'registered_at'
|
||||
];
|
||||
|
||||
/**
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function affiliate(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Affiliate::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Affiliate;
|
||||
use App\Classes\ValueObjects\Constants\RoleTypes;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class AffiliatePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Check if user is admin
|
||||
*
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
private function isAdmin(User $user): bool
|
||||
{
|
||||
return in_array($user->type, RoleTypes::ADMIN_ROLES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view any affiliates.
|
||||
*
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function viewAny(User $user)
|
||||
{
|
||||
// Only admins can view affiliates list
|
||||
return $this->isAdmin($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the affiliate.
|
||||
*
|
||||
* @param User $user
|
||||
* @param Affiliate $affiliate
|
||||
* @return bool
|
||||
*/
|
||||
public function view(User $user, Affiliate $affiliate)
|
||||
{
|
||||
return $this->isAdmin($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create affiliates.
|
||||
*
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function create(User $user)
|
||||
{
|
||||
return $this->isAdmin($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the affiliate.
|
||||
*
|
||||
* @param User $user
|
||||
* @param Affiliate $affiliate
|
||||
* @return bool
|
||||
*/
|
||||
public function update(User $user, Affiliate $affiliate)
|
||||
{
|
||||
return $this->isAdmin($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the affiliate.
|
||||
*
|
||||
* @param User $user
|
||||
* @param Affiliate $affiliate
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(User $user, Affiliate $affiliate)
|
||||
{
|
||||
return $this->isAdmin($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can manage affiliate settings.
|
||||
*
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function manageSettings(User $user)
|
||||
{
|
||||
return $this->isAdmin($user);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||
*/
|
||||
protected $policies = [
|
||||
// 'App\Model' => 'App\Policies\ModelPolicy',
|
||||
\App\Models\Affiliate::class => \App\Policies\AffiliatePolicy::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateAffiliatesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('affiliates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('code')->unique();
|
||||
$table->string('campaign_name');
|
||||
$table->text('campaign_description')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->unsignedBigInteger('created_by')->nullable();
|
||||
$table->unsignedInteger('clicks_count')->default(0);
|
||||
$table->unsignedInteger('registrations_count')->default(0);
|
||||
$table->unsignedInteger('orders_count')->default(0);
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index('code');
|
||||
$table->index('is_active');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('affiliates');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateUserAffiliatesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('user_affiliates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->unsignedBigInteger('affiliate_id');
|
||||
$table->timestamp('clicked_at')->nullable();
|
||||
$table->timestamp('registered_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'affiliate_id']);
|
||||
$table->index('user_id');
|
||||
$table->index('affiliate_id');
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||
$table->foreign('affiliate_id')->references('id')->on('affiliates')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('user_affiliates');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddForeignKeyToAffiliatesCreatedBy extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('affiliates', function (Blueprint $table) {
|
||||
// Add foreign key constraint to created_by column
|
||||
$table->foreign('created_by')
|
||||
->references('id')
|
||||
->on('users')
|
||||
->onDelete('set null'); // Set to null if user is deleted, preserving affiliate record
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('affiliates', function (Blueprint $table) {
|
||||
// Drop the foreign key constraint
|
||||
$table->dropForeign(['created_by']);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -204,7 +204,9 @@
|
||||
|
||||
},
|
||||
submitForm(){
|
||||
this.step === 2 ? this.submit(this.route('api.account.registration.register'), 'post', 'registrationSection', false, false) : this.changeStep('next');
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const tracking = urlParams.get('tracking');
|
||||
this.step === 2 ? this.submit(this.route('api.account.registration.register') + (tracking ? '?tracking=' + tracking : ''), 'post', 'registrationSection', false, false) : this.changeStep('next');
|
||||
}
|
||||
},
|
||||
mixins: [registrationFormValidation]
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<loading-component style="height: 200px; top: 0;" key="1" color="success" v-show="$store.getters.isLoading('affiliateSettingsSection')"></loading-component>
|
||||
<div class="row" v-show="!$store.getters.isLoading('affiliateSettingsSection')">
|
||||
<div class="col">
|
||||
<!-- <div class="row p-b-5 m-b-20 b-b b-grey align-items-center parentContainer">
|
||||
<div class="col">
|
||||
<div class="font-heading all-caps fs-10 hint-text">
|
||||
Settings
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="row m-b-20">
|
||||
<div class="col">
|
||||
<affiliate-settings-component section="affiliateSettingsSection"></affiliate-settings-component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-t-30" v-show="!$store.getters.isLoading('affiliateSettingsSection')">
|
||||
<div class="col">
|
||||
<div class="row p-b-5 m-b-20 b-b b-grey align-items-center parentContainer">
|
||||
<div class="col">
|
||||
<div class="font-heading all-caps fs-10 hint-text">
|
||||
Affiliate Codes
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-xs btn-primary b-rad-none requestModal" data-type="createModal">
|
||||
<i class="fa fa-plus m-r-5"></i>
|
||||
New Code
|
||||
</button>
|
||||
<button class="btn btn-xs btn-outline-success" @click="refreshAffiliateCodes()">
|
||||
<i class="fa fa-refresh m-r-5"></i>
|
||||
Refresh
|
||||
</button>
|
||||
<modal-form-component section="affiliatesSection">
|
||||
<template slot="form" slot-scope="{section}">
|
||||
<affiliate-form-component :section="section"></affiliate-form-component>
|
||||
</template>
|
||||
</modal-form-component>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b-10 b-b p-b-5" style="border-color: #e0e0e0 !important;">
|
||||
<div class="col-2">
|
||||
<div class="font-heading all-caps fs-9 muted">Campaign</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="font-heading all-caps fs-9 muted">Tracking Link</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<div class="font-heading all-caps fs-9 muted">Clicks</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="font-heading all-caps fs-9 muted">Registers</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="font-heading all-caps fs-9 muted">Orders</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<div class="font-heading all-caps fs-9 muted">Status</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="font-heading all-caps fs-9 muted">Actions</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b-20">
|
||||
<div class="col">
|
||||
<list-component section="affiliatesSection" :endpoint="route('api.affiliate.list')" :key="refreshKey">
|
||||
<template slot="list" slot-scope="{data}">
|
||||
<affiliate-single-item-component section="affiliatesSection" :data="data"></affiliate-single-item-component>
|
||||
</template>
|
||||
</list-component>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
refreshKey: 1,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
refreshAffiliateCodes() {
|
||||
this.refreshKey++;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<loading-component style="height: 50px; top: 0;" key="1" color="success" v-show="$store.getters.isLoading(section)"></loading-component>
|
||||
<div class="row" v-show="!$store.getters.isLoading(section)">
|
||||
<div class="col">
|
||||
<div class="row m-b-20 parentContainer align-items-center p-b-10 b-b" style="border-color: #e0e0e0 !important;">
|
||||
<div class="col">
|
||||
<validation-wrapper-component :validator="$v.parameters.code_binding_days">
|
||||
<label>Code Binding Days</label>
|
||||
<input
|
||||
type="number"
|
||||
class="form-control"
|
||||
v-model.number="parameters.code_binding_days"
|
||||
min="1"
|
||||
max="365"
|
||||
placeholder="e.g., 30">
|
||||
</validation-wrapper-component>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="btn btn-sm btn-success b-rad-none" @click="submitForm()">
|
||||
<i class="fa fa-save m-r-5"></i>Save
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b-5 animate__animated animate__fadeInUpBig animate__fast" v-if="error">
|
||||
<div class="col">
|
||||
<small class="bold fs-10 text-danger">{{error}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { required, minValue, maxValue, integer } from "vuelidate/lib/validators";
|
||||
import request from '../../../general/mixins/request';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
section: {
|
||||
type: String,
|
||||
default: "affiliateSettingsSection"
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: '',
|
||||
parameters: {
|
||||
code_binding_days: 30
|
||||
}
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
parameters: {
|
||||
code_binding_days: {
|
||||
required,
|
||||
integer,
|
||||
minValue: minValue(1),
|
||||
maxValue: maxValue(365)
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadSettings();
|
||||
},
|
||||
methods: {
|
||||
loadSettings() {
|
||||
this.$store.dispatch('toggleLoading', {name: this.section, status: true});
|
||||
this.$store.dispatch('crudRequest', {
|
||||
endpoint: this.route('api.affiliate.settings.get'),
|
||||
method: 'get'
|
||||
}).then(response => {
|
||||
this.$store.dispatch('toggleLoading', {name: this.section, status: false});
|
||||
if (response.ok) {
|
||||
response.json().then(data => {
|
||||
if (data && data.code_binding_days !== undefined) {
|
||||
this.parameters.code_binding_days = parseInt(data.code_binding_days);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
response.json().then(data => {
|
||||
this.error = data.message || 'Failed to load settings';
|
||||
});
|
||||
}
|
||||
}).catch(error => {
|
||||
this.$store.dispatch('toggleLoading', {name: this.section, status: false});
|
||||
this.error = 'Failed to load settings';
|
||||
});
|
||||
},
|
||||
submitForm() {
|
||||
if (!this.validate()) {
|
||||
return;
|
||||
}
|
||||
this.error = '';
|
||||
this.submit(
|
||||
this.route('api.affiliate.settings.update'),
|
||||
'post',
|
||||
this.section,
|
||||
true,
|
||||
true
|
||||
);
|
||||
},
|
||||
validate() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
successHandler(response, section) {
|
||||
this.$store.dispatch('createNotification', {
|
||||
title: 'Success',
|
||||
message: 'Affiliate settings updated successfully',
|
||||
type: 'success'
|
||||
});
|
||||
},
|
||||
errorHandler(response, statusCode, section) {
|
||||
this.error = response.message || 'Failed to update settings';
|
||||
}
|
||||
},
|
||||
mixins: [request]
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="row m-b-10 parentContainer align-items-center b-b p-b-10 p-t-5" style="border-color: #e0e0e0 !important;">
|
||||
<div class="col-2">
|
||||
<div class="font-heading fs-10 bold">{{item.campaign_name}}</div>
|
||||
<div class="font-heading fs-9 text-info">{{item.code}}</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="copyLink font-heading fs-10 text-primary pointer" @click="copyLink">Copy link</span>
|
||||
</div>
|
||||
<div class="font-heading fs-9 hint-text" v-if="item.campaign_description" :title="item.campaign_description">
|
||||
{{ item.campaign_description.length > 30 ? item.campaign_description.substring(0, 30) + '...' : item.campaign_description }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="row">
|
||||
<div class="col-4">
|
||||
<div class="font-heading fs-10 bold">{{item.clicks_count || 0}}</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="font-heading fs-10 bold">{{item.registrations_count || 0}}</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="font-heading fs-10 bold">{{item.orders_count || 0}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-1">
|
||||
<span class="badge" :class="(item.is_active === true || item.is_active === 1 || item.is_active === '1' || item.is_active === 'true') ? 'badge-success' : 'badge-secondary'">
|
||||
{{ (item.is_active === true || item.is_active === 1 || item.is_active === '1' || item.is_active === 'true') ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-xs btn-primary b-rad-none requestModal" data-type="createModal">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-xs btn-outline-danger b-rad-none m-l-5 requestModal" data-type="deleteAffiliate">
|
||||
<i class="fa fa-times"></i>
|
||||
</button>
|
||||
<modal-component class="animate__animated animate__fast animate__fadeIn" styleType="fill-in" type="deleteAffiliate">
|
||||
<delete-affiliate-form-component :data="item" :section="section" class="text-center"></delete-affiliate-form-component>
|
||||
</modal-component>
|
||||
<modal-form-component size="large" section="affiliatesSection">
|
||||
<template slot="form" slot-scope="{section}">
|
||||
<affiliate-form-component :data="data" :section="section"></affiliate-form-component>
|
||||
</template>
|
||||
</modal-form-component>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import componentHandler from '../../../general/mixins/componentHandler';
|
||||
export default {
|
||||
props: {
|
||||
section: {
|
||||
default: 'affiliatesSection'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
trackingLink() {
|
||||
if (this.item && this.item.code) {
|
||||
const baseUrl = window.location.origin;
|
||||
return `${baseUrl}/signup?tracking=${this.item.code}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyLink() {
|
||||
if (this.trackingLink) {
|
||||
navigator.clipboard.writeText(this.trackingLink).then(() => {
|
||||
this.$store.dispatch('createNotification', {
|
||||
title: 'Success',
|
||||
message: 'Tracking link copied to clipboard',
|
||||
type: 'success'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
mixins: [componentHandler]
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<loading-component style="height: 50px; top: 0;" key="1" color="success" v-show="$store.getters.isLoading(section)"></loading-component>
|
||||
<div class="row" v-show="!$store.getters.isLoading(section)">
|
||||
<div class="col">
|
||||
<div class="row m-b-20">
|
||||
<div class="col">
|
||||
<h6 class="all-caps m-b-5 bold no-margin">{{ parameters.id ? 'Update Affiliate Code' : 'Create Affiliate Code' }}</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b-5 animate__animated animate__fadeInUpBig animate__fast" v-if="error">
|
||||
<div class="col">
|
||||
<small class="bold fs-10 text-danger">{{error}}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b-10">
|
||||
<div class="col">
|
||||
<input type="hidden" class="form-control" v-if="parameters.id" v-model="parameters.id">
|
||||
<validation-wrapper-component :validator="$v.parameters.campaign_name">
|
||||
<label>Campaign Name</label>
|
||||
<input type="text" class="form-control" v-model="parameters.campaign_name" placeholder="e.g., Summer Campaign 2024">
|
||||
</validation-wrapper-component>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b-10">
|
||||
<div class="col">
|
||||
<validation-wrapper-component :validator="$v.parameters.code">
|
||||
<label>Code</label>
|
||||
<input type="text" class="form-control" v-model="parameters.code" placeholder="e.g., SUMMER2024" :disabled="!!parameters.id">
|
||||
<small class="hint-text" v-if="!parameters.id">Leave empty to auto-generate a unique code</small>
|
||||
<small class="hint-text" v-if="parameters.id">Code cannot be changed after creation</small>
|
||||
</validation-wrapper-component>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b-10">
|
||||
<div class="col">
|
||||
<validation-wrapper-component :validator="$v.parameters.campaign_description">
|
||||
<label>Campaign Description</label>
|
||||
<textarea class="form-control" v-model="parameters.campaign_description" rows="3" placeholder="Optional description for this affiliate campaign"></textarea>
|
||||
</validation-wrapper-component>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b-10">
|
||||
<div class="col">
|
||||
<label>Is Active</label>
|
||||
<div class="row m-t-5">
|
||||
<div class="col-auto">
|
||||
<div class="pointer" @click="parameters.is_active = !parameters.is_active">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto p-r-10">
|
||||
<div class="icon-thumbnail fs-12 icon-30 btn-rounded" :class="[{'bg-success': parameters.is_active}, {'bg-master-light': !parameters.is_active}]">
|
||||
<i class="fa fa-check fs-12 text-white" v-if="parameters.is_active"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col p-l-0">
|
||||
<div class="font-heading fs-10" :class="[{'text-success': parameters.is_active}, {'text-muted': !parameters.is_active}]">
|
||||
{{ parameters.is_active ? 'Active' : 'Inactive' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b-20" v-if="parameters.id && trackingLink">
|
||||
<div class="col">
|
||||
<label>Tracking Link</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" :value="trackingLink" readonly>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-sm btn-default" @click="copyLink" type="button">
|
||||
<i class="fa fa-copy"></i> Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col p-r-5">
|
||||
<div class="btn btn-sm btn-default bg-master-lightest btn-block b-rad-none" data-dismiss="modal">Cancel</div>
|
||||
</div>
|
||||
<div class="col p-l-5">
|
||||
<div class="btn btn-sm btn-success btn-block b-rad-none" @click="submitForm()">{{ parameters.id ? 'Update' : 'Create' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ModalFormHandler from '../../../general/mixins/modalFormHandler';
|
||||
import { required } from "vuelidate/lib/validators";
|
||||
export default {
|
||||
props: {
|
||||
section: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
error: '',
|
||||
parameters: {
|
||||
id: null,
|
||||
campaign_name: '',
|
||||
code: '',
|
||||
campaign_description: '',
|
||||
is_active: true
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
trackingLink() {
|
||||
if (this.parameters.id && this.parameters.code) {
|
||||
const baseUrl = window.location.origin;
|
||||
return `${baseUrl}/signup?tracking=${this.parameters.code}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// Override formHandler mixin's created() to create a copy instead of reference
|
||||
if (this.data) {
|
||||
// Create a deep copy to avoid modifying the original data object
|
||||
this.parameters = JSON.parse(JSON.stringify(this.data));
|
||||
// Normalize is_active to boolean
|
||||
if (this.parameters.is_active !== undefined) {
|
||||
if (this.parameters.is_active === false || this.parameters.is_active === 'false' || this.parameters.is_active === 0 || this.parameters.is_active === '0') {
|
||||
this.parameters.is_active = false;
|
||||
} else if (this.parameters.is_active === true || this.parameters.is_active === 'true' || this.parameters.is_active === 1 || this.parameters.is_active === '1') {
|
||||
this.parameters.is_active = true;
|
||||
}
|
||||
} else {
|
||||
this.parameters.is_active = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
handler(newData) {
|
||||
// Create a deep copy to avoid modifying the original data object
|
||||
if (newData) {
|
||||
this.parameters = JSON.parse(JSON.stringify(newData));
|
||||
// Normalize is_active to boolean
|
||||
if (this.parameters.is_active !== undefined) {
|
||||
if (this.parameters.is_active === false || this.parameters.is_active === 'false' || this.parameters.is_active === 0 || this.parameters.is_active === '0') {
|
||||
this.parameters.is_active = false;
|
||||
} else if (this.parameters.is_active === true || this.parameters.is_active === 'true' || this.parameters.is_active === 1 || this.parameters.is_active === '1') {
|
||||
this.parameters.is_active = true;
|
||||
}
|
||||
} else {
|
||||
this.parameters.is_active = true;
|
||||
}
|
||||
} else {
|
||||
// For new records, reset to defaults
|
||||
this.parameters = {
|
||||
id: null,
|
||||
campaign_name: '',
|
||||
code: '',
|
||||
campaign_description: '',
|
||||
is_active: true
|
||||
};
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
validations: {
|
||||
parameters: {
|
||||
campaign_name: { required },
|
||||
code: {},
|
||||
campaign_description: {},
|
||||
is_active: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
successHandler() {
|
||||
// Only refresh the list on successful create/update
|
||||
this.closeModal();
|
||||
// Set loading to false
|
||||
this.isLoading = false;
|
||||
// Reset form
|
||||
this.resetForm();
|
||||
// Reload the affiliates list only on success
|
||||
this.$store.dispatch('reloadList', {'name': 'affiliatesSection'});
|
||||
},
|
||||
errorHandler(error) {
|
||||
// Don't reload the list on error - just handle the error message
|
||||
this.isLoading = false;
|
||||
this.errorMessageHandler(error);
|
||||
},
|
||||
resetForm() {
|
||||
if (this.$v) {
|
||||
this.$v.$reset();
|
||||
}
|
||||
if (!this.data) {
|
||||
this.parameters = {
|
||||
id: null,
|
||||
campaign_name: '',
|
||||
code: '',
|
||||
campaign_description: '',
|
||||
is_active: true
|
||||
};
|
||||
}
|
||||
},
|
||||
submitForm() {
|
||||
if (!this.validate()) {
|
||||
return;
|
||||
}
|
||||
const url = this.parameters.id
|
||||
? this.route('api.affiliate.update', this.parameters.id)
|
||||
: this.route('api.affiliate.create');
|
||||
const method = this.parameters.id ? 'put' : 'post';
|
||||
this.submit(url, method, this.section, true, false);
|
||||
},
|
||||
copyLink() {
|
||||
if (this.trackingLink) {
|
||||
navigator.clipboard.writeText(this.trackingLink).then(() => {
|
||||
this.$store.dispatch('createNotification', {
|
||||
title: 'Success',
|
||||
message: 'Tracking link copied to clipboard',
|
||||
type: 'success'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
mixins: [ModalFormHandler]
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row m-b-20">
|
||||
<div class="col">
|
||||
<h6 class="all-caps m-b-5 bold no-margin">Delete Affiliate Code</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-b-20">
|
||||
<div class="col">
|
||||
<p>Are you sure you want to delete the affiliate code <strong>{{data.campaign_name}}</strong> ({{data.code}})?</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col p-r-5">
|
||||
<div class="btn btn-sm btn-default bg-master-lightest btn-block b-rad-none" data-dismiss="modal">Cancel</div>
|
||||
</div>
|
||||
<div class="col p-l-5">
|
||||
<div class="btn btn-sm btn-danger btn-block b-rad-none" @click="submitForm()">Delete</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ModalFormHandler from '../../../general/mixins/modalFormHandler';
|
||||
export default {
|
||||
props: {
|
||||
section: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submitForm() {
|
||||
this.submit(this.route('api.affiliate.delete', this.data.id), 'delete', this.section, true, false);
|
||||
}
|
||||
},
|
||||
mixins: [ModalFormHandler]
|
||||
}
|
||||
</script>
|
||||
|
||||
+19
-2
@@ -5,8 +5,25 @@ export default {
|
||||
(this.$store.getters.isAuthenticated && !this.isProtectedRoute()&& !this.isWithTokenRoute()) ? window.location.href = this.route('dashboard') : '';
|
||||
},
|
||||
isProtectedRoute(){
|
||||
const unprotectedRoutes = [this.route('login'), this.route('signup'), this.route('account.email.verification')];
|
||||
return !unprotectedRoutes.includes(window.location.href);
|
||||
// Compare pathname only (without query parameters) to handle affiliate tracking links
|
||||
const currentPath = window.location.pathname;
|
||||
|
||||
// Define unprotected routes as pathnames directly
|
||||
// This ensures affiliate tracking links like /signup?tracking=xxx work correctly
|
||||
const unprotectedPaths = [
|
||||
'/', // login route
|
||||
'/signup', // signup route
|
||||
'/account/email/verification' // email verification route (may have token param)
|
||||
];
|
||||
|
||||
// Check if current path matches any unprotected path
|
||||
// For email verification, check if path starts with the base path
|
||||
if (currentPath.startsWith('/account/email/verification')) {
|
||||
return false; // It's an email verification route (unprotected)
|
||||
}
|
||||
|
||||
// Check exact matches for other routes
|
||||
return !unprotectedPaths.includes(currentPath);
|
||||
},
|
||||
isWithTokenRoute(){
|
||||
return window.location.href.includes(this.route('account.password.reset'));
|
||||
|
||||
@@ -264,6 +264,31 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="$store.getters.isAdmin">
|
||||
<div class="row m-b-5">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col bg-master-light tabButton" tab-name="affiliateSettings">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-auto p-t-10 p-b-10 b-r b-grey">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
|
||||
width="35" height="35"
|
||||
viewBox="0 0 172 172"
|
||||
style=" fill:#000000;"><defs><linearGradient x1="86" y1="28.55469" x2="86" y2="143.10938" gradientUnits="userSpaceOnUse" id="color-4_affiliate_gr4"><stop offset="0" stop-color="#009add"></stop><stop offset="1" stop-color="#00baa4"></stop></linearGradient></defs><g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><path d="M0,172v-172h172v172z" fill="none"></path><g><path d="M86,21.5c-35.56369,0 -64.5,28.93631 -64.5,64.5c0,35.56369 28.93631,64.5 64.5,64.5c35.56369,0 64.5,-28.93631 64.5,-64.5c0,-35.56369 -28.93631,-64.5 -64.5,-64.5zM86,32.25c29.63631,0 53.75,24.11369 53.75,53.75c0,29.63631 -24.11369,53.75 -53.75,53.75c-29.63631,0 -53.75,-24.11369 -53.75,-53.75c0,-29.63631 24.11369,-53.75 53.75,-53.75zM86,48.375c-20.81887,0 -37.625,16.80613 -37.625,37.625c0,20.81887 16.80613,37.625 37.625,37.625c20.81887,0 37.625,-16.80613 37.625,-37.625c0,-20.81887 -16.80613,-37.625 -37.625,-37.625zM86,59.125c14.81887,0 26.875,12.05613 26.875,26.875c0,14.81887 -12.05613,26.875 -26.875,26.875c-14.81887,0 -26.875,-12.05613 -26.875,-26.875c0,-14.81887 12.05613,-26.875 26.875,-26.875z" fill="url(#color-4_affiliate_gr4)"></path></g></g></svg>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="fs-12 m-t-5 all-caps m-b-5">Affiliate</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="row m-b-5" v-if="$store.getters.isAdmin">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
@@ -689,6 +714,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="$store.getters.isSuperAdmin">
|
||||
<div class="row tabsContainer hide tabContent" tab-name="affiliateSettings">
|
||||
<div class="col">
|
||||
<affiliate-section-component section="affiliateSettingsSection"></affiliate-section-component>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="row tabsContainer hide tabContent" tab-name="rewardsSegment" v-if="$store.getters.isAdmin">
|
||||
<div class="col">
|
||||
<loading-component style="height: 200px; top: 0;" key="1" color="success" v-show="$store.getters.isLoading('rewardsSection')"></loading-component>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::group(['prefix' => 'affiliate', 'as' => 'affiliate.'], function () {
|
||||
Route::get('/settings', 'Affiliate\GetAffiliateSettingsController@get')->name('settings.get');
|
||||
Route::post('/settings', 'Affiliate\UpdateAffiliateSettingsController@update')->name('settings.update');
|
||||
|
||||
Route::get('/list', 'Affiliate\ListAffiliatesController@list')->name('list');
|
||||
Route::post('/create', 'Affiliate\CreateAffiliateController@create')->name('create');
|
||||
Route::put('/update/{id}', 'Affiliate\UpdateAffiliateController@update')->name('update');
|
||||
Route::delete('/delete/{id}', 'Affiliate\DeleteAffiliateController@delete')->name('delete');
|
||||
});
|
||||
|
||||
@@ -76,6 +76,8 @@ Route::group(['middleware' => 'api', 'prefix' => 'v1', 'as' => 'api.'], function
|
||||
|
||||
require __DIR__ . '/export.php';
|
||||
|
||||
require __DIR__ . '/affiliate.php';
|
||||
|
||||
// require __DIR__ . '/rate.php';
|
||||
// require __DIR__ . '/receipt.php';
|
||||
});
|
||||
|
||||
+4
-1
@@ -59,7 +59,10 @@ Route::get('', function () {
|
||||
|
||||
Route::get('/signup', function () {
|
||||
return view('pages.accounts.sign_up');
|
||||
})->name('signup');
|
||||
})->middleware([
|
||||
\App\Http\Middleware\TrackAffiliateClick::class,
|
||||
'guest:web'
|
||||
])->name('signup');
|
||||
|
||||
Route::get('/account/email/verification/{token}', function ($token) {
|
||||
return view('pages.accounts.email_verified', ['token' => $token]);
|
||||
|
||||
Reference in New Issue
Block a user