On a sale's detail page, unpaid sales (retail or restaurant) can now be cancelled or deleted: - Cancel (pending only) marks it cancelled and frees its table / clears it from the kitchen (PosSaleService::cancelSale). - Delete removes the sale and cascades its lines, modifiers and payments, freeing the table first (deleteSale). Paid sales are protected — they can't be deleted. Status badge now distinguishes cancelled/failed. Covered by PosRestaurantTest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5d8e185223
commit
f64a28cb21
@@ -44,6 +44,32 @@ class SaleController extends Controller
|
||||
return view('pos.sales.show', compact('sale', 'invoiceUrl'));
|
||||
}
|
||||
|
||||
public function cancel(Request $request, PosSale $sale): RedirectResponse
|
||||
{
|
||||
$this->authorizeOwner($request, $sale);
|
||||
|
||||
try {
|
||||
$this->sales->cancelSale($sale);
|
||||
} catch (RuntimeException $e) {
|
||||
return back()->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
return redirect()->route('pos.sales.show', $sale)->with('success', 'Sale cancelled.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, PosSale $sale): RedirectResponse
|
||||
{
|
||||
$this->authorizeOwner($request, $sale);
|
||||
|
||||
try {
|
||||
$this->sales->deleteSale($sale);
|
||||
} catch (RuntimeException $e) {
|
||||
return back()->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
return redirect()->route('pos.sales.index')->with('success', 'Sale deleted.');
|
||||
}
|
||||
|
||||
public function callback(Request $request, PosSale $sale): RedirectResponse
|
||||
{
|
||||
$reference = trim((string) $request->query('reference', $sale->payment_reference ?? ''));
|
||||
|
||||
@@ -446,6 +446,33 @@ class PosSaleService
|
||||
return $sale->fresh('lines');
|
||||
}
|
||||
|
||||
/** Cancel a pending sale/ticket — frees its table and clears it from the kitchen. */
|
||||
public function cancelSale(PosSale $sale): void
|
||||
{
|
||||
if ($sale->status !== PosSale::STATUS_PENDING) {
|
||||
throw new RuntimeException('Only pending sales can be cancelled.');
|
||||
}
|
||||
|
||||
$sale->forceFill(['status' => PosSale::STATUS_CANCELLED])->save();
|
||||
$this->closeTicket($sale);
|
||||
}
|
||||
|
||||
/** Permanently delete an unpaid sale (and its lines, modifiers, payments). */
|
||||
public function deleteSale(PosSale $sale): void
|
||||
{
|
||||
if ($sale->status === PosSale::STATUS_PAID) {
|
||||
throw new RuntimeException('Paid sales cannot be deleted.');
|
||||
}
|
||||
|
||||
if ($sale->table_id) {
|
||||
PosTable::where('id', $sale->table_id)
|
||||
->where('current_sale_id', $sale->id)
|
||||
->update(['status' => PosTable::STATUS_FREE, 'current_sale_id' => null]);
|
||||
}
|
||||
|
||||
$sale->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close a ticket once it's settled: free its table and mark kitchen done.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user