857 lines
33 KiB
PHP
857 lines
33 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Master;
|
|
|
|
use App\Enums\OwnerVerificationAction;
|
|
use App\Enums\OwnerVerificationStatus;
|
|
use App\Enums\Permission;
|
|
use App\Enums\ProductStatus;
|
|
use App\Models\Category;
|
|
use App\Models\OwnerVerificationRequest;
|
|
use App\Models\Product;
|
|
use App\Models\ProductVariant;
|
|
use App\Models\User;
|
|
use App\Services\Concerns\CachesQuery;
|
|
use App\Services\Concerns\RunsInTransaction;
|
|
use App\Services\Media\MediaService;
|
|
use App\Services\System\PushNotificationService;
|
|
use App\Support\Media\MediaPresenter;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class ProductService
|
|
{
|
|
use CachesQuery, RunsInTransaction;
|
|
|
|
private const MAX_VARIANT_IMAGES = 5;
|
|
|
|
public function __construct(
|
|
private readonly MediaService $mediaService,
|
|
private readonly PushNotificationService $pushNotificationService,
|
|
) {}
|
|
|
|
public function paginateForIndex(array $tableQuery, string $status, string $categoryId = '', string $stockStatus = '', string $productId = ''): LengthAwarePaginator
|
|
{
|
|
$query = Product::query()
|
|
->with([
|
|
'categories',
|
|
'pendingOwnerVerificationRequest.submittedBy.profile',
|
|
'variants' => fn ($query) => $query
|
|
->with(['media', 'prices' => fn ($query) => $query->orderBy('type')])
|
|
->orderBy('created_at')
|
|
->when($tableQuery['search'] !== '', function ($query) use ($tableQuery): void {
|
|
$query->where('name', 'like', "%{$tableQuery['search']}%");
|
|
}),
|
|
])
|
|
->when(($tableQuery['search_id'] ?? '') !== '', function (Builder $query) use ($tableQuery): void {
|
|
$query->where('products.id', $tableQuery['search_id']);
|
|
})
|
|
->when(($tableQuery['search_id'] ?? '') === '' && $tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
|
|
$search = $tableQuery['search'];
|
|
$query->where(function (Builder $q) use ($search): void {
|
|
$q->where('products.name', 'like', "%{$search}%")
|
|
->orWhereHas('variants', fn (Builder $q) => $q->where('name', 'like', "%{$search}%"));
|
|
});
|
|
})
|
|
->when($productId !== '', fn (Builder $query) => $query->where('products.id', $productId))
|
|
->when($status !== '', fn (Builder $query) => $query->where('status', $status))
|
|
->when($categoryId !== '', function (Builder $query) use ($categoryId): void {
|
|
$query->whereHas('categories', fn (Builder $query) => $query->where('categories.id', $categoryId));
|
|
})
|
|
->when($stockStatus === 'out_of_stock', function (Builder $query): void {
|
|
$query->whereHas('variants', fn (Builder $variantQuery) => $variantQuery->where('stock', '<=', 0));
|
|
})
|
|
->when($stockStatus === 'low_stock', function (Builder $query): void {
|
|
$query->whereHas('variants', fn (Builder $variantQuery) => $variantQuery->where('stock', '>', 0)->where('stock', '<', ProductVariant::minStock()));
|
|
});
|
|
|
|
$this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);
|
|
|
|
return $query
|
|
->paginate(25)
|
|
->withQueryString()
|
|
->through(function (Product $product) {
|
|
$product->variants->each(function (ProductVariant $variant): void {
|
|
$variant->setAttribute(
|
|
'images',
|
|
MediaPresenter::collection($variant, 'images'),
|
|
);
|
|
});
|
|
|
|
$pendingRequest = $product->pendingOwnerVerificationRequest;
|
|
|
|
// Also check for pending retail stock transfer on variants
|
|
if ($pendingRequest === null) {
|
|
$pendingRequest = OwnerVerificationRequest::query()
|
|
->where('subject_type', ProductVariant::class)
|
|
->whereIn('subject_id', $product->variants->pluck('id'))
|
|
->where('action', OwnerVerificationAction::RETAIL_STOCK_TRANSFER)
|
|
->pending()
|
|
->latest()
|
|
->first();
|
|
}
|
|
|
|
$product->setAttribute('has_pending_request', $pendingRequest !== null);
|
|
$product->setAttribute('pending_request_id', $pendingRequest?->id);
|
|
$product->setAttribute('pending_request_action', $pendingRequest?->action->value);
|
|
$product->setAttribute('pending_request_action_label', $pendingRequest?->action->label());
|
|
$product->setAttribute('pending_request_submitted_by_name', $pendingRequest?->submittedBy?->profile?->full_name ?? $pendingRequest?->submittedBy?->username);
|
|
$product->setAttribute(
|
|
'display_status',
|
|
$pendingRequest?->pendingToggleStatus() ?? $product->status->value,
|
|
);
|
|
|
|
return $product;
|
|
});
|
|
}
|
|
|
|
public function findForEdit(Product $product): Product
|
|
{
|
|
$product->load([
|
|
'categories',
|
|
'variants' => fn ($query) => $query
|
|
->with(['media', 'prices'])
|
|
->orderBy('created_at'),
|
|
]);
|
|
|
|
$product->variants->each(function (ProductVariant $variant): void {
|
|
$variant->setAttribute('images', MediaPresenter::collection($variant, 'images'));
|
|
});
|
|
|
|
return $product;
|
|
}
|
|
|
|
public function create(array $validated, User $user): void
|
|
{
|
|
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
|
$isDraft = ($validated['status'] ?? '') === ProductStatus::DRAFT->value;
|
|
|
|
$product = $this->runInTransaction(
|
|
function () use ($validated, $user, $isOwner, $isDraft): Product {
|
|
$product = Product::create([
|
|
'name' => $validated['name'],
|
|
'description' => $validated['description'] ?? null,
|
|
'status' => $isDraft ? ProductStatus::DRAFT : ($isOwner ? ProductStatus::ACTIVE : ProductStatus::INACTIVE),
|
|
]);
|
|
|
|
$product->categories()->sync($validated['category_ids'] ?? []);
|
|
|
|
foreach ($validated['variants'] as $index => $variantData) {
|
|
$variant = $product->variants()->create([
|
|
'name' => $variantData['name'],
|
|
'stock' => $variantData['stock'],
|
|
'reject_stock' => $variantData['reject_stock'],
|
|
'retail_stock' => $variantData['retail_stock'],
|
|
]);
|
|
|
|
$this->syncVariantImages($variant, $variantData, $index);
|
|
|
|
if (! empty($variantData['prices'])) {
|
|
foreach ($variantData['prices'] as $type => $priceValue) {
|
|
$variant->prices()->create([
|
|
'type' => $type,
|
|
'price' => $priceValue,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (! $isOwner && ! $isDraft) {
|
|
OwnerVerificationRequest::create([
|
|
'action' => OwnerVerificationAction::CREATE,
|
|
'status' => OwnerVerificationStatus::PENDING,
|
|
'subject_type' => Product::class,
|
|
'subject_id' => $product->id,
|
|
'submitted_by_id' => $user->id,
|
|
'payload' => [
|
|
'old' => null,
|
|
'new' => $this->snapshotProduct($product->fresh(['categories', 'variants'])),
|
|
],
|
|
]);
|
|
}
|
|
|
|
return $product;
|
|
},
|
|
'Gagal membuat produk',
|
|
);
|
|
|
|
if ($isDraft) {
|
|
$this->notifyOwner(
|
|
'Simpan Draft Produk',
|
|
"Produk '{$validated['name']}' disimpan sebagai draft oleh {$user->profile?->full_name}.",
|
|
route('admin.master.products.index'),
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
if ($isOwner) {
|
|
$this->cacheForgetByPattern('master:products:*');
|
|
|
|
$this->notifyOwner(
|
|
'Tambah Produk',
|
|
"Produk '{$validated['name']}' telah ditambahkan oleh {$user->profile?->full_name}.",
|
|
route('admin.master.products.index', ['search_id' => $product->id]),
|
|
);
|
|
}
|
|
|
|
if (! $isOwner) {
|
|
$this->notifyForPendingRequest(
|
|
$user,
|
|
'Tambah Produk',
|
|
"Pengajuan tambah produk '{$validated['name']}' oleh {$user->profile?->full_name} menunggu verifikasi owner.",
|
|
route('admin.master.products.index', ['search_id' => $product->id]),
|
|
(string) $product->id,
|
|
);
|
|
}
|
|
}
|
|
|
|
public function update(Product $product, array $validated, User $user): void
|
|
{
|
|
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
|
$isDraft = $product->status === ProductStatus::DRAFT;
|
|
$changingToActive = ($validated['status'] ?? '') === ProductStatus::ACTIVE->value;
|
|
$canEditDirectly = $isOwner || ($isDraft && ! $changingToActive);
|
|
|
|
$this->runInTransaction(
|
|
function () use ($validated, $product, $user, $canEditDirectly): void {
|
|
|
|
if ($canEditDirectly) {
|
|
$payload = $this->enrichPayload($this->buildPayloadFromValidated($validated));
|
|
$this->applyPayloadToProduct($product, $payload);
|
|
} else {
|
|
$verificationRequest = OwnerVerificationRequest::create([
|
|
'action' => OwnerVerificationAction::UPDATE,
|
|
'status' => OwnerVerificationStatus::PENDING,
|
|
'subject_type' => Product::class,
|
|
'subject_id' => $product->id,
|
|
'submitted_by_id' => $user->id,
|
|
'payload' => [
|
|
'old' => $this->snapshotProduct($product),
|
|
'new' => $this->enrichPayload($this->buildPayloadFromValidated($validated)),
|
|
],
|
|
]);
|
|
|
|
foreach ($validated['variants'] as $index => $variantData) {
|
|
$isNewVariant = empty($variantData['id']);
|
|
$this->syncRequestVariantImages($verificationRequest, $variantData, $index, required: $isNewVariant);
|
|
}
|
|
}
|
|
},
|
|
'Gagal memperbarui produk',
|
|
);
|
|
|
|
if ($canEditDirectly) {
|
|
$this->cacheForgetByPattern('master:products:*');
|
|
$this->cacheForgetByPattern('prices:*');
|
|
$this->cacheForget('homepage:page_data');
|
|
|
|
$actionLabel = $isDraft && $changingToActive
|
|
? 'Publikasi Draft Produk'
|
|
: 'Ubah Produk';
|
|
|
|
$this->notifyOwner(
|
|
$actionLabel,
|
|
"Produk '{$product->name}' telah diperbarui oleh {$user->profile?->full_name}.",
|
|
route('admin.master.products.index', ['search_id' => $product->id]),
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
$this->cacheForgetByPattern('master:products:*');
|
|
$this->cacheForgetByPattern('prices:*');
|
|
$this->cacheForget('homepage:page_data');
|
|
|
|
if ($isOwner) {
|
|
$this->notifyOwner(
|
|
'Ubah Produk',
|
|
"Produk '{$product->name}' telah diperbarui oleh {$user->profile?->full_name}.",
|
|
route('admin.master.products.index', ['search_id' => $product->id]),
|
|
);
|
|
}
|
|
|
|
if (! $isOwner) {
|
|
$changedVariants = [];
|
|
foreach ($validated['variants'] as $variantData) {
|
|
if (! empty($variantData['id'])) {
|
|
$originalVariant = $product->variants->firstWhere('id', $variantData['id']);
|
|
if ($originalVariant) {
|
|
$isChanged = false;
|
|
if ($originalVariant->name !== $variantData['name']) {
|
|
$isChanged = true;
|
|
}
|
|
if (rtrim(rtrim(number_format((float) $originalVariant->stock, 4, '.', ''), '0'), '.') !== rtrim(rtrim(number_format((float) $variantData['stock'], 4, '.', ''), '0'), '.')) {
|
|
$isChanged = true;
|
|
}
|
|
if (rtrim(rtrim(number_format((float) $originalVariant->retail_stock, 4, '.', ''), '0'), '.') !== rtrim(rtrim(number_format((float) $variantData['retail_stock'], 4, '.', ''), '0'), '.')) {
|
|
$isChanged = true;
|
|
}
|
|
if (! empty($variantData['prices'])) {
|
|
foreach ($variantData['prices'] as $type => $priceValue) {
|
|
$originalPrice = $originalVariant->prices->firstWhere('type', $type);
|
|
if (! $originalPrice || $originalPrice->price !== (int) $priceValue) {
|
|
$isChanged = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($isChanged) {
|
|
$changedVariants[] = $variantData['name'];
|
|
}
|
|
}
|
|
} else {
|
|
$changedVariants[] = $variantData['name'];
|
|
}
|
|
}
|
|
|
|
if (! empty($changedVariants)) {
|
|
$variantsStr = implode(', ', $changedVariants);
|
|
$this->notifyForPendingRequest(
|
|
$user,
|
|
'Ubah Varian Produk',
|
|
"Pengajuan ubah varian '{$variantsStr}' pada produk '{$product->name}' oleh {$user->profile?->full_name} menunggu verifikasi owner.",
|
|
route('admin.master.products.index', ['search_id' => $product->id]),
|
|
(string) $product->id,
|
|
);
|
|
} else {
|
|
$this->notifyForPendingRequest(
|
|
$user,
|
|
'Ubah Produk',
|
|
"Pengajuan ubah produk '{$product->name}' oleh {$user->profile?->full_name} menunggu verifikasi owner.",
|
|
route('admin.master.products.index', ['search_id' => $product->id]),
|
|
(string) $product->id,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function delete(Product $product, User $user): void
|
|
{
|
|
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
|
|
|
if ($isOwner) {
|
|
$name = $product->name;
|
|
$this->applyDeleteSubject($product);
|
|
$this->cacheForgetByPattern('master:products:*');
|
|
$this->cacheForgetByPattern('prices:*');
|
|
$this->cacheForget('homepage:page_data');
|
|
|
|
$this->notifyOwner(
|
|
'Hapus Produk',
|
|
"Produk '{$name}' telah dihapus oleh {$user->profile?->full_name}.",
|
|
route('admin.master.products.index'),
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
$this->runInTransaction(
|
|
function () use ($product, $user): void {
|
|
OwnerVerificationRequest::create([
|
|
'action' => OwnerVerificationAction::DELETE,
|
|
'status' => OwnerVerificationStatus::PENDING,
|
|
'subject_type' => Product::class,
|
|
'subject_id' => $product->id,
|
|
'submitted_by_id' => $user->id,
|
|
'payload' => [
|
|
'old' => $this->snapshotProduct($product),
|
|
'new' => null,
|
|
],
|
|
]);
|
|
},
|
|
'Gagal mengajukan penghapusan produk',
|
|
);
|
|
|
|
$this->notifyForPendingRequest(
|
|
$user,
|
|
'Hapus Produk',
|
|
"Pengajuan hapus produk '{$product->name}' oleh {$user->profile?->full_name} menunggu verifikasi owner.",
|
|
route('admin.master.products.index', ['search_id' => $product->id]),
|
|
(string) $product->id,
|
|
);
|
|
}
|
|
|
|
public function toggleStatus(Product $product, array $validated, User $user): void
|
|
{
|
|
$isOwner = $user->can(Permission::OWNER_VERIFICATIONS_VERIFY->value);
|
|
$newStatus = ProductStatus::tryFrom($validated['status'] ?? '');
|
|
|
|
if ($isOwner) {
|
|
$product->update([
|
|
'status' => $newStatus,
|
|
]);
|
|
|
|
$this->cacheForgetByPattern('master:products:*');
|
|
$this->cacheForget('homepage:page_data');
|
|
|
|
$statusLabel = $newStatus?->label() ?? 'unknown';
|
|
|
|
$this->notifyOwner(
|
|
'Ubah Status Produk',
|
|
"Status produk '{$product->name}' telah diubah menjadi {$statusLabel} oleh {$user->profile?->full_name}.",
|
|
route('admin.master.products.index', ['search_id' => $product->id]),
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
$this->runInTransaction(
|
|
function () use ($product, $user, $newStatus): void {
|
|
OwnerVerificationRequest::create([
|
|
'action' => OwnerVerificationAction::TOGGLE_STATUS,
|
|
'status' => OwnerVerificationStatus::PENDING,
|
|
'subject_type' => Product::class,
|
|
'subject_id' => $product->id,
|
|
'submitted_by_id' => $user->id,
|
|
'payload' => [
|
|
'old' => [
|
|
'name' => $product->name,
|
|
'status' => $product->status->value,
|
|
],
|
|
'new' => [
|
|
'name' => $product->name,
|
|
'status' => $newStatus->value,
|
|
],
|
|
],
|
|
]);
|
|
},
|
|
'Gagal mengajukan perubahan status produk',
|
|
);
|
|
|
|
$statusLabel = $newStatus?->label() ?? 'unknown';
|
|
|
|
$this->notifyForPendingRequest(
|
|
$user,
|
|
'Ubah Status Produk',
|
|
"Pengajuan ubah status produk '{$product->name}' menjadi {$statusLabel} oleh {$user->profile?->full_name} menunggu verifikasi owner.",
|
|
route('admin.master.products.index', ['search_id' => $product->id]),
|
|
(string) $product->id,
|
|
);
|
|
}
|
|
|
|
public function applyVerificationRequest(OwnerVerificationRequest $verificationRequest): void
|
|
{
|
|
match ($verificationRequest->action) {
|
|
OwnerVerificationAction::CREATE => $this->applyCreate($verificationRequest),
|
|
OwnerVerificationAction::UPDATE => $this->applyUpdate($verificationRequest),
|
|
OwnerVerificationAction::DELETE => $this->applyDelete($verificationRequest),
|
|
OwnerVerificationAction::TOGGLE_STATUS => $this->applyToggleStatus($verificationRequest),
|
|
default => throw ValidationException::withMessages([
|
|
'action' => 'Aksi verifikasi produk tidak didukung.',
|
|
]),
|
|
};
|
|
|
|
$this->cacheForgetByPattern('master:products:*');
|
|
$this->cacheForgetByPattern('prices:*');
|
|
}
|
|
|
|
public function rejectVerificationRequest(OwnerVerificationRequest $verificationRequest): void
|
|
{
|
|
match ($verificationRequest->action) {
|
|
OwnerVerificationAction::CREATE => $this->rejectCreate($verificationRequest),
|
|
OwnerVerificationAction::UPDATE => $this->rollbackUpdate($verificationRequest),
|
|
OwnerVerificationAction::DELETE, OwnerVerificationAction::TOGGLE_STATUS => null,
|
|
default => throw ValidationException::withMessages([
|
|
'action' => 'Aksi verifikasi produk tidak didukung.',
|
|
]),
|
|
};
|
|
|
|
$this->cacheForgetByPattern('master:products:*');
|
|
$this->cacheForgetByPattern('prices:*');
|
|
$this->cacheForget('homepage:page_data');
|
|
}
|
|
|
|
public function clearVerificationRequestMedia(OwnerVerificationRequest $verificationRequest): void
|
|
{
|
|
$verificationRequest->load('media');
|
|
|
|
$newPayload = $this->payloadNew($verificationRequest);
|
|
|
|
foreach ($newPayload['variants'] ?? [] as $index => $_variant) {
|
|
$verificationRequest->clearMediaCollection($verificationRequest->variantImageCollection((int) $index));
|
|
}
|
|
}
|
|
|
|
public function applyCreate(OwnerVerificationRequest $verificationRequest): void
|
|
{
|
|
$product = $verificationRequest->subject;
|
|
|
|
if (! $product instanceof Product) {
|
|
throw ValidationException::withMessages([
|
|
'product' => 'Produk tidak ditemukan.',
|
|
]);
|
|
}
|
|
|
|
$product->update([
|
|
'status' => ProductStatus::ACTIVE,
|
|
]);
|
|
}
|
|
|
|
public function applyUpdate(OwnerVerificationRequest $verificationRequest): void
|
|
{
|
|
$product = $verificationRequest->subject;
|
|
|
|
if (! $product instanceof Product) {
|
|
throw ValidationException::withMessages([
|
|
'product' => 'Produk tidak ditemukan.',
|
|
]);
|
|
}
|
|
|
|
$this->applyPayloadToProduct($product, $this->payloadNew($verificationRequest), $verificationRequest);
|
|
}
|
|
|
|
private function applyPayloadToProduct(
|
|
Product $product,
|
|
array $payload,
|
|
?OwnerVerificationRequest $verificationRequest = null,
|
|
): void {
|
|
$product->update([
|
|
'name' => $payload['name'],
|
|
'description' => $payload['description'] ?? null,
|
|
]);
|
|
|
|
if (array_key_exists('status', $payload)) {
|
|
$status = ProductStatus::tryFrom($payload['status']);
|
|
if ($status !== null) {
|
|
$product->update([
|
|
'status' => $status,
|
|
]);
|
|
}
|
|
}
|
|
|
|
$product->categories()->sync($payload['category_ids'] ?? []);
|
|
|
|
$submittedVariantIds = collect($payload['variants'] ?? [])
|
|
->pluck('id')
|
|
->filter()
|
|
->map(fn ($id) => (int) $id)
|
|
->all();
|
|
|
|
$product->variants()
|
|
->whereNotIn('id', $submittedVariantIds)
|
|
->get()
|
|
->each(function (ProductVariant $variant): void {
|
|
$variant->delete();
|
|
});
|
|
|
|
foreach ($payload['variants'] ?? [] as $index => $variantData) {
|
|
if (! empty($variantData['id'])) {
|
|
$variant = $product->variants()->findOrFail($variantData['id']);
|
|
$variant->update([
|
|
'name' => $variantData['name'],
|
|
'stock' => $variantData['stock'],
|
|
'reject_stock' => $variantData['reject_stock'] ?? 0,
|
|
'retail_stock' => $variantData['retail_stock'],
|
|
]);
|
|
|
|
if ($verificationRequest !== null) {
|
|
$this->applyVariantImageChanges($verificationRequest, $variant, $variantData, (int) $index);
|
|
} else {
|
|
$this->syncVariantImages($variant, $variantData, $index);
|
|
}
|
|
|
|
if (! empty($variantData['prices'])) {
|
|
foreach ($variantData['prices'] as $type => $priceValue) {
|
|
$variant->prices()->updateOrCreate(
|
|
['type' => $type],
|
|
['price' => $priceValue]
|
|
);
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
$variant = $product->variants()->create([
|
|
'name' => $variantData['name'],
|
|
'stock' => $variantData['stock'],
|
|
'reject_stock' => $variantData['reject_stock'] ?? 0,
|
|
'retail_stock' => $variantData['retail_stock'],
|
|
]);
|
|
|
|
if ($verificationRequest !== null) {
|
|
$this->copyRequestVariantImages($verificationRequest, (int) $index, $variant);
|
|
} else {
|
|
$this->syncVariantImages($variant, $variantData, $index);
|
|
}
|
|
|
|
if (! empty($variantData['prices'])) {
|
|
foreach ($variantData['prices'] as $type => $priceValue) {
|
|
$variant->prices()->create([
|
|
'type' => $type,
|
|
'price' => $priceValue,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function applyDelete(OwnerVerificationRequest $verificationRequest): void
|
|
{
|
|
$product = $verificationRequest->subject;
|
|
|
|
if (! $product instanceof Product) {
|
|
throw ValidationException::withMessages([
|
|
'product' => 'Produk tidak ditemukan.',
|
|
]);
|
|
}
|
|
|
|
$this->applyDeleteSubject($product);
|
|
}
|
|
|
|
private function applyDeleteSubject(Product $product): void
|
|
{
|
|
$this->runInTransaction(
|
|
function () use ($product): void {
|
|
$product->variants()->delete();
|
|
$product->categories()->detach();
|
|
$product->delete();
|
|
},
|
|
'Gagal menghapus produk',
|
|
);
|
|
}
|
|
|
|
public function applyToggleStatus(OwnerVerificationRequest $verificationRequest): void
|
|
{
|
|
$product = $verificationRequest->subject;
|
|
|
|
if (! $product instanceof Product) {
|
|
throw ValidationException::withMessages([
|
|
'product' => 'Produk tidak ditemukan.',
|
|
]);
|
|
}
|
|
|
|
$newPayload = $this->payloadNew($verificationRequest);
|
|
|
|
$status = ProductStatus::tryFrom($newPayload['status'] ?? '');
|
|
|
|
$product->update([
|
|
'status' => $status ?? ProductStatus::INACTIVE,
|
|
]);
|
|
}
|
|
|
|
private function notifyOwner(string $typeLabel, string $body, string $url): void
|
|
{
|
|
$this->pushNotificationService->sendToRoles(
|
|
"📦 {$typeLabel}",
|
|
$body,
|
|
['owner', 'developer'],
|
|
$url,
|
|
);
|
|
}
|
|
|
|
private function notifyForPendingRequest(User $user, string $typeLabel, string $body, string $submitterUrl, ?string $search = null): void
|
|
{
|
|
$ownerUrl = route('admin.master.products.index');
|
|
if ($search !== null) {
|
|
$ownerUrl = route('admin.master.products.index', ['search_id' => $search]);
|
|
}
|
|
|
|
$this->pushNotificationService->sendToRoles(
|
|
"📦 {$typeLabel} Menunggu Persetujuan Owner",
|
|
$body,
|
|
['owner', 'developer'],
|
|
$ownerUrl,
|
|
);
|
|
|
|
$this->pushNotificationService->sendToUser(
|
|
'📤 Pengajuan Terkirim',
|
|
$body,
|
|
$user->id,
|
|
$submitterUrl,
|
|
);
|
|
}
|
|
|
|
private function rejectCreate(OwnerVerificationRequest $verificationRequest): void
|
|
{
|
|
$product = $verificationRequest->subject;
|
|
|
|
if (! $product instanceof Product) {
|
|
return;
|
|
}
|
|
|
|
$this->runInTransaction(
|
|
function () use ($product): void {
|
|
$product->variants()->delete();
|
|
$product->categories()->detach();
|
|
$product->delete();
|
|
},
|
|
'Gagal menolak produk',
|
|
);
|
|
}
|
|
|
|
private function rollbackUpdate(OwnerVerificationRequest $verificationRequest): void
|
|
{
|
|
$product = $verificationRequest->subject;
|
|
|
|
if (! $product instanceof Product) {
|
|
throw ValidationException::withMessages([
|
|
'product' => 'Produk tidak ditemukan.',
|
|
]);
|
|
}
|
|
|
|
$oldPayload = $this->payloadOld($verificationRequest);
|
|
|
|
if ($oldPayload === []) {
|
|
return;
|
|
}
|
|
|
|
$this->applyPayloadToProduct($product, $oldPayload);
|
|
}
|
|
|
|
private function payloadOld(OwnerVerificationRequest $verificationRequest): array
|
|
{
|
|
$payload = $verificationRequest->payload ?? [];
|
|
|
|
if (array_key_exists('old', $payload)) {
|
|
return is_array($payload['old']) ? $payload['old'] : [];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
private function payloadNew(OwnerVerificationRequest $verificationRequest): array
|
|
{
|
|
$payload = $verificationRequest->payload ?? [];
|
|
|
|
if (array_key_exists('new', $payload)) {
|
|
return is_array($payload['new']) ? $payload['new'] : [];
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
private function snapshotProduct(Product $product): array
|
|
{
|
|
$product->load(['categories', 'variants.prices']);
|
|
|
|
return $this->enrichPayload([
|
|
'name' => $product->name,
|
|
'description' => $product->description,
|
|
'category_ids' => $product->categories->pluck('id')->all(),
|
|
'status' => $product->status->value,
|
|
'variants' => $product->variants
|
|
->map(fn (ProductVariant $variant) => [
|
|
'id' => $variant->id,
|
|
'name' => $variant->name,
|
|
'stock' => $variant->stock,
|
|
'reject_stock' => $variant->reject_stock,
|
|
'retail_stock' => $variant->retail_stock,
|
|
'prices' => $variant->prices
|
|
->mapWithKeys(fn ($price) => [$price->type->value => $price->price])
|
|
->all(),
|
|
])
|
|
->all(),
|
|
]);
|
|
}
|
|
|
|
private function enrichPayload(array $data): array
|
|
{
|
|
$categoryIds = $data['category_ids'] ?? [];
|
|
|
|
$data['category_names'] = $categoryIds === []
|
|
? []
|
|
: Category::query()->whereIn('id', $categoryIds)->pluck('name')->all();
|
|
|
|
return $data;
|
|
}
|
|
|
|
private function buildPayloadFromValidated(array $validated): array
|
|
{
|
|
return [
|
|
'name' => $validated['name'],
|
|
'description' => $validated['description'] ?? null,
|
|
'status' => $validated['status'] ?? ProductStatus::ACTIVE->value,
|
|
'category_ids' => $validated['category_ids'] ?? [],
|
|
'variants' => collect($validated['variants'] ?? [])
|
|
->map(fn (array $variantData) => [
|
|
'id' => $variantData['id'] ?? null,
|
|
'name' => $variantData['name'],
|
|
'stock' => $variantData['stock'],
|
|
'reject_stock' => $variantData['reject_stock'],
|
|
'retail_stock' => $variantData['retail_stock'],
|
|
'prices' => $variantData['prices'] ?? [],
|
|
'remove_media_ids' => $variantData['remove_media_ids'] ?? [],
|
|
'images' => $variantData['images'] ?? null,
|
|
's3_keys' => $variantData['s3_keys'] ?? null,
|
|
])
|
|
->all(),
|
|
];
|
|
}
|
|
|
|
private function syncVariantImages(
|
|
ProductVariant $variant,
|
|
array $variantData,
|
|
int $index,
|
|
bool $required = true,
|
|
): void {
|
|
$this->mediaService->syncCollection(
|
|
$variant,
|
|
'images',
|
|
$variantData['images'] ?? null,
|
|
$variantData['remove_media_ids'] ?? null,
|
|
self::MAX_VARIANT_IMAGES,
|
|
required: $required,
|
|
errorKey: "variants.{$index}.s3_keys",
|
|
s3Keys: $variantData['s3_keys'] ?? null,
|
|
);
|
|
}
|
|
|
|
private function syncRequestVariantImages(
|
|
OwnerVerificationRequest $verificationRequest,
|
|
array $variantData,
|
|
int $index,
|
|
bool $required = true,
|
|
): void {
|
|
$collection = $verificationRequest->variantImageCollection($index);
|
|
|
|
$this->mediaService->syncCollection(
|
|
$verificationRequest,
|
|
$collection,
|
|
$variantData['images'] ?? null,
|
|
$variantData['remove_media_ids'] ?? null,
|
|
self::MAX_VARIANT_IMAGES,
|
|
required: $required,
|
|
errorKey: "variants.{$index}.s3_keys",
|
|
s3Keys: $variantData['s3_keys'] ?? null,
|
|
);
|
|
}
|
|
|
|
private function applyVariantImageChanges(
|
|
OwnerVerificationRequest $verificationRequest,
|
|
ProductVariant $variant,
|
|
array $variantData,
|
|
int $index,
|
|
): void {
|
|
$removeIds = $variantData['remove_media_ids'] ?? [];
|
|
|
|
if ($removeIds !== []) {
|
|
$variant->getMedia('images')
|
|
->whereIn('id', $removeIds)
|
|
->each->delete();
|
|
}
|
|
|
|
$this->copyRequestVariantImages($verificationRequest, $index, $variant);
|
|
}
|
|
|
|
private function copyRequestVariantImages(OwnerVerificationRequest $verificationRequest, int $index, ProductVariant $variant): void
|
|
{
|
|
$verificationRequest->getMedia($verificationRequest->variantImageCollection($index))
|
|
->each(fn ($media) => $media->copy($variant, 'images'));
|
|
}
|
|
|
|
private function applySorting(Builder $query, string $sort, string $direction): void
|
|
{
|
|
if (in_array($sort, ['name', 'slug', 'status'], true)) {
|
|
$query->orderBy($sort, $direction);
|
|
|
|
return;
|
|
}
|
|
|
|
$query->latest();
|
|
}
|
|
}
|