Scaffold Apollo backend domain API

This commit is contained in:
Ruslan Bakiev
2026-03-30 21:41:06 +07:00
parent 498b800bc4
commit 6a6c6bfdbb
16 changed files with 4539 additions and 1 deletions

553
src/resolvers.js Normal file
View 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),
},
};