"use server"; import { getAdminUser } from "@/lib/admin-permissions"; import { getPaymentSettings } from "@/actions/payments"; import { withBrand } from "@/db/client"; import { products } from "@/db/schema"; import { inArray } from "drizzle-orm"; function getSquareBaseUrl() { return process.env.SQUARE_ENVIRONMENT === "production" ? "https://connect.squareup.com" : "https://connect.squareupsandbox.com"; } async function getSquareCatalogItemVariation( accessToken: string, catalogObjectId: string ) { const baseUrl = getSquareBaseUrl(); const response = await fetch( `${baseUrl}/v2/catalog/object/${catalogObjectId}`, { method: "GET", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", "Square-Version": "2025-01-16", }, } ); if (!response.ok) throw new Error(`Catalog object fetch failed: ${await response.text()}`); return response.json(); } async function batchUpdateSquareInventory( accessToken: string, locationId: string, updates: Array<{ catalogObjectId: string; quantity: number; type: "AVAILABLE" | "ON_HAND" | "SOLD_OUT"; }> ) { const baseUrl = getSquareBaseUrl(); const response = await fetch( `${baseUrl}/v2/inventory/changes/batch-create`, { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", "Square-Version": "2025-01-16", }, body: JSON.stringify({ idempotency_key: crypto.randomUUID(), changes: updates.map((u) => ({ type: "ADJUSTMENT", physical_count: { catalog_object_id: u.catalogObjectId, location_id: locationId, quantity: String(u.quantity), measurement_unit: { quantity_unit: u.type }, }, occurred_at: new Date().toISOString(), })), }), } ); if (!response.ok) { const err = await response.text(); throw new Error(`Batch inventory update failed: ${err}`); } return response.json(); } export type SyncResult = { success: boolean; synced: number; errors: string[]; }; /** * Sync inventory from Route Commerce products TO Square. * Reduces Square inventory for each product sold. * Called after an RC order is placed or updated. * * @param brandId - brand to sync for * @param items - array of { productId, quantity } representing sold items */ export async function syncInventoryToSquare( brandId: string, items: Array<{ productId: string; quantity: number }> ): Promise { const adminUser = await getAdminUser(); if (!adminUser) return { success: false, synced: 0, errors: ["Not authenticated"] }; const settingsResult = await getPaymentSettings(brandId); if (!settingsResult.success || !settingsResult.settings) { return { success: false, synced: 0, errors: ["Could not load payment settings"] }; } const settings = settingsResult.settings; if ( !settings.square_access_token || !settings.square_location_id || !settings.square_sync_enabled || !["rc_to_square", "bidirectional"].includes(settings.square_inventory_mode) ) { return { success: true, synced: 0, errors: [] }; // Not enabled, no-op } const errors: string[] = []; const synced = 0; // Build product name → quantity map const itemQtyMap = new Map(items.map((i) => [i.productId, i.quantity])); try { // Fetch product details from RC const productIds = items.map((i) => i.productId); const rows = await withBrand(brandId, (db) => db .select({ id: products.id, name: products.name }) .from(products) .where(inArray(products.id, productIds)) ); if (rows.length === 0 && productIds.length > 0) { return { success: false, synced: 0, errors: ["Failed to fetch products from RC"] }; } // Find Square catalog items matching product names and reduce quantity const updates: Array<{ catalogObjectId: string; quantity: number; type: "AVAILABLE" | "ON_HAND" | "SOLD_OUT" }> = []; for (const product of rows) { const qty = itemQtyMap.get(product.id) ?? 0; if (qty <= 0) continue; // In a real implementation, we'd maintain a mapping of RC product ID → Square catalog object ID // For now, we skip this without the mapping (requires manual catalog linking) // A future improvement would store square_catalog_object_id on RC products table errors.push(`Product "${product.name}": inventory sync requires Square catalog object ID mapping (not yet implemented)`); } } catch (err) { errors.push(`Inventory sync error: ${String(err)}`); } return { success: errors.length === 0, synced, errors }; } /** * Sync inventory from Square TO Route Commerce (polling). * Fetches Square inventory counts and updates RC product inventory. */ export async function syncInventoryFromSquare(brandId: string): Promise { const adminUser = await getAdminUser(); if (!adminUser) return { success: false, synced: 0, errors: ["Not authenticated"] }; if (adminUser.role === "brand_admin" && adminUser.brand_id !== brandId) { return { success: false, synced: 0, errors: ["Not authorized"] }; } const settingsResult = await getPaymentSettings(brandId); if (!settingsResult.success || !settingsResult.settings) { return { success: false, synced: 0, errors: ["Could not load payment settings"] }; } const settings = settingsResult.settings; if ( !settings.square_access_token || !settings.square_location_id || !settings.square_sync_enabled || !["square_to_rc", "bidirectional"].includes(settings.square_inventory_mode) ) { return { success: true, synced: 0, errors: [] }; // Not enabled, no-op } const errors: string[] = []; const synced = 0; try { // Fetch Square inventory counts for the location const baseUrl = getSquareBaseUrl(); const response = await fetch( `${baseUrl}/v2/inventory/${settings.square_location_id}/counts`, { method: "GET", headers: { Authorization: `Bearer ${settings.square_access_token}`, "Content-Type": "application/json", "Square-Version": "2025-01-16", }, } ); if (!response.ok) { return { success: false, synced: 0, errors: [`Square inventory fetch failed: ${await response.text()}`] }; } const data = await response.json(); // For each inventory count, update RC product if we have a mapping for (const count of data.counts ?? []) { // count.catalog_object_id, count.quantity, count.calculated_at // Would need a mapping table to update correct RC product // This is noted as a future enhancement errors.push(`Inventory item ${count.catalog_object_id}: requires Square catalog object ID mapping`); } } catch (err) { errors.push(`Inventory sync error: ${String(err)}`); } return { success: errors.length === 0, synced, errors }; }