1
0

Basic transaction system

This commit is contained in:
Ahmed Al-Taiar
2023-11-14 18:54:44 -05:00
parent f6f01594ec
commit 8060e1e452
60 changed files with 2037 additions and 507 deletions

View File

@@ -0,0 +1,58 @@
export const schema = gql`
type Transaction {
id: Int!
date: DateTime!
user: User!
userId: Int!
type: TransactionType!
parts: [JSON]!
}
enum TransactionType {
in
out
}
enum FilterTransactionsByType {
in
out
both
}
type UserTransactions {
transactions: [Transaction!]!
filter: FilterTransactionsByType!
}
type Query {
transactions(filter: FilterTransactionsByType!): UserTransactions!
@requireAuth(roles: "admin")
transaction(id: Int!): Transaction @requireAuth(roles: "admin")
userTransactions(
userId: Int!
filter: FilterTransactionsByType!
): UserTransactions! @requireAuth
}
input CreateTransactionInput {
date: DateTime!
userId: Int!
type: TransactionType!
parts: [JSON]!
}
input UpdateTransactionInput {
date: DateTime
userId: Int
type: TransactionType
parts: [JSON]!
}
type Mutation {
createTransaction(input: CreateTransactionInput!): Transaction! @requireAuth
returnTransaction(id: Int!, userId: Int!): Transaction! @requireAuth
updateTransaction(id: Int!, input: UpdateTransactionInput!): Transaction!
@requireAuth(roles: "admin")
deleteTransaction(id: Int!): Transaction! @requireAuth(roles: "admin")
}
`

View File

@@ -0,0 +1,48 @@
export const schema = gql`
type User {
id: Int!
firstName: String!
lastName: String!
email: String!
hashedPassword: String!
salt: String!
resetToken: String
resetTokenExpiresAt: DateTime
roles: String!
transactions: [Transaction]!
}
type Query {
users: [User!]! @requireAuth(roles: "admin")
user(id: Int!): User @requireAuth(roles: "admin")
}
input CreateUserInput {
firstName: String!
lastName: String!
email: String!
hashedPassword: String!
salt: String!
resetToken: String
resetTokenExpiresAt: DateTime
roles: String!
}
input UpdateUserInput {
firstName: String
lastName: String
email: String
hashedPassword: String
salt: String
resetToken: String
resetTokenExpiresAt: DateTime
roles: String
}
type Mutation {
createUser(input: CreateUserInput!): User! @requireAuth(roles: "admin")
updateUser(id: Int!, input: UpdateUserInput!): User!
@requireAuth(roles: "admin")
deleteUser(id: Int!): User! @requireAuth(roles: "admin")
}
`

View File

@@ -27,7 +27,7 @@ export const getCurrentUser = async (session: Decoded) => {
return await db.user.findUnique({
where: { id: session.id },
select: { id: true, firstName: true, roles: true },
select: { id: true, firstName: true, roles: true, transactions: true },
})
}

View File

