# Overview.Admin

## What it does

Shopify Commerce connects Shopify Admin API stores to Capell sites, maintains local product and customer caches, and makes an explicitly selected connection's product data available to integrations. It is not a Shopify product editor, hosted storefront, cart, checkout, order-management, fulfilment, or payment integration.

## Setup requirements

Run `capell-shopify-commerce:install`, then complete the application's package and settings migration process. The installer publishes configuration and migrations and creates the `shopify-commerce.manage` permission; it does not prove that the migrations have subsequently run.

Create a Shopify app and set `SHOPIFY_APP_CLIENT_ID` and `SHOPIFY_APP_CLIENT_SECRET`. Register the Capell OAuth callback URL (`/capell/oauth/shopify/callback`) in that app. Keep the application encryption key available for stored Admin API tokens. Setting `CAPELL_SHOPIFY_COMMERCE_ENABLED=false` removes the admin integration surface.

Keep a queue worker and Laravel's scheduler running. A successful OAuth callback and **Sync now** both dispatch product-sync work. The scheduler starts a bulk product sync for every Active connection every 15 minutes when scheduled sync is enabled, and prunes abandoned/expired OAuth state rows hourly. Customer synchronisation is not part of that schedule.

If webhooks are required, register the public `/capell/webhooks/shopify` endpoint and the required topics in Shopify. The package does not create Shopify webhook subscriptions during OAuth. Its shipped handlers recognise `products/create`, `products/update`, `products/delete`, `customers/create`, `customers/update`, and `app/uninstalled`; other valid topics are recorded and completed without changing cached data.

## Connect a store

Go to **Integrations > Shopify Commerce**, select a site you are allowed to manage, enter the exact `myshopify.com` domain, and choose **Connect**. The OAuth state is single-use, bound to the initiating user, shop, and site, and expires after ten minutes. Complete approval in the same authenticated admin session.

The requested scopes come from **Shopify Commerce settings** and default to `read_products` and `read_customers`. Changing the scope list does not change an existing token; reconnect the store and approve the new scopes. Changing the API version affects subsequent API calls, so confirm Shopify supports that version before saving it.

The page manages the latest Active, Connecting, or Error connection for the selected site, while the database and scheduled command can retain more than one Active shop for a site. Keep one intended active connection per site unless an integration deliberately selects among several; an older Active connection can otherwise continue syncing even when it is not the one shown on the page.

The callback stores the Admin API token encrypted, records the granted scopes, marks the connection Active, and queues the first bulk product sync. Use **Verify token** to make a live shop query. A successful check confirms the token can currently query the shop; it does not validate every requested scope, webhook subscription, queue worker, or cached-data freshness.

## Sync and search the catalogue

**Sync now** queues work and returns immediately. The job starts a Shopify bulk operation, polls it at the configured interval, downloads its JSONL result, and imports products and variants in one database transaction. A successful full import removes cached products and variants that were not present in that result. Product webhooks update or delete individual cached products between full syncs.

The continuation job allows up to 120 polling attempts, with a 15-second delay by default. Shopify GraphQL requests retry HTTP 429 responses up to three attempts and honour `Retry-After` for at most 60 seconds; other HTTP or GraphQL failures are not silently retried at that layer. Failed starts/imports store a sanitised error and can move the connection to Error. If a sync remains Queued, Running, or Importing for more than 30 minutes, Diagnostics reports it as stale. The page disables another sync while work is busy, and an Error connection is no longer Active, so inspect failed jobs and the stored sync state and reconnect when **Sync now** cannot restart it.

**Search catalogue** checks the local cache first. When a non-empty search has no local result, it makes a live Shopify query and persists the returned products before displaying them. It therefore requires a healthy token and can change the local cache. Search results are cached for the settings value (five minutes by default) and invalidated by product writes.

Customer data follows a separate lifecycle. Run `capell-shopify-commerce:sync-customers {connection}` explicitly when a full customer refresh is required. It pages through the API up to the configured limits and upserts customers, but does not delete local customers missing from Shopify. Customer create/update webhooks also upsert records; customer-delete and Shopify privacy topics are not handled.

## Webhook delivery and recovery

Webhook ingestion is limited to 120 requests per minute per source IP. It validates the raw-body HMAC with the Shopify app secret, resolves the exact shop domain to an Active connection, requires Shopify's webhook ID for idempotency, and rejects a parseable `X-Shopify-Triggered-At` outside the configured five-minute window. If that timestamp header is absent or invalid, freshness cannot be checked and the HMAC plus webhook-ID guard remains the protection.

Accepted payloads are encrypted and queued for processing. Each event job has five attempts and records Pending, Processing, Completed, or Failed state plus an attempt count. A duplicate completed webhook is ignored; a later delivery of the same ID can requeue a Failed event. There is no webhook-event admin screen, manual replay action, or scheduled failed-event reprocessor, so monitor failed jobs/database state and use queue tooling or a deliberate integration to recover events that Shopify no longer retries.

## Disconnect and remove data

**Disconnect** marks the selected local connection Revoked, clears its stored Admin API token and bulk-operation references, and prevents its scheduled sync and webhook handlers from running. It does not call Shopify to uninstall the app or revoke the installation remotely. Remove the app or access in Shopify as well when the relationship must end.

Disconnecting does not delete cached products, variants, customers, webhook events, connection metadata, or search-cache entries. Reconnecting the same shop/site reuses the connection row and a later successful product sync refreshes the catalogue. Delete retained local data through an approved application process when it should not remain.

## Access, privacy, and retention

The admin page requires `shopify-commerce.manage`. Non-global administrators can only select assigned sites; global administrators can select any site and initially default to the lowest-ID site. Treat the permission as integration-administrator access because it can initiate OAuth, make live Shopify queries, queue syncs, and remove the local token.

The Admin API token, customer email/name/phone, and webhook payload are encrypted. Customer `raw_snapshot` remains ordinary JSON and can contain the same personal data; the email lookup hash is an unsalted SHA-256 value. Product snapshots, shop domain, granted scopes, external IDs, marketing state, order count/spend, sync metadata, webhook topic/ID/status, and stored errors are also plaintext. Protect database, cache, queue, logs, backups, and the Shopify app secret accordingly.

Only expired OAuth states have scheduled deletion. Shopify Commerce does not contribute a Privacy Center exporter or eraser, does not implement Shopify's customer-data request/redaction or shop-redaction topics, and does not automatically prune customer or webhook-event records. Include the local cache in subject-access, correction, erasure, app-uninstall, and retention procedures; do not assume deleting or anonymising a customer in Shopify removes the Capell copy.