From d90d2261acc3fc807d51d443849259ffd95e2fbc Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Tue, 4 Aug 2026 21:57:38 +0700 Subject: [PATCH] feat: implement cash and stock handling traits for improved transaction management --- app/Services/Admin/AdminSettingsService.php | 2 +- .../Admin/Finance/CashAccountService.php | 66 +++++-------------- .../Admin/Finance/EmployeeAdvanceService.php | 45 ++++--------- app/Services/Admin/Finance/ExpenseService.php | 32 +++------ .../Admin/Finance/PayrollPeriodService.php | 23 +++---- app/Services/Admin/HR/AttendanceService.php | 2 +- app/Services/Admin/Manage/CuttingService.php | 28 +------- app/Services/Admin/Manage/PurchaseService.php | 28 +------- app/Services/Admin/Manage/RestockService.php | 51 +------------- .../Admin/Manage/TransactionService.php | 52 +-------------- app/Services/Admin/Master/CategoryService.php | 4 +- app/Services/Admin/Master/CustomerService.php | 4 +- .../Admin/Master/Product/ProductService.php | 6 +- .../Master/Product/ProductVariantService.php | 4 +- .../Master/RawMaterial/RawMaterialService.php | 2 +- .../RawMaterial/RawMaterialVariantService.php | 2 +- app/Services/Admin/Master/SupplierService.php | 4 +- app/Services/Admin/Settings/RoleService.php | 2 +- .../Concerns/HandlesCashTransactions.php | 57 ++++++++++++++++ app/Services/Concerns/HasStockAdjustment.php | 47 +++++++++++++ app/Services/Concerns/RegistersMedia.php | 48 ++++++-------- 21 files changed, 197 insertions(+), 312 deletions(-) create mode 100644 app/Services/Concerns/HandlesCashTransactions.php create mode 100644 app/Services/Concerns/HasStockAdjustment.php diff --git a/app/Services/Admin/AdminSettingsService.php b/app/Services/Admin/AdminSettingsService.php index de925c8..5b184a6 100644 --- a/app/Services/Admin/AdminSettingsService.php +++ b/app/Services/Admin/AdminSettingsService.php @@ -13,7 +13,7 @@ class AdminSettingsService { public function __construct( - private S3PresignedService $s3Service = new S3PresignedService, + private readonly S3PresignedService $s3Service, ) {} public function getSystemData(): array diff --git a/app/Services/Admin/Finance/CashAccountService.php b/app/Services/Admin/Finance/CashAccountService.php index ba757fc..76a9087 100644 --- a/app/Services/Admin/Finance/CashAccountService.php +++ b/app/Services/Admin/Finance/CashAccountService.php @@ -5,6 +5,7 @@ use App\Enums\CashTransactionType; use App\Models\CashAccount; use App\Models\CashTransaction; +use App\Services\Concerns\HandlesCashTransactions; use App\Services\Concerns\RegistersMedia; use App\Services\NotificationService; use App\Services\S3PresignedService; @@ -16,10 +17,10 @@ class CashAccountService { - use RegistersMedia; + use HandlesCashTransactions, RegistersMedia; public function __construct( - private S3PresignedService $s3Service = new S3PresignedService, + private readonly S3PresignedService $s3Service, ) {} public function get(): ?CashAccount @@ -94,27 +95,14 @@ private function formatTransaction(CashTransaction $transaction): array public function deposit(array $data): CashTransaction { - $transaction = DB::transaction(function () use ($data) { - $cashAccount = CashAccount::firstOrFail(); - $newBalance = $cashAccount->balance + $data['amount']; + $transaction = DB::transaction(fn () => $this->creditCash( + amount: $data['amount'], + description: $data['description'], + )); - $cashAccount->update(['balance' => $newBalance]); - - $transaction = CashTransaction::create([ - 'cash_account_id' => $cashAccount->id, - 'created_by_id' => auth()->id(), - 'amount' => $data['amount'], - 'balance_after' => $newBalance, - 'type' => CashTransactionType::DEPOSIT, - 'description' => $data['description'], - ]); - - if (! empty($data['receipt_key'])) { - $this->registerMedia($transaction, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null); - } - - return $transaction; - }); + if (! empty($data['receipt_key'])) { + $this->registerMedia($transaction, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null); + } NotificationService::notify( roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], @@ -128,33 +116,15 @@ public function deposit(array $data): CashTransaction public function withdrawal(array $data): CashTransaction { - $transaction = DB::transaction(function () use ($data) { - $cashAccount = CashAccount::firstOrFail(); + $transaction = DB::transaction(fn () => $this->debitCash( + amount: $data['amount'], + description: $data['description'], + type: CashTransactionType::WITHDRAWAL, + )); - if ($cashAccount->balance < $data['amount']) { - throw ValidationException::withMessages([ - 'amount' => 'Saldo tidak mencukupi.', - ]); - } - - $newBalance = $cashAccount->balance - $data['amount']; - $cashAccount->update(['balance' => $newBalance]); - - $transaction = CashTransaction::create([ - 'cash_account_id' => $cashAccount->id, - 'created_by_id' => auth()->id(), - 'amount' => $data['amount'], - 'balance_after' => $newBalance, - 'type' => CashTransactionType::WITHDRAWAL, - 'description' => $data['description'], - ]); - - if (! empty($data['receipt_key'])) { - $this->registerMedia($transaction, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null); - } - - return $transaction; - }); + if (! empty($data['receipt_key'])) { + $this->registerMedia($transaction, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null); + } NotificationService::notify( roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], diff --git a/app/Services/Admin/Finance/EmployeeAdvanceService.php b/app/Services/Admin/Finance/EmployeeAdvanceService.php index 4a6fee3..0cd24f2 100644 --- a/app/Services/Admin/Finance/EmployeeAdvanceService.php +++ b/app/Services/Admin/Finance/EmployeeAdvanceService.php @@ -7,6 +7,7 @@ use App\Models\CashAccount; use App\Models\CashTransaction; use App\Models\EmployeeAdvance; +use App\Services\Concerns\HandlesCashTransactions; use App\Services\NotificationService; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; @@ -15,7 +16,8 @@ class EmployeeAdvanceService { - public function getAll(): Collection + use HandlesCashTransactions; + public function getAll(array $filters = []): Collection { return EmployeeAdvance::select('id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at') ->with(['employee.user.userProfile']) @@ -23,7 +25,7 @@ public function getAll(): Collection ->get(); } - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator { return EmployeeAdvance::query() ->select('id', 'employee_id', 'amount', 'paid_amount', 'description', 'due_date', 'status', 'created_at') @@ -36,30 +38,16 @@ public function paginated(int $perPage = 25, string $search = '', string $sort = public function create(array $data): EmployeeAdvance { $employeeAdvance = DB::transaction(function () use ($data) { - $cashAccount = CashAccount::firstOrFail(); $employee = auth()->user()->employee; if (! $employee) { throw new \Exception('Anda tidak terdaftar sebagai karyawan.'); } - if ($cashAccount->balance < $data['amount']) { - throw ValidationException::withMessages([ - 'amount' => 'Saldo tidak mencukupi.', - ]); - } - - $newBalance = $cashAccount->balance - $data['amount']; - $cashAccount->update(['balance' => $newBalance]); - - $cashTransaction = CashTransaction::create([ - 'cash_account_id' => $cashAccount->id, - 'created_by_id' => auth()->id(), - 'amount' => $data['amount'], - 'balance_after' => $newBalance, - 'type' => CashTransactionType::EXPENSE, - 'description' => 'Kasbon: '.$data['description'], - ]); + $cashTransaction = $this->debitCash( + amount: $data['amount'], + description: 'Kasbon: '.$data['description'], + ); return EmployeeAdvance::create([ 'cash_transaction_id' => $cashTransaction->id, @@ -155,19 +143,10 @@ public function approve(EmployeeAdvance $employeeAdvance): EmployeeAdvance public function pay(EmployeeAdvance $employeeAdvance): EmployeeAdvance { $employeeAdvance = DB::transaction(function () use ($employeeAdvance) { - $cashAccount = CashAccount::firstOrFail(); - - $newBalance = $cashAccount->balance + $employeeAdvance->amount; - $cashAccount->update(['balance' => $newBalance]); - - $cashTransaction = CashTransaction::create([ - 'cash_account_id' => $cashAccount->id, - 'created_by_id' => auth()->id(), - 'amount' => $employeeAdvance->amount, - 'balance_after' => $newBalance, - 'type' => CashTransactionType::DEPOSIT, - 'description' => 'Pembayaran kasbon: '.$employeeAdvance->description, - ]); + $cashTransaction = $this->creditCash( + amount: $employeeAdvance->amount, + description: 'Pembayaran kasbon: '.$employeeAdvance->description, + ); $employeeAdvance->update([ 'status' => EmployeeAdvanceStatus::PAID, diff --git a/app/Services/Admin/Finance/ExpenseService.php b/app/Services/Admin/Finance/ExpenseService.php index 17b4ff7..5f99c9c 100644 --- a/app/Services/Admin/Finance/ExpenseService.php +++ b/app/Services/Admin/Finance/ExpenseService.php @@ -6,6 +6,7 @@ use App\Models\CashAccount; use App\Models\CashTransaction; use App\Models\Expense; +use App\Services\Concerns\HandlesCashTransactions; use App\Services\Concerns\RegistersMedia; use App\Services\NotificationService; use App\Services\S3PresignedService; @@ -17,13 +18,13 @@ class ExpenseService { - use RegistersMedia; + use HandlesCashTransactions, RegistersMedia; public function __construct( - private S3PresignedService $s3Service = new S3PresignedService, + private readonly S3PresignedService $s3Service, ) {} - public function getAll(): Collection + public function getAll(array $filters = []): Collection { return Expense::select('id', 'created_by_id', 'amount', 'description', 'created_at') ->with('createdBy.userProfile', 'media') @@ -32,7 +33,7 @@ public function getAll(): Collection ->map(fn (Expense $expense) => $this->formatExpense($expense)); } - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator { $paginator = Expense::query() ->select('id', 'created_by_id', 'amount', 'description', 'created_at') @@ -78,25 +79,10 @@ private function formatExpense(Expense $expense): array public function create(array $data): Expense { $expense = DB::transaction(function () use ($data) { - $cashAccount = CashAccount::firstOrFail(); - - if ($cashAccount->balance < $data['amount']) { - throw ValidationException::withMessages([ - 'amount' => 'Saldo tidak mencukupi.', - ]); - } - - $newBalance = $cashAccount->balance - $data['amount']; - $cashAccount->update(['balance' => $newBalance]); - - $cashTransaction = CashTransaction::create([ - 'cash_account_id' => $cashAccount->id, - 'created_by_id' => auth()->id(), - 'amount' => $data['amount'], - 'balance_after' => $newBalance, - 'type' => CashTransactionType::EXPENSE, - 'description' => $data['description'], - ]); + $cashTransaction = $this->debitCash( + amount: $data['amount'], + description: $data['description'], + ); $expense = Expense::create([ 'cash_transaction_id' => $cashTransaction->id, diff --git a/app/Services/Admin/Finance/PayrollPeriodService.php b/app/Services/Admin/Finance/PayrollPeriodService.php index 409fd59..9511b00 100644 --- a/app/Services/Admin/Finance/PayrollPeriodService.php +++ b/app/Services/Admin/Finance/PayrollPeriodService.php @@ -9,6 +9,7 @@ use App\Models\CashTransaction; use App\Models\Payroll; use App\Models\PayrollPeriod; +use App\Services\Concerns\HandlesCashTransactions; use App\Services\NotificationService; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; @@ -17,7 +18,8 @@ class PayrollPeriodService { - public function getAll(): Collection + use HandlesCashTransactions; + public function getAll(array $filters = []): Collection { return PayrollPeriod::select('id', 'year', 'month', 'status', 'closed_at', 'created_at') ->withCount('payrolls') @@ -35,7 +37,7 @@ public function getAll(): Collection ->get(); } - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator { return PayrollPeriod::query() ->select('id', 'year', 'month', 'status', 'closed_at', 'created_at') @@ -123,19 +125,10 @@ public function pay(Payroll $payroll): Payroll } $payroll = DB::transaction(function () use ($payroll) { - $cashAccount = CashAccount::firstOrFail(); - - $newBalance = $cashAccount->balance + $payroll->total_amount; - $cashAccount->update(['balance' => $newBalance]); - - $cashTransaction = CashTransaction::create([ - 'cash_account_id' => $cashAccount->id, - 'created_by_id' => auth()->id(), - 'amount' => $payroll->total_amount, - 'balance_after' => $newBalance, - 'type' => CashTransactionType::DEPOSIT, - 'description' => 'Pembayaran gaji karyawan', - ]); + $cashTransaction = $this->creditCash( + amount: $payroll->total_amount, + description: 'Pembayaran gaji karyawan', + ); $payroll->update([ 'status' => PayrollStatus::PAID, diff --git a/app/Services/Admin/HR/AttendanceService.php b/app/Services/Admin/HR/AttendanceService.php index 678268c..43279a9 100644 --- a/app/Services/Admin/HR/AttendanceService.php +++ b/app/Services/Admin/HR/AttendanceService.php @@ -16,7 +16,7 @@ class AttendanceService use RegistersMedia; public function __construct( - private S3PresignedService $s3Service = new S3PresignedService, + private readonly S3PresignedService $s3Service, ) {} public function getAll(): Collection diff --git a/app/Services/Admin/Manage/CuttingService.php b/app/Services/Admin/Manage/CuttingService.php index ef90e21..0a91a63 100644 --- a/app/Services/Admin/Manage/CuttingService.php +++ b/app/Services/Admin/Manage/CuttingService.php @@ -19,7 +19,7 @@ class CuttingService use RegistersMedia; public function __construct( - private S3PresignedService $s3Service = new S3PresignedService, + private readonly S3PresignedService $s3Service, ) {} public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator @@ -342,36 +342,12 @@ public function update(Cutting $cutting, array $data): Cutting 'updated_at' => $now, ]); - $this->syncCuttingPhoto($cutting, $data); + $this->syncPhoto($cutting, $data); return $cutting; }); } - private function syncCuttingPhoto(Cutting $cutting, array $data): void - { - if (! array_key_exists('photo_key', $data)) { - return; - } - - $currentKey = $cutting->getFirstMedia('photos')?->file_name; - - if ($data['photo_key'] === $currentKey) { - return; - } - - $cutting->clearMediaCollection('photos'); - - if (! empty($data['photo_key'])) { - $this->registerMedia( - model: $cutting, - s3Key: $data['photo_key'], - collectionName: 'photos', - orderColumn: 1, - ); - } - } - public function delete(Cutting $cutting): bool { return DB::transaction(function () use ($cutting) { diff --git a/app/Services/Admin/Manage/PurchaseService.php b/app/Services/Admin/Manage/PurchaseService.php index 85a1b68..804bef7 100644 --- a/app/Services/Admin/Manage/PurchaseService.php +++ b/app/Services/Admin/Manage/PurchaseService.php @@ -18,7 +18,7 @@ class PurchaseService use RegistersMedia; public function __construct( - private S3PresignedService $s3Service = new S3PresignedService, + private readonly S3PresignedService $s3Service, ) {} public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator @@ -455,36 +455,12 @@ public function update(Purchase $purchase, array $data): Purchase } DB::table('purchase_items')->insert($itemRows); - $this->syncPurchasePhoto($purchase, $data); + $this->syncPhoto($purchase, $data); return $purchase; }); } - private function syncPurchasePhoto(Purchase $purchase, array $data): void - { - if (! array_key_exists('photo_key', $data)) { - return; - } - - $currentKey = $purchase->getFirstMedia('photos')?->file_name; - - if ($data['photo_key'] === $currentKey) { - return; - } - - $purchase->clearMediaCollection('photos'); - - if (! empty($data['photo_key'])) { - $this->registerMedia( - model: $purchase, - s3Key: $data['photo_key'], - collectionName: 'photos', - orderColumn: 1, - ); - } - } - public function delete(Purchase $purchase): bool { return DB::transaction(function () use ($purchase) { diff --git a/app/Services/Admin/Manage/RestockService.php b/app/Services/Admin/Manage/RestockService.php index c1ebce3..3a3c14b 100644 --- a/app/Services/Admin/Manage/RestockService.php +++ b/app/Services/Admin/Manage/RestockService.php @@ -8,6 +8,7 @@ use App\Models\ProductVariant; use App\Models\Restock; use App\Models\RestockItem; +use App\Services\Concerns\HasStockAdjustment; use App\Services\Concerns\RegistersMedia; use App\Services\NotificationService; use App\Services\S3PresignedService; @@ -16,15 +17,10 @@ class RestockService { - use RegistersMedia; - - private const QUALITY_STOCK_MAP = [ - ProductStockQuality::GOOD->value => 'stock', - ProductStockQuality::REJECT->value => 'reject_stock', - ]; + use HasStockAdjustment, RegistersMedia; public function __construct( - private S3PresignedService $s3Service = new S3PresignedService, + private readonly S3PresignedService $s3Service, ) {} public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator @@ -240,45 +236,4 @@ private function buildItemRows(array $items, string $stockType, $now, int &$subt })->toArray(); } - 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 adjustVariantStock(int $variantId, int $quantity, int $sign, string $stockType): void - { - $field = self::QUALITY_STOCK_MAP[$stockType] ?? 'stock'; - - if ($sign > 0) { - ProductVariant::whereKey($variantId)->increment($field, $quantity); - } else { - ProductVariant::whereKey($variantId)->decrement($field, $quantity); - } - } - - private function syncPhoto(Restock $restock, array $data): void - { - if (! array_key_exists('photo_key', $data)) { - return; - } - - $currentKey = $restock->getFirstMedia('photos')?->file_name; - - if ($data['photo_key'] === $currentKey) { - return; - } - - $restock->clearMediaCollection('photos'); - - if (! empty($data['photo_key'])) { - $this->registerMedia( - model: $restock, - s3Key: $data['photo_key'], - collectionName: 'photos', - orderColumn: 1, - ); - } - } } diff --git a/app/Services/Admin/Manage/TransactionService.php b/app/Services/Admin/Manage/TransactionService.php index 6c2c2e8..8a93083 100644 --- a/app/Services/Admin/Manage/TransactionService.php +++ b/app/Services/Admin/Manage/TransactionService.php @@ -13,6 +13,7 @@ use App\Models\Product; use App\Models\ProductVariant; use App\Models\User; +use App\Services\Concerns\HasStockAdjustment; use App\Services\Concerns\RegistersMedia; use App\Services\NotificationService; use App\Services\S3PresignedService; @@ -21,12 +22,7 @@ class TransactionService { - use RegistersMedia; - - private const QUALITY_STOCK_MAP = [ - ProductStockQuality::GOOD->value => 'stock', - ProductStockQuality::REJECT->value => 'reject_stock', - ]; + use HasStockAdjustment, RegistersMedia; private const SELLING_PRICE_MAP = [ PriceType::DISTRIBUTOR->value => PriceType::DISTRIBUTOR, @@ -39,7 +35,7 @@ class TransactionService ]; public function __construct( - private S3PresignedService $s3Service = new S3PresignedService, + private readonly S3PresignedService $s3Service, ) {} public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator @@ -378,48 +374,6 @@ private function buildItemRows(array $items, string $stockType, string $priceTyp })->toArray(); } - 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 adjustVariantStock(int $variantId, int $quantity, int $sign, string $stockType): void - { - $field = self::QUALITY_STOCK_MAP[$stockType] ?? 'stock'; - - if ($sign > 0) { - ProductVariant::whereKey($variantId)->increment($field, $quantity); - } else { - ProductVariant::whereKey($variantId)->decrement($field, $quantity); - } - } - - private function syncPhoto(Order $order, array $data): void - { - if (! array_key_exists('photo_key', $data)) { - return; - } - - $currentKey = $order->getFirstMedia('photos')?->file_name; - - if ($data['photo_key'] === $currentKey) { - return; - } - - $order->clearMediaCollection('photos'); - - if (! empty($data['photo_key'])) { - $this->registerMedia( - model: $order, - s3Key: $data['photo_key'], - collectionName: 'photos', - orderColumn: 1, - ); - } - } - private function generateOrderNumber(): string { $prefix = 'TRX'; diff --git a/app/Services/Admin/Master/CategoryService.php b/app/Services/Admin/Master/CategoryService.php index b030c0d..3305fca 100644 --- a/app/Services/Admin/Master/CategoryService.php +++ b/app/Services/Admin/Master/CategoryService.php @@ -8,12 +8,12 @@ class CategoryService { - public function getAll(): Collection + public function getAll(array $filters = []): Collection { return Category::select('id', 'name')->latest()->get(); } - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator { return Category::query() ->select('id', 'name') diff --git a/app/Services/Admin/Master/CustomerService.php b/app/Services/Admin/Master/CustomerService.php index 023b6b7..f60ce14 100644 --- a/app/Services/Admin/Master/CustomerService.php +++ b/app/Services/Admin/Master/CustomerService.php @@ -8,12 +8,12 @@ class CustomerService { - public function getAll(): Collection + public function getAll(array $filters = []): Collection { return Customer::select('id', 'name', 'phone_number', 'address')->latest()->get(); } - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator { return Customer::query() ->select('id', 'name', 'phone_number', 'address') diff --git a/app/Services/Admin/Master/Product/ProductService.php b/app/Services/Admin/Master/Product/ProductService.php index 74968f8..fb145a6 100644 --- a/app/Services/Admin/Master/Product/ProductService.php +++ b/app/Services/Admin/Master/Product/ProductService.php @@ -14,9 +14,9 @@ class ProductService { public function __construct( - private ProductVariantService $variantService = new ProductVariantService, - private S3PresignedService $s3Service = new S3PresignedService, - private StockMutationService $stockMutationService = new StockMutationService, + private readonly ProductVariantService $variantService, + private readonly S3PresignedService $s3Service, + private readonly StockMutationService $stockMutationService, ) {} public function getAll(array $filters = []): Collection diff --git a/app/Services/Admin/Master/Product/ProductVariantService.php b/app/Services/Admin/Master/Product/ProductVariantService.php index fd65edb..57468cb 100644 --- a/app/Services/Admin/Master/Product/ProductVariantService.php +++ b/app/Services/Admin/Master/Product/ProductVariantService.php @@ -16,8 +16,8 @@ class ProductVariantService use RegistersMedia; public function __construct( - private S3PresignedService $s3Service = new S3PresignedService, - private StockMutationService $stockMutationService = new StockMutationService, + private readonly S3PresignedService $s3Service, + private readonly StockMutationService $stockMutationService, ) {} public function getForEdit(ProductVariant $variant): array diff --git a/app/Services/Admin/Master/RawMaterial/RawMaterialService.php b/app/Services/Admin/Master/RawMaterial/RawMaterialService.php index a875775..b4c008a 100644 --- a/app/Services/Admin/Master/RawMaterial/RawMaterialService.php +++ b/app/Services/Admin/Master/RawMaterial/RawMaterialService.php @@ -15,7 +15,7 @@ class RawMaterialService use RegistersMedia; public function __construct( - private S3PresignedService $s3Service = new S3PresignedService, + private readonly S3PresignedService $s3Service, ) {} public function getAll(array $filters = []): Collection diff --git a/app/Services/Admin/Master/RawMaterial/RawMaterialVariantService.php b/app/Services/Admin/Master/RawMaterial/RawMaterialVariantService.php index 6598ab2..5fed5dc 100644 --- a/app/Services/Admin/Master/RawMaterial/RawMaterialVariantService.php +++ b/app/Services/Admin/Master/RawMaterial/RawMaterialVariantService.php @@ -14,7 +14,7 @@ class RawMaterialVariantService use RegistersMedia; public function __construct( - private S3PresignedService $s3Service = new S3PresignedService, + private readonly S3PresignedService $s3Service, ) {} public function getForEdit(RawMaterialPrice $variant): array diff --git a/app/Services/Admin/Master/SupplierService.php b/app/Services/Admin/Master/SupplierService.php index f25aa0e..db2a302 100644 --- a/app/Services/Admin/Master/SupplierService.php +++ b/app/Services/Admin/Master/SupplierService.php @@ -8,12 +8,12 @@ class SupplierService { - public function getAll(): Collection + public function getAll(array $filters = []): Collection { return Supplier::select('id', 'name', 'phone_number', 'address')->latest()->get(); } - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator { return Supplier::query() ->select('id', 'name', 'phone_number', 'address') diff --git a/app/Services/Admin/Settings/RoleService.php b/app/Services/Admin/Settings/RoleService.php index b91b1bf..c87f133 100644 --- a/app/Services/Admin/Settings/RoleService.php +++ b/app/Services/Admin/Settings/RoleService.php @@ -15,7 +15,7 @@ public function getAll(): Collection return Role::withCount('permissions')->get(); } - public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc'): LengthAwarePaginator + public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator { return Role::query() ->withCount('permissions') diff --git a/app/Services/Concerns/HandlesCashTransactions.php b/app/Services/Concerns/HandlesCashTransactions.php new file mode 100644 index 0000000..03a21b8 --- /dev/null +++ b/app/Services/Concerns/HandlesCashTransactions.php @@ -0,0 +1,57 @@ +getCashAccount(); + $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, + ]); + } + + private function debitCash(int $amount, string $description, CashTransactionType $type = CashTransactionType::EXPENSE): CashTransaction + { + $cashAccount = $this->getCashAccount(); + + 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, + ]); + } +} diff --git a/app/Services/Concerns/HasStockAdjustment.php b/app/Services/Concerns/HasStockAdjustment.php new file mode 100644 index 0000000..04f9485 --- /dev/null +++ b/app/Services/Concerns/HasStockAdjustment.php @@ -0,0 +1,47 @@ +value => 'stock', + ProductStockQuality::REJECT->value => 'reject_stock', + ]; + + private function adjustStock(Model $model, string $field, int $quantity, int $sign): void + { + if ($sign > 0) { + $model->increment($field, $quantity); + } else { + $model->decrement($field, $quantity); + } + } + + private function adjustVariantStock(int $variantId, int $quantity, int $sign, string $stockType): void + { + $field = self::QUALITY_STOCK_MAP[$stockType] ?? 'stock'; + + $this->adjustStock( + model: app(\App\Models\ProductVariant::class)->newQuery()->findOrFail($variantId), + field: $field, + quantity: $quantity, + sign: $sign, + ); + } + + 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); + } +} diff --git a/app/Services/Concerns/RegistersMedia.php b/app/Services/Concerns/RegistersMedia.php index 007d4b9..de556cb 100644 --- a/app/Services/Concerns/RegistersMedia.php +++ b/app/Services/Concerns/RegistersMedia.php @@ -39,35 +39,27 @@ private function registerMedia( ]); } - private function registerMediaFromBase64( - Model $model, - string $data, - string $collectionName, - string $subdirectory, - array $generatedConversions = [], - ): void { - if (str_starts_with($data, 'data:image')) { - $base64 = explode(',', $data)[1]; - $imageData = base64_decode($base64); - $filename = $collectionName.'_'.time().'_'.uniqid().'.jpg'; - $s3Key = $subdirectory.'/'.$filename; - - app('filesystem')->disk('s3')->put($s3Key, $imageData); - $mimeType = 'image/jpeg'; - $fileSize = strlen($imageData); - } else { - $s3Key = $data; - $mimeType = 'image/jpeg'; - $fileSize = 0; + private function syncPhoto(Model $model, array $data, string $collectionName = 'photos'): void + { + if (! array_key_exists('photo_key', $data)) { + return; } - $this->registerMedia( - model: $model, - s3Key: $s3Key, - collectionName: $collectionName, - generatedConversions: $generatedConversions, - fileSize: $fileSize, - mimeType: $mimeType, - ); + $currentKey = $model->getFirstMedia($collectionName)?->file_name; + + if ($data['photo_key'] === $currentKey) { + return; + } + + $model->clearMediaCollection($collectionName); + + if (! empty($data['photo_key'])) { + $this->registerMedia( + model: $model, + s3Key: $data['photo_key'], + collectionName: $collectionName, + orderColumn: 1, + ); + } } }