refactor: rename create/delete methods to store/destroy across multiple services; add transaction handling for cash and stock adjustments

This commit is contained in:
Yoga Pangestu 2026-08-08 14:31:58 +07:00
parent 1c19e6c690
commit 46af27e537
19 changed files with 188 additions and 125 deletions

View File

@ -43,7 +43,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
->paginate($perPage);
}
public function create(array $data): EmployeeAdvance
public function store(array $data): EmployeeAdvance
{
$employee = auth()->user()->employee;
@ -123,7 +123,7 @@ public function update(EmployeeAdvance $employeeAdvance, array $data): EmployeeA
return $employeeAdvance;
}
public function delete(EmployeeAdvance $employeeAdvance): bool
public function destroy(EmployeeAdvance $employeeAdvance): bool
{
return DB::transaction(function () use ($employeeAdvance) {
if ($employeeAdvance->status === EmployeeAdvanceStatus::APPROVED && $employeeAdvance->cash_transaction_id) {

View File

@ -75,7 +75,7 @@ private function formatExpense(Expense $expense): array
];
}
public function create(array $data): Expense
public function store(array $data): Expense
{
$expense = DB::transaction(function () use ($data) {
$cashTransaction = $this->debitCash(
@ -173,7 +173,7 @@ public function update(Expense $expense, array $data): Expense
return $expense;
}
public function delete(Expense $expense): bool
public function destroy(Expense $expense): bool
{
return DB::transaction(function () use ($expense) {
$this->creditCash(

View File

@ -11,7 +11,7 @@
class PayrollAdjustmentService
{
public function create(Payroll $payroll, array $data): PayrollAdjustment
public function store(Payroll $payroll, array $data): PayrollAdjustment
{
if ($payroll->status !== PayrollStatus::UNPAID) {
throw ValidationException::withMessages([
@ -35,7 +35,7 @@ public function create(Payroll $payroll, array $data): PayrollAdjustment
});
}
public function delete(PayrollAdjustment $adjustment): bool
public function destroy(PayrollAdjustment $adjustment): bool
{
$payroll = $adjustment->payroll;

View File

@ -23,6 +23,16 @@ private function canViewAll(): bool
return auth()->user()->hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko']);
}
public function getCurrentOrCreate(): PayrollPeriod
{
$now = now();
return PayrollPeriod::firstOrCreate(
['year' => $now->year, 'month' => $now->month],
['status' => PayrollPeriodStatus::OPEN]
);
}
public function getAll(array $filters = []): Collection
{
return PayrollPeriod::select(['id', 'year', 'month', 'status', 'closed_at', 'created_at'])

View File

@ -9,9 +9,11 @@
use App\Services\Concerns\RegistersMedia;
use App\Services\NotificationService;
use App\Services\S3PresignedService;
use App\Settings\HRSettings;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\ValidationException;
class AttendanceService
{
@ -21,6 +23,32 @@ public function __construct(
private S3PresignedService $s3Service,
) {}
public function getIndexData(int $year, int $month): array
{
$user = auth()->user();
$isAdmin = $user->hasAnyRole(['developer', 'owner']);
$hrSettings = app(HRSettings::class);
$employeeId = $isAdmin ? null : $user->employee?->id;
return [
'attendances' => $this->getByMonth($year, $month, $employeeId),
'leaves' => $this->getLeavesByMonth($year, $month, $employeeId),
'employees' => $isAdmin ? $this->getAllEmployees() : [],
'todayAttendance' => $isAdmin ? null : $this->getToday(),
'currentYear' => $year,
'currentMonth' => $month,
'monthStats' => $this->getMonthStats($year, $month, $employeeId),
'hrSettings' => [
'scheduled_check_in_time' => $hrSettings->scheduled_check_in_time,
'scheduled_check_out_time' => $hrSettings->scheduled_check_out_time,
],
'isOnLeave' => $isAdmin ? false : $this->isOnLeave($user),
'canCheckIn' => $isAdmin ? false : $user->employee !== null,
'isAdmin' => $isAdmin,
];
}
public function getAll(): Collection
{
return Attendance::with(['employee.user.userProfile', 'media'])
@ -217,7 +245,9 @@ public function checkIn(array $data): Attendance
$employee = auth()->user()->employee;
if (! $employee) {
throw new \Exception('Anda tidak terdaftar sebagai karyawan.');
throw ValidationException::withMessages([
'employee' => 'Anda tidak terdaftar sebagai karyawan.',
]);
}
$today = now()->toDateString();
@ -227,7 +257,9 @@ public function checkIn(array $data): Attendance
->first();
if ($existing) {
throw new \Exception('Anda sudah melakukan presensi hari ini.');
throw ValidationException::withMessages([
'attendance' => 'Anda sudah melakukan presensi hari ini.',
]);
}
$attendance = Attendance::create([

View File

@ -66,7 +66,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
->paginate($perPage);
}
public function create(array $data): User
public function store(array $data): User
{
return DB::transaction(function () use ($data) {
$user = User::create([
@ -132,7 +132,7 @@ public function update(User $user, array $data): User
return $user->fresh(['userProfile', 'employee']);
}
public function delete(User $user): bool
public function destroy(User $user): bool
{
return DB::transaction(function () use ($user) {
$user->employee()->delete();

View File

@ -9,6 +9,7 @@
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class LeaveRequestService
{
@ -43,13 +44,15 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
->paginate($perPage);
}
public function create(array $data): LeaveRequest
public function store(array $data): LeaveRequest
{
$leaveRequest = DB::transaction(function () use ($data) {
$employee = auth()->user()->employee;
if (! $employee) {
throw new \Exception('Anda tidak terdaftar sebagai karyawan.');
throw ValidationException::withMessages([
'employee' => 'Anda tidak terdaftar sebagai karyawan.',
]);
}
$startDate = new Carbon($data['start_date']);
@ -95,7 +98,7 @@ public function update(LeaveRequest $leaveRequest, array $data): LeaveRequest
});
}
public function delete(LeaveRequest $leaveRequest): bool
public function destroy(LeaveRequest $leaveRequest): bool
{
return $leaveRequest->delete();
}

View File

@ -6,8 +6,8 @@
use App\Models\CuttingMaterial;
use App\Models\CuttingMaterialCombination;
use App\Models\CuttingResult;
use App\Models\RawMaterial;
use App\Models\RawMaterialPrice;
use App\Services\Admin\Master\RawMaterial\RawMaterialService;
use App\Services\Concerns\RegistersMedia;
use App\Services\S3PresignedService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
@ -20,6 +20,7 @@ class CuttingService
public function __construct(
private S3PresignedService $s3Service,
private RawMaterialService $rawMaterialService,
) {}
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator
@ -64,22 +65,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
public function getForCreate(): array
{
return [
'rawMaterials' => RawMaterial::query()
->select(['id', 'name', 'unit', 'is_active'])
->with([
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
])
->active()
->orderBy('name')
->get()
->each(function (RawMaterial $rawMaterial) {
$rawMaterial->rawMaterialPrices->each(function (RawMaterialPrice $price) {
$media = $price->getFirstMedia('photos');
$price->photo_url = $media
? $this->s3Service->getTemporaryUrl($media->file_name)
: null;
});
}),
'rawMaterials' => $this->rawMaterialService->getVariantsForCutting(),
];
}

View File

@ -334,12 +334,6 @@ public function update(Purchase $purchase, array $data): Purchase
}
});
$oldMaterial = $oldItems
->map(fn (PurchaseItem $item) => $item->rawMaterialPrice?->rawMaterial)
->filter()
->unique(fn (RawMaterial $material) => $material->id)
->first();
$oldMaterial = $oldItems
->map(fn (PurchaseItem $item) => $item->rawMaterialPrice?->rawMaterial)
->filter()

View File

@ -22,7 +22,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
->paginate($perPage);
}
public function create(array $data): Category
public function store(array $data): Category
{
return Category::create($data);
}
@ -34,7 +34,7 @@ public function update(Category $category, array $data): Category
return $category;
}
public function delete(Category $category): bool
public function destroy(Category $category): bool
{
return $category->delete();
}

View File

@ -22,7 +22,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
->paginate($perPage);
}
public function create(array $data): Customer
public function store(array $data): Customer
{
return Customer::create($data);
}
@ -34,7 +34,7 @@ public function update(Customer $customer, array $data): Customer
return $customer;
}
public function delete(Customer $customer): bool
public function destroy(Customer $customer): bool
{
return $customer->delete();
}

View File

@ -71,7 +71,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
return $paginator;
}
public function create(array $data): RawMaterial
public function store(array $data): RawMaterial
{
return DB::transaction(function () use ($data) {
$rawMaterial = RawMaterial::create([
@ -220,7 +220,7 @@ public function update(RawMaterial $rawMaterial, array $data): RawMaterial
});
}
public function delete(RawMaterial $rawMaterial): bool
public function destroy(RawMaterial $rawMaterial): bool
{
return DB::transaction(function () use ($rawMaterial) {
$rawMaterial->rawMaterialPrices->each(function (RawMaterialPrice $price) {
@ -239,6 +239,26 @@ public function toggleStatus(RawMaterial $rawMaterial): void
]);
}
public function getVariantsForCutting(): array
{
return RawMaterial::query()
->select(['id', 'name', 'unit', 'is_active'])
->with([
'rawMaterialPrices:id,raw_material_id,variant,price,stock',
])
->active()
->orderBy('name')
->get()
->each(function (RawMaterial $rawMaterial) {
$rawMaterial->rawMaterialPrices->each(function (RawMaterialPrice $price) {
$media = $price->getFirstMedia('photos');
$price->photo_url = $media
? $this->s3Service->getTemporaryUrl($media->file_name)
: null;
});
});
}
private function registerPhoto(RawMaterialPrice $price, string $s3Key): void
{
$this->registerMedia(

View File

@ -22,7 +22,7 @@ public function paginated(int $perPage = 25, string $search = '', string $sort =
->paginate($perPage);
}
public function create(array $data): Supplier
public function store(array $data): Supplier
{
return Supplier::create($data);
}
@ -34,7 +34,7 @@ public function update(Supplier $supplier, array $data): Supplier
return $supplier;
}
public function delete(Supplier $supplier): bool
public function destroy(Supplier $supplier): bool
{
return $supplier->delete();
}

View File

@ -10,26 +10,17 @@
class RoleService
{
public function getAll(): Collection
{
return Role::withCount('permissions')->get();
}
public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
{
return Role::query()
->select(['id', 'name'])
->withCount('permissions')
->when($search, fn ($q) => $q->where('name', 'like', "%{$search}%"))
->orderBy($sort, $direction)
->paginate($perPage);
}
public function getById(int $id): Role
{
return Role::with('permissions')->findOrFail($id);
}
public function create(array $data): Role
public function store(array $data): Role
{
return DB::transaction(function () use ($data) {
$role = Role::create(['name' => $data['name']]);
@ -49,7 +40,7 @@ public function update(Role $role, array $data): Role
return $role->fresh('permissions');
}
public function delete(Role $role): bool
public function destroy(Role $role): bool
{
return $role->delete();
}
@ -61,4 +52,22 @@ public function getPermissionsByModule(): array
->map(fn ($group) => $group->pluck('name')->map(fn ($name) => explode('.', $name, 2)[1])->values()->toArray())
->toArray();
}
public function getForEmployee(): Collection
{
$query = Role::where('name', '!=', 'Developer');
$user = auth()->user();
if ($user->hasAnyRole(['admin-toko', 'direktur'])) {
$query->where('name', '!=', 'admin-bahan-baku');
}
if (! $user->hasAnyRole(['developer', 'owner', 'direktur', 'admin-toko'])) {
$userRoles = $user->roles->pluck('name');
$query->whereIn('name', $userRoles);
}
return $query->get(['id', 'name']);
}
}

View File

@ -5,6 +5,7 @@
use App\Enums\CashTransactionType;
use App\Models\CashAccount;
use App\Models\CashTransaction;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
trait HandlesCashTransactions
@ -16,41 +17,45 @@ private function getCashAccount(): CashAccount
private function creditCash(int $amount, string $description, CashTransactionType $type = CashTransactionType::DEPOSIT): CashTransaction
{
$cashAccount = $this->getCashAccount();
$newBalance = $cashAccount->balance + $amount;
return DB::transaction(function () use ($amount, $description, $type) {
$cashAccount = $this->getCashAccount();
$newBalance = $cashAccount->balance + $amount;
$cashAccount->update(['balance' => $newBalance]);
$cashAccount->update(['balance' => $newBalance]);
return CashTransaction::create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => auth()->id(),
'amount' => $amount,
'balance_after' => $newBalance,
'type' => $type,
'description' => $description,
]);
return CashTransaction::create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => auth()->id(),
'amount' => $amount,
'balance_after' => $newBalance,
'type' => $type,
'description' => $description,
]);
});
}
private function debitCash(int $amount, string $description, CashTransactionType $type = CashTransactionType::EXPENSE): CashTransaction
{
$cashAccount = $this->getCashAccount();
return DB::transaction(function () use ($amount, $description, $type) {
$cashAccount = $this->getCashAccount();
if ($cashAccount->balance < $amount) {
throw ValidationException::withMessages([
'amount' => 'Saldo tidak mencukupi.',
if ($cashAccount->balance < $amount) {
throw ValidationException::withMessages([
'amount' => 'Saldo tidak mencukupi.',
]);
}
$newBalance = $cashAccount->balance - $amount;
$cashAccount->update(['balance' => $newBalance]);
return CashTransaction::create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => auth()->id(),
'amount' => $amount,
'balance_after' => $newBalance,
'type' => $type,
'description' => $description,
]);
}
$newBalance = $cashAccount->balance - $amount;
$cashAccount->update(['balance' => $newBalance]);
return CashTransaction::create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => auth()->id(),
'amount' => $amount,
'balance_after' => $newBalance,
'type' => $type,
'description' => $description,
]);
});
}
}

View File

@ -5,6 +5,7 @@
use App\Enums\ProductStockQuality;
use App\Models\ProductVariant;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
trait HasStockAdjustment
@ -48,13 +49,10 @@ private function adjustVariantStock(int $variantId, int $quantity, int $sign, st
private function applyStock(array $items, string $stockType, int $sign): void
{
foreach ($items as $item) {
$this->adjustVariantStock($item['product_variant_id'], $item['quantity'], $sign, $stockType);
}
}
private function reverseStock(array $items, string $stockType, int $sign): void
{
$this->applyStock($items, $stockType, -$sign);
DB::transaction(function () use ($items, $stockType, $sign) {
foreach ($items as $item) {
$this->adjustVariantStock($item['product_variant_id'], $item['quantity'], $sign, $stockType);
}
});
}
}

View File

@ -49,15 +49,13 @@ public function getRevenueSummary(): array
->selectRaw('COUNT(*) as total_orders')
->selectRaw('COALESCE(SUM(total_amount), 0) as total_revenue')
->selectRaw('COALESCE(SUM(discount), 0) as total_discount')
->selectRaw('COALESCE(SUM(total_amount), 0) - COALESCE(SUM(total_amount), 0) as total_marketplace_fees')
->selectRaw('COALESCE(SUM(discount), 0) as total_deduction')
->first();
return [
'total_revenue' => (int) $stats->total_revenue,
'total_discount' => (int) $stats->total_discount,
'total_marketplace_fees' => (int) $stats->total_marketplace_fees,
'total_deduction' => (int) $stats->total_deduction,
'total_marketplace_fees' => 0,
'total_deduction' => (int) $stats->total_discount,
'total_orders' => (int) $stats->total_orders,
'avg_order' => $stats->total_orders > 0 ? (int) ($stats->total_revenue / $stats->total_orders) : 0,
];

View File

@ -4,12 +4,14 @@
use App\Models\User;
use App\Notifications\WebPushNotification;
use Illuminate\Support\Facades\Log;
class NotificationService
{
public static function notify(array $roles, string $title, string $body, string $url, ?User $additionalUser = null): void
{
$users = User::query()
->select(['id'])
->where('is_active', true)
->whereHas('roles', fn ($q) => $q->whereIn('name', $roles))
->get();
@ -19,13 +21,17 @@ public static function notify(array $roles, string $title, string $body, string
}
$users->each(function (User $user) use ($title, $body, $url) {
$user->notifications()->create([
'title' => $title,
'body' => $body,
'url' => $url,
]);
try {
$user->notifications()->create([
'title' => $title,
'body' => $body,
'url' => $url,
]);
$user->notify(new WebPushNotification($title, $body));
$user->notify(new WebPushNotification($title, $body));
} catch (\Exception $e) {
Log::error("Gagal mengirim notifikasi ke user {$user->id}: {$e->getMessage()}");
}
});
}
}

View File

@ -144,31 +144,33 @@ public function recordTransfer(
int $toBefore,
string $description = 'Transfer stok',
): void {
$userId = auth()->id();
DB::transaction(function () use ($model, $quantity, $fromQuality, $toQuality, $fromBefore, $toBefore, $description) {
$userId = auth()->id();
StockMutation::create([
'user_id' => $userId,
'stockable_type' => get_class($model),
'stockable_id' => $model->id,
'type' => 'out',
'quantity' => -$quantity,
'stock_before' => $fromBefore,
'stock_after' => $fromBefore - $quantity,
'stock_quality' => $fromQuality,
'description' => $description,
]);
StockMutation::create([
'user_id' => $userId,
'stockable_type' => get_class($model),
'stockable_id' => $model->id,
'type' => 'out',
'quantity' => -$quantity,
'stock_before' => $fromBefore,
'stock_after' => $fromBefore - $quantity,
'stock_quality' => $fromQuality,
'description' => $description,
]);
StockMutation::create([
'user_id' => $userId,
'stockable_type' => get_class($model),
'stockable_id' => $model->id,
'type' => 'in',
'quantity' => $quantity,
'stock_before' => $toBefore,
'stock_after' => $toBefore + $quantity,
'stock_quality' => $toQuality,
'description' => $description,
]);
StockMutation::create([
'user_id' => $userId,
'stockable_type' => get_class($model),
'stockable_id' => $model->id,
'type' => 'in',
'quantity' => $quantity,
'stock_before' => $toBefore,
'stock_after' => $toBefore + $quantity,
'stock_quality' => $toQuality,
'description' => $description,
]);
});
}
public function paginated(Model $model, int $perPage = 20): LengthAwarePaginator