314 lines
12 KiB
PHP
314 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Manage;
|
|
|
|
use App\Enums\CuttingStatus;
|
|
use App\Models\Cutting;
|
|
use App\Models\CuttingResultPrice;
|
|
use App\Models\ProductPrice;
|
|
use App\Models\ProductVariant;
|
|
use App\Models\User;
|
|
use App\Services\System\PushNotificationService;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class StockService
|
|
{
|
|
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()
|
|
->with([
|
|
'createdBy.profile',
|
|
'rejection.rejectedBy.profile',
|
|
'materials.rawMaterialPrice.rawMaterial:id,name,unit',
|
|
'results.productVariant.product:id,name',
|
|
'results.productVariant:id,product_id,name',
|
|
])
|
|
->where('status', CuttingStatus::COMPLETED)
|
|
->latest()
|
|
->get()
|
|
->each(function (Cutting $cutting): void {
|
|
$this->appendCostPreview($cutting);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get all cuttings pending owner approval.
|
|
*
|
|
* @return Collection<int, Cutting>
|
|
*/
|
|
public function getPendingApprovalCuttings(User $user): Collection
|
|
{
|
|
return Cutting::query()
|
|
->with([
|
|
'createdBy.profile',
|
|
'rejection.rejectedBy.profile',
|
|
'materials.rawMaterialPrice.rawMaterial:id,name,unit',
|
|
'results.productVariant.product:id,name',
|
|
'results.productVariant:id,product_id,name',
|
|
'resultPrices.productVariant:id,product_id,name',
|
|
])
|
|
->where('status', CuttingStatus::PENDING_VERIFICATION)
|
|
->latest()
|
|
->get()
|
|
->each(function (Cutting $cutting): void {
|
|
$this->appendCostPreview($cutting);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Submit verification for a completed cutting - saves data but does NOT add stock yet.
|
|
*
|
|
* @param list<array{product_variant_id: int, warehouse_stock: int, cutting_reject: 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,
|
|
?string $verificationNote = null,
|
|
?array $results = null,
|
|
?array $resultPrices = null,
|
|
): void {
|
|
if ($cutting->status !== CuttingStatus::COMPLETED) {
|
|
throw ValidationException::withMessages([
|
|
'status' => 'Hanya cutting yang sudah selesai yang dapat diverifikasi.',
|
|
]);
|
|
}
|
|
|
|
DB::transaction(function () use ($cutting, $user, $verificationNote, $results, $resultPrices): void {
|
|
$cutting->load(['materials.rawMaterialPrice', 'results']);
|
|
|
|
if ($results !== null) {
|
|
foreach ($results as $item) {
|
|
$cutting->results()
|
|
->where('product_variant_id', $item['product_variant_id'])
|
|
->update([
|
|
'warehouse_stock' => $item['warehouse_stock'],
|
|
'cutting_reject' => $item['cutting_reject'],
|
|
]);
|
|
}
|
|
$cutting->load('results');
|
|
}
|
|
|
|
$this->storeResultPrices($cutting, $resultPrices ?? []);
|
|
|
|
if ($verificationNote !== null && trim($verificationNote) !== '') {
|
|
$cutting->rejection()->create([
|
|
'reason' => trim($verificationNote),
|
|
'rejected_by_id' => $user->id,
|
|
]);
|
|
}
|
|
|
|
$cutting->submitted_by_id = $user->id;
|
|
$cutting->status = CuttingStatus::PENDING_VERIFICATION;
|
|
$cutting->save();
|
|
});
|
|
|
|
$description = $cutting->description ?? '-';
|
|
|
|
$this->pushNotificationService->sendToRoles(
|
|
'📦 Verifikasi Stok Menunggu Persetujuan',
|
|
"Cutting dengan deskripsi '{$description}' telah diajukan verifikasi dan menunggu persetujuan owner.",
|
|
['owner', 'developer'],
|
|
'/admin/manage/owner-verifications',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Owner approves verification - adds stock to store.
|
|
*/
|
|
public function approveVerification(
|
|
Cutting $cutting,
|
|
User $user,
|
|
?string $approvalNote = null,
|
|
): void {
|
|
if ($cutting->status !== CuttingStatus::PENDING_VERIFICATION) {
|
|
throw ValidationException::withMessages([
|
|
'status' => 'Hanya cutting yang menunggu verifikasi yang dapat disetujui.',
|
|
]);
|
|
}
|
|
|
|
DB::transaction(function () use ($cutting, $user, $approvalNote): void {
|
|
$cutting->load(['materials.rawMaterialPrice', 'results', 'resultPrices']);
|
|
|
|
$this->applyProductStockOnVerify($cutting);
|
|
$this->applyResultPricesToProducts($cutting);
|
|
|
|
if ($approvalNote !== null && trim($approvalNote) !== '') {
|
|
$cutting->rejection()->create([
|
|
'reason' => trim($approvalNote),
|
|
'rejected_by_id' => $user->id,
|
|
]);
|
|
}
|
|
|
|
$cutting->status = CuttingStatus::VERIFIED;
|
|
$cutting->save();
|
|
});
|
|
|
|
$description = $cutting->description ?? '-';
|
|
|
|
$this->pushNotificationService->sendToRoles(
|
|
'📦 Stok Cutting Diverifikasi',
|
|
"Cutting dengan deskripsi '{$description}' telah disetujui owner dan stok produk telah ditambahkan ke toko.",
|
|
['owner', 'developer'],
|
|
'/admin/manage/owner-verifications',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Owner rejects verification - sends back to completed.
|
|
*/
|
|
public function rejectVerification(
|
|
Cutting $cutting,
|
|
User $user,
|
|
string $reason,
|
|
): void {
|
|
if ($cutting->status !== CuttingStatus::PENDING_VERIFICATION) {
|
|
throw ValidationException::withMessages([
|
|
'status' => 'Hanya cutting yang menunggu verifikasi yang dapat ditolak.',
|
|
]);
|
|
}
|
|
|
|
DB::transaction(function () use ($cutting, $user, $reason): void {
|
|
$cutting->rejection()->create([
|
|
'reason' => trim($reason),
|
|
'rejected_by_id' => $user->id,
|
|
]);
|
|
|
|
$cutting->status = CuttingStatus::COMPLETED;
|
|
$cutting->save();
|
|
});
|
|
|
|
$description = $cutting->description ?? '-';
|
|
$this->pushNotificationService->sendToRoles(
|
|
'📦 Verifikasi Cutting Ditolak Owner',
|
|
"Cutting dengan deskripsi '{$description}' ditolak oleh owner dengan alasan: '{$reason}'.",
|
|
['owner', 'developer'],
|
|
'/admin/manage/owner-verifications',
|
|
);
|
|
|
|
if ($cutting->submitted_by_id) {
|
|
$this->pushNotificationService->sendToUser(
|
|
'📦 Verifikasi Cutting Ditolak Owner',
|
|
"Cutting dengan deskripsi '{$description}' yang Anda verifikasi ditolak oleh owner dengan alasan: '{$reason}'.",
|
|
$cutting->submitted_by_id,
|
|
'/admin/manage/owner-verifications',
|
|
);
|
|
}
|
|
}
|
|
|
|
private function applyProductStockOnVerify(Cutting $cutting): void
|
|
{
|
|
foreach ($cutting->results as $result) {
|
|
if ($result->warehouse_stock > 0) {
|
|
ProductVariant::query()
|
|
->whereKey($result->product_variant_id)
|
|
->increment('stock', $result->warehouse_stock);
|
|
}
|
|
|
|
if ($result->cutting_reject > 0) {
|
|
ProductVariant::query()
|
|
->whereKey($result->product_variant_id)
|
|
->increment('reject_stock', $result->cutting_reject);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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
|
|
$costPerUnit = 0;
|
|
foreach ($resultPrices as $resultData) {
|
|
foreach ($resultData['prices'] as $priceData) {
|
|
if ($priceData['type'] === 'harga_modal' && (int) $priceData['price'] > 0) {
|
|
$costPerUnit = (int) $priceData['price'];
|
|
break 2;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($costPerUnit > 0) {
|
|
$cutting->cost_per_unit = $costPerUnit;
|
|
} else {
|
|
$costPerUnit = (int) ($cutting->cost_per_unit ?? 0);
|
|
}
|
|
|
|
foreach ($resultPrices as $resultData) {
|
|
foreach ($resultData['prices'] as $priceData) {
|
|
if ((int) $priceData['price'] > 0) {
|
|
// Store/update to cutting_result_prices
|
|
CuttingResultPrice::query()->updateOrCreate(
|
|
[
|
|
'cutting_id' => $cutting->id,
|
|
'product_variant_id' => $resultData['product_variant_id'],
|
|
'price_type' => $priceData['type'],
|
|
],
|
|
[
|
|
'price' => (int) $priceData['price'],
|
|
'cost_per_unit' => $costPerUnit,
|
|
],
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update product_prices from cutting result prices (called on owner approval).
|
|
*/
|
|
private function applyResultPricesToProducts(Cutting $cutting): void
|
|
{
|
|
$resultPrices = $cutting->resultPrices()->with('productVariant')->get();
|
|
|
|
foreach ($resultPrices as $resultPrice) {
|
|
if ($resultPrice->price > 0) {
|
|
ProductPrice::query()->updateOrCreate(
|
|
[
|
|
'variant_id' => $resultPrice->product_variant_id,
|
|
'type' => $resultPrice->price_type->value,
|
|
],
|
|
[
|
|
'price' => $resultPrice->price,
|
|
],
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function appendCostPreview(Cutting $cutting): void
|
|
{
|
|
$cutting->setAttribute('total_result_pieces', (int) $cutting->results->sum('cutting_result'));
|
|
$cutting->setAttribute('total_material_usage', (float) $cutting->materials->sum('material_usage'));
|
|
|
|
$totalMaterialCost = $cutting->total_material_cost ?? 0;
|
|
$sewingCost = (int) ($cutting->sewing_cost ?? 0);
|
|
$otherCost = (int) ($cutting->other_cost ?? 0);
|
|
$totalProductionCost = $totalMaterialCost + $sewingCost + $otherCost;
|
|
$costPerUnit = $cutting->cost_per_unit ?? 0;
|
|
|
|
$cutting->setAttribute('total_material_cost', $totalMaterialCost);
|
|
$cutting->setAttribute('total_material_cost_formatted', 'Rp '.number_format($totalMaterialCost, 0, ',', '.'));
|
|
$cutting->setAttribute('sewing_cost', $sewingCost);
|
|
$cutting->setAttribute('sewing_cost_formatted', 'Rp '.number_format($sewingCost, 0, ',', '.'));
|
|
$cutting->setAttribute('other_cost', $otherCost);
|
|
$cutting->setAttribute('other_cost_formatted', 'Rp '.number_format($otherCost, 0, ',', '.'));
|
|
$cutting->setAttribute('total_production_cost', $totalProductionCost);
|
|
$cutting->setAttribute('total_production_cost_formatted', 'Rp '.number_format($totalProductionCost, 0, ',', '.'));
|
|
$cutting->setAttribute('estimated_cost_per_unit', $costPerUnit);
|
|
$cutting->setAttribute('estimated_cost_per_unit_formatted', 'Rp '.number_format($costPerUnit, 0, ',', '.'));
|
|
}
|
|
}
|