diff --git a/app/Classes/Modules/Affiliate/ControllersLogic/CreateAffiliateLogic.php b/app/Classes/Modules/Affiliate/ControllersLogic/CreateAffiliateLogic.php index ac462a6f..b6470ffe 100644 --- a/app/Classes/Modules/Affiliate/ControllersLogic/CreateAffiliateLogic.php +++ b/app/Classes/Modules/Affiliate/ControllersLogic/CreateAffiliateLogic.php @@ -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)); } } diff --git a/app/Classes/Modules/Affiliate/Services/CreatesAffiliate.php b/app/Classes/Modules/Affiliate/Services/CreatesAffiliate.php index 327e25f4..6eb4d455 100644 --- a/app/Classes/Modules/Affiliate/Services/CreatesAffiliate.php +++ b/app/Classes/Modules/Affiliate/Services/CreatesAffiliate.php @@ -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(); diff --git a/app/Classes/Modules/Affiliate/Standards/Rules/CanCreateAffiliate.php b/app/Classes/Modules/Affiliate/Standards/Rules/CanCreateAffiliate.php new file mode 100644 index 00000000..03e7cee1 --- /dev/null +++ b/app/Classes/Modules/Affiliate/Standards/Rules/CanCreateAffiliate.php @@ -0,0 +1,52 @@ +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; + } +} diff --git a/app/Classes/Modules/Affiliate/Standards/Validators/AffiliateCreateValidation.php b/app/Classes/Modules/Affiliate/Standards/Validators/AffiliateCreateValidation.php new file mode 100644 index 00000000..85fa5079 --- /dev/null +++ b/app/Classes/Modules/Affiliate/Standards/Validators/AffiliateCreateValidation.php @@ -0,0 +1,69 @@ + $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.', + ]; + } +} diff --git a/app/Http/Controllers/Affiliate/CreateAffiliateController.php b/app/Http/Controllers/Affiliate/CreateAffiliateController.php index 609bf463..9a0679c7 100644 --- a/app/Http/Controllers/Affiliate/CreateAffiliateController.php +++ b/app/Http/Controllers/Affiliate/CreateAffiliateController.php @@ -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); } } diff --git a/app/Http/Requests/CreateAffiliateRequest.php b/app/Http/Requests/CreateAffiliateRequest.php index f655794f..babdadf6 100644 --- a/app/Http/Requests/CreateAffiliateRequest.php +++ b/app/Http/Requests/CreateAffiliateRequest.php @@ -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', diff --git a/resources/assets/vue/components/settings/elements/AffiliateSectionComponent.vue b/resources/assets/vue/components/settings/elements/AffiliateSectionComponent.vue index 3104f89c..8da2cdbf 100644 --- a/resources/assets/vue/components/settings/elements/AffiliateSectionComponent.vue +++ b/resources/assets/vue/components/settings/elements/AffiliateSectionComponent.vue @@ -42,6 +42,28 @@ +
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
Campaign
@@ -71,7 +93,7 @@
- + @@ -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++; } } } diff --git a/resources/assets/vue/components/settings/forms/AffiliateFormComponent.vue b/resources/assets/vue/components/settings/forms/AffiliateFormComponent.vue index 48cfda06..691e0902 100644 --- a/resources/assets/vue/components/settings/forms/AffiliateFormComponent.vue +++ b/resources/assets/vue/components/settings/forms/AffiliateFormComponent.vue @@ -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) {