where('status', 'COMPLETED') ->where('synced', 0) ->where('sync_attempts', '<', 3) ->whereNotNull('transaction_id') ->get(); $synced = 0; $failed = []; foreach ($entries as $entry) { 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) { $failed[] = (string) $entry->transaction_id; Log::error('PayPal sync: no user for parent_school_id', ['parent_school_id' => $entry->parent_school_id]); continue; } $invoice = Invoice::query() ->where('parent_id', $user->id) ->where('school_year', $schoolYear) ->latest('created_at') ->first(); if (! $reportOnly && ! $dryRun) { if ($invoice) { $ok = $this->applyPayment($invoice, $entry->amount, (string) $entry->transaction_id, $schoolYear, $semester, $entry->created_at); if (! $ok) { $failed[] = (string) $entry->transaction_id; continue; } } else { Payment::query()->create([ 'parent_id' => $user->id, 'invoice_id' => 0, 'total_amount' => $entry->amount, 'paid_amount' => $entry->amount, 'balance' => 0.00, 'number_of_installments' => 1, 'transaction_id' => $entry->transaction_id, 'payment_method' => 'PayPal', 'payment_date' => optional($entry->created_at)->format('Y-m-d'), 'school_year' => $schoolYear, 'semester' => $semester, 'status' => 'Completed', 'updated_by' => null, ]); } $entry->synced = 1; $entry->save(); } $synced++; } $this->sendReport($mode, $synced, $failed); return [ 'mode' => $mode, 'processed' => $synced, 'failed' => $failed, ]; } private function applyPayment(Invoice $invoice, float $amount, string $transactionId, string $schoolYear, string $semester, $createdAt): bool { $newPaid = (float) $invoice->paid_amount + $amount; $newBalance = (float) $invoice->balance - $amount; $invoice->paid_amount = $newPaid; $invoice->balance = $newBalance; if ($newBalance <= 0) { $invoice->status = 'Paid'; } if (! $invoice->save()) { return false; } Payment::query()->create([ 'parent_id' => $invoice->parent_id, 'invoice_id' => $invoice->id, 'total_amount' => $invoice->total_amount, 'paid_amount' => $amount, 'balance' => $newBalance, 'number_of_installments' => 1, 'transaction_id' => $transactionId, 'payment_method' => 'PayPal', 'payment_date' => $createdAt ? date('Y-m-d', strtotime((string) $createdAt)) : date('Y-m-d'), 'status' => $newBalance <= 0 ? 'Full' : 'Partial', 'check_file' => null, 'updated_by' => null, 'school_year' => $schoolYear, 'semester' => $semester, ]); $this->updateEnrollmentStatusIfPaid($invoice->id, $schoolYear); return true; } private function updateEnrollmentStatusIfPaid(int $invoiceId, string $schoolYear): void { $invoice = Invoice::query()->find($invoiceId); if (! $invoice || (float) $invoice->balance > 0) { return; } $students = Student::query() ->where('parent_id', $invoice->parent_id) ->where('school_year', $schoolYear) ->get(); foreach ($students as $student) { Enrollment::query() ->where('student_id', $student->id) ->update(['enrollment_status' => 'enrolled']); } } private function sendReport(string $mode, int $processed, array $failed): void { 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'); $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); } else { $body .= "No failed transactions.\n"; } $this->emailService->send('support@alrahmaisgl.org', $subject, nl2br($body), 'finance'); } }