Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions server/src/Console/Commands/PurgeExpiredCarts.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Fleetbase\Storefront\Console\Commands;

use Fleetbase\Storefront\Models\Cart;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

Expand Down Expand Up @@ -51,6 +52,13 @@ public function handle()
$schema->enableForeignKeyConstraints();
}

// The rows were deleted through the query builder without model events and span
// companies, so the table wide cart cache version is advanced (which every
// company's cart caches read from fleetbase/core-api#301 on).
if ($dbDeletedCount > 0) {
Cart::invalidateApiCacheManually();
}

// Log output
$this->info("Successfully deleted {$dbDeletedCount} expired carts.");

Expand Down
9 changes: 8 additions & 1 deletion server/src/Http/Controllers/CatalogController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Fleetbase\Storefront\Models\CatalogSubject;
use Fleetbase\Storefront\Models\FoodTruck;
use Fleetbase\Storefront\Models\Store;
use Fleetbase\Support\ApiModelCache;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
Expand Down Expand Up @@ -56,10 +57,16 @@ private function syncSubjects(Catalog $catalog, string $subjectType, array $subj
->unique()
->values();

CatalogSubject::where(['catalog_uuid' => $catalog->uuid, 'subject_type' => $subjectType])
$removed = CatalogSubject::where(['catalog_uuid' => $catalog->uuid, 'subject_type' => $subjectType])
->whereNotIn('subject_uuid', $incoming)
->delete();

// A builder delete raises no model events, so the cached lists embedding the
// assignments (catalogs, food trucks) are invalidated here.
if ($removed > 0) {
ApiModelCache::invalidateModelCache(new CatalogSubject(), $catalog->company_uuid);
}

foreach ($incoming as $subjectUuid) {
CatalogSubject::firstOrCreate(
[
Expand Down
9 changes: 8 additions & 1 deletion server/src/Http/Controllers/NetworkController.php
Original file line number Diff line number Diff line change
Expand Up @@ -766,12 +766,15 @@ protected function setMembershipStatus(string $networkId, array $stores, string
/**
* Membership rows change through the query builder, which raises no model events,
* so the cached store listings (which carry category and membership status) are
* invalidated here the way a store update would.
* invalidated here the way a store update would, and so are the lists that embed
* `network_stores` (stores, products and food trucks, by their declared cache
* dependencies), the way a membership's own model events would.
*/
protected function forgetCachedResponses(): void
{
if (class_exists(ApiModelCache::class)) {
ApiModelCache::invalidateModelCache(new Store(), session('company'));
ApiModelCache::invalidateModelCache(new NetworkStore(), session('company'));
}
}

Expand Down Expand Up @@ -846,6 +849,10 @@ public function deleteCategory(string $id, NetworkActionRequest $request)
// delete the category
Category::where(['owner_uuid' => $id, 'uuid' => $category])->delete();

// Both writes went through the query builder without model events
$this->forgetCachedResponses();
Category::invalidateApiCacheManually(session('company'));

return response()->json(['status' => 'ok']);
}
}
3 changes: 3 additions & 0 deletions server/src/Http/Controllers/v1/CheckoutController.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use Fleetbase\Storefront\Support\Storefront;
use Fleetbase\Storefront\Support\StorefrontSocket;
use Fleetbase\Storefront\Support\StripeUtils;
use Fleetbase\Support\ApiModelCache;
use Fleetbase\Support\SocketCluster\SocketClusterService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
Expand Down Expand Up @@ -2147,6 +2148,8 @@ protected static function discardFailedCapture(?Transaction $transaction, array
}
if ($transaction) {
TransactionItem::where('transaction_uuid', $transaction->uuid)->delete();
// The builder delete raises no model events; clear the cached lists embedding the items
ApiModelCache::invalidateModelCache(new TransactionItem(), $transaction->company_uuid);
$transaction->delete();
}
} catch (\Throwable $cleanup) {
Expand Down
16 changes: 16 additions & 0 deletions server/src/Models/AddonCategory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Fleetbase\Casts\Money as MoneyCast;
use Fleetbase\Models\Category;
use Fleetbase\Support\ApiModelCache;
use Illuminate\Support\Str;

class AddonCategory extends Category
Expand All @@ -15,6 +16,14 @@ class AddonCategory extends Category
*/
protected $with = ['addons'];

/**
* Tables the cached addon category lists embed: the addons loaded with every category
* (read by core's ApiModelCache from fleetbase/core-api#301 on).
*
* @var string[]
*/
protected $apiCacheDependencies = ['addons'];

/**
* The key to use in the payload responses.
*/
Expand Down Expand Up @@ -42,6 +51,7 @@ public function addons()

public function setAddons(array $addons = []): AddonCategory
{
$updatedQuietly = false;
foreach ($addons as $addon) {
// get uuid if set
$id = data_get($addon, 'uuid');
Expand All @@ -60,6 +70,7 @@ public function setAddons(array $addons = []): AddonCategory
// update product addon category
if (Str::isUuid($id)) {
ProductAddon::where('uuid', $id)->update($upsertableProductAddon);
$updatedQuietly = true;
continue;
}

Expand All @@ -70,6 +81,11 @@ public function setAddons(array $addons = []): AddonCategory
]);
}

// Builder updates raise no model events, so the cached addon category and product lists are invalidated here
if ($updatedQuietly) {
ApiModelCache::invalidateModelCache(new ProductAddon(), $this->company_uuid);
}

return $this;
}
}
10 changes: 10 additions & 0 deletions server/src/Models/Catalog.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ class Catalog extends StorefrontModel
*/
protected $table = 'catalogs';

