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