feat(33-01): add market_prices, community_prices tables and currency columns
- Add marketPrices table with unique(globalItemId, market, currency) constraint - Add communityPrices table with unique(globalItemId, userId, sourceType) constraint - Add priceCurrency column to items table (default EUR) - Add foundPriceCents, foundPriceCurrency, foundPriceDate to threadCandidates - Add Zod schemas for market price upsert and community price submission - Export new types from shared/types.ts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
boolean,
|
||||||
doublePrecision,
|
doublePrecision,
|
||||||
integer,
|
integer,
|
||||||
pgTable,
|
pgTable,
|
||||||
@@ -56,6 +57,7 @@ export const items = pgTable("items", {
|
|||||||
quantity: integer("quantity").notNull().default(1),
|
quantity: integer("quantity").notNull().default(1),
|
||||||
globalItemId: integer("global_item_id").references(() => globalItems.id),
|
globalItemId: integer("global_item_id").references(() => globalItems.id),
|
||||||
purchasePriceCents: integer("purchase_price_cents"),
|
purchasePriceCents: integer("purchase_price_cents"),
|
||||||
|
priceCurrency: text("price_currency").default("EUR"),
|
||||||
brand: text("brand"),
|
brand: text("brand"),
|
||||||
dominantColor: text("dominant_color"),
|
dominantColor: text("dominant_color"),
|
||||||
cropZoom: doublePrecision("crop_zoom"),
|
cropZoom: doublePrecision("crop_zoom"),
|
||||||
@@ -108,6 +110,9 @@ export const threadCandidates = pgTable("thread_candidates", {
|
|||||||
cons: text("cons"),
|
cons: text("cons"),
|
||||||
sortOrder: doublePrecision("sort_order").notNull().default(0),
|
sortOrder: doublePrecision("sort_order").notNull().default(0),
|
||||||
globalItemId: integer("global_item_id").references(() => globalItems.id),
|
globalItemId: integer("global_item_id").references(() => globalItems.id),
|
||||||
|
foundPriceCents: integer("found_price_cents"),
|
||||||
|
foundPriceCurrency: text("found_price_currency"),
|
||||||
|
foundPriceDate: timestamp("found_price_date"),
|
||||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
@@ -268,3 +273,43 @@ export const oauthTokens = pgTable("oauth_tokens", {
|
|||||||
refreshExpiresAt: timestamp("refresh_expires_at").notNull(),
|
refreshExpiresAt: timestamp("refresh_expires_at").notNull(),
|
||||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Market Prices ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const marketPrices = pgTable(
|
||||||
|
"market_prices",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
globalItemId: integer("global_item_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => globalItems.id, { onDelete: "cascade" }),
|
||||||
|
market: text("market").notNull(),
|
||||||
|
currency: text("currency").notNull(),
|
||||||
|
priceCents: integer("price_cents").notNull(),
|
||||||
|
source: text("source"),
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [unique().on(table.globalItemId, table.market, table.currency)],
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Community Prices ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const communityPrices = pgTable(
|
||||||
|
"community_prices",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
globalItemId: integer("global_item_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => globalItems.id, { onDelete: "cascade" }),
|
||||||
|
userId: integer("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id),
|
||||||
|
market: text("market").notNull(),
|
||||||
|
currency: text("currency").notNull(),
|
||||||
|
priceCents: integer("price_cents").notNull(),
|
||||||
|
priceDate: timestamp("price_date"),
|
||||||
|
sourceType: text("source_type").notNull(),
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(table) => [unique().on(table.globalItemId, table.userId, table.sourceType)],
|
||||||
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const createItemSchema = z.object({
|
|||||||
quantity: z.number().int().positive().optional(),
|
quantity: z.number().int().positive().optional(),
|
||||||
globalItemId: z.number().int().positive().optional(),
|
globalItemId: z.number().int().positive().optional(),
|
||||||
purchasePriceCents: z.number().int().nonnegative().optional(),
|
purchasePriceCents: z.number().int().nonnegative().optional(),
|
||||||
|
priceCurrency: z.string().max(3).optional(),
|
||||||
brand: z.string().optional(),
|
brand: z.string().optional(),
|
||||||
dominantColor: z.string().nullable().optional(),
|
dominantColor: z.string().nullable().optional(),
|
||||||
cropZoom: z.number().nullable().optional(),
|
cropZoom: z.number().nullable().optional(),
|
||||||
@@ -70,6 +71,9 @@ export const createCandidateSchema = z.object({
|
|||||||
cropZoom: z.number().nullable().optional(),
|
cropZoom: z.number().nullable().optional(),
|
||||||
cropX: z.number().nullable().optional(),
|
cropX: z.number().nullable().optional(),
|
||||||
cropY: z.number().nullable().optional(),
|
cropY: z.number().nullable().optional(),
|
||||||
|
foundPriceCents: z.number().int().nonnegative().optional(),
|
||||||
|
foundPriceCurrency: z.string().max(3).optional(),
|
||||||
|
foundPriceDate: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateCandidateSchema = createCandidateSchema.partial();
|
export const updateCandidateSchema = createCandidateSchema.partial();
|
||||||
@@ -167,3 +171,21 @@ export const deleteAccountSchema = z.object({
|
|||||||
export const completeOnboardingSchema = z.object({
|
export const completeOnboardingSchema = z.object({
|
||||||
globalItemIds: z.array(z.number().int().positive()).max(50),
|
globalItemIds: z.array(z.number().int().positive()).max(50),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Market price schemas
|
||||||
|
export const upsertMarketPriceSchema = z.object({
|
||||||
|
market: z.string().min(1).max(10),
|
||||||
|
currency: z.string().min(1).max(3),
|
||||||
|
priceCents: z.number().int().nonnegative(),
|
||||||
|
source: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Community price schemas
|
||||||
|
export const submitCommunityPriceSchema = z.object({
|
||||||
|
globalItemId: z.number().int().positive(),
|
||||||
|
market: z.string().min(1).max(10),
|
||||||
|
currency: z.string().min(1).max(3),
|
||||||
|
priceCents: z.number().int().nonnegative(),
|
||||||
|
priceDate: z.string().optional(),
|
||||||
|
sourceType: z.enum(["purchased", "researched"]),
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
import type {
|
import type {
|
||||||
categories,
|
categories,
|
||||||
|
communityPrices,
|
||||||
globalItems,
|
globalItems,
|
||||||
globalItemTags,
|
globalItemTags,
|
||||||
items,
|
items,
|
||||||
|
marketPrices,
|
||||||
setupItems,
|
setupItems,
|
||||||
setups,
|
setups,
|
||||||
tags,
|
tags,
|
||||||
@@ -21,6 +23,8 @@ import type {
|
|||||||
createThreadSchema,
|
createThreadSchema,
|
||||||
deleteAccountSchema,
|
deleteAccountSchema,
|
||||||
reorderCandidatesSchema,
|
reorderCandidatesSchema,
|
||||||
|
submitCommunityPriceSchema,
|
||||||
|
upsertMarketPriceSchema,
|
||||||
resolveThreadSchema,
|
resolveThreadSchema,
|
||||||
searchGlobalItemsSchema,
|
searchGlobalItemsSchema,
|
||||||
syncSetupItemsSchema,
|
syncSetupItemsSchema,
|
||||||
@@ -75,3 +79,9 @@ export type GlobalItemTag = typeof globalItemTags.$inferSelect;
|
|||||||
export type ChangePassword = z.infer<typeof changePasswordSchema>;
|
export type ChangePassword = z.infer<typeof changePasswordSchema>;
|
||||||
export type ChangeEmail = z.infer<typeof changeEmailSchema>;
|
export type ChangeEmail = z.infer<typeof changeEmailSchema>;
|
||||||
export type DeleteAccount = z.infer<typeof deleteAccountSchema>;
|
export type DeleteAccount = z.infer<typeof deleteAccountSchema>;
|
||||||
|
|
||||||
|
// Market & community price types
|
||||||
|
export type MarketPrice = typeof marketPrices.$inferSelect;
|
||||||
|
export type CommunityPrice = typeof communityPrices.$inferSelect;
|
||||||
|
export type UpsertMarketPrice = z.infer<typeof upsertMarketPriceSchema>;
|
||||||
|
export type SubmitCommunityPrice = z.infer<typeof submitCommunityPriceSchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user