feat: implement stock transfer functionality for product variants with validation and notifications
This commit is contained in:
parent
fbd7aa5862
commit
15161b6ea4
@ -4,6 +4,7 @@
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\Master\Product\ProductVariantRequest;
|
||||
use App\Http\Requests\Admin\Master\Product\TransferStockRequest;
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Services\Admin\Master\Product\ProductVariantService;
|
||||
@ -43,4 +44,13 @@ public function destroy(Product $product, ProductVariant $variant): RedirectResp
|
||||
'admin.master.products.index'
|
||||
);
|
||||
}
|
||||
|
||||
public function transferStock(TransferStockRequest $request, Product $product, ProductVariant $variant): RedirectResponse
|
||||
{
|
||||
return $this->handleAction(
|
||||
fn () => $this->variantService->transferStock($variant, $request->validated()),
|
||||
'Transfer stok berhasil dilakukan.',
|
||||
'admin.master.products.index'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Master\Product;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class TransferStockRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'quantity' => ['required', 'integer', 'min:1'],
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'quantity' => 'Jumlah Transfer',
|
||||
'description' => 'Keterangan',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -45,4 +45,10 @@ public function stokOpnameItems(): HasMany
|
||||
{
|
||||
return $this->hasMany(StokOpnameItem::class);
|
||||
}
|
||||
|
||||
public function stockMutations(): HasMany
|
||||
{
|
||||
return $this->hasMany(StockMutation::class, 'stockable_id')
|
||||
->where('stockable_type', self::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,10 +5,12 @@
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductPrice;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\StockMutation;
|
||||
use App\Services\Concerns\RegistersMedia;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\S3PresignedService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class ProductVariantService
|
||||
{
|
||||
@ -113,4 +115,58 @@ public function getTemporaryUrl(ProductVariant $variant): ?string
|
||||
|
||||
return $media ? $this->s3Service->getTemporaryUrl($media->file_name) : null;
|
||||
}
|
||||
|
||||
public function transferStock(ProductVariant $variant, array $data): ProductVariant
|
||||
{
|
||||
$quantity = (int) $data['quantity'];
|
||||
|
||||
if ($variant->stock < $quantity) {
|
||||
throw ValidationException::withMessages([
|
||||
'quantity' => "Stok bagus tidak mencukupi. Stok tersedia: {$variant->stock}.",
|
||||
]);
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($variant, $quantity, $data) {
|
||||
$stockBefore = $variant->stock;
|
||||
$retailBefore = $variant->retail_stock;
|
||||
|
||||
$variant->update([
|
||||
'stock' => $stockBefore - $quantity,
|
||||
'retail_stock' => $retailBefore + $quantity,
|
||||
]);
|
||||
|
||||
StockMutation::create([
|
||||
'user_id' => auth()->id(),
|
||||
'stockable_type' => ProductVariant::class,
|
||||
'stockable_id' => $variant->id,
|
||||
'type' => 'out',
|
||||
'quantity' => -$quantity,
|
||||
'stock_before' => $stockBefore,
|
||||
'stock_after' => $stockBefore - $quantity,
|
||||
'stock_quality' => 'good',
|
||||
'description' => $data['description'] ?? 'Transfer stok bagus ke stok ecer',
|
||||
]);
|
||||
|
||||
StockMutation::create([
|
||||
'user_id' => auth()->id(),
|
||||
'stockable_type' => ProductVariant::class,
|
||||
'stockable_id' => $variant->id,
|
||||
'type' => 'in',
|
||||
'quantity' => $quantity,
|
||||
'stock_before' => $retailBefore,
|
||||
'stock_after' => $retailBefore + $quantity,
|
||||
'stock_quality' => 'retail',
|
||||
'description' => $data['description'] ?? 'Transfer stok bagus ke stok ecer',
|
||||
]);
|
||||
});
|
||||
|
||||
NotificationService::notify(
|
||||
roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'],
|
||||
title: 'Transfer Stok',
|
||||
body: "{$quantity} unit dari varian \"{$variant->name}\" berhasil ditransfer dari stok bagus ke stok ecer".' oleh '.auth()->user()->full_name.'.',
|
||||
url: route('admin.master.products.index'),
|
||||
);
|
||||
|
||||
return $variant->fresh();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Pencil, Trash2 } from 'lucide-react';
|
||||
import { ArrowRightLeft, Pencil, Trash2 } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { ImagePreviewModal } from '@/components/image-preview-modal';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@ -16,7 +16,8 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import type { Product, ProductVariant } from './columns';
|
||||
import type { Product, ProductVariant } from '../columns';
|
||||
import { TransferStockDialog } from './transfer-stock-dialog';
|
||||
|
||||
function formatCurrency(amount: number): string {
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
@ -65,6 +66,10 @@ export function VariantSubRow({
|
||||
onDeleteVariantClick: (product: Product, variant: ProductVariant) => void;
|
||||
}) {
|
||||
const variants = product.product_variants ?? [];
|
||||
const [transferVariant, setTransferVariant] = useState<{
|
||||
product: Product;
|
||||
variant: ProductVariant;
|
||||
} | null>(null);
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
@ -84,7 +89,7 @@ export function VariantSubRow({
|
||||
</TableHead>
|
||||
<TableHead className="text-center">Stok Ecer</TableHead>
|
||||
<TableHead>Harga</TableHead>
|
||||
<TableHead className="w-[80px] text-center">
|
||||
<TableHead className="w-[120px] text-center">
|
||||
Aksi
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
@ -151,6 +156,25 @@ export function VariantSubRow({
|
||||
<TableCell>
|
||||
<TooltipProvider>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
setTransferVariant({
|
||||
product,
|
||||
variant,
|
||||
})
|
||||
}
|
||||
>
|
||||
<ArrowRightLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
Transfer Stok
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
@ -197,6 +221,19 @@ export function VariantSubRow({
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{transferVariant && (
|
||||
<TransferStockDialog
|
||||
open={transferVariant !== null}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setTransferVariant(null);
|
||||
}
|
||||
}}
|
||||
product={transferVariant.product}
|
||||
variant={transferVariant.variant}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,121 @@
|
||||
import { Form } from '@inertiajs/react';
|
||||
import InputError from '@/components/input-error';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { transferStock } from '@/routes/admin/master/products/variants';
|
||||
import type { Product, ProductVariant } from '../columns';
|
||||
|
||||
type TransferStockDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
product: Product;
|
||||
variant: ProductVariant;
|
||||
};
|
||||
|
||||
export function TransferStockDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
product,
|
||||
variant,
|
||||
}: TransferStockDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<Form
|
||||
action={transferStock.post({
|
||||
product: product.id,
|
||||
variant: variant.id,
|
||||
})}
|
||||
resetOnSuccess
|
||||
onSuccess={() => onOpenChange(false)}
|
||||
>
|
||||
{({ errors, processing }) => (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Transfer Stok Bagus → Stok Ecer
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
Produk
|
||||
</Label>
|
||||
<p className="text-sm font-medium">
|
||||
{product.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
Varian
|
||||
</Label>
|
||||
<p className="text-sm font-medium">
|
||||
{variant.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
Stok Bagus Tersedia
|
||||
</Label>
|
||||
<p className="text-sm font-medium">
|
||||
{variant.stock}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="quantity">
|
||||
Jumlah Transfer{' '}
|
||||
<span className="text-destructive">
|
||||
*
|
||||
</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="quantity"
|
||||
name="quantity"
|
||||
type="number"
|
||||
min={1}
|
||||
max={variant.stock}
|
||||
placeholder="Masukkan jumlah transfer"
|
||||
/>
|
||||
<InputError message={errors.quantity} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">
|
||||
Keterangan
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
name="description"
|
||||
placeholder="Masukkan keterangan"
|
||||
rows={3}
|
||||
/>
|
||||
<InputError message={errors.description} />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button type="submit" disabled={processing}>
|
||||
{processing ? 'Menyimpan...' : 'Transfer'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -37,6 +37,7 @@
|
||||
Route::delete('products/{product}/variants/{variant}', [ProductVariantController::class, 'destroy'])->name('products.variants.destroy');
|
||||
Route::get('products/{product}/variants/{variant}/edit', [ProductVariantController::class, 'edit'])->name('products.variants.edit');
|
||||
Route::put('products/{product}/variants/{variant}', [ProductVariantController::class, 'update'])->name('products.variants.update');
|
||||
Route::post('products/{product}/variants/{variant}/transfer-stock', [ProductVariantController::class, 'transferStock'])->name('products.variants.transfer-stock');
|
||||
Route::resource('suppliers', SupplierController::class)->except(['show', 'create', 'edit']);
|
||||
Route::resource('customers', CustomerController::class)->except(['show', 'create', 'edit']);
|
||||
});
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
use App\Models\Product;
|
||||
use App\Models\ProductPrice;
|
||||
use App\Models\ProductVariant;
|
||||
use App\Models\StockMutation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@ -2266,3 +2267,262 @@ function allPriceTypes(): array
|
||||
$response = $this->delete(route('admin.master.products.variants.destroy', [$product, 99999]));
|
||||
$response->assertStatus(404);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| TRANSFER STOCK (BAGUS -> ECER)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('guest cannot transfer stock', function () {
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->for($product)->create(['stock' => 100]);
|
||||
|
||||
$response = $this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), [
|
||||
'quantity' => 10,
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
|
||||
test('authenticated user can transfer stock', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->for($product)->create([
|
||||
'stock' => 100,
|
||||
'retail_stock' => 20,
|
||||
]);
|
||||
|
||||
$response = $this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), [
|
||||
'quantity' => 10,
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$variant->refresh();
|
||||
expect($variant->stock)->toBe(90);
|
||||
expect($variant->retail_stock)->toBe(30);
|
||||
});
|
||||
|
||||
test('transfer stock creates two stock mutation records', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->for($product)->create(['stock' => 100, 'retail_stock' => 20]);
|
||||
|
||||
$this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), [
|
||||
'quantity' => 10,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseCount('stock_mutations', 2);
|
||||
|
||||
$outMutation = StockMutation::where('stockable_id', $variant->id)->where('type', 'out')->first();
|
||||
expect($outMutation)->not->toBeNull();
|
||||
expect((float) $outMutation->quantity)->toBe(-10.0);
|
||||
expect((float) $outMutation->stock_before)->toBe(100.0);
|
||||
expect((float) $outMutation->stock_after)->toBe(90.0);
|
||||
expect($outMutation->stock_quality)->toBe('good');
|
||||
expect($outMutation->user_id)->toBe($user->id);
|
||||
|
||||
$inMutation = StockMutation::where('stockable_id', $variant->id)->where('type', 'in')->first();
|
||||
expect($inMutation)->not->toBeNull();
|
||||
expect((float) $inMutation->quantity)->toBe(10.0);
|
||||
expect((float) $inMutation->stock_before)->toBe(20.0);
|
||||
expect((float) $inMutation->stock_after)->toBe(30.0);
|
||||
expect($inMutation->stock_quality)->toBe('retail');
|
||||
expect($inMutation->user_id)->toBe($user->id);
|
||||
});
|
||||
|
||||
test('transfer stock with description saves to mutations', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->for($product)->create(['stock' => 50, 'retail_stock' => 10]);
|
||||
|
||||
$this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), [
|
||||
'quantity' => 5,
|
||||
'description' => 'Transfer untuk display toko',
|
||||
]);
|
||||
|
||||
$mutation = StockMutation::where('stockable_id', $variant->id)->where('type', 'out')->first();
|
||||
expect($mutation->description)->toBe('Transfer untuk display toko');
|
||||
});
|
||||
|
||||
test('transfer stock without description uses default', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->for($product)->create(['stock' => 50, 'retail_stock' => 10]);
|
||||
|
||||
$this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), [
|
||||
'quantity' => 5,
|
||||
]);
|
||||
|
||||
$mutation = StockMutation::where('stockable_id', $variant->id)->where('type', 'out')->first();
|
||||
expect($mutation->description)->toBe('Transfer stok bagus ke stok ecer');
|
||||
});
|
||||
|
||||
test('transfer stock quantity must be provided', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->for($product)->create(['stock' => 100]);
|
||||
|
||||
$response = $this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), []);
|
||||
|
||||
$variant->refresh();
|
||||
expect($variant->stock)->toBe(100);
|
||||
$this->assertDatabaseCount('stock_mutations', 0);
|
||||
});
|
||||
|
||||
test('transfer stock quantity must be integer', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->for($product)->create(['stock' => 100]);
|
||||
|
||||
$response = $this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), [
|
||||
'quantity' => 'abc',
|
||||
]);
|
||||
|
||||
$variant->refresh();
|
||||
expect($variant->stock)->toBe(100);
|
||||
$this->assertDatabaseCount('stock_mutations', 0);
|
||||
});
|
||||
|
||||
test('transfer stock quantity must be at least 1', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->for($product)->create(['stock' => 100]);
|
||||
|
||||
$response = $this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), [
|
||||
'quantity' => 0,
|
||||
]);
|
||||
|
||||
$variant->refresh();
|
||||
expect($variant->stock)->toBe(100);
|
||||
$this->assertDatabaseCount('stock_mutations', 0);
|
||||
});
|
||||
|
||||
test('transfer stock quantity cannot exceed available stock', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->for($product)->create(['stock' => 10, 'retail_stock' => 5]);
|
||||
|
||||
$response = $this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), [
|
||||
'quantity' => 15,
|
||||
]);
|
||||
|
||||
$variant->refresh();
|
||||
expect($variant->stock)->toBe(10);
|
||||
expect($variant->retail_stock)->toBe(5);
|
||||
$this->assertDatabaseCount('stock_mutations', 0);
|
||||
});
|
||||
|
||||
test('transfer stock exactly equal to available stock is allowed', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->for($product)->create(['stock' => 10, 'retail_stock' => 5]);
|
||||
|
||||
$response = $this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), [
|
||||
'quantity' => 10,
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$variant->refresh();
|
||||
expect($variant->stock)->toBe(0);
|
||||
expect($variant->retail_stock)->toBe(15);
|
||||
});
|
||||
|
||||
test('transfer stock from zero stock is rejected', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->for($product)->create(['stock' => 0, 'retail_stock' => 0]);
|
||||
|
||||
$response = $this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), [
|
||||
'quantity' => 1,
|
||||
]);
|
||||
|
||||
$variant->refresh();
|
||||
expect($variant->stock)->toBe(0);
|
||||
expect($variant->retail_stock)->toBe(0);
|
||||
$this->assertDatabaseCount('stock_mutations', 0);
|
||||
});
|
||||
|
||||
test('transfer stock multiple times sequentially', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->for($product)->create(['stock' => 100, 'retail_stock' => 0]);
|
||||
|
||||
$this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), ['quantity' => 20]);
|
||||
$this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), ['quantity' => 30]);
|
||||
$this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), ['quantity' => 10]);
|
||||
|
||||
$variant->refresh();
|
||||
expect($variant->stock)->toBe(40);
|
||||
expect($variant->retail_stock)->toBe(60);
|
||||
$this->assertDatabaseCount('stock_mutations', 6);
|
||||
});
|
||||
|
||||
test('transfer stock does not affect reject_stock', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->for($product)->create([
|
||||
'stock' => 100,
|
||||
'reject_stock' => 50,
|
||||
'retail_stock' => 10,
|
||||
]);
|
||||
|
||||
$this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), [
|
||||
'quantity' => 25,
|
||||
]);
|
||||
|
||||
$variant->refresh();
|
||||
expect($variant->stock)->toBe(75);
|
||||
expect($variant->reject_stock)->toBe(50);
|
||||
expect($variant->retail_stock)->toBe(35);
|
||||
});
|
||||
|
||||
test('transfer stock on non-existent variant returns 404', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
|
||||
$response = $this->post(route('admin.master.products.variants.transfer-stock', [$product, 99999]), [
|
||||
'quantity' => 10,
|
||||
]);
|
||||
$response->assertStatus(404);
|
||||
});
|
||||
|
||||
test('transfer stock redirects to product index', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$variant = ProductVariant::factory()->for($product)->create(['stock' => 100]);
|
||||
|
||||
$response = $this->post(route('admin.master.products.variants.transfer-stock', [$product, $variant]), [
|
||||
'quantity' => 10,
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('admin.master.products.index'));
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user