update affiliate program

This commit is contained in:
Edmond Lang
2025-12-03 14:05:54 +08:00
parent 914ce02d2c
commit e015e01485
8 changed files with 213 additions and 36 deletions
@@ -2,29 +2,54 @@
namespace App\Classes\Modules\Affiliate\ControllersLogic;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\Affiliate\Services\CreatesAffiliate;
use App\Classes\Modules\Affiliate\DataTransferObjects\AffiliateObject;
use App\Classes\Modules\Affiliate\Standards\Rules\CanCreateAffiliate;
use App\Http\Resources\AffiliateResource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CreateAffiliateLogic
class CreateAffiliateLogic extends AbstractControllerLogic
{
/**
* @return array
*/
protected function notification(): array
{
return [
'title' => 'Created Affiliate',
'message' => 'You have successfully created a new Affiliate Code'
];
}
/** @var CanCreateAffiliate */
private $canCreateAffiliate;
/** @var CreatesAffiliate */
private $createsAffiliate;
/**
* CreateAffiliateLogic constructor.
* @param CanCreateAffiliate $canCreateAffiliate
* @param CreatesAffiliate $createsAffiliate
*/
public function __construct(CreatesAffiliate $createsAffiliate)
public function __construct(
CanCreateAffiliate $canCreateAffiliate,
CreatesAffiliate $createsAffiliate
)
{
$this->canCreateAffiliate = $canCreateAffiliate;
$this->createsAffiliate = $createsAffiliate;
}
/**
* @param Request $request
* @return JsonResponse
* @throws \App\Classes\Exceptions\AccessForbiddenException
* @throws \App\Classes\Exceptions\RequestValidationException
*/
public function execute(Request $request): JsonResponse
public function logic(Request $request): JsonResponse
{
$code = $request->input('code');
$code = $code !== null ? (string) $code : '';
@@ -43,12 +68,13 @@ class CreateAffiliateLogic
$isActive
);
// Validate the affiliate object
$this->canCreateAffiliate->passes($object);
// Create the affiliate
$affiliate = $this->createsAffiliate->execute($object);
return response()->json([
'message' => 'Affiliate code created successfully',
'data' => $affiliate
]);
return $this->resourceResponse(new AffiliateResource($affiliate));
}
}
@@ -23,14 +23,8 @@ class CreatesAffiliate extends AbstractUpdateRecord
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.');
}
// Uppercase the code for consistency
$code = strtoupper($code);
}
$model = new Affiliate();
@@ -0,0 +1,52 @@
<?php
namespace App\Classes\Modules\Affiliate\Standards\Rules;
use App\Classes\General\Abstracts\AbstractRule;
use App\Classes\Modules\Affiliate\DataTransferObjects\AffiliateObject;
use App\Classes\Modules\Affiliate\Standards\Validators\AffiliateCreateValidation;
class CanCreateAffiliate extends AbstractRule
{
/** @var AffiliateCreateValidation */
private $affiliateCreateValidation;
/**
* CanCreateAffiliate constructor.
* @param AffiliateCreateValidation $affiliateCreateValidation
*/
public function __construct(AffiliateCreateValidation $affiliateCreateValidation)
{
$this->affiliateCreateValidation = $affiliateCreateValidation;
}
/**
* @param AffiliateObject $object
* @return bool
*/
protected function authorized($object): bool
{
// Authorization is handled by policy in controller
return true;
}
/**
* @param AffiliateObject $object
* @return bool
* @throws \App\Classes\Exceptions\RequestValidationException
*/
protected function validators($object): bool
{
return $this->affiliateCreateValidation->validate($object);
}
/**
* @param AffiliateObject $object
* @return bool
*/
protected function criteria($object): bool
{
// No additional criteria needed for affiliate creation
return true;
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Classes\Modules\Affiliate\Standards\Validators;
use App\Classes\General\Abstracts\AbstractValidation;
use App\Classes\Modules\Affiliate\DataTransferObjects\AffiliateObject;
use App\Models\Affiliate;
use Illuminate\Validation\Rule;
class AffiliateCreateValidation extends AbstractValidation
{
/**
* @param AffiliateObject $object
* @return array
*/
protected function data($object): array
{
return [
'code' => $object->getCode(),
'campaign_name' => $object->getCampaignName(),
'campaign_description' => $object->getCampaignDescription(),
'is_active' => $object->getIsActive(),
];
}
/**
* @return array
*/
protected function rules(): array
{
return [
'code' => [
'nullable',
'string',
'min:3',
'max:255',
'regex:/^[a-zA-Z0-9_-]+$/',
function ($attribute, $value, $fail) {
if (!empty($value)) {
$uppercaseCode = strtoupper($value);
$exists = Affiliate::whereRaw('UPPER(code) = ?', [$uppercaseCode])
->whereNull('deleted_at')
->exists();
if ($exists) {
$fail('Affiliate code already exists. Please choose a different code.');
}
}
},
],
'campaign_name' => 'required|string|max:255',
'campaign_description' => 'nullable|string|max:65535',
'is_active' => 'required|boolean',
];
}
/**
* @return array
*/
protected function messages(): array
{
return [
'code.regex' => 'The affiliate code may only contain letters, numbers, hyphens, and underscores.',
'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.',
];
}
}
@@ -3,35 +3,21 @@
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;
use Illuminate\Http\Request;
class CreateAffiliateController
{
/**
* @param CreateAffiliateRequest $request
* @param Request $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 = [])
public function create(Request $request, CreateAffiliateLogic $logic): JsonResponse
{
return app(\Illuminate\Contracts\Auth\Access\Gate::class)->authorize($ability, $arguments);
return $logic->execute($request);
}
}
@@ -31,7 +31,6 @@ class CreateAffiliateRequest extends FormRequest
'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',
@@ -42,6 +42,28 @@
</modal-form-component>
</div>
</div>
<div class="row m-b-15">
<div class="col">
<div class="row">
<div class="col p-r-0">
<div class="form-group no-margin form-group-default b-rad-none">
<label class="text-primary">Search</label>
<input type="text" class="form-control" v-model="searchTerm" @keyup.enter="search" placeholder="Search by campaign name, code, or description"/>
</div>
</div>
<div class="col-auto p-l-0 p-r-0">
<div class="btn btn-primary b-rad-none" @click="search">
<i class="fa fa-search lh-40"></i>
</div>
</div>
<div class="col-auto p-l-0">
<div class="btn btn-secondary b-rad-none" @click="reset">
<i class="fa fa-remove lh-40"></i>
</div>
</div>
</div>
</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>
@@ -71,7 +93,7 @@
</div>
<div class="row m-b-20">
<div class="col">
<list-component section="affiliatesSection" :endpoint="route('api.affiliate.list')" :key="refreshKey">
<list-component section="affiliatesSection" :endpoint="route('api.affiliate.list')" :key="refreshKey" :options="options">
<template slot="list" slot-scope="{data}">
<affiliate-single-item-component section="affiliatesSection" :data="data"></affiliate-single-item-component>
</template>
@@ -89,11 +111,26 @@
data() {
return {
refreshKey: 1,
searchTerm: '',
options: {}
}
},
methods: {
refreshAffiliateCodes() {
this.refreshKey++;
},
search() {
if (this.searchTerm && this.searchTerm.trim()) {
this.options['affiliate_search'] = this.searchTerm.trim();
} else {
delete this.options['affiliate_search'];
}
this.refreshKey++;
},
reset() {
this.searchTerm = '';
delete this.options['affiliate_search'];
this.refreshKey++;
}
}
}
@@ -195,7 +195,21 @@
errorHandler(error) {
// Don't reload the list on error - just handle the error message
this.isLoading = false;
this.errorMessageHandler(error);
// Parse error message properly
let errorMessage = error.message || error;
// If error.message is a JSON string, parse it
if (typeof errorMessage === 'string') {
try {
const parsed = JSON.parse(errorMessage);
errorMessage = parsed.message || errorMessage;
} catch (e) {
// If parsing fails, use the message as-is
}
}
this.errorMessageHandler(errorMessage);
},
resetForm() {
if (this.$v) {