add controllers, servoices
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Discounts;
|
||||
|
||||
use App\Models\DiscountVoucher;
|
||||
use App\Models\Invoice;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class DiscountApplyService
|
||||
{
|
||||
private DiscountContextService $context;
|
||||
private DiscountInvoiceService $invoiceService;
|
||||
|
||||
public function __construct(DiscountContextService $context, DiscountInvoiceService $invoiceService)
|
||||
{
|
||||
$this->context = $context;
|
||||
$this->invoiceService = $invoiceService;
|
||||
}
|
||||
|
||||
public function applyVoucher(int $voucherId, array $parentIds, bool $allowAdditional, int $actorId): array
|
||||
{
|
||||
$schoolYear = $this->context->getSchoolYear();
|
||||
$semester = $this->context->getSemester();
|
||||
|
||||
$voucher = DiscountVoucher::query()->find($voucherId);
|
||||
if (!$voucher) {
|
||||
return ['ok' => false, 'message' => 'Voucher not found.'];
|
||||
}
|
||||
|
||||
$voucherDescription = trim((string) ($voucher->description ?? ''));
|
||||
$voucherDescription = preg_replace(
|
||||
'/^Auto-generated on \\d{4}-\\d{2}-\\d{2}\\s*[—-]\\s*reason:\\s*/i',
|
||||
'',
|
||||
$voucherDescription
|
||||
);
|
||||
|
||||
$maxUsesRaw = $voucher->max_uses;
|
||||
$maxUses = ($maxUsesRaw === null || $maxUsesRaw === '') ? null : (int) $maxUsesRaw;
|
||||
$timesUsed = (int) ($voucher->times_used ?? 0);
|
||||
$remainingUses = ($maxUses === null) ? PHP_INT_MAX : ($maxUses - $timesUsed);
|
||||
if ($remainingUses <= 0) {
|
||||
return ['ok' => false, 'message' => 'This voucher has reached its maximum allowed uses.'];
|
||||
}
|
||||
|
||||
if (!$allowAdditional) {
|
||||
$rows = DB::table('discount_usages as du')
|
||||
->select('i.parent_id')
|
||||
->join('invoices as i', 'i.id', '=', 'du.invoice_id')
|
||||
->whereIn('i.parent_id', $parentIds)
|
||||
->where('i.school_year', $schoolYear)
|
||||
->groupBy('i.parent_id')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
$blockedParentIds = array_map(static fn ($r) => (int) ($r['parent_id'] ?? 0), $rows);
|
||||
if (!empty($blockedParentIds)) {
|
||||
$parentIds = array_values(array_diff(array_map('intval', $parentIds), $blockedParentIds));
|
||||
}
|
||||
if (empty($parentIds)) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'All selected parents already have discounts. Enable "Allow additional discounts" to apply another.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$paymentDate = function_exists('utc_now') ? utc_now() : now('UTC')->toDateTimeString();
|
||||
$fullyCoveredInvoiceIds = [];
|
||||
$touchedInvoiceIds = [];
|
||||
$appliedCount = 0;
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($parentIds as $parentId) {
|
||||
$invoices = Invoice::getInvoicesByUserId((int) $parentId, $schoolYear);
|
||||
if (empty($invoices)) {
|
||||
Log::warning('Discount apply: parent has no invoices', [
|
||||
'parent_id' => (int) $parentId,
|
||||
'voucher_id' => $voucherId,
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($invoices as $invoice) {
|
||||
if ($remainingUses <= 0) {
|
||||
break 2;
|
||||
}
|
||||
|
||||
$invoiceId = (int) ($invoice['id'] ?? 0);
|
||||
if ($invoiceId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$initialPreBalance = (float) $this->invoiceService->getCurrentInvoiceBalance($invoiceId, $schoolYear);
|
||||
if ($initialPreBalance <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$allowAdditional) {
|
||||
$exists = DB::table('discount_usages as du')
|
||||
->join('invoices as i', 'du.invoice_id', '=', 'i.id')
|
||||
->where('du.voucher_id', $voucherId)
|
||||
->where('du.invoice_id', $invoiceId)
|
||||
->where('i.school_year', $schoolYear)
|
||||
->count();
|
||||
if ($exists) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$rawDiscount = $voucher->discount_type === 'percent'
|
||||
? round(((float) ($invoice['total_amount'] ?? 0) * (float) $voucher->discount_value) / 100, 2)
|
||||
: (float) $voucher->discount_value;
|
||||
|
||||
$discount = min($rawDiscount, $initialPreBalance);
|
||||
if ($discount <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$now = function_exists('utc_now') ? utc_now() : now('UTC')->toDateTimeString();
|
||||
DB::table('discount_usages')->insert([
|
||||
'voucher_id' => $voucherId,
|
||||
'invoice_id' => $invoiceId,
|
||||
'parent_id' => (int) $parentId,
|
||||
'discount_amount' => $discount,
|
||||
'description' => $voucherDescription,
|
||||
'school_year' => $schoolYear,
|
||||
'updated_by' => $actorId,
|
||||
'used_at' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
'semester' => $semester,
|
||||
]);
|
||||
|
||||
$newBalance = max(0.0, round($initialPreBalance - $discount, 2));
|
||||
DB::table('invoices')
|
||||
->where('id', $invoiceId)
|
||||
->update([
|
||||
'balance' => $newBalance,
|
||||
'has_discount' => 1,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
$postBalance = round($initialPreBalance - $discount, 2);
|
||||
if ($postBalance < 0) {
|
||||
$postBalance = 0.0;
|
||||
}
|
||||
|
||||
$currentBalance = (float) $this->invoiceService->getCurrentInvoiceBalance($invoiceId, $schoolYear);
|
||||
|
||||
DB::table('discount_vouchers')
|
||||
->where('id', $voucherId)
|
||||
->update([
|
||||
'times_used' => DB::raw('COALESCE(times_used,0) + 1'),
|
||||
]);
|
||||
|
||||
$this->triggerPaymentReceived(
|
||||
$invoiceId,
|
||||
(string) $voucherId,
|
||||
$discount,
|
||||
$paymentDate,
|
||||
$initialPreBalance,
|
||||
$postBalance,
|
||||
$schoolYear,
|
||||
$semester
|
||||
);
|
||||
|
||||
$touchedInvoiceIds[] = $invoiceId;
|
||||
$appliedCount++;
|
||||
|
||||
$epsilon = 0.01;
|
||||
$fullyCovered = (abs($discount - $initialPreBalance) <= $epsilon)
|
||||
|| ($postBalance <= $epsilon)
|
||||
|| ($currentBalance <= $epsilon);
|
||||
|
||||
if ($fullyCovered) {
|
||||
$fullyCoveredInvoiceIds[] = $invoiceId;
|
||||
}
|
||||
|
||||
$remainingUses--;
|
||||
|
||||
if ($remainingUses <= 0 && $maxUses !== null) {
|
||||
DB::table('discount_vouchers')
|
||||
->where('id', $voucherId)
|
||||
->update([
|
||||
'is_active' => 0,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
return ['ok' => false, 'message' => 'Voucher application failed. Transaction rolled back.'];
|
||||
}
|
||||
|
||||
if ($appliedCount === 0) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => 'No discounts were applied. The voucher may already be used for these invoices or balances are zero.',
|
||||
];
|
||||
}
|
||||
|
||||
if ($maxUses !== null) {
|
||||
$row = (array) DB::table('discount_vouchers')
|
||||
->select('times_used')
|
||||
->where('id', $voucherId)
|
||||
->first();
|
||||
$usedNow = (int) ($row['times_used'] ?? 0);
|
||||
if ($usedNow >= $maxUses) {
|
||||
DB::table('discount_vouchers')
|
||||
->where('id', $voucherId)
|
||||
->update([
|
||||
'is_active' => 0,
|
||||
'updated_at' => function_exists('utc_now') ? utc_now() : now('UTC')->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_unique($touchedInvoiceIds) as $iid) {
|
||||
try {
|
||||
$this->invoiceService->recalculateInvoice($iid, $schoolYear);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('recalculateInvoice failed for invoice {id}: {err}', [
|
||||
'id' => $iid,
|
||||
'err' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$updatedTotal = 0;
|
||||
foreach (array_unique($fullyCoveredInvoiceIds) as $iid) {
|
||||
try {
|
||||
$updatedTotal += $this->invoiceService->updateEnrollmentStatusIfPaid($iid, $schoolYear);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('updateEnrollmentStatusIfPaid failed for invoice {id}: {err}', [
|
||||
'id' => $iid,
|
||||
'err' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => $updatedTotal > 0
|
||||
? 'Voucher applied successfully and enrollments updated.'
|
||||
: 'Voucher applied successfully.',
|
||||
'appliedCount' => $appliedCount,
|
||||
'updatedEnrollments' => $updatedTotal,
|
||||
'touchedInvoices' => array_values(array_unique($touchedInvoiceIds)),
|
||||
];
|
||||
}
|
||||
|
||||
private function triggerPaymentReceived(
|
||||
int $invoiceId,
|
||||
string $voucherId,
|
||||
float $amount,
|
||||
string $paymentDate,
|
||||
float $preBalance,
|
||||
float $postBalance,
|
||||
string $schoolYear,
|
||||
string $semester
|
||||
): void {
|
||||
try {
|
||||
[$eventData, $studentData] = $this->invoiceService->buildPaymentEventData(
|
||||
$invoiceId,
|
||||
$voucherId,
|
||||
$amount,
|
||||
'discount',
|
||||
$paymentDate,
|
||||
'',
|
||||
0,
|
||||
$preBalance,
|
||||
$postBalance,
|
||||
$schoolYear,
|
||||
$semester
|
||||
);
|
||||
|
||||
if (class_exists(\CodeIgniter\Events\Events::class)) {
|
||||
\CodeIgniter\Events\Events::trigger('paymentReceived', $eventData, $studentData);
|
||||
} elseif (function_exists('event')) {
|
||||
event('paymentReceived', [$eventData, $studentData]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('paymentReceived hook failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user