Quality / test (push) Canceled after 0s
Add acquisition persistence, cost-basis calculation, seller-scoped admin routes, contracts, validation, migration, and focused application/HTTP tests.
361 lines
11 KiB
JavaScript
361 lines
11 KiB
JavaScript
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),
|
|
],
|
|
); |