feat: enhance service and controller logic for Cutting and Order management, implement best practices for method handling, and improve pagination defaults

This commit is contained in:
Yoga Pangestu 2026-06-28 16:23:57 +07:00
parent 7a1c4efe4a
commit b3790f2b2f
35 changed files with 317 additions and 684 deletions

View File

@ -284,6 +284,49 @@ ### Controller
}
```
### Service
- **Tugas Service:**
- Service menampung **semua logika bisnis** (query kompleks, transformasi data, transaksi, authorization bisnis).
- Controller hanya memanggil service dan mengembalikan response.
- **Urutan Method:**
1. `paginateForIndex` / method index/list
2. `findForEdit` / `findForShow` (jika ada)
3. `create` / `store`
4. `update`
5. `delete` / `destroy`
6. Method tambahan (custom action)
7. Method `private` di paling bawah
- **Docblock & Comment:**
- **Jangan** gunakan PHPDoc/docblock pada service.
- **Jangan** gunakan inline comment tipe `@var Model $variable`.
- **Pagination:**
- Default pagination untuk data table: **25** (`->paginate(25)`).
- **Update Model:**
- Gunakan `$model->update([...])`, **bukan** assign property lalu `$model->save()`.
- **Hindari:**
```php
$account->balance = $newBalance;
$account->save();
```
- **Gunakan:**
```php
$account->update(['balance' => $newBalance]);
```
- **Logic di Model/Enum, Bukan Wrapper di Service:**
- Method yang hanya meneruskan ke model/enum **wajib** ditempatkan di model/enum, bukan di service.
- **Contoh:** `isEditable()`, `transitionStatusMessage()`, `ensureEditable()` → enum/model.
- Service memanggil langsung: `$order->status->isEditable()`, `$status->transitionStatusMessage()`, `$order->ensureEditable()`.
- **Penamaan:**
- Variable, method, file **Bahasa Inggris**.
- Pesan error/flash ke user **Bahasa Indonesia**.
## Database & Migrasi
### Struktur File Migrasi

View File

@ -31,6 +31,17 @@ public function isEditable(): bool
return in_array($this, [self::IN_PROGRESS, self::REJECTED], true);
}
public function transitionStatusMessage(): string
{
return match ($this) {
self::COMPLETED => 'Proses cutting berhasil diselesaikan. Menunggu verifikasi admin toko.',
self::VERIFIED => 'Proses cutting berhasil diverifikasi. Stok produk telah diperbarui.',
self::REJECTED => 'Proses cutting berhasil ditolak.',
self::IN_PROGRESS => 'Proses cutting dikembalikan ke proses.',
default => 'Status proses cutting berhasil diperbarui.',
};
}
public function canTransitionTo(self $status): bool
{
return match ($this) {

View File

@ -29,6 +29,16 @@ public function isEditable(): bool
return in_array($this, [self::PENDING, self::PROCESSING], true);
}
public function transitionStatusMessage(): string
{
return match ($this) {
self::PROCESSING => 'Pesanan berhasil dikirim.',
self::COMPLETED => 'Pesanan berhasil diselesaikan.',
self::CANCELLED => 'Pesanan berhasil dibatalkan.',
default => 'Status pesanan berhasil diperbarui.',
};
}
public function canTransitionTo(self $status): bool
{
return match ($this) {

View File

@ -59,7 +59,7 @@ public function store(CuttingRequest $request): RedirectResponse
public function edit(Cutting $cutting): Response|RedirectResponse
{
try {
$this->cuttingService->ensureEditable($cutting);
$cutting->ensureEditable();
} catch (ValidationException $exception) {
$this->flashError($exception->validator->errors()->first('status'));
@ -105,7 +105,7 @@ public function transitionStatus(CuttingStatusTransitionRequest $request, Cuttin
$request->validated('result_prices'),
);
$this->flashSuccess($this->cuttingService->transitionStatusMessage($status));
$this->flashSuccess($status->transitionStatusMessage());
return redirect()->route('admin.manage.cuttings.index');
}

View File

@ -77,7 +77,7 @@ public function show(Order $order): Response
public function edit(Order $order): Response|RedirectResponse
{
try {
$this->orderService->ensureEditable($order);
$order->ensureEditable();
} catch (ValidationException $exception) {
$this->flashError($exception->validator->errors()->first('status'));
@ -119,7 +119,7 @@ public function transitionStatus(OrderStatusTransitionRequest $request, Order $o
$this->orderService->transitionStatus($order, $status);
$this->flashSuccess($this->orderService->transitionStatusMessage($status));
$this->flashSuccess($status->transitionStatusMessage());
return redirect()->route('admin.manage.orders.index');
}

View File

@ -15,6 +15,7 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Validation\ValidationException;
#[Guarded(['id'])]
#[Appends([
@ -79,7 +80,17 @@ public function statusLabel(): Attribute
);
}
// 5. Relation
// 5. Other Methods
public function ensureEditable(): void
{
if (! $this->status->isEditable()) {
throw ValidationException::withMessages([
'status' => 'Proses cutting tidak dapat diubah.',
]);
}
}
// 6. Relation
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by_id');

View File

@ -18,6 +18,7 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Validation\ValidationException;
use Spatie\MediaLibrary\HasMedia;
#[Guarded(['id'])]
@ -271,6 +272,15 @@ public static function mediaModuleName(): string
return 'order';
}
public function ensureEditable(): void
{
if (! $this->status->isEditable()) {
throw ValidationException::withMessages([
'status' => 'Pesanan tidak dapat diubah.',
]);
}
}
public function registerMediaCollections(): void
{
$this->addMediaCollection('photos');

View File

@ -3,7 +3,6 @@
namespace App\Services\Account;
use App\Models\User;
use App\Models\UserProfile;
use App\Services\Media\MediaService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
@ -16,9 +15,6 @@ public function __construct(
private readonly MediaService $mediaService,
) {}
/**
* @param array<string, mixed> $validated
*/
public function update(array $validated, User $user): void
{
try {
@ -28,7 +24,6 @@ public function update(array $validated, User $user): void
'username' => $validated['username'],
]);
/** @var UserProfile $profile */
$profile = $user->profile()->updateOrCreate(
['user_id' => $user->id],
[
@ -58,9 +53,6 @@ public function update(array $validated, User $user): void
}
}
/**
* Update user password.
*/
public function updatePassword(User $user, string $password): void
{
$user->update([

View File

@ -30,9 +30,7 @@ public function getDefaultAccount(): CashAccount
return CashAccount::query()->firstOrFail();
}
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
public function paginateForIndex(CashAccount $cashAccount, array $tableQuery, string $referenceType = ''): LengthAwarePaginator
{
$query = CashTransaction::query()
@ -55,7 +53,7 @@ public function paginateForIndex(CashAccount $cashAccount, array $tableQuery, st
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString()
->through(function (CashTransaction $transaction) {
$transaction->setAttribute(
@ -67,9 +65,7 @@ public function paginateForIndex(CashAccount $cashAccount, array $tableQuery, st
});
}
/**
* @param array{amount: int, description: string} $validated
*/
public function deposit(CashAccount $cashAccount, array $validated, User $user): CashTransaction
{
try {
@ -78,8 +74,7 @@ public function deposit(CashAccount $cashAccount, array $validated, User $user):
$amount = (int) $validated['amount'];
$newBalance = $account->balance + $amount;
$account->balance = $newBalance;
$account->save();
$account->update(['balance' => $newBalance]);
$transaction = CashTransaction::create([
'cash_account_id' => $account->id,
@ -116,9 +111,7 @@ public function deposit(CashAccount $cashAccount, array $validated, User $user):
return $transaction;
}
/**
* @param array{amount: int, description: string} $validated
*/
public function withdraw(CashAccount $cashAccount, array $validated, User $user): CashTransaction
{
try {
@ -134,8 +127,7 @@ public function withdraw(CashAccount $cashAccount, array $validated, User $user)
$newBalance = $account->balance - $amount;
$account->balance = $newBalance;
$account->save();
$account->update(['balance' => $newBalance]);
$transaction = CashTransaction::create([
'cash_account_id' => $account->id,
@ -193,8 +185,7 @@ public function recordOutgoing(
$newBalance = $account->balance - $amount;
$account->balance = $newBalance;
$account->save();
$account->update(['balance' => $newBalance]);
$transaction = new CashTransaction([
'cash_account_id' => $account->id,
@ -238,8 +229,7 @@ public function recordIncoming(
$newBalance = $account->balance + $amount;
$account->balance = $newBalance;
$account->save();
$account->update(['balance' => $newBalance]);
$transaction = new CashTransaction([
'cash_account_id' => $account->id,
@ -268,9 +258,7 @@ public function recordIncoming(
}
}
/**
* @param array{amount: int, description: string} $validated
*/
public function updateDeposit(CashTransaction $transaction, array $validated): void
{
$this->ensureEditable($transaction);
@ -279,9 +267,10 @@ public function updateDeposit(CashTransaction $transaction, array $validated): v
DB::transaction(function () use ($transaction, $validated): void {
CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id);
$transaction->amount = (int) $validated['amount'];
$transaction->description = $validated['description'];
$transaction->save();
$transaction->update([
'amount' => (int) $validated['amount'],
'description' => $validated['description'],
]);
$this->syncPhotos($transaction, $validated);
@ -365,9 +354,10 @@ public function updateReferencedTransaction(
DB::transaction(function () use ($transaction, $amount, $description): void {
CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id);
$transaction->amount = $amount;
$transaction->description = $description;
$transaction->save();
$transaction->update([
'amount' => $amount,
'description' => $description,
]);
$this->recalculateBalances($transaction->cashAccount);
@ -422,9 +412,7 @@ public function deleteReferencedTransaction(CashTransaction $transaction): void
}
}
/**
* @param array<string, mixed> $validated
*/
private function syncPhotos(CashTransaction $transaction, array $validated): void
{
$this->mediaService->syncCollection(
@ -466,12 +454,10 @@ private function recalculateBalances(CashAccount $cashAccount): void
$runningBalance -= $transaction->amount;
}
$transaction->balance_after = $runningBalance;
$transaction->saveQuietly();
$transaction->updateQuietly(['balance_after' => $runningBalance]);
}
$account->balance = $runningBalance;
$account->save();
$account->update(['balance' => $runningBalance]);
}
private function applySorting(Builder $query, string $sort, string $direction): void

View File

@ -25,9 +25,7 @@ public function __construct(
private readonly PushNotificationService $pushNotificationService,
) {}
/**
* @return array{outstanding_amount: int, outstanding_amount_formatted: string, outstanding_count: int}
*/
public function outstandingSummary(?User $user = null): array
{
$query = EmployeeAdvance::query()
@ -45,9 +43,7 @@ public function outstandingSummary(?User $user = null): array
];
}
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
public function paginateForIndex(array $tableQuery, User $user, string $status = ''): LengthAwarePaginator
{
$query = EmployeeAdvance::query()
@ -70,13 +66,11 @@ public function paginateForIndex(array $tableQuery, User $user, string $status =
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString();
}
/**
* @param array{amount: int, description: string, due_date: string} $validated
*/
public function create(array $validated, User $user): void
{
$employee = $this->resolveAuthenticatedEmployee($user);
@ -111,9 +105,7 @@ public function create(array $validated, User $user): void
);
}
/**
* @param array{amount: int, description: string, due_date: string} $validated
*/
public function update(EmployeeAdvance $employeeAdvance, array $validated, User $user): void
{
try {

View File

@ -23,9 +23,7 @@ public function __construct(
private readonly PushNotificationService $pushNotificationService,
) {}
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
{
$query = Expense::query()
@ -40,7 +38,7 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString()
->through(function (Expense $expense) {
$expense->setAttribute(
@ -52,9 +50,7 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
});
}
/**
* @param array{amount: int, description: string} $validated
*/
public function create(array $validated, User $user): void
{
try {
@ -103,9 +99,7 @@ public function create(array $validated, User $user): void
);
}
/**
* @param array{amount: int, description: string} $validated
*/
public function update(Expense $expense, array $validated): void
{
try {
@ -179,9 +173,7 @@ public function delete(Expense $expense): void
);
}
/**
* @param array<string, mixed> $validated
*/
private function syncPhotos(Expense $expense, array $validated): void
{
$this->mediaService->syncCollection(

View File

@ -29,9 +29,7 @@ public function __construct(
private readonly PushNotificationService $pushNotificationService,
) {}
/**
* @return Collection<int, PayrollPeriod>
*/
public function listPeriods(): Collection
{
return PayrollPeriod::query()
@ -57,9 +55,7 @@ public function resolvePeriod(?int $periodId): ?PayrollPeriod
->first();
}
/**
* @return array{total_amount: int, total_amount_formatted: string, total_count: int, status: string, status_label: string}
*/
public function periodSummary(PayrollPeriod $period, User $user): array
{
$query = Payroll::query()
@ -83,9 +79,7 @@ public function periodSummary(PayrollPeriod $period, User $user): array
];
}
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
public function paginateForPeriod(PayrollPeriod $period, array $tableQuery, User $user): LengthAwarePaginator
{
$query = Payroll::query()
@ -106,7 +100,7 @@ public function paginateForPeriod(PayrollPeriod $period, array $tableQuery, User
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString();
}
@ -212,9 +206,7 @@ public function generatePayrollsForPeriod(PayrollPeriod $period): void
}
}
/**
* @param array{type: string, amount: int, description: string} $validated
*/
public function addAdjustment(Payroll $payroll, array $validated, User $user): void
{
@ -404,9 +396,7 @@ public function pay(Payroll $payroll, User $user): void
}
}
/**
* @return Collection<int, Employee>
*/
private function payrollEligibleEmployees(PayrollPeriod $period): Collection
{
$periodStart = Carbon::create($period->year, $period->month, 1)->startOfMonth();

View File

@ -46,9 +46,7 @@ public function listForCalendar(
->get();
}
/**
* @return array<string, mixed>|null
*/
public function todayAttendanceForEmployee(Employee $employee): ?array
{
$attendance = Attendance::query()
@ -70,9 +68,7 @@ public function isOnLeaveToday(Employee $employee): bool
->exists();
}
/**
* @param array{photo: string, latitude: float, longitude: float} $validated
*/
public function checkIn(array $validated, User $user): void
{
$employee = $this->resolveAuthenticatedEmployee($user);
@ -111,9 +107,7 @@ public function checkIn(array $validated, User $user): void
);
}
/**
* @param array{photo: string, latitude: float, longitude: float} $validated
*/
public function checkOut(array $validated, User $user): void
{
$employee = $this->resolveAuthenticatedEmployee($user);

View File

@ -21,9 +21,7 @@ public function __construct(
private readonly MediaService $mediaService,
) {}
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
public function paginateForIndex(
array $tableQuery,
string $role = '',
@ -55,7 +53,7 @@ public function paginateForIndex(
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString();
}
@ -76,9 +74,7 @@ public function findForEdit(User $user): array
];
}
/**
* @param array<string, mixed> $validated
*/
public function create(array $validated): void
{
try {
@ -124,9 +120,7 @@ public function create(array $validated): void
}
}
/**
* @param array<string, mixed> $validated
*/
public function update(User $user, array $validated): void
{
$employee = $user->employee;
@ -138,7 +132,7 @@ public function update(User $user, array $validated): void
'username' => $validated['username'],
]);
/** @var UserProfile $profile */
$profile = $user->profile()->updateOrCreate(
['user_id' => $user->id],
[
@ -231,9 +225,7 @@ public function delete(User $user): void
}
}
/**
* @param array<string, mixed> $validated
*/
private function syncProfilePhoto(UserProfile $profile, array $validated): void
{
$newFiles = ! empty($validated['profile_photo']) ? [$validated['profile_photo']] : [];

View File

@ -23,9 +23,7 @@ public function __construct(
private readonly PushNotificationService $pushNotificationService,
) {}
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
public function paginateForIndex(array $tableQuery, User $user): LengthAwarePaginator
{
$query = LeaveRequest::query()
@ -46,13 +44,11 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString();
}
/**
* @param array{start_date: string, end_date: string} $validated
*/
public function create(array $validated, User $user): void
{
$employee = $this->resolveAuthenticatedEmployee($user);
@ -89,9 +85,7 @@ public function create(array $validated, User $user): void
);
}
/**
* @param array{start_date: string, end_date: string} $validated
*/
public function update(LeaveRequest $leaveRequest, array $validated, User $user): void
{
$startDate = Carbon::parse($validated['start_date'])->startOfDay();

View File

@ -20,9 +20,7 @@ public function resolve(int $productVariantId, PriceType $priceType): ?CuttingRe
->first();
}
/**
* @return list<CuttingResultPrice>
*/
public function latestPricesForVariant(int $productVariantId): array
{
$prices = [];
@ -38,14 +36,7 @@ public function latestPricesForVariant(int $productVariantId): array
return $prices;
}
/**
* Batch-fetch latest prices for multiple variant IDs in a single query.
* Returns a Collection keyed by product_variant_id, where each value is
* a Collection of CuttingResultPrice (one per price type, the latest).
*
* @param list<int> $variantIds
* @return Collection<int, Collection<int, CuttingResultPrice>>
*/
public function latestPricesForVariants(array $variantIds): Collection
{
if (empty($variantIds)) {

View File

@ -3,7 +3,6 @@
namespace App\Services\Manage;
use App\Enums\CuttingStatus;
use App\Enums\Permission;
use App\Models\Cutting;
use App\Models\CuttingMaterial;
use App\Models\CuttingResult;
@ -29,52 +28,6 @@ public function __construct(
private readonly PushNotificationService $pushNotificationService,
) {}
public function isEditable(CuttingStatus $status): bool
{
return $status->isEditable();
}
public function ensureEditable(Cutting $cutting): void
{
if (! $this->isEditable($cutting->status)) {
throw ValidationException::withMessages([
'status' => 'Proses cutting tidak dapat diubah.',
]);
}
}
public function transitionStatusMessage(CuttingStatus $status): string
{
return match ($status) {
CuttingStatus::COMPLETED => 'Proses cutting berhasil diselesaikan. Menunggu verifikasi admin toko.',
CuttingStatus::VERIFIED => 'Proses cutting berhasil diverifikasi. Stok produk telah diperbarui.',
CuttingStatus::REJECTED => 'Proses cutting berhasil ditolak.',
CuttingStatus::IN_PROGRESS => 'Proses cutting dikembalikan ke proses.',
default => 'Status proses cutting berhasil diperbarui.',
};
}
public function canTransitionTo(CuttingStatus $from, CuttingStatus $to): bool
{
return $from->canTransitionTo($to);
}
public function transitionPermission(CuttingStatus $status): Permission
{
return $status->transitionPermission();
}
/**
* @return list<array{status: string, label: string, destructive: bool, permission: string, icon_only: bool}>
*/
public function availableActions(CuttingStatus $status): array
{
return $status->availableActions();
}
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
public function paginateForIndex(array $tableQuery, User $user): LengthAwarePaginator
{
$query = Cutting::query()
@ -103,25 +56,23 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString()
->through(function (Cutting $cutting) use ($user) {
$actions = collect($this->availableActions($cutting->status))
$actions = collect($cutting->status->availableActions())
->filter(fn (array $action) => $user->can($action['permission']))
->values()
->all();
$cutting->setAttribute('available_actions', $actions);
$cutting->setAttribute('is_editable', $this->isEditable($cutting->status));
$cutting->setAttribute('is_editable', $cutting->status->isEditable());
$this->appendCostPreview($cutting);
return $cutting;
});
}
/**
* @return Collection<int, Cutting>
*/
public function getInProgressCuttings(User $user): Collection
{
return Cutting::query()
@ -136,20 +87,18 @@ public function getInProgressCuttings(User $user): Collection
->latest()
->get()
->each(function (Cutting $cutting) use ($user): void {
$actions = collect($this->availableActions($cutting->status))
$actions = collect($cutting->status->availableActions())
->filter(fn (array $action) => $user->can($action['permission']))
->values()
->all();
$cutting->setAttribute('available_actions', $actions);
$cutting->setAttribute('is_editable', $this->isEditable($cutting->status));
$cutting->setAttribute('is_editable', $cutting->status->isEditable());
$this->appendCostPreview($cutting);
});
}
/**
* @return Collection<int, Cutting>
*/
public function getCompletedCuttings(User $user): Collection
{
return Cutting::query()
@ -164,20 +113,18 @@ public function getCompletedCuttings(User $user): Collection
->latest()
->get()
->each(function (Cutting $cutting) use ($user): void {
$actions = collect($this->availableActions($cutting->status))
$actions = collect($cutting->status->availableActions())
->filter(fn (array $action) => $user->can($action['permission']))
->values()
->all();
$cutting->setAttribute('available_actions', $actions);
$cutting->setAttribute('is_editable', $this->isEditable($cutting->status));
$cutting->setAttribute('is_editable', $cutting->status->isEditable());
$this->appendCostPreview($cutting);
});
}
/**
* @return Collection<int, RawMaterial>
*/
public function rawMaterialCatalog(?Cutting $cutting = null, ?User $user = null): Collection
{
$selectedPriceIds = $cutting
@ -212,9 +159,7 @@ public function rawMaterialCatalog(?Cutting $cutting = null, ?User $user = null)
});
}
/**
* @return Collection<int, Product>
*/
public function productCatalog(?Cutting $cutting = null, ?User $user = null): Collection
{
$selectedVariantIds = $cutting
@ -284,9 +229,7 @@ public function findForEdit(Cutting $cutting): Cutting
return $cutting;
}
/**
* @return list<array<string, mixed>>
*/
public function draftMaterialsForUser(User $user): array
{
return $this->draftMaterialsQuery($user)
@ -300,9 +243,7 @@ public function draftMaterialsForUser(User $user): array
->all();
}
/**
* @return list<array<string, mixed>>
*/
public function draftResultsForUser(User $user): array
{
return $this->draftResultsQuery($user)
@ -316,10 +257,7 @@ public function draftResultsForUser(User $user): array
->all();
}
/**
* @param array<string, mixed> $validated
* @return array<string, mixed>
*/
public function syncDraftMaterial(array $validated, User $user): array
{
$price = RawMaterialPrice::query()
@ -353,10 +291,7 @@ public function syncDraftMaterial(array $validated, User $user): array
return $this->presentDraftMaterial($item);
}
/**
* @param array<string, mixed> $validated
* @return array<string, mixed>
*/
public function syncDraftResult(array $validated, User $user): array
{
$variant = ProductVariant::query()
@ -430,20 +365,18 @@ public function removeDraftResult(User $user, ProductVariant $productVariant): v
->delete();
}
/**
* @param array<string, mixed> $validated
*/
public function create(array $validated, User $user): Cutting
{
try {
$cutting = DB::transaction(function () use ($validated, $user): Cutting {
/** @var EloquentCollection<int, CuttingMaterial> $draftMaterials */
$draftMaterials = $this->draftMaterialsQuery($user)
->with('rawMaterialPrice.rawMaterial')
->lockForUpdate()
->get();
/** @var EloquentCollection<int, CuttingResult> $draftResults */
$draftResults = $this->draftResultsQuery($user)
->lockForUpdate()
->get();
@ -507,12 +440,10 @@ public function create(array $validated, User $user): Cutting
return $cutting;
}
/**
* @param array<string, mixed> $validated
*/
public function update(Cutting $cutting, array $validated): void
{
if (! $this->isEditable($cutting->status)) {
if (! $cutting->status->isEditable()) {
throw ValidationException::withMessages([
'status' => 'Proses cutting tidak dapat diubah.',
]);
@ -630,7 +561,7 @@ public function transitionStatus(
?array $results = null,
?array $resultPrices = null,
): void {
if (! $this->canTransitionTo($cutting->status, $status)) {
if (! $cutting->status->canTransitionTo($status)) {
throw ValidationException::withMessages([
'status' => 'Status proses cutting tidak dapat diubah.',
]);
@ -720,10 +651,7 @@ public function transitionStatus(
);
}
/**
* @param list<array{raw_material_price_id: int, material_usage: float|int|string}> $materials
* @return list<array{raw_material_price_id: int, material_usage: float}>
*/
private function buildMaterials(array $materials): array
{
return collect($materials)
@ -754,10 +682,7 @@ private function buildMaterials(array $materials): array
->all();
}
/**
* @param list<array{product_variant_id: int, cutting_result: int|string, sampel: int|string, hasil_cutting_diluar_sampel?: int|string}> $results
* @return list<array{product_variant_id: int, cutting_result: int, sampel: int, hasil_cutting_diluar_sampel: int}>
*/
private function buildResults(array $results): array
{
return collect($results)
@ -909,9 +834,7 @@ private function applySorting(Builder $query, string $sort, string $direction):
$query->latest();
}
/**
* @return Builder<CuttingMaterial>
*/
private function draftMaterialsQuery(User $user): Builder
{
return CuttingMaterial::query()
@ -919,9 +842,7 @@ private function draftMaterialsQuery(User $user): Builder
->where('user_id', $user->id);
}
/**
* @return Builder<CuttingResult>
*/
private function draftResultsQuery(User $user): Builder
{
return CuttingResult::query()
@ -929,9 +850,7 @@ private function draftResultsQuery(User $user): Builder
->where('user_id', $user->id);
}
/**
* @return array<string, mixed>
*/
private function presentDraftMaterial(CuttingMaterial $item): array
{
$price = $item->rawMaterialPrice;
@ -949,9 +868,7 @@ private function presentDraftMaterial(CuttingMaterial $item): array
];
}
/**
* @return array<string, mixed>
*/
private function presentDraftResult(CuttingResult $item): array
{
$variant = $item->productVariant;
@ -1019,9 +936,7 @@ public function calculateCostPerUnit(Cutting $cutting): int
return (int) round($this->calculateTotalProductionCost($cutting) / $totalPieces);
}
/**
* @param list<array{product_variant_id: int, prices: list<array{type: string, price: int}>}> $resultPrices
*/
private function storeResultPrices(Cutting $cutting, array $resultPrices): void
{
$costPerUnit = (int) ($cutting->cost_per_unit ?? $this->calculateCostPerUnit($cutting));

View File

@ -5,7 +5,6 @@
use App\Enums\OrderChannel;
use App\Enums\OrderStatus;
use App\Enums\PaymentType;
use App\Enums\Permission;
use App\Enums\PriceType;
use App\Enums\ProductStockQuality;
use App\Models\Customer;
@ -39,48 +38,6 @@ public function __construct(
private readonly MediaService $mediaService,
) {}
public function isEditable(OrderStatus $status): bool
{
return $status->isEditable();
}
public function ensureEditable(Order $order): void
{
if (! $this->isEditable($order->status)) {
throw ValidationException::withMessages([
'status' => 'Pesanan tidak dapat diubah.',
]);
}
}
public function transitionStatusMessage(OrderStatus $status): string
{
return match ($status) {
OrderStatus::PROCESSING => 'Pesanan berhasil dikirim.',
OrderStatus::COMPLETED => 'Pesanan berhasil diselesaikan.',
OrderStatus::CANCELLED => 'Pesanan berhasil dibatalkan.',
default => 'Status pesanan berhasil diperbarui.',
};
}
public function canTransitionTo(OrderStatus $from, OrderStatus $to): bool
{
return $from->canTransitionTo($to);
}
public function transitionPermission(OrderStatus $status): Permission
{
return $status->transitionPermission();
}
/**
* @return list<array{status: string, label: string, destructive: bool, permission: string, icon_only: bool}>
*/
public function availableActions(OrderStatus $status): array
{
return $status->availableActions();
}
public function defaultPriceType(OrderChannel $channel): ?PriceType
{
return match ($channel) {
@ -99,9 +56,7 @@ public function stockColumn(ProductStockQuality $quality): string
};
}
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
public function paginateForIndex(array $tableQuery, User $user): LengthAwarePaginator
{
$query = Order::query()
@ -131,24 +86,22 @@ public function paginateForIndex(array $tableQuery, User $user): LengthAwarePagi
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString()
->through(function (Order $order) use ($user) {
$actions = collect($this->availableActions($order->status))
$actions = collect($order->status->availableActions())
->filter(fn (array $action) => $user->can($action['permission']))
->values()
->all();
$order->setAttribute('available_actions', $actions);
$order->setAttribute('is_editable', $this->isEditable($order->status));
$order->setAttribute('is_editable', $order->status->isEditable());
return $order;
});
}
/**
* @return list<array{value: int, label: string}>
*/
public function customerOptions(): array
{
return Customer::query()
@ -161,9 +114,7 @@ public function customerOptions(): array
->all();
}
/**
* @return list<array{value: int, label: string}>
*/
public function marketingOptions(): array
{
return User::query()
@ -179,9 +130,7 @@ public function marketingOptions(): array
->all();
}
/**
* @return list<array{value: string, label: string}>
*/
public function storePriceTypeOptions(): array
{
return collect(PriceType::cases())
@ -194,9 +143,7 @@ public function storePriceTypeOptions(): array
->all();
}
/**
* @return Collection<int, Product>
*/
public function catalogItems(?Order $order = null, ?User $user = null): Collection
{
$orderVariantIds = $order
@ -318,19 +265,17 @@ public function findForShow(Order $order): Order
}
});
$availableActions = collect($this->availableActions($order->status))
$availableActions = collect($order->status->availableActions())
->values()
->all();
$order->setAttribute('available_actions', $availableActions);
$order->setAttribute('is_editable', $this->isEditable($order->status));
$order->setAttribute('is_editable', $order->status->isEditable());
return $order;
}
/**
* @return list<array<string, mixed>>
*/
public function draftItemsForUser(User $user): array
{
return $this->draftItemsQuery($user)
@ -344,10 +289,7 @@ public function draftItemsForUser(User $user): array
->all();
}
/**
* @param array<string, mixed> $validated
* @return array<string, mixed>
*/
public function syncDraftItem(array $validated, User $user): array
{
// Force retail price type and stock quality for cashier role
@ -405,9 +347,7 @@ public function removeDraftItem(User $user, ProductVariant $productVariant, Prod
->delete();
}
/**
* @return list<array<string, mixed>>
*/
public function resyncDraftPrices(User $user, string $priceTypeValue): array
{
// Force retail price type for cashier role
@ -440,9 +380,7 @@ public function resyncDraftPrices(User $user, string $priceTypeValue): array
return $this->draftItemsForUser($user);
}
/**
* @param array<string, mixed> $validated
*/
public function create(array $validated, User $user): Order
{
try {
@ -457,7 +395,7 @@ public function create(array $validated, User $user): Order
$order = DB::transaction(function () use ($validated, $user): Order {
$priceType = $this->resolvePriceType($validated['channel'], $validated['price_type']);
/** @var EloquentCollection<int, OrderItem> $draftItems */
$draftItems = $this->draftItemsQuery($user)
->lockForUpdate()
->get();
@ -563,16 +501,10 @@ public function create(array $validated, User $user): Order
return $order;
}
/**
* @param array<string, mixed> $validated
*/
public function update(Order $order, array $validated): void
{
if (! $this->isEditable($order->status)) {
throw ValidationException::withMessages([
'status' => 'Pesanan tidak dapat diubah.',
]);
}
$order->ensureEditable();
try {
DB::transaction(function () use ($order, $validated): void {
@ -591,36 +523,38 @@ public function update(Order $order, array $validated): void
$negoPrice = isset($validated['nego_price']) && $validated['nego_price'] !== '' ? (int) $validated['nego_price'] : null;
$totalAmount = $negoPrice !== null ? max($negoPrice, 0) : max($subtotal - $discount, 0);
$channel = OrderChannel::from($validated['channel']);
$order->customer_id = $validated['customer_id'] ?? null;
$order->marketing_id = $validated['marketing_id'] ?? null;
$order->channel = $channel;
$order->price_type = $priceType;
$order->payment_type = PaymentType::from($validated['payment_type']);
$order->is_affiliate = $validated['is_affiliate'] ?? false;
$order->tiktok_order_id = $validated['tiktok_order_id'] ?? null;
$order->shopee_order_id = $validated['shopee_order_id'] ?? null;
$order->subtotal = $subtotal;
$order->discount = $discount;
$order->nego_price = $negoPrice;
$order->marketplace_settings_snapshot = $this->marketplaceService->buildOrderSnapshot(
$channel,
$totalAmount,
$this->lineItemsForSnapshot($lineItems),
$validated['is_affiliate'] ?? false,
);
$order->total_amount = $totalAmount;
$order->notes = $validated['notes'] ?? null;
$status = $order->status;
if (isset($validated['status'])) {
$newStatus = OrderStatus::from($validated['status']);
if ($order->status->canTransitionTo($newStatus) || $newStatus === $order->status) {
$order->status = $newStatus;
$status = $newStatus;
}
}
$order->save();
$order->update([
'customer_id' => $validated['customer_id'] ?? null,
'marketing_id' => $validated['marketing_id'] ?? null,
'channel' => $channel,
'price_type' => $priceType,
'payment_type' => PaymentType::from($validated['payment_type']),
'is_affiliate' => $validated['is_affiliate'] ?? false,
'tiktok_order_id' => $validated['tiktok_order_id'] ?? null,
'shopee_order_id' => $validated['shopee_order_id'] ?? null,
'subtotal' => $subtotal,
'discount' => $discount,
'nego_price' => $negoPrice,
'marketplace_settings_snapshot' => $this->marketplaceService->buildOrderSnapshot(
$channel,
$totalAmount,
$this->lineItemsForSnapshot($lineItems),
$validated['is_affiliate'] ?? false,
),
'total_amount' => $totalAmount,
'notes' => $validated['notes'] ?? null,
'status' => $status,
]);
$this->syncPhotos($order, $validated);
@ -673,7 +607,7 @@ public function delete(Order $order): void
DB::transaction(function () use ($order): void {
$order->load('items');
if ($this->isEditable($order->status)) {
if ($order->status->isEditable()) {
foreach ($order->items as $item) {
$this->incrementStock($item);
}
@ -708,7 +642,7 @@ public function delete(Order $order): void
public function transitionStatus(Order $order, OrderStatus $status): void
{
if (! $this->canTransitionTo($order->status, $status)) {
if (! $order->status->canTransitionTo($status)) {
throw ValidationException::withMessages([
'status' => 'Status pesanan tidak dapat diubah.',
]);
@ -752,10 +686,7 @@ public function transitionStatus(Order $order, OrderStatus $status): void
);
}
/**
* @param list<array{product_variant_id: int, quantity: int|string, stock_quality?: string}> $items
* @return list<array{product_variant_id: int, stock_quality: string, quantity: int, unit_price: int, subtotal: int}>
*/
private function buildLineItems(array $items, PriceType $priceType): array
{
return collect($items)
@ -814,9 +745,7 @@ private function resolvePriceType(string $channel, string $priceType): PriceType
return $priceTypeEnum;
}
/**
* @return Builder<OrderItem>
*/
private function draftItemsQuery(User $user): Builder
{
return OrderItem::query()
@ -824,9 +753,7 @@ private function draftItemsQuery(User $user): Builder
->where('user_id', $user->id);
}
/**
* @return array<string, mixed>
*/
private function presentDraftItem(OrderItem $item): array
{
$variant = $item->productVariant;
@ -843,9 +770,7 @@ private function presentDraftItem(OrderItem $item): array
];
}
/**
* @param EloquentCollection<int, OrderItem> $items
*/
private function applyDraftPrices(EloquentCollection $items, PriceType $priceType): void
{
foreach ($items as $index => $item) {
@ -884,9 +809,7 @@ private function availableStock(ProductVariant $variant, ProductStockQuality $st
return (int) $variant->{$this->stockColumn($stockQuality)};
}
/**
* @param array<string, mixed> $validated
*/
private function syncPhotos(Order $order, array $validated): void
{
$paymentType = $validated['payment_type'] ?? null;
@ -903,10 +826,7 @@ private function syncPhotos(Order $order, array $validated): void
);
}
/**
* @param EloquentCollection<int, OrderItem>|list<array{quantity: int, subtotal: int}> $items
* @return list<array{quantity: int, subtotal: int}>
*/
private function lineItemsForSnapshot(EloquentCollection|array $items): array
{
return collect($items)
@ -942,17 +862,7 @@ private function resolveUnitPrice(int $variantId, PriceType $priceType, ?string
return (int) $price->price;
}
/**
* @return list<array{
* type: string,
* type_label: string,
* price: int,
* price_formatted: string,
* price_input: string,
* cost_per_unit: int,
* cost_per_unit_formatted: string,
* }>
*/
private function presentVariantPrices(int $variantId): array
{
return collect($this->cuttingResultPriceResolver->latestPricesForVariant($variantId))

View File

@ -47,9 +47,7 @@ public function hasPendingMarketplaceVerification(): bool
->exists();
}
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
public function paginateForIndex(
User $user,
array $tableQuery,
@ -57,7 +55,7 @@ public function paginateForIndex(
string $subjectType = '',
string $action = '',
): LengthAwarePaginator {
$perPage = 10;
$perPage = 25;
$page = Paginator::resolveCurrentPage();
$includeCuttings = $this->shouldIncludeCuttings($user, $status, $subjectType, $action);
$cuttingRows = $includeCuttings
@ -103,9 +101,7 @@ public function paginateForIndex(
->through(fn (OwnerVerificationRequest $request) => $this->presentRequestRow($request));
}
/**
* @return Collection<int, array<string, mixed>>
*/
private function pendingCuttingRows(User $user, array $tableQuery): Collection
{
$rows = $this->stockService
@ -147,9 +143,7 @@ public function pendingCountForUser(User $user): int
return $requestCount + Cutting::query()->pendingVerification()->count();
}
/**
* @return list<array{value: string, label: string}>
*/
public function subjectTypeOptions(User $user): array
{
$options = OwnerVerificationRequest::query()
@ -323,9 +317,7 @@ private function clearVerificationRequestMedia(OwnerVerificationRequest $request
};
}
/**
* @return array<string, mixed>
*/
private function presentRequestRow(OwnerVerificationRequest $request): array
{
$payload = is_array($request->payload) ? $request->payload : [];
@ -355,9 +347,7 @@ private function presentRequestRow(OwnerVerificationRequest $request): array
];
}
/**
* @return array<string, mixed>
*/
private function presentCuttingRow(Cutting $cutting): array
{
$totalPieces = $cutting->results->sum('cutting_result');
@ -389,9 +379,7 @@ private function presentCuttingRow(Cutting $cutting): array
];
}
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
private function buildVerificationRequestQuery(
User $user,
array $tableQuery,
@ -466,9 +454,7 @@ private function shouldIncludeCuttings(
return Cutting::query()->pendingVerification()->exists();
}
/**
* @param Collection<int, array<string, mixed>> $items
*/
private function makePaginator(
Collection $items,
int $total,

View File

@ -31,9 +31,7 @@ public function __construct(
private readonly PushNotificationService $pushNotificationService,
) {}
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
{
$query = Purchase::query()
@ -59,7 +57,7 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString()
->through(function (Purchase $purchase) {
$purchase->setAttribute(
@ -78,9 +76,7 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
});
}
/**
* @return list<array{value: int, label: string}>
*/
public function supplierOptions(): array
{
return Supplier::query()
@ -93,9 +89,7 @@ public function supplierOptions(): array
->all();
}
/**
* @return Collection<int, RawMaterial>
*/
public function catalogItems(?Purchase $purchase = null, ?User $user = null): Collection
{
$purchasePriceIds = $purchase
@ -156,9 +150,7 @@ public function findForEdit(Purchase $purchase): Purchase
return $purchase;
}
/**
* @return list<array<string, mixed>>
*/
public function draftItemsForUser(User $user): array
{
return $this->draftItemsQuery($user)
@ -172,10 +164,7 @@ public function draftItemsForUser(User $user): array
->all();
}
/**
* @param array<string, mixed> $validated
* @return array<string, mixed>
*/
public function syncDraftItem(array $validated, User $user): array
{
$price = RawMaterialPrice::query()
@ -216,14 +205,12 @@ public function removeDraftItem(User $user, RawMaterialPrice $rawMaterialPrice):
->delete();
}
/**
* @param array<string, mixed> $validated
*/
public function create(array $validated, User $user): Purchase
{
try {
$purchase = DB::transaction(function () use ($validated, $user): Purchase {
/** @var EloquentCollection<int, PurchaseItem> $draftItems */
$draftItems = $this->draftItemsQuery($user)
->lockForUpdate()
->get();
@ -295,9 +282,7 @@ public function create(array $validated, User $user): Purchase
return $purchase;
}
/**
* @param array<string, mixed> $validated
*/
public function update(Purchase $purchase, array $validated, User $user): void
{
$purchase->load(['supplier', 'items.rawMaterialPrice.rawMaterial:id,name']);
@ -449,10 +434,7 @@ public function applyDelete(OwnerVerificationRequest $verificationRequest): void
$this->executeDelete($purchase);
}
/**
* @param list<array{raw_material_price_id: int, quantity: float|int|string}> $items
* @return list<array{raw_material_price_id: int, quantity: float, unit_price: int, subtotal: int}>
*/
private function buildLineItems(array $items): array
{
return collect($items)
@ -479,9 +461,7 @@ private function buildLineItems(array $items): array
->all();
}
/**
* @param array<string, mixed> $validated
*/
private function syncPhotos(Purchase $purchase, array $validated): void
{
$this->mediaService->syncCollection(
@ -495,9 +475,7 @@ private function syncPhotos(Purchase $purchase, array $validated): void
);
}
/**
* @return Builder<PurchaseItem>
*/
private function draftItemsQuery(User $user): Builder
{
return PurchaseItem::query()
@ -505,9 +483,7 @@ private function draftItemsQuery(User $user): Builder
->where('user_id', $user->id);
}
/**
* @return array<string, mixed>
*/
private function presentDraftItem(PurchaseItem $item): array
{
$price = $item->rawMaterialPrice;
@ -585,9 +561,7 @@ private function executeDelete(Purchase $purchase): void
});
}
/**
* @param array<string, mixed> $payload
*/
private function applyPayloadToPurchase(
Purchase $purchase,
array $payload,
@ -627,9 +601,7 @@ private function applyPayloadToPurchase(
});
}
/**
* @return array<string, mixed>
*/
private function snapshotPurchase(Purchase $purchase): array
{
$purchase->load([
@ -659,10 +631,7 @@ private function snapshotPurchase(Purchase $purchase): array
];
}
/**
* @param array<string, mixed> $validated
* @return array<string, mixed>
*/
private function buildPayloadFromValidated(array $validated): array
{
$lineItems = $this->enrichLineItems($this->buildLineItems($validated['items']));
@ -685,10 +654,7 @@ private function buildPayloadFromValidated(array $validated): array
];
}
/**
* @param list<array{raw_material_price_id: int, quantity: float, unit_price: int, subtotal: int}> $lineItems
* @return list<array<string, mixed>>
*/
private function enrichLineItems(array $lineItems): array
{
$prices = RawMaterialPrice::query()
@ -709,9 +675,7 @@ private function enrichLineItems(array $lineItems): array
->all();
}
/**
* @param array<string, mixed> $validated
*/
private function syncRequestPhotos(OwnerVerificationRequest $verificationRequest, array $validated): void
{
$this->mediaService->syncCollection(
@ -725,9 +689,7 @@ private function syncRequestPhotos(OwnerVerificationRequest $verificationRequest
);
}
/**
* @param array<string, mixed> $payload
*/
private function applyRequestPhotos(
OwnerVerificationRequest $verificationRequest,
Purchase $purchase,
@ -748,9 +710,7 @@ private function applyRequestPhotos(
}
}
/**
* @return array<string, mixed>
*/
private function payloadNew(OwnerVerificationRequest $verificationRequest): array
{
$payload = $verificationRequest->payload ?? [];

View File

@ -19,9 +19,7 @@ public function __construct(
private readonly PushNotificationService $pushNotificationService,
) {}
/**
* Submit a stock retail transfer request for owner verification.
*/
public function transfer(int $variantId, int $quantity, User $user, ?string $notes = null): OwnerVerificationRequest
{
if ($quantity <= 0) {
@ -102,9 +100,7 @@ public function transfer(int $variantId, int $quantity, User $user, ?string $not
}
}
/**
* Apply the stock retail transfer when owner approves.
*/
public function applyStockRetailTransfer(OwnerVerificationRequest $request): void
{
$payload = $request->payload ?? [];
@ -141,9 +137,7 @@ public function applyStockRetailTransfer(OwnerVerificationRequest $request): voi
});
}
/**
* Reject the stock retail transfer (no-op since nothing changed).
*/
public function rejectStockRetailTransfer(OwnerVerificationRequest $request): void
{
// No-op: nothing was changed yet, so nothing to rollback.

View File

@ -20,11 +20,7 @@ public function __construct(
private readonly PushNotificationService $pushNotificationService,
) {}
/**
* Get all completed cuttings pending verification.
*
* @return Collection<int, Cutting>
*/
public function getPendingVerificationCuttings(User $user): Collection
{
return Cutting::query()
@ -43,11 +39,7 @@ public function getPendingVerificationCuttings(User $user): Collection
});
}
/**
* Get all cuttings pending owner approval.
*
* @return Collection<int, Cutting>
*/
public function getPendingApprovalCuttings(User $user): Collection
{
return Cutting::query()
@ -68,12 +60,7 @@ public function getPendingApprovalCuttings(User $user): Collection
});
}
/**
* Submit verification for a completed cutting - saves data but does NOT add stock yet.
*
* @param list<array{product_variant_id: int, sampel: int, hasil_cutting_diluar_sampel: int}>|null $results
* @param list<array{product_variant_id: int, prices: list<array{type: string, price: int}>}>|null $resultPrices
*/
public function submitVerification(
Cutting $cutting,
User $user,
@ -138,9 +125,7 @@ public function submitVerification(
);
}
/**
* Owner approves verification - adds stock to store.
*/
public function approveVerification(
Cutting $cutting,
User $user,
@ -191,9 +176,7 @@ public function approveVerification(
);
}
/**
* Owner rejects verification - sends back to completed.
*/
public function rejectVerification(
Cutting $cutting,
User $user,
@ -262,9 +245,7 @@ private function applyProductStockOnVerify(Cutting $cutting): void
}
}
/**
* @param list<array{product_variant_id: int, prices: list<array{type: string, price: int}>}> $resultPrices
*/
private function storeResultPrices(Cutting $cutting, array $resultPrices): void
{
// Extract harga_modal from the first variant's prices and set as cost_per_unit
@ -304,9 +285,7 @@ private function storeResultPrices(Cutting $cutting, array $resultPrices): void
}
}
/**
* Update product_prices from cutting result prices (called on owner approval).
*/
private function applyResultPricesToProducts(Cutting $cutting): void
{
$resultPrices = $cutting->resultPrices()->with('productVariant')->get();

View File

@ -19,9 +19,7 @@ public function __construct(
private readonly PushNotificationService $pushNotificationService,
) {}
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
public function paginateForIndex(array $tableQuery): LengthAwarePaginator
{
$query = StokOpname::query()
@ -42,13 +40,11 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString();
}
/**
* Get product variants with current stock for the opname form.
*/
public function catalogItems(): array
{
return Product::query()
@ -72,9 +68,7 @@ public function catalogItems(): array
->toArray();
}
/**
* Get stok opname with items for editing.
*/
public function findForEdit(StokOpname $stokOpname): array
{
$stokOpname->load(['items.productVariant.product', 'createdBy.profile']);
@ -101,9 +95,7 @@ public function findForEdit(StokOpname $stokOpname): array
];
}
/**
* @param array{opname_date: string, notes: string|null, items: list<array{product_variant_id: int, physical_stock: int, notes: string|null}>} $validated
*/
public function create(array $validated, User $user): StokOpname
{
try {
@ -132,9 +124,7 @@ public function create(array $validated, User $user): StokOpname
}
}
/**
* @param array{opname_date: string, notes: string|null, items: list<array{product_variant_id: int, physical_stock: int, notes: string|null}>} $validated
*/
public function update(StokOpname $stokOpname, array $validated): void
{
if ($stokOpname->status !== StokOpnameStatus::DRAFT && $stokOpname->status !== StokOpnameStatus::REJECTED) {
@ -192,9 +182,7 @@ public function delete(StokOpname $stokOpname): void
}
}
/**
* Submit stok opname for verification.
*/
public function submit(StokOpname $stokOpname, User $user): void
{
if ($stokOpname->status !== StokOpnameStatus::DRAFT && $stokOpname->status !== StokOpnameStatus::REJECTED) {
@ -235,9 +223,7 @@ public function submit(StokOpname $stokOpname, User $user): void
);
}
/**
* Verify stok opname - apply stock adjustments.
*/
public function verify(StokOpname $stokOpname, User $user, ?string $verificationNotes = null): void
{
if ($stokOpname->status !== StokOpnameStatus::PENDING) {
@ -282,9 +268,7 @@ public function verify(StokOpname $stokOpname, User $user, ?string $verification
);
}
/**
* Reject stok opname.
*/
public function reject(StokOpname $stokOpname, User $user, string $reason): void
{
if ($stokOpname->status !== StokOpnameStatus::PENDING) {
@ -321,11 +305,7 @@ public function reject(StokOpname $stokOpname, User $user, string $reason): void
);
}
/**
* Auto-save: create or update a draft silently.
*
* @param array{stok_opname_id?: int|null, opname_date: string, notes: string|null, items: list<array{product_variant_id: int, physical_stock: int|null, notes: string|null}>|null} $validated
*/
public function autoSave(array $validated, User $user): StokOpname
{
try {
@ -364,9 +344,7 @@ public function autoSave(array $validated, User $user): StokOpname
}
}
/**
* @param list<array{product_variant_id: int, physical_stock: int, notes: string|null}> $items
*/
private function syncItems(StokOpname $stokOpname, array $items): void
{
$stokOpname->items()->delete();

View File

@ -21,7 +21,7 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString();
}

View File

@ -23,7 +23,7 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString();
}

View File

@ -27,9 +27,7 @@ public function __construct(
private readonly PushNotificationService $pushNotificationService,
) {}
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
public function paginateForIndex(array $tableQuery, string $isActive, string $categoryId = '', string $stockStatus = ''): LengthAwarePaginator
{
$query = Product::query()
@ -64,7 +62,7 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $ca
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString()
->through(function (Product $product) {
$product->variants->each(function (ProductVariant $variant): void {
@ -116,9 +114,7 @@ public function findForEdit(Product $product): Product
return $product;
}
/**
* @param array<string, mixed> $validated
*/
public function create(array $validated, User $user): void
{
try {
@ -173,9 +169,7 @@ public function create(array $validated, User $user): void
);
}
/**
* @param array<string, mixed> $validated
*/
public function update(Product $product, array $validated, User $user): void
{
try {
@ -249,9 +243,7 @@ public function delete(Product $product, User $user): void
);
}
/**
* @param array<string, mixed> $validated
*/
public function toggleStatus(Product $product, array $validated, User $user): void
{
try {
@ -356,9 +348,7 @@ public function applyUpdate(OwnerVerificationRequest $verificationRequest): void
$this->applyPayloadToProduct($product, $this->payloadNew($verificationRequest), $verificationRequest);
}
/**
* @param array<string, mixed> $payload
*/
private function applyPayloadToProduct(
Product $product,
array $payload,
@ -510,9 +500,7 @@ private function rollbackUpdate(OwnerVerificationRequest $verificationRequest):
$this->applyPayloadToProduct($product, $oldPayload);
}
/**
* @return array<string, mixed>
*/
private function payloadOld(OwnerVerificationRequest $verificationRequest): array
{
$payload = $verificationRequest->payload ?? [];
@ -524,9 +512,7 @@ private function payloadOld(OwnerVerificationRequest $verificationRequest): arra
return [];
}
/**
* @return array<string, mixed>
*/
private function payloadNew(OwnerVerificationRequest $verificationRequest): array
{
$payload = $verificationRequest->payload ?? [];
@ -538,9 +524,7 @@ private function payloadNew(OwnerVerificationRequest $verificationRequest): arra
return $payload;
}
/**
* @return array<string, mixed>
*/
private function snapshotProduct(Product $product): array
{
$product->load(['categories', 'variants']);
@ -561,10 +545,7 @@ private function snapshotProduct(Product $product): array
]);
}
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
private function enrichPayload(array $data): array
{
$categoryIds = $data['category_ids'] ?? [];
@ -576,10 +557,7 @@ private function enrichPayload(array $data): array
return $data;
}
/**
* @param array<string, mixed> $validated
* @return array<string, mixed>
*/
private function buildPayloadFromValidated(array $validated): array
{
return [
@ -598,9 +576,7 @@ private function buildPayloadFromValidated(array $validated): array
];
}
/**
* @param array<string, mixed> $variantData
*/
private function syncVariantImages(
ProductVariant $variant,
array $variantData,
@ -617,9 +593,7 @@ private function syncVariantImages(
);
}
/**
* @param array<string, mixed> $variantData
*/
private function syncRequestVariantImages(
OwnerVerificationRequest $verificationRequest,
array $variantData,
@ -639,9 +613,7 @@ private function syncRequestVariantImages(
);
}
/**
* @param array<string, mixed> $variantData
*/
private function applyVariantImageChanges(
OwnerVerificationRequest $verificationRequest,
ProductVariant $variant,

View File

@ -27,9 +27,7 @@ public function __construct(
private readonly PushNotificationService $pushNotificationService,
) {}
/**
* @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery
*/
public function paginateForIndex(array $tableQuery, string $isActive, string $stockStatus = ''): LengthAwarePaginator
{
$query = RawMaterial::query()
@ -72,7 +70,7 @@ public function paginateForIndex(array $tableQuery, string $isActive, string $st
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString()
->through(function (RawMaterial $rawMaterial) {
$rawMaterial->prices->each(function (RawMaterialPrice $price): void {
@ -110,9 +108,7 @@ public function findForEdit(RawMaterial $rawMaterial): RawMaterial
return $rawMaterial;
}
/**
* @param array<string, mixed> $validated
*/
public function create(array $validated, User $user): void
{
try {
@ -159,9 +155,7 @@ public function create(array $validated, User $user): void
);
}
/**
* @param array<string, mixed> $validated
*/
public function update(RawMaterial $rawMaterial, array $validated, User $user): void
{
try {
@ -235,9 +229,7 @@ public function delete(RawMaterial $rawMaterial, User $user): void
);
}
/**
* @param array<string, mixed> $validated
*/
public function toggleStatus(RawMaterial $rawMaterial, array $validated, User $user): void
{
try {
@ -395,9 +387,7 @@ public function applyToggleStatus(OwnerVerificationRequest $verificationRequest)
]);
}
/**
* @param array<string, mixed> $payload
*/
private function applyPayloadToRawMaterial(
RawMaterial $rawMaterial,
array $payload,
@ -492,9 +482,7 @@ private function rollbackUpdate(OwnerVerificationRequest $verificationRequest):
$this->applyPayloadToRawMaterial($rawMaterial, $oldPayload);
}
/**
* @return array<string, mixed>
*/
private function payloadOld(OwnerVerificationRequest $verificationRequest): array
{
$payload = $verificationRequest->payload ?? [];
@ -506,9 +494,7 @@ private function payloadOld(OwnerVerificationRequest $verificationRequest): arra
return [];
}
/**
* @return array<string, mixed>
*/
private function payloadNew(OwnerVerificationRequest $verificationRequest): array
{
$payload = $verificationRequest->payload ?? [];
@ -520,9 +506,7 @@ private function payloadNew(OwnerVerificationRequest $verificationRequest): arra
return $payload;
}
/**
* @return array<string, mixed>
*/
private function snapshotRawMaterial(RawMaterial $rawMaterial): array
{
$rawMaterial->load(['prices']);
@ -542,10 +526,7 @@ private function snapshotRawMaterial(RawMaterial $rawMaterial): array
]);
}
/**
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
private function enrichPayload(array $data): array
{
if (isset($data['unit'])) {
@ -560,10 +541,7 @@ private function enrichPayload(array $data): array
return $data;
}
/**
* @param array<string, mixed> $validated
* @return array<string, mixed>
*/
private function buildPayloadFromValidated(array $validated): array
{
return [
@ -592,9 +570,7 @@ private function applySorting(Builder $query, string $sort, string $direction):
$query->latest();
}
/**
* @param array<string, mixed> $priceData
*/
private function createPrice(RawMaterial $rawMaterial, array $priceData, int $index): RawMaterialPrice
{
$price = $rawMaterial->prices()->create([
@ -608,9 +584,7 @@ private function createPrice(RawMaterial $rawMaterial, array $priceData, int $in
return $price;
}
/**
* @param array<string, mixed> $priceData
*/
private function syncPriceImages(RawMaterialPrice $price, array $priceData, int $index): void
{
$this->mediaService->syncCollection(
@ -624,9 +598,7 @@ private function syncPriceImages(RawMaterialPrice $price, array $priceData, int
);
}
/**
* @param array<string, mixed> $priceData
*/
private function syncRequestPriceImages(
OwnerVerificationRequest $verificationRequest,
array $priceData,
@ -646,9 +618,7 @@ private function syncRequestPriceImages(
);
}
/**
* @param array<string, mixed> $priceData
*/
private function applyPriceImageChanges(
OwnerVerificationRequest $verificationRequest,
RawMaterialPrice $price,

View File

@ -23,7 +23,7 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString();
}

View File

@ -12,10 +12,7 @@
class MediaService
{
/**
* @param list<UploadedFile>|null $newFiles
* @param list<int>|null $removeIds
*/
public function syncCollection(
HasMedia $model,
string $collection,
@ -118,9 +115,7 @@ public function replaceSingleFile(
return $this->addUploadedFile($model, $file, $collection, $type);
}
/**
* @return array{module: string, type?: string}
*/
private function customProperties(HasMedia $model, ?string $type): array
{
$properties = [

View File

@ -36,7 +36,7 @@ public function paginateForIndex(array $tableQuery, string $event = '', string $
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString()
->through(fn (Activity $activity) => [
'id' => $activity->id,

View File

@ -19,9 +19,7 @@
class DashboardService
{
/**
* @return array<string, mixed>|null
*/
public function getTodayAttendanceForUser(User $user): ?array
{
$employee = $user->employee;

View File

@ -6,27 +6,19 @@
class PushNotificationService
{
/**
* Send notification to users with specific roles.
*
* @param list<string> $roles
*/
public function sendToRoles(string $title, string $body, array $roles, string $url = '/admin/dashboard'): void
{
SendPushNotificationJob::dispatch($title, $body, $url, $roles);
}
/**
* Send notification to a specific user.
*/
public function sendToUser(string $title, string $body, int $userId, string $url = '/admin/dashboard'): void
{
SendPushNotificationJob::dispatch($title, $body, $url, [], $userId);
}
/**
* Send notification to all users.
*/
public function sendToAll(string $title, string $body, string $url = '/admin/dashboard'): void
{
SendPushNotificationJob::dispatch($title, $body, $url, []);

View File

@ -6,11 +6,7 @@
class PushSubscriptionService
{
/**
* Store or update a push subscription.
*
* @param array<string, mixed> $validated
*/
public function updateOrCreateSubscription(array $validated, int $userId): void
{
PushSubscription::updateOrCreate(
@ -25,9 +21,7 @@ public function updateOrCreateSubscription(array $validated, int $userId): void
);
}
/**
* Delete a push subscription.
*/
public function deleteSubscription(string $endpoint, int $userId): void
{
PushSubscription::where('user_id', $userId)

View File

@ -28,7 +28,7 @@ public function paginateForIndex(array $tableQuery): LengthAwarePaginator
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
return $query
->paginate(10)
->paginate(25)
->withQueryString();
}

View File

@ -20,9 +20,7 @@ public function __construct(
private readonly PushNotificationService $pushNotificationService,
) {}
/**
* @return list<string>
*/
private function tiktokFeeKeys(): array
{
return [
@ -35,9 +33,7 @@ private function tiktokFeeKeys(): array
];
}
/**
* @return list<string>
*/
private function shopeeFeeKeys(): array
{
return [
@ -157,10 +153,7 @@ public function applyVerificationRequest(OwnerVerificationRequest $request): voi
$this->saveSettings($newPayload);
}
/**
* @param list<array{quantity: int, subtotal: int}> $lineItems
* @return array<string, mixed>|null
*/
public function buildOrderSnapshot(OrderChannel $channel, int $totalAmount, array $lineItems, bool $isAffiliate = false): ?array
{
$feeSnapshot = $this->feeRulesForChannel($channel);
@ -190,10 +183,7 @@ public function buildOrderSnapshot(OrderChannel $channel, int $totalAmount, arra
];
}
/**
* @param array<string, array{scope: string, value_type: string, value: float}> $fees
* @return array<string, array{scope: string, value_type: string, value: float}>
*/
private function excludeAffiliateFee(array $fees, OrderChannel $channel): array
{
$affiliateKey = match ($channel) {
@ -213,9 +203,7 @@ private function excludeAffiliateFee(array $fees, OrderChannel $channel): array
return $fees;
}
/**
* @return array{platform: string, fees: array<string, array{scope: string, value_type: string, value: float}>}|null
*/
private function feeRulesForChannel(OrderChannel $channel): ?array
{
$settings = app(MarketplaceSettings::class);
@ -250,19 +238,13 @@ private function feeRulesForChannel(OrderChannel $channel): ?array
};
}
/**
* @param array{scope: string, value_type: string, value: float|int|string} $data
* @return array{scope: string, value_type: string, value: float}
*/
private function presentFeeRule(array $data): array
{
return MarketplaceFeeRule::fromArray($data)->toArray();
}
/**
* @param array{scope: string, value_type: string, value: float|int|string} $data
* @return array{scope: string, value_type: string, value: float}
*/
private function normalizeFeeRule(array $data): array
{
return MarketplaceFeeRule::fromArray($data)->toArray();