From 7fa20fbc3c04e75b821e02caf02ccae6c8c89f90 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 14 Aug 2026 04:08:13 +0700 Subject: [PATCH] feat: update payroll generation logic to schedule on the 5th of each month and adjust attendance penalties calculation --- .../Commands/GeneratePayrollCommand.php | 4 +- app/Jobs/CheckAttendancePenaltiesJob.php | 19 ++++-- .../Finance/Payroll/PayrollPeriodService.php | 15 ++++- .../Admin/Manage/TransactionService.php | 58 +++++++++---------- database/seeders/RolePermissionSeeder.php | 2 +- routes/console.php | 7 ++- 6 files changed, 67 insertions(+), 38 deletions(-) diff --git a/app/Console/Commands/GeneratePayrollCommand.php b/app/Console/Commands/GeneratePayrollCommand.php index 82d0630..5dd5f65 100644 --- a/app/Console/Commands/GeneratePayrollCommand.php +++ b/app/Console/Commands/GeneratePayrollCommand.php @@ -4,6 +4,7 @@ use App\Enums\PayrollPeriodStatus; use App\Enums\PayrollStatus; +use App\Enums\Role; use App\Models\Employee; use App\Models\Payroll; use App\Models\PayrollPeriod; @@ -13,7 +14,7 @@ class GeneratePayrollCommand extends Command { protected $signature = 'payroll:generate'; - protected $description = 'Generate payroll for all active employees for the current month'; + protected $description = 'Generate payroll for all active employees (scheduled on 5th of each month)'; public function handle(): int { @@ -33,6 +34,7 @@ public function handle(): int } $employees = Employee::whereHas('user', fn ($q) => $q->where('is_active', true)) + ->whereDoesntHave('user.roles', fn ($q) => $q->where('name', Role::ADMIN_BAHAN_BAKU)) ->where(fn ($q) => $q->whereNull('resign_date')->orWhere('resign_date', '>=', $now->toDateString())) ->get(); diff --git a/app/Jobs/CheckAttendancePenaltiesJob.php b/app/Jobs/CheckAttendancePenaltiesJob.php index c8a9934..7fb9cba 100644 --- a/app/Jobs/CheckAttendancePenaltiesJob.php +++ b/app/Jobs/CheckAttendancePenaltiesJob.php @@ -40,11 +40,22 @@ public function handle(): void return; } - $year = $yesterday->year; - $month = $yesterday->month; + $cuttingDay = 5; - $period = PayrollPeriod::where('year', $year) - ->where('month', $month) + if ($yesterday->day < $cuttingDay) { + $periodMonth = $yesterday->month; + $periodYear = $yesterday->year; + } else { + $periodMonth = $yesterday->month + 1; + $periodYear = $yesterday->year; + if ($periodMonth > 12) { + $periodMonth = 1; + $periodYear++; + } + } + + $period = PayrollPeriod::where('year', $periodYear) + ->where('month', $periodMonth) ->first(); if (! $period) { diff --git a/app/Services/Admin/Finance/Payroll/PayrollPeriodService.php b/app/Services/Admin/Finance/Payroll/PayrollPeriodService.php index bd23f76..09bfe2b 100644 --- a/app/Services/Admin/Finance/Payroll/PayrollPeriodService.php +++ b/app/Services/Admin/Finance/Payroll/PayrollPeriodService.php @@ -47,9 +47,22 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = public function getCurrentOrCreate(): PayrollPeriod { $now = now(); + $cuttingDay = 5; + + if ($now->day < $cuttingDay) { + $year = $now->year; + $month = $now->month; + } else { + $year = $now->year; + $month = $now->month + 1; + if ($month > 12) { + $month = 1; + $year++; + } + } return PayrollPeriod::firstOrCreate( - ['year' => $now->year, 'month' => $now->month], + ['year' => $year, 'month' => $month], ['status' => PayrollPeriodStatus::OPEN] ); } diff --git a/app/Services/Admin/Manage/TransactionService.php b/app/Services/Admin/Manage/TransactionService.php index eebbd0c..9470066 100644 --- a/app/Services/Admin/Manage/TransactionService.php +++ b/app/Services/Admin/Manage/TransactionService.php @@ -53,20 +53,20 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = 'orderItems.productVariant:id,product_id,name,stock,reject_stock,retail_stock', 'orderItems.productVariant.product:id,name', ]) - ->when($user && $this->isMarketingUser($user), fn($q) => $q->where('marketing_id', $user->id)) + ->when($user && $this->isMarketingUser($user), fn ($q) => $q->where('marketing_id', $user->id)) ->when($search, function ($q) use ($search) { - $q->whereHas('orderItems.productVariant.product', fn($sq) => $sq->where('name', 'like', "%{$search}%")) + $q->whereHas('orderItems.productVariant.product', fn ($sq) => $sq->where('name', 'like', "%{$search}%")) ->orWhere('order_number', 'like', "%{$search}%") ->orWhere('notes', 'like', "%{$search}%"); }) - ->when($filters['status'] ?? null, fn($q, $status) => $q->where('status', $status)) - ->when($filters['channel'] ?? null, fn($q, $channel) => $q->where('channel', $channel)) - ->when($filters['payment_type'] ?? null, fn($q, $paymentType) => $q->where('payment_type', $paymentType)) - ->when($filters['customer_id'] ?? null, fn($q, $customerId) => $q->where('customer_id', $customerId)) - ->when($filters['marketing_id'] ?? null, fn($q, $marketingId) => $q->where('marketing_id', $marketingId)) - ->when($filters['created_by_id'] ?? null, fn($q, $createdById) => $q->where('created_by_id', $createdById)) - ->when($filters['date_from'] ?? null, fn($q, $dateFrom) => $q->whereDate('created_at', '>=', $dateFrom)) - ->when($filters['date_to'] ?? null, fn($q, $dateTo) => $q->whereDate('created_at', '<=', $dateTo)) + ->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status)) + ->when($filters['channel'] ?? null, fn ($q, $channel) => $q->where('channel', $channel)) + ->when($filters['payment_type'] ?? null, fn ($q, $paymentType) => $q->where('payment_type', $paymentType)) + ->when($filters['customer_id'] ?? null, fn ($q, $customerId) => $q->where('customer_id', $customerId)) + ->when($filters['marketing_id'] ?? null, fn ($q, $marketingId) => $q->where('marketing_id', $marketingId)) + ->when($filters['created_by_id'] ?? null, fn ($q, $createdById) => $q->where('created_by_id', $createdById)) + ->when($filters['date_from'] ?? null, fn ($q, $dateFrom) => $q->whereDate('created_at', '>=', $dateFrom)) + ->when($filters['date_to'] ?? null, fn ($q, $dateTo) => $q->whereDate('created_at', '<=', $dateTo)) ->orderBy($sort, $direction) ->paginate($perPage); @@ -112,15 +112,15 @@ public function getSummary(array $filters = [], ?User $user = null): array ->selectRaw('COALESCE(SUM(discount), 0) as total_discount') ->selectRaw('COALESCE(SUM(nego_price), 0) as total_deduction') ->selectRaw('COALESCE(SUM(cogs), 0) as total_cogs') - ->when($user && $this->isMarketingUser($user), fn($q) => $q->where('marketing_id', $user->id)) - ->when($filters['status'] ?? null, fn($q, $status) => $q->where('status', $status)) - ->when($filters['channel'] ?? null, fn($q, $channel) => $q->where('channel', $channel)) - ->when($filters['payment_type'] ?? null, fn($q, $paymentType) => $q->where('payment_type', $paymentType)) - ->when($filters['customer_id'] ?? null, fn($q, $customerId) => $q->where('customer_id', $customerId)) - ->when($filters['marketing_id'] ?? null, fn($q, $marketingId) => $q->where('marketing_id', $marketingId)) - ->when($filters['created_by_id'] ?? null, fn($q, $createdById) => $q->where('created_by_id', $createdById)) - ->when($filters['date_from'] ?? null, fn($q, $dateFrom) => $q->whereDate('created_at', '>=', $dateFrom)) - ->when($filters['date_to'] ?? null, fn($q, $dateTo) => $q->whereDate('created_at', '<=', $dateTo)) + ->when($user && $this->isMarketingUser($user), fn ($q) => $q->where('marketing_id', $user->id)) + ->when($filters['status'] ?? null, fn ($q, $status) => $q->where('status', $status)) + ->when($filters['channel'] ?? null, fn ($q, $channel) => $q->where('channel', $channel)) + ->when($filters['payment_type'] ?? null, fn ($q, $paymentType) => $q->where('payment_type', $paymentType)) + ->when($filters['customer_id'] ?? null, fn ($q, $customerId) => $q->where('customer_id', $customerId)) + ->when($filters['marketing_id'] ?? null, fn ($q, $marketingId) => $q->where('marketing_id', $marketingId)) + ->when($filters['created_by_id'] ?? null, fn ($q, $createdById) => $q->where('created_by_id', $createdById)) + ->when($filters['date_from'] ?? null, fn ($q, $dateFrom) => $q->whereDate('created_at', '>=', $dateFrom)) + ->when($filters['date_to'] ?? null, fn ($q, $dateTo) => $q->whereDate('created_at', '<=', $dateTo)) ->first(); return [ @@ -148,9 +148,9 @@ public function getFilterOptions(): array ->with('userProfile:id,user_id,full_name') ->orderBy('id') ->get() - ->filter(fn(User $user) => $user->userProfile?->full_name) + ->filter(fn (User $user) => $user->userProfile?->full_name) ->values() - ->map(fn(User $user) => [ + ->map(fn (User $user) => [ 'id' => $user->id, 'name' => $user->userProfile->full_name, ]), @@ -213,7 +213,7 @@ public function store(array $data): Order NotificationService::notify( roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_TOKO], title: 'Transaksi Baru', - body: 'Transaksi ' . $order->order_number . ' sebesar Rp ' . number_format($totalAmount, 0, ',', '.') . ' berhasil dicatat oleh ' . auth()->user()->full_name . '.', + body: 'Transaksi '.$order->order_number.' sebesar Rp '.number_format($totalAmount, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.', url: route('admin.manage.transactions.index'), ); @@ -324,20 +324,20 @@ private function buildItemRows(array $items, string $stockType, string $priceTyp ->with(['productPrices:id,variant_id,type,price', 'product:id,name']) ->get(); - $variantLabels = $variants->mapWithKeys(fn(ProductVariant $v) => [ - $v->id => $v->product->name . ' - ' . $v->name, + $variantLabels = $variants->mapWithKeys(fn (ProductVariant $v) => [ + $v->id => $v->product->name.' - '.$v->name, ]); $prices = $variants->mapWithKeys(function (ProductVariant $variant) use ($resolvedPriceType) { $price = $variant->productPrices - ->first(fn($p) => $p->type === $resolvedPriceType); + ->first(fn ($p) => $p->type === $resolvedPriceType); return [$variant->id => $price?->price ?? 0]; }); $capitalPrices = $variants->mapWithKeys(function (ProductVariant $variant) { $price = $variant->productPrices - ->first(fn($p) => $p->type === PriceType::CAPITAL); + ->first(fn ($p) => $p->type === PriceType::CAPITAL); return [$variant->id => $price?->price ?? 0]; }); @@ -349,7 +349,7 @@ private function buildItemRows(array $items, string $stockType, string $priceTyp if ($unitPrice <= 0) { throw ValidationException::withMessages([ - 'items' => 'Harga untuk "' . $label . '" belum diatur.', + 'items' => 'Harga untuk "'.$label.'" belum diatur.', ]); } @@ -360,7 +360,7 @@ private function buildItemRows(array $items, string $stockType, string $priceTyp if ($capitalPrice <= 0) { throw ValidationException::withMessages([ - 'items' => 'Harga modal untuk "' . $label . '" belum diatur.', + 'items' => 'Harga modal untuk "'.$label.'" belum diatur.', ]); } @@ -396,7 +396,7 @@ private function generateOrderNumber(): string $sequence = 1; } - return $prefix . $date . str_pad($sequence, 4, '0', STR_PAD_LEFT); + return $prefix.$date.str_pad($sequence, 4, '0', STR_PAD_LEFT); } private function isMarketingUser(User $user): bool diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php index 3ecc569..9e22073 100644 --- a/database/seeders/RolePermissionSeeder.php +++ b/database/seeders/RolePermissionSeeder.php @@ -59,7 +59,7 @@ public function run(): void $developerOwnerPerms = array_values(array_filter( $allPermissions, - fn($p) => ! in_array($p, $excludedFromDeveloperOwner, true) + fn ($p) => ! in_array($p, $excludedFromDeveloperOwner, true) )); $rolePermissions = [ diff --git a/routes/console.php b/routes/console.php index 26bbe3b..03f29fd 100644 --- a/routes/console.php +++ b/routes/console.php @@ -5,6 +5,9 @@ use App\Jobs\CleanupOrphanedMediaJob; use Illuminate\Support\Facades\Schedule; -Schedule::command(GeneratePayrollCommand::class)->monthlyOn(1, '00:00'); -Schedule::job(new CheckAttendancePenaltiesJob)->dailyAt('00:00'); +Schedule::command(GeneratePayrollCommand::class) + ->daily() + ->when(fn () => now()->day === 5) + ->at('00:00'); +Schedule::job(new CheckAttendancePenaltiesJob)->dailyAt('12:00'); Schedule::job(new CleanupOrphanedMediaJob)->monthlyOn(1, '01:00');