Email digital receipts, add promo settings, and apply tips at charge.
Deploy Ladill POS / deploy (push) Successful in 31s
Deploy Ladill POS / deploy (push) Successful in 31s
Send real receipt emails from the customer display and sale page, store branch promo content for the idle screen, and fold selected tips into totals.
This commit is contained in:
@@ -180,6 +180,7 @@ class RegisterController extends Controller
|
||||
'customer_phone' => ['nullable', 'string', 'max:40'],
|
||||
'crm_customer_id' => ['nullable', 'integer'],
|
||||
'loyalty_redeem_points' => ['nullable', 'integer', 'min:0'],
|
||||
'tip_minor' => ['nullable', 'integer', 'min:0'],
|
||||
'payment_method' => ['required', 'in:pay,cash'],
|
||||
]);
|
||||
|
||||
@@ -201,6 +202,7 @@ class RegisterController extends Controller
|
||||
'customer_phone' => $data['customer_phone'] ?? null,
|
||||
'crm_customer_id' => $data['crm_customer_id'] ?? null,
|
||||
'loyalty_redeem_points' => (int) ($data['loyalty_redeem_points'] ?? 0),
|
||||
'tip_minor' => (int) ($data['tip_minor'] ?? 0),
|
||||
]);
|
||||
|
||||
if ($data['payment_method'] === 'cash') {
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Http\Controllers\Pos\Concerns\ScopesToAccount;
|
||||
use App\Models\PosSale;
|
||||
use App\Services\CrossApp\CrossAppLinkService;
|
||||
use App\Services\Pos\PosSaleService;
|
||||
use App\Services\Pos\ReceiptEmailService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
@@ -19,6 +20,7 @@ class SaleController extends Controller
|
||||
public function __construct(
|
||||
private PosSaleService $sales,
|
||||
private CrossAppLinkService $links,
|
||||
private ReceiptEmailService $receiptEmail,
|
||||
) {}
|
||||
|
||||
public function index(Request $request): View
|
||||
@@ -69,6 +71,27 @@ class SaleController extends Controller
|
||||
return redirect()->route('pos.sales.show', $sale)->with('success', 'Sale cancelled.');
|
||||
}
|
||||
|
||||
public function emailReceipt(Request $request, PosSale $sale): RedirectResponse
|
||||
{
|
||||
$this->authorizeOwner($request, $sale);
|
||||
|
||||
if (! $sale->isPaid()) {
|
||||
return back()->with('error', 'Only paid sales can be emailed as receipts.');
|
||||
}
|
||||
|
||||
$data = $request->validate([
|
||||
'email' => ['required', 'email', 'max:255'],
|
||||
]);
|
||||
|
||||
try {
|
||||
$result = $this->receiptEmail->send($sale, $data['email']);
|
||||
} catch (RuntimeException $e) {
|
||||
return back()->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
return back()->with('success', $result['message']);
|
||||
}
|
||||
|
||||
public function destroy(Request $request, PosSale $sale): RedirectResponse
|
||||
{
|
||||
$this->authorizeOwner($request, $sale);
|
||||
|
||||
@@ -68,6 +68,10 @@ class SettingsController extends Controller
|
||||
'remove_receipt_logo' => ['sometimes', 'boolean'],
|
||||
'printer_paper_mm' => ['required', 'in:58,80'],
|
||||
'printer_auto_print' => ['sometimes', 'boolean'],
|
||||
'customer_promo_headline' => ['nullable', 'string', 'max:160'],
|
||||
'customer_promo_body' => ['nullable', 'string', 'max:500'],
|
||||
'customer_promo_image' => ['nullable', 'image', 'mimes:jpeg,jpg,png,gif,webp', 'max:2048'],
|
||||
'remove_customer_promo_image' => ['sometimes', 'boolean'],
|
||||
'gateway_provider' => ['nullable', Rule::in([
|
||||
PaymentGatewaySetting::PROVIDER_PAYSTACK,
|
||||
PaymentGatewaySetting::PROVIDER_FLUTTERWAVE,
|
||||
@@ -103,6 +107,21 @@ class SettingsController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
if ($request->boolean('remove_customer_promo_image') && $location->customer_promo_image_path) {
|
||||
Storage::disk('public')->delete($location->customer_promo_image_path);
|
||||
$location->customer_promo_image_path = null;
|
||||
}
|
||||
|
||||
if ($request->hasFile('customer_promo_image')) {
|
||||
if ($location->customer_promo_image_path) {
|
||||
Storage::disk('public')->delete($location->customer_promo_image_path);
|
||||
}
|
||||
$location->customer_promo_image_path = $request->file('customer_promo_image')->store(
|
||||
'pos/customer-promo/'.$this->ownerRef($request),
|
||||
'public'
|
||||
);
|
||||
}
|
||||
|
||||
$location->update([
|
||||
'name' => $data['name'],
|
||||
'currency' => strtoupper($data['currency']),
|
||||
@@ -112,6 +131,9 @@ class SettingsController extends Controller
|
||||
'printer_paper_mm' => (int) $data['printer_paper_mm'],
|
||||
'printer_auto_print' => $request->boolean('printer_auto_print'),
|
||||
'receipt_logo_path' => $location->receipt_logo_path,
|
||||
'customer_promo_headline' => $data['customer_promo_headline'] ?? null,
|
||||
'customer_promo_body' => $data['customer_promo_body'] ?? null,
|
||||
'customer_promo_image_path' => $location->customer_promo_image_path,
|
||||
]);
|
||||
|
||||
$this->persistGateway($request, $user);
|
||||
|
||||
@@ -4,15 +4,21 @@ namespace App\Http\Controllers\Public;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\PosCustomerDisplay;
|
||||
use App\Models\PosSale;
|
||||
use App\Services\Pos\CustomerDisplayService;
|
||||
use App\Services\Pos\ReceiptEmailService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class CustomerDisplayController extends Controller
|
||||
{
|
||||
public function __construct(private CustomerDisplayService $displays) {}
|
||||
public function __construct(
|
||||
private CustomerDisplayService $displays,
|
||||
private ReceiptEmailService $receiptEmail,
|
||||
) {}
|
||||
|
||||
public function show(string $token): View
|
||||
{
|
||||
@@ -122,6 +128,9 @@ class CustomerDisplayController extends Controller
|
||||
|
||||
if ($data['type'] === 'receipt_email') {
|
||||
$action['email'] = $data['email'] ?? null;
|
||||
$emailResult = $this->emailReceiptForDisplay($display, (string) ($data['email'] ?? ''));
|
||||
$action['email_sent'] = $emailResult['ok'];
|
||||
$action['email_message'] = $emailResult['message'];
|
||||
}
|
||||
|
||||
if ($data['type'] === 'payment_option') {
|
||||
@@ -133,9 +142,43 @@ class CustomerDisplayController extends Controller
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'state' => $this->displays->publicState($display->fresh()),
|
||||
'email_sent' => $action['email_sent'] ?? null,
|
||||
'message' => $action['email_message'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return array{ok: bool, message: string} */
|
||||
private function emailReceiptForDisplay(PosCustomerDisplay $display, string $email): array
|
||||
{
|
||||
$payload = is_array($display->payload) ? $display->payload : [];
|
||||
$saleId = (int) ($payload['sale_id'] ?? $payload['receipt']['sale_id'] ?? 0);
|
||||
$reference = (string) ($payload['sale_reference'] ?? $payload['receipt']['reference'] ?? '');
|
||||
|
||||
$sale = null;
|
||||
if ($saleId > 0) {
|
||||
$sale = PosSale::query()
|
||||
->where('id', $saleId)
|
||||
->where('location_id', $display->location_id)
|
||||
->first();
|
||||
}
|
||||
if (! $sale && $reference !== '') {
|
||||
$sale = PosSale::query()
|
||||
->where('reference', $reference)
|
||||
->where('location_id', $display->location_id)
|
||||
->first();
|
||||
}
|
||||
|
||||
if (! $sale) {
|
||||
return ['ok' => false, 'message' => 'No receipt is ready to email yet.'];
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->receiptEmail->send($sale, $email);
|
||||
} catch (RuntimeException $e) {
|
||||
return ['ok' => false, 'message' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
private function find(string $token): PosCustomerDisplay
|
||||
{
|
||||
return PosCustomerDisplay::query()
|
||||
|
||||
Reference in New Issue
Block a user