Files
product_inventory/docs/ARCHITECTURE.md
Ben Miller d67897aa17 Fix Media Manager critical syntax errors and enforce ES5 architecture
- Resolved persistent 'SyntaxError: Unexpected token class' by refactoring 'MediaState' and 'UI' classes in MediaManager.html to standard ES5 function constructors.

- Resolved 'SyntaxError: Unexpected identifier src' by rewriting 'createCard' to use 'document.createElement' instead of template strings for dynamic media elements.

- Consolidated script tags in MediaManager.html to prevent Apps Script parser merge issues.

- Updated docs/ARCHITECTURE.md and MEMORY.md to formally document client-side constraints (No ES6 classes, strict DOM manipulation for media).

- Note: Google Drive video animate-on-hover functionality is implemented but currently pending verification/fix.
2025-12-28 20:35:29 -07:00

8.3 KiB

Architecture Documentation

System Overview

This project serves as a bridge between Google Sheets and Shopify. It enables a two-way sync (primarily Sheets to Shopify for products) and allows managing inventory directly from a spreadsheet.

Core Flows

  1. Product Updates:

    • User edits a cell in the "product_inventory" sheet.
    • onEditQueue trigger fires, capturing the SKU and timestamp.
    • Edits are batched in PropertiesService (script properties).
    • A time-based trigger runs processBatchedEdits every minute.
    • The processing function locks the script, reads the queue, and pushes changes to Shopify via the Admin API.
  2. Order Sync:

    • Users can run menu commands to fetch orders from Shopify.
    • The Shop class fetches orders via the REST API, handling pagination.
    • Data is populated into specific sheets (_orders, _line_items, _customer, etc.).

Key Components

1. Queue System (src/onEditQueue.ts)

To avoid hitting Shopify API rate limits and Google Apps Script execution time limits, edits are not processed immediately.

  • onEditQueue(e):

    • Triggered on every cell edit.
    • Checks if the edit is valid (correct sheet, valid SKU).
    • Acquires a DocumentLock.
    • Updates a JSON list in ScriptProperties (pendingEdits).
    • Debounces edits (updates timestamp if SKU is already pending).
  • processBatchedEdits():

    • Run via time-based trigger (every 1 minute).
    • Acquires a ScriptLock.
    • Reads pendingEdits.
    • Filters for edits older than BATCH_INTERVAL_MS (30s) to allow for multiple quick edits to the same SKU.
    • Iterates through valid edits and calls Product.UpdateShopifyProduct.
    • SKU Validation: Before any action, checks if the SKU is valid (not empty, ?, or n). Aborts if invalid.

2. Product Lifecycle Logic

  • Creation:

    • Uses the SKU as the Shopify Handle (URL slug).
    • Prevents creation if the SKU is a placeholder.
  • Updates:

    • Prioritizes ID-based lookup.
    • If the shopify_id column is populated, the system trusts this ID to locate the product in Shopify, even if the SKU has changed in the sheet.
    • As a result, changing a SKU in the sheet and syncing will rename the existing product (handle/SKU) rather than creating a duplicate.

2. Shopify Integration (src/shopifyApi.ts)

The project uses a hybrid approach for the Shopify Admin API:

  • REST API: Used primarily for fetching Orders (legacy support).

  • GraphQL API: Used for fetching and updating Products and Inventory.

  • GraphQL API: Used for fetching and updating Products and Inventory.

The Shop class implements the IShop interface, handling authentication using credentials stored in the "vars" sheet. The interface decoupling facilitates robust unit testing via MockShop.

3. Configuration (src/config.ts)

Configuration, including API keys, is stored in a dedicated Google Sheet named "vars". The Config class reads these values at runtime using a vlookup style helper.

Required "vars" columns:

  • key: The name of the configuration variable.
  • value: The actual value.

4. Global Entry Points (src/global.ts)

Since Apps Script functions must be top-level to be triggered or attached to buttons, src/global.ts explicitly exposes necessary functions from the modules to the global scope.

5. Status Automation (src/statusHandlers.ts)

A modular system handles changes to the status column. It uses a registry of StatusHandler implementations:

  • Published: Sets Shopify Status ACTIVE, Quantity 1.
  • Sold/Artist Swap: Sets Shopify Status ACTIVE, Quantity 0.
  • Drafted: Sets Shopify Status DRAFT.

