Compare commits

...

8 Commits

15 changed files with 406 additions and 27 deletions
+3
View File
@@ -43,3 +43,6 @@ NORTH_CHECKOUT_ID=
NORTH_PROFILE_ID=
NORTH_PRIVATE_API_KEY=
AUTO_PRINT_ENABLED=false
PRINTER_EMAIL=
+1 -1
View File
@@ -1 +1 @@
{"version":2,"defects":{"Tests\\Feature\\MyWinningsTest::test_mywinnings_form_is_accessible":8,"Tests\\Feature\\MyWinningsTest::test_mywinnings_results_show_correct_data":8,"Tests\\Feature\\MyWinningsTest::test_mywinnings_invalid_bidder_shows_error":8},"times":{"Tests\\Unit\\PhoneFormattingTest::it_formats_a_10_digit_phone_number":0.018,"Tests\\Unit\\PhoneFormattingTest::it_strips_non_numeric_characters_before_formatting":0,"Tests\\Unit\\PhoneFormattingTest::it_returns_original_value_if_not_10_digits":0,"Tests\\Unit\\PhoneFormattingTest::bidder_model_accessor_formats_phone_number":0.003}}
{"version":2,"defects":{"Tests\\Feature\\MyWinningsTest::test_mywinnings_form_is_accessible":8,"Tests\\Feature\\MyWinningsTest::test_mywinnings_results_show_correct_data":8,"Tests\\Feature\\MyWinningsTest::test_mywinnings_invalid_bidder_shows_error":8,"Tests\\Unit\\PrinterServiceTest::test_printPickupSlip_sends_email_when_enabled":7},"times":{"Tests\\Unit\\PhoneFormattingTest::it_formats_a_10_digit_phone_number":0.018,"Tests\\Unit\\PhoneFormattingTest::it_strips_non_numeric_characters_before_formatting":0,"Tests\\Unit\\PhoneFormattingTest::it_returns_original_value_if_not_10_digits":0,"Tests\\Unit\\PhoneFormattingTest::bidder_model_accessor_formats_phone_number":0.003,"Tests\\Unit\\PrinterServiceTest::test_printPickupSlip_sends_email_when_enabled":0.019,"Tests\\Unit\\PrinterServiceTest::test_printPickupSlip_does_not_send_email_when_disabled":0.001,"Tests\\Unit\\PrinterServiceTest::test_printForCheckout_calls_printPickupSlip_with_correct_data":0.09}}
@@ -9,4 +9,14 @@ use Filament\Resources\Pages\CreateRecord;
class CreateCheckout extends CreateRecord
{
protected static string $resource = CheckoutResource::class;
protected function afterCreate(): void
{
try {
$printerService = app(\App\Services\PrinterService::class);
$printerService->printForCheckout($this->record);
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Auto-print failed from Filament: ' . $e->getMessage());
}
}
}
@@ -197,6 +197,14 @@ class NorthCheckoutController extends Controller
GROUP BY winning_bids.winning_bidder_num
");
// Automatically print item pickup slips
try {
$printerService = app(\App\Services\PrinterService::class);
$printerService->printForCheckout(\App\Models\Checkout::find($checkout_id));
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Auto-print failed: ' . $e->getMessage());
}
return view('checkout_complete', [
'checkout_result' => $checkout_id,
'checkout_list_results' => $checkout_list_results,
+14 -1
View File
@@ -141,6 +141,15 @@ class PagesController extends Controller
'cc_amount' => $cc_amount,
]
);
// Automatically print item pickup slips
try {
$printerService = app(\App\Services\PrinterService::class);
$printerService->printForCheckout(\App\Models\Checkout::find($checkout_result));
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Auto-print failed: ' . $e->getMessage());
}
return view('checkout_complete', [
'checkout_result' => $checkout_result,
'checkout_list_results' => $checkout_list_results,
@@ -351,7 +360,11 @@ class PagesController extends Controller
public function winnersbyitem()
{
$winnersbyitem_results = WinningBids::with(['items', 'bidders'])->get();
$winnersbyitem_results = WinningBids::with(['items', 'bidders'])
->join('items', 'winning_bids.winning_item_num', '=', 'items.iditems')
->orderByRaw('CAST(items.item_assigned_num AS UNSIGNED) ASC')
->select('winning_bids.*')
->get();
return view('winnersbyitem', ['winnersbyitem_results' => $winnersbyitem_results]);
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class PickupSlipMail extends Mailable
{
use Queueable, SerializesModels;
protected $pdfOutput;
/**
* Create a new message instance.
*/
public function __construct($pdfOutput)
{
$this->pdfOutput = $pdfOutput;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
subject: 'Item Pickup Slip',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'emails.pickup_slip',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, Attachment>
*/
public function attachments(): array
{
return [
Attachment::fromData(fn () => $this->pdfOutput, 'pickup_slip.pdf')
->withMime('application/pdf'),
];
}
}
+86
View File
@@ -0,0 +1,86 @@
<?php
namespace App\Services;
use Dompdf\Dompdf;
use Dompdf\Options;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
class PrinterService
{
/**
* Print the pickup slip for a given Checkout model.
*
* @param \App\Models\Checkout $checkout
* @return void
*/
public function printForCheckout(\App\Models\Checkout $checkout)
{
$bidder_num = $checkout->bidder_num;
$checkout_list_results = \Illuminate\Support\Facades\DB::select("SELECT
*, items.item_assigned_num, items.item_desc
FROM winning_bids
INNER JOIN items AS items
ON winning_bids.winning_item_num=items.iditems
WHERE winning_bidder_num = $bidder_num
");
$checkout_info_results = \Illuminate\Support\Facades\DB::select("SELECT
winning_bids.*,
bidders.*,
sum(winning_cost) AS total_cost
FROM winning_bids
INNER JOIN bidders AS bidders
ON winning_bids.winning_bidder_num=bidders.idbidders
WHERE winning_bidder_num = $bidder_num
GROUP BY winning_bids.winning_bidder_num
");
$this->printPickupSlip([
'checkout_final_results' => $checkout,
'checkout_list_results' => $checkout_list_results,
'checkout_info_results' => $checkout_info_results
]);
}
/**
* Print the pickup slip by sending it to the HP ePrint email address.
*
* @param array $checkoutData
* @return void
*/
public function printPickupSlip($checkoutData)
{
if (!config('services.printer.enabled')) {
Log::info('Auto-print is disabled.');
return;
}
$printerEmail = config('services.printer.email');
if (empty($printerEmail)) {
Log::error('Printer email is not configured.');
return;
}
try {
$options = new Options();
$options->set('isHtml5ParserEnabled', true);
$options->set('isRemoteEnabled', true);
$dompdf = new Dompdf($options);
$html = view('receiptpdf', $checkoutData)->render();
$dompdf->loadHtml($html);
$dompdf->setPaper('letter', 'portrait');
$dompdf->render();
$pdfOutput = $dompdf->output();
Mail::to($printerEmail)->send(new \App\Mail\PickupSlipMail($pdfOutput));
Log::info('Pickup slip sent to printer: ' . $printerEmail);
} catch (\Exception $e) {
Log::error('Failed to print pickup slip: ' . $e->getMessage());
}
}
}
+5
View File
@@ -48,4 +48,9 @@ return [
'private_api_key' => env('NORTH_PRIVATE_API_KEY'),
],
'printer' => [
'enabled' => env('AUTO_PRINT_ENABLED', false),
'email' => env('PRINTER_EMAIL'),
],
];
+3
View File
@@ -58,6 +58,7 @@ The Laravel framework is open-sourced software licensed under the [MIT license](
- **Searchable Dropdowns:** Integrated `TomSelect` across various forms (Checkout, Reprint Receipt, etc.) for enhanced usability.
- **Self-Checkout Flow:** Added a prominent self-checkout link for bidders, integrated with North Embedded Checkout.
- **Pickup Instructions:** Added clear instructions for bidders on the checkout completion page to proceed to the Item Pickup Table.
- **North Checkout Fallback:** Restored the manual "Verify Payment Status" button and enhanced error reporting for digital wallet payments (Google Pay).
#### Fixed
- **Logo Integration:** Corrected logo sizing issues using inline styles to override theme constraints.
@@ -66,6 +67,8 @@ The Laravel framework is open-sourced software licensed under the [MIT license](
- **Data Restoration:** Re-added bidder information and itemized winning lists to the checkout completion views.
#### Changed
- **Expanded Reports:** Standardized `bidderlist`, `showcarlist`, and `checkout_complete_list` to show full contact details (Address, City, State, Zip, Phone, Email).
- **Reporting Enhancements:** Added Year, Make, and Model to the `showcarlist` and included item numbers in the `checkout_complete_list` for improved tracking.
- **UI Cleanup:** Disabled default Tablar footers and notifications across all layout templates to maintain a focused auction interface.
- **Theme Consistency:** Updated various partials to ensure a seamless transition from the previous custom theme to Tablar.
+14 -6
View File
@@ -17,24 +17,32 @@
<div class="col-12">
<div class="card">
<div class="table-responsive">
<table class="table table-vcenter card-table">
<table class="table table-vcenter card-table text-nowrap">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Last Name</th>
<th>First Name</th>
<th>Address</th>
<th>City</th>
<th>State</th>
<th>Zip</th>
<th>Phone</th>
<th>Email</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach($bidderlist_results as $bidder)
<tr>
<td>{{ $bidder->bidder_assigned_number }}</td>
<td>{{ $bidder->bidder_fname }} {{ $bidder->bidder_lname }}</td>
<td>{{ $bidder->bidder_phone }}</td>
<td>{{ $bidder->bidder_lname }}</td>
<td>{{ $bidder->bidder_fname }}</td>
<td>{{ $bidder->bidder_addr }}</td>
<td>{{ $bidder->bidder_city }}</td>
<td>{{ $bidder->bidder_state }}</td>
<td>{{ $bidder->bidder_zip }}</td>
<td>{{ \App\Helpers\PhoneHelper::format($bidder->bidder_phone) }}</td>
<td>{{ $bidder->bidder_email }}</td>
<td>{{ $bidder->bidder_addr }}, {{ $bidder->bidder_city }}</td>
</tr>
@endforeach
</tbody>
@@ -16,28 +16,47 @@
<div class="col-12">
<div class="card">
<div class="table-responsive">
<table class="table table-vcenter card-table">
<table class="table table-vcenter card-table text-nowrap">
<thead>
<tr>
<th>Bidder #</th>
<th>Name</th>
<th>Items Won</th>
<th>#</th>
<th>Last Name</th>
<th>First Name</th>
<th>Address</th>
<th>City</th>
<th>State</th>
<th>Zip</th>
<th>Phone</th>
<th>Email</th>
<th>Total</th>
<th>Payment</th>
<th>Payment Method</th>
<th>Items Won</th>
</tr>
</thead>
<tbody>
@foreach($checkout_complete_results as $c)
<tr>
<td>{{ $c->bidder_assigned_number }}</td>
<td>{{ $c->bidder_fname }} {{ $c->bidder_lname }}</td>
<td>{{ $c->bidder_lname }}</td>
<td>{{ $c->bidder_fname }}</td>
<td>{{ $c->bidder_addr }}</td>
<td>{{ $c->bidder_city }}</td>
<td>{{ $c->bidder_state }}</td>
<td>{{ $c->bidder_zip }}</td>
<td>{{ \App\Helpers\PhoneHelper::format($c->bidder_phone) }}</td>
<td>{{ $c->bidder_email }}</td>
<td>${{ number_format($c->winnertotal, 2) }}</td>
<td>
@if($c->payment_method == 1) Cash
@elseif($c->payment_method == 2) Check ({{ $c->check_number }})
@else Credit ({{ $c->cc_transaction }})
@endif
</td>
<td>
@foreach(\App\Models\WinningBids::with('items')->where('winning_bidder_num', $c->bidder_num)->get() as $bid)
{{ $bid->items->item_desc ?? 'N/A' }}<br>
<strong>#{{ $bid->items->item_assigned_num ?? '?' }}</strong> - {{ $bid->items->item_desc ?? 'N/A' }}<br>
@endforeach
</td>
<td>${{ number_format($c->winnertotal, 2) }}</td>
<td>{{ $c->payment_method == 1 ? 'Cash' : ($c->payment_method == 2 ? 'Check' : 'Credit') }}</td>
</tr>
@endforeach
</tbody>
@@ -0,0 +1 @@
Please find the attached item pickup slip.
+43 -2
View File
@@ -26,6 +26,7 @@
<div class="page-body">
<div class="container-xl">
<div class="card">
<div id="payment-error-container"></div>
<div class="card-body">
<div id="checkout-container">
<div class="text-center">
@@ -33,6 +34,13 @@
<div class="spinner-border" role="status"></div>
</div>
</div>
<div id="fallback-container" class="mt-4 text-center" style="display: none;">
<div class="hr-text">Still here?</div>
<p class="text-muted">If you have already completed your payment but the page hasn't redirected, please click the button below.</p>
<a id="verify-button" href="#" class="btn btn-info">
<i class="ti ti-shield-check me-2"></i>Verify Payment Status
</a>
</div>
</div>
</div>
</div>
@@ -67,19 +75,52 @@
return;
}
// Set up fallback button
const verifyUrl = `/north/verify/${bidderId}?sessionToken=${sessionToken}`;
const verifyButton = document.getElementById('verify-button');
const fallbackContainer = document.getElementById('fallback-container');
verifyButton.href = verifyUrl;
// Show fallback after 5 seconds to give user a manual way out if auto-redirect fails
setTimeout(() => {
fallbackContainer.style.display = 'block';
}, 5000);
await checkout.mount(sessionToken, 'checkout-container');
const handleCompletion = (result) => {
window.location.href = `/north/verify/${bidderId}?sessionToken=${sessionToken}`;
console.log('Payment complete event received:', result);
window.location.href = verifyUrl;
};
// Register standard completion events
checkout.onPaymentComplete(handleCompletion);
if (typeof checkout.onPaymentSuccess === 'function') {
checkout.onPaymentSuccess(handleCompletion);
}
// Handle errors
if (typeof checkout.onPaymentError === 'function') {
checkout.onPaymentError((error) => {
console.error('Payment Error:', error);
const errorDiv = document.createElement('div');
errorDiv.className = 'alert alert-danger alert-dismissible';
errorDiv.innerHTML = `
<div class="d-flex">
<div><i class="ti ti-alert-triangle me-2"></i></div>
<div>
<strong>Payment Error:</strong> ${error.message || 'An error occurred during payment.'}
</div>
</div>
<a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a>
`;
document.getElementById('payment-error-container').prepend(errorDiv);
});
}
} catch (error) {
console.error('Checkout Error:', error);
document.getElementById('checkout-container').innerHTML = '<div class="alert alert-danger">An error occurred. Please try again.</div>';
document.getElementById('checkout-container').innerHTML = '<div class="alert alert-danger">An error occurred while initializing checkout. Please try again.</div>';
}
});
</script>
+22 -8
View File
@@ -14,24 +14,38 @@
<div class="container-xl">
<div class="card">
<div class="table-responsive">
<table class="table table-vcenter card-table" id="showcars-table">
<table class="table table-vcenter card-table text-nowrap" id="showcars-table">
<thead>
<tr>
<th class="sort" data-sort="bidder-num">Bidder #</th>
<th class="sort" data-sort="bidder-num">#</th>
<th class="sort" data-sort="lname">Last Name</th>
<th class="sort" data-sort="fname">First Name</th>
<th>Address</th>
<th>City</th>
<th>State</th>
<th>Zip</th>
<th>Phone</th>
<th>Email</th>
<th class="sort" data-sort="year">Year</th>
<th class="sort" data-sort="make">Make</th>
<th class="sort" data-sort="model">Model</th>
<th class="sort" data-sort="year">Year</th>
<th class="sort" data-sort="owner">Owner</th>
</tr>
</thead>
<tbody class="list">
@foreach($showcarlist_results as $vehicle)
<tr>
<td class="bidder-num">{{ $vehicle->bidder_assigned_number ?? 'N/A' }}</td>
<td class="bidder-num">{{ $vehicle->bidder_assigned_number }}</td>
<td class="lname">{{ $vehicle->bidder_lname }}</td>
<td class="fname">{{ $vehicle->bidder_fname }}</td>
<td>{{ $vehicle->bidder_addr }}</td>
<td>{{ $vehicle->bidder_city }}</td>
<td>{{ $vehicle->bidder_state }}</td>
<td>{{ $vehicle->bidder_zip }}</td>
<td>{{ \App\Helpers\PhoneHelper::format($vehicle->bidder_phone) }}</td>
<td>{{ $vehicle->bidder_email }}</td>
<td class="year">{{ $vehicle->year }}</td>
<td class="make">{{ $vehicle->make }}</td>
<td class="model">{{ $vehicle->model }}</td>
<td class="year">{{ $vehicle->year }}</td>
<td class="owner">{{ $vehicle->bidder_fname }} {{ $vehicle->bidder_lname }}</td>
</tr>
@endforeach
</tbody>
@@ -46,7 +60,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/list.js/2.3.1/list.min.js"></script>
<script>
new List('showcars-table', {
valueNames: ['bidder-num', 'make', 'model', 'year', 'owner']
valueNames: ['bidder-num', 'lname', 'fname', 'year', 'make', 'model']
});
</script>
@endsection
+110
View File
@@ -0,0 +1,110 @@
<?php
namespace Tests\Unit;
use Tests\TestCase;
use App\Services\PrinterService;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\View;
class PrinterServiceTest extends TestCase
{
public function test_printForCheckout_calls_printPickupSlip_with_correct_data()
{
Mail::fake();
Config::set('services.printer.enabled', true);
Config::set('services.printer.email', 'printer@example.com');
Config::set('mail.from.address', 'hello@example.com');
Config::set('mail.from.name', 'Example');
// Mock Checkout model
$checkout = \Mockery::mock(\App\Models\Checkout::class)->makePartial();
$checkout->bidder_num = 1;
$checkout->checkout_id = 1;
$checkout->payment_method = 1;
$checkout->check_number = null;
$checkout->cc_transaction = null;
// Mock DB calls
\Illuminate\Support\Facades\DB::shouldReceive('select')
->twice()
->andReturn([
(object)['item_assigned_num' => 'A1', 'item_desc' => 'Test', 'winning_cost' => 50]
], [
(object)[
'bidder_assigned_number' => '123',
'total_cost' => '50',
'bidder_fname' => 'John',
'bidder_lname' => 'Doe',
'bidder_phone' => '1234567890',
'bidder_addr' => '123 St',
'bidder_city' => 'Troy',
'bidder_state' => 'MI',
'bidder_zip' => '48083'
]
]);
$service = new PrinterService();
$service->printForCheckout($checkout);
Mail::assertSent(\App\Mail\PickupSlipMail::class);
}
public function test_printPickupSlip_sends_email_when_enabled()
{
Mail::fake();
Config::set('services.printer.enabled', true);
Config::set('services.printer.email', 'printer@example.com');
Config::set('mail.from.address', 'hello@example.com');
Config::set('mail.from.name', 'Example');
// Mock data matching what receiptpdf expects
$checkoutData = [
'checkout_final_results' => (object)[
'payment_method' => 1,
'check_number' => null,
'cc_transaction' => null,
'checkout_id' => 1
],
'checkout_list_results' => [
(object)[
'item_assigned_num' => 'A1',
'item_desc' => 'Test Item',
'winning_cost' => '50'
]
],
'checkout_info_results' => [
(object)[
'bidder_assigned_number' => '123',
'total_cost' => '50',
'bidder_fname' => 'John',
'bidder_lname' => 'Doe',
'bidder_phone' => '1234567890',
'bidder_addr' => '123 St',
'bidder_city' => 'Troy',
'bidder_state' => 'MI',
'bidder_zip' => '48083'
]
]
];
$service = new PrinterService();
$service->printPickupSlip($checkoutData);
Mail::assertSent(\App\Mail\PickupSlipMail::class, function ($mail) {
return $mail->hasTo('printer@example.com');
});
}
public function test_printPickupSlip_does_not_send_email_when_disabled()
{
Mail::fake();
Config::set('services.printer.enabled', false);
$service = new PrinterService();
$service->printPickupSlip([]);
Mail::assertNothingSent();
}
}