feat: MercadoLibre integration + inventory bulk publish + WhatsApp bridge fixes
- Add MercadoLibre OAuth, listings, orders, webhooks and category search - New marketplace_external_bp.py, meli_service.py, marketplace_external_service.py - New marketplace_external.html/js with ML management UI - Inventory: bulk publish to ML with category autocomplete, listing type and shipping selectors - Inventory: new .btn--meli styles, select/label CSS fixes - WhatsApp bridge: rate limiting, 440/515/408 error handling, stale watchdog - DB migration v3.4_meli_integration.sql for marketplace_listings, orders, sync_queue - Add Celery tasks for ML sync and webhook processing - Sidebar: MercadoLibre navigation link
This commit is contained in:
91
pos/migrations/v3.3_marketplace_any_part.sql
Normal file
91
pos/migrations/v3.3_marketplace_any_part.sql
Normal file
@@ -0,0 +1,91 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- v3.3 — Marketplace accepts any part number (seller listings)
|
||||
-- Target: nexus_autoparts (master DB)
|
||||
-- Date: 2026-05-17
|
||||
--
|
||||
-- Makes warehouse_inventory part_id nullable and adds seller-defined
|
||||
-- fields so any seller can list parts that don't exist in the OEM catalog.
|
||||
-- Existing OEM-matched listings are untouched.
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- ─── 1. WAREHOUSE_INVENTORY — add seller listing columns ─────────────
|
||||
|
||||
ALTER TABLE warehouse_inventory
|
||||
ADD COLUMN IF NOT EXISTS seller_part_number VARCHAR(100),
|
||||
ADD COLUMN IF NOT EXISTS seller_part_name VARCHAR(300),
|
||||
ADD COLUMN IF NOT EXISTS seller_category VARCHAR(100),
|
||||
ADD COLUMN IF NOT EXISTS tenant_inventory_id INTEGER;
|
||||
|
||||
-- Make part_id nullable so seller listings (without catalog match) can exist
|
||||
ALTER TABLE warehouse_inventory ALTER COLUMN part_id DROP NOT NULL;
|
||||
|
||||
-- ─── 2. WAREHOUSE_INVENTORY — drop old unique, add partial uniques ───
|
||||
|
||||
-- The old constraint was on (user_id, part_id, warehouse_location).
|
||||
-- We replace it with two partial unique indexes:
|
||||
-- - OEM items: (bodega_id, part_id, warehouse_location) WHERE part_id IS NOT NULL
|
||||
-- - Seller items: (bodega_id, seller_part_number, warehouse_location) WHERE part_id IS NULL
|
||||
|
||||
ALTER TABLE warehouse_inventory
|
||||
DROP CONSTRAINT IF EXISTS warehouse_inventory_user_id_part_id_warehouse_location_key;
|
||||
|
||||
DROP INDEX IF EXISTS idx_wi_unique_composite;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_wi_unique_oem
|
||||
ON warehouse_inventory(bodega_id, part_id, warehouse_location)
|
||||
WHERE part_id IS NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_wi_unique_seller
|
||||
ON warehouse_inventory(bodega_id, seller_part_number, warehouse_location)
|
||||
WHERE part_id IS NULL;
|
||||
|
||||
-- Ensure every row has either part_id or seller_part_number
|
||||
ALTER TABLE warehouse_inventory
|
||||
DROP CONSTRAINT IF EXISTS chk_wi_part_or_seller;
|
||||
|
||||
ALTER TABLE warehouse_inventory
|
||||
ADD CONSTRAINT chk_wi_part_or_seller
|
||||
CHECK (
|
||||
(part_id IS NOT NULL AND seller_part_number IS NULL)
|
||||
OR
|
||||
(part_id IS NULL AND seller_part_number IS NOT NULL)
|
||||
);
|
||||
|
||||
-- ─── 3. WAREHOUSE_INVENTORY — search indexes ─────────────────────────
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_wi_seller_pn
|
||||
ON warehouse_inventory (bodega_id, seller_part_number)
|
||||
WHERE part_id IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_wi_seller_category
|
||||
ON warehouse_inventory (seller_category)
|
||||
WHERE part_id IS NULL;
|
||||
|
||||
-- GIN index for text search on seller listings
|
||||
CREATE INDEX IF NOT EXISTS idx_wi_seller_search
|
||||
ON warehouse_inventory
|
||||
USING gin (to_tsvector('spanish',
|
||||
COALESCE(seller_part_name, '') || ' ' || COALESCE(seller_part_number, '')
|
||||
))
|
||||
WHERE part_id IS NULL;
|
||||
|
||||
-- ─── 4. PURCHASE_ORDER_ITEMS — make part_id nullable ─────────────────
|
||||
|
||||
ALTER TABLE purchase_order_items
|
||||
ALTER COLUMN part_id DROP NOT NULL;
|
||||
|
||||
-- Add a flag so seller listings can be distinguished in POs
|
||||
ALTER TABLE purchase_order_items
|
||||
ADD COLUMN IF NOT EXISTS is_seller_listing BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
-- ─── 5. Back-compat: ensure existing rows are valid ──────────────────
|
||||
|
||||
-- Existing rows should have part_id set and seller_part_number NULL.
|
||||
-- If any row violates the new check, this will fail loudly.
|
||||
UPDATE warehouse_inventory
|
||||
SET seller_part_number = NULL
|
||||
WHERE part_id IS NOT NULL AND seller_part_number IS NOT NULL;
|
||||
|
||||
UPDATE warehouse_inventory
|
||||
SET part_id = NULL
|
||||
WHERE part_id IS NULL AND seller_part_number IS NULL;
|
||||
110
pos/migrations/v3.4_meli_integration.sql
Normal file
110
pos/migrations/v3.4_meli_integration.sql
Normal file
@@ -0,0 +1,110 @@
|
||||
-- ============================================================
|
||||
-- v3.4 MercadoLibre Integration
|
||||
-- ============================================================
|
||||
-- Adds tables for external marketplace listings, orders,
|
||||
-- order items, and a generic sync queue.
|
||||
-- All tables live in the tenant DB.
|
||||
-- ============================================================
|
||||
|
||||
-- Listings published on MercadoLibre (extensible to Amazon later)
|
||||
CREATE TABLE IF NOT EXISTS marketplace_listings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
inventory_id INTEGER REFERENCES inventory(id),
|
||||
channel VARCHAR(20) NOT NULL DEFAULT 'mercadolibre',
|
||||
external_item_id VARCHAR(50) NOT NULL,
|
||||
external_status VARCHAR(30) DEFAULT 'active',
|
||||
external_permalink TEXT,
|
||||
title TEXT,
|
||||
meli_category_id VARCHAR(30),
|
||||
publish_price NUMERIC(12,2),
|
||||
last_sync_at TIMESTAMPTZ,
|
||||
sync_errors TEXT,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_marketplace_listings_inventory
|
||||
ON marketplace_listings(inventory_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_marketplace_listings_external
|
||||
ON marketplace_listings(external_item_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_marketplace_listings_unique
|
||||
ON marketplace_listings(inventory_id, channel) WHERE is_active = true;
|
||||
|
||||
-- Orders received from MercadoLibre
|
||||
CREATE TABLE IF NOT EXISTS marketplace_orders (
|
||||
id SERIAL PRIMARY KEY,
|
||||
channel VARCHAR(20) NOT NULL DEFAULT 'mercadolibre',
|
||||
external_order_id VARCHAR(50) NOT NULL UNIQUE,
|
||||
external_status VARCHAR(30) NOT NULL,
|
||||
buyer_name VARCHAR(200),
|
||||
buyer_email VARCHAR(200),
|
||||
buyer_phone VARCHAR(50),
|
||||
buyer_nickname VARCHAR(100),
|
||||
shipping_address JSONB,
|
||||
total_amount NUMERIC(12,2),
|
||||
shipping_cost NUMERIC(12,2),
|
||||
meli_shipping_id VARCHAR(50),
|
||||
nexus_sale_id INTEGER REFERENCES sales(id),
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
notes TEXT,
|
||||
raw_json JSONB,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_marketplace_orders_status
|
||||
ON marketplace_orders(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_marketplace_orders_external
|
||||
ON marketplace_orders(external_order_id);
|
||||
|
||||
-- Items inside a marketplace order
|
||||
CREATE TABLE IF NOT EXISTS marketplace_order_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
marketplace_order_id INTEGER REFERENCES marketplace_orders(id) ON DELETE CASCADE,
|
||||
inventory_id INTEGER REFERENCES inventory(id),
|
||||
external_item_id VARCHAR(50),
|
||||
title VARCHAR(300),
|
||||
quantity INTEGER NOT NULL,
|
||||
unit_price NUMERIC(12,2),
|
||||
total_price NUMERIC(12,2),
|
||||
listing_id INTEGER REFERENCES marketplace_listings(id)
|
||||
);
|
||||
|
||||
-- Generic sync queue (reusable for future Amazon integration)
|
||||
CREATE TABLE IF NOT EXISTS marketplace_sync_queue (
|
||||
id SERIAL PRIMARY KEY,
|
||||
inventory_id INTEGER REFERENCES inventory(id),
|
||||
channel VARCHAR(20) NOT NULL,
|
||||
action VARCHAR(20) NOT NULL,
|
||||
status VARCHAR(20) DEFAULT 'pending',
|
||||
payload JSONB,
|
||||
error_message TEXT,
|
||||
retry_count INT DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
processed_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_marketplace_sync_queue_pending
|
||||
ON marketplace_sync_queue(status, channel) WHERE status = 'pending';
|
||||
|
||||
-- Add source column to sales to track origin (POS, ML, Amazon, etc.)
|
||||
-- If the column already exists from another migration, do nothing.
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'sales' AND column_name = 'source'
|
||||
) THEN
|
||||
ALTER TABLE sales ADD COLUMN source VARCHAR(30) DEFAULT 'pos';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'sales' AND column_name = 'external_order_id'
|
||||
) THEN
|
||||
ALTER TABLE sales ADD COLUMN external_order_id VARCHAR(50);
|
||||
END IF;
|
||||
END $$;
|
||||
Reference in New Issue
Block a user