Triggers

Triggers are managed programmatically via src/triggers.ts. Running reinstallTriggers will wipe existing project triggers and set up the standard set:

  • onEdit -> onEditHandler (Main Router)
  • TimeBased (1 min) -> processBatchedEdits
  • TimeBased (10 min) -> checkRecentSales

5. Troubleshooting Panel (src/sidebar.ts, src/Sidebar.html)

A dedicated side panel provides visibility into the background queue system.

  • Backend (src/sidebar.ts):

    • getQueueStatus(): Returns the current state of the queue and global toggle.
    • setQueueEnabled(): Toggles the global queueEnabled script property.
    • deleteEdit() / pushEdit(): Manages specific items in the queue with safety checks.
  • Frontend (src/Sidebar.html):

    • Displays pending edits with timestamps.
    • Provides controls to globally enable/disable processing.
    • Allows manual intervention (delete/push) for individual items.

6. Service Layer, Testing & Quality

To enable unit testing without Google Apps Script dependencies, the project uses a Service pattern with Dependency Injection.

Architecture

  • ISpreadsheetService: Interface for all sheet interactions.
  • GASSpreadsheetService: Production implementation wrapping SpreadsheetApp.
  • MockSpreadsheetService: In-memory implementation for tests.

Quality Assurance

We use Husky and lint-staged to enforce quality standards at the commit level:

  1. Pre-commit Hook: Automatically runs npm test -- --onlyChanged --coverage.
  2. Coverage Policy: Any file modified in a commit must meet an 80% line coverage threshold. This ensures the codebase quality improves monotonically ("Boy Scout Rule").

Classes (like Product) should accept an ISpreadsheetService in their constructor. This allows providing the Mock service during tests to verify logic without touching real Google Sheets.

7. Media Manager (src/mediaHandlers.ts, src/MediaSidebar.html)

We implemented a "Sidebar-First" architecture for product media to handle the complexity of Google Picker and Shopify Sync.

Frontend (MediaSidebar.html)

  • Glassmorphism UI: Uses modern CSS for a premium feel.
  • Polling: Since the sidebar can't listen to Sheet events directly efficiently, it polls getMediaState(sku) to detect when the user selects a different product row.
  • Google Picker API:
    • Uses the New Google Photos Picker (Session-based) for selecting photos.
    • Uses the Google Drive Picker (Legacy) for selecting existing Drive files.
    • Handles OAuth token passing securely from the server side (google.script.run).

Backend (mediaHandlers.ts)

  • Import Strategy:
    • Safe Zone: Files are first downloaded/copied to the Drive Root to ensure we have the asset.
    • Move: Then they are moved to the organized SKU folder (/Product Photos/[SKU]/).
    • Resilience: The file creation logic tries multiple methods (Standard DriveApp, Sanitized Blob, Advanced Drive API) to handle the notoriously fickle nature of UrlFetchApp blobs.
  • Shopify Sync:
    • MediaService manages the state.
    • Calculates checksums to avoid re-uploading duplicate images.
    • Uses Shopify's "Staged Uploads" -> "Create Media" mutation flow.

8. Apps Script & HTML Service Constraints

When working with HtmlService (client-side code), the environment differs significantly from the server-side V8 runtime.

  1. Server-Side (.ts/.gs):

    • Runtime: V8 Engine.
    • Syntax: Modern ES6+ (Classes, Arrow Functions, const/let) is fully supported.
    • Recommendation: Use standard TypeScript patterns.
  2. Client-Side (.html served via createHtmlOutputFromFile):

    • Runtime: Legacy Browser Environment / Strict Caja Sanitization.
    • Constraint: The parser often chokes on ES6 class syntax and complex template strings inside HTML attributes.
    • Rule 1: NO ES6 CLASSES. Use ES5 function constructors and prototype methods.
    • Rule 2: NO Complex Template Strings in Attributes. Do not use src="${var}" if the variable contains a URL. Use document.createElement and set properties (e.g., element.src = value) programmatically.
    • Rule 3: Unified Script Tags. Consolidate scripts into a single block where possible to avoid parser merge errors.
    • Rule 4: Var over Let/Const. Top-level variables should use var or explicit window assignment to ensure they are accessible to inline HTML handlers (e.g., onclick="handler()").