Remove Amazon filament price tracking
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 2m41s
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 2m41s
This commit is contained in:
@@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Filament;
|
||||
use App\Models\FilamentPrice;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Attributes\Description;
|
||||
use Illuminate\Console\Attributes\Signature;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Throwable;
|
||||
|
||||
#[Signature('filaments:refresh')]
|
||||
#[Description('Collect current Amazon prices for tracked filament')]
|
||||
class RefreshFilamentPrices extends Command
|
||||
{
|
||||
public function handle(): int
|
||||
{
|
||||
$drops = [];
|
||||
foreach (Filament::cursor() as $filament) {
|
||||
$host = strtolower((string) parse_url($filament->amazonUrl, PHP_URL_HOST));
|
||||
if (! in_array($host, ['amazon.co.uk', 'www.amazon.co.uk', 'amazon.com', 'www.amazon.com'], true)) {
|
||||
$this->warn($filament->filamentName.': unsupported product host');
|
||||
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$html = Http::timeout(15)->withHeaders(['User-Agent' => 'Mozilla/5.0 HickLab3D/1.0'])
|
||||
->get($filament->amazonUrl)->throw()->body();
|
||||
if (! preg_match('/class="[^"]*a-price-whole[^"]*"[^>]*>([0-9,.]+)/', $html, $whole)
|
||||
|| ! preg_match('/class="[^"]*a-price-fraction[^"]*"[^>]*>([0-9]{2})/', $html, $fraction)) {
|
||||
$this->warn($filament->filamentName.': price unavailable');
|
||||
|
||||
continue;
|
||||
}
|
||||
$price = (float) (str_replace(',', '', $whole[1]).'.'.$fraction[1]);
|
||||
if ($price <= 0) {
|
||||
continue;
|
||||
}
|
||||
preg_match('/class="[^"]*savingsPercentage[^"]*"[^>]*>[^0-9]*([0-9]+)%/', $html, $discount);
|
||||
$previous = FilamentPrice::where('filamentId', $filament->id)->orderByDesc('recordedAt')->orderByDesc('id')->first();
|
||||
FilamentPrice::create([
|
||||
'filamentId' => $filament->id,
|
||||
'price' => $price,
|
||||
'currentDiscount' => ['discount' => ['value' => (int) ($discount[1] ?? 0), 'type' => 'percentage'], 'voucher' => ['value' => 0, 'type' => 'none']],
|
||||
]);
|
||||
if ($previous && $price < (float) $previous->price) {
|
||||
$drops[] = $filament->filamentName.': £'.number_format((float) $previous->price, 2).' → £'.number_format($price, 2).' ('.$filament->amazonUrl.')';
|
||||
}
|
||||
$this->line($filament->filamentName.': £'.number_format($price, 2));
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
$this->warn($filament->filamentName.': collection failed');
|
||||
}
|
||||
}
|
||||
if ($drops) {
|
||||
$body = "HickLab3D filament price drops\n\n".implode("\n", $drops);
|
||||
foreach (User::where('alertEmails', true)->where('disabled', false)->pluck('email') as $email) {
|
||||
Mail::raw($body, fn ($message) => $message->to($email)->subject('HickLab3D filament price drops'));
|
||||
}
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\EtsyConnection;
|
||||
use App\Models\Filament;
|
||||
use App\Services\Bambuddy;
|
||||
use App\Services\DryerClient;
|
||||
use App\Services\Etsy;
|
||||
@@ -18,16 +17,6 @@ class DashboardController extends Controller
|
||||
public function __invoke(Request $request, Etsy $etsy, Bambuddy $bambuddy, Parcel2Go $parcel2Go, DryerClient $dryer): View
|
||||
{
|
||||
$isAdmin = $request->user()->role === 'admin';
|
||||
$filamentCount = Filament::count();
|
||||
$recentFilaments = Filament::with(['prices' => fn ($query) => $query->orderByDesc('recordedAt')->limit(1)])
|
||||
->orderByDesc('id')->limit(4)->get();
|
||||
$materials = Filament::query()->select('material')->selectRaw('COUNT(*) as total')
|
||||
->groupBy('material')->orderByDesc('total')->get()
|
||||
->map(fn (Filament $filament): array => [
|
||||
'label' => $filament->material ?: 'Unspecified',
|
||||
'count' => (int) $filament->total,
|
||||
])->all();
|
||||
|
||||
$etsyStatus = $isAdmin ? 'Not connected' : null;
|
||||
$etsyConnected = false;
|
||||
$receipts = [];
|
||||
@@ -75,11 +64,25 @@ class DashboardController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$printingCount = collect($printers)->filter(fn (array $printer): bool => in_array(
|
||||
strtoupper((string) ($printer['live']['state'] ?? $printer['status'] ?? '')),
|
||||
['RUNNING', 'PRINTING'],
|
||||
true,
|
||||
))->count();
|
||||
$printerStates = collect($printers)->countBy(function (array $printer): string {
|
||||
$live = $printer['live'] ?? [];
|
||||
$state = strtoupper((string) ($live['state'] ?? $printer['status'] ?? ''));
|
||||
|
||||
if ($live === []) {
|
||||
return 'Unavailable';
|
||||
}
|
||||
if (($live['connected'] ?? false) === false) {
|
||||
return 'Offline';
|
||||
}
|
||||
if (in_array($state, ['RUNNING', 'PRINTING'], true)) {
|
||||
return 'Printing';
|
||||
}
|
||||
if (in_array($state, ['IDLE', 'READY', 'FINISH'], true)) {
|
||||
return 'Idle';
|
||||
}
|
||||
|
||||
return 'Other';
|
||||
});
|
||||
|
||||
$dryerState = 'Not configured';
|
||||
if (filled(config('services.dryer.api_key'))) {
|
||||
@@ -99,9 +102,6 @@ class DashboardController extends Controller
|
||||
|
||||
return view('dashboard', [
|
||||
'isAdmin' => $isAdmin,
|
||||
'filamentCount' => $filamentCount,
|
||||
'recentFilaments' => $recentFilaments,
|
||||
'materials' => $materials,
|
||||
'receipts' => array_slice($receipts, 0, 5),
|
||||
'awaitingDispatch' => count($receipts),
|
||||
'listingCount' => $listingCount,
|
||||
@@ -111,7 +111,11 @@ class DashboardController extends Controller
|
||||
'shippingStatus' => $shippingStatus,
|
||||
'printers' => $printers,
|
||||
'printerStatus' => $printerStatus,
|
||||
'printingCount' => $printingCount,
|
||||
'printingCount' => $printerStates->get('Printing', 0),
|
||||
'printerChart' => $printerStates->map(fn (int $count, string $state): array => [
|
||||
'label' => $state,
|
||||
'count' => $count,
|
||||
])->values()->all(),
|
||||
'dryerState' => $dryerState,
|
||||
'orderChart' => [
|
||||
'labels' => $days->map(fn (Carbon $day): string => $day->format('d M'))->all(),
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Filament;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class FilamentController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('filaments.index', ['filaments' => Filament::with(['prices' => fn ($query) => $query->orderByDesc('recordedAt')->limit(10)])->orderBy('filamentName')->paginate(20)]);
|
||||
}
|
||||
|
||||
public function create(Request $request): View
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
|
||||
return view('filaments.create');
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
$data = $request->validate([
|
||||
'filamentName' => ['required', 'string', 'max:255'],
|
||||
'amazonUrl' => ['required', 'url:http,https', 'max:2048'],
|
||||
'brand' => ['nullable', 'string', 'max:255'],
|
||||
'material' => ['nullable', 'string', 'max:255'],
|
||||
'color' => ['nullable', 'string', 'max:255'],
|
||||
'filamentWeight' => ['nullable', 'numeric', 'min:0'],
|
||||
'itemDiameter' => ['nullable', 'numeric', 'min:0'],
|
||||
]);
|
||||
$host = strtolower((string) parse_url($data['amazonUrl'], PHP_URL_HOST));
|
||||
if (! in_array($host, ['amazon.co.uk', 'www.amazon.co.uk', 'amazon.com', 'www.amazon.com'], true)) {
|
||||
return back()->withErrors(['amazonUrl' => 'Enter an Amazon UK or US product URL.'])->withInput();
|
||||
}
|
||||
Filament::create([...$data, 'userId' => $request->user()->id]);
|
||||
|
||||
return redirect()->route('filaments.index')->with('status', 'Filament added. Price collection runs on the schedule.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, Filament $filament): RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->role === 'admin', 403);
|
||||
$filament->delete();
|
||||
|
||||
return redirect()->route('filaments.index')->with('status', 'Filament removed.');
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@ class UserController extends Controller
|
||||
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
|
||||
'role' => ['required', Rule::in(['admin', 'user'])],
|
||||
'password' => ['required', 'string', 'min:12'],
|
||||
'alertEmails' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
User::create($data);
|
||||
|
||||
@@ -39,7 +38,6 @@ class UserController extends Controller
|
||||
'email' => ['sometimes', 'email', Rule::unique('users', 'email')->ignore($user->id)],
|
||||
'role' => ['sometimes', Rule::in(['admin', 'user'])],
|
||||
'disabled' => ['sometimes', 'boolean'],
|
||||
'alertEmails' => ['sometimes', 'boolean'],
|
||||
'password' => ['nullable', 'string', 'min:12'],
|
||||
]);
|
||||
if (empty($data['password'])) {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\FilamentFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Filament extends Model
|
||||
{
|
||||
/** @use HasFactory<FilamentFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'filamentTracker';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = ['userId', 'filamentName', 'amazonUrl', 'filamentWeight', 'brand', 'material', 'color', 'itemDiameter'];
|
||||
|
||||
public function prices(): HasMany
|
||||
{
|
||||
return $this->hasMany(FilamentPrice::class, 'filamentId');
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\FilamentPriceFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class FilamentPrice extends Model
|
||||
{
|
||||
/** @use HasFactory<FilamentPriceFactory> */
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'filamentPriceHistory';
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = ['filamentId', 'price', 'currentDiscount'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['currentDiscount' => 'array', 'recordedAt' => 'datetime', 'price' => 'decimal:2'];
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
#[Fillable(['username', 'email', 'role', 'disabled', 'alertEmails', 'password'])]
|
||||
#[Fillable(['username', 'email', 'role', 'disabled', 'password'])]
|
||||
#[Hidden(['password'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
@@ -29,7 +29,6 @@ class User extends Authenticatable
|
||||
return [
|
||||
'password' => 'hashed',
|
||||
'disabled' => 'boolean',
|
||||
'alertEmails' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user