# ZealPOS — Inventory Item TODO

## Overview

Create models, migrations, types, and resources for:
- `InventoryItem`
- `InventoryItemImage`
- `InventoryItemSupplier`

Also reorder the `ProductVariant` and `ProductVariantOption` migrations to run **after** `InventoryItems` so they can reference the table.

---

## Conventions (follow existing patterns)

Study `src/models/Brand.ts`, `src/migrations/create-brand.ts`, `src/types/brand.ts`, and `src/resource/BrandResource.ts` before writing anything. All new files must follow the same structure:

- Models use `uuid` (UUID v4) as `primaryKey`, with a separate auto-increment `id` (not exposed in responses)
- All foreign keys are `DataTypes.UUID`, `allowNull: true`
- Migrations include `id`, `uuid`, `...columns`, `createdAt`, `updatedAt`
- Types export: `*Attributes`, `Create*Payload`, `Update*Payload`, `*Response`
- Resources export: `*Resource(item)` and `*Collection(paginatedResult)`
- `*Response` always omits: `id`, `business_id`, and all foreign key `_id` fields — replace them with their associated object types

---

## 1. Types

### `src/types/inventory-item.ts`

```typescript
import { BrandAttributes } from './brand';
import { CategoryAttributes } from './category';

export interface InventoryItemAttributes {
  id: number;
  uuid: string;
  business_id: string | null;
  name: string | null;
  barcode_name: string | null;
  quantity: number | null;
  sellable_quantity: number | null;
  reorder_level: number | null;
  category_id: string | null;
  brand_id: string | null;
  isbn: string | null;
  manufacturer: string | null;
  description: string | null;
  weight: string | null;
  weight_unit: string | null;
  dimension_length: number | null;
  dimention_width: number | null;
  dimension_height: number | null;
  inactive: boolean | null;
  isbarcoded: boolean | null;
  cost_price: number | null;
  selling_price: number | null;
  promo_price: number | null;
  promo_start_date: Date | null;
  promo_end_date: Date | null;
  createdAt: Date;
  updatedAt: Date;
}

export type CreateInventoryItemPayload = Omit<InventoryItemAttributes, 'id' | 'uuid' | 'createdAt' | 'updatedAt'>;

export type UpdateInventoryItemPayload = Partial<CreateInventoryItemPayload>;

export type InventoryItemResponse = Omit<
  InventoryItemAttributes,
  'id' | 'business_id' | 'category_id' | 'brand_id' | 'createdAt' | 'updatedAt'
> & {
  category?: CategoryAttributes;
  brand?: BrandAttributes;
};
```

---

### `src/types/inventory-item-image.ts`

```typescript
export interface InventoryItemImageAttributes {
  id: number;
  uuid: string;
  inventory_item_id: string | null;
  image: string | null;
  createdAt: Date;
  updatedAt: Date;
}

export type CreateInventoryItemImagePayload = Omit<InventoryItemImageAttributes, 'id' | 'uuid' | 'createdAt' | 'updatedAt'>;

export type UpdateInventoryItemImagePayload = Partial<CreateInventoryItemImagePayload>;

export type InventoryItemImageResponse = Omit<
  InventoryItemImageAttributes,
  'id' | 'inventory_item_id' | 'createdAt' | 'updatedAt'
>;
```

---

### `src/types/inventory-item-supplier.ts`

```typescript
import { SupplierAttributes } from './supplier';

export interface InventoryItemSupplierAttributes {
  id: number;
  uuid: string;
  inventory_item_id: string | null;
  supplier_id: string | null;
  createdAt: Date;
  updatedAt: Date;
}

export type CreateInventoryItemSupplierPayload = Omit<InventoryItemSupplierAttributes, 'id' | 'uuid' | 'createdAt' | 'updatedAt'>;

export type UpdateInventoryItemSupplierPayload = Partial<CreateInventoryItemSupplierPayload>;

export type InventoryItemSupplierResponse = Omit<
  InventoryItemSupplierAttributes,
  'id' | 'inventory_item_id' | 'supplier_id' | 'createdAt' | 'updatedAt'
> & {
  supplier?: SupplierAttributes;
};
```

