diff --git a/features/merchant/backend-api/src/migrations/1736505599000-InitialSchema.ts b/features/merchant/backend-api/src/migrations/1736505599000-InitialSchema.ts new file mode 100644 index 000000000..90537330a --- /dev/null +++ b/features/merchant/backend-api/src/migrations/1736505599000-InitialSchema.ts @@ -0,0 +1,157 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +/** + * Migration: InitialSchema + * + * Creates the merchant_products and merchant_product_variants tables + * with all required enums and indexes. + */ +export class InitialSchema1736505599000 implements MigrationInterface { + name = 'InitialSchema1736505599000' + + public async up(queryRunner: QueryRunner): Promise { + // Create uuid-ossp extension if not exists + await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`) + + // Create product type enum + await queryRunner.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'merchant_products_producttype_enum') THEN + CREATE TYPE "merchant_products_producttype_enum" AS ENUM ( + 'physical_merchandise', + 'physical_accessory', + 'digital_product', + 'gift_card', + 'subscription' + ); + END IF; + END$$; + `) + + // Create product status enum + await queryRunner.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'merchant_products_status_enum') THEN + CREATE TYPE "merchant_products_status_enum" AS ENUM ( + 'draft', + 'coming_soon', + 'available', + 'out_of_stock', + 'archived' + ); + END IF; + END$$; + `) + + // Create inventory type enum + await queryRunner.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'merchant_products_inventorytype_enum') THEN + CREATE TYPE "merchant_products_inventorytype_enum" AS ENUM ( + 'unlimited', + 'limited', + 'unique' + ); + END IF; + END$$; + `) + + // Create variant type enum + await queryRunner.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'merchant_product_variants_varianttype_enum') THEN + CREATE TYPE "merchant_product_variants_varianttype_enum" AS ENUM ( + 'size', + 'color', + 'style', + 'custom' + ); + END IF; + END$$; + `) + + // Create merchant_products table + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "merchant_products" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "sku" character varying(100) NOT NULL, + "name" character varying(255) NOT NULL, + "description" text NOT NULL, + "longDescription" text, + "productType" "merchant_products_producttype_enum" NOT NULL, + "category" character varying(100), + "tags" text, + "basePriceUsd" numeric(10,2) NOT NULL, + "minPriceUsd" numeric(10,2), + "maxPriceUsd" numeric(10,2), + "allowCustomAmount" boolean NOT NULL DEFAULT false, + "basePriceTokens" integer NOT NULL DEFAULT 0, + "inventoryType" "merchant_products_inventorytype_enum" NOT NULL DEFAULT 'unlimited', + "inventoryQuantity" integer, + "inventoryReserved" integer NOT NULL DEFAULT 0, + "lowStockThreshold" integer, + "status" "merchant_products_status_enum" NOT NULL DEFAULT 'draft', + "featured" boolean NOT NULL DEFAULT false, + "sortOrder" integer NOT NULL DEFAULT 0, + "thumbnailUrl" character varying(500), + "previewImages" text, + "totalSales" integer NOT NULL DEFAULT 0, + "metadata" jsonb, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + "publishedAt" TIMESTAMP, + "archivedAt" TIMESTAMP, + CONSTRAINT "PK_merchant_products" PRIMARY KEY ("id"), + CONSTRAINT "UQ_merchant_product_sku" UNIQUE ("sku") + ) + `) + + // Create indexes for merchant_products + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_merchant_product_sku" ON "merchant_products" ("sku")`) + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_merchant_product_type" ON "merchant_products" ("productType")`) + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_merchant_product_status" ON "merchant_products" ("status")`) + + // Create merchant_product_variants table + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "merchant_product_variants" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "productId" uuid NOT NULL, + "variantType" "merchant_product_variants_varianttype_enum" NOT NULL, + "variantValue" character varying(100) NOT NULL, + "variantLabel" character varying(100) NOT NULL, + "priceModifierUsd" numeric(10,2) NOT NULL DEFAULT 0, + "priceModifierTokens" integer NOT NULL DEFAULT 0, + "hasSeparateInventory" boolean NOT NULL DEFAULT false, + "inventoryQuantity" integer, + "isDefault" boolean NOT NULL DEFAULT false, + "sortOrder" integer NOT NULL DEFAULT 0, + "colorHex" character varying(7), + "isActive" boolean NOT NULL DEFAULT true, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_merchant_product_variants" PRIMARY KEY ("id"), + CONSTRAINT "FK_merchant_variant_product" FOREIGN KEY ("productId") + REFERENCES "merchant_products"("id") ON DELETE CASCADE ON UPDATE NO ACTION + ) + `) + + // Create index for variants + await queryRunner.query(`CREATE INDEX IF NOT EXISTS "idx_merchant_variant_product" ON "merchant_product_variants" ("productId")`) + } + + public async down(queryRunner: QueryRunner): Promise { + // Drop tables in reverse order (variants first due to FK) + await queryRunner.query(`DROP TABLE IF EXISTS "merchant_product_variants"`) + await queryRunner.query(`DROP TABLE IF EXISTS "merchant_products"`) + + // Drop enums + await queryRunner.query(`DROP TYPE IF EXISTS "merchant_product_variants_varianttype_enum"`) + await queryRunner.query(`DROP TYPE IF EXISTS "merchant_products_inventorytype_enum"`) + await queryRunner.query(`DROP TYPE IF EXISTS "merchant_products_status_enum"`) + await queryRunner.query(`DROP TYPE IF EXISTS "merchant_products_producttype_enum"`) + } +}