diff --git a/src/Product.ts b/src/Product.ts index 1f9d782..deecce6 100644 --- a/src/Product.ts +++ b/src/Product.ts @@ -198,6 +198,10 @@ export class Product { "UpdateShopifyProduct: no product matched, this will be a new product" ) newProduct = true + // Default to DRAFT for auto-created products + if (!this.shopify_status) { + this.shopify_status = "DRAFT"; + } } console.log("UpdateShopifyProduct: calling productSet") let sps = this.ToShopifyProductSet() diff --git a/src/mediaHandlers.test.ts b/src/mediaHandlers.test.ts index e8bf26c..19caad4 100644 --- a/src/mediaHandlers.test.ts +++ b/src/mediaHandlers.test.ts @@ -96,7 +96,9 @@ jest.mock("./Product", () => { sku: sku, shopify_id: "shopify_id_123", title: "Test Product Title", + shopify_status: "ACTIVE", MatchToShopifyProduct: jest.fn(), + UpdateShopifyProduct: jest.fn(), ImportFromInventory: jest.fn() } }) @@ -395,16 +397,21 @@ describe("mediaHandlers", () => { expect(calledInstance.processMediaChanges).toHaveBeenCalledWith("SKU123", finalState, expect.anything(), null) }) - test("should throw if product not synced", () => { - const MockProduct = Product as unknown as jest.Mock - MockProduct.mockImplementationOnce(() => ({ - shopify_id: null, - MatchToShopifyProduct: jest.fn(), - ImportFromInventory: jest.fn() - })) + test("saveMediaChanges should auto-create product if not synced", () => { + const MockProduct = Product as unknown as jest.Mock + const mockUpdateShopify = jest.fn().mockImplementation(function(this: any) { + this.shopify_id = "NEW_ID" + }) + MockProduct.mockImplementationOnce(() => ({ + shopify_id: null, + MatchToShopifyProduct: jest.fn(), + UpdateShopifyProduct: mockUpdateShopify, + ImportFromInventory: jest.fn() + })) - expect(() => saveMediaChanges("SKU123", [])).toThrow("Product must be synced") - }) + saveMediaChanges("SKU123", []) + expect(mockUpdateShopify).toHaveBeenCalled() + }) test("should update sheet thumbnail with first image", () => { // Setup mock MediaService to NOT throw and just return logs @@ -592,9 +599,11 @@ describe("mediaHandlers", () => { (global.SpreadsheetApp.getActiveSheet as jest.Mock).mockReturnValue(mockSheet); const mockSSInstance = { - setCellValueByColumnName: jest.fn() + setCellValueByColumnName: jest.fn(), + getRowNumberByColumnValue: jest.fn().mockReturnValue(5), // Added for robustness + getHeaders: jest.fn().mockReturnValue(["sku", "title", "product_type", "product_style", "body_html"]) }; - (GASSpreadsheetService as unknown as jest.Mock).mockReturnValue(mockSSInstance); + (GASSpreadsheetService as unknown as jest.Mock).mockReturnValueOnce(mockSSInstance); (newSku as jest.Mock).mockReturnValue("SKU-123"); @@ -608,6 +617,37 @@ describe("mediaHandlers", () => { expect(result).toBe("SKU-123"); }) + test("saveMediaChanges should auto-create product if unsynced", () => { + // Mock defaults for this test + const mockRange = { getRow: () => 5 }; + const mockSheet = { + getName: jest.fn().mockReturnValue("product_inventory"), + getActiveRange: jest.fn().mockReturnValue(mockRange), + getLastColumn: jest.fn().mockReturnValue(5), + getRange: jest.fn().mockReturnValue(mockRange) + }; + (global.SpreadsheetApp.getActiveSheet as jest.Mock).mockReturnValue(mockSheet); + + // Setup Unsynced Product + const MockProduct = Product as unknown as jest.Mock + const mockUpdateShopify = jest.fn().mockImplementation(function(this: any) { + this.shopify_id = "CREATED_ID_123" + this.shopify_status = "DRAFT" + }) + + MockProduct.mockImplementationOnce(() => ({ + shopify_id: "", + MatchToShopifyProduct: jest.fn(), + UpdateShopifyProduct: mockUpdateShopify + })) + + // Proceed with save + const finalState = [{ id: "1" }] + saveMediaChanges("SKU_NEW", finalState) + + expect(mockUpdateShopify).toHaveBeenCalled() + }) + test("generateSkuForActiveRow should delegate to newSku", () => { const mockSheet = { getName: jest.fn().mockReturnValue("product_inventory"), diff --git a/src/mediaHandlers.ts b/src/mediaHandlers.ts index 36c7d28..05080cd 100644 --- a/src/mediaHandlers.ts +++ b/src/mediaHandlers.ts @@ -141,10 +141,12 @@ export function saveMediaChanges(sku: string, finalState: any[], jobId: string | } if (!product.shopify_id) { - // Allow saving Drive-only changes? No, we need Shopify context for "Staging" usually. - // But if we just rename drive files, we could? - // For now, fail safe. - throw new Error("Product must be synced to Shopify before saving media changes.") + console.log("saveMediaChanges: Product not synced. Auto-creating Draft Product..."); + product.UpdateShopifyProduct(shop); + + if (!product.shopify_id) { + throw new Error("Failed to auto-create Draft Product. Cannot save media."); + } } const logs = mediaService.processMediaChanges(sku, finalState, product.shopify_id, jobId) @@ -252,10 +254,11 @@ export function getMediaSavePlan(sku: string, finalState: any[]) { } if (!product.shopify_id) { - throw new Error("Product must be synced to Shopify before saving media changes.") + console.log("getMediaSavePlan: Product not synced. Proceeding with empty Shopify state."); } - return mediaService.calculatePlan(sku, finalState, product.shopify_id); + // Pass empty string if no ID, ensure calculatePlan handles it (it expects string) + return mediaService.calculatePlan(sku, finalState, product.shopify_id || ""); } export function executeSavePhase(sku: string, phase: string, planData: any, jobId: string | null = null) { @@ -274,7 +277,9 @@ export function executeSavePhase(sku: string, phase: string, planData: any, jobI } if (!product.shopify_id) { - throw new Error("Product must be synced to Shopify before saving media changes.") + console.log("executeSavePhase: Product not synced. Auto-creating Draft Product..."); + product.UpdateShopifyProduct(shop); + if (!product.shopify_id) throw new Error("Failed to auto-create Draft Product."); } return mediaService.executeSavePhase(sku, phase, planData, product.shopify_id, jobId); @@ -296,7 +301,9 @@ export function executeFullSavePlan(sku: string, plan: any, jobId: string | null } if (!product.shopify_id) { - throw new Error("Product must be synced to Shopify before saving media changes.") + console.log("executeFullSavePlan: Product not synced. Auto-creating Draft Product..."); + product.UpdateShopifyProduct(shop); + if (!product.shopify_id) throw new Error("Failed to auto-create Draft Product."); } return mediaService.executeFullSavePlan(sku, plan, product.shopify_id, jobId);