Compare commits

..

50 Commits

Author SHA1 Message Date
rlong 05ab1c6014 fix: use correct Options object for Dompdf configuration 2026-05-01 10:10:16 -04:00
rlong 9065dcd911 missing use 2026-05-01 10:09:35 -04:00
rlong dcd08ae530 fix duplicate function 2026-05-01 10:09:01 -04:00
rlong d468991bd1 typo 2026-05-01 10:08:01 -04:00
rlong 7d1ec59dea feat: switch to dompdf for receipt generation 2026-05-01 10:06:35 -04:00
rlong 24d1eda717 feat: install spatie/browsershot 2026-05-01 09:57:46 -04:00
rlong 9d981f3a34 feat: migrate receipt PDF generation to Spatie Laravel PDF 2026-05-01 09:56:16 -04:00
rlong b9c018a4a6 fix: set tempDir via getMpdf to bypass problematic loadView config array 2026-05-01 09:44:04 -04:00
rlong 23b0b30434 fix: provide full mPDF configuration to avoid null array offset errors 2026-05-01 09:42:05 -04:00
rlong 4b9220c89e fix: update mPDF temp directory configuration key to temp_dir 2026-05-01 09:40:35 -04:00
rlong 298f3fa22b fix: resolve mPDF temp directory access error by using storage_path 2026-05-01 09:38:46 -04:00
rlong 98be7131f4 update views 2026-05-01 09:36:43 -04:00
rlong 999c1dfc58 fix: switch to correct PDF facade for mPDF and resolve receipt 500 error 2026-05-01 09:34:56 -04:00
rlong a3fd278536 fix: resolve receipt 404 and add save receipt pdf functionality 2026-05-01 09:32:26 -04:00
rlong 290498c728 fix: improve Google Pay handling with additional status checks, robust data extraction, and a fallback verification button 2026-05-01 09:27:36 -04:00
rlong aca21ae115 fix display 2026-05-01 09:01:28 -04:00
rlong 982efbf2bd fix: handle 'Approved' status and extract nested transaction ID from North API response 2026-05-01 08:59:09 -04:00
rlong 3953a6f4c8 fix height 2026-05-01 08:55:06 -04:00
rlong fbb10a01d4 ui: switch from min-height to explicit height for checkout container 2026-05-01 08:46:46 -04:00
rlong 6ccb754e0c ui: increase checkout container height to 800px 2026-05-01 08:44:57 -04:00
rlong 50826c8c20 update div height 2026-05-01 08:43:26 -04:00
rlong 68f75ac2fc feat: include products in checkout and increase form container size 2026-05-01 08:41:58 -04:00
rlong 9587c44657 fix: improve API request headers and error reporting for session creation and verification 2026-05-01 08:38:47 -04:00
rlong 06de3c0145 fix: broaden session token extraction to handle various North API response formats 2026-05-01 08:35:41 -04:00
rlong d2d53e961b fix: resolve invalid session token by correctly extracting token from North API response 2026-05-01 08:32:31 -04:00
rlong b0816231d6 feat: integrate North Embedded Checkout for bidder payments 2026-05-01 08:18:26 -04:00
rlong e4cccba942 Merge pull request 'change button color' (#9) from feature/all-searchable-selects into master
Reviewed-on: TFMM/silent-auction#9
2026-04-30 15:39:37 -04:00
rlong e023f68a84 change button color 2026-04-30 15:38:39 -04:00
rlong 4edb7fac10 Merge pull request 'feat: implement global searchable selects using Select2 and remove redundant per-view implementations' (#8) from feature/all-searchable-selects into master
Reviewed-on: TFMM/silent-auction#8
2026-04-30 15:36:37 -04:00
rlong 19f93f95bb feat: implement global searchable selects using Select2 and remove redundant per-view implementations 2026-04-30 15:33:34 -04:00
rlong 885161cc2b Merge pull request 'Add seeders and factories for all models' (#7) from feature/add-model-seeders into master
Reviewed-on: TFMM/silent-auction#7
2026-04-29 20:39:40 -04:00
rlong a8fd03f256 Add seeders and factories for all models 2026-04-29 20:38:05 -04:00
rlong 00ff08e3a6 Enable profile and add OIDC menu item to Filament panel configuration 2026-04-26 17:10:28 -04:00
rlong 7e8b35bf20 Add Link OIDC Account and Admin links to frontend user menu 2026-04-26 17:09:52 -04:00
rlong 681d8b73d6 Move OIDC menu item registration to boot method for reliability 2026-04-26 17:08:00 -04:00
rlong f294ded6e8 fix resource 2026-04-26 16:46:28 -04:00
rlong eca564eac7 fix userresource 2026-04-26 16:44:17 -04:00
rlong e8df889f7e Merge pull request 'Implement OIDC authentication via Laravel Socialite' (#6) from feature/socialite-oidc-auth into master
Reviewed-on: TFMM/silent-auction#6
2026-04-26 16:39:34 -04:00
rlong ad02f65b54 Add UserResource and OIDC linking to Filament user menu 2026-04-26 16:38:43 -04:00
rlong c2c9464133 Implement OIDC authentication via Laravel Socialite 2026-04-26 16:32:53 -04:00
rlong c0176f9924 change winner form order to match paper 2026-04-26 16:07:32 -04:00
rlong ffa8d30be4 Merge pull request 'change phone number display' (#5) from format-phone-numbers into master
Reviewed-on: TFMM/silent-auction#5
2026-04-26 15:25:12 -04:00
rlong d64e1d5578 change phone number display 2026-04-26 15:24:05 -04:00
rlong ad66fef19b fix menu location 2026-04-26 15:08:47 -04:00
rlong 5a7684bc8c move menu item 2026-04-26 15:08:00 -04:00
rlong aed29fbf56 Merge pull request 'Add customer-facing check for winning items' (#4) from North-Hackathon/silent-auction:bidder-facing-checkout into master
Reviewed-on: TFMM/silent-auction#4
2026-04-26 15:05:13 -04:00
rlong 816fba5121 Merge pull request 'feat: implement searchable selects for bidder and item lookups using Select2' (#3) from feature/searchable-selects into master
Reviewed-on: TFMM/silent-auction#3
2026-04-26 15:01:31 -04:00
rlong ddbfc642f2 feat: implement searchable selects for bidder and item lookups using Select2 2026-04-25 11:55:52 -04:00
rlong b6db2f200b Add customer-facing check for winning items 2026-04-24 15:56:29 -04:00
rlong ef4ef144a6 Merge pull request 'v5 update fixes' (#2) from v5-upgrade into master
Reviewed-on: TFMM/silent-auction#2
2026-04-20 15:16:51 -04:00
54 changed files with 2439 additions and 55 deletions
+10
View File
@@ -33,3 +33,13 @@ MAIL_FROM_NAME=null
PUSHER_APP_ID= PUSHER_APP_ID=
PUSHER_APP_KEY= PUSHER_APP_KEY=
PUSHER_APP_SECRET= PUSHER_APP_SECRET=
OIDC_BASE_URL=
OIDC_CLIENT_ID=
OIDC_CLIENT_SECRET=
OIDC_REDIRECT_URI="${APP_URL}/auth/social/oidc/callback"
NORTH_CHECKOUT_ID=
NORTH_PROFILE_ID=
NORTH_PRIVATE_API_KEY=
+1
View File
@@ -0,0 +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}}
+2 -1
View File
@@ -57,7 +57,8 @@ class BiddersResource extends Resource
TextColumn::make('bidder_city')->label('City')->sortable(), TextColumn::make('bidder_city')->label('City')->sortable(),
TextColumn::make('bidder_state')->label('State')->sortable(), TextColumn::make('bidder_state')->label('State')->sortable(),
TextColumn::make('bidder_zip')->label('Zip')->sortable(), TextColumn::make('bidder_zip')->label('Zip')->sortable(),
TextColumn::make('bidder_phone')->label('Phone Number')->sortable(), TextColumn::make('bidder_phone')->label('Phone Number')->sortable()
->formatStateUsing(fn ($state) => \App\Helpers\PhoneHelper::format($state)),
TextColumn::make('bidder_email')->label('Email')->sortable(), TextColumn::make('bidder_email')->label('Email')->sortable(),
]) ])
->filters([ ->filters([
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace App\Filament\Resources;
use App\User;
use Filament\Resources\Resource;
use Filament\Tables\Table;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
use Filament\Actions\EditAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Support\Icons\Heroicon;
use BackedEnum;
class UserResource extends Resource
{
protected static ?string $model = User::class;
protected static ?string $recordTitleAttribute = 'name';
protected static string | BackedEnum | null $navigationIcon = 'heroicon-o-users';
public static function form(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->required()
->maxLength(255),
TextInput::make('email')
->email()
->required()
->maxLength(255),
TextInput::make('provider_name')
->label('OIDC Provider')
->disabled(),
TextInput::make('provider_id')
->label('OIDC ID')
->disabled(),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')->sortable()->searchable(),
TextColumn::make('email')->sortable()->searchable(),
TextColumn::make('provider_name')->label('OIDC Provider'),
TextColumn::make('provider_id')->label('OIDC ID'),
TextColumn::make('created_at')->dateTime()->sortable(),
])
->filters([
//
])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
public static function getPages(): array
{
return [
'index' => UserResource\Pages\ListUsers::route('/'),
'create' => UserResource\Pages\CreateUsers::route('/create'),
'edit' => UserResource\Pages\EditUsers::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Resources\Pages\CreateRecord;
class CreateUsers extends CreateRecord
{
protected static string $resource = UserResource::class;
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Resources\Pages\EditRecord;
class EditUsers extends EditRecord
{
protected static string $resource = UserResource::class;
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\UserResource\Pages;
use App\Filament\Resources\UserResource;
use Filament\Resources\Pages\ListRecords;
class ListUsers extends ListRecords
{
protected static string $resource = UserResource::class;
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\Helpers;
class PhoneHelper
{
public static function format($phone)
{
$phone = preg_replace('/[^0-9]/', '', $phone);
if (strlen($phone) == 10) {
return preg_replace('/([0-9]{3})([0-9]{3})([0-9]{4})/', '$1-$2-$3', $phone);
}
return $phone;
}
}
@@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
use Illuminate\Support\Str;
class SocialiteController extends Controller
{
public function redirectToProvider($provider)
{
return Socialite::driver($provider)->redirect();
}
public function handleProviderCallback($provider)
{
try {
$socialUser = Socialite::driver($provider)->user();
} catch (\Exception $e) {
return redirect('/login')->with('error', 'Authentication failed.');
}
if (Auth::check()) {
$user = Auth::user();
$user->update([
'provider_name' => $provider,
'provider_id' => $socialUser->getId(),
]);
return redirect()->intended('/admin')->with('status', 'Account linked successfully!');
}
// Find user by provider_id
$user = User::where('provider_name', $provider)
->where('provider_id', $socialUser->getId())
->first();
if (!$user) {
// Find user by email to link
$user = User::where('email', $socialUser->getEmail())->first();
if ($user) {
// Link the account
$user->update([
'provider_name' => $provider,
'provider_id' => $socialUser->getId(),
]);
} else {
// Optionally create a new user
$user = User::create([
'name' => $socialUser->getName() ?? $socialUser->getNickname() ?? $socialUser->getEmail(),
'email' => $socialUser->getEmail(),
'provider_name' => $provider,
'provider_id' => $socialUser->getId(),
'password' => bcrypt(Str::random(24)),
]);
}
}
Auth::login($user);
return redirect()->intended('/admin'); // Redirect to filament admin or home
}
}
@@ -0,0 +1,212 @@
<?php
namespace App\Http\Controllers;
use App\Models\Bidders;
use App\Models\Checkout;
use App\Models\WinningBids;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
class NorthCheckoutController extends Controller
{
public function checkout(Request $request, $bidder_id)
{
$bidder = Bidders::where('idbidders', $bidder_id)->firstOrFail();
// Check if already checked out
if (Checkout::where('bidder_num', $bidder->idbidders)->exists()) {
return redirect('/mywinnings?bidder_number=' . $bidder->bidder_assigned_number)->with('error', 'Bidder has already checked out.');
}
$winnings = WinningBids::where('winning_bidder_num', $bidder->idbidders)->get();
$total_cost = $winnings->sum('winning_cost');
if ($total_cost <= 0) {
return redirect('/mywinnings?bidder_number=' . $bidder->bidder_assigned_number)->with('error', 'No winnings found for this bidder.');
}
return view('north_checkout', [
'bidder' => $bidder,
'total_cost' => $total_cost,
]);
}
public function createSession(Request $request, $bidder_id)
{
$bidder = Bidders::findOrFail($bidder_id);
$winnings = WinningBids::with('items')->where('winning_bidder_num', $bidder->idbidders)->get();
$total_cost = $winnings->sum('winning_cost');
$products = $winnings->map(function($winning) {
return [
'name' => $winning->items->item_desc ?? 'Auction Item',
'price' => (float)$winning->winning_cost,
'quantity' => 1
];
})->toArray();
$apiKey = config('services.north.private_api_key');
$checkoutId = config('services.north.checkout_id');
$profileId = config('services.north.profile_id');
if (!$apiKey || !$checkoutId || !$profileId) {
return response()->json(['error' => 'North configuration missing.'], 500);
}
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $apiKey,
'Accept' => 'application/json',
'Content-Type' => 'application/json',
])->post('https://checkout.north.com/api/sessions', [
'checkoutId' => $checkoutId,
'profileId' => $profileId,
'amount' => (float)$total_cost,
'products' => $products,
]);
if ($response->failed()) {
Log::error('North Session Creation Failed: ' . $response->status() . ' ' . $response->body());
return response()->json([
'error' => 'Failed to create checkout session: ' . ($response->json('message') ?? 'Unknown error'),
'status' => $response->status(),
'body' => $response->body()
], 500);
}
$data = $response->json();
// If json() is null but body is not empty, it might be a parsing error
if (is_null($data) && !empty($response->body())) {
Log::error('North Session Response is not JSON: ' . $response->body());
return response()->json([
'error' => 'Response from North is not valid JSON.',
'raw_body' => $response->body()
], 500);
}
$token = $data['token'] ??
$data['sessionToken'] ??
$data['id'] ??
$data['session_id'] ??
($data['session']['id'] ?? null);
if (!$token) {
Log::error('North Session Token Missing in Response: ' . json_encode($data));
return response()->json([
'error' => 'Session token not found in API response.',
'debug_response' => $data,
'raw_body' => $response->body()
], 500);
}
return response()->json(['sessionToken' => $token]);
}
public function verify(Request $request, $bidder_id)
{
$bidder = Bidders::findOrFail($bidder_id);
$sessionToken = $request->query('sessionToken');
if (!$sessionToken) {
return redirect('/mywinnings?bidder_number=' . $bidder->bidder_assigned_number)->with('error', 'Missing session token.');
}
$apiKey = config('services.north.private_api_key');
$checkoutId = config('services.north.checkout_id');
$profileId = config('services.north.profile_id');
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $apiKey,
'SessionToken' => $sessionToken,
'CheckoutId' => $checkoutId,
'ProfileId' => $profileId,
'Accept' => 'application/json',
])->get('https://checkout.north.com/api/sessions/status');
if ($response->failed()) {
Log::error('North Session Verification Failed: ' . $response->status() . ' ' . $response->body());
return redirect('/mywinnings?bidder_number=' . $bidder->bidder_assigned_number)->with('error', 'Failed to verify payment status. Status: ' . $response->status());
}
$status = $response->json();
$currentStatus = $status['status'] ?? '';
// The North API status check might return Approved, completed, success, authorized, or captured.
$successStatuses = ['approved', 'completed', 'success', 'authorized', 'captured'];
if (in_array(strtolower($currentStatus), $successStatuses)) {
// Check if already checked out to avoid duplicates
$existingCheckout = Checkout::where('bidder_num', $bidder->idbidders)->first();
if (!$existingCheckout) {
// According to docs, when status is Approved, transaction details are in 'body'
// Digital wallets might have these at the top level
$body = $status['body'] ?? [];
$winnertotal = $status['amount'] ??
($body['amount'] ??
($status['amount_total'] ??
WinningBids::where('winning_bidder_num', $bidder->idbidders)->sum('winning_cost')));
$payment_method = 3; // Credit Card
$cc_transaction = $body['auth_guid'] ??
($status['transaction_id'] ??
($status['transactionId'] ??
($status['id'] ?? 'NORTH_EC')));
$cc_amount = $winnertotal;
$check_number = null;
$checkout_id = DB::table('checkout')->insertGetID(
[
'bidder_num' => $bidder->idbidders,
'winnertotal' => $winnertotal,
'payment_method' => $payment_method,
'check_number' => $check_number,
'cc_transaction' => $cc_transaction,
'cc_amount' => $cc_amount,
'created_at' => now(),
'updated_at' => now(),
]
);
} else {
$checkout_id = $existingCheckout->checkout_id;
$payment_method = $existingCheckout->payment_method;
$cc_transaction = $existingCheckout->cc_transaction;
$check_number = $existingCheckout->check_number;
}
// Replicate the data for checkout_complete view
$checkout_list_results = 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->idbidders
");
$checkout_info_results = 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->idbidders
GROUP BY winning_bids.winning_bidder_num
");
return view('checkout_complete', [
'checkout_result' => $checkout_id,
'checkout_list_results' => $checkout_list_results,
'checkout_info_results' => $checkout_info_results,
'payment_method' => $payment_method,
'check_number' => $check_number,
'cc_transaction' => $cc_transaction
]);
}
return redirect('/mywinnings?bidder_number=' . $bidder->bidder_assigned_number)->with('error', 'Payment not completed. Status: ' . ($status['status'] ?? 'unknown'));
}
}
+74 -2
View File
@@ -6,7 +6,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use View; use View;
use App\helpers; use App\helpers;
use PDF; use Spatie\LaravelPdf\Facades\Pdf;
use App\Models\Bidders; use App\Models\Bidders;
use App\Models\Items; use App\Models\Items;
use App\Models\Checkout; use App\Models\Checkout;
@@ -19,7 +19,7 @@ use App\Models\CarShowCategory;
use App\Models\Types; use App\Models\Types;
use App\Models\Vehicles; use App\Models\Vehicles;
use App\Models\VehicleScores; use App\Models\VehicleScores;
use Dompdf\Dompdf;
class PagesController extends Controller class PagesController extends Controller
{ {
public function home() public function home()
@@ -250,6 +250,49 @@ class PagesController extends Controller
return view('receiptpdf', $checkout_data); return view('receiptpdf', $checkout_data);
} }
public function downloadReceiptPdf(Request $request)
{
$checkoutid = $request->checkout_id;
$checkout_final_results = Checkout::where('checkout_id', '=', $checkoutid)->first();
if (!$checkout_final_results) {
return redirect('/mywinnings')->with('error', 'Checkout record not found.');
}
$bidder_num = $checkout_final_results->bidder_num;
$checkout_list_results = 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 = 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
");
$options = new \Dompdf\Options();
$options->set('isHtml5ParserEnabled', true);
$options->set('isRemoteEnabled', true);
$dompdf = new Dompdf($options);
$html = view('receiptpdf', [
'checkout_final_results' => $checkout_final_results,
'checkout_list_results' => $checkout_list_results,
'checkout_info_results' => $checkout_info_results
])->render();
$dompdf->loadHtml($html);
$dompdf->setPaper('letter', 'portrait');
$dompdf->render();
return $dompdf->stream('receipt-'.$checkoutid.'.pdf');
}
public function reprintReceipt(Request $reprint_receipt_req) public function reprintReceipt(Request $reprint_receipt_req)
{ {
if (!$reprint_receipt_req->reprintbiddernum) { if (!$reprint_receipt_req->reprintbiddernum) {
@@ -515,4 +558,33 @@ class PagesController extends Controller
//dd($showcarlist_results); //dd($showcarlist_results);
return view('showcarlist', ['showcarlist_results' => $showcarlist_results]); return view('showcarlist', ['showcarlist_results' => $showcarlist_results]);
} }
public function myWinnings(Request $request)
{
if (!$request->bidder_number) {
return view('mywinnings_form');
}
$bidder_number = $request->bidder_number;
$bidder = Bidders::where('bidder_assigned_number', $bidder_number)->first();
if (!$bidder) {
return view('mywinnings_form', ['error' => 'Bidder number not found.']);
}
$winnings = WinningBids::with('items')
->where('winning_bidder_num', $bidder->idbidders)
->get();
$total_cost = $winnings->sum('winning_cost');
$is_checked_out = \App\Models\Checkout::where('bidder_num', $bidder->idbidders)->exists();
return view('mywinnings_results', [
'bidder' => $bidder,
'winnings' => $winnings,
'total_cost' => $total_cost,
'is_checked_out' => $is_checked_out
]);
}
} }
+1 -1
View File
@@ -28,7 +28,7 @@ class WinningBids extends Model
public function items() public function items()
{ {
return $this->hasOne(Items::class, 'iditems', 'winning_item_num'); return $this->belongsTo(Items::class, 'winning_item_num', 'iditems');
} }
public function bidders() public function bidders()
+3
View File
@@ -16,6 +16,9 @@ class EventServiceProvider extends ServiceProvider
'App\Events\Event' => [ 'App\Events\Event' => [
'App\Listeners\EventListener', 'App\Listeners\EventListener',
], ],
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
\SocialiteProviders\OIDC\OIDCExtendSocialite::class.'@handle',
],
]; ];
/** /**
@@ -18,8 +18,41 @@ use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\StartSession; use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession; use Illuminate\View\Middleware\ShareErrorsFromSession;
use Filament\Support\Facades\FilamentView;
use Illuminate\Support\Facades\Blade;
use Filament\Navigation\MenuItem;
use Filament\Facades\Filament;
class AdminPanelProvider extends PanelProvider class AdminPanelProvider extends PanelProvider
{ {
public function boot()
{
FilamentView::registerRenderHook(
'panels::auth.login.form.after',
fn (): string => Blade::render('
<div class="mt-4">
<x-filament::button
:href="url(\'auth/social/oidc\')"
tag="a"
color="info"
class="w-full"
>
Login with OIDC
</x-filament::button>
</div>
'),
);
Filament::registerUserMenuItems([
MenuItem::make()
->label('Link OIDC Account')
->icon('heroicon-o-link')
->url(fn (): string => url('auth/social/oidc')),
]);
}
public function panel(Panel $panel): Panel public function panel(Panel $panel): Panel
{ {
return $panel return $panel
@@ -27,6 +60,13 @@ class AdminPanelProvider extends PanelProvider
->id('admin') ->id('admin')
->path('admin') ->path('admin')
->login() ->login()
->profile()
->userMenuItems([
'oidc' => MenuItem::make()
->label('Link OIDC Account')
->icon('heroicon-o-link')
->url(fn (): string => url('auth/social/oidc')),
])
->brandName(env('APP_NAME')) ->brandName(env('APP_NAME'))
->homeUrl('/') ->homeUrl('/')
->colors([ ->colors([
+1 -1
View File
@@ -18,7 +18,7 @@ class User extends Authenticatable implements FilamentUser, HasName
* @var array * @var array
*/ */
protected $fillable = [ protected $fillable = [
'name', 'email', 'password', 'name', 'email', 'password', 'provider_name', 'provider_id',
]; ];
/** /**
+6 -1
View File
@@ -8,10 +8,15 @@
"php": "^8.2", "php": "^8.2",
"barryvdh/laravel-snappy": "^1.0", "barryvdh/laravel-snappy": "^1.0",
"carlos-meneses/laravel-mpdf": "^2.1", "carlos-meneses/laravel-mpdf": "^2.1",
"dompdf/dompdf": "^3.1",
"filament/filament": "^5.0", "filament/filament": "^5.0",
"kovah/laravel-socialite-oidc": "^0.7.0",
"laravel/framework": "^11.0", "laravel/framework": "^11.0",
"laravel/socialite": "^5.26",
"laravel/tinker": "^2.9", "laravel/tinker": "^2.9",
"laravel/ui": "^4.2" "laravel/ui": "^4.2",
"socialiteproviders/manager": "^4.9",
"spatie/laravel-pdf": "^2.8"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-debugbar": "^3.8", "barryvdh/laravel-debugbar": "^3.8",
Generated
+1064 -1
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -35,4 +35,17 @@ return [
'secret' => env('STRIPE_SECRET'), 'secret' => env('STRIPE_SECRET'),
], ],
'oidc' => [
'base_url' => env('OIDC_BASE_URL'),
'client_id' => env('OIDC_CLIENT_ID'),
'client_secret' => env('OIDC_CLIENT_SECRET'),
'redirect' => env('OIDC_REDIRECT_URI'),
],
'north' => [
'checkout_id' => env('NORTH_CHECKOUT_ID'),
'profile_id' => env('NORTH_PROFILE_ID'),
'private_api_key' => env('NORTH_PRIVATE_API_KEY'),
],
]; ];
+125 -8
View File
@@ -23,14 +23,131 @@ $factory->define(App\User::class, function (Faker\Generator $faker) {
]; ];
}); });
//Bidder // Bidders
$factory->define(App\Models\Bidder::class, function (Faker\Generator $faker) { $factory->define(App\Models\Bidders::class, function (Faker\Generator $faker) {
static $password;
return [ return [
'name' => $faker->name, 'bidder_fname' => $faker->firstName,
'email' => $faker->unique()->safeEmail, 'bidder_lname' => $faker->lastName,
'password' => $password ?: $password = bcrypt('secret'), 'bidder_addr' => $faker->streetAddress,
'remember_token' => str_random(10), 'bidder_city' => $faker->city,
'bidder_state' => $faker->stateAbbr,
'bidder_zip' => $faker->postcode,
'bidder_phone' => $faker->phoneNumber,
'bidder_email' => $faker->unique()->safeEmail,
'bidder_assigned_number' => $faker->unique()->numberBetween(1, 1000),
];
});
// Items
$factory->define(App\Models\Items::class, function (Faker\Generator $faker) {
return [
'item_desc' => $faker->sentence(3),
'item_min_bid' => $faker->numberBetween(10, 100),
'item_est_value' => $faker->numberBetween(50, 500),
'item_assigned_num' => $faker->unique()->numberBetween(1, 500),
];
});
// CarShowCategory
$factory->define(App\Models\CarShowCategory::class, function (Faker\Generator $faker) {
return [
'category_name' => $faker->words(2, true),
'vehicle_type' => $faker->boolean,
];
});
// Vehicles
$factory->define(App\Models\Vehicles::class, function (Faker\Generator $faker) {
return [
'year' => $faker->year,
'make' => $faker->company,
'model' => $faker->word,
'type' => function () {
return factory(App\Models\CarShowCategory::class)->create()->id;
},
'doNotJudge' => $faker->boolean,
'owner' => function () {
return factory(App\Models\Bidders::class)->create()->bidder_assigned_number;
},
];
});
// PaymentMethods
$factory->define(App\Models\PaymentMethods::class, function (Faker\Generator $faker) {
return [
'pm_name' => $faker->randomElement(['Cash', 'Check', 'Credit Card']),
];
});
// Judges
$factory->define(App\Models\Judges::class, function (Faker\Generator $faker) {
return [
'judge_number' => $faker->unique()->numberBetween(1, 50),
];
});
// VehicleScores
$factory->define(App\Models\VehicleScores::class, function (Faker\Generator $faker) {
return [
'judge' => function () {
return factory(App\Models\Judges::class)->create()->id;
},
'overall_score' => $faker->numberBetween(1, 100),
'vehicle' => function () {
return factory(App\Models\Vehicles::class)->create()->id;
},
];
});
// PeoplesChoice
$factory->define(App\Models\PeoplesChoice::class, function (Faker\Generator $faker) {
return [
'vehicle' => function () {
return factory(App\Models\Vehicles::class)->create()->id;
},
'pc_count' => $faker->numberBetween(0, 50),
];
});
// WinningBids
$factory->define(App\Models\WinningBids::class, function (Faker\Generator $faker) {
return [
'winning_bidder_num' => function () {
return factory(App\Models\Bidders::class)->create()->idbidders;
},
'winning_cost' => $faker->numberBetween(10, 1000),
'winning_item_num' => function () {
return factory(App\Models\Items::class)->create()->iditems;
},
];
});
// Checkout
$factory->define(App\Models\Checkout::class, function (Faker\Generator $faker) {
return [
'bidder_num' => function () {
return factory(App\Models\Bidders::class)->create()->idbidders;
},
'winnertotal' => $faker->numberBetween(10, 2000),
'payment_method' => function () {
return factory(App\Models\PaymentMethods::class)->create()->pm_id;
},
'check_number' => $faker->optional()->numberBetween(100, 999),
'cc_transaction' => $faker->optional()->uuid,
'cc_amount' => $faker->optional()->numberBetween(10, 2000),
];
});
// CarShowWinner
$factory->define(App\Models\CarShowWinner::class, function (Faker\Generator $faker) {
return [
'vehicle' => function () {
return factory(App\Models\Vehicles::class)->create()->id;
},
'category' => function () {
return factory(App\Models\CarShowCategory::class)->create()->id;
},
'place' => $faker->numberBetween(1, 3),
'total_score' => $faker->numberBetween(50, 300),
]; ];
}); });
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('provider_name')->nullable();
$table->string('provider_id')->nullable();
$table->unique(['provider_name', 'provider_id']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropUnique(['provider_name', 'provider_id']);
$table->dropColumn(['provider_name', 'provider_id']);
});
}
};
+12
View File
@@ -0,0 +1,12 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\Bidders;
class BiddersTableSeeder extends Seeder
{
public function run()
{
factory(Bidders::class, 50)->create();
}
}
@@ -0,0 +1,12 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\CarShowCategory;
class CarShowCategoryTableSeeder extends Seeder
{
public function run()
{
factory(CarShowCategory::class, 10)->create();
}
}
@@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\CarShowWinner;
use App\Models\Vehicles;
use App\Models\CarShowCategory;
class CarShowWinnerTableSeeder extends Seeder
{
public function run()
{
$categories = CarShowCategory::all();
$vehicles = Vehicles::all();
foreach ($categories as $category) {
for ($i = 1; $i <= 3; $i++) {
factory(CarShowWinner::class)->create([
'category' => $category->id,
'vehicle' => $vehicles->random()->id,
'place' => $i,
]);
}
}
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\Checkout;
use App\Models\Bidders;
use App\Models\PaymentMethods;
class CheckoutTableSeeder extends Seeder
{
public function run()
{
$bidders = Bidders::all();
$paymentMethods = PaymentMethods::all();
foreach ($bidders as $bidder) {
if (rand(0, 1)) {
factory(Checkout::class)->create([
'bidder_num' => $bidder->idbidders,
'payment_method' => $paymentMethods->random()->pm_id,
]);
}
}
}
}
+12 -1
View File
@@ -11,6 +11,17 @@ class DatabaseSeeder extends Seeder
*/ */
public function run() public function run()
{ {
// $this->call(UsersTableSeeder::class); $this->call(UsersTableSeeder::class);
$this->call(PaymentMethodsTableSeeder::class);
$this->call(CarShowCategoryTableSeeder::class);
$this->call(BiddersTableSeeder::class);
$this->call(ItemsTableSeeder::class);
$this->call(JudgesTableSeeder::class);
$this->call(VehiclesTableSeeder::class);
$this->call(VehicleScoresTableSeeder::class);
$this->call(PeoplesChoiceTableSeeder::class);
$this->call(WinningBidsTableSeeder::class);
$this->call(CheckoutTableSeeder::class);
$this->call(CarShowWinnerTableSeeder::class);
} }
} }
+12
View File
@@ -0,0 +1,12 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\Items;
class ItemsTableSeeder extends Seeder
{
public function run()
{
factory(Items::class, 30)->create();
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\Judges;
class JudgesTableSeeder extends Seeder
{
public function run()
{
factory(Judges::class, 5)->create();
}
}
@@ -0,0 +1,14 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\PaymentMethods;
class PaymentMethodsTableSeeder extends Seeder
{
public function run()
{
PaymentMethods::create(['pm_name' => 'Cash']);
PaymentMethods::create(['pm_name' => 'Check']);
PaymentMethods::create(['pm_name' => 'Credit Card']);
}
}
@@ -0,0 +1,19 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\PeoplesChoice;
use App\Models\Vehicles;
class PeoplesChoiceTableSeeder extends Seeder
{
public function run()
{
$vehicles = Vehicles::all();
foreach ($vehicles as $vehicle) {
factory(PeoplesChoice::class)->create([
'vehicle' => $vehicle->id,
]);
}
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
use Illuminate\Database\Seeder;
use App\User;
class UsersTableSeeder extends Seeder
{
public function run()
{
factory(User::class, 10)->create();
}
}
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\VehicleScores;
use App\Models\Judges;
use App\Models\Vehicles;
class VehicleScoresTableSeeder extends Seeder
{
public function run()
{
$judges = Judges::all();
$vehicles = Vehicles::all();
foreach ($vehicles as $vehicle) {
foreach ($judges as $judge) {
factory(VehicleScores::class)->create([
'judge' => $judge->id,
'vehicle' => $vehicle->id,
]);
}
}
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\Vehicles;
use App\Models\CarShowCategory;
use App\Models\Bidders;
class VehiclesTableSeeder extends Seeder
{
public function run()
{
$categories = CarShowCategory::all();
$bidders = Bidders::all();
factory(Vehicles::class, 40)->create([
'type' => function () use ($categories) {
return $categories->random()->id;
},
'owner' => function () use ($bidders) {
return $bidders->random()->bidder_assigned_number;
},
]);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Seeder;
use App\Models\WinningBids;
use App\Models\Bidders;
use App\Models\Items;
class WinningBidsTableSeeder extends Seeder
{
public function run()
{
$bidders = Bidders::all();
$items = Items::all();
foreach ($items as $item) {
if (rand(0, 1)) {
factory(WinningBids::class)->create([
'winning_bidder_num' => $bidders->random()->idbidders,
'winning_item_num' => $item->iditems,
]);
}
}
}
}
+18
View File
@@ -49,3 +49,21 @@ If you discover a security vulnerability within Laravel, please send an e-mail t
## License ## License
The Laravel framework is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT). The Laravel framework is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT).
## Changelog
### [bidder-facing-checkout] - 2026-04-24
#### Added
- **Public Winnings View:** Introduced a new feature allowing bidders to check their won items by entering their bidder number.
- **Routes:** Added `/mywinnings` (GET and POST) routes to `routes/web.php`.
- **Controller Logic:** Implemented `myWinnings` method in `PagesController` to handle bidder lookups and display results.
- **Views:**
- `resources/views/mywinnings_form.blade.php`: Search form for bidder number.
- `resources/views/mywinnings_results.blade.php`: Detailed list of won items and total cost.
- **Navigation:** Added "Check My Winnings" link to the main layout navbar for easy public access.
- **Testing:** Created `tests/Feature/MyWinningsTest.php` to ensure the new feature works as expected.
#### Changed
- **Models:** Updated `App\Models\WinningBids` relationship `items()` from `hasOne` to `belongsTo` to correctly map the database structure and support eager loading.
+4
View File
@@ -54,6 +54,10 @@
Login Login
</button> </button>
<a href="{{ url('auth/social/oidc') }}" class="btn btn-primary">
Login with OIDC
</a>
<a class="btn btn-link" href="{{ route('password.request') }}"> <a class="btn btn-link" href="{{ route('password.request') }}">
Forgot Your Password? Forgot Your Password?
</a> </a>
+1 -1
View File
@@ -66,7 +66,7 @@
{{ $bidderlist_result->bidder_zip }} {{ $bidderlist_result->bidder_zip }}
</td> </td>
<td> <td>
{{ $bidderlist_result->bidder_phone }} {{ \App\Helpers\PhoneHelper::format($bidderlist_result->bidder_phone) }}
</td> </td>
<td> <td>
{{ $bidderlist_result->bidder_email }} {{ $bidderlist_result->bidder_email }}
+1 -1
View File
@@ -33,7 +33,7 @@
Name: {{ $checkout_info_result->bidder_fname }} {{ $checkout_info_result->bidder_lname }} Name: {{ $checkout_info_result->bidder_fname }} {{ $checkout_info_result->bidder_lname }}
</th> </th>
<th colspan="2"> <th colspan="2">
Phone Number: {{ $checkout_info_result->bidder_phone }} Phone Number: {{ \App\Helpers\PhoneHelper::format($checkout_info_result->bidder_phone) }}
</th> </th>
</tr> </tr>
<tr> <tr>
+10 -7
View File
@@ -14,21 +14,24 @@
<tr> <tr>
<th> <th>
<h4> <h4>
St. John Catholic Church North Hackathon
<br> <br>
Car Show Silent Auction Car Show Silent Auction
<br> <br>
2099 N. Hacker Rd. 250 Stephenson Hwy
<br> <br>
Howell, MI 48855 Troy, MI 48083
</h4> </h4>
</th> </th>
<th align='right'> <th align='right'>
<h2> <div class="btn-group">
<a class="btn btn-primary" target=_blank href="receiptpdf?checkout_id={{$checkout_result }}" role="button"> <a class="btn btn-primary" target=_blank href="{{ route('receiptpdf', ['checkout_id' => $checkout_result]) }}" role="button">
Print Receipt Print Receipt
</a> </a>
</h2> <a class="btn btn-success" href="{{ route('download_receipt', ['checkout_id' => $checkout_result]) }}" role="button">
Save Receipt PDF
</a>
</div>
</th> </th>
</tr> </tr>
</table> </table>
@@ -51,7 +54,7 @@
Name: {{ $checkout_info_result->bidder_fname }} {{ $checkout_info_result->bidder_lname }} Name: {{ $checkout_info_result->bidder_fname }} {{ $checkout_info_result->bidder_lname }}
</th> </th>
<th colspan="2"> <th colspan="2">
Phone Number: {{ $checkout_info_result->bidder_phone }} Phone Number: {{ \App\Helpers\PhoneHelper::format($checkout_info_result->bidder_phone) }}
</th> </th>
</tr> </tr>
<tr> <tr>
@@ -70,7 +70,7 @@
{{ $checkout_complete_result->bidder_zip }} {{ $checkout_complete_result->bidder_zip }}
</td> </td>
<td> <td>
{{ $checkout_complete_result->bidder_phone }} {{ \App\Helpers\PhoneHelper::format($checkout_complete_result->bidder_phone) }}
</td> </td>
<td> <td>
{{ $checkout_complete_result->bidder_email }} {{ $checkout_complete_result->bidder_email }}
+2 -2
View File
@@ -13,7 +13,7 @@
<label for=winneritemnum class="col-sm-3 col-sm-offset-1 control-label"> <label for=winneritemnum class="col-sm-3 col-sm-offset-1 control-label">
Item Number: Item Number:
</label> </label>
<div class=col-sm-2> <div class=col-sm-4>
<select name="winid" id="winid" required class=form-control> <select name="winid" id="winid" required class=form-control>
<?php echo App\Helpers\WinningBidSelectList::winningBidShowNumbers(); ?> <?php echo App\Helpers\WinningBidSelectList::winningBidShowNumbers(); ?>
</select> </select>
@@ -23,7 +23,7 @@
<label for=winnerbiddernum class="col-sm-3 col-sm-offset-1 control-label"> <label for=winnerbiddernum class="col-sm-3 col-sm-offset-1 control-label">
Bidder Number: Bidder Number:
</label> </label>
<div class=col-sm-2> <div class=col-sm-4>
<select name="winnerbiddernum" id="winnerbiddernum" required class=form-control> <select name="winnerbiddernum" id="winnerbiddernum" required class=form-control>
<?php echo App\Helpers\BidderSelectList::bidderShowNumbers(); ?> <?php echo App\Helpers\BidderSelectList::bidderShowNumbers(); ?>
</select> </select>
+1 -2
View File
@@ -67,8 +67,7 @@
{{ $finaltally_result->bidder_zip }} {{ $finaltally_result->bidder_zip }}
</td> </td>
<td> <td>
{{ $finaltally_result->bidder_phone }} {{ \App\Helpers\PhoneHelper::format($finaltally_result->bidder_phone) }} </td>
</td>
<td> <td>
{{ $finaltally_result->bidder_email }} {{ $finaltally_result->bidder_email }}
</td> </td>
+40
View File
@@ -12,6 +12,21 @@
<!-- Styles --> <!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet"> <link href="{{ asset('css/app.css') }}" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
<style>
.select2-container .select2-selection--single {
height: 34px;
border: 1px solid #ccc;
border-radius: 4px;
}
.select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 34px;
}
.select2-container--default .select2-selection--single .select2-selection__arrow {
height: 32px;
}
</style>
@stack('styles')
</head> </head>
<body> <body>
<div id="app"> <div id="app">
@@ -102,6 +117,11 @@
Winners by Item Winners by Item
</a> </a>
</li> </li>
<li>
<a href="/mywinnings">
Check My Winnings
</a>
</li>
</ul> </ul>
</li> </li>
<li class="dropdown"> <li class="dropdown">
@@ -215,6 +235,16 @@
</a> </a>
<ul class="dropdown-menu" role="menu"> <ul class="dropdown-menu" role="menu">
<li>
<a href="{{ url('admin') }}">
Admin
</a>
</li>
<li>
<a href="{{ url('auth/social/oidc') }}">
Link OIDC Account
</a>
</li>
<li> <li>
<a href="{{ route('logout') }}" <a href="{{ route('logout') }}"
onclick="event.preventDefault(); onclick="event.preventDefault();
@@ -239,5 +269,15 @@
<!-- Scripts --> <!-- Scripts -->
<script src="{{ asset('js/app.js') }}"></script> <script src="{{ asset('js/app.js') }}"></script>
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
<script>
$(document).ready(function() {
$('select').select2({
placeholder: "Select an option",
allowClear: true
});
});
</script>
@stack('scripts')
</body> </body>
</html> </html>
+41
View File
@@ -0,0 +1,41 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Check My Winnings</div>
<div class="panel-body">
@if (isset($error))
<div class="alert alert-danger">
{{ $error }}
</div>
@endif
<form class="form-horizontal" method="POST" action="/mywinnings">
{{ csrf_field() }}
<div class="form-group">
<label for="bidder_number" class="col-md-4 control-label">Bidder Number</label>
<div class="col-md-6">
<input id="bidder_number" type="text" class="form-control" name="bidder_number" value="{{ old('bidder_number') }}" required autofocus>
</div>
</div>
<div class="form-group">
<div class="col-md-8 col-md-offset-4">
<button type="submit" class="btn btn-primary">
Check Winnings
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
@@ -0,0 +1,77 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="panel panel-default">
<div class="panel-heading">Winnings for Bidder #{{ $bidder->bidder_assigned_number }} - {{ $bidder->bidder_fname }} {{ $bidder->bidder_lname }}</div>
<div class="panel-body">
@if (isset($error))
<div class="alert alert-danger">
{{ $error }}
</div>
@endif
@if (session('error'))
<div class="alert alert-danger">
{{ session('error') }}
</div>
@endif
@if (session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
@if($winnings->count() > 0)
<table class="table table-striped">
<thead>
<tr>
<th>Item #</th>
<th>Description</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
@foreach($winnings as $winning)
<tr>
<td>{{ $winning->items->item_assigned_num }}</td>
<td>{{ $winning->items->item_desc }}</td>
<td>${{ number_format($winning->winning_cost, 2) }}</td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr>
<th colspan="2" class="text-right">Total:</th>
<th>${{ number_format($total_cost, 2) }}</th>
</tr>
</tfoot>
</table>
@if(!$is_checked_out)
<div class="text-right">
<a href="{{ route('north.checkout', ['bidder_id' => $bidder->idbidders]) }}" class="btn btn-success btn-lg">Pay Now with Credit Card</a>
</div>
@else
<div class="alert alert-success text-center">
<strong>Checked Out!</strong> Your payment has been processed.
</div>
@endif
@else
<p>No winning bids found for this bidder number.</p>
@endif
<hr>
<div class="text-center">
<a href="/mywinnings" class="btn btn-default">Back to Search</a>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
+112
View File
@@ -0,0 +1,112 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-12 col-md-offset-0">
<div class="panel panel-default">
<div class="panel-heading">Checkout for Bidder #{{ $bidder->bidder_assigned_number }}</div>
<div class="panel-body">
<h4>Total Amount Due: ${{ number_format($total_cost, 2) }}</h4>
<hr>
<div id="checkout-container" style="height: 100vh; overflow: hidden;">
<div class="text-center">
<p>Loading secure checkout...</p>
<div class="spinner-border" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
<hr>
<div class="text-center">
<a href="/mywinnings?bidder_number={{ $bidder->bidder_assigned_number }}" class="btn btn-default">Cancel and Return</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://checkout.north.com/checkout.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async () => {
const bidderId = "{{ $bidder->idbidders }}";
const csrfToken = "{{ csrf_token() }}";
try {
// Create a checkout session
const response = await fetch(`/north/session/${bidderId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
}
});
const data = await response.json();
console.log('North Session Response:', data);
if (data.error) {
document.getElementById('checkout-container').innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
return;
}
const sessionToken = data.sessionToken;
if (!sessionToken) {
document.getElementById('checkout-container').innerHTML = `<div class="alert alert-danger">Invalid session token received from server.</div>`;
return;
}
// Initialize North Checkout
// The global 'checkout' object is provided by checkout.js
console.log('Mounting checkout with token:', sessionToken);
await checkout.mount(sessionToken, 'checkout-container');
const handleCompletion = (result) => {
console.log('Payment complete event received:', result);
// Redirect to verify the payment on the server
window.location.href = `/north/verify/${bidderId}?sessionToken=${sessionToken}`;
};
// Handle completion
checkout.onPaymentComplete(handleCompletion);
// Support for possible variations in event names
if (typeof checkout.onPaymentSuccess === 'function') {
checkout.onPaymentSuccess(handleCompletion);
}
// Handle errors
if (typeof checkout.onPaymentError === 'function') {
checkout.onPaymentError((error) => {
console.error('Payment Error:', error);
// Don't clear the container, just prepend the error
const errorDiv = document.createElement('div');
errorDiv.className = 'alert alert-danger';
errorDiv.innerHTML = `<strong>Payment Error:</strong> ${error.message || 'An error occurred during payment.'}`;
document.querySelector('.panel-body').prepend(errorDiv);
});
}
// Show a fallback button after a short delay to allow for manual verification if the redirect fails
setTimeout(() => {
const fallbackDiv = document.createElement('div');
fallbackDiv.className = 'text-center';
fallbackDiv.style.marginTop = '20px';
fallbackDiv.innerHTML = `
<p class="text-muted">Already completed your payment but still on this page?</p>
<a href="/north/verify/${bidderId}?sessionToken=${sessionToken}" class="btn btn-info">Verify Payment Status</a>
`;
document.querySelector('.panel-body').appendChild(fallbackDiv);
}, 5000);
} catch (error) {
console.error('Checkout Error:', error);
document.getElementById('checkout-container').innerHTML = '<div class="alert alert-danger">An error occurred while initializing checkout. Please try again.</div>';
}
});
</script>
@endsection
+4 -5
View File
@@ -10,13 +10,13 @@
<tr> <tr>
<th> <th>
<h4> <h4>
St. John Catholic Church North Hackathon
<br> <br>
Car Show Silent Auction Car Show Silent Auction
<br> <br>
2099 N. Hacker Rd. 250 Stephenson Hwy
<br> <br>
Howell, MI 48855 Troy, MI 48083
</h4> </h4>
</th> </th>
<th align='right'> <th align='right'>
@@ -49,8 +49,7 @@
Name: {{ $checkout_info_result->bidder_fname }} {{ $checkout_info_result->bidder_lname }} Name: {{ $checkout_info_result->bidder_fname }} {{ $checkout_info_result->bidder_lname }}
</td> </td>
<td colspan="2"> <td colspan="2">
Phone Number: {{ $checkout_info_result->bidder_phone }} Phone Number: {{ \App\Helpers\PhoneHelper::format($checkout_info_result->bidder_phone) }} </td>
</td>
</tr> </tr>
<tr> <tr>
<td> <td>
+4 -5
View File
@@ -10,13 +10,13 @@
<tr> <tr>
<th> <th>
<h4> <h4>
St. John Catholic Church North Hackathon
<br> <br>
Car Show Silent Auction Car Show Silent Auction
<br> <br>
2099 N. Hacker Rd. 250 Stephenson Hwy
<br> <br>
Howell, MI 48855 Troy, MI 48083
</h4> </h4>
</th> </th>
<th align='right'> <th align='right'>
@@ -49,8 +49,7 @@
Name: {{ $checkout_info_result->bidder_fname }} {{ $checkout_info_result->bidder_lname }} Name: {{ $checkout_info_result->bidder_fname }} {{ $checkout_info_result->bidder_lname }}
</td> </td>
<td colspan="2"> <td colspan="2">
Phone Number: {{ $checkout_info_result->bidder_phone }} Phone Number: {{ \App\Helpers\PhoneHelper::format($checkout_info_result->bidder_phone) }} </td>
</td>
</tr> </tr>
<tr> <tr>
<td> <td>
@@ -15,7 +15,7 @@
Select Bidder Number: Select Bidder Number:
</label> </label>
<div class=col-sm-4> <div class=col-sm-4>
<select name="reprintbiddernum" id="checkoutbiddernum" required class=form-control> <select name="reprintbiddernum" id="reprintbiddernum" required class=form-control>
<?php echo App\Helpers\CheckoutBidderSelectList::checkoutBidderShowNumbers(); ?> <?php echo App\Helpers\CheckoutBidderSelectList::checkoutBidderShowNumbers(); ?>
</select> </select>
</div> </div>
+1 -1
View File
@@ -78,7 +78,7 @@
{{ $showcarlist_result->bidder_zip }} {{ $showcarlist_result->bidder_zip }}
</td> </td>
<td> <td>
{{ $showcarlist_result->bidder_phone }} {{ \App\Helpers\PhoneHelper::format($showcarlist_result->bidder_phone) }}
</td> </td>
<td> <td>
{{ $showcarlist_result->bidder_email }} {{ $showcarlist_result->bidder_email }}
+11 -11
View File
@@ -14,22 +14,12 @@
<label for=winneritemnum class="col-sm-3 col-sm-offset-1 control-label"> <label for=winneritemnum class="col-sm-3 col-sm-offset-1 control-label">
Item Number: Item Number:
</label> </label>
<div class=col-sm-2> <div class=col-sm-4>
<select name="winneritemnum" id="winneritemnum" required class=form-control> <select name="winneritemnum" id="winneritemnum" required class=form-control>
<?php echo App\Helpers\ItemSelectList::itemShowNumbers(); ?> <?php echo App\Helpers\ItemSelectList::itemShowNumbers(); ?>
</select> </select>
</div> </div>
</div> </div>
<div class=form-group>
<label for=winnerbiddernum class="col-sm-3 col-sm-offset-1 control-label">
Bidder Number:
</label>
<div class=col-sm-2>
<select name="winnerbiddernum" id="winnerbiddernum" required class=form-control>
<?php echo App\Helpers\BidderSelectList::bidderShowNumbers(); ?>
</select>
</div>
</div>
<div class=form-group> <div class=form-group>
<label for=winnerbid class="col-sm-3 col-sm-offset-1 control-label"> <label for=winnerbid class="col-sm-3 col-sm-offset-1 control-label">
Winning Bid: Winning Bid:
@@ -38,6 +28,16 @@
<input type="text" name="winnerbid" id="winnerbid" class=form-control /> <input type="text" name="winnerbid" id="winnerbid" class=form-control />
</div> </div>
</div> </div>
<div class=form-group>
<label for=winnerbiddernum class="col-sm-3 col-sm-offset-1 control-label">
Bidder Number:
</label>
<div class=col-sm-4>
<select name="winnerbiddernum" id="winnerbiddernum" required class=form-control>
<?php echo App\Helpers\BidderSelectList::bidderShowNumbers(); ?>
</select>
</div>
</div>
<div class=form-group> <div class=form-group>
<div class="col-sm-offset-5 col-sm-10"> <div class="col-sm-offset-5 col-sm-10">
<button type="submit" value="Submit" class="btn btn-primary">Submit</button> <button type="submit" value="Submit" class="btn btn-primary">Submit</button>
+1 -2
View File
@@ -20,8 +20,7 @@
Name: {{ $winnertotal_info_result->bidder_fname }} {{ $winnertotal_info_result->bidder_lname }} Name: {{ $winnertotal_info_result->bidder_fname }} {{ $winnertotal_info_result->bidder_lname }}
</th> </th>
<th> <th>
Phone Number: {{ $winnertotal_info_result->bidder_phone }} Phone Number: {{ \App\Helpers\PhoneHelper::format($winnertotal_info_result->bidder_phone) }} </th>
</th>
<th> <th>
Total: ${{ $winnertotal_info_result->total_cost }} Total: ${{ $winnertotal_info_result->total_cost }}
</th> </th>
+12
View File
@@ -14,12 +14,23 @@ if (env('APP_ENV') === 'production') {
URL::forceScheme('https'); URL::forceScheme('https');
} }
Auth::routes(['register' => false]); Auth::routes(['register' => false]);
Route::get('auth/social/{provider}', 'Auth\SocialiteController@redirectToProvider');
Route::get('auth/social/{provider}/callback', 'Auth\SocialiteController@handleProviderCallback');
Route::get('/', [ 'uses' => 'PagesController@home']); Route::get('/', [ 'uses' => 'PagesController@home']);
Route::get('home', [ 'uses' => 'PagesController@home']); Route::get('home', [ 'uses' => 'PagesController@home']);
Route::get('winningbidderlist', [ 'uses' => 'PagesController@winningbidderlist']); Route::get('winningbidderlist', [ 'uses' => 'PagesController@winningbidderlist']);
Route::get('showwinners', [ 'uses' => 'PagesController@showwinners']); Route::get('showwinners', [ 'uses' => 'PagesController@showwinners']);
Route::get('showscores', [ 'uses' => 'PagesController@showscores']); Route::get('showscores', [ 'uses' => 'PagesController@showscores']);
Route::get('showscoresbycar', [ 'uses' => 'PagesController@showscoresbycar']); Route::get('showscoresbycar', [ 'uses' => 'PagesController@showscoresbycar']);
Route::get('mywinnings', [ 'uses' => 'PagesController@myWinnings']);
Route::post('mywinnings', [ 'uses' => 'PagesController@myWinnings']);
// North Embedded Checkout
Route::get('north/checkout/{bidder_id}', [ 'uses' => 'NorthCheckoutController@checkout' ])->name('north.checkout');
Route::post('north/session/{bidder_id}', [ 'uses' => 'NorthCheckoutController@createSession' ])->name('north.session');
Route::get('north/verify/{bidder_id}', [ 'uses' => 'NorthCheckoutController@verify' ])->name('north.verify');
Route::group(['middleware' => 'auth'], function() { Route::group(['middleware' => 'auth'], function() {
Route::get('bidders', [ 'uses' => 'PagesController@bidders']); Route::get('bidders', [ 'uses' => 'PagesController@bidders']);
@@ -39,6 +50,7 @@ Route::group(['middleware' => 'auth'], function() {
Route::get('reprint_receipt', ['uses' => 'PagesController@reprintReceipt']); Route::get('reprint_receipt', ['uses' => 'PagesController@reprintReceipt']);
Route::post('reprint_receipt', ['uses' => 'PagesController@reprintReceipt']); Route::post('reprint_receipt', ['uses' => 'PagesController@reprintReceipt']);
Route::get('receiptpdf', ['uses' => 'PagesController@receiptpdf'])->name('receiptpdf'); Route::get('receiptpdf', ['uses' => 'PagesController@receiptpdf'])->name('receiptpdf');
Route::get('download_receipt', ['uses' => 'PagesController@downloadReceiptPdf'])->name('download_receipt');
Route::get('winners', [ 'uses' => 'PagesController@winners']); Route::get('winners', [ 'uses' => 'PagesController@winners']);
Route::post('winners', [ 'uses' => 'PagesController@winners']); Route::post('winners', [ 'uses' => 'PagesController@winners']);
Route::get('winnerlist', [ 'uses' => 'PagesController@winnerlist']); Route::get('winnerlist', [ 'uses' => 'PagesController@winnerlist']);
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use App\Models\Bidders;
use App\Models\Items;
use App\Models\WinningBids;
class MyWinningsTest extends TestCase
{
use DatabaseTransactions;
public function test_mywinnings_form_is_accessible()
{
$response = $this->get('/mywinnings');
$response->assertStatus(200);
$response->assertSee('Check My Winnings');
}
public function test_mywinnings_results_show_correct_data()
{
// Create a bidder
$bidder = Bidders::create([
'bidder_assigned_number' => '999',
'bidder_fname' => 'Test',
'bidder_lname' => 'User',
]);
// Create an item
$item = Items::create([
'item_assigned_num' => '777',
'item_desc' => 'Test Item',
'item_min_bid' => 10,
'item_est_value' => 20,
]);
// Create a winning bid
WinningBids::create([
'winning_bidder_num' => $bidder->idbidders,
'winning_item_num' => $item->iditems,
'winning_cost' => 15,
]);
$response = $this->post('/mywinnings', [
'bidder_number' => '999'
]);
$response->assertStatus(200);
$response->assertSee('Winnings for Bidder #999');
$response->assertSee('Test Item');
$response->assertSee('$15.00');
}
public function test_mywinnings_invalid_bidder_shows_error()
{
$response = $this->post('/mywinnings', [
'bidder_number' => 'NONEXISTENT'
]);
$response->assertStatus(200);
$response->assertSee('Bidder number not found.');
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace Tests\Unit;
use Tests\TestCase;
use App\Models\Bidders;
use App\Helpers\PhoneHelper;
class PhoneFormattingTest extends TestCase
{
/** @test */
public function it_formats_a_10_digit_phone_number()
{
$formatted = PhoneHelper::format('1234567890');
$this->assertEquals('123-456-7890', $formatted);
}
/** @test */
public function it_strips_non_numeric_characters_before_formatting()
{
$formatted = PhoneHelper::format('(123) 456-7890');
$this->assertEquals('123-456-7890', $formatted);
}
/** @test */
public function it_returns_original_value_if_not_10_digits()
{
$formatted = PhoneHelper::format('1234567');
$this->assertEquals('1234567', $formatted);
}
}