/**
* Tables the cached catalog lists embed, so writes to them invalidate those lists
* (read by core's ApiModelCache from fleetbase/core-api#301 on). The console list
* eager loads `categories.products`, `hours` and `assignments.subject`; the subject
* is a morphTo, so its tables are named.
*
* @var string[]
*/
protected $apiCacheDependencies = ['categories', 'categories.products', 'hours', 'assignments', 'stores', 'food_trucks'];

/**
* Searchable columns.
*
Expand Down
22 changes: 21 additions & 1 deletion server/src/Models/CatalogCategory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Fleetbase\Storefront\Models;

use Fleetbase\Models\Category;
use Fleetbase\Support\ApiModelCache;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
Expand Down Expand Up @@ -111,11 +112,13 @@ public function setProducts(array $products = [], ?array $overrides = null): Cat
$incomingProductUuids = array_values(array_unique($incomingProductUuids));

// 1) Remove pivot rows for products not in incoming list
$toRemove = array_diff($existingProductUuids, $incomingProductUuids);
$toRemove = array_diff($existingProductUuids, $incomingProductUuids);
$pivotsWrittenQuietly = false;
if (!empty($toRemove)) {
CatalogProduct::where('catalog_category_uuid', $this->uuid)
->whereIn('product_uuid', $toRemove)
->delete();
$pivotsWrittenQuietly = true;
}

// 2) Create pivot rows for new products, carrying their overrides
Expand Down Expand Up @@ -149,11 +152,28 @@ public function setProducts(array $products = [], ?array $overrides = null): Cat
CatalogProduct::where('catalog_category_uuid', $this->uuid)
->where('product_uuid', $pivot->product_uuid)
->update($next);
$pivotsWrittenQuietly = true;
}

// Removals and override edits go through the query builder, which raises no model
// events, so the cached lists embedding these pivots (catalogs, products) are
// invalidated here.
if ($pivotsWrittenQuietly) {
static::invalidateCatalogProductCache($this->company_uuid);
}

return $this;
}

/**
* Invalidate the cached lists that embed `catalog_category_products`, after writes
* to it that raise no model events.
*/
public static function invalidateCatalogProductCache(?string $companyUuid = null): void
{
ApiModelCache::invalidateModelCache(new CatalogProduct(), $companyUuid);
}

/**
* The storable form of a per-catalog override: a price in minor units or null, and an
* availability flag or null (inherit from the store).
Expand Down
2 changes: 2 additions & 0 deletions server/src/Models/CatalogProduct.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Fleetbase\Storefront\Models;

use Fleetbase\Traits\HasApiModelBehavior;
use Fleetbase\Traits\HasUuid;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Eloquent\SoftDeletes;
Expand All @@ -10,6 +11,7 @@ class CatalogProduct extends Pivot
{
use SoftDeletes;
use HasUuid;
use HasApiModelBehavior;

protected $connection = 'storefront';
protected $table = 'catalog_category_products';
Expand Down
16 changes: 16 additions & 0 deletions server/src/Models/FoodTruck.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Fleetbase\FleetOps\Models\Zone;
use Fleetbase\LaravelMysqlSpatial\Types\Point;
use Fleetbase\Storefront\Http\Resources\FoodTruck as FoodTruckResource;
use Fleetbase\Support\ApiModelCache;
use Fleetbase\Traits\HasApiModelBehavior;
use Fleetbase\Traits\HasApiModelCache;
use Fleetbase\Traits\HasPublicid;
Expand Down Expand Up @@ -51,6 +52,16 @@ class FoodTruck extends StorefrontModel
*/
protected $table = 'food_trucks';

/**
* Tables the cached food truck lists embed, so writes to them invalidate those lists
* (read by core's ApiModelCache from fleetbase/core-api#301 on): the relations the
* console and storefront lists eager load (a service area loads its zones), and the
* store's networks, which decide the trucks a network lists.
*
* @var string[]
*/
protected $apiCacheDependencies = ['store', 'store.networks', 'network', 'vehicle', 'vehicle.driver', 'serviceArea', 'serviceArea.zones', 'zone', 'catalogs'];

/**
* The attributes that are mass assignable.
*
Expand Down Expand Up @@ -216,6 +227,11 @@ public function setCatalogs(array $catalogs = []): FoodTruck
$this->invalidateApiCache();
}

// Removed assignments were deleted without model events; the catalog lists embed them too.
if ($removed->isNotEmpty()) {
ApiModelCache::invalidateModelCache(new CatalogSubject(), $this->company_uuid);
}

return $this;
}

Expand Down
35 changes: 35 additions & 0 deletions server/src/Models/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Fleetbase\Models\Category;
use Fleetbase\Models\File;
use Fleetbase\Models\User;
use Fleetbase\Support\ApiModelCache;
use Fleetbase\Traits\HasApiModelBehavior;
use Fleetbase\Traits\HasApiModelCache;
use Fleetbase\Traits\HasMetaAttributes;
Expand Down Expand Up @@ -51,6 +52,21 @@ class Product extends StorefrontModel
*/
protected $table = 'products';

/**
* Tables the cached product lists embed, so writes to them invalidate those lists
* (read by core's ApiModelCache from fleetbase/core-api#301 on): what the storefront
* list eager loads (addon categories with their addons, variants with their options,
* the store), the store's networks a network storefront filters by, and the catalog
* categories `catalogs_count` counts. The images (`files`, and the store's logo) are
* left out: every upload in a company writes the files table, which would keep
* product lists from ever being served from cache. FileObserver invalidates product
* lists when a product's own files change instead, and a new store logo is written
* to the store row.
*
* @var string[]
*/
protected $apiCacheDependencies = ['addonCategories', 'addonCategories.category', 'addonCategories.category.addons', 'variants', 'variants.options', 'store', 'store.networks', 'catalogCategories'];

/**
* The default database connection to use.
*
Expand Down Expand Up @@ -364,6 +380,7 @@ public function setSalePriceAttribute($value)
*/
public function setAddonCategories(array $addonCategories = []): Product
{
$updatedQuietly = false;
foreach ($addonCategories as $addonCategory) {
// get uuid if set
$id = data_get($addonCategory, 'uuid');
Expand All @@ -379,13 +396,19 @@ public function setAddonCategories(array $addonCategories = []): Product
// update product addon category
if (Str::isUuid($id)) {
ProductAddonCategory::where('uuid', $id)->update($upsertableAddonCategory);
$updatedQuietly = true;
continue;
}

// create new product addon category
ProductAddonCategory::create($upsertableAddonCategory);
}

// Builder updates raise no model events, so the cached product lists are invalidated here
if ($updatedQuietly) {
ApiModelCache::invalidateModelCache(new ProductAddonCategory(), $this->company_uuid);
}

return $this;
}

Expand All @@ -404,6 +427,8 @@ public function setAddonCategories(array $addonCategories = []): Product
*/
public function setProductVariants(array $variants = []): Product
{
$variantsUpdatedQuietly = false;
$optionsUpdatedQuietly = false;
foreach ($variants as $variant) {
$id = data_get($variant, 'uuid');

Expand All @@ -419,6 +444,7 @@ public function setProductVariants(array $variants = []): Product
'min' => data_get($variant, 'min') ?? 0,
'max' => data_get($variant, 'max') ?? 100,
]);
$variantsUpdatedQuietly = true;

// Update product variant options if applicable
if (is_array($variant['options'])) {
Expand All @@ -438,6 +464,7 @@ public function setProductVariants(array $variants = []): Product

$productVariantOptionInput = Arr::except($option, ['uuid', 'created_at', 'updated_at']);
ProductVariantOption::where('uuid', $option['uuid'])->update($productVariantOptionInput);
$optionsUpdatedQuietly = true;
continue;
}

Expand All @@ -464,6 +491,14 @@ public function setProductVariants(array $variants = []): Product
}
}

// Builder updates raise no model events, so the cached product lists are invalidated here
if ($variantsUpdatedQuietly) {
ApiModelCache::invalidateModelCache(new ProductVariant(), $this->company_uuid);
}
if ($optionsUpdatedQuietly) {
ApiModelCache::invalidateModelCache(new ProductVariantOption(), $this->company_uuid);
}

return $this;
}

Expand Down
12 changes: 12 additions & 0 deletions server/src/Models/Store.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ class Store extends StorefrontModel
*/
protected $table = 'stores';

/**
* Tables the cached store lists embed, so writes to them invalidate those lists
* (read by core's ApiModelCache from fleetbase/core-api#301 on): the rating average
* over `reviews`, and `networks` (membership, status and category in `network_stores`),
* which the console list loads and filters by. Logo and backdrop are left out: a new
* one changes `logo_uuid`/`backdrop_uuid` on the store row itself, and file rows are
* not edited, so depending on the busy `files` table would only cost cache hits.
*
* @var string[]
*/
protected $apiCacheDependencies = ['reviews', 'networks'];

/**
* These attributes that can be queried.
*
Expand Down
5 changes: 5 additions & 0 deletions server/src/Observers/CatalogObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,10 @@ public function deleted(Catalog $catalog): void
// Delete the category itself
$category->delete();
}

// The pivot rows were deleted without model events
if ($categories->isNotEmpty()) {
CatalogCategory::invalidateCatalogProductCache($catalog->company_uuid);
}
}
}
Loading