---

## 2. Migrations

> **Important:** Timestamp order matters. Create these migrations in this exact order so foreign key references resolve correctly:
>
> 1. `InventoryItems` migration
> 2. `InventoryItemImages` migration
> 3. `InventoryItemSuppliers` migration
> 4. `ProductVariants` migration (rename/recreate with later timestamp so it runs after `InventoryItems`)
> 5. `ProductVariantOptions` migration (rename/recreate with later timestamp so it runs after `ProductVariants`)

---

### `src/migrations/TIMESTAMP-create-inventory-item.ts`

```typescript
'use strict';
import { QueryInterface, DataTypes } from 'sequelize';

export default {
  async up(queryInterface: QueryInterface): Promise<void> {
    await queryInterface.createTable('InventoryItems', {
      id: {
        type: DataTypes.INTEGER,
        allowNull: false,
        autoIncrement: true,
        unique: true,
      },
      uuid: {
        type: DataTypes.UUID,
        allowNull: false,
        primaryKey: true,
        defaultValue: DataTypes.UUIDV4,
      },
      business_id: {
        type: DataTypes.UUID,
        allowNull: true,
        references: { model: 'Businesses', key: 'uuid' },
        onUpdate: 'CASCADE',
        onDelete: 'CASCADE',
      },
      name: { type: DataTypes.STRING, allowNull: true },
      barcode_name: { type: DataTypes.STRING, allowNull: true },
      quantity: { type: DataTypes.INTEGER, allowNull: true },
      sellable_quantity: { type: DataTypes.INTEGER, allowNull: true },
      reorder_level: { type: DataTypes.INTEGER, allowNull: true },
      category_id: {
        type: DataTypes.UUID,
        allowNull: true,
        references: { model: 'Categories', key: 'uuid' },
        onUpdate: 'CASCADE',
        onDelete: 'SET NULL',
      },
      brand_id: {
        type: DataTypes.UUID,
        allowNull: true,
        references: { model: 'Brands', key: 'uuid' },
        onUpdate: 'CASCADE',
        onDelete: 'SET NULL',
      },
      isbn: { type: DataTypes.STRING, allowNull: true },
      manufacturer: { type: DataTypes.STRING, allowNull: true },
      description: { type: DataTypes.TEXT, allowNull: true },
      weight: { type: DataTypes.STRING, allowNull: true },
      weight_unit: { type: DataTypes.STRING, allowNull: true },
      dimension_length: { type: DataTypes.FLOAT, allowNull: true },
      dimention_width: { type: DataTypes.FLOAT, allowNull: true },
      dimension_height: { type: DataTypes.FLOAT, allowNull: true },
      inactive: { type: DataTypes.BOOLEAN, allowNull: true, defaultValue: false },
      isbarcoded: { type: DataTypes.BOOLEAN, allowNull: true, defaultValue: false },
      cost_price: { type: DataTypes.DECIMAL(10, 2), allowNull: true },
      selling_price: { type: DataTypes.DECIMAL(10, 2), allowNull: true },
      promo_price: { type: DataTypes.DECIMAL(10, 2), allowNull: true },
      promo_start_date: { type: DataTypes.DATE, allowNull: true },
      promo_end_date: { type: DataTypes.DATE, allowNull: true },
      createdAt: { allowNull: false, type: DataTypes.DATE },
      updatedAt: { allowNull: false, type: DataTypes.DATE },
    });
  },
  async down(queryInterface: QueryInterface): Promise<void> {
    await queryInterface.dropTable('InventoryItems');
  },
};
```

---

### `src/migrations/TIMESTAMP-create-inventory-item-image.ts`

