43 |
44 |
--------------------------------------------------------------------------------
/app/Actions/ShowProduct.php:
--------------------------------------------------------------------------------
1 | loadVariationTree($product);
16 |
17 | // load product's variations stock
18 | $product->loadStock();
19 |
20 | // prepare media urls
21 | $this->loadMedia($product);
22 |
23 | // Preload product on variations
24 | $this->preloadProductsOnVariation($product);
25 |
26 | return $product;
27 | }
28 |
29 | private function loadVariationTree(Product $product): void
30 | {
31 | $product->setRelation(
32 | 'variations',
33 | Variation::query()
34 | ->with('stocks')
35 | ->treeOf(fn($query) => $query->isRoot()->where('product_id', $product->id))
36 | ->get()
37 | ->toTree()
38 | );
39 | }
40 |
41 | private function loadMedia($product): void
42 | {
43 | // Load relationship
44 | // This way, we can load media to multiple product at once when needed
45 | $product->load('media');
46 |
47 | // Do in-memory image generation
48 | $product->getMediaUrls();
49 | }
50 |
51 | private function preloadProductsOnVariation($product): void
52 | {
53 | $product->variations->each->setRelation('product', $product);
54 | }
55 | }
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/RegisteredUserController.php:
--------------------------------------------------------------------------------
1 | validate([
38 | 'name' => 'required|string|max:255',
39 | 'email' => 'required|string|email|max:255|unique:'.User::class,
40 | 'password' => ['required', 'confirmed', Rules\Password::defaults()],
41 | ]);
42 |
43 | $user = User::create([
44 | 'name' => $request->name,
45 | 'email' => $request->email,
46 | 'password' => Hash::make($request->password),
47 | ]);
48 |
49 | event(new Registered($user));
50 |
51 | Auth::login($user);
52 |
53 | return redirect(RouteServiceProvider::HOME);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/PasswordResetLinkController.php:
--------------------------------------------------------------------------------
1 | session('status'),
22 | ]);
23 | }
24 |
25 | /**
26 | * Handle an incoming password reset link request.
27 | *
28 | * @param \Illuminate\Http\Request $request
29 | * @return \Illuminate\Http\RedirectResponse
30 | *
31 | * @throws \Illuminate\Validation\ValidationException
32 | */
33 | public function store(Request $request)
34 | {
35 | $request->validate([
36 | 'email' => 'required|email',
37 | ]);
38 |
39 | // We will send the password reset link to this user. Once we have attempted
40 | // to send the link, we will examine the response then see the message we
41 | // need to show to the user. Finally, we'll send out a proper response.
42 | $status = Password::sendResetLink(
43 | $request->only('email')
44 | );
45 |
46 | if ($status == Password::RESET_LINK_SENT) {
47 | return back()->with('status', __($status));
48 | }
49 |
50 | throw ValidationException::withMessages([
51 | 'email' => [trans($status)],
52 | ]);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/config/hashing.php:
--------------------------------------------------------------------------------
1 | 'bcrypt',
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Bcrypt Options
23 | |--------------------------------------------------------------------------
24 | |
25 | | Here you may specify the configuration options that should be used when
26 | | passwords are hashed using the Bcrypt algorithm. This will allow you
27 | | to control the amount of time it takes to hash the given password.
28 | |
29 | */
30 |
31 | 'bcrypt' => [
32 | 'rounds' => env('BCRYPT_ROUNDS', 10),
33 | ],
34 |
35 | /*
36 | |--------------------------------------------------------------------------
37 | | Argon Options
38 | |--------------------------------------------------------------------------
39 | |
40 | | Here you may specify the configuration options that should be used when
41 | | passwords are hashed using the Argon algorithm. These will allow you
42 | | to control the amount of time it takes to hash the given password.
43 | |
44 | */
45 |
46 | 'argon' => [
47 | 'memory' => 65536,
48 | 'threads' => 1,
49 | 'time' => 4,
50 | ],
51 |
52 | ];
53 |
--------------------------------------------------------------------------------
/app/Http/Middleware/HandleInertiaRequests.php:
--------------------------------------------------------------------------------
1 | [
44 | 'user' => $request->user(),
45 | ],
46 | 'cart' => fn () => $this->cart->toResource(),
47 | 'money' => [
48 | 'locale' => config('money.locale'),
49 | 'currency' => config('money.defaultCurrency'),
50 | ],
51 | 'flash' => [
52 | 'notification' => fn () => $request->session()->get('notification')
53 | ],
54 | 'ziggy' => function () use ($request) {
55 | return array_merge((new Ziggy())->toArray(), [
56 | 'location' => $request->url(),
57 | ]);
58 | },
59 | ]);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/resources/js/Pages/Auth/ConfirmPassword.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | This is a secure area of the application. Please confirm your password before continuing.
26 |
27 |
28 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/app/Models/Order.php:
--------------------------------------------------------------------------------
1 | 'Order Placed',
33 | 'packaged_at' => 'Order Packaged',
34 | 'shipped_at' => 'Order Shipped',
35 | ];
36 |
37 | protected function getMoneyAttribute(): string
38 | {
39 | return 'subtotal';
40 | }
41 |
42 | public function user(): BelongsTo
43 | {
44 | return $this->belongsTo(User::class);
45 | }
46 |
47 | public function shippingAddress(): BelongsTo
48 | {
49 | return $this->belongsTo(ShippingAddress::class);
50 | }
51 |
52 | public function shippingType(): BelongsTo
53 | {
54 | return $this->belongsTo(ShippingType::class);
55 | }
56 |
57 | public function variations(): BelongsToMany
58 | {
59 | return $this->belongsToMany(Variation::class)
60 | ->withPivot(['quantity'])
61 | ->withTimestamps();
62 | }
63 |
64 | public function status()
65 | {
66 | return collect($this->statuses)
67 | ->last(fn ($status, $key) => filled($this->{$key}));
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/Traits/HasStock.php:
--------------------------------------------------------------------------------
1 | getStock($this);
12 | }
13 |
14 | private function getStock (Variation $variation): int
15 | {
16 | $stockFigures = StockFigures::make();
17 |
18 | if ($variation->relationLoaded('stocks'))
19 | {
20 | $stockFigures->stockCount = $this->calculateSelfStock($variation);
21 |
22 | $this->calculateStockState($stockFigures);
23 | $variation['stockFigures'] = $stockFigures;
24 | }
25 |
26 | if ($variation->relationLoaded('children')) {
27 | foreach ($variation->children as $childVariation)
28 | {
29 | $stockFigures->stockCount += $this->getStock($childVariation);
30 |
31 | $this->calculateStockState($stockFigures);
32 | $variation['stockFigures'] = $stockFigures;
33 | }
34 | }
35 |
36 | return $stockFigures->stockCount;
37 | }
38 |
39 | private function calculateSelfStock(Variation $variation): int
40 | {
41 | return array_reduce($variation->stocks->toArray(), function ($carry, $item) {
42 | return $carry + $item['amount'];
43 | }, 0);
44 | }
45 |
46 | private function minStock(): int
47 | {
48 | return intVal(config('services.shop.lowStock'));
49 | }
50 |
51 | private function calculateStockState($stockFigures): void
52 | {
53 | $stockFigures->inStock = $stockFigures->stockCount > 0;
54 | $stockFigures->outOfStock = $stockFigures->stockCount <= 0;
55 | $stockFigures->lowStock = !$stockFigures->outOfStock && $stockFigures->stockCount < $this->minStock();
56 | }
57 | }
--------------------------------------------------------------------------------
/bootstrap/app.php:
--------------------------------------------------------------------------------
1 | singleton(
30 | Illuminate\Contracts\Http\Kernel::class,
31 | App\Http\Kernel::class
32 | );
33 |
34 | $app->singleton(
35 | Illuminate\Contracts\Console\Kernel::class,
36 | App\Console\Kernel::class
37 | );
38 |
39 | $app->singleton(
40 | Illuminate\Contracts\Debug\ExceptionHandler::class,
41 | App\Exceptions\Handler::class
42 | );
43 |
44 | /*
45 | |--------------------------------------------------------------------------
46 | | Return The Application
47 | |--------------------------------------------------------------------------
48 | |
49 | | This script returns the application instance. The instance is given to
50 | | the calling script so we can separate the building of the instances
51 | | from the actual running of the application and sending responses.
52 | |
53 | */
54 |
55 | return $app;
56 |
--------------------------------------------------------------------------------
/artisan:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | make(Illuminate\Contracts\Console\Kernel::class);
34 |
35 | $status = $kernel->handle(
36 | $input = new Symfony\Component\Console\Input\ArgvInput,
37 | new Symfony\Component\Console\Output\ConsoleOutput
38 | );
39 |
40 | /*
41 | |--------------------------------------------------------------------------
42 | | Shutdown The Application
43 | |--------------------------------------------------------------------------
44 | |
45 | | Once Artisan has finished running, we will fire off the shutdown events
46 | | so that any final work may be done by the application before we shut
47 | | down the process. This is the last thing to happen to the request.
48 | |
49 | */
50 |
51 | $kernel->terminate($input, $status);
52 |
53 | exit($status);
54 |
--------------------------------------------------------------------------------
/app/Http/Controllers/PaymentIntentController.php:
--------------------------------------------------------------------------------
1 | hasPaymentIntent()) {
22 | $paymentIntent = app('stripe')->paymentIntents->create([
23 | 'amount' => (int)$request->total, // cents
24 | 'currency' => config('money.defaultCurrency'),
25 | 'setup_future_usage' => 'on_session', // on-time payment
26 | 'metadata' => [
27 | 'user_id' => $request->user()?->id, // can be used later in Webhooks
28 | ],
29 | 'payment_method_types' => ['card'],
30 | ]);
31 |
32 | // store payment intent id on cart
33 | $cart->updatePaymentIntentId($paymentIntent->id);
34 | } else {
35 | $paymentIntent = app('stripe')->paymentIntents->retrieve($cart->getPaymentIntentId());
36 |
37 | // update only pending payment intent
38 | if ($paymentIntent->status !== 'succeeded') {
39 | app('stripe')->paymentIntents->update($cart->getPaymentIntentId(), [
40 | 'amount' => (int)$request->total,
41 | ]);
42 |
43 | $paymentIntent->amount = (int)$request->total;
44 | }
45 | }
46 |
47 | return PaymentIntentResource::make($paymentIntent);
48 | }
49 | }
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 | make(Kernel::class);
50 |
51 | $response = $kernel->handle(
52 | $request = Request::capture()
53 | )->send();
54 |
55 | $kernel->terminate($request, $response);
56 |
--------------------------------------------------------------------------------
/resources/js/Pages/Auth/VerifyEmail.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Thanks for signing up! Before getting started, could you verify your email address by clicking on the link
26 | we just emailed to you? If you didn't receive the email, we will gladly send you another.
27 |
28 |
29 |
30 | A new verification link has been sent to the email address you provided during registration.
31 |
27 | Forgot your password? No problem. Just let us know your email address and we will email you a password reset
28 | link that will allow you to choose a new one.
29 |