Author: vheyn@technocore.co.za
Updated: 2026-04-16
Shopify GraphQL Admin API integration for FullHouse retail. Manages product
catalogue, promotions, pricing, collections, and variant management on the
FullHouse Shopify store.
X-Shopify-Access-Token header/admin/api/2026-01/graphql.jsonpython ObjServiceFHShopify.py <command> [options]
| Command | Description |
|---|---|
batch-push [set] |
Push products: promo, active, old, comma-separated SKUs, or GROUP-xxxxx |
sync-audit |
Audit Shopify vs trader data (title, price, status, tags, SKUs) |
sync-audit --fix |
Auto-fix mismatched products |
singleton --sku SKU |
Push specific SKUs (comma-separated) |
promo-email |
Send branded promotion status email with AI review |
calibrate |
Measure push speed (N products, timing stats) |
promotions |
Show past, current, and future promotions |
list-products |
Retrieve and display all Shopify products |
recon |
Reconcile trader promotions vs Shopify prices |
product --sku SKU |
Look up a product on Shopify |
status-email |
Generate and send status email |
delete --sku SKU |
Delete products by SKU |
sync |
Sync all products (active, promo, deleted sets) |
update |
Update a specific product set |
locations |
Update inventory locations |
cache-config |
Configure inventory cache settings |
batch-push promoclean_previous_promo_tags() removes special_build_product_data_bulk() fetches trader datais_product_cache_valid() checks price, promotion,create_product() for each SKU:
_colour_to_sku_map per product (prevents group state leaks)_update_existing_variants()productCreateMedia, links to variantslog_event("shopify", "product_push", ...) for each| Level | Type | Options | Example |
|---|---|---|---|
| 0 | Single variant | None | Simple product |
| 1 | Bedding/Size | Size + Length | Bed sets, rugs, vinyl |
| 2 | Warranty | Warranty (No extension / 2 years @ Rxxx) | Appliances |
| 3 | Colour | Colour | Furniture (GROUP push) |
When a product exists and variant count matches, _update_existing_variants()
runs via productVariantsBulkUpdate to fix SKUs, prices, and compare-at
prices. Matches variants by option values first, falls back to positional
matching when option text has changed (e.g. warranty price update).
Each variant in a GROUP-* push carries the underlying trader SKU on a
private _alt_sku field. _variant_data_for(real_sku, base_*) looks up
the variant's own promo, warranty, stock, lead time and price (cached per
push) and overlays them onto the base product data. The metafield builders
(_build_variant_metafields) are then invoked with the real SKU so credit
amortisation, deposit, instalment, stock and warranty all reflect that
variant rather than the parent.
Without this, every variant would inherit the parent's snapshot — a King
size bed would show the Double's R7999 / R644 instalment instead of its
own R11499 / R890.
variant_images dict built per product:
local.documents/pims/<alt_sku>_*pims.jpg for23494_1_pims.jpg).is_update), compute _canonical_image_name(url) to strip Shopify'sproductCreateMedia with originalSource pointing atfhgo.co.za/image/pims) and alt set to either"Double|Standard Length")._dedupe_product_media(gid) groups uploaded_link_images_to_variants(gid) runs on everyTrader promotion_header.PH_STARTDATE / PH_ENDDATE are char(50) storing
dates as DD.MM.YYYY. All queries use STR_TO_DATE(col, '%d.%m.%Y') for
date comparison.
| Query | Purpose |
|---|---|
get_promotion_data_single |
Single SKU promo lookup with date filter |
get_promotion_data_bulk |
Bulk SKU promo lookup with date filter |
get_product_set_promo |
Current promo SKUs (up to 7 days past end) |
get_previous_promotion |
Last expired promotion headers |
get_previous_promotion_skus |
SKUs from last expired promotion |
special tag + promotion name tagclean_previous_promo_tags() removes these from expired promo productspromo-email)Branded HTML email with FullHouse styling via ObjTemplate.build_email_html().
Sections:
AiConstants.DEFAULT_OLLAMA_MODEL)Recipients from config.yaml shopify.notify_emails or --to flag.
sync-audit)Compares all Shopify products against trader data. Checks:
| Check | Description |
|---|---|
| TITLE | Shopify title longer than expected (description leak) |
| PRICE | Price mismatch vs trader/promo price |
| STATUS | Should be ACTIVE but ARCHIVED, or vice versa |
| MISSING_TAG | On promotion but no special tag |
| STALE_TAG | Has special tag but not on promotion |
| NULL_SKU | Variant with null SKU |
| NO_IMAGE | No media attached |
| NOT_IN_TRADER | SKU not found in trader database |
--fix clears inventory/variant cache for affected SKUs and re-pushes.
config.yaml)fullhouse:
shopify:
image_base_url: https://fhgo.co.za/image/pims
vat_rate: 15
notify_emails: vheyn@technocore.co.za,owen@technocore.co.za
| Mutation | Usage |
|---|---|
productCreate |
New product with options |
productUpdate |
Update title, description, tags, status |
productVariantsBulkCreate |
Create variants (strategy: REMOVE_STANDALONE_VARIANT) |
productVariantsBulkUpdate |
Update variant SKU, price, mediaId, metafields |
productCreateMedia |
Upload images |
productDeleteMedia |
Remove duplicate media (_dedupe_product_media) |
metafieldDefinitionCreate |
Register my_fields definitions with PUBLIC_READ |
productDelete |
Delete product |
tagsRemove |
Remove specific tags (promo cleanup) |
Product- and variant-level metafields are pushed under the my_fields
namespace with PUBLIC_READ storefront access (matches the legacy
classic site so existing storefront templates keep working without rewrites).
meta_* keys, 18 total)Credit (price, instalment, deposit, months), promotion (name, price,
discount, on_promotion flag), guarantee/warranty (period, extended
warranty + cost), stock (level, lead_time, can_order, can_order_message),
TV-licence flag.
m_* keys + variant_level_description)Product metafields rewritten with the m_ prefix so storefront templates
that drill into specific variants (e.g. extended-warranty selector) keep
working. variant_level_description is a multi_line_text_field carrying
the per-variant body HTML — header lists the variant's option values
("Double · Standard Length"), then the shared product copy follows.
_compute_credit_data(sku) defers to
ObjTextFHInstalment.ObjProcessText.Compute() — the same module the legacy
fullhouse_product_api report uses — so credit values match what's printed
on customer-facing artifacts. Replaces the snapshot
meta_product.Web_Instalment / Deposit columns which can drift after rate
or fee changes. Returns {credit_installment, credit_deposit, credit_months, credit_price}.
| Table | Purpose |
|---|---|
data_shopify_inventory |
Product cache (title, status, price, promo, sync time) |
data_shopify_variants |
Variant cache (SKU, product ID, GraphQL ID) |
data_shopify_variants_all |
Full variant data from GraphQL response |
data_shopify_images |
Image hash cache for dedup |
data_shopify_colourmap |
68 colour code to display name mappings |
data_shopify_meta |
Product metafield snapshot |
data_shopify_confirmed |
Manually-verified product list (ProductCode, ConfirmedBy, ConfirmedAt, Notes) |
Push a variant group: singleton --sku GROUP-21924 or
batch-push GROUP-21924. The singleton CLI auto-detects the
GROUP- prefix, calls get_group_skus, and passes the group set to
push_products_batch which collapses N alternates into 1 product with N
variants (rather than N separate products).
get_group_with_descriptions"Product - Colour" (1 option) or "Product - Size - Length"_variant_prices map_alt_sku so per-variant metafields fetch its"Size|Length" or colour name; linkedRaw trader categories mapped to Shopify collection names via
collection_name_map in YAML:
BEDROOM SUITE → Bedroom
BASE SETS → Bedroom
WHITES → Appliances
KITCHEN APPLIANCES → Appliances
Smart collections created from tags (category, subcategory, promotion name).
Custom collection membership via collectionsToJoin on ProductInput.
| Trader State | Shopify Status |
|---|---|
allowdisplay=0 |
ARCHIVED |
meta_can_order_message=Archived |
ARCHIVED |
| All other | ACTIVE |
Archived products are skipped during promo push if not already on Shopify,
or if already ARCHIVED on Shopify (no redundant update).
Every push_products_batch call reports:
log_event per successful push and batch completioncalibrate command runs a timed push of N products for benchmarking.

Product display rules:
PM_IS_WEB=0 or status in (3,7,8,9,11) -- hiddenFind stale local records:
SELECT * FROM `core.axion`.data_shopify_variants
WHERE NOT Sku LIKE '%\_%' AND NOT Sku = ''
AND DATEDIFF(NOW(), LastUpdate) > 0
AND NOT Sku IN ('CURRENT', 'COMPLETE')
ORDER BY LastUpdate
PIMS images: https://fhgo.co.za/image/pims/{sku}_pims.jpg