======================================== FILE PATH: src/app/Domains/Notification/Models/NotificationTemplate.php ======================================== 'datetime', 'updated_at' => 'datetime', ]; } ======================================== FILE PATH: src/app/Domains/Notification/DTOs/NotificationResultDTO.php ======================================== installmentContextQuery->forInstallment($clientId, $installmentId); $templateType = $this->resolveTemplateType($ctx); return new NotificationResultDTO( phone: $ctx->customerPhone, generatedText: $this->renderer->render($templateType, [ '{{customer_name}}' => $ctx->customerName, '{{installment_number}}' => (string) $ctx->installmentNumber, '{{total_installments}}' => (string) $ctx->totalInstallments, '{{item_name}}' => $ctx->itemName, '{{due_date}}' => $ctx->dueDate, '{{amount}}' => $ctx->amount->formatted(), ]), ); } private function resolveTemplateType(InstallmentNotificationContextDTO $ctx): string { return ($ctx->installmentNumber === $ctx->totalInstallments) ? NotificationTemplateType::ContractCompleted->value : NotificationTemplateType::InstallmentPaid->value; } } ======================================== FILE PATH: src/app/Domains/Notification/Services/ShareNotificationService.php ======================================== shareContextQuery->get($clientId, $shareLogId); if ($ctx->transactionType !== $operationType) { throw new ShareTypeMismatchException($operationType->value, $ctx->transactionType->value); } return new NotificationResultDTO( phone: $ctx->customerPhone, generatedText: $this->renderer->render($operationType->templateType(), [ '{{customer_name}}' => $ctx->customerName, '{{shares_count}}' => (string) $ctx->sharesCount, '{{total_shares}}' => (string) $ctx->totalShares, ]), ); } } ======================================== FILE PATH: src/app/Domains/Notification/Services/NotificationTemplateRenderer.php ======================================== firstOrFail(); return str_replace( array_keys($variables), array_values($variables), $template->body_template, ); } } ======================================== FILE PATH: src/app/Domains/Notification/Services/ReminderNotificationService.php ======================================== installmentContextQuery->forNextUnpaid($clientId, $contractId); return new NotificationResultDTO( phone: $ctx->customerPhone, generatedText: $this->renderer->render($templateType->value, [ '{{customer_name}}' => $ctx->customerName, '{{installment_number}}' => (string) $ctx->installmentNumber, '{{total_installments}}' => (string) $ctx->totalInstallments, '{{item_name}}' => $ctx->itemName, '{{due_date}}' => $ctx->dueDate, '{{amount}}' => $ctx->amount->formatted(), ]), ); } } ======================================== FILE PATH: src/app/Domains/Notification/Http/Controllers/NotificationController.php ======================================== reminderService->generate( templateType: NotificationTemplateType::from($request->input('type')), clientId: $request->input('client_id'), contractId: $request->input('contract_id'), ); return $this->success( data: [ 'phone' => $result->phone, 'generated_text' => $result->generatedText, ], msg: __('success/notification.generated'), ); } public function generateInstallmentPayment(InstallmentPaymentNotificationRequest $request): JsonResponse { $result = $this->installmentService->generate( clientId: $request->input('client_id'), installmentId: $request->input('installment_id'), ); return $this->success( data: [ 'phone' => $result->phone, 'generated_text' => $result->generatedText, ], msg: __('success/notification.generated'), ); } public function generateShare(ShareNotificationRequest $request): JsonResponse { $result = $this->shareService->generate( operationType: ShareTransactionType::from($request->input('type')), clientId: $request->input('client_id'), shareLogId: $request->input('share_log_id'), ); return $this->success( data: [ 'phone' => $result->phone, 'generated_text' => $result->generatedText, ], msg: __('success/notification.generated'), ); } } ======================================== FILE PATH: src/app/Domains/Notification/Http/Requests/ShareNotificationRequest.php ======================================== ['required', 'string', Rule::in(array_column(ShareTransactionType::cases(), 'value'))], 'client_id' => ['required', 'integer', 'exists:clients,id'], 'share_log_id' => ['required', 'integer', 'exists:client_shares_logs,id'], ]; } public function messages(): array { return [ 'type.required' => __('errors/notification.operation_type_required'), 'type.in' => __('errors/notification.operation_type_invalid'), 'client_id.required' => __('errors/notification.client_id_required'), 'client_id.exists' => __('errors/notification.client_not_found'), 'share_log_id.required' => __('errors/notification.share_log_id_required'), 'share_log_id.exists' => __('errors/notification.share_log_not_found'), ]; } } ======================================== FILE PATH: src/app/Domains/Notification/Http/Requests/ReminderNotificationRequest.php ======================================== ['required', 'string', Rule::in(array_column(NotificationTemplateType::cases(), 'value'))], 'client_id' => ['required', 'integer', 'exists:clients,id'], 'contract_id' => ['required', 'integer', 'exists:contracts,id'], ]; } public function messages(): array { return [ 'type.required' => __('errors/notification.type_required'), 'type.in' => __('errors/notification.type_invalid'), 'client_id.required' => __('errors/notification.client_id_required'), 'client_id.exists' => __('errors/notification.client_not_found'), 'contract_id.required' => __('errors/notification.contract_id_required'), 'contract_id.exists' => __('errors/notification.contract_not_found'), ]; } } ======================================== FILE PATH: src/app/Domains/Notification/Http/Requests/InstallmentPaymentNotificationRequest.php ======================================== ['required', 'integer', 'exists:clients,id'], 'installment_id' => ['required', 'integer', 'exists:installments,id'], ]; } public function messages(): array { return [ 'client_id.required' => __('errors/notification.client_id_required'), 'client_id.exists' => __('errors/notification.client_not_found'), 'installment_id.required' => __('errors/notification.installment_id_required'), 'installment_id.exists' => __('errors/notification.installment_not_found'), ]; } } ======================================== FILE PATH: src/app/Domains/Notification/Enums/NotificationTemplateType.php ======================================== join('contracts as c', 'c.id', '=', 'i.contract_id') ->join('clients as cl', 'cl.id', '=', 'c.customer_id') ->where('cl.id', $clientId) ->where('i.id', $installmentId) ->select(self::COLUMNS) ->first(); if (!$row) { throw new MissingRelationException('installment', $installmentId); } return $this->buildDTO($row); } public function forNextUnpaid(int $clientId, int $contractId): InstallmentNotificationContextDTO { $row = DB::table('contracts as c') ->join('clients as cl', 'cl.id', '=', 'c.customer_id') ->join('installments as i', 'i.contract_id', '=', 'c.id') ->where('c.id', $contractId) ->where('cl.id', $clientId) ->whereIn('i.status', ['pending', 'overdue']) ->select(self::COLUMNS) ->orderBy('i.installment_number', 'asc') ->first(); if (!$row) { throw new NoPendingInstallmentException($contractId); } return $this->buildDTO($row); } private function buildDTO(object $row): InstallmentNotificationContextDTO { return new InstallmentNotificationContextDTO( customerName: $row->customer_name, customerPhone: $row->customer_phone, installmentNumber: (int) $row->installment_number, totalInstallments: (int) $row->total_installments, itemName: $row->item_name, dueDate: Carbon::parse($row->due_date)->format('Y-m-d'), amount: Money::of($row->amount), ); } } ======================================== FILE PATH: src/app/Domains/Notification/Queries/GetShareNotificationContextQuery.php ======================================== join('clients as cl', 'cl.id', '=', 'sl.client_id') ->where('sl.id', $shareLogId) ->where('sl.client_id', $clientId) ->select([ 'cl.name as customer_name', 'cl.phone as customer_phone', 'sl.shares_count', 'sl.transaction_type', ]) ->first(); if (!$row) { throw new MissingRelationException('share_log', $shareLogId); } $totalShares = (int) DB::table('client_shares_logs') ->where('client_id', $clientId) ->where('status', 'active') ->selectRaw(" COALESCE(SUM(CASE WHEN transaction_type = 'deposit' THEN shares_count WHEN transaction_type = 'withdraw' THEN -shares_count END), 0) as total ") ->value('total'); return new ShareNotificationContextDTO( customerName: $row->customer_name, customerPhone: $row->customer_phone, sharesCount: (int) $row->shares_count, totalShares: $totalShares, transactionType: ShareTransactionType::from($row->transaction_type), ); } } ======================================== FILE PATH: src/app/Domains/Notification/Routes/v1/api.php ======================================== middleware('permission:notifications.generate,admin'); Route::post('templates/installment-payment', [NotificationController::class, 'generateInstallmentPayment']) ->middleware('permission:notifications.generate,admin'); Route::post('templates/share', [NotificationController::class, 'generateShare']) ->middleware('permission:notifications.generate,admin');