```typescript
'use strict';
import { QueryInterface, DataTypes } from 'sequelize';

export default {
  async up(queryInterface: QueryInterface): Promise<void> {
    await queryInterface.createTable('InventoryItemImages', {
      id: { type: DataTypes.INTEGER, allowNull: false, autoIncrement: true, unique: true },
      uuid: { type: DataTypes.UUID, allowNull: false, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
      inventory_item_id: {
        type: DataTypes.UUID,
        allowNull: true,
        references: { model: 'InventoryItems', key: 'uuid' },
        onUpdate: 'CASCADE',
        onDelete: 'CASCADE',
      },
      image: { type: DataTypes.STRING, allowNull: true },
      createdAt: { allowNull: false, type: DataTypes.DATE },
      updatedAt: { allowNull: false, type: DataTypes.DATE },
    });
  },
  async down(queryInterface: QueryInterface): Promise<void> {
    await queryInterface.dropTable('InventoryItemImages');
  },
};
```

---

### `src/migrations/TIMESTAMP-create-inventory-item-supplier.ts`

```typescript
'use strict';
import { QueryInterface, DataTypes } from 'sequelize';

export default {
  async up(queryInterface: QueryInterface): Promise<void> {
    await queryInterface.createTable('InventoryItemSuppliers', {
      id: { type: DataTypes.INTEGER, allowNull: false, autoIncrement: true, unique: true },
      uuid: { type: DataTypes.UUID, allowNull: false, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
      inventory_item_id: {
        type: DataTypes.UUID,
        allowNull: true,
        references: { model: 'InventoryItems', key: 'uuid' },
        onUpdate: 'CASCADE',
        onDelete: 'CASCADE',
      },
      supplier_id: {
        type: DataTypes.UUID,
        allowNull: true,
        references: { model: 'Suppliers', key: 'uuid' },
        onUpdate: 'CASCADE',
        onDelete: 'SET NULL',
      },
      createdAt: { allowNull: false, type: DataTypes.DATE },
      updatedAt: { allowNull: false, type: DataTypes.DATE },
    });
  },
  async down(queryInterface: QueryInterface): Promise<void> {
    await queryInterface.dropTable('InventoryItemSuppliers');
  },
};
```

---

### ProductVariant + ProductVariantOption migrations

**Delete the old migration files** for `ProductVariants` and `ProductVariantOptions` and recreate them with a **later timestamp** (after `InventoryItems`) so they run in the correct order.

`ProductVariant` migration should add the FK reference:
```typescript
inventory_item_id: {
  type: DataTypes.UUID,
  allowNull: true,
  references: { model: 'InventoryItems', key: 'uuid' },
  onUpdate: 'CASCADE',
  onDelete: 'SET NULL',
},
```

`ProductVariantOption` migration remains the same structure but with the later timestamp.

---

## 3. Models

### `src/models/InventoryItem.ts`