@@ -77,12 +77,22 @@ export const partPage = ({
}
export const createPart: MutationResolvers['createPart'] = ({ input }) => {
input.description =
input.description.length == 0
? 'No description provided'
: input.description
return db.part.create({
data: input,
})
}
export const updatePart: MutationResolvers['updatePart'] = ({ id, input }) => {
input.description =
input.description.length == 0
? 'No description provided'
: input.description
return db.part.update({
data: input,
where: { id },

View File

@@ -0,0 +1,39 @@
import type { Prisma, Transaction } from '@prisma/client'
import type { ScenarioData } from '@redwoodjs/testing/api'
export const standard = defineScenario<Prisma.TransactionCreateArgs>({
transaction: {
one: {
data: {
type: 'in',
parts: { foo: 'bar' },
user: {
create: {
firstName: 'String',
lastName: 'String',
email: 'String2420106',
hashedPassword: 'String',
salt: 'String',
},
},
},
},
two: {
data: {
type: 'in',
parts: { foo: 'bar' },
user: {
create: {
firstName: 'String',
lastName: 'String',
email: 'String1596322',
hashedPassword: 'String',
salt: 'String',
},
},
},
},
},
})
export type StandardScenario = ScenarioData<Transaction, 'transaction'>

View File

@@ -0,0 +1,68 @@
import type { Transaction } from '@prisma/client'
import {
transactions,
transaction,
createTransaction,
updateTransaction,
deleteTransaction,
} from './transactions'
import type { StandardScenario } from './transactions.scenarios'
// Generated boilerplate tests do not account for all circumstances
// and can fail without adjustments, e.g. Float.
// Please refer to the RedwoodJS Testing Docs:
// https://redwoodjs.com/docs/testing#testing-services
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
describe('transactions', () => {
scenario('returns all transactions', async (scenario: StandardScenario) => {
const result = await transactions()
expect(result.length).toEqual(Object.keys(scenario.transaction).length)
})
scenario(
'returns a single transaction',
async (scenario: StandardScenario) => {
const result = await transaction({ id: scenario.transaction.one.id })
expect(result).toEqual(scenario.transaction.one)
}
)
scenario('creates a transaction', async (scenario: StandardScenario) => {
const result = await createTransaction({
input: {
userId: scenario.transaction.two.userId,
type: 'in',
parts: { foo: 'bar' },
},
})
expect(result.userId).toEqual(scenario.transaction.two.userId)
expect(result.type).toEqual('in')
expect(result.parts).toEqual({ foo: 'bar' })
})
scenario('updates a transaction', async (scenario: StandardScenario) => {
const original = (await transaction({
id: scenario.transaction.one.id,
})) as Transaction
const result = await updateTransaction({
id: original.id,
input: { type: 'out' },
})
expect(result.type).toEqual('out')
})
scenario('deletes a transaction', async (scenario: StandardScenario) => {
const original = (await deleteTransaction({
id: scenario.transaction.one.id,
})) as Transaction
const result = await transaction({ id: original.id })
expect(result).toEqual(null)
})
})

View File

@@ -0,0 +1,158 @@
import type {
QueryResolvers,
MutationResolvers,
TransactionRelationResolvers,
Part,
} from 'types/graphql'
import { UserInputError } from '@redwoodjs/graphql-server'
import { db } from 'src/lib/db'
export const transactions: QueryResolvers['transactions'] = async ({
filter,
}) => {
const transactions =
filter == 'both'
? await db.transaction.findMany({
orderBy: {
date: 'desc',
},
})
: await db.transaction.findMany({
where: {
type: filter,
},
orderBy: {
date: 'desc',
},
})
return {
transactions,
filter,
}
}
export const userTransactions: QueryResolvers['userTransactions'] = async ({
userId,
filter,
}) => {
return {
transactions:
filter == 'both'
? await db.transaction.findMany({
where: {
userId,
},
orderBy: {
date: 'desc',
},
})
: await db.transaction.findMany({
where: {
AND: {
userId,
type: filter,
},
},
orderBy: {
date: 'desc',
},
}),
filter,
}
}
export const transaction: QueryResolvers['transaction'] = ({ id }) => {
return db.transaction.findUnique({
where: { id },
})
}
export const returnTransaction: MutationResolvers['returnTransaction'] =
async ({ id, userId }) => {
const transaction = await db.transaction.findUnique({ where: { id } })
if (transaction.type == 'out' && userId == transaction.userId) {
for (const partRaw of transaction.parts) {
const transactionPart = JSON.parse(partRaw['part']) as Part
const part = await db.part.findUnique({
where: { id: transactionPart.id },
})
await db.part.update({
where: { id: part.id },
data: {
availableStock: {
increment: partRaw['quantity'],
},
},
})
}
return await db.transaction.update({
where: { id: transaction.id },
data: { type: 'in' },
})
} else return transaction
}
export const createTransaction: MutationResolvers['createTransaction'] =
async ({ input }) => {
const basket = input.parts.map((item) => {
const part: Part = JSON.parse(item['part']) as Part
return { part: part.id, quantity: item['quantity'] }
})
for (const item of basket) {
const part = await db.part.findUnique({ where: { id: item.part } })
if (!part) throw new UserInputError(`Part ${item.part} does not exist`)
if (input.type == 'out') {
if (part.availableStock < item.quantity)
throw new UserInputError(
`Cannot take out more than available stock (${part.availableStock} available, ${item.quantity} requested)`
)
await db.part.update({
where: { id: item.part },
data: { availableStock: { decrement: item.quantity } },
})
} else if (input.type == 'in') {
await db.part.update({
where: { id: item.part },
data: { availableStock: { increment: item.quantity } },
})
}
}
return db.transaction.create({
data: input,
})
}
export const updateTransaction: MutationResolvers['updateTransaction'] = ({
id,
input,
}) => {
return db.transaction.update({
data: input,
where: { id },
})
}
export const deleteTransaction: MutationResolvers['deleteTransaction'] = ({
id,
}) => {
return db.transaction.delete({
where: { id },
})
}
export const Transaction: TransactionRelationResolvers = {
user: (_obj, { root }) => {
return db.transaction.findUnique({ where: { id: root?.id } }).user()
},
}

View File

@@ -0,0 +1,27 @@
import type { Prisma, User } from '@prisma/client'
import type { ScenarioData } from '@redwoodjs/testing/api'
export const standard = defineScenario<Prisma.UserCreateArgs>({
user: {
one: {
data: {
firstName: 'String',
lastName: 'String',
email: 'String6743396',
hashedPassword: 'String',
salt: 'String',
},
},
two: {
data: {
firstName: 'String',
lastName: 'String',
email: 'String6958264',
hashedPassword: 'String',
salt: 'String',
},
},
},
})
export type StandardScenario = ScenarioData<User, 'user'>

View File

@@ -0,0 +1,59 @@
import type { User } from '@prisma/client'
import { users, user, createUser, updateUser, deleteUser } from './users'
import type { StandardScenario } from './users.scenarios'
// Generated boilerplate tests do not account for all circumstances
// and can fail without adjustments, e.g. Float.
// Please refer to the RedwoodJS Testing Docs:
// https://redwoodjs.com/docs/testing#testing-services
// https://redwoodjs.com/docs/testing#jest-expect-type-considerations
describe('users', () => {
scenario('returns all users', async (scenario: StandardScenario) => {
const result = await users()
expect(result.length).toEqual(Object.keys(scenario.user).length)
})
scenario('returns a single user', async (scenario: StandardScenario) => {
const result = await user({ id: scenario.user.one.id })
expect(result).toEqual(scenario.user.one)
})
scenario('creates a user', async () => {
const result = await createUser({
input: {
firstName: 'String',
lastName: 'String',
email: 'String3921297',
hashedPassword: 'String',
salt: 'String',
},
})
expect(result.firstName).toEqual('String')
expect(result.lastName).toEqual('String')
expect(result.email).toEqual('String3921297')
expect(result.hashedPassword).toEqual('String')
expect(result.salt).toEqual('String')
})
scenario('updates a user', async (scenario: StandardScenario) => {
const original = (await user({ id: scenario.user.one.id })) as User
const result = await updateUser({
id: original.id,
input: { firstName: 'String2' },
})
expect(result.firstName).toEqual('String2')
})
scenario('deletes a user', async (scenario: StandardScenario) => {
const original = (await deleteUser({ id: scenario.user.one.id })) as User
const result = await user({ id: original.id })
expect(result).toEqual(null)
})
})

View File

@@ -0,0 +1,42 @@
import type {
QueryResolvers,
MutationResolvers,
UserRelationResolvers,
} from 'types/graphql'
import { db } from 'src/lib/db'
export const users: QueryResolvers['users'] = () => {
return db.user.findMany()
}
export const user: QueryResolvers['user'] = ({ id }) => {
return db.user.findUnique({
where: { id },
})
}
export const createUser: MutationResolvers['createUser'] = ({ input }) => {
return db.user.create({
data: input,
})
}
export const updateUser: MutationResolvers['updateUser'] = ({ id, input }) => {
return db.user.update({
data: input,
where: { id },
})
}
export const deleteUser: MutationResolvers['deleteUser'] = ({ id }) => {
return db.user.delete({
where: { id },
})
}
export const User: UserRelationResolvers = {
transactions: (_obj, { root }) => {
return db.user.findUnique({ where: { id: root?.id } }).transactions()
},
}