From 1039c90918209baf6669c01998e2e9439e4be70f Mon Sep 17 00:00:00 2001 From: Jurgis Sakalauskas Date: Tue, 14 Oct 2025 13:30:44 +0300 Subject: [PATCH] refactor prices schema, add missing shared files --- client/src/api/item-prices.ts | 18 +- .../project-detail/item-prices-panel.tsx | 153 ++++------------- .../src/controllers/item-prices-controller.ts | 27 +-- server/src/controllers/url-helpers.ts | 16 +- server/src/db/item-prices-repository.ts | 80 +-------- server/src/db/items-repository.ts | 10 +- .../0005-create-item-prices-table.ts | 29 +--- server/src/middleware/error-handler.ts | 1 + server/src/validation/item-prices.ts | 160 +++--------------- shared/models/item-price.d.ts | 14 ++ shared/models/item.d.ts | 32 ++++ shared/models/pagination.d.ts | 10 ++ shared/models/project.d.ts | 12 ++ 13 files changed, 161 insertions(+), 401 deletions(-) create mode 100644 shared/models/item-price.d.ts create mode 100644 shared/models/item.d.ts create mode 100644 shared/models/pagination.d.ts create mode 100644 shared/models/project.d.ts diff --git a/client/src/api/item-prices.ts b/client/src/api/item-prices.ts index 9fe9107..41bf57e 100644 --- a/client/src/api/item-prices.ts +++ b/client/src/api/item-prices.ts @@ -1,12 +1,8 @@ import type { PaginatedResponse } from '@shared/models/pagination'; -import type { - ItemPrice, - PriceCondition, - PriceSourceType -} from '@shared/models/item-price'; +import type { ItemPrice, PriceCondition } from '@shared/models/item-price'; import { apiFetch } from './http-client'; -export type { PriceCondition, PriceSourceType }; +export type { PriceCondition }; export type ItemPricesResponse = PaginatedResponse; @@ -47,13 +43,9 @@ export interface CreateItemPricePayload { condition: PriceCondition; amount: number; currency: string; - sourceType: PriceSourceType; sourceUrl?: string | null; sourceUrlId?: string | null; - sourceNote?: string | null; note?: string | null; - observedAt?: string; - isPrimary?: boolean; } interface SingleItemPriceResponse { @@ -72,13 +64,9 @@ export const createItemPrice = async ( condition: payload.condition, amount: payload.amount, currency: payload.currency, - sourceType: payload.sourceType, sourceUrl: payload.sourceUrl ?? null, sourceUrlId: payload.sourceUrlId ?? null, - sourceNote: payload.sourceNote ?? null, - note: payload.note ?? null, - observedAt: payload.observedAt ?? null, - isPrimary: payload.isPrimary ?? false + note: payload.note ?? null } } ); diff --git a/client/src/pages/project-detail/item-prices-panel.tsx b/client/src/pages/project-detail/item-prices-panel.tsx index 86ae369..bb4e086 100644 --- a/client/src/pages/project-detail/item-prices-panel.tsx +++ b/client/src/pages/project-detail/item-prices-panel.tsx @@ -1,9 +1,8 @@ import { FormEvent, useMemo, useState } from 'react'; import { useCreateItemPriceMutation, useItemPricesQuery } from '../../query/item-prices'; -import type { PriceCondition, PriceSourceType } from '../../api/item-prices'; +import type { PriceCondition } from '../../api/item-prices'; const priceConditionOptions: PriceCondition[] = ['new', 'used']; -const priceSourceOptions: PriceSourceType[] = ['url', 'manual']; interface ItemPricesPanelProps { itemId: string; @@ -13,12 +12,8 @@ export function ItemPricesPanel({ itemId }: ItemPricesPanelProps) { const [condition, setCondition] = useState('new'); const [amount, setAmount] = useState(''); const [currency, setCurrency] = useState('USD'); - const [sourceType, setSourceType] = useState('manual'); const [sourceUrl, setSourceUrl] = useState(''); const [note, setNote] = useState(''); - const [sourceNote, setSourceNote] = useState(''); - const [observedAt, setObservedAt] = useState(''); - const [isPrimary, setIsPrimary] = useState(true); const [formError, setFormError] = useState(null); const pricesQuery = useItemPricesQuery(itemId, true); @@ -46,20 +41,13 @@ export function ItemPricesPanel({ itemId }: ItemPricesPanelProps) { condition, amount: parsedAmount, currency: currency.trim().toUpperCase(), - sourceType, - sourceUrl: sourceType === 'url' ? (sourceUrl.trim() || null) : null, - note: note.trim() ? note.trim() : null, - sourceNote: sourceNote.trim() ? sourceNote.trim() : null, - observedAt: observedAt ? new Date(observedAt).toISOString() : undefined, - isPrimary + sourceUrl: sourceUrl.trim() ? sourceUrl.trim() : null, + note: note.trim() ? note.trim() : null }); setAmount(''); setSourceUrl(''); setNote(''); - setSourceNote(''); - setObservedAt(''); - setIsPrimary(true); } catch (error) { setFormError((error as Error).message); } @@ -130,97 +118,33 @@ export function ItemPricesPanel({ itemId }: ItemPricesPanelProps) { -
-
- - setCurrency(event.target.value.toUpperCase())} - maxLength={3} - className="uppercase rounded-md border border-slate-300 px-2 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200" - placeholder="USD" - required - /> -
- -
- setIsPrimary(event.target.checked)} - className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500" - /> - -
-
- -
-
- - -
-
- - setObservedAt(event.target.value)} - className="rounded-md border border-slate-300 px-2 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200" - /> -
-
- - {sourceType === 'url' ? ( -
- - setSourceUrl(event.target.value)} - className="rounded-md border border-slate-300 px-2 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200" - placeholder="https://shop.example.com/deal" - required - /> -
- ) : null} -
-
+ +
+ + setSourceUrl(event.target.value)} className="rounded-md border border-slate-300 px-2 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200" - placeholder="Amazon seller" + placeholder="https://shop.example.com/deal" />
@@ -263,20 +187,11 @@ export function ItemPricesPanel({ itemId }: ItemPricesPanelProps) { {price.condition} • {price.amount.toFixed(2)} {price.currency} - {price.isPrimary ? ( - - Primary - - ) : null}
-
-
Source
-
{price.sourceType}
-
{price.sourceUrl ? (
-
URL
+
Source URL
) : null} - {price.sourceNote ? ( -
-
Source Note
-
{price.sourceNote}
-
- ) : null} {price.note ? (
Notes
@@ -302,9 +211,15 @@ export function ItemPricesPanel({ itemId }: ItemPricesPanelProps) {
) : null}
-
Observed
-
{new Date(price.observedAt).toLocaleString()}
+
Created
+
{new Date(price.createdAt).toLocaleString()}
+ {price.updatedAt !== price.createdAt ? ( +
+
Updated
+
{new Date(price.updatedAt).toLocaleString()}
+
+ ) : null}
))} diff --git a/server/src/controllers/item-prices-controller.ts b/server/src/controllers/item-prices-controller.ts index a88732a..556fff5 100644 --- a/server/src/controllers/item-prices-controller.ts +++ b/server/src/controllers/item-prices-controller.ts @@ -43,13 +43,10 @@ export const createItemPrice = async (req: Request, res: Response) => { const itemId = parseUuid(req.params.itemId, 'itemId'); const payload = parseItemPriceCreatePayload(req.body); - let sourceUrlId: string | null = null; - if (payload.sourceType === 'url') { - sourceUrlId = await resolveUrlId({ - sourceUrlId: payload.sourceUrlId, - sourceUrl: payload.sourceUrl - }); - } + const sourceUrlId = await resolveUrlId({ + sourceUrlId: payload.sourceUrlId, + sourceUrl: payload.sourceUrl + }); try { const price = await createItemPriceRepo({ @@ -57,12 +54,8 @@ export const createItemPrice = async (req: Request, res: Response) => { condition: payload.condition, amount: payload.amount, currency: payload.currency, - sourceType: payload.sourceType, sourceUrlId, - sourceNote: payload.sourceNote, - note: payload.note, - observedAt: payload.observedAt, - isPrimary: payload.isPrimary + note: payload.note }); res.status(201).json({ data: price }); @@ -106,9 +99,7 @@ export const updateItemPrice = async (req: Request, res: Response) => { 'sourceUrlId' ); - if (payload.sourceType === 'manual') { - sourceUrlId = null; - } else if (payload.sourceType === 'url' || hasSourceUrlField || hasSourceUrlIdField) { + if (hasSourceUrlField || hasSourceUrlIdField) { sourceUrlId = await resolveUrlId({ sourceUrlId: payload.sourceUrlId ?? null, sourceUrl: payload.sourceUrl ?? null @@ -119,12 +110,8 @@ export const updateItemPrice = async (req: Request, res: Response) => { condition: payload.condition, amount: payload.amount, currency: payload.currency, - sourceType: payload.sourceType, sourceUrlId, - sourceNote: payload.sourceNote, - note: payload.note, - observedAt: payload.observedAt, - isPrimary: payload.isPrimary + note: payload.note }); if (!price) { diff --git a/server/src/controllers/url-helpers.ts b/server/src/controllers/url-helpers.ts index 2ed82a8..f37bdd5 100644 --- a/server/src/controllers/url-helpers.ts +++ b/server/src/controllers/url-helpers.ts @@ -1,4 +1,8 @@ -import { createUrl, findUrlById } from '../db/urls-repository.js'; +import { + createUrl, + findUrlById, + findUrlByValue +} from '../db/urls-repository.js'; import { HttpError } from '../errors/http-error.js'; import { normalizeUrl } from '../utils/url-normalizer.js'; @@ -10,8 +14,14 @@ export const resolveUrlId = async (params: { if (params.sourceUrl) { const normalized = normalizeUrl(params.sourceUrl); - const url = await createUrl({ url: normalized }); - urlId = url.id; + const existing = await findUrlByValue(normalized); + + if (existing) { + urlId = existing.id; + } else { + const url = await createUrl({ url: normalized }); + urlId = url.id; + } } else if (urlId) { const existing = await findUrlById(urlId); if (!existing) { diff --git a/server/src/db/item-prices-repository.ts b/server/src/db/item-prices-repository.ts index 55b735a..fc75ebb 100644 --- a/server/src/db/item-prices-repository.ts +++ b/server/src/db/item-prices-repository.ts @@ -1,4 +1,4 @@ -import type { ItemPrice, PriceCondition, PriceSourceType } from '@shared/models/item-price'; +import type { ItemPrice, PriceCondition } from '@shared/models/item-price'; import { pool, query } from './pool.js'; const priceColumns = ` @@ -7,12 +7,8 @@ const priceColumns = ` p.condition, p.amount, p.currency, - p.source_type, p.source_url_id, - p.source_note, p.note, - p.observed_at, - p.is_primary, p.created_at, p.updated_at, u.url AS source_url @@ -24,12 +20,8 @@ export interface ItemPriceRow { condition: PriceCondition; amount: string; currency: string; - source_type: PriceSourceType; source_url_id: string | null; - source_note: string | null; note: string | null; - observed_at: Date; - is_primary: boolean; created_at: Date; updated_at: Date; source_url: string | null; @@ -43,13 +35,9 @@ const mapItemPriceRow = (row: ItemPriceRow): ItemPriceRecord => ({ condition: row.condition, amount: Number(row.amount), currency: row.currency, - sourceType: row.source_type, sourceUrlId: row.source_url_id, sourceUrl: row.source_url, - sourceNote: row.source_note, note: row.note, - observedAt: row.observed_at.toISOString(), - isPrimary: row.is_primary, createdAt: row.created_at.toISOString(), updatedAt: row.updated_at.toISOString() }); @@ -83,7 +71,7 @@ export const listItemPrices = async ( FROM item_prices p LEFT JOIN urls u ON u.id = p.source_url_id WHERE ${filters.join(' AND ')} - ORDER BY p.observed_at DESC + ORDER BY p.created_at DESC LIMIT $${limitIndex} OFFSET $${offsetIndex} `, @@ -98,12 +86,8 @@ export interface CreateItemPriceParams { condition: PriceCondition; amount: number; currency: string; - sourceType: PriceSourceType; sourceUrlId: string | null; - sourceNote: string | null; note: string | null; - observedAt?: string; - isPrimary: boolean; } export const createItemPrice = async ( @@ -113,17 +97,6 @@ export const createItemPrice = async ( try { await client.query('BEGIN'); - if (params.isPrimary) { - await client.query( - ` - UPDATE item_prices - SET is_primary = FALSE - WHERE item_id = $1 AND condition = $2 - `, - [params.itemId, params.condition] - ); - } - const result = await client.query<{ id: string }>( ` INSERT INTO item_prices ( @@ -131,14 +104,10 @@ export const createItemPrice = async ( condition, amount, currency, - source_type, source_url_id, - source_note, - note, - observed_at, - is_primary + note ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, COALESCE($9, NOW()), $10) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING id `, [ @@ -146,12 +115,8 @@ export const createItemPrice = async ( params.condition, params.amount, params.currency, - params.sourceType, params.sourceUrlId, - params.sourceNote, - params.note, - params.observedAt ?? null, - params.isPrimary + params.note ] ); @@ -202,12 +167,8 @@ export interface UpdateItemPriceParams { condition?: PriceCondition; amount?: number; currency?: string; - sourceType?: PriceSourceType; sourceUrlId?: string | null; - sourceNote?: string | null; note?: string | null; - observedAt?: string; - isPrimary?: boolean; } export const updateItemPrice = async ( @@ -234,7 +195,6 @@ export const updateItemPrice = async ( return null; } - const existing = existingResult.rows[0]; const fields: string[] = []; const values: unknown[] = []; @@ -250,30 +210,14 @@ export const updateItemPrice = async ( values.push(params.currency); fields.push(`currency = $${values.length}`); } - if (params.sourceType !== undefined) { - values.push(params.sourceType); - fields.push(`source_type = $${values.length}`); - } if (params.sourceUrlId !== undefined) { values.push(params.sourceUrlId); fields.push(`source_url_id = $${values.length}`); } - if (params.sourceNote !== undefined) { - values.push(params.sourceNote); - fields.push(`source_note = $${values.length}`); - } if (params.note !== undefined) { values.push(params.note); fields.push(`note = $${values.length}`); } - if (params.observedAt !== undefined) { - values.push(params.observedAt); - fields.push(`observed_at = COALESCE($${values.length}, NOW())`); - } - if (params.isPrimary !== undefined) { - values.push(params.isPrimary); - fields.push(`is_primary = $${values.length}`); - } if (fields.length > 0) { values.push(id); @@ -289,20 +233,6 @@ export const updateItemPrice = async ( ); } - const newCondition = params.condition ?? existing.condition; - const newIsPrimary = params.isPrimary ?? existing.is_primary; - - if (newIsPrimary) { - await client.query( - ` - UPDATE item_prices - SET is_primary = FALSE - WHERE item_id = $1 AND condition = $2 AND id <> $3 - `, - [existing.item_id, newCondition, id] - ); - } - const updatedResult = await client.query( ` SELECT ${priceColumns} diff --git a/server/src/db/items-repository.ts b/server/src/db/items-repository.ts index d94fd0c..331b391 100644 --- a/server/src/db/items-repository.ts +++ b/server/src/db/items-repository.ts @@ -212,7 +212,15 @@ export const getItemById = async (id: string): Promise => { MAX(ip.amount) AS max_amount, COUNT(*)::INT AS total_count, COUNT(DISTINCT ip.currency)::INT AS currency_count, - MIN(ip.currency) AS currency + MIN(ip.currency) AS currency, + MIN(CASE WHEN ip.condition = 'new' THEN ip.amount END) AS min_new_amount, + COUNT(CASE WHEN ip.condition = 'new' THEN 1 END)::INT AS new_count, + COUNT(DISTINCT CASE WHEN ip.condition = 'new' THEN ip.currency END)::INT AS new_currency_count, + MIN(CASE WHEN ip.condition = 'new' THEN ip.currency END) AS new_currency, + MIN(CASE WHEN ip.condition = 'used' THEN ip.amount END) AS min_used_amount, + COUNT(CASE WHEN ip.condition = 'used' THEN 1 END)::INT AS used_count, + COUNT(DISTINCT CASE WHEN ip.condition = 'used' THEN ip.currency END)::INT AS used_currency_count, + MIN(CASE WHEN ip.condition = 'used' THEN ip.currency END) AS used_currency FROM item_prices ip WHERE ip.item_id = i.id ) price_summary ON TRUE diff --git a/server/src/db/migrations/0005-create-item-prices-table.ts b/server/src/db/migrations/0005-create-item-prices-table.ts index 2c5a190..411ff65 100644 --- a/server/src/db/migrations/0005-create-item-prices-table.ts +++ b/server/src/db/migrations/0005-create-item-prices-table.ts @@ -14,15 +14,6 @@ export const migration: Migration = { END$$; `); - await client.query(` - DO $$ - BEGIN - CREATE TYPE price_source AS ENUM ('url', 'manual'); - EXCEPTION - WHEN duplicate_object THEN NULL; - END$$; - `); - await client.query(` CREATE TABLE IF NOT EXISTS item_prices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), @@ -30,19 +21,11 @@ export const migration: Migration = { condition price_condition NOT NULL, amount NUMERIC(12, 2) NOT NULL CHECK (amount >= 0), currency CHAR(3) NOT NULL, - source_type price_source NOT NULL, source_url_id UUID REFERENCES urls(id) ON DELETE SET NULL, - source_note TEXT, note TEXT, - observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - is_primary BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - CHECK (char_length(currency) = 3), - CHECK ( - (source_type = 'manual'::price_source AND source_url_id IS NULL) - OR (source_type = 'url'::price_source AND source_url_id IS NOT NULL) - ) + CHECK (char_length(currency) = 3) ); `); @@ -53,15 +36,5 @@ export const migration: Migration = { await client.query(` CREATE INDEX IF NOT EXISTS item_prices_source_url_id_idx ON item_prices (source_url_id); `); - - await client.query(` - CREATE INDEX IF NOT EXISTS item_prices_observed_at_idx ON item_prices (observed_at); - `); - - await client.query(` - CREATE UNIQUE INDEX IF NOT EXISTS item_prices_primary_condition_idx - ON item_prices (item_id, condition) - WHERE is_primary = TRUE; - `); } }; diff --git a/server/src/middleware/error-handler.ts b/server/src/middleware/error-handler.ts index 7c2adc4..7b96a41 100644 --- a/server/src/middleware/error-handler.ts +++ b/server/src/middleware/error-handler.ts @@ -2,6 +2,7 @@ import type { ErrorRequestHandler } from 'express'; import { HttpError } from '../errors/http-error.js'; export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => { + void _next; if (error instanceof HttpError) { res.status(error.status).json({ error: { diff --git a/server/src/validation/item-prices.ts b/server/src/validation/item-prices.ts index f002ae0..5b657dd 100644 --- a/server/src/validation/item-prices.ts +++ b/server/src/validation/item-prices.ts @@ -1,18 +1,12 @@ -import type { PriceCondition, PriceSourceType } from '@shared/models/item-price'; +import type { PriceCondition } from '@shared/models/item-price'; import { HttpError } from '../errors/http-error.js'; import { parseUuid } from './common.js'; const priceConditions = new Set(['new', 'used']); -const priceSources = new Set(['url', 'manual']); - const isPriceCondition = (value: unknown): value is PriceCondition => { return typeof value === 'string' && priceConditions.has(value as PriceCondition); }; -const isPriceSourceType = (value: unknown): value is PriceSourceType => { - return typeof value === 'string' && priceSources.has(value as PriceSourceType); -}; - const normalizeCurrency = (value: unknown): string => { if (typeof value !== 'string' || value.trim().length !== 3) { throw new HttpError(400, 'currency must be a 3-letter code'); @@ -20,34 +14,13 @@ const normalizeCurrency = (value: unknown): string => { return value.trim().toUpperCase(); }; -const parseObservedAt = (value: unknown): string | undefined => { - if (value === undefined) { - return undefined; - } - if (value === null) { - throw new HttpError(400, 'observedAt cannot be null'); - } - if (typeof value !== 'string') { - throw new HttpError(400, 'observedAt must be an ISO datetime string'); - } - const date = new Date(value); - if (Number.isNaN(date.valueOf())) { - throw new HttpError(400, 'observedAt must be a valid ISO datetime string'); - } - return date.toISOString(); -}; - export interface ItemPriceCreateInput { condition: PriceCondition; amount: number; currency: string; - sourceType: PriceSourceType; sourceUrlId: string | null; sourceUrl: string | null; - sourceNote: string | null; note: string | null; - observedAt: string | undefined; - isPrimary: boolean; } export type ItemPriceUpdateInput = Partial; @@ -59,12 +32,19 @@ const parseAmount = (value: unknown): number => { return value; }; -const parseSourceNote = (value: unknown): string | null => { +const parseSourceUrlId = (value: unknown): string | null => { if (value === undefined || value === null) { return null; } - if (typeof value !== 'string') { - throw new HttpError(400, 'sourceNote must be a string or null'); + return parseUuid(value, 'sourceUrlId'); +}; + +const parseSourceUrl = (value: unknown): string | null => { + if (value === undefined || value === null) { + return null; + } + if (typeof value !== 'string' || value.trim().length === 0) { + throw new HttpError(400, 'sourceUrl must be a non-empty string'); } return value; }; @@ -79,60 +59,6 @@ const parseNote = (value: unknown): string | null => { return value; }; -const parseIsPrimary = (value: unknown): boolean => { - if (value === undefined) { - return false; - } - if (typeof value !== 'boolean') { - throw new HttpError(400, 'isPrimary must be a boolean'); - } - return value; -}; - -const parseSourceFields = ( - sourceType: PriceSourceType, - sourceUrlIdValue: unknown, - sourceUrlValue: unknown -): { sourceUrlId: string | null; sourceUrl: string | null } => { - if (sourceType === 'manual') { - if (sourceUrlIdValue !== undefined && sourceUrlIdValue !== null) { - throw new HttpError( - 400, - 'sourceUrlId cannot be provided when sourceType is manual' - ); - } - if (sourceUrlValue !== undefined && sourceUrlValue !== null) { - throw new HttpError( - 400, - 'sourceUrl cannot be provided when sourceType is manual' - ); - } - return { sourceUrlId: null, sourceUrl: null }; - } - - let sourceUrlId: string | null = null; - if (sourceUrlIdValue !== undefined && sourceUrlIdValue !== null) { - sourceUrlId = parseUuid(sourceUrlIdValue, 'sourceUrlId'); - } - - let sourceUrl: string | null = null; - if (sourceUrlValue !== undefined && sourceUrlValue !== null) { - if (typeof sourceUrlValue !== 'string' || sourceUrlValue.trim().length === 0) { - throw new HttpError(400, 'sourceUrl must be a non-empty string'); - } - sourceUrl = sourceUrlValue; - } - - if (!sourceUrlId && !sourceUrl) { - throw new HttpError( - 400, - 'sourceUrl or sourceUrlId must be provided when sourceType is url' - ); - } - - return { sourceUrlId, sourceUrl }; -}; - export const parseItemPriceCreatePayload = ( payload: unknown ): ItemPriceCreateInput => { @@ -144,40 +70,22 @@ export const parseItemPriceCreatePayload = ( condition, amount, currency, - sourceType, sourceUrlId, sourceUrl, - sourceNote, - note, - observedAt, - isPrimary + note } = payload as Record; if (!isPriceCondition(condition)) { throw new HttpError(400, 'condition must be one of new, used'); } - if (!isPriceSourceType(sourceType)) { - throw new HttpError(400, 'sourceType must be one of url, manual'); - } - - const source = parseSourceFields( - sourceType, - sourceUrlId, - sourceUrl - ); - return { condition, amount: parseAmount(amount), currency: normalizeCurrency(currency), - sourceType, - sourceUrlId: source.sourceUrlId, - sourceUrl: source.sourceUrl, - sourceNote: parseSourceNote(sourceNote), - note: parseNote(note), - observedAt: parseObservedAt(observedAt), - isPrimary: parseIsPrimary(isPrimary) + sourceUrlId: parseSourceUrlId(sourceUrlId), + sourceUrl: parseSourceUrl(sourceUrl), + note: parseNote(note) }; }; @@ -193,13 +101,9 @@ export const parseItemPriceUpdatePayload = ( condition, amount, currency, - sourceType, sourceUrlId, sourceUrl, - sourceNote, - note, - observedAt, - isPrimary + note } = payload as Record; if (condition !== undefined) { @@ -217,42 +121,18 @@ export const parseItemPriceUpdatePayload = ( result.currency = normalizeCurrency(currency); } - if (sourceType !== undefined) { - if (!isPriceSourceType(sourceType)) { - throw new HttpError(400, 'sourceType must be one of url, manual'); - } - result.sourceType = sourceType; + if (sourceUrlId !== undefined) { + result.sourceUrlId = parseSourceUrlId(sourceUrlId); } - if (sourceUrlId !== undefined || sourceUrl !== undefined) { - const type = result.sourceType ?? (isPriceSourceType(sourceType) ? sourceType : undefined); - if (!type) { - throw new HttpError( - 400, - 'sourceType must be provided when changing sourceUrl or sourceUrlId' - ); - } - const parsed = parseSourceFields(type, sourceUrlId, sourceUrl); - result.sourceUrlId = parsed.sourceUrlId; - result.sourceUrl = parsed.sourceUrl; - } - - if (sourceNote !== undefined) { - result.sourceNote = parseSourceNote(sourceNote); + if (sourceUrl !== undefined) { + result.sourceUrl = parseSourceUrl(sourceUrl); } if (note !== undefined) { result.note = parseNote(note); } - if (observedAt !== undefined) { - result.observedAt = parseObservedAt(observedAt); - } - - if (isPrimary !== undefined) { - result.isPrimary = parseIsPrimary(isPrimary); - } - if (Object.keys(result).length === 0) { throw new HttpError(400, 'At least one field must be provided for update'); } diff --git a/shared/models/item-price.d.ts b/shared/models/item-price.d.ts new file mode 100644 index 0000000..ed89604 --- /dev/null +++ b/shared/models/item-price.d.ts @@ -0,0 +1,14 @@ +export type PriceCondition = 'new' | 'used'; + +export interface ItemPrice { + id: string; + itemId: string; + condition: PriceCondition; + amount: number; + currency: string; + sourceUrlId: string | null; + sourceUrl: string | null; + note: string | null; + createdAt: string; + updatedAt: string; +} diff --git a/shared/models/item.d.ts b/shared/models/item.d.ts new file mode 100644 index 0000000..fd082fc --- /dev/null +++ b/shared/models/item.d.ts @@ -0,0 +1,32 @@ +export type ItemStatus = 'active' | 'rejected'; + +export interface ItemPriceSummary { + minAmount: number; + maxAmount: number; + currency: string | null; + priceCount: number; + hasMixedCurrency: boolean; + newMinAmount: number | null; + newCount: number; + newCurrency: string | null; + newHasMixedCurrency: boolean; + usedMinAmount: number | null; + usedCount: number; + usedCurrency: string | null; + usedHasMixedCurrency: boolean; +} + +export interface Item { + id: string; + projectId: string; + manufacturer: string | null; + model: string; + sourceUrlId: string | null; + sourceUrl: string | null; + status: ItemStatus; + note: string | null; + attributes: Record; + createdAt: string; + updatedAt: string; + priceSummary: ItemPriceSummary | null; +} diff --git a/shared/models/pagination.d.ts b/shared/models/pagination.d.ts new file mode 100644 index 0000000..0547210 --- /dev/null +++ b/shared/models/pagination.d.ts @@ -0,0 +1,10 @@ +export interface PaginationMeta { + limit: number; + offset: number; + count: number; +} + +export interface PaginatedResponse { + data: T[]; + meta: PaginationMeta; +} diff --git a/shared/models/project.d.ts b/shared/models/project.d.ts new file mode 100644 index 0000000..6bf58d7 --- /dev/null +++ b/shared/models/project.d.ts @@ -0,0 +1,12 @@ +export type ProjectStatus = 'active' | 'archived'; + +export interface Project { + id: string; + name: string; + description: string | null; + status: ProjectStatus; + attributes: string[]; + priorityRules: unknown; + createdAt: string; + updatedAt: string; +}