fix gitlab tests
This commit is contained in:
@@ -10,7 +10,8 @@ class PaymentEnrollmentEventService
|
||||
{
|
||||
public function __construct(
|
||||
private ApplicationUrlService $urls,
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildEventData(
|
||||
int $parentId,
|
||||
@@ -28,7 +29,7 @@ class PaymentEnrollmentEventService
|
||||
->where('id', $parentId)
|
||||
->first();
|
||||
|
||||
if (! $parent) {
|
||||
if (!$parent) {
|
||||
throw new \RuntimeException("Parent user #{$parentId} not found");
|
||||
}
|
||||
|
||||
@@ -103,7 +104,7 @@ class PaymentEnrollmentEventService
|
||||
}
|
||||
$seen[$sid] = true;
|
||||
|
||||
$teacherFull = trim(trim((string) ($row['t_firstname'] ?? '')).' '.trim((string) ($row['t_lastname'] ?? '')));
|
||||
$teacherFull = trim(trim((string) ($row['t_firstname'] ?? '')) . ' ' . trim((string) ($row['t_lastname'] ?? '')));
|
||||
$teacherFull = $teacherFull !== '' ? $teacherFull : null;
|
||||
|
||||
$students[] = [
|
||||
@@ -111,7 +112,7 @@ class PaymentEnrollmentEventService
|
||||
'student_id' => $sid,
|
||||
'firstname' => (string) ($row['s_firstname'] ?? ''),
|
||||
'lastname' => (string) ($row['s_lastname'] ?? ''),
|
||||
'name' => trim(((string) ($row['s_firstname'] ?? '')).' '.((string) ($row['s_lastname'] ?? ''))),
|
||||
'name' => trim(((string) ($row['s_firstname'] ?? '')) . ' ' . ((string) ($row['s_lastname'] ?? ''))),
|
||||
'registration_grade' => (string) ($row['registration_grade'] ?? ''),
|
||||
'class_section_id' => isset($row['class_section_id']) ? (int) $row['class_section_id'] : null,
|
||||
'class_section_name' => $row['class_section_name'] ?? null,
|
||||
|
||||
@@ -11,7 +11,9 @@ use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PaymentEventChargesService
|
||||
{
|
||||
public function __construct(private ChargeService $chargeService) {}
|
||||
public function __construct(private ChargeService $chargeService)
|
||||
{
|
||||
}
|
||||
|
||||
public function listCharges(?string $schoolYear, ?string $semester): array
|
||||
{
|
||||
@@ -54,7 +56,7 @@ class PaymentEventChargesService
|
||||
|
||||
$count = 0;
|
||||
|
||||
if (! empty($studentIds)) {
|
||||
if (!empty($studentIds)) {
|
||||
foreach ($studentIds as $studentId) {
|
||||
$result = $this->chargeService->createEventCharge([
|
||||
'parent_id' => $parentId,
|
||||
@@ -71,7 +73,7 @@ class PaymentEventChargesService
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
} elseif (! empty($payload['student_id'])) {
|
||||
} elseif (!empty($payload['student_id'])) {
|
||||
$result = $this->chargeService->createEventCharge([
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => (int) $payload['student_id'],
|
||||
|
||||
@@ -17,7 +17,8 @@ class PaymentManualService
|
||||
public function __construct(
|
||||
private DiscountInvoiceService $invoiceService,
|
||||
private PaymentEnrollmentEventService $enrollmentEventService
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function search(string $searchTerm, ?string $schoolYear = null): array
|
||||
{
|
||||
@@ -47,9 +48,9 @@ class PaymentManualService
|
||||
$builder->where(function ($q) use ($searchTerm, $searchClean, $searchFormatted) {
|
||||
$q->where('email', $searchTerm);
|
||||
|
||||
if (! empty($searchClean)) {
|
||||
if (!empty($searchClean)) {
|
||||
if (strlen($searchClean) === 10) {
|
||||
$formattedPhone = substr($searchClean, 0, 3).'-'.substr($searchClean, 3, 3).'-'.substr($searchClean, 6, 4);
|
||||
$formattedPhone = substr($searchClean, 0, 3) . '-' . substr($searchClean, 3, 3) . '-' . substr($searchClean, 6, 4);
|
||||
$q->orWhere('cellphone', $formattedPhone);
|
||||
}
|
||||
|
||||
@@ -59,13 +60,13 @@ class PaymentManualService
|
||||
);
|
||||
}
|
||||
|
||||
if (! empty($searchFormatted)) {
|
||||
if (!empty($searchFormatted)) {
|
||||
$q->orWhere('cellphone', $searchFormatted);
|
||||
}
|
||||
});
|
||||
|
||||
$parent = (array) $builder->first();
|
||||
if (! empty($parent)) {
|
||||
if (!empty($parent)) {
|
||||
unset($parent['password']);
|
||||
$parentData = $parent;
|
||||
$parentId = (int) ($parent['id'] ?? 0);
|
||||
@@ -126,23 +127,23 @@ class PaymentManualService
|
||||
OR email LIKE ?
|
||||
OR cellphone LIKE ?";
|
||||
|
||||
$params = ['%'.$q.'%', '%'.$q.'%', '%'.$q.'%'];
|
||||
$params = ['%' . $q . '%', '%' . $q . '%', '%' . $q . '%'];
|
||||
|
||||
if ($digits !== '') {
|
||||
$sql .= " OR REPLACE(REPLACE(REPLACE(REPLACE(cellphone, '(', ''), ')', ''), '-', ''), ' ', '') LIKE ?";
|
||||
$params[] = '%'.$digits.'%';
|
||||
$params[] = '%' . $digits . '%';
|
||||
}
|
||||
|
||||
$sql .= ') ORDER BY lastname, firstname LIMIT 20';
|
||||
$sql .= ") ORDER BY lastname, firstname LIMIT 20";
|
||||
|
||||
$rows = DB::select($sql, $params);
|
||||
|
||||
$items = [];
|
||||
foreach ($rows as $row) {
|
||||
$label = trim(($row->firstname ?? '').' '.($row->lastname ?? ''));
|
||||
$label = trim(($row->firstname ?? '') . ' ' . ($row->lastname ?? ''));
|
||||
$email = trim((string) ($row->email ?? ''));
|
||||
$phone = trim((string) ($row->cellphone ?? ''));
|
||||
$sub = trim($email.' '.$phone);
|
||||
$sub = trim($email . ' ' . $phone);
|
||||
$value = $email !== '' ? $email : ($phone !== '' ? $phone : $label);
|
||||
|
||||
$items[] = [
|
||||
@@ -171,7 +172,7 @@ class PaymentManualService
|
||||
$paymentMethod = strtolower(trim($paymentMethod));
|
||||
$allowedMethods = ['cash', 'check', 'card'];
|
||||
|
||||
if (! in_array($paymentMethod, $allowedMethods, true)) {
|
||||
if (!in_array($paymentMethod, $allowedMethods, true)) {
|
||||
throw new \InvalidArgumentException('Invalid payment method.');
|
||||
}
|
||||
|
||||
@@ -184,7 +185,7 @@ class PaymentManualService
|
||||
}
|
||||
|
||||
$invoice = Invoice::query()->find($invoiceId);
|
||||
if (! $invoice) {
|
||||
if (!$invoice) {
|
||||
throw new \RuntimeException('Invoice not found.');
|
||||
}
|
||||
|
||||
@@ -208,7 +209,7 @@ class PaymentManualService
|
||||
|
||||
try {
|
||||
$locked = DB::table('invoices')->where('id', $invoiceId)->lockForUpdate()->first();
|
||||
if (! $locked) {
|
||||
if (!$locked) {
|
||||
DB::rollBack();
|
||||
throw new \RuntimeException('Invoice not found.');
|
||||
}
|
||||
@@ -229,7 +230,7 @@ class PaymentManualService
|
||||
$paidCount = $this->getSuccessfulPaymentCount($invoiceId);
|
||||
$installmentSeq = $paidCount + 1;
|
||||
|
||||
$transactionId = 'INV-'.$invoiceId.'-'.str_replace('.', '', (string) microtime(true));
|
||||
$transactionId = 'INV-' . $invoiceId . '-' . str_replace('.', '', (string) microtime(true));
|
||||
|
||||
$ok = $this->processPayment(
|
||||
$invoiceId,
|
||||
@@ -245,7 +246,7 @@ class PaymentManualService
|
||||
$userId
|
||||
);
|
||||
|
||||
if (! $ok) {
|
||||
if (!$ok) {
|
||||
DB::rollBack();
|
||||
throw new \RuntimeException('Failed to record payment.');
|
||||
}
|
||||
@@ -307,7 +308,7 @@ class PaymentManualService
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
Log::error('[manualPayUpdate] '.$e->getMessage());
|
||||
Log::error('[manualPayUpdate] ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
@@ -327,12 +328,12 @@ class PaymentManualService
|
||||
}
|
||||
|
||||
$payment = Payment::query()->find($paymentId);
|
||||
if (! $payment) {
|
||||
if (!$payment) {
|
||||
throw new \RuntimeException('Original payment not found.');
|
||||
}
|
||||
|
||||
$invoice = Invoice::query()->find((int) $payment->invoice_id);
|
||||
if (! $invoice) {
|
||||
if (!$invoice) {
|
||||
throw new \RuntimeException('Linked invoice not found.');
|
||||
}
|
||||
|
||||
@@ -366,10 +367,10 @@ class PaymentManualService
|
||||
{
|
||||
$filename = basename($filename);
|
||||
$roots = [
|
||||
storage_path('app/uploads/checks/'.$filename),
|
||||
storage_path('app/uploads/cards/'.$filename),
|
||||
storage_path('app/uploads/misc/'.$filename),
|
||||
storage_path('app/uploads/'.$filename),
|
||||
storage_path('app/uploads/checks/' . $filename),
|
||||
storage_path('app/uploads/cards/' . $filename),
|
||||
storage_path('app/uploads/misc/' . $filename),
|
||||
storage_path('app/uploads/' . $filename),
|
||||
];
|
||||
|
||||
$path = null;
|
||||
@@ -380,14 +381,14 @@ class PaymentManualService
|
||||
}
|
||||
}
|
||||
|
||||
if (! $path) {
|
||||
if (!$path) {
|
||||
abort(404, 'Payment file not found.');
|
||||
}
|
||||
|
||||
if ($mode === 'inline') {
|
||||
return response()->file($path, [
|
||||
'Content-Type' => mime_content_type($path) ?: 'application/octet-stream',
|
||||
'Content-Disposition' => 'inline; filename="'.$filename.'"',
|
||||
'Content-Disposition' => 'inline; filename="' . $filename . '"',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -408,7 +409,7 @@ class PaymentManualService
|
||||
?int $userId
|
||||
): bool {
|
||||
$invoice = Invoice::query()->find($invoiceId);
|
||||
if (! $invoice) {
|
||||
if (!$invoice) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -432,7 +433,7 @@ class PaymentManualService
|
||||
'updated_by' => $userId,
|
||||
];
|
||||
|
||||
if (! Invoice::query()->whereKey($invoiceId)->update($invoiceUpdateData)) {
|
||||
if (!Invoice::query()->whereKey($invoiceId)->update($invoiceUpdateData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -465,7 +466,7 @@ class PaymentManualService
|
||||
private function getCurrentInvoiceBalanceExcludingPayment(int $invoiceId, int $excludePaymentId): float
|
||||
{
|
||||
$invoice = Invoice::query()->find($invoiceId);
|
||||
if (! $invoice) {
|
||||
if (!$invoice) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@@ -529,7 +530,7 @@ class PaymentManualService
|
||||
$discountByInvoice = [];
|
||||
$refundByInvoice = [];
|
||||
|
||||
if (! empty($invoiceIds)) {
|
||||
if (!empty($invoiceIds)) {
|
||||
$rows = Payment::query()
|
||||
->selectRaw('invoice_id, COALESCE(SUM(paid_amount),0) AS total_paid')
|
||||
->whereIn('invoice_id', $invoiceIds)
|
||||
@@ -580,7 +581,7 @@ class PaymentManualService
|
||||
|
||||
$issueYmd = '';
|
||||
foreach (['issue_date', 'start_date', 'created_at'] as $k) {
|
||||
if (! empty($inv[$k])) {
|
||||
if (!empty($inv[$k])) {
|
||||
$its = strtotime((string) $inv[$k]);
|
||||
if ($its !== false) {
|
||||
$issueYmd = date('Y-m-d', $its);
|
||||
@@ -612,7 +613,7 @@ class PaymentManualService
|
||||
$okTypes = ['image/jpeg', 'image/png', 'application/pdf'];
|
||||
$okExts = ['jpg', 'jpeg', 'png', 'pdf'];
|
||||
|
||||
if (! in_array($mime, $okTypes, true) || ! in_array($ext, $okExts, true)) {
|
||||
if (!in_array($mime, $okTypes, true) || !in_array($ext, $okExts, true)) {
|
||||
throw new \InvalidArgumentException('Unsupported file type. Use JPG, PNG, or PDF.');
|
||||
}
|
||||
|
||||
@@ -622,9 +623,9 @@ class PaymentManualService
|
||||
|
||||
$fileName = $file->hashName();
|
||||
$subdir = $paymentMethod === 'check' ? 'checks' : ($paymentMethod === 'card' ? 'cards' : 'misc');
|
||||
$targetDir = storage_path('app/uploads/'.$subdir);
|
||||
$targetDir = storage_path('app/uploads/' . $subdir);
|
||||
|
||||
if (! is_dir($targetDir)) {
|
||||
if (!is_dir($targetDir)) {
|
||||
mkdir($targetDir, 0775, true);
|
||||
}
|
||||
|
||||
@@ -640,7 +641,7 @@ class PaymentManualService
|
||||
event($name, $payload);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning($name.' hook failed: '.$e->getMessage());
|
||||
Log::warning($name . ' hook failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ class PaymentMissedCheckService
|
||||
public function __construct(
|
||||
private UserNotificationDispatchService $notifier,
|
||||
private EmailService $emailService
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function findUsersWithMissedPayments(): array
|
||||
{
|
||||
@@ -45,7 +46,7 @@ class PaymentMissedCheckService
|
||||
foreach ($users as $user) {
|
||||
$userId = (int) ($user['user_id'] ?? 0);
|
||||
$email = (string) ($user['email'] ?? '');
|
||||
$name = trim((string) ($user['firstname'] ?? '').' '.(string) ($user['lastname'] ?? ''));
|
||||
$name = trim((string) ($user['firstname'] ?? '') . ' ' . (string) ($user['lastname'] ?? ''));
|
||||
|
||||
$this->notifier->notifyUser(
|
||||
$userId,
|
||||
@@ -58,8 +59,8 @@ class PaymentMissedCheckService
|
||||
|
||||
if ($email !== '') {
|
||||
$subject = 'Payment Missed';
|
||||
$body = '<p>Dear '.e($name !== '' ? $name : 'Parent').',</p>'
|
||||
.'<p>You have a missed payment. Please pay to avoid penalty.</p>';
|
||||
$body = '<p>Dear ' . e($name !== '' ? $name : 'Parent') . ',</p>'
|
||||
. '<p>You have a missed payment. Please pay to avoid penalty.</p>';
|
||||
$ok = $this->emailService->send($email, $subject, $body, 'finance');
|
||||
$ok ? $sent++ : $failed++;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
namespace App\Services\Payments;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use App\Models\Notification;
|
||||
use App\Models\UserNotification;
|
||||
use App\Models\Configuration;
|
||||
|
||||
class PaymentNotificationDispatchService
|
||||
{
|
||||
|
||||
@@ -15,19 +15,20 @@ class PaymentNotificationService
|
||||
public function __construct(
|
||||
private EmailService $emailService,
|
||||
private PaymentNotificationDispatchService $dispatchService
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function listLogs(?string $from, ?string $to, ?string $type): array
|
||||
{
|
||||
$query = PaymentNotificationLog::query()->with('parent')->orderByDesc('sent_at');
|
||||
|
||||
if (! empty($from)) {
|
||||
if (!empty($from)) {
|
||||
$query->where('sent_at', '>=', $from);
|
||||
}
|
||||
if (! empty($to)) {
|
||||
if (!empty($to)) {
|
||||
$query->where('sent_at', '<=', $to);
|
||||
}
|
||||
if (! empty($type)) {
|
||||
if (!empty($type)) {
|
||||
$query->where('type', $type);
|
||||
}
|
||||
|
||||
@@ -84,17 +85,15 @@ class PaymentNotificationService
|
||||
|
||||
$type = $this->resolveType($parentId, $schoolYear, $typeInput);
|
||||
|
||||
if (! $force && PaymentNotificationLog::existsForPeriod($parentId, $year, $month, $type)) {
|
||||
if (!$force && PaymentNotificationLog::existsForPeriod($parentId, $year, $month, $type)) {
|
||||
$results['skipped']++;
|
||||
$results['details'][] = ['parent_id' => $parentId, 'result' => 'already_sent'];
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
if ($balance <= 0 && ! $force) {
|
||||
if ($balance <= 0 && !$force) {
|
||||
$results['skipped']++;
|
||||
$results['details'][] = ['parent_id' => $parentId, 'result' => 'no_balance'];
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
@@ -229,13 +228,13 @@ class PaymentNotificationService
|
||||
private function composeMessage(int $parentId, float $balance, string $schoolYear, \DateTime $now): array
|
||||
{
|
||||
$user = User::query()->find($parentId);
|
||||
$parentName = $user ? trim(($user->firstname ?? '').' '.($user->lastname ?? '')) : '';
|
||||
$parentName = $user ? trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? '')) : '';
|
||||
$greeting = $parentName !== '' ? "Dear {$parentName}," : 'Dear Parent,';
|
||||
|
||||
$monthYear = $now->format('F Y');
|
||||
$subject = sprintf('Monthly Tuition Reminder — %s', $schoolYear);
|
||||
$balanceFmt = '$'.number_format($balance, 2);
|
||||
$intro = 'This is your monthly installment reminder for '.$schoolYear.'.';
|
||||
$balanceFmt = '$' . number_format($balance, 2);
|
||||
$intro = 'This is your monthly installment reminder for ' . $schoolYear . '.';
|
||||
|
||||
$installmentInfo = $this->computeInstallmentSuggestion($balance);
|
||||
|
||||
@@ -299,14 +298,14 @@ class PaymentNotificationService
|
||||
'remaining_installments' => $remainingInstallments,
|
||||
'max_installments' => $maxInstallments,
|
||||
'installment_due' => round($installmentDue, 2),
|
||||
'installment_due_fmt' => '$'.number_format($installmentDue, 2),
|
||||
'installment_due_fmt' => '$' . number_format($installmentDue, 2),
|
||||
];
|
||||
}
|
||||
|
||||
private function getSecondaryGuardianEmail(int $primaryUserId): ?string
|
||||
{
|
||||
$row = DB::table('family_guardians')->where('user_id', $primaryUserId)->first();
|
||||
if (! $row || empty($row->family_id)) {
|
||||
if (!$row || empty($row->family_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -331,10 +330,9 @@ class PaymentNotificationService
|
||||
private function getUserEmail(int $userId): ?string
|
||||
{
|
||||
$email = (string) (DB::table('users')->where('id', $userId)->value('email') ?? '');
|
||||
if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
@@ -355,7 +353,7 @@ class PaymentNotificationService
|
||||
);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Unable to notify heads of finance: '.$e->getMessage());
|
||||
Log::warning('Unable to notify heads of finance: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,10 +362,9 @@ class PaymentNotificationService
|
||||
$base['sent'] += $update['sent'] ?? 0;
|
||||
$base['skipped'] += $update['skipped'] ?? 0;
|
||||
$base['failed'] += $update['failed'] ?? 0;
|
||||
if (! empty($update['details'])) {
|
||||
if (!empty($update['details'])) {
|
||||
$base['details'] = array_merge($base['details'], $update['details']);
|
||||
}
|
||||
|
||||
return $base;
|
||||
}
|
||||
|
||||
@@ -388,7 +385,6 @@ class PaymentNotificationService
|
||||
if (function_exists('user_timezone')) {
|
||||
return (string) user_timezone();
|
||||
}
|
||||
|
||||
return (string) (config('app.timezone') ?: 'UTC');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,14 @@ use DateTimeZone;
|
||||
|
||||
class PaymentTestNotificationService
|
||||
{
|
||||
public function __construct(private EmailService $emailService) {}
|
||||
public function __construct(private EmailService $emailService)
|
||||
{
|
||||
}
|
||||
|
||||
public function send(string $email, string $type = 'no_payment'): array
|
||||
{
|
||||
$email = trim($email);
|
||||
if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
return ['ok' => false, 'message' => 'Invalid email address.'];
|
||||
}
|
||||
|
||||
@@ -51,11 +53,11 @@ class PaymentTestNotificationService
|
||||
|
||||
$ccEmail = $this->findSecondaryGuardianEmail($parentId, $email);
|
||||
|
||||
$parentName = $user ? trim(($user->firstname ?? '').' '.($user->lastname ?? '')) : '';
|
||||
$parentName = $user ? trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? '')) : '';
|
||||
$monthYear = $now->format('F Y');
|
||||
$subject = sprintf('Monthly Tuition Reminder — %s', $schoolYear);
|
||||
$greeting = $parentName !== '' ? "Dear {$parentName}," : 'Dear Parent,';
|
||||
$balanceFmt = '$'.number_format($totalBalance, 2);
|
||||
$balanceFmt = '$' . number_format($totalBalance, 2);
|
||||
$intro = ($type === 'no_payment')
|
||||
? "We noticed there are outstanding tuition charges for {$schoolYear} but no payment has been recorded yet."
|
||||
: "This is your monthly installment reminder for {$schoolYear}.";
|
||||
@@ -77,11 +79,11 @@ class PaymentTestNotificationService
|
||||
</div>
|
||||
HTML;
|
||||
|
||||
$bodyHtml .= '<ul>'
|
||||
."<li><strong>Remaining installments:</strong> {$installmentInfo['remaining_installments']}</li>"
|
||||
."<li><strong>Suggested installment this month:</strong> {$installmentInfo['installment_due_fmt']}</li>"
|
||||
."<li><strong>Maximum installments available:</strong> {$installmentInfo['max_installments']}</li>"
|
||||
.'</ul>';
|
||||
$bodyHtml .= "<ul>"
|
||||
. "<li><strong>Remaining installments:</strong> {$installmentInfo['remaining_installments']}</li>"
|
||||
. "<li><strong>Suggested installment this month:</strong> {$installmentInfo['installment_due_fmt']}</li>"
|
||||
. "<li><strong>Maximum installments available:</strong> {$installmentInfo['max_installments']}</li>"
|
||||
. "</ul>";
|
||||
|
||||
$body = MailHtml::document('Monthly Tuition Reminder', $bodyHtml);
|
||||
|
||||
@@ -166,7 +168,7 @@ class PaymentTestNotificationService
|
||||
'remaining_installments' => $remainingInstallments,
|
||||
'max_installments' => $maxInstallments,
|
||||
'installment_due' => round($installmentDue, 2),
|
||||
'installment_due_fmt' => '$'.number_format($installmentDue, 2),
|
||||
'installment_due_fmt' => '$' . number_format($installmentDue, 2),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -177,7 +179,7 @@ class PaymentTestNotificationService
|
||||
}
|
||||
|
||||
$row = FamilyGuardian::query()->where('user_id', $parentId)->first();
|
||||
if (! $row || empty($row->family_id)) {
|
||||
if (!$row || empty($row->family_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class PaymentTransactionService
|
||||
'payment_status' => $payload['payment_status'] ?? 'Pending',
|
||||
'transaction_fee' => $payload['transaction_fee'] ?? null,
|
||||
'payment_reference' => $payload['payment_reference'] ?? null,
|
||||
'is_full_payment' => ! empty($payload['is_full_payment']),
|
||||
'is_full_payment' => !empty($payload['is_full_payment']),
|
||||
'school_year' => $payload['school_year'] ?? null,
|
||||
'semester' => $payload['semester'] ?? null,
|
||||
];
|
||||
|
||||
@@ -6,19 +6,13 @@ use App\Models\Payment;
|
||||
use App\Services\ApplicationUrlService;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use PayPal\Api\Amount;
|
||||
use PayPal\Api\Payer;
|
||||
use PayPal\Api\PaymentExecution;
|
||||
use PayPal\Api\RedirectUrls;
|
||||
use PayPal\Api\Transaction;
|
||||
use PayPal\Auth\OAuthTokenCredential;
|
||||
use PayPal\Rest\ApiContext;
|
||||
|
||||
class PaypalPaymentService
|
||||
{
|
||||
public function __construct(
|
||||
private ApplicationUrlService $urls,
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRedirectUrl(): string
|
||||
{
|
||||
@@ -30,11 +24,11 @@ class PaypalPaymentService
|
||||
public function createPayment(int $paymentId): array
|
||||
{
|
||||
$payment = Payment::query()->find($paymentId);
|
||||
if (! $payment) {
|
||||
if (!$payment) {
|
||||
throw new \RuntimeException('Payment not found.');
|
||||
}
|
||||
|
||||
if (! $this->sdkAvailable()) {
|
||||
if (!$this->sdkAvailable()) {
|
||||
return [
|
||||
'redirect_url' => $this->getRedirectUrl(),
|
||||
'mode' => 'hosted',
|
||||
@@ -45,23 +39,23 @@ class PaypalPaymentService
|
||||
try {
|
||||
$apiContext = $this->buildApiContext();
|
||||
|
||||
$payer = new Payer;
|
||||
$payer = new \PayPal\Api\Payer();
|
||||
$payer->setPaymentMethod('paypal');
|
||||
|
||||
$amount = new Amount;
|
||||
$amount = new \PayPal\Api\Amount();
|
||||
$amount->setCurrency('USD')->setTotal($payment->balance ?? $payment->total_amount ?? 0);
|
||||
|
||||
$transaction = new Transaction;
|
||||
$transaction = new \PayPal\Api\Transaction();
|
||||
$transaction->setAmount($amount)
|
||||
->setDescription('Payment for school fees')
|
||||
->setInvoiceNumber(uniqid('inv_', true));
|
||||
|
||||
$paypalPayment = new \PayPal\Api\Payment;
|
||||
$paypalPayment = new \PayPal\Api\Payment();
|
||||
$paypalPayment->setIntent('sale')
|
||||
->setPayer($payer)
|
||||
->setTransactions([$transaction]);
|
||||
|
||||
$redirectUrls = new RedirectUrls;
|
||||
$redirectUrls = new \PayPal\Api\RedirectUrls();
|
||||
$redirectUrls->setReturnUrl($this->urls->paypalExecuteReturnUrl())
|
||||
->setCancelUrl($this->urls->paypalCancelUrl());
|
||||
|
||||
@@ -77,14 +71,14 @@ class PaypalPaymentService
|
||||
'payment_id' => $paymentId,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('PayPal create payment failed: '.$e->getMessage());
|
||||
Log::error('PayPal create payment failed: ' . $e->getMessage());
|
||||
throw new \RuntimeException('Unable to create PayPal payment.');
|
||||
}
|
||||
}
|
||||
|
||||
public function executePayment(string $payerId, ?string $paypalPaymentId = null, ?int $paymentId = null): void
|
||||
{
|
||||
if (! $this->sdkAvailable()) {
|
||||
if (!$this->sdkAvailable()) {
|
||||
throw new \RuntimeException('PayPal SDK not installed.');
|
||||
}
|
||||
|
||||
@@ -95,13 +89,13 @@ class PaypalPaymentService
|
||||
|
||||
$apiContext = $this->buildApiContext();
|
||||
$payment = \PayPal\Api\Payment::get($paypalId, $apiContext);
|
||||
$execution = new PaymentExecution;
|
||||
$execution = new \PayPal\Api\PaymentExecution();
|
||||
$execution->setPayerId($payerId);
|
||||
|
||||
$payment->execute($execution, $apiContext);
|
||||
|
||||
$internalPaymentId = $paymentId;
|
||||
if (! $internalPaymentId && $paypalId !== '') {
|
||||
if (!$internalPaymentId && $paypalId !== '') {
|
||||
$internalPaymentId = (int) Cache::get($this->cacheKey($paypalId));
|
||||
}
|
||||
if ($internalPaymentId) {
|
||||
@@ -119,18 +113,18 @@ class PaypalPaymentService
|
||||
|
||||
private function sdkAvailable(): bool
|
||||
{
|
||||
return class_exists(ApiContext::class)
|
||||
&& class_exists(OAuthTokenCredential::class);
|
||||
return class_exists(\PayPal\Rest\ApiContext::class)
|
||||
&& class_exists(\PayPal\Auth\OAuthTokenCredential::class);
|
||||
}
|
||||
|
||||
private function buildApiContext(): ApiContext
|
||||
private function buildApiContext(): \PayPal\Rest\ApiContext
|
||||
{
|
||||
$clientId = (string) (config('services.paypal.client_id') ?: env('PAYPAL_CLIENT_ID'));
|
||||
$secret = (string) (config('services.paypal.secret') ?: env('PAYPAL_SECRET'));
|
||||
$mode = (string) (config('services.paypal.mode') ?: env('PAYPAL_MODE') ?: 'sandbox');
|
||||
|
||||
$apiContext = new ApiContext(
|
||||
new OAuthTokenCredential($clientId, $secret)
|
||||
$apiContext = new \PayPal\Rest\ApiContext(
|
||||
new \PayPal\Auth\OAuthTokenCredential($clientId, $secret)
|
||||
);
|
||||
|
||||
$apiContext->setConfig([
|
||||
@@ -143,6 +137,6 @@ class PaypalPaymentService
|
||||
|
||||
private function cacheKey(string $paypalPaymentId): string
|
||||
{
|
||||
return 'paypal_payment:'.$paypalPaymentId;
|
||||
return 'paypal_payment:' . $paypalPaymentId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ namespace App\Services\Payments;
|
||||
use App\Models\Configuration;
|
||||
use App\Models\Enrollment;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Payment;
|
||||
use App\Models\PayPalPayment;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Student;
|
||||
use App\Models\User;
|
||||
use App\Services\EmailService;
|
||||
@@ -14,7 +14,9 @@ use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PaypalPaymentSyncService
|
||||
{
|
||||
public function __construct(private EmailService $emailService) {}
|
||||
public function __construct(private EmailService $emailService)
|
||||
{
|
||||
}
|
||||
|
||||
public function sync(bool $dryRun = false, bool $reportOnly = false): array
|
||||
{
|
||||
@@ -34,16 +36,15 @@ class PaypalPaymentSyncService
|
||||
$failed = [];
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if (! $reportOnly) {
|
||||
if (!$reportOnly) {
|
||||
$entry->sync_attempts = (int) $entry->sync_attempts + 1;
|
||||
$entry->save();
|
||||
}
|
||||
|
||||
$user = User::query()->where('school_id', $entry->parent_school_id)->first();
|
||||
if (! $user) {
|
||||
if (!$user) {
|
||||
$failed[] = (string) $entry->transaction_id;
|
||||
Log::error('PayPal sync: no user for parent_school_id', ['parent_school_id' => $entry->parent_school_id]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -53,12 +54,11 @@ class PaypalPaymentSyncService
|
||||
->latest('created_at')
|
||||
->first();
|
||||
|
||||
if (! $reportOnly && ! $dryRun) {
|
||||
if (!$reportOnly && !$dryRun) {
|
||||
if ($invoice) {
|
||||
$ok = $this->applyPayment($invoice, $entry->amount, (string) $entry->transaction_id, $schoolYear, $semester, $entry->created_at);
|
||||
if (! $ok) {
|
||||
if (!$ok) {
|
||||
$failed[] = (string) $entry->transaction_id;
|
||||
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
@@ -106,7 +106,7 @@ class PaypalPaymentSyncService
|
||||
$invoice->status = 'Paid';
|
||||
}
|
||||
|
||||
if (! $invoice->save()) {
|
||||
if (!$invoice->save()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ class PaypalPaymentSyncService
|
||||
private function updateEnrollmentStatusIfPaid(int $invoiceId, string $schoolYear): void
|
||||
{
|
||||
$invoice = Invoice::query()->find($invoiceId);
|
||||
if (! $invoice || (float) $invoice->balance > 0) {
|
||||
if (!$invoice || (float) $invoice->balance > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,16 +155,15 @@ class PaypalPaymentSyncService
|
||||
{
|
||||
if ($processed === 0 && empty($failed)) {
|
||||
Log::info("[{$mode}] No PayPal sync updates.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$subject = "[{$mode}] PayPal Sync Report - ".now()->format('Y-m-d H:i');
|
||||
$subject = "[{$mode}] PayPal Sync Report - " . now()->format('Y-m-d H:i');
|
||||
$body = "PayPal Sync Mode: {$mode}\n\n";
|
||||
$body .= "{$processed} PayPal payments processed.\n\n";
|
||||
|
||||
if (! empty($failed)) {
|
||||
$body .= count($failed)." failed transactions:\n".implode("\n", $failed);
|
||||
if (!empty($failed)) {
|
||||
$body .= count($failed) . " failed transactions:\n" . implode("\n", $failed);
|
||||
} else {
|
||||
$body .= "No failed transactions.\n";
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ class PaypalTransactionsService
|
||||
|
||||
if ($keyword) {
|
||||
$query->where(function ($q) use ($keyword) {
|
||||
$q->where('transaction_id', 'like', '%'.$keyword.'%')
|
||||
->orWhere('payer_email', 'like', '%'.$keyword.'%')
|
||||
->orWhere('event_type', 'like', '%'.$keyword.'%')
|
||||
->orWhere('order_id', 'like', '%'.$keyword.'%')
|
||||
->orWhere('parent_school_id', 'like', '%'.$keyword.'%');
|
||||
$q->where('transaction_id', 'like', '%' . $keyword . '%')
|
||||
->orWhere('payer_email', 'like', '%' . $keyword . '%')
|
||||
->orWhere('event_type', 'like', '%' . $keyword . '%')
|
||||
->orWhere('order_id', 'like', '%' . $keyword . '%')
|
||||
->orWhere('parent_school_id', 'like', '%' . $keyword . '%');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ class PaypalTransactionsService
|
||||
|
||||
if ($keyword) {
|
||||
$query->where(function ($q) use ($keyword) {
|
||||
$q->where('transaction_id', 'like', '%'.$keyword.'%')
|
||||
->orWhere('payer_email', 'like', '%'.$keyword.'%')
|
||||
->orWhere('event_type', 'like', '%'.$keyword.'%')
|
||||
->orWhere('order_id', 'like', '%'.$keyword.'%')
|
||||
->orWhere('parent_school_id', 'like', '%'.$keyword.'%');
|
||||
$q->where('transaction_id', 'like', '%' . $keyword . '%')
|
||||
->orWhere('payer_email', 'like', '%' . $keyword . '%')
|
||||
->orWhere('event_type', 'like', '%' . $keyword . '%')
|
||||
->orWhere('order_id', 'like', '%' . $keyword . '%')
|
||||
->orWhere('parent_school_id', 'like', '%' . $keyword . '%');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user