feat: update payroll generation logic to schedule on the 5th of each month and adjust attendance penalties calculation

This commit is contained in:
Yoga Pangestu 2026-08-14 04:08:13 +07:00
parent 9d4f6c94ea
commit 7fa20fbc3c
6 changed files with 67 additions and 38 deletions

View File

@ -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();

View File

@ -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) {

View File

@ -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]
);
}

View File

@ -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

View File

@ -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 = [

View File

@ -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');