diff --git a/apps/api/app/api/v1/users/[id]/positions/route.ts b/apps/api/app/api/v1/users/[id]/positions/route.ts index e0398251..33c77d18 100644 --- a/apps/api/app/api/v1/users/[id]/positions/route.ts +++ b/apps/api/app/api/v1/users/[id]/positions/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server' import type { SchemaResponse } from '@play-money/api-helpers' -import { getPositions } from '@play-money/finance/lib/getPositions' import { getUserById } from '@play-money/users/lib/getUserById' +import { getUserPositions } from '@play-money/users/lib/getUserPositions' import schema from './schema' export const dynamic = 'force-dynamic' @@ -15,24 +15,13 @@ export async function GET( const searchParams = new URLSearchParams(url.search) const urlParams = Object.fromEntries(searchParams) - const { id, pageSize, status } = schema.get.parameters.parse({ ...(params || {}), ...urlParams }) + const { id: userId, status, ...paginationParams } = schema.get.parameters.parse({ ...(params || {}), ...urlParams }) - const user = await getUserById({ id }) + await getUserById({ id: userId }) - const { positions } = await getPositions( - { - accountId: user.primaryAccountId, - status, - }, - { field: 'updatedAt', direction: 'desc' }, - { take: pageSize ?? 25, skip: 0 } - ) + const results = await getUserPositions({ userId, status }, paginationParams) - return NextResponse.json({ - data: { - positions, - }, - }) + return NextResponse.json(results) } catch (error) { console.log(error) // eslint-disable-line no-console -- Log error for debugging diff --git a/apps/api/app/api/v1/users/[id]/positions/schema.ts b/apps/api/app/api/v1/users/[id]/positions/schema.ts index a6a71278..5d38fb2d 100644 --- a/apps/api/app/api/v1/users/[id]/positions/schema.ts +++ b/apps/api/app/api/v1/users/[id]/positions/schema.ts @@ -1,20 +1,23 @@ import { z } from 'zod' -import { ApiEndpoints, ServerErrorSchema } from '@play-money/api-helpers' -import { MarketOptionPositionSchema, UserSchema } from '@play-money/database' +import { + ApiEndpoints, + createPaginatedResponseSchema, + paginationSchema, + ServerErrorSchema, +} from '@play-money/api-helpers' +import { MarketOptionPositionSchema } from '@play-money/database' export default { get: { summary: 'Get positions for a user', - parameters: UserSchema.pick({ id: true }).extend({ - pageSize: z.coerce.number().optional(), - status: z.enum(['active', 'closed', 'all']).optional(), - }), + parameters: z + .object({ + id: z.string(), + status: z.enum(['active', 'closed', 'all']).optional(), + }) + .merge(paginationSchema), responses: { - 200: z.object({ - data: z.object({ - positions: z.array(MarketOptionPositionSchema), - }), - }), + 200: createPaginatedResponseSchema(MarketOptionPositionSchema), 404: ServerErrorSchema, 500: ServerErrorSchema, }, diff --git a/apps/api/openapi.json b/apps/api/openapi.json index 80cb2f1f..af674877 100644 --- a/apps/api/openapi.json +++ b/apps/api/openapi.json @@ -108,12 +108,6 @@ ] } }, - "pageSize": { - "type": [ - "number", - "null" - ] - }, "username": { "type": "string" }, @@ -265,14 +259,6 @@ "name": "transactionType", "in": "path" }, - "pageSize": { - "schema": { - "$ref": "#/components/schemas/pageSize" - }, - "required": false, - "name": "pageSize", - "in": "path" - }, "username": { "schema": { "$ref": "#/components/schemas/username" @@ -2159,6 +2145,106 @@ } } }, + "/v1/lists/[id]/graph": { + "get": { + "summary": "Get the graph for a List of Markets", + "parameters": [ + { + "$ref": "#/components/parameters/id" + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "startAt": { + "type": "string" + }, + "endAt": { + "type": "string" + }, + "markets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "probability": { + "type": "number" + } + }, + "required": [ + "id", + "probability" + ] + } + } + }, + "required": [ + "startAt", + "endAt", + "markets" + ] + } + } + }, + "required": [ + "data" + ] + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + } + } + } + } + } + }, "/v1/lists/[id]/markets": { "post": { "summary": "Create a market in a list", @@ -6949,10 +7035,19 @@ "$ref": "#/components/parameters/id" }, { - "$ref": "#/components/parameters/pageSize" + "$ref": "#/components/parameters/status" }, { - "$ref": "#/components/parameters/status" + "$ref": "#/components/parameters/cursor" + }, + { + "$ref": "#/components/parameters/limit" + }, + { + "$ref": "#/components/parameters/sortField" + }, + { + "$ref": "#/components/parameters/sortDirection" } ], "responses": { @@ -6964,60 +7059,71 @@ "type": "object", "properties": { "data": { - "type": "object", - "properties": { - "positions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "cuid" - }, - "accountId": { - "type": "string" - }, - "marketId": { - "type": "string" - }, - "optionId": { - "type": "string" - }, - "cost": {}, - "quantity": {}, - "value": {}, - "createdAt": { - "type": [ - "string", - "null" - ] - }, - "updatedAt": { - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "id", - "accountId", - "marketId", - "optionId", - "createdAt", - "updatedAt" + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "cuid" + }, + "accountId": { + "type": "string" + }, + "marketId": { + "type": "string" + }, + "optionId": { + "type": "string" + }, + "cost": {}, + "quantity": {}, + "value": {}, + "createdAt": { + "type": [ + "string", + "null" + ] + }, + "updatedAt": { + "type": [ + "string", + "null" ] } + }, + "required": [ + "id", + "accountId", + "marketId", + "optionId", + "createdAt", + "updatedAt" + ] + } + }, + "pageInfo": { + "type": "object", + "properties": { + "hasNextPage": { + "type": "boolean" + }, + "endCursor": { + "type": "string" + }, + "total": { + "type": "number" } }, "required": [ - "positions" + "hasNextPage", + "total" ] } }, "required": [ - "data" + "data", + "pageInfo" ] } } diff --git a/packages/api-helpers/client/index.ts b/packages/api-helpers/client/index.ts index 30b20c2e..c5ce1a8b 100644 --- a/packages/api-helpers/client/index.ts +++ b/packages/api-helpers/client/index.ts @@ -488,13 +488,24 @@ export async function getUserTransactions({ userId }: { userId: string }) { export async function getUserPositions({ userId, - pageSize, + status, + ...paginationParams }: { userId: string - pageSize?: number -}): Promise<{ data: { positions: Array } }> { - return apiHandler<{ data: { positions: Array } }>( - `${process.env.NEXT_PUBLIC_API_URL}/v1/users/${userId}/positions${pageSize ? `?pageSize=${pageSize}` : ''}` + status?: 'active' | 'closed' | 'all' +} & PaginationRequest): Promise> { + const currentParams = new URLSearchParams( + JSON.parse( + JSON.stringify({ + status, + ...paginationParams, + }) + ) + ) + const search = currentParams.toString() + + return apiHandler>( + `${process.env.NEXT_PUBLIC_API_URL}/v1/users/${userId}/positions${search ? `?${search}` : ''}` ) } diff --git a/packages/finance/types.ts b/packages/finance/types.ts index 204c574d..e21c2316 100644 --- a/packages/finance/types.ts +++ b/packages/finance/types.ts @@ -1,4 +1,12 @@ -import { Market, MarketOption, MarketOptionPosition, Transaction, TransactionEntry, User } from '@play-money/database' +import { + Market, + MarketOption, + MarketOptionPosition, + Transaction, + TransactionEntry, + User, + Account, +} from '@play-money/database' export type TransactionEntryInput = Pick< TransactionEntry, @@ -24,4 +32,7 @@ export type LeaderboardUser = { export type ExtendedMarketOptionPosition = MarketOptionPosition & { market: Market option: MarketOption + account: Account & { + user: User + } } diff --git a/packages/users/components/UserProfilePage.tsx b/packages/users/components/UserProfilePage.tsx index 539525b0..6ef7daf8 100644 --- a/packages/users/components/UserProfilePage.tsx +++ b/packages/users/components/UserProfilePage.tsx @@ -199,8 +199,8 @@ async function UserPositionsTab({ sortDirection?: 'asc' | 'desc' } }) { - const { data: marketPositions, pageInfo } = await getMarketPositions({ - ownerId: userId, + const { data: marketPositions, pageInfo } = await getUserPositions({ + userId, ...filters, status: filters?.status ?? 'active', }) @@ -226,9 +226,7 @@ export async function UserProfilePage({ } }) { const { data: user } = await getUserUsername({ username }) - const { - data: { positions }, - } = await getUserPositions({ userId: user.id, pageSize: 5 }) + const { data: positions } = await getUserPositions({ userId: user.id, limit: 5 }) const { data: markets } = await getUserMarkets({ userId: user.id }) return ( diff --git a/packages/users/lib/getUserPositions.ts b/packages/users/lib/getUserPositions.ts new file mode 100644 index 00000000..ce69b06d --- /dev/null +++ b/packages/users/lib/getUserPositions.ts @@ -0,0 +1,49 @@ +import Decimal from 'decimal.js' +import { getPaginatedItems, PaginationRequest } from '@play-money/api-helpers' +import db, { MarketOptionPosition } from '@play-money/database' + +interface MarketPositionFilterOptions { + status?: 'active' | 'closed' | 'all' + userId?: string +} + +export async function getUserPositions(filters: MarketPositionFilterOptions = {}, pagination?: PaginationRequest) { + const statusFilters = + filters.status === 'active' + ? { + quantity: { + gt: new Decimal(0.0001), + }, + } + : filters.status === 'closed' + ? { + quantity: { + lt: new Decimal(0.0001), + }, + } + : filters.status === 'all' + ? {} + : {} + + return getPaginatedItems({ + model: db.marketOptionPosition, + pagination: pagination ?? {}, + where: { + ...statusFilters, + account: { + userPrimary: { + id: filters.userId, + }, + }, + }, + include: { + account: { + include: { + user: true, + }, + }, + market: true, + option: true, + }, + }) +}