diff --git a/GEMINI.md b/GEMINI.md index edcd322..fb87726 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -6,3 +6,4 @@ This file serves as a guide for future sessions working on this codebase. 2. **Update Memory**: If we make significant architectural decisions or change our working patterns, update `MEMORY.md` to reflect this. 3. **Check Documentation**: `README.md`, `docs/ARCHITECTURE.md`, and `docs/SETUP.md` are the sources of truth for the system. Keep them updated as code changes. 4. **Task Tracking**: Use the `task.md` artifact to track progress on multi-step tasks. +5. **Shopify API Reference**: When developing features involving Shopify, **ALWAYS** check the [Shopify API Reference](https://shopify.dev/docs/api/admin-graphql) for the specific version in use (check `shopifyApi.ts` for version). Do not guess field names or structure. diff --git a/README.md b/README.md index ceeda5b..ff8d06c 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ The system allows you to: - Monitor and troubleshoot background processes via a custom side panel. - **Automated Sales Sync**: Periodically check Shopify for recent sales and mark items as "sold" in the sheet. - **Manual Reconciliation**: Backfill sales data for a specific time range via menu command. +- **Status Workflow Automation**: Automatically update Shopify status and inventory based on the sheet's "status" column (e.g., "Sold" -> Active, 0 Qty). ## Prerequisites diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 3432a4c..0d4cbfa 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -59,13 +59,19 @@ Configuration, including API keys, is stored in a dedicated Google Sheet named " 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` -> `newSkuHandler` -- `onEdit` -> `matchProductToShopifyOnEditHandler` -- `onEdit` -> `onEditQueue` +- `onEdit` -> `onEditHandler` (Main Router) - `TimeBased (1 min)` -> `processBatchedEdits` +- `TimeBased (10 min)` -> `checkRecentSales` ### 5. Troubleshooting Panel (`src/sidebar.ts`, `src/Sidebar.html`) diff --git a/src/OnEditHandler.ts b/src/OnEditHandler.ts index 3d9e932..5bafbfc 100644 --- a/src/OnEditHandler.ts +++ b/src/OnEditHandler.ts @@ -4,10 +4,15 @@ import { getCellRangeByColumnName } from "./sheetUtils" import { matchProductToShopify, updateProductToShopify } from "./match" import { getColumnName, toastAndLog } from "./sheetUtils" +import { onEditQueue } from "./onEditQueue" +import { statusOnEditHandler } from "./statusHandlers" + export function onEditHandler(e: GoogleAppsScript.Events.SheetsOnEdit) { - //TODO: process each edited row + // Router pattern: execute all handlers newSkuHandler(e) matchProductToShopifyOnEditHandler(e) + onEditQueue(e) + statusOnEditHandler(e) } export function matchProductToShopifyOnEditHandler( diff --git a/src/Product.ts b/src/Product.ts index c8caf87..0a902b6 100644 --- a/src/Product.ts +++ b/src/Product.ts @@ -39,6 +39,7 @@ export class Product { product_depth_cm: number = 0 product_height_cm: number = 0 photos: string = "" + quantity: number | null = null shopify_product: shopify.Product shopify_default_variant_id: string = "" shopify_default_option_id: string = "" @@ -76,7 +77,7 @@ export class Product { console.log("skipping '" + headers[i] + "'") continue } - if (productValues[i] == "") { + if (productValues[i] === "") { console.log( "keeping '" + headers[i] + "' default: '" + this[headers[i]] + "'" ) @@ -211,7 +212,10 @@ export class Product { shopify.WeightUnit.GRAMS ) } - if (newProduct) { + if (this.quantity !== null) { + console.log("UpdateShopifyProduct: setting inventory item quantity to " + this.quantity) + shop.SetInventoryItemQuantity(item, this.quantity, config) + } else if (newProduct) { console.log("UpdateShopifyProduct: setting defaults on new product") console.log("UpdateShopifyProduct: adjusting inventory item quantity") shop.UpdateInventoryItemQuantity(item, 1, config) diff --git a/src/global.ts b/src/global.ts index 32f3c6a..73b534b 100644 --- a/src/global.ts +++ b/src/global.ts @@ -14,7 +14,7 @@ import { import { createMissingPhotoFolders } from "./createMissingPhotoFolders" import { reinstallTriggers } from "./triggers" import { newSkuHandler } from "./newSku" -import { columnOnEditHandler } from "./OnEditHandler" +import { columnOnEditHandler, onEditHandler } from "./OnEditHandler" import { onEditQueue, processBatchedEdits @@ -33,6 +33,7 @@ import { installSalesSyncTrigger } from "./triggers" ;(global as any).matchProductToShopifyOnEditHandler = matchProductToShopifyOnEditHandler ;(global as any).updateShopifyProductHandler = updateShopifyProductHandler ;(global as any).columnOnEditHandler = columnOnEditHandler +;(global as any).onEditHandler = onEditHandler ;(global as any).onEditQueue = onEditQueue ;(global as any).processBatchedEdits = processBatchedEdits ;(global as any).reauthorizeScript = reauthorizeScript diff --git a/src/shopifyApi.ts b/src/shopifyApi.ts index a2068e2..ab63da4 100644 --- a/src/shopifyApi.ts +++ b/src/shopifyApi.ts @@ -671,6 +671,49 @@ export class Shop { return newItem } + SetInventoryItemQuantity( + item: shopify.InventoryItem, + quantity: number, + config: Config + ) { + console.log("SetInventoryItemQuantity(" + JSON.stringify(item) + ", " + quantity + ")") + let gql = /* GraphQL */ ` + mutation inventorySetQuantities($input: InventorySetQuantitiesInput!) { + inventorySetQuantities(input: $input) { + inventoryAdjustmentGroup { + changes { + name + delta + } + } + userErrors { + field + message + } + } + } + ` + let variables = { + input: { + name: "available", + reason: "correction", + ignoreCompareQuantity: true, + quantities: [ + { + inventoryItemId: item.id, + locationId: config.shopifyLocationId, + quantity: quantity, + }, + ], + }, + } + let query = buildGqlQuery(gql, variables) + let response = this.shopifyGraphQLAPI(query) + // Response structure is different for setQuantities + console.log("SetInventoryItemQuantity response:\n" + JSON.stringify(response, null, 2)) + return response.content + } + SetInventoryItemDefaults(item: shopify.InventoryItem, config: Config) { let gql = /* GraphQL */ ` mutation inventoryItemUpdate($id: ID!, $input: InventoryItemInput!) { diff --git a/src/statusHandlers.ts b/src/statusHandlers.ts new file mode 100644 index 0000000..cdc6d3f --- /dev/null +++ b/src/statusHandlers.ts @@ -0,0 +1,89 @@ +import { getCellRangeByColumnName, getColumnName, toastAndLog } from "./sheetUtils" + +export function statusOnEditHandler(e: GoogleAppsScript.Events.SheetsOnEdit) { + const sheet = e.range.getSheet() + if (sheet.getName() !== "product_inventory") return + + const range = e.range + const col = range.getColumn() + // Optimization: Check if we are in the 'status' column (usually relatively early or fixed position, + // but looking up by name is safer against loose columns). + // Note: getColumnName technically opens the sheet again, but it's cached in Apps Script context usually. + const header = getColumnName("product_inventory", col) + + if (header !== "status") return + + // Handle multiple rows edit? + // Current requirement implies single row interactions, but let's just handle the top-left cell + // of the range for now or iterate if needed. + // Given e.value is used for single cell, let's stick to simple single-line logic or iterate. + // Safest to iterate if multiple rows selected. + + const numRows = range.getNumRows() + const startRow = range.getRow() + + // If e.value is present, it's a single cell edit. + // If not, it might be a paste. + + const values = range.getValues() // 2D array + + for (let i = 0; i < numRows; i++) { + const row = startRow + i + const val = String(values[i][0]).toLowerCase() + + const handler = handlers[val] + if (handler) { + console.log(`Executing handler for status: ${val} on row ${row}`) + handler.handle(sheet, row) + } else { + console.log(`No specific action for status: ${val} on row ${row}`) + } + } +} + +interface StatusHandler { + handle(sheet: GoogleAppsScript.Spreadsheet.Sheet, row: number): void +} + +class PublishedHandler implements StatusHandler { + handle(sheet: GoogleAppsScript.Spreadsheet.Sheet, row: number) { + const statusCell = getCellRangeByColumnName(sheet, "shopify_status", row) + if (statusCell) statusCell.setValue("ACTIVE") + + const qtyCell = getCellRangeByColumnName(sheet, "quantity", row) + if (qtyCell) qtyCell.setValue(1) + + toastAndLog("Status 'Published': Set to Active, Qty 1") + } +} + +class DraftedHandler implements StatusHandler { + handle(sheet: GoogleAppsScript.Spreadsheet.Sheet, row: number) { + const statusCell = getCellRangeByColumnName(sheet, "shopify_status", row) + if (statusCell) statusCell.setValue("DRAFT") + toastAndLog("Status 'Drafted': Set to Draft") + } +} + +class SoldHandler implements StatusHandler { + handle(sheet: GoogleAppsScript.Spreadsheet.Sheet, row: number) { + const statusCell = getCellRangeByColumnName(sheet, "shopify_status", row) + if (statusCell) statusCell.setValue("ACTIVE") + + const qtyCell = getCellRangeByColumnName(sheet, "quantity", row) + if (qtyCell) qtyCell.setValue(0) + + toastAndLog("Status 'Sold': Set to Active, Qty 0") + } +} + +const handlers: { [key: string]: StatusHandler } = { + "published": new PublishedHandler(), + "drafted": new DraftedHandler(), + "sold": new SoldHandler(), + "artist swap": new SoldHandler(), + "freebee": new SoldHandler(), + "sold: destroyed": new SoldHandler(), + "sold: gift": new SoldHandler(), + "sold: home use": new SoldHandler(), +} diff --git a/src/triggers.ts b/src/triggers.ts index 0a44105..b97f177 100644 --- a/src/triggers.ts +++ b/src/triggers.ts @@ -5,14 +5,7 @@ export function reinstallTriggers() { } let ss = SpreadsheetApp.getActive() - ScriptApp.newTrigger("newSkuHandler").forSpreadsheet(ss).onEdit().create() - ScriptApp.newTrigger("matchProductToShopifyOnEditHandler") - .forSpreadsheet(ss) - .onEdit() - .create() - // ScriptApp.newTrigger("columnOnEditHandler").forSpreadsheet(ss).onEdit().create() - // ScriptApp.newTrigger("onEditQueue").forSpreadsheet(ss).onEdit().create() - ScriptApp.newTrigger("onEditQueue").forSpreadsheet(ss).onEdit().create() + ScriptApp.newTrigger("onEditHandler").forSpreadsheet(ss).onEdit().create() ScriptApp.newTrigger("processBatchedEdits") .timeBased() .everyMinutes(1)