Skip to content

Shopify Commerce — Improvement & Growth Plan

Package: capell-app/shopify-commerce · Kind: package · Tier: premium · Product group: Capell Commerce · Bundle: commerce · Status: Draft

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 (SyncShopifyProductsAction as 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 in ShopifyCommerceServiceProvider::registerProtectedTables).
  • Dependencies: requires capell-app/admin, capell-app/core; supports capell-app/contacts, capell-app/search, and capell-app/diagnostics for 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.

  • Done/Shipped: Bulk sync now continues from Start → Poll → Import. SyncShopifyProductsAction dispatches ContinueShopifyProductBulkSyncAction after Shopify accepts the bulk operation. The continuation action polls the operation, imports completed JSONL output, and releases unfinished jobs for another poll using CAPELL_SHOPIFY_COMMERCE_BULK_SYNC_POLL_DELAY_SECONDS. Evidence: SyncShopifyProductsActionTest covers continuation dispatch, completed poll-to-import composition, and unfinished poll release. src/Actions/Catalog/SyncShopifyProductsAction.php, src/Actions/Catalog/ContinueShopifyProductBulkSyncAction.phpM.
  • FetchShopifyProductAction runs live GraphQL inside a DB::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 the updateOrCreate. S.
  • Done/Shipped: GraphQL throttle pacing honours Shopify retry signals. ExecuteShopifyAdminGraphqlAction now retries 429 responses with Retry-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.phpM.
  • ShopifyGraphqlException swallows partial-data GraphQL responses. throw_if(is_array($errors) && $errors !== [], ...) treats any errors array as fatal, but Shopify often returns errors alongside usable data (e.g. throttle warnings, deprecation notices). This can fail an otherwise-successful sync. Distinguish fatal errors from extensions/throttle notices. src/Actions/Graphql/ExecuteShopifyAdminGraphqlAction.phpS/M.
  • Done/Shipped: live search and bulk import share product persistence. PersistShopifyProductAction now owns the canonical local product write shape and variant pruning. ImportShopifyProductBulkSyncAction delegates to it, and SearchShopifyProductsAction maps live GraphQL nodes into ShopifyProductData so 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 GraphQL first variable. src/Actions/Catalog/PersistShopifyProductAction.php, src/Actions/Catalog/SearchShopifyProductsAction.php, src/Actions/Catalog/ImportShopifyProductBulkSyncAction.php, tests/Unit/Actions/SearchShopifyProductsActionTest.phpM.
  • Done/Shipped: last_sync_error is scrubbed before persistence. StartShopifyProductBulkSyncAction and ImportShopifyProductBulkSyncAction persist exception summaries through SanitizeShopifySyncErrorAction, 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: SyncShopifyProductsActionTest covers 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.phpS.
  • Done/Shipped: product sync command can intentionally target all/site-scoped connections. capell-shopify-commerce:sync now supports --all and --site= for active-connection iteration, while the no-option path preserves the single latest active connection behavior for compatibility. src/Console/Commands/SyncShopifyProductsCommand.phpS.
  • No bounded search-result cap from config; SearchShopifyProductsAction hard-codes first: 20. The page passes limit=20 but the live GraphQL query string is fixed at first: 20, so the $limit argument is partly cosmetic. Thread the limit through. src/Actions/Catalog/SearchShopifyProductsAction.phpS.
  • Done/Shipped: sync_status values are centralized in an enum. ShopifySyncStatus now 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.

  • Done/Shipped: Shopify webhook ingestion. POST /capell/webhooks/shopify validates X-Shopify-Hmac-Sha256, finds the connection by X-Shopify-Shop-Domain, and delegates to IngestShopifyWebhookAction for products/create, products/update, products/delete, customers/create, customers/update, and app/uninstalled. Product updates reuse PersistShopifyProductAction, customer updates reuse UpsertShopifyCustomerAction, product deletes clear cached rows, and app uninstall revokes the encrypted token through DisconnectShopifyStoreAction. — 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. SyncShopifyCustomersAction now pages Shopify Admin GraphQL customers(first:, after:), upserts each node through UpsertShopifyCustomerAction, dispatches the existing ShopifyCustomerSynced event from the upsert path, and is exposed through capell-shopify-commerce:sync-customers {connection?} plus manifest action/command metadata. Default OAuth scopes now include read_customers, and customer create/update webhooks now provide the real-time follow-up. Table-stakes.
  • Done/Shipped: automated catalog sync is scheduled. ShopifyCommerceServiceProvider registers capell-shopify-commerce:sync --all every fifteen minutes when the package is installed and scheduled_sync_enabled is true, using withoutOverlapping()/onOneServer() and the existing per-connection WithoutOverlapping Action middleware. Table-stakes.
  • Done/Shipped: app/uninstalled handling / token-revocation reconciliation. The Shopify webhook route now passes app/uninstalled to DisconnectShopifyStoreAction, 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 from priceV2. 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. No inventoryQuantity, locations, or inventory_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. ShopifyCommerceHealthCheck now 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. VerifyShopifyConnectionTokenAction runs a lightweight shop { name } Admin GraphQL probe, ShopifyCommerceHealthCheck reuses the same Action, and the connection page exposes a translated “Verify token” button with success/failure notifications. Differentiator.

  • Broken autonomous sync loop (functional bug). As in §2/§3: Start is never followed by Poll/Import anywhere in the codebase. OAuth And Catalog Sync > Troubleshooting even 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. FetchShopifyProductAction holds a transaction around ExecuteShopifyAdminGraphqlAction::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 PollShopifyProductBulkSyncAction standalone, no test for the full Start→Poll→Import chain. SyncShopifyProductsActionTest tests 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.php now asserts the package imports no frontend/authoring/public-action runtime namespaces, OAuth routes stay behind web + 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.
  • Done/Shipped: ExecuteShopifyAdminGraphqlAction throttle and retry coverage. Focused Action coverage documents 429 Retry-After retry 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. SyncShopifyCustomersActionTest fakes paginated Admin GraphQL customer responses, asserts local encrypted customer rows, verifies pagination variables, skips inactive connections, and keeps manifest metadata aligned.
  • Done/Shipped: connection-page resolved state is memoised. ShopifyConnectionPage caches 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_error is scrubbed at write time. Persisted start/import exception messages now pass through SanitizeShopifySyncErrorAction before reaching shopify_connections.last_sync_error, so the admin Blade remains a display concern rather than the secret boundary. Evidence: SyncShopifyProductsActionTest proves secret-bearing start/import messages are redacted before persistence.
  • Done/Shipped: OAuth state cleanup is scheduled. PruneExpiredShopifyOAuthStatesAction removes expired shopify_oauth_states, capell-shopify-commerce:prune-oauth-states exposes the maintenance entrypoint, and ShopifyCommerceServiceProvider schedules 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. ImportShopifyProductBulkSyncAction re-downloads bulk_operation_url and upserts; if run twice concurrently the cache lock (capell-shopify-commerce.sync.{id}, 300s block 10s) protects it, but a stale bulk_operation_url from a previous operation is not validated against the current bulk_operation_id before import. src/Actions/Catalog/ImportShopifyProductBulkSyncAction.php.
  • Done/Shipped: bundled Spanish locale. resources/lang/es/capell-shopify-commerce.php mirrors the package UI, command, error, and health keys, and ShopifyGraphqlException now resolves through a package translation key instead of a hard-coded English sentence.
  • http_timeout default of 15s for a bulk JSONL download. ImportShopifyProductBulkSyncAction uses the same http_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.

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.


ItemBucketEffortImpactSection 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.DoneMCritical§2, §3, §4
Done/Shipped: Add public-output-safety + full-chain composition coverage; add tests/ArchDoneMHigh§4
Done/Shipped: Ship a producer for shopify_customers using paginated Admin GraphQL customer syncDoneMHigh§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.DoneSHigh§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.DoneSMed§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.DoneSMed (security)§2, §4
Done/Shipped: Webhook ingestion (products/*, customers/*, app/uninstalled) with Shopify header HMAC validationDoneLHigh§3
Scheduled per-connection sync with backoff + WithoutOverlappingDoneMHigh§3
Introduce ShopifySyncStatus enum to replace string literalsDoneMMed§2
Memoise resolved connection in ShopifyConnectionPage; cut redundant queriesDoneSMed (perf budget)§4
Done/Shipped: Unify search/import persist path (prune + identical record shape)DoneMMed§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 actionDoneMMed§2
Connection “verify token” probe action + admin buttonDoneSMed§3
Multi-currency presentment + inventory levels + collections/tags in syncLaterLHigh (differentiator)§3
Public read API / view-model DTO for downstream consumersLaterMMed§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.LaterSMed (sales)§5
Done/Shipped: Declare supports deps for Contacts, Search, and Diagnostics; refresh remaining marketplace metadataDoneSMed (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.DoneSLow§4