Scaffold Apollo backend domain API
This commit is contained in:
553
src/resolvers.js
Normal file
553
src/resolvers.js
Normal file
@@ -0,0 +1,553 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
import { dateTimeScalar, jsonScalar } from './scalars.js';
|
||||
|
||||
const ACTIVE_ORDER_STATUSES = ['NEW', 'MANAGER_PROCESSING', 'WAITING_DOUBLE_CONFIRM', 'CONFIRMED', 'IN_PROGRESS'];
|
||||
|
||||
function toFloat(value) {
|
||||
return value == null ? null : Number(value);
|
||||
}
|
||||
|
||||
function requireUser(context) {
|
||||
if (!context.user) {
|
||||
throw new Error('Authentication required. Pass x-user-id header.');
|
||||
}
|
||||
return context.user;
|
||||
}
|
||||
|
||||
function requireRole(context, role) {
|
||||
const user = requireUser(context);
|
||||
if (user.role !== role) {
|
||||
throw new Error(`Only ${role} can perform this operation.`);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async function appendOrderEvent(prisma, orderId, status, actorUserId, note = null) {
|
||||
return prisma.orderStatusEvent.create({
|
||||
data: {
|
||||
orderId,
|
||||
status,
|
||||
actorUserId,
|
||||
note,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function orderCode() {
|
||||
return `FR-${Date.now()}-${crypto.randomInt(1000, 9999)}`;
|
||||
}
|
||||
|
||||
function invitationToken() {
|
||||
return crypto.randomBytes(24).toString('hex');
|
||||
}
|
||||
|
||||
export const resolvers = {
|
||||
DateTime: dateTimeScalar,
|
||||
JSON: jsonScalar,
|
||||
|
||||
Query: {
|
||||
healthcheck: () => 'ok',
|
||||
|
||||
me: (_, __, context) => context.user,
|
||||
|
||||
clientProducts: (_, __, context) =>
|
||||
context.prisma.product.findMany({
|
||||
where: { isActive: true },
|
||||
include: {
|
||||
inventory: {
|
||||
include: { warehouse: true },
|
||||
},
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
}),
|
||||
|
||||
myOrders: (_, __, context) => {
|
||||
const user = requireUser(context);
|
||||
return context.prisma.order.findMany({
|
||||
where: user.role === 'MANAGER' ? { managerId: user.id } : { customerId: user.id },
|
||||
include: {
|
||||
items: true,
|
||||
history: { orderBy: { createdAt: 'desc' } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
},
|
||||
|
||||
myCurrentOrders: (_, __, context) => {
|
||||
const user = requireUser(context);
|
||||
return context.prisma.order.findMany({
|
||||
where: {
|
||||
...(user.role === 'MANAGER' ? { managerId: user.id } : { customerId: user.id }),
|
||||
status: { in: ACTIVE_ORDER_STATUSES },
|
||||
},
|
||||
include: {
|
||||
items: true,
|
||||
history: { orderBy: { createdAt: 'desc' } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
},
|
||||
|
||||
managerOrders: (_, { status }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
return context.prisma.order.findMany({
|
||||
where: {
|
||||
managerId: manager.id,
|
||||
...(status ? { status } : {}),
|
||||
},
|
||||
include: {
|
||||
items: true,
|
||||
history: { orderBy: { createdAt: 'desc' } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
},
|
||||
|
||||
registrationRequests: (_, { status }, context) => {
|
||||
requireRole(context, 'MANAGER');
|
||||
return context.prisma.registrationRequest.findMany({
|
||||
where: status ? { status } : undefined,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
},
|
||||
|
||||
referralStats: async (_, __, context) => {
|
||||
const user = requireUser(context);
|
||||
const [links, transactions, pendingWithdrawals] = await Promise.all([
|
||||
context.prisma.referralLink.count({ where: { referrerId: user.id } }),
|
||||
context.prisma.bonusTransaction.findMany({
|
||||
where: { userId: user.id },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
context.prisma.rewardWithdrawalRequest.findMany({
|
||||
where: {
|
||||
requesterId: user.id,
|
||||
status: 'PENDING',
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
]);
|
||||
|
||||
const txSum = transactions.reduce((acc, tx) => acc + Number(tx.amount), 0);
|
||||
const pendingSum = pendingWithdrawals.reduce((acc, tx) => acc + Number(tx.amount), 0);
|
||||
|
||||
return {
|
||||
referrerId: user.id,
|
||||
availableBalance: txSum - pendingSum,
|
||||
referralsCount: links,
|
||||
transactions,
|
||||
pendingWithdrawals,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
Mutation: {
|
||||
registerSelf: (_, { input }, context) =>
|
||||
context.prisma.registrationRequest.create({
|
||||
data: {
|
||||
companyName: input.companyName,
|
||||
inn: input.inn,
|
||||
contactName: input.contactName,
|
||||
email: input.email,
|
||||
status: 'PENDING',
|
||||
},
|
||||
}),
|
||||
|
||||
reviewRegistrationRequest: async (_, { input }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
return context.prisma.registrationRequest.update({
|
||||
where: { id: input.requestId },
|
||||
data: {
|
||||
status: input.decision === 'APPROVE' ? 'APPROVED' : 'REJECTED',
|
||||
rejectionReason: input.decision === 'REJECT' ? input.rejectionReason ?? 'Rejected by manager' : null,
|
||||
reviewedById: manager.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
createInvitation: async (_, { input }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
const expiresInDays = input.expiresInDays > 0 ? input.expiresInDays : 7;
|
||||
const expiresAt = new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000);
|
||||
|
||||
return context.prisma.invitation.create({
|
||||
data: {
|
||||
token: invitationToken(),
|
||||
email: input.email,
|
||||
companyName: input.companyName,
|
||||
managerId: manager.id,
|
||||
expiresAt,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
acceptInvitation: async (_, { input }, context) => {
|
||||
const invitation = await context.prisma.invitation.findUnique({ where: { token: input.token } });
|
||||
if (!invitation) {
|
||||
throw new Error('Invitation token is invalid.');
|
||||
}
|
||||
if (invitation.acceptedAt) {
|
||||
throw new Error('Invitation has already been used.');
|
||||
}
|
||||
if (invitation.expiresAt < new Date()) {
|
||||
throw new Error('Invitation has expired.');
|
||||
}
|
||||
|
||||
const company = await context.prisma.company.upsert({
|
||||
where: { name: invitation.companyName },
|
||||
update: {},
|
||||
create: { name: invitation.companyName },
|
||||
});
|
||||
|
||||
const user = await context.prisma.user.create({
|
||||
data: {
|
||||
email: invitation.email,
|
||||
fullName: input.fullName,
|
||||
role: 'CLIENT',
|
||||
companyId: company.id,
|
||||
},
|
||||
});
|
||||
|
||||
await context.prisma.invitation.update({
|
||||
where: { id: invitation.id },
|
||||
data: {
|
||||
acceptedById: user.id,
|
||||
acceptedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return user;
|
||||
},
|
||||
|
||||
connectMessenger: (_, { input }, context) => {
|
||||
const user = requireUser(context);
|
||||
return context.prisma.messengerConnection.upsert({
|
||||
where: {
|
||||
userId_type_channelId: {
|
||||
userId: user.id,
|
||||
type: input.type,
|
||||
channelId: input.channelId,
|
||||
},
|
||||
},
|
||||
update: { isActive: true },
|
||||
create: {
|
||||
userId: user.id,
|
||||
type: input.type,
|
||||
channelId: input.channelId,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
submitReadyOrder: async (_, { input }, context) => {
|
||||
const customer = requireRole(context, 'CLIENT');
|
||||
if (!input.items.length) {
|
||||
throw new Error('Order must contain at least one item.');
|
||||
}
|
||||
|
||||
const productIds = input.items.map((item) => item.productId);
|
||||
const products = await context.prisma.product.findMany({
|
||||
where: { id: { in: productIds }, isActive: true },
|
||||
});
|
||||
|
||||
if (products.length !== input.items.length) {
|
||||
throw new Error('Some products are invalid or inactive.');
|
||||
}
|
||||
|
||||
const productMap = new Map(products.map((product) => [product.id, product]));
|
||||
|
||||
const order = await context.prisma.order.create({
|
||||
data: {
|
||||
code: orderCode(),
|
||||
kind: 'READY',
|
||||
customerId: customer.id,
|
||||
status: 'NEW',
|
||||
items: {
|
||||
create: input.items.map((item) => {
|
||||
const product = productMap.get(item.productId);
|
||||
return {
|
||||
productId: item.productId,
|
||||
productName: product.name,
|
||||
quantity: item.quantity,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
items: true,
|
||||
history: true,
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(context.prisma, order.id, 'NEW', customer.id, 'Ready order created by client');
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: order.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
submitCalculationOrder: async (_, { input }, context) => {
|
||||
const customer = requireRole(context, 'CLIENT');
|
||||
const order = await context.prisma.order.create({
|
||||
data: {
|
||||
code: orderCode(),
|
||||
kind: 'CALCULATION',
|
||||
customerId: customer.id,
|
||||
status: 'NEW',
|
||||
calculationPayload: input.parameters,
|
||||
items: {
|
||||
create: [
|
||||
{
|
||||
productName: input.productName,
|
||||
quantity: input.quantity,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
items: true,
|
||||
history: true,
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(context.prisma, order.id, 'NEW', customer.id, 'Calculation request created by client');
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: order.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
managerSetOrderOffer: async (_, { input }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
const order = await context.prisma.order.update({
|
||||
where: { id: input.orderId },
|
||||
data: {
|
||||
managerId: manager.id,
|
||||
status: 'WAITING_DOUBLE_CONFIRM',
|
||||
deliveryTerms: input.deliveryTerms,
|
||||
deliveryFee: input.deliveryFee,
|
||||
totalPrice: input.totalPrice,
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(context.prisma, order.id, 'WAITING_DOUBLE_CONFIRM', manager.id, 'Offer is published by manager');
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: order.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
clientReviewOrder: async (_, { orderId, decision }, context) => {
|
||||
const customer = requireRole(context, 'CLIENT');
|
||||
const order = await context.prisma.order.findUnique({ where: { id: orderId } });
|
||||
|
||||
if (!order || order.customerId !== customer.id) {
|
||||
throw new Error('Order is not available for this client.');
|
||||
}
|
||||
|
||||
const status = decision === 'REJECT'
|
||||
? 'CLIENT_REJECTED'
|
||||
: order.managerApproved
|
||||
? 'CONFIRMED'
|
||||
: 'WAITING_DOUBLE_CONFIRM';
|
||||
|
||||
const updated = await context.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
clientApproved: decision === 'APPROVE',
|
||||
status,
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(
|
||||
context.prisma,
|
||||
updated.id,
|
||||
status,
|
||||
customer.id,
|
||||
decision === 'APPROVE' ? 'Client approved offer' : 'Client rejected offer',
|
||||
);
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: updated.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
managerFinalizeOrder: async (_, { orderId, decision }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
const order = await context.prisma.order.findUnique({ where: { id: orderId } });
|
||||
if (!order) {
|
||||
throw new Error('Order was not found.');
|
||||
}
|
||||
|
||||
const status = decision === 'REJECT'
|
||||
? 'MANAGER_REJECTED'
|
||||
: order.clientApproved
|
||||
? 'CONFIRMED'
|
||||
: 'WAITING_DOUBLE_CONFIRM';
|
||||
|
||||
const updated = await context.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
managerId: manager.id,
|
||||
managerApproved: decision === 'APPROVE',
|
||||
status,
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(
|
||||
context.prisma,
|
||||
updated.id,
|
||||
status,
|
||||
manager.id,
|
||||
decision === 'APPROVE' ? 'Manager approved order' : 'Manager rejected order',
|
||||
);
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: updated.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
blockOrder: async (_, { input }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
const updated = await context.prisma.order.update({
|
||||
where: { id: input.orderId },
|
||||
data: {
|
||||
managerId: manager.id,
|
||||
status: 'MANAGER_BLOCKED',
|
||||
blockReason: input.reason,
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(context.prisma, updated.id, 'MANAGER_BLOCKED', manager.id, input.reason);
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: updated.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
startOrderWork: async (_, { orderId }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
const order = await context.prisma.order.findUnique({ where: { id: orderId } });
|
||||
if (!order || order.status !== 'CONFIRMED') {
|
||||
throw new Error('Only confirmed order can be started.');
|
||||
}
|
||||
|
||||
const updated = await context.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
managerId: manager.id,
|
||||
status: 'IN_PROGRESS',
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(context.prisma, updated.id, 'IN_PROGRESS', manager.id, 'Order moved to in-progress');
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: updated.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
completeOrder: async (_, { orderId }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
const order = await context.prisma.order.findUnique({ where: { id: orderId } });
|
||||
if (!order || order.status !== 'IN_PROGRESS') {
|
||||
throw new Error('Only in-progress order can be completed.');
|
||||
}
|
||||
|
||||
const updated = await context.prisma.order.update({
|
||||
where: { id: orderId },
|
||||
data: {
|
||||
managerId: manager.id,
|
||||
status: 'COMPLETED',
|
||||
},
|
||||
});
|
||||
|
||||
await appendOrderEvent(context.prisma, updated.id, 'COMPLETED', manager.id, 'Order completed');
|
||||
|
||||
return context.prisma.order.findUnique({
|
||||
where: { id: updated.id },
|
||||
include: { items: true, history: { orderBy: { createdAt: 'desc' } } },
|
||||
});
|
||||
},
|
||||
|
||||
createReferral: (_, { input }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
return context.prisma.referralLink.create({
|
||||
data: {
|
||||
referrerId: manager.id,
|
||||
refereeId: input.refereeUserId,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
addBonusTransaction: (_, { input }, context) => {
|
||||
requireRole(context, 'MANAGER');
|
||||
return context.prisma.bonusTransaction.create({
|
||||
data: {
|
||||
userId: input.userId,
|
||||
amount: input.amount,
|
||||
reason: input.reason,
|
||||
orderId: input.orderId,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
requestRewardWithdrawal: (_, { input }, context) => {
|
||||
const client = requireRole(context, 'CLIENT');
|
||||
if (input.amount < 100) {
|
||||
throw new Error('Minimum withdrawal amount is 100.');
|
||||
}
|
||||
|
||||
return context.prisma.rewardWithdrawalRequest.create({
|
||||
data: {
|
||||
requesterId: client.id,
|
||||
amount: input.amount,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
reviewRewardWithdrawal: (_, { input }, context) => {
|
||||
const manager = requireRole(context, 'MANAGER');
|
||||
return context.prisma.rewardWithdrawalRequest.update({
|
||||
where: { id: input.withdrawalId },
|
||||
data: {
|
||||
reviewedById: manager.id,
|
||||
status: input.decision === 'APPROVE' ? 'APPROVED' : 'REJECTED',
|
||||
reviewComment: input.reviewComment,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
Product: {
|
||||
availableInWarehouses: (product) =>
|
||||
product.inventory.map((stock) => ({
|
||||
warehouse: stock.warehouse,
|
||||
availableQty: toFloat(stock.availableQty),
|
||||
})),
|
||||
},
|
||||
|
||||
Order: {
|
||||
deliveryFee: (order) => toFloat(order.deliveryFee),
|
||||
totalPrice: (order) => toFloat(order.totalPrice),
|
||||
},
|
||||
|
||||
OrderItem: {
|
||||
quantity: (item) => toFloat(item.quantity),
|
||||
},
|
||||
|
||||
BonusTransaction: {
|
||||
amount: (tx) => toFloat(tx.amount),
|
||||
},
|
||||
|
||||
RewardWithdrawalRequest: {
|
||||
amount: (tx) => toFloat(tx.amount),
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user