add more controllers and fix tests

This commit is contained in:
root
2026-03-09 11:54:13 -04:00
parent 0c3e9b16f7
commit 1cb3573d4b
74 changed files with 2761 additions and 2728 deletions
@@ -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;
}
}