import { boolean, check, index, integer, jsonb, pgEnum, pgTable, text, timestamp, uniqueIndex, uuid, } from 'drizzle-orm/pg-core'; import { sql } from 'drizzle-orm'; export const userRole = pgEnum('user_role', ['ADMIN']); export const inventoryStatus = pgEnum('inventory_status', [ 'ACTIVE', 'SOLD', 'ARCHIVED', ]); export const listingStatus = pgEnum('listing_status', [ 'DRAFT', 'AVAILABLE', 'PENDING', 'CLOSED', 'ARCHIVED', ]); const timestamps = { createdAt: timestamp('created_at', { withTimezone: true }) .defaultNow() .notNull(), updatedAt: timestamp('updated_at', { withTimezone: true }) .defaultNow() .notNull(), }; export const users = pgTable( 'users', { id: uuid('id').defaultRandom().primaryKey(), email: text('email').notNull(), passwordHash: text('password_hash').notNull(), role: userRole('role').default('ADMIN').notNull(), ...timestamps, }, (table) => [uniqueIndex('users_email_unique').on(table.email)], ); export const sellers = pgTable( 'sellers', { id: uuid('id').defaultRandom().primaryKey(), userId: uuid('user_id') .notNull() .references(() => users.id), displayName: text('display_name').notNull(), ...timestamps, }, (table) => [uniqueIndex('sellers_user_id_unique').on(table.userId)], ); export const sessions = pgTable( 'sessions', { id: uuid('id').defaultRandom().primaryKey(), userId: uuid('user_id') .notNull() .references(() => users.id), sessionTokenHash: text('session_token_hash').notNull(), csrfTokenHash: text('csrf_token_hash').notNull(), expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), lastActivityAt: timestamp('last_activity_at', { withTimezone: true }) .defaultNow() .notNull(), ipAddress: text('ip_address'), userAgent: text('user_agent'), revokedAt: timestamp('revoked_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }) .defaultNow() .notNull(), }, (table) => [ uniqueIndex('sessions_token_hash_unique').on(table.sessionTokenHash), index('sessions_user_id_index').on(table.userId), index('sessions_expires_at_index').on(table.expiresAt), ], ); export const securityAuditEvents = pgTable( 'security_audit_events', { id: uuid('id').defaultRandom().primaryKey(), userId: uuid('user_id').references(() => users.id), event: text('event').notNull(), ipAddress: text('ip_address'), userAgent: text('user_agent'), metadata: jsonb('metadata').default({}).notNull(), createdAt: timestamp('created_at', { withTimezone: true }) .defaultNow() .notNull(), }, (table) => [ index('security_audit_events_user_id_index').on(table.userId), index('security_audit_events_event_created_at_index').on( table.event, table.createdAt, ), ], ); export const passwordResetTokens = pgTable( 'password_reset_tokens', { id: uuid('id').defaultRandom().primaryKey(), userId: uuid('user_id') .notNull() .references(() => users.id), tokenHash: text('token_hash').notNull(), expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), usedAt: timestamp('used_at', { withTimezone: true }), createdAt: timestamp('created_at', { withTimezone: true }) .defaultNow() .notNull(), }, (table) => [ uniqueIndex('password_reset_tokens_token_hash_unique').on(table.tokenHash), index('password_reset_tokens_user_id_index').on(table.userId), ], ); export const mfaMethods = pgTable( 'mfa_methods', { id: uuid('id').defaultRandom().primaryKey(), userId: uuid('user_id') .notNull() .references(() => users.id), type: text('type').notNull(), secretCiphertext: text('secret_ciphertext').notNull(), name: text('name').notNull(), isEnabled: boolean('is_enabled').default(false).notNull(), createdAt: timestamp('created_at', { withTimezone: true }) .defaultNow() .notNull(), lastUsedAt: timestamp('last_used_at', { withTimezone: true }), disabledAt: timestamp('disabled_at', { withTimezone: true }), }, (table) => [index('mfa_methods_user_id_index').on(table.userId)], ); export const categories = pgTable( 'categories', { id: uuid('id').defaultRandom().primaryKey(), name: text('name').notNull(), slug: text('slug').notNull(), active: boolean('active').default(true).notNull(), sort: integer('sort').default(0).notNull(), ...timestamps, }, (table) => [ uniqueIndex('categories_name_unique').on(table.name), uniqueIndex('categories_slug_unique').on(table.slug), ], ); export const conditionProfiles = pgTable('condition_profiles', { id: uuid('id').defaultRandom().primaryKey(), categoryId: uuid('category_id') .notNull() .references(() => categories.id), name: text('name').notNull(), active: boolean('active').default(true).notNull(), sort: integer('sort').default(0).notNull(), ...timestamps, }); export const conditions = pgTable( 'conditions', { id: uuid('id').defaultRandom().primaryKey(), conditionProfileId: uuid('condition_profile_id') .notNull() .references(() => conditionProfiles.id), name: text('name').notNull(), code: text('code').notNull(), qualityRank: integer('quality_rank').notNull(), description: text('description'), active: boolean('active').default(true).notNull(), sort: integer('sort').default(0).notNull(), ...timestamps, }, (table) => [ uniqueIndex('conditions_profile_code_unique').on( table.conditionProfileId, table.code, ), ], ); export const products = pgTable( 'products', { id: uuid('id').defaultRandom().primaryKey(), sellerId: uuid('seller_id') .notNull() .references(() => sellers.id), categoryId: uuid('category_id') .notNull() .references(() => categories.id), name: text('name').notNull(), slug: text('slug').notNull(), description: text('description'), brand: text('brand'), setName: text('set_name'), cardNumber: text('card_number'), eventName: text('event_name'), releaseYear: integer('release_year'), ...timestamps, }, (table) => [ uniqueIndex('products_seller_slug_unique').on(table.sellerId, table.slug), check( 'products_release_year_valid', sql`${table.releaseYear} IS NULL OR ${table.releaseYear} BETWEEN 1996 AND 9999`, ), ], ); export const inventoryItems = pgTable('inventory_items', { id: uuid('id').defaultRandom().primaryKey(), productId: uuid('product_id') .notNull() .references(() => products.id), conditionId: uuid('condition_id') .notNull() .references(() => conditions.id), status: inventoryStatus('status').default('ACTIVE').notNull(), notes: text('notes'), ...timestamps, }); export const acquisitions = pgTable('acquisitions', { id: uuid('id').defaultRandom().primaryKey(), inventoryItemId: uuid('inventory_item_id') .notNull() .references(() => inventoryItems.id), method: text('method').notNull(), costMinor: integer('cost_minor').notNull(), currency: text('currency').notNull(), date: text('date'), source: text('source'), location: text('location'), shippingMinor: integer('shipping_minor').default(0).notNull(), taxMinor: integer('tax_minor').default(0).notNull(), otherFeesMinor: integer('other_fees_minor').default(0).notNull(), estimatedTradeValueMinor: integer('estimated_trade_value_minor').default(0).notNull(), notes: text('notes'), ...timestamps, }); export const inventoryImages = pgTable( 'inventory_images', { id: uuid('id').defaultRandom().primaryKey(), inventoryItemId: uuid('inventory_item_id') .notNull() .references(() => inventoryItems.id), storageKey: text('storage_key').notNull(), originalFilename: text('original_filename').notNull(), mimeType: text('mime_type').notNull(), fileSize: integer('file_size').notNull(), width: integer('width').notNull(), height: integer('height').notNull(), altText: text('alt_text'), sortOrder: integer('sort_order').default(0).notNull(), isPrimary: boolean('is_primary').default(false).notNull(), processingStatus: text('processing_status').default('PENDING').notNull(), ...timestamps, }, (table) => [index('inventory_images_inventory_item_id_index').on(table.inventoryItemId)], ); export const imageDerivativeJobs = pgTable( 'image_derivative_jobs', { id: uuid('id').defaultRandom().primaryKey(), inventoryImageId: uuid('inventory_image_id') .notNull() .references(() => inventoryImages.id), sourceKey: text('source_key').notNull(), status: text('status').default('PENDING').notNull(), derivatives: jsonb('derivatives').default([]).notNull(), error: text('error'), ...timestamps, }, (table) => [index('image_derivative_jobs_image_id_index').on(table.inventoryImageId)], ); export const listings = pgTable('listings', { id: uuid('id').defaultRandom().primaryKey(), inventoryItemId: uuid('inventory_item_id') .notNull() .references(() => inventoryItems.id), productName: text('product_name').notNull(), categoryId: uuid('category_id').notNull(), conditionId: uuid('condition_id') .notNull() .references(() => conditions.id), askingPriceMinor: integer('asking_price_minor').notNull(), status: listingStatus('status').default('DRAFT').notNull(), notes: text('notes'), ...timestamps, }, (table) => [ index('listings_inventory_item_id_index').on(table.inventoryItemId), index('listings_status_index').on(table.status), ]); export const sales = pgTable('sales', { id: uuid('id').defaultRandom().primaryKey(), sellerId: uuid('seller_id') .notNull() .references(() => sellers.id), status: text('status').default('COMPLETED').notNull(), saleDate: text('sale_date').notNull(), saleLocation: text('sale_location'), soldPriceMinor: integer('sold_price_minor').notNull(), currency: text('currency').notNull(), platformFeesMinor: integer('platform_fees_minor').default(0).notNull(), paymentProcessingFeesMinor: integer('payment_processing_fees_minor').default(0).notNull(), shippingCostMinor: integer('shipping_cost_minor').default(0).notNull(), taxesMinor: integer('taxes_minor').default(0).notNull(), otherFeesMinor: integer('other_fees_minor').default(0).notNull(), buyerName: text('buyer_name'), buyerContact: text('buyer_contact'), notes: text('notes'), voidReason: text('void_reason'), voidedAt: timestamp('voided_at', { withTimezone: true }), ...timestamps, }); export const saleItems = pgTable( 'sale_items', { id: uuid('id').defaultRandom().primaryKey(), saleId: uuid('sale_id') .notNull() .references(() => sales.id), inventoryItemId: uuid('inventory_item_id') .notNull() .references(() => inventoryItems.id), soldPriceMinor: integer('sold_price_minor').notNull(), costBasisMinor: integer('cost_basis_minor').notNull(), }, (table) => [ uniqueIndex('sale_items_inventory_item_id_unique').on(table.inventoryItemId), index('sale_items_sale_id_index').on(table.saleId), ], );