```typescript
'use strict';
import { Model, Sequelize } from 'sequelize';
import { InventoryItemAttributes } from '../types/inventory-item';

export default (sequelize: Sequelize, DataTypes: any) => {
  class InventoryItem extends Model implements InventoryItemAttributes {
    id!: number;
    uuid!: string;
    business_id!: string | null;
    name!: string | null;
    barcode_name!: string | null;
    quantity!: number | null;
    sellable_quantity!: number | null;
    reorder_level!: number | null;
    category_id!: string | null;
    brand_id!: string | null;
    isbn!: string | null;
    manufacturer!: string | null;
    description!: string | null;
    weight!: string | null;
    weight_unit!: string | null;
    dimension_length!: number | null;
    dimention_width!: number | null;
    dimension_height!: number | null;
    inactive!: boolean | null;
    isbarcoded!: boolean | null;
    cost_price!: number | null;
    selling_price!: number | null;
    promo_price!: number | null;
    promo_start_date!: Date | null;
    promo_end_date!: Date | null;
    createdAt!: Date;
    updatedAt!: Date;

    static associate(models: any) {
      InventoryItem.belongsTo(models.Business, { foreignKey: 'business_id', as: 'business' });
      InventoryItem.belongsTo(models.Category, { foreignKey: 'category_id', as: 'category' });
      InventoryItem.belongsTo(models.Brand, { foreignKey: 'brand_id', as: 'brand' });
      InventoryItem.hasMany(models.InventoryItemImage, { foreignKey: 'inventory_item_id', as: 'images' });
      InventoryItem.hasMany(models.InventoryItemSupplier, { foreignKey: 'inventory_item_id', as: 'itemSuppliers' });
      InventoryItem.hasMany(models.ProductVariant, { foreignKey: 'inventory_item_id', as: 'variants' });
    }
  }

  InventoryItem.init({
    uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
    business_id: { type: DataTypes.UUID, allowNull: true },
    name: DataTypes.STRING,
    barcode_name: DataTypes.STRING,
    quantity: DataTypes.INTEGER,
    sellable_quantity: DataTypes.INTEGER,
    reorder_level: DataTypes.INTEGER,
    category_id: { type: DataTypes.UUID, allowNull: true },
    brand_id: { type: DataTypes.UUID, allowNull: true },
    isbn: DataTypes.STRING,
    manufacturer: DataTypes.STRING,
    description: DataTypes.TEXT,
    weight: DataTypes.STRING,
    weight_unit: DataTypes.STRING,
    dimension_length: DataTypes.FLOAT,
    dimention_width: DataTypes.FLOAT,
    dimension_height: DataTypes.FLOAT,
    inactive: DataTypes.BOOLEAN,
    isbarcoded: DataTypes.BOOLEAN,
    cost_price: DataTypes.DECIMAL(10, 2),
    selling_price: DataTypes.DECIMAL(10, 2),
    promo_price: DataTypes.DECIMAL(10, 2),
    promo_start_date: DataTypes.DATE,
    promo_end_date: DataTypes.DATE,
  }, {
    sequelize,
    modelName: 'InventoryItem',
  });

  return InventoryItem;
};
```

---

### `src/models/InventoryItemImage.ts`

```typescript
'use strict';
import { Model, Sequelize } from 'sequelize';
import { InventoryItemImageAttributes } from '../types/inventory-item-image';

export default (sequelize: Sequelize, DataTypes: any) => {
  class InventoryItemImage extends Model implements InventoryItemImageAttributes {
    id!: number;
    uuid!: string;
    inventory_item_id!: string | null;
    image!: string | null;
    createdAt!: Date;
    updatedAt!: Date;

    static associate(models: any) {
      InventoryItemImage.belongsTo(models.InventoryItem, { foreignKey: 'inventory_item_id', as: 'inventoryItem' });
    }
  }

  InventoryItemImage.init({
    uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
    inventory_item_id: { type: DataTypes.UUID, allowNull: true },
    image: DataTypes.STRING,
  }, {
    sequelize,
    modelName: 'InventoryItemImage',
  });

  return InventoryItemImage;
};
```

---

### `src/models/InventoryItemSupplier.ts`

```typescript
'use strict';
import { Model, Sequelize } from 'sequelize';
import { InventoryItemSupplierAttributes } from '../types/inventory-item-supplier';

export default (sequelize: Sequelize, DataTypes: any) => {
  class InventoryItemSupplier extends Model implements InventoryItemSupplierAttributes {
    id!: number;
    uuid!: string;
    inventory_item_id!: string | null;
    supplier_id!: string | null;
    createdAt!: Date;
    updatedAt!: Date;

    static associate(models: any) {
      InventoryItemSupplier.belongsTo(models.InventoryItem, { foreignKey: 'inventory_item_id', as: 'inventoryItem' });
      InventoryItemSupplier.belongsTo(models.Supplier, { foreignKey: 'supplier_id', as: 'supplier' });
    }
  }

  InventoryItemSupplier.init({
    uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true },
    inventory_item_id: { type: DataTypes.UUID, allowNull: true },
    supplier_id: { type: DataTypes.UUID, allowNull: true },
  }, {
    sequelize,
    modelName: 'InventoryItemSupplier',
  });

  return InventoryItemSupplier;
};
```

---

## 4. Resources

### `src/resource/InventoryItemResource.ts`

```typescript
import { InventoryItemResponse } from '../types/inventory-item';
import { CollectionResponse } from '../interface/response';
import { PaginatedResult, paginationLinks } from '../lib/paginate';
import { url } from '../lib/url';
import { brandResource } from './BrandResource';
import { categoryResource } from './CategoryResource';

export function inventoryItemResource(item: any): InventoryItemResponse {
  return {
    uuid: item.uuid,
    name: item.name,
    barcode_name: item.barcode_name,
    quantity: item.quantity,
    sellable_quantity: item.sellable_quantity,
    reorder_level: item.reorder_level,
    isbn: item.isbn,
    manufacturer: item.manufacturer,
    description: item.description,
    weight: item.weight,
    weight_unit: item.weight_unit,
    dimension_length: item.dimension_length,
    dimention_width: item.dimention_width,
    dimension_height: item.dimension_height,
    inactive: item.inactive,
    isbarcoded: item.isbarcoded,
    cost_price: item.cost_price,
    selling_price: item.selling_price,
    promo_price: item.promo_price,
    promo_start_date: item.promo_start_date,
    promo_end_date: item.promo_end_date,
    category: item.category ? categoryResource(item.category) : undefined,
    brand: item.brand ? brandResource(item.brand) : undefined,
  };
}

export function inventoryItemCollection(items: PaginatedResult<any>): CollectionResponse<InventoryItemResponse> {
  return {
    data: items.data.map(i => inventoryItemResource(i)),
    meta: {
      total: items.total,
      currentPage: items.currentPage,
      totalPages: items.totalPages,
      pageSize: items.pageSize,
    },
    links: paginationLinks('inventory-items', items.currentPage, items.totalPages),
  };
}
```

---

### `src/resource/InventoryItemImageResource.ts`

```typescript
import { InventoryItemImageResponse } from '../types/inventory-item-image';
import { url } from '../lib/url';

export function inventoryItemImageResource(image: any): InventoryItemImageResponse {
  return {
    uuid: image.uuid,
    image: image.image ? url(image.image) : null,
  };
}
```

---

### `src/resource/InventoryItemSupplierResource.ts`

```typescript
import { InventoryItemSupplierResponse } from '../types/inventory-item-supplier';
import { supplierResource } from './SupplierResource';

export function inventoryItemSupplierResource(record: any): InventoryItemSupplierResponse {
  return {
    uuid: record.uuid,
    supplier: record.supplier ? supplierResource(record.supplier) : undefined,
  };
}
```

---

## 5. Register models in `src/models/index.ts`

Add the three new models to the models loader alongside existing ones:
- `InventoryItem`
- `InventoryItemImage`
- `InventoryItemSupplier`

---

## 6. Checklist

- [ ] Create `src/types/inventory-item.ts`
- [ ] Create `src/types/inventory-item-image.ts`
- [ ] Create `src/types/inventory-item-supplier.ts`
- [ ] Create migration `TIMESTAMP-create-inventory-item.ts` (timestamp must be before variants)
- [ ] Create migration `TIMESTAMP-create-inventory-item-image.ts`
- [ ] Create migration `TIMESTAMP-create-inventory-item-supplier.ts`
- [ ] Delete old `ProductVariant` migration and recreate with later timestamp + `inventory_item_id` FK reference
- [ ] Delete old `ProductVariantOption` migration and recreate with later timestamp
- [ ] Create `src/models/InventoryItem.ts`
- [ ] Create `src/models/InventoryItemImage.ts`
- [ ] Create `src/models/InventoryItemSupplier.ts`
- [ ] Register all three models in `src/models/index.ts`
- [ ] Create `src/resource/InventoryItemResource.ts`
- [ ] Create `src/resource/InventoryItemImageResource.ts`
- [ ] Create `src/resource/InventoryItemSupplierResource.ts`
- [ ] Run `npm run migrate` and verify no errors