add more controllers and fix tests
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\PurchaseOrders;
|
||||
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Models\PurchaseOrderItem;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use RuntimeException;
|
||||
|
||||
class PurchaseOrderCreateService
|
||||
{
|
||||
public function create(array $payload): PurchaseOrder
|
||||
{
|
||||
$itemsPayload = $payload['items'] ?? [];
|
||||
if (empty($itemsPayload) || !is_array($itemsPayload)) {
|
||||
throw new RuntimeException('Add at least one line item.');
|
||||
}
|
||||
|
||||
$subtotal = 0.0;
|
||||
$items = [];
|
||||
|
||||
foreach ($itemsPayload as $item) {
|
||||
$supplyId = (int) ($item['supply_id'] ?? 0);
|
||||
$qty = max(0, (int) ($item['quantity'] ?? 0));
|
||||
$unitCost = (float) ($item['unit_cost'] ?? 0);
|
||||
|
||||
if ($supplyId <= 0 || $qty <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$line = $qty * $unitCost;
|
||||
$subtotal += $line;
|
||||
|
||||
$items[] = [
|
||||
'supply_id' => $supplyId,
|
||||
'description' => trim((string) ($item['description'] ?? '')),
|
||||
'quantity' => $qty,
|
||||
'unit_cost' => $unitCost,
|
||||
'received_qty' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($items)) {
|
||||
throw new RuntimeException('Valid line items required.');
|
||||
}
|
||||
|
||||
$status = (string) ($payload['status'] ?? 'ordered');
|
||||
$status = in_array($status, [PurchaseOrder::STATUS_DRAFT, PurchaseOrder::STATUS_ORDERED], true) ? $status : PurchaseOrder::STATUS_ORDERED;
|
||||
|
||||
$tax = 0.0;
|
||||
$total = $subtotal + $tax;
|
||||
|
||||
return DB::transaction(function () use ($payload, $items, $status, $subtotal, $tax, $total) {
|
||||
$po = PurchaseOrder::query()->create([
|
||||
'po_number' => $payload['po_number'] ?? null,
|
||||
'supplier_id' => $payload['supplier_id'] ?? null,
|
||||
'order_date' => $payload['order_date'] ?? null,
|
||||
'expected_date' => $payload['expected_date'] ?? null,
|
||||
'notes' => $payload['notes'] ?? null,
|
||||
'status' => $status,
|
||||
'subtotal' => $subtotal,
|
||||
'tax' => $tax,
|
||||
'total' => $total,
|
||||
]);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$item['purchase_order_id'] = $po->id;
|
||||
PurchaseOrderItem::query()->create($item);
|
||||
}
|
||||
|
||||
return $po->refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\PurchaseOrders;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PurchaseOrderQueryService
|
||||
{
|
||||
public function list(?string $q = null): array
|
||||
{
|
||||
$builder = DB::table('purchase_orders')
|
||||
->select('purchase_orders.*', 'suppliers.name as supplier_name')
|
||||
->leftJoin('suppliers', 'suppliers.id', '=', 'purchase_orders.supplier_id');
|
||||
|
||||
if ($q !== null && trim($q) !== '') {
|
||||
$query = trim($q);
|
||||
$builder->where(function ($qbuilder) use ($query) {
|
||||
$qbuilder->where('purchase_orders.po_number', 'like', "%{$query}%")
|
||||
->orWhere('suppliers.name', 'like', "%{$query}%");
|
||||
});
|
||||
}
|
||||
|
||||
return $builder->orderByDesc('purchase_orders.created_at')
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
}
|
||||
|
||||
public function options(): array
|
||||
{
|
||||
$suppliers = DB::table('suppliers')->orderBy('name')->get()->map(fn ($r) => (array) $r)->all();
|
||||
$supplies = DB::table('supplies')->orderBy('name')->get()->map(fn ($r) => (array) $r)->all();
|
||||
|
||||
return [
|
||||
'suppliers' => $suppliers,
|
||||
'supplies' => $supplies,
|
||||
];
|
||||
}
|
||||
|
||||
public function find(int $id): ?array
|
||||
{
|
||||
$po = DB::table('purchase_orders')
|
||||
->select('purchase_orders.*', 'suppliers.name as supplier_name')
|
||||
->leftJoin('suppliers', 'suppliers.id', '=', 'purchase_orders.supplier_id')
|
||||
->where('purchase_orders.id', $id)
|
||||
->first();
|
||||
|
||||
if (!$po) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$items = DB::table('purchase_order_items')
|
||||
->select('purchase_order_items.*', 'supplies.name as supply_name', 'supplies.unit as supply_unit')
|
||||
->leftJoin('supplies', 'supplies.id', '=', 'purchase_order_items.supply_id')
|
||||
->where('purchase_order_id', $id)
|
||||
->get()
|
||||
->map(fn ($row) => (array) $row)
|
||||
->all();
|
||||
|
||||
return [
|
||||
'purchase_order' => (array) $po,
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\PurchaseOrders;
|
||||
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Models\PurchaseOrderItem;
|
||||
use App\Models\Supply;
|
||||
use App\Models\SupplyTransaction;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use RuntimeException;
|
||||
|
||||
class PurchaseOrderReceiveService
|
||||
{
|
||||
public function receive(int $poId, array $received, string $issuedBy): array
|
||||
{
|
||||
$po = PurchaseOrder::query()->find($poId);
|
||||
if (!$po || in_array($po->status, [PurchaseOrder::STATUS_CANCELED, PurchaseOrder::STATUS_RECEIVED], true)) {
|
||||
throw new RuntimeException('PO not receivable.');
|
||||
}
|
||||
|
||||
if (empty($received)) {
|
||||
throw new RuntimeException('No items to receive.');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($po, $received, $issuedBy) {
|
||||
$completed = true;
|
||||
|
||||
foreach ($received as $item) {
|
||||
$itemId = (int) ($item['id'] ?? 0);
|
||||
$qty = (int) ($item['quantity'] ?? 0);
|
||||
if ($itemId <= 0 || $qty <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$poItem = PurchaseOrderItem::query()
|
||||
->where('purchase_order_id', $po->id)
|
||||
->where('id', $itemId)
|
||||
->first();
|
||||
|
||||
if (!$poItem) {
|
||||
$completed = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
$remaining = (int) $poItem->quantity - (int) $poItem->received_qty;
|
||||
$toReceive = min($remaining, $qty);
|
||||
if ($toReceive <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$poItem->received_qty = (int) $poItem->received_qty + $toReceive;
|
||||
$poItem->save();
|
||||
|
||||
$supply = Supply::query()->find($poItem->supply_id);
|
||||
if (!$supply) {
|
||||
$completed = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
$supply->qty_on_hand = (int) $supply->qty_on_hand + $toReceive;
|
||||
$supply->save();
|
||||
|
||||
SupplyTransaction::query()->create([
|
||||
'supply_id' => $supply->id,
|
||||
'type' => 'in',
|
||||
'quantity' => $toReceive,
|
||||
'ref' => 'PO ' . ($po->po_number ?? $po->id),
|
||||
'issued_to' => 'Inventory',
|
||||
'issued_by' => $issuedBy,
|
||||
'notes' => 'Received against PO',
|
||||
]);
|
||||
|
||||
if ($poItem->received_qty < $poItem->quantity) {
|
||||
$completed = false;
|
||||
}
|
||||
}
|
||||
|
||||
$po->status = $completed ? PurchaseOrder::STATUS_RECEIVED : PurchaseOrder::STATUS_ORDERED;
|
||||
$po->save();
|
||||
|
||||
return [
|
||||
'status' => $po->status,
|
||||
'completed' => $completed,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\PurchaseOrders;
|
||||
|
||||
use App\Models\PurchaseOrder;
|
||||
use RuntimeException;
|
||||
|
||||
class PurchaseOrderStatusService
|
||||
{
|
||||
public function cancel(int $poId): PurchaseOrder
|
||||
{
|
||||
$po = PurchaseOrder::query()->find($poId);
|
||||
if (!$po || $po->status === PurchaseOrder::STATUS_RECEIVED) {
|
||||
throw new RuntimeException('Cannot cancel this PO.');
|
||||
}
|
||||
|
||||
$po->status = PurchaseOrder::STATUS_CANCELED;
|
||||
$po->save();
|
||||
|
||||
return $po;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user