Shopify Commerce — Improvement & Growth Plan
Package: capell-app/shopify-commerce · Kind: package · Tier: premium · Product group: Capell Commerce · Bundle: commerce · Status: Draft
1. Snapshot
Section titled “1. Snapshot”Shopify Commerce adds a site-scoped Shopify Admin API integration to Capell: admins connect a *.myshopify.com store via OAuth, the encrypted Admin API token is stored per site, and a GraphQL bulk operation syncs products/variants into local cache tables for admin-side catalog lookup. It also ships a customer-cache table, Shopify webhook ingestion for catalog/customer/app uninstall changes, and a ShopifyCustomerSynced event consumed by the contacts package for CRM. It explicitly does not own storefront rendering, checkout, carts, orders, or merchandising UI.
- Surfaces (
capell.json):admin,console(manifest); plus authenticated OAuth web routes (routes/oauth.php), queue (SyncShopifyProductsActionas job), and database. - Key Actions (
src/Actions): OAuth —BuildShopifyAuthorizeUrlAction,CreateShopifyOAuthStateAction,ValidateShopifyShopDomainAction,ValidateShopifyHmacAction,ValidateShopifyOAuthStateAction,PruneExpiredShopifyOAuthStatesAction,ExchangeShopifyAuthorizationCodeAction,ConnectShopifyStoreAction,DisconnectShopifyStoreAction. Catalog —SyncShopifyProductsAction,StartShopifyProductBulkSyncAction,PollShopifyProductBulkSyncAction,ImportShopifyProductBulkSyncAction,FetchShopifyProductAction,SearchShopifyProductsAction,InvalidateShopifyProductSearchCacheAction. GraphQL —ExecuteShopifyAdminGraphqlAction. Customers —UpsertShopifyCustomerAction. Install —InstallShopifyCommercePackageAction,InstallShopifyCommercePermissionsAction. - Models/tables:
shopify_connections,shopify_oauth_states,shopify_products,shopify_product_variants,shopify_customers(all registered as protected tables inShopifyCommerceServiceProvider::registerProtectedTables). - Dependencies: requires
capell-app/admin,capell-app/core; supportscapell-app/contacts,capell-app/search, andcapell-app/diagnosticsfor CRM customer-sync consumption, catalog/search adjacency, and health visibility. Third-party:laravel/framework,lorisleiva/laravel-actions,spatie/laravel-data,spatie/laravel-package-tools,spatie/laravel-settings. - Marketplace summary (verbatim): “Site-scoped Shopify Admin API OAuth, catalog sync, and customer cache foundations for Capell.”
- Screenshots: 3 marketplace entries: the extension card plus the connected-store page in light/dark mode. Catalog sync and cached product search captures were demoted because they reused the connected-store screen under distinct captions; recapture those states before broader promotion.
2. Improvements (existing functionality)
Section titled “2. Improvements (existing functionality)”- Done/Shipped: Bulk sync now continues from Start → Poll → Import.
SyncShopifyProductsActiondispatchesContinueShopifyProductBulkSyncActionafter Shopify accepts the bulk operation. The continuation action polls the operation, imports completed JSONL output, and releases unfinished jobs for another poll usingCAPELL_SHOPIFY_COMMERCE_BULK_SYNC_POLL_DELAY_SECONDS. Evidence:SyncShopifyProductsActionTestcovers continuation dispatch, completed poll-to-import composition, and unfinished poll release.src/Actions/Catalog/SyncShopifyProductsAction.php,src/Actions/Catalog/ContinueShopifyProductBulkSyncAction.php— M. FetchShopifyProductActionruns live GraphQL inside aDB::transaction. The HTTP call to Shopify happens within the DB transaction wrapper (src/Actions/Catalog/FetchShopifyProductAction.php), holding a connection/row lock across a network round-trip. Move the GraphQL fetch outside the transaction and only wrap theupdateOrCreate. S.- Done/Shipped: GraphQL throttle pacing honours Shopify retry signals.
ExecuteShopifyAdminGraphqlActionnow retries 429 responses withRetry-After, caches a short per-connection next-safe-request timestamp from Shopify cost/throttle metadata, and paces subsequent calls before sending the next request. Evidence covers retry-after throttling and carry-forward throttle cache state.src/Actions/Graphql/ExecuteShopifyAdminGraphqlAction.php— M. ShopifyGraphqlExceptionswallows partial-data GraphQL responses.throw_if(is_array($errors) && $errors !== [], ...)treats anyerrorsarray as fatal, but Shopify often returnserrorsalongside usabledata(e.g. throttle warnings, deprecation notices). This can fail an otherwise-successful sync. Distinguish fatalerrorsfromextensions/throttle notices.src/Actions/Graphql/ExecuteShopifyAdminGraphqlAction.php— S/M.- Done/Shipped: live search and bulk import share product persistence.
PersistShopifyProductActionnow owns the canonical local product write shape and variant pruning.ImportShopifyProductBulkSyncActiondelegates to it, andSearchShopifyProductsActionmaps live GraphQL nodes intoShopifyProductDataso first-seen search rows store options, raw snapshots, variants, featured images, synced timestamps, and stale-variant pruning just like bulk imports. Search also threads the requested limit into the GraphQLfirstvariable.src/Actions/Catalog/PersistShopifyProductAction.php,src/Actions/Catalog/SearchShopifyProductsAction.php,src/Actions/Catalog/ImportShopifyProductBulkSyncAction.php,tests/Unit/Actions/SearchShopifyProductsActionTest.php— M. - Done/Shipped:
last_sync_erroris scrubbed before persistence.StartShopifyProductBulkSyncActionandImportShopifyProductBulkSyncActionpersist exception summaries throughSanitizeShopifySyncErrorAction, which removes raw URLs, Shopify shop domains, Shopify token prefixes, authorization headers, Shopify access-token headers, and query token values while preserving the useful failure text. Evidence:SyncShopifyProductsActionTestcovers secret-bearing start/import exceptions and an ordinary import error remaining readable.src/Actions/Catalog/SanitizeShopifySyncErrorAction.php,src/Actions/Catalog/StartShopifyProductBulkSyncAction.php,src/Actions/Catalog/ImportShopifyProductBulkSyncAction.php— S. - Done/Shipped: product sync command can intentionally target all/site-scoped connections.
capell-shopify-commerce:syncnow supports--alland--site=for active-connection iteration, while the no-option path preserves the single latest active connection behavior for compatibility.src/Console/Commands/SyncShopifyProductsCommand.php— S. - No bounded search-result cap from config;
SearchShopifyProductsActionhard-codesfirst: 20. The page passeslimit=20but the live GraphQL query string is fixed atfirst: 20, so the$limitargument is partly cosmetic. Thread the limit through.src/Actions/Catalog/SearchShopifyProductsAction.php— S. - Done/Shipped:
sync_statusvalues are centralized in an enum.ShopifySyncStatusnow owns queued/running/importing/completed/idle/failed/canceled/revoked values plus busy/running helper lists, and catalog/OAuth/page/health write and guard paths use those enum values instead of scattered string literals. Multiple files — M.
3. Missing Features (gaps)
Section titled “3. Missing Features (gaps)”- Done/Shipped: Shopify webhook ingestion.
POST /capell/webhooks/shopifyvalidatesX-Shopify-Hmac-Sha256, finds the connection byX-Shopify-Shop-Domain, and delegates toIngestShopifyWebhookActionforproducts/create,products/update,products/delete,customers/create,customers/update, andapp/uninstalled. Product updates reusePersistShopifyProductAction, customer updates reuseUpsertShopifyCustomerAction, product deletes clear cached rows, and app uninstall revokes the encrypted token throughDisconnectShopifyStoreAction. —src/Actions/Webhooks/IngestShopifyWebhookAction.php,src/Actions/Webhooks/ValidateShopifyWebhookHmacAction.php,src/Http/Controllers/Webhooks/ShopifyWebhookController.php,routes/oauth.php,tests/Feature/Webhooks/ShopifyWebhookControllerTest.php - Done/Shipped: Customer-cache producer is reachable.
SyncShopifyCustomersActionnow pages Shopify Admin GraphQLcustomers(first:, after:), upserts each node throughUpsertShopifyCustomerAction, dispatches the existingShopifyCustomerSyncedevent from the upsert path, and is exposed throughcapell-shopify-commerce:sync-customers {connection?}plus manifest action/command metadata. Default OAuth scopes now includeread_customers, and customer create/update webhooks now provide the real-time follow-up. Table-stakes. - Done/Shipped: automated catalog sync is scheduled.
ShopifyCommerceServiceProviderregisterscapell-shopify-commerce:sync --allevery fifteen minutes when the package is installed andscheduled_sync_enabledis true, usingwithoutOverlapping()/onOneServer()and the existing per-connectionWithoutOverlappingAction middleware. Table-stakes. - Done/Shipped:
app/uninstalledhandling / token-revocation reconciliation. The Shopify webhook route now passesapp/uninstalledtoDisconnectShopifyStoreAction, which revokes the connection, clears the encrypted token, and marks sync status revoked. Table-stakes. - Multi-currency presentment (differentiator). Variants store a single
price_amount+price_currency(shopify_product_variants), taken frompriceV2.config.default_currency='USD'is a blunt fallback. Shopify supports presentment currencies / price ranges; storing only one shop currency limits any storefront consumer. Differentiator. - Inventory levels (differentiator). Only
available_for_sale(bool) is captured. NoinventoryQuantity, locations, orinventory_policy. E-commerce buyers expect stock counts for merchandising. Differentiator. - Collections / product types / tags (differentiator). Sync covers products + variants + options + featured image only. No collections, tags, vendor, or product type — all standard merchandising primitives. Differentiator.
- A read API / view-model for consumers (table-stakes given README claims). README says frontends “should consume synced catalog/customer records through explicit Actions or package-owned view models”, but the package exposes no public read Action or DTO for downstream packages — only the admin page uses
SearchShopifyProductsAction. The promised integration surface doesn’t exist yet. Table-stakes. - Done/Shipped: Diagnostics health probes are real.
ShopifyCommerceHealthChecknow checks package storage tables, Shopify app credentials, active connection tokens, lightweight Admin API token validity, stale sync operations, and catalog freshness against a configurable age threshold. Health output uses translations and avoids leaking shop domains or token values. Table-stakes. - Done/Shipped: admins can verify stored Shopify tokens without syncing.
VerifyShopifyConnectionTokenActionruns a lightweightshop { name }Admin GraphQL probe,ShopifyCommerceHealthCheckreuses the same Action, and the connection page exposes a translated “Verify token” button with success/failure notifications. Differentiator.
4. Issues / Risks
Section titled “4. Issues / Risks”- Broken autonomous sync loop (functional bug). As in §2/§3:
Startis never followed byPoll/Importanywhere in the codebase.OAuth And Catalog Sync > Troubleshootingeven documents “Sync stays running → Run or schedule the poll/import workflow” as expected operator behaviour — i.e. the package ships without the loop closed.src/Actions/Catalog/SyncShopifyProductsAction.php,PollShopifyProductBulkSyncAction.php,ImportShopifyProductBulkSyncAction.php. - Network call inside DB transaction.
FetchShopifyProductActionholds a transaction aroundExecuteShopifyAdminGraphqlAction::run(...). Under load this ties a DB connection to Shopify latency and risks lock timeouts.src/Actions/Catalog/FetchShopifyProductAction.php. - Thin/missing test coverage in high-risk areas:
- No test for
PollShopifyProductBulkSyncActionstandalone, no test for the full Start→Poll→Import chain.SyncShopifyProductsActionTesttests Start, Poll, and Import as separate fakes; nothing proves they compose, which is exactly where the loop is broken.tests/Unit/Actions/SyncShopifyProductsActionTest.php. - Done/Shipped: Public-output and Arch safety coverage.
tests/Arch/ShopifyCommerceBoundaryTest.phpnow asserts the package imports no frontend/authoring/public-action runtime namespaces, OAuth routes stay behindweb+auth, the manifest declares no frontend provider while marking cache safety sensitive/non-cacheable, and any future public asset directories stay free of Shopify tokens, OAuth state, raw snapshots, GraphQL/admin API fragments, and frontend authoring markers.
- No test for
- Done/Shipped:
ExecuteShopifyAdminGraphqlActionthrottle and retry coverage. Focused Action coverage documents 429Retry-Afterretry behavior and carry-forward throttle cache state.- No test for
DisconnectShopifyStoreAction(token nulling / status transition) despite it being a security-relevant path.src/Actions/OAuth/DisconnectShopifyStoreAction.php. - Done/Shipped: Customer producer coverage.
SyncShopifyCustomersActionTestfakes paginated Admin GraphQL customer responses, asserts local encrypted customer rows, verifies pagination variables, skips inactive connections, and keeps manifest metadata aligned.
- No test for
- Done/Shipped: connection-page resolved state is memoised.
ShopifyConnectionPagecaches the manageable connection and site options during a component request, resets that cache around actions and site changes, and clears cached product previews before reloading the selected site’s products. This cuts repeated render-time connection/site lookups without changing the authorization path.src/Filament/Pages/ShopifyConnectionPage.php. - Done/Shipped: Secret leakage into
last_sync_erroris scrubbed at write time. Persisted start/import exception messages now pass throughSanitizeShopifySyncErrorActionbefore reachingshopify_connections.last_sync_error, so the admin Blade remains a display concern rather than the secret boundary. Evidence:SyncShopifyProductsActionTestproves secret-bearing start/import messages are redacted before persistence. - Done/Shipped: OAuth state cleanup is scheduled.
PruneExpiredShopifyOAuthStatesActionremoves expiredshopify_oauth_states,capell-shopify-commerce:prune-oauth-statesexposes the maintenance entrypoint, andShopifyCommerceServiceProviderschedules it hourly while the package is installed. Evidence: focused Action coverage leaves future/null-expiry states intact, manifest metadata declares the action/command, and the OAuth sync docs describe the scheduled maintenance path.src/Actions/OAuth/PruneExpiredShopifyOAuthStatesAction.php,src/Console/Commands/PruneExpiredShopifyOAuthStatesCommand.php,src/Providers/ShopifyCommerceServiceProvider.php. - No idempotency key on bulk import.
ImportShopifyProductBulkSyncActionre-downloadsbulk_operation_urland upserts; if run twice concurrently the cache lock (capell-shopify-commerce.sync.{id}, 300s block 10s) protects it, but a stalebulk_operation_urlfrom a previous operation is not validated against the currentbulk_operation_idbefore import.src/Actions/Catalog/ImportShopifyProductBulkSyncAction.php. - Done/Shipped: bundled Spanish locale.
resources/lang/es/capell-shopify-commerce.phpmirrors the package UI, command, error, and health keys, andShopifyGraphqlExceptionnow resolves through a package translation key instead of a hard-coded English sentence. http_timeoutdefault of 15s for a bulk JSONL download.ImportShopifyProductBulkSyncActionuses the samehttp_timeout(15s) for downloading a potentially large bulk JSONL file as for a single GraphQL call. Large catalogs will time out. Use a separate, larger streaming timeout for the JSONL sink.config/capell-shopify-commerce.php,src/Actions/Catalog/ImportShopifyProductBulkSyncAction.php.
5. Marketplace & Selling
Section titled “5. Marketplace & Selling”Critique of current copy. The capell.json marketplace.summary (“Site-scoped Shopify Admin API OAuth, catalog sync, and customer cache foundations for Capell.”) and the composer description (“Shopify Admin API connection and catalog sync foundation for Capell CMS.”) are accurate but read like internal architecture notes: “foundations”, “site-scoped”, “Admin API OAuth” describe plumbing, not buyer value. They lead with implementation and the word “foundations” signals “incomplete” — a poor look for a premium/paid listing. Neither says what an admin can do or why it beats hand-rolling a Shopify integration.
Improved 1-sentence summary:
Connect any Shopify store to Capell in minutes and keep its product catalog and customer data in sync — securely, per site, with no storefront lock-in.
Improved 3–4 sentence listing description:
Shopify Commerce links your Shopify store to Capell with a guided, per-site OAuth connection — your Admin API token is encrypted at rest and never touches content code. It syncs your product catalog (products, variants, options, images, pricing) into fast local tables you can search and reference directly from the admin, and caches customer records for CRM and contact workflows. Designed to coexist with your existing storefront and checkout, it owns the integration layer so your team doesn’t have to maintain Shopify API glue. Multi-store ready, permission-gated, and observable through Capell Diagnostics.
(Customer-cache and “sync” claims are now backed by the customer producer plus webhook ingestion for real-time updates.)
Screenshot/media gaps. The listing now includes real Capell runner PNG captures for the connected-store and cached-catalog workflows, with safe fake Shopify connection/product data. Remaining media depth: (1) the pre-connect shop-domain entry + site picker state, (2) the Settings panel (API version, scopes, search TTL), (3) the Diagnostics health-check row, and (4) a short GIF of the OAuth connect → sync → search flow.
Pricing/tier/bundle positioning. premium + commerce bundle is defensible now that the sync continuation, customer producer, health diagnostics, and runner-backed screenshots are shipped. The commerce bundle and Capell Commerce group are right. Cross-sell is now wired through capell.json supports for capell-app/contacts, capell-app/search, and capell-app/diagnostics, matching the customer-sync event, catalog/search adjacency, and Diagnostics health probes. A natural upsell: a future shopify-storefront/merchandising package that consumes this catalog.
Top differentiators / value props. (1) Encrypted, per-site token storage with no credentials in content code; (2) multi-store/site scoping with permission gating (manage_shopify_commerce) and global-vs-assigned-site logic baked in (ShopifySiteContext); (3) Actions-first design that lets other Capell packages consume catalog/customer data cleanly; (4) “no storefront lock-in” — coexists with any checkout.
Target buyer persona. Agencies and in-house teams running multi-brand/multi-store Capell sites backed by Shopify who want catalog and customer data available inside the CMS admin (for content, search, CRM) without rebuilding the storefront on Shopify or maintaining bespoke API integrations.
Marketplace search keywords/tags (8–12): shopify, shopify admin api, ecommerce, product catalog sync, oauth, multi-store, commerce integration, customer sync, crm, graphql, bulk operations, inventory.
6. Prioritized Roadmap
Section titled “6. Prioritized Roadmap”| Item | Bucket | Effort | Impact | Section ref |
|---|---|---|---|---|
| Done/Shipped: Close the Start→Poll→Import sync loop (queued chain or scheduler). Evidence: sync start dispatches a continuation job; continuation polls, imports completed operations, and releases unfinished operations for another poll. | Done | M | Critical | §2, §3, §4 |
Done/Shipped: Add public-output-safety + full-chain composition coverage; add tests/Arch | Done | M | High | §4 |
Done/Shipped: Ship a producer for shopify_customers using paginated Admin GraphQL customer sync | Done | M | High | §3 |
Done/Shipped: Implement the Diagnostics health check probe (token, last-sync age, stuck ops). Evidence: ShopifyCommerceHealthCheck now returns storage, app credential, connection credential, token validity, sync-state, and catalog-freshness diagnostics with focused secret-safety coverage. | Done | S | High | §3, §4 |
Done/Shipped: Move GraphQL fetch out of DB::transaction in FetchShopifyProductAction. Evidence: focused action tests verify the Shopify GraphQL HTTP fake observes only the test harness baseline transaction level, not the action persistence transaction, and the fetched product still persists locally. | Done | S | Med | §2, §4 |
Done/Shipped: Scrub secrets from last_sync_error before persisting. Evidence: start/import exception persistence tests redact URLs, shop domains, token values, and auth headers while ordinary errors remain readable. | Done | S | Med (security) | §2, §4 |
Done/Shipped: Webhook ingestion (products/*, customers/*, app/uninstalled) with Shopify header HMAC validation | Done | L | High | §3 |
Scheduled per-connection sync with backoff + WithoutOverlapping | Done | M | High | §3 |
Introduce ShopifySyncStatus enum to replace string literals | Done | M | Med | §2 |
Memoise resolved connection in ShopifyConnectionPage; cut redundant queries | Done | S | Med (perf budget) | §4 |
| Done/Shipped: Unify search/import persist path (prune + identical record shape) | Done | M | Med | §2 — PersistShopifyProductAction is shared by live search and bulk import, including options, raw snapshots, variants, synced timestamps, and stale-variant pruning. |
Done/Shipped: Retry-After/429 handling + carry-forward throttle state in GraphQL action | Done | M | Med | §2 |
| Connection “verify token” probe action + admin button | Done | S | Med | §3 |
| Multi-currency presentment + inventory levels + collections/tags in sync | Later | L | High (differentiator) | §3 |
| Public read API / view-model DTO for downstream consumers | Later | M | Med | §3 |
| Recapture distinct catalog sync and cached product search screenshots before promotion. Evidence: duplicate connected-store captures were demoted from marketplace media. Deferred until styled runner recapture; no implementation blocker. | Later | S | Med (sales) | §5 |
Done/Shipped: Declare supports deps for Contacts, Search, and Diagnostics; refresh remaining marketplace metadata | Done | S | Med (sales) | §5 |
Done/Shipped: Expired shopify_oauth_states cleanup command/scheduler and bundled es locale. Evidence: PruneExpiredShopifyOAuthStatesAction, capell-shopify-commerce:prune-oauth-states, hourly schedule registration, manifest action/command metadata, docs updates, translated GraphQL exception, and Spanish language file. | Done | S | Low | §4 |