Add HL3D branding, Etsy Live/Draft paging and Projects lifecycle
Deploy HickLab3D Dashboard / Deploy to production (push) Failing after 23s

Replace the old touchmark/PNG identity with the supplied lockup and
monogram, segmented role-aware navigation, Live/Draft listing filters
with generation-based cache invalidation, and admin Projects covering
immutable file revisions, listing live pointers, and exact-decimal
filament/packaging/shipping costing. Additive schema only; no Etsy
publication. Isolated clone tests: 301 tests, 1162 assertions.
This commit is contained in:
2026-11-08 12:50:56 +00:00
parent c8c5822166
commit d4928789c4
83 changed files with 5808 additions and 84 deletions
+36 -11
View File
@@ -5,6 +5,7 @@ namespace App\Services;
use App\Models\EtsyAppCredential;
use App\Models\EtsyConnection;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
@@ -188,24 +189,34 @@ class Etsy
/**
* @return array{count: int, limit: int, offset: int, state: string, results: list<array<string, mixed>>}
*/
public function listListings(EtsyConnection $connection, string $state = 'active', int $limit = 25, int $offset = 0): array
public function listListings(EtsyConnection $connection, string $state = 'active', int $limit = 25, int $offset = 0, bool $fresh = false): array
{
$state = strtolower($state);
if (! in_array($state, self::LISTING_STATES, true)) {
throw new RuntimeException('Listing state must be one of: '.implode(', ', self::LISTING_STATES).'.');
}
[$limit, $offset] = $this->page($limit, $offset);
$response = $this->request('GET', 'application/shops/'.$connection->shop_id.'/listings', $this->accessToken($connection), [
'state' => $state, 'limit' => $limit, 'offset' => $offset,
]);
$generation = Cache::get('etsy.listing-generation.'.$connection->shop_id, '0');
$cacheKey = 'etsy.listListings.'.$connection->shop_id.'.'.$generation.'.'.$state.'.'.$limit.'.'.$offset;
return [
'count' => (int) ($response['count'] ?? count($response['results'] ?? [])),
'limit' => $limit,
'offset' => $offset,
'state' => $state,
'results' => array_map(fn (array $listing): array => $this->summarizeListing($listing), $this->results($response)),
];
$result = $this->cache->remember($cacheKey, 600, function () use ($connection, $state, $limit, $offset): array {
$response = $this->request('GET', 'application/shops/'.$connection->shop_id.'/listings', $this->accessToken($connection), [
'state' => $state, 'limit' => $limit, 'offset' => $offset,
]);
return [
'count' => (int) ($response['count'] ?? count($response['results'] ?? [])),
'limit' => $limit,
'offset' => $offset,
'state' => $state,
'results' => array_map(fn (array $listing): array => $this->summarizeListing($listing), $this->results($response)),
];
}, $fresh);
$entry = Cache::get('integration:'.$cacheKey);
$result['fetched_at'] = is_array($entry) ? $entry['fetched_at'] : null;
$result['stale'] = $result['fetched_at'] === null || time() >= $result['fetched_at'] + 600;
return $result;
}
/**
@@ -270,6 +281,7 @@ class Etsy
$payload = $this->validatedListingPayload($payload, true);
$listing = $this->request('POST', 'application/shops/'.$connection->shop_id.'/listings', $this->accessToken($connection), body: $payload);
$this->cache->forget('etsy.listings.'.$connection->shop_id);
$this->forgetListingStateCaches($connection);
return $this->summarizeListing($listing, detailed: true);
}
@@ -289,6 +301,7 @@ class Etsy
$listing = $this->request('PATCH', 'application/shops/'.$connection->shop_id.'/listings/'.$listingId, $this->accessToken($connection), body: $payload);
$this->assertListingBelongsToShop($connection, $listing);
$this->cache->forget('etsy.listings.'.$connection->shop_id);
$this->forgetListingStateCaches($connection);
return $this->summarizeListing($listing, detailed: true);
}
@@ -306,10 +319,21 @@ class Etsy
$listing = $this->request('PATCH', 'application/shops/'.$connection->shop_id.'/listings/'.$listingId, $this->accessToken($connection), body: ['state' => 'active']);
$this->assertListingBelongsToShop($connection, $listing);
$this->cache->forget('etsy.listings.'.$connection->shop_id);
$this->forgetListingStateCaches($connection);
return $this->summarizeListing($listing, detailed: true);
}
/**
* Invalidate the first page of both Live and Draft state caches so a
* write immediately shown to the operator doesn't keep serving a stale
* cached page. Older pages expire on their own TTL.
*/
private function forgetListingStateCaches(EtsyConnection $connection): void
{
Cache::put('etsy.listing-generation.'.$connection->shop_id, bin2hex(random_bytes(16)), 172800);
}
/**
* @return array<string, mixed>
*/
@@ -359,6 +383,7 @@ class Etsy
}
$inventory = $this->request('PUT', 'application/listings/'.$listingId.'/inventory', $this->accessToken($connection), body: $body, jsonBody: true);
$this->cache->forget('etsy.listings.'.$connection->shop_id);
$this->forgetListingStateCaches($connection);
return $inventory;
}