Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
17 changes: 12 additions & 5 deletions pages/api/payments/paymentId/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -14,11 +13,10 @@ export default async (req: any, res: any): Promise<void> => {
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) {
Expand All @@ -29,6 +27,15 @@ export default async (req: any, res: any): Promise<void> => {
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 })
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `ClientPayment` ADD COLUMN `fields` LONGTEXT NOT NULL DEFAULT '[]';
1 change: 1 addition & 0 deletions prisma-local/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
34 changes: 32 additions & 2 deletions services/clientPaymentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> => {
export interface ClientPaymentField {
name: string
text?: string
type?: string
value?: string | boolean
}

export const generatePaymentId = async (address: string, amount?: Prisma.Decimal, fields?: ClientPaymentField[]): Promise<string> => {
const rawUUID = uuidv4()
const cleanUUID = rawUUID.replace(/-/g, '')
const status = 'PENDING' as ClientPaymentStatus
Expand All @@ -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
Expand Down Expand Up @@ -57,6 +65,28 @@ export const getClientPayment = async (paymentId: string): Promise<Prisma.Client
})
}

export const getClientPaymentFields = async (paymentId: string): Promise<ClientPaymentField[]> => {
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<void> => {
await prisma.clientPayment.update({
where: { paymentId },
data: { fields: JSON.stringify(fields) }
})
}

export const cleanupExpiredClientPayments = async (): Promise<void> => {
const cutoff = moment.utc().subtract(CLIENT_PAYMENT_EXPIRATION_TIME, 'milliseconds').toDate()

Expand Down
76 changes: 74 additions & 2 deletions utils/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -574,10 +575,77 @@ export interface CreateInvoicePOSTParameters {
export interface CreatePaymentIdPOSTParameters {
address?: string
amount?: string
fields?: string
}
export interface ClientPaymentFieldInput {
name?: string
text?: string
type?: string
value?: string | boolean
}
Comment on lines +580 to +585
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n 'ClientPaymentFieldInput' --type=ts -g '!utils/validators.ts'

Repository: PayButton/paybutton-server

Length of output: 52


Remove unused ClientPaymentFieldInput interface.

This interface is not referenced anywhere in the codebase. The parseClientPaymentFields function validates unknown input directly and casts to ClientPaymentField[], making ClientPaymentFieldInput dead code. Remove it unless it's intentionally exposed for external API documentation or client-side typing.

🤖 Prompt for AI Agents
In `@utils/validators.ts` around lines 580 - 585, Remove the dead
ClientPaymentFieldInput interface: delete the interface declaration for
ClientPaymentFieldInput and any export that references it, since
parseClientPaymentFields validates unknown directly and code uses
ClientPaymentField[]; also search for and remove any re-exports or type
references to ClientPaymentFieldInput (or replace with ClientPaymentField where
appropriate). If the type was intentionally exposed for external consumers,
instead update parseClientPaymentFields signature or module exports to expose
ClientPaymentField and add a brief comment in place of the removed interface
documenting expected shape.


export interface CreatePaymentIdInput {
address: string
amount?: string
amount?: Prisma.Decimal
fields?: ClientPaymentField[]
}

export const parseClientPaymentFields = function (fieldsInput: string | object | undefined): ClientPaymentField[] | undefined {
if (fieldsInput === undefined || fieldsInput === '') {
return undefined
}

let parsedFields: unknown
try {
if (typeof fieldsInput === 'object') {
parsedFields = fieldsInput
} else {
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.value !== undefined && typeof field.value !== 'string' && typeof field.value !== 'boolean') {
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.text !== undefined && typeof field.text !== 'string') {
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) || !isFinite(numericAmount) || numericAmount <= 0) {
throw new Error(RESPONSE_MESSAGES.INVALID_AMOUNT_400.message)
}

return new Prisma.Decimal(trimmedAmount)
}

export const parseCreatePaymentIdPOSTRequest = function (params: CreatePaymentIdPOSTParameters): CreatePaymentIdInput {
Expand All @@ -588,8 +656,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
}
}