diff --git a/modules/market/market.models.ts b/modules/market/market.models.ts index c86378f..143db19 100644 --- a/modules/market/market.models.ts +++ b/modules/market/market.models.ts @@ -1,5 +1,6 @@ +import { optional } from 'zod'; import * as mongo from '../../util/mongo'; -import type * as Types from './market.types'; +import * as Types from './market.types'; export const Market = mongo.createModel('Market', { value: { type: String, required: true }, @@ -111,3 +112,14 @@ export const MarketStockSentiment = mongo.createModel('MarketListing', { + sellerId: { type: mongo.Schema.Types.ObjectId, required: true, ref: 'Profile' }, + quantity: { type: Number, required: true }, + currency: { type: String, required: true }, + exchange: { type: String, required: true }, + marketId: { type: mongo.Schema.Types.ObjectId, required: true, ref: 'MarketStock' }, + category: { type: String, required: true }, + status: { type: String, required: true }, + expiryDate: { type: Date, required: false }, +}); diff --git a/modules/market/market.router.ts b/modules/market/market.router.ts index 94d1db8..3e0cca9 100644 --- a/modules/market/market.router.ts +++ b/modules/market/market.router.ts @@ -4,13 +4,21 @@ import { z as zod } from 'zod'; import { initTRPC, inferRouterInputs, inferRouterOutputs } from '@trpc/server'; import { customErrorFormatter, hasRole } from '../../util/rpc'; import type { RouterContext } from '../../types'; -import { Market, MarketPair, MarketExchange } from './market.schema'; +import { Market, MarketPair, MarketExchange, MarketListing } from './market.schema'; +import { Query, getQueryInput, getQueryOutput } from '../../schema'; export const z = zod; export const t = initTRPC.context().create(); export const router = t.router; export const procedure = t.procedure; +const MarketListingsInput = z.object({ + category: z.string().optional(), + status: z.string().optional(), + exchange: z.string().optional(), + sellerId: z.string().optional(), +}); + export const createRouter = () => router({ getMarket: procedure @@ -66,6 +74,36 @@ export const createRouter = () => .use(customErrorFormatter(t)) .input(z.object({ exchangeId: z.string(), data: MarketExchange.partial() })) .mutation(({ input, ctx }) => (ctx.app.service.Market.updateMarketExchange as any)(input, ctx)), + + getMarketListing: procedure + .use(hasRole('guest', t)) + .use(customErrorFormatter(t)) + .input(getQueryInput(z.object({ listingId: z.string() }))) + .query(({ input, ctx }) => (ctx.app.service.Market.getMarketListing as any)(input, ctx)), + + createMarketListing: procedure + .use(hasRole('user', t)) + .use(customErrorFormatter(t)) + .input(getQueryInput(MarketListing)) + .mutation(({ input, ctx }) => (ctx.app.service.Market.createMarketListing as any)(input, ctx)), + + updateMarketListing: procedure + .use(hasRole('user', t)) + .use(customErrorFormatter(t)) + .input(getQueryInput(MarketListing.partial())) + .mutation(({ input, ctx }) => (ctx.app.service.Market.updateMarketListing as any)(input, ctx)), + + deleteMarketListing: procedure + .use(hasRole('user', t)) + .use(customErrorFormatter(t)) + .input(getQueryInput(z.object({ listingId: z.string() }))) + .mutation(({ input, ctx }) => (ctx.app.service.Market.deleteMarketListing as any)(input, ctx)), + + getMarketListings: procedure + .use(hasRole('guest', t)) + .use(customErrorFormatter(t)) + .input(getQueryInput(MarketListingsInput)) + .query(({ input, ctx }) => (ctx.app.service.Market.getMarketListings as any)(input, ctx)), }); export type Router = ReturnType; diff --git a/modules/market/market.schema.ts b/modules/market/market.schema.ts index c506cfa..d5e014e 100644 --- a/modules/market/market.schema.ts +++ b/modules/market/market.schema.ts @@ -138,3 +138,17 @@ export const MarketStockSentiment = Entity.merge( confidence: z.number().min(0).max(1), // overall confidence level for the analysis }) ); + +export const MarketListing = Entity.merge( + z.object({ + price: z.number(), + currency: z.string(), + quantity: z.number(), + exchange: z.string(), + sellerId: ObjectId, + marketId: ObjectId, + category: z.enum(['Stock', 'ChainToken']), + status: z.enum(['Active', 'Closed', 'Withdrawn', 'Expired']), + expiryDate: z.date().optional(), + }) +); diff --git a/modules/market/market.service.ts b/modules/market/market.service.ts index 4f60662..ddbd672 100644 --- a/modules/market/market.service.ts +++ b/modules/market/market.service.ts @@ -4,11 +4,14 @@ import type { Market, MarketPair, MarketExchange, + MarketListing, RouterContext, Router, RouterInput, RouterOutput, } from './market.types'; +import { getFilter } from '../../util/api'; +import { ARXError } from '../../util/rpc'; export class Service { async getMarket(input: RouterInput['getMarket'], ctx: RouterContext): Promise { @@ -119,4 +122,79 @@ export class Service { return updatedMarketExchange as MarketExchange; } + + async getMarketListing( + input: RouterInput['getMarketListing'], + ctx: RouterContext + ): Promise { + if (!input) throw new ARXError('NO_INPUT'); + console.log('Market.Service.getMarketListing', input); + + const listing = await ctx.app.model.MarketListing.findOne(getFilter(input)).exec(); + if (!listing) throw new Error('MarketListing not found'); + + return listing as MarketListing; + } + + async createMarketListing( + input: RouterInput['createMarketListing'], + ctx: RouterContext + ): Promise { + if (!input) throw new ARXError('NO_INPUT'); + console.log('Market.Service.createMarketListing', input); + + const marketListing = await ctx.app.model.MarketListing.create(input); + return marketListing as MarketListing; + } + + async updateMarketListing( + input: RouterInput['updateMarketListing'], + ctx: RouterContext + ): Promise { + if (!input) throw new ARXError('NO_INPUT'); + console.log('Market.Service.updateMarketListing', input); + + const updatedListing = await ctx.app.model.MarketListing.findByIdAndUpdate(input.where.id.equals, input.data, { + new: true, + }) + .lean() + .exec(); + if (!updatedListing) throw new Error('MarketListing update failed'); + + return updatedListing as MarketListing; + } + + async deleteMarketListing( + input: RouterInput['deleteMarketListing'], + ctx: RouterContext + ): Promise { + if (!input) throw new ARXError('NO_INPUT'); + console.log('Market.Service.deleteMarketListing', input); + + const deletedListing = await ctx.app.model.MarketListing.findByIdAndDelete(input.where.id.quals).lean().exec(); + if (!deletedListing) throw new Error('MarketListing deletion failed'); + + return deletedListing as MarketListing; + } + + async getMarketListings( + input: { + category?: string; + status?: string; + exchange?: string; + sellerId?: string; + }, + ctx: RouterContext + ): Promise { + console.log('Market.Service.getMarketListings', input); + + const query: Record = {}; + if (input.category) query.category = input.category; + if (input.status) query.status = input.status; + if (input.exchange) query.exchange = input.exchange; + if (input.sellerId) query.sellerId = input.sellerId; + + const listings = await ctx.app.model.MarketListing.find(query).lean().exec(); + return listings as MarketListing[]; + } } diff --git a/modules/market/market.types.ts b/modules/market/market.types.ts index d79c5dd..b11ca73 100644 --- a/modules/market/market.types.ts +++ b/modules/market/market.types.ts @@ -23,6 +23,7 @@ export type MarketToken = z.infer; export type MarketCompany = z.infer; export type MarketETF = z.infer; export type MarketStockSentiment = z.infer; +export type MarketListing = z.infer; export type MarketDocument = Market & Document; export type MarketPairDocument = MarketPair & Document; @@ -36,6 +37,7 @@ export type MarketTokenDocument = MarketToken & Document; export type MarketCompanyDocument = MarketCompany & Document; export type MarketETFDocument = MarketETF & Document; export type MarketStockSentimentDocument = MarketStockSentiment & Document; +export type MarketListingDocument = MarketListing & Document; export type Mappings = { Market: Model; @@ -50,6 +52,7 @@ export type Mappings = { MarketCompany: Model; MarketETF: Model; MarketStockSentiment: Model; + MarketListing: Model; }; export type RouterInput = inferRouterInputs;