diff --git a/constants/index.ts b/constants/index.ts index de1daea1..af63a13f 100644 --- a/constants/index.ts +++ b/constants/index.ts @@ -104,7 +104,10 @@ export const RESPONSE_MESSAGES = { INVALID_EMAIL_400: { statusCode: 400, message: 'Invalid email.' }, USER_OUT_OF_EMAIL_CREDITS_400: { statusCode: 400, message: 'User out of email credits.' }, USER_OUT_OF_POST_CREDITS_400: { statusCode: 400, message: 'User out of post credits.' }, - NO_INVOICE_FOUND_404: { statusCode: 404, message: 'No invoice found.' } + NO_INVOICE_FOUND_404: { statusCode: 404, message: 'No invoice found.' }, + INVALID_FIELDS_FORMAT_400: { statusCode: 400, message: "'fields' must be a valid JSON array." }, + INVALID_FIELD_STRUCTURE_400: { statusCode: 400, message: "Each field must have 'name' property as string." }, + INVALID_AMOUNT_400: { statusCode: 400, message: "'amount' must be a valid positive number." } } export const SOCKET_MESSAGES = { diff --git a/pages/api/payments/paymentId/index.ts b/pages/api/payments/paymentId/index.ts index 9e070c73..8dbde388 100644 --- a/pages/api/payments/paymentId/index.ts +++ b/pages/api/payments/paymentId/index.ts @@ -1,4 +1,3 @@ -import { Decimal } from '@prisma/client/runtime/library' import { generatePaymentId } from 'services/clientPaymentService' import { parseAddress, parseCreatePaymentIdPOSTRequest } from 'utils/validators' import { RESPONSE_MESSAGES } from 'constants/index' @@ -14,11 +13,10 @@ export default async (req: any, res: any): Promise => { await runMiddleware(req, res, cors) if (req.method === 'POST') { try { - const values = parseCreatePaymentIdPOSTRequest(req.body) - const address = parseAddress(values.address) - const amount = values.amount as Decimal | undefined + const { amount, fields, address } = parseCreatePaymentIdPOSTRequest(req.body) + const parsedAddress = parseAddress(address) - const paymentId = await generatePaymentId(address, amount) + const paymentId = await generatePaymentId(parsedAddress, amount, fields) res.status(200).json({ paymentId }) } catch (error: any) { @@ -29,6 +27,15 @@ export default async (req: any, res: any): Promise => { case RESPONSE_MESSAGES.INVALID_ADDRESS_400.message: res.status(RESPONSE_MESSAGES.INVALID_ADDRESS_400.statusCode).json(RESPONSE_MESSAGES.INVALID_ADDRESS_400) break + case RESPONSE_MESSAGES.INVALID_FIELDS_FORMAT_400.message: + res.status(RESPONSE_MESSAGES.INVALID_FIELDS_FORMAT_400.statusCode).json(RESPONSE_MESSAGES.INVALID_FIELDS_FORMAT_400) + break + case RESPONSE_MESSAGES.INVALID_FIELD_STRUCTURE_400.message: + res.status(RESPONSE_MESSAGES.INVALID_FIELD_STRUCTURE_400.statusCode).json(RESPONSE_MESSAGES.INVALID_FIELD_STRUCTURE_400) + break + case RESPONSE_MESSAGES.INVALID_AMOUNT_400.message: + res.status(RESPONSE_MESSAGES.INVALID_AMOUNT_400.statusCode).json(RESPONSE_MESSAGES.INVALID_AMOUNT_400) + break default: res.status(500).json({ statusCode: 500, message: error.message }) } diff --git a/prisma-local/migrations/20251228021700_client_payment_fields/migration.sql b/prisma-local/migrations/20251228021700_client_payment_fields/migration.sql new file mode 100644 index 00000000..885eb307 --- /dev/null +++ b/prisma-local/migrations/20251228021700_client_payment_fields/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `ClientPayment` ADD COLUMN `fields` LONGTEXT NOT NULL DEFAULT '[]'; diff --git a/prisma-local/schema.prisma b/prisma-local/schema.prisma index dab9ffb1..68290e54 100644 --- a/prisma-local/schema.prisma +++ b/prisma-local/schema.prisma @@ -286,6 +286,7 @@ model ClientPayment { addressString String amount Decimal? address Address @relation(fields: [addressString], references: [address]) + fields String @db.LongText @default("[]") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } diff --git a/services/clientPaymentService.ts b/services/clientPaymentService.ts index 4668ad04..0af437d7 100644 --- a/services/clientPaymentService.ts +++ b/services/clientPaymentService.ts @@ -7,7 +7,14 @@ import { parseAddress } from 'utils/validators' import { addressExists } from './addressService' import moment from 'moment' -export const generatePaymentId = async (address: string, amount?: Prisma.Decimal): Promise => { +export interface ClientPaymentField { + name: string + text?: string + type?: string + value?: string | boolean +} + +export const generatePaymentId = async (address: string, amount?: Prisma.Decimal, fields?: ClientPaymentField[]): Promise => { const rawUUID = uuidv4() const cleanUUID = rawUUID.replace(/-/g, '') const status = 'PENDING' as ClientPaymentStatus @@ -29,7 +36,8 @@ export const generatePaymentId = async (address: string, amount?: Prisma.Decimal }, paymentId: cleanUUID, status, - amount + amount, + fields: fields !== undefined ? JSON.stringify(fields) : '[]' }, include: { address: true @@ -57,6 +65,28 @@ export const getClientPayment = async (paymentId: string): Promise => { + const clientPayment = await prisma.clientPayment.findUnique({ + where: { paymentId }, + select: { fields: true } + }) + if (clientPayment === null) { + return [] + } + try { + return JSON.parse(clientPayment.fields) as ClientPaymentField[] + } catch { + return [] + } +} + +export const updateClientPaymentFields = async (paymentId: string, fields: ClientPaymentField[]): Promise => { + await prisma.clientPayment.update({ + where: { paymentId }, + data: { fields: JSON.stringify(fields) } + }) +} + export const cleanupExpiredClientPayments = async (): Promise => { const cutoff = moment.utc().subtract(CLIENT_PAYMENT_EXPIRATION_TIME, 'milliseconds').toDate() diff --git a/utils/validators.ts b/utils/validators.ts index a802add9..65f25978 100644 --- a/utils/validators.ts +++ b/utils/validators.ts @@ -10,6 +10,7 @@ import crypto from 'crypto' import { getUserPrivateKey } from '../services/userService' import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library' import moment from 'moment-timezone' +import { ClientPaymentField } from 'services/clientPaymentService' /* The functions exported here should validate the data structure / syntax of an * input by throwing an error in case something is different from the expected. @@ -574,10 +575,70 @@ export interface CreateInvoicePOSTParameters { export interface CreatePaymentIdPOSTParameters { address?: string amount?: string + fields?: string } +export interface ClientPaymentFieldInput { + name?: string + text?: string + type?: string + value?: string | boolean +} + export interface CreatePaymentIdInput { address: string - amount?: string + amount?: Prisma.Decimal + fields?: ClientPaymentField[] +} + +export const parseClientPaymentFields = function (fieldsInput: string | undefined): ClientPaymentField[] | undefined { + if (fieldsInput === undefined || fieldsInput === '') { + return undefined + } + + let parsedFields: unknown + try { + parsedFields = JSON.parse(fieldsInput) + } catch { + throw new Error(RESPONSE_MESSAGES.INVALID_FIELDS_FORMAT_400.message) + } + + if (!Array.isArray(parsedFields)) { + throw new Error(RESPONSE_MESSAGES.INVALID_FIELDS_FORMAT_400.message) + } + + for (const field of parsedFields) { + if ( + typeof field !== 'object' || + field === null || + typeof field.name !== 'string' || + field.name?.trim() === '' + ) { + throw new Error(RESPONSE_MESSAGES.INVALID_FIELD_STRUCTURE_400.message) + } + if (field.type !== undefined && typeof field.type !== 'string') { + throw new Error(RESPONSE_MESSAGES.INVALID_FIELD_STRUCTURE_400.message) + } + if (field.value !== undefined && typeof field.value !== 'string' && typeof field.value !== 'boolean') { + throw new Error(RESPONSE_MESSAGES.INVALID_FIELD_STRUCTURE_400.message) + } + } + + return parsedFields as ClientPaymentField[] +} + +export const parseAmount = function (amountInput: string | undefined): Prisma.Decimal | undefined { + if (amountInput === undefined || amountInput === '') { + return undefined + } + + const trimmedAmount = amountInput.trim() + const numericAmount = Number(trimmedAmount) + + if (isNaN(numericAmount) || numericAmount <= 0) { + throw new Error(RESPONSE_MESSAGES.INVALID_AMOUNT_400.message) + } + + return new Prisma.Decimal(trimmedAmount) } export const parseCreatePaymentIdPOSTRequest = function (params: CreatePaymentIdPOSTParameters): CreatePaymentIdInput { @@ -588,8 +649,12 @@ export const parseCreatePaymentIdPOSTRequest = function (params: CreatePaymentId throw new Error(RESPONSE_MESSAGES.ADDRESS_NOT_PROVIDED_400.message) } + const amount = parseAmount(params.amount) + const fields = parseClientPaymentFields(params.fields) + return { address: params.address, - amount: params.amount === '' ? undefined : params.amount + amount, + fields } }