# Shopify Commerce — Improvement & Growth Plan

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

## 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 (`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.

---

## 2. Improvements (existing functionality)

- **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.php` — **M**.
- **`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.php` — **M**.
- **`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.php` — **S/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.php` — **M**.
- **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.php` — **S**.
- **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.php` — **S**.
- **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.php` — **S**.
- **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**.

---

## 3. Missing Features (gaps)

- **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.**

---

## 4. Issues / Risks

- **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`.

---

## 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

| 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                                                                                                                                                                     |