Scaffold Apollo backend domain API
This commit is contained in:
9
.env.example
Normal file
9
.env.example
Normal file
@@ -0,0 +1,9 @@
|
||||
PORT=4000
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/fregat
|
||||
|
||||
VAULT_ENABLED=auto
|
||||
VAULT_ADDR=
|
||||
VAULT_TOKEN=
|
||||
VAULT_KV_MOUNT=secret
|
||||
VAULT_SHARED_PATH=
|
||||
VAULT_PROJECT_PATH=
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
.env
|
||||
.output
|
||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
||||
FROM node:22-bookworm-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update -y && apt-get install -y --no-install-recommends openssl curl jq ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY package*.json ./
|
||||
COPY prisma ./prisma
|
||||
COPY prisma.config.ts ./
|
||||
RUN npm ci && npx prisma generate
|
||||
|
||||
COPY src ./src
|
||||
COPY scripts ./scripts
|
||||
|
||||
CMD ["sh", "-c", ". /app/scripts/load-vault-env.sh && npx prisma migrate deploy && node src/server.js"]
|
||||
33
README.md
33
README.md
@@ -1,2 +1,33 @@
|
||||
# apollo-backend
|
||||
# Fregat Apollo Backend
|
||||
|
||||
GraphQL backend for Fregat client cabinet and manager cabinet.
|
||||
|
||||
## Main capabilities
|
||||
|
||||
- Client self-registration requests and manager review flow.
|
||||
- Invite-based registration flow.
|
||||
- Catalog and stock by warehouses.
|
||||
- Ready-order and calculation-order flows.
|
||||
- Dual approval flow (client + manager), blocking and lifecycle statuses.
|
||||
- Referral links, bonus transactions and withdrawal requests.
|
||||
|
||||
## Local run
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
npm ci
|
||||
npm run prisma:generate
|
||||
npm run prisma:push
|
||||
npm run seed
|
||||
npm run dev
|
||||
```
|
||||
|
||||
GraphQL endpoint: `http://localhost:4000/graphql`
|
||||
|
||||
## Auth model (temporary)
|
||||
|
||||
Pass `x-user-id` header with an existing user id.
|
||||
|
||||
## Deploy
|
||||
|
||||
Container entrypoint loads env from Vault and then applies `prisma migrate deploy` before start.
|
||||
|
||||
3083
package-lock.json
generated
Normal file
3083
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
package.json
Normal file
36
package.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "fregat-apollo-backend",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node src/server.js",
|
||||
"dev": "node --watch src/server.js",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate deploy",
|
||||
"prisma:push": "prisma db push",
|
||||
"seed": "node scripts/seed.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"private": "true",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@apollo/server": "^5.5.0",
|
||||
"@as-integrations/express5": "^1.1.2",
|
||||
"@prisma/adapter-pg": "^7.6.0",
|
||||
"@prisma/client": "^7.6.0",
|
||||
"body-parser": "^2.2.2",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"graphql": "^16.13.2",
|
||||
"pg": "^8.20.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "^7.6.0"
|
||||
}
|
||||
}
|
||||
13
prisma.config.ts
Normal file
13
prisma.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'dotenv/config';
|
||||
|
||||
import { defineConfig, env } from 'prisma/config';
|
||||
|
||||
export default defineConfig({
|
||||
schema: 'prisma/schema.prisma',
|
||||
migrations: {
|
||||
path: 'prisma/migrations',
|
||||
},
|
||||
datasource: {
|
||||
url: env('DATABASE_URL'),
|
||||
},
|
||||
});
|
||||
233
prisma/schema.prisma
Normal file
233
prisma/schema.prisma
Normal file
@@ -0,0 +1,233 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
enum UserRole {
|
||||
CLIENT
|
||||
MANAGER
|
||||
}
|
||||
|
||||
enum RegistrationStatus {
|
||||
PENDING
|
||||
APPROVED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
enum MessengerType {
|
||||
TELEGRAM
|
||||
MAX
|
||||
}
|
||||
|
||||
enum OrderKind {
|
||||
READY
|
||||
CALCULATION
|
||||
}
|
||||
|
||||
enum OrderStatus {
|
||||
NEW
|
||||
MANAGER_PROCESSING
|
||||
WAITING_DOUBLE_CONFIRM
|
||||
CLIENT_REJECTED
|
||||
MANAGER_REJECTED
|
||||
MANAGER_BLOCKED
|
||||
CONFIRMED
|
||||
IN_PROGRESS
|
||||
COMPLETED
|
||||
}
|
||||
|
||||
enum WithdrawalStatus {
|
||||
PENDING
|
||||
APPROVED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
model Company {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
inn String? @unique
|
||||
users User[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
fullName String
|
||||
role UserRole
|
||||
companyId String?
|
||||
company Company? @relation(fields: [companyId], references: [id])
|
||||
registrationRequests RegistrationRequest[] @relation("RegistrationRequester")
|
||||
reviewedRequests RegistrationRequest[] @relation("RegistrationReviewer")
|
||||
sentInvitations Invitation[] @relation("InvitationManager")
|
||||
acceptedInvitations Invitation[] @relation("InvitationAcceptor")
|
||||
messengerConnections MessengerConnection[]
|
||||
clientOrders Order[] @relation("OrderClient")
|
||||
managerOrders Order[] @relation("OrderManager")
|
||||
orderStatusEvents OrderStatusEvent[]
|
||||
referralAsReferrer ReferralLink[] @relation("ReferralReferrer")
|
||||
referralAsReferee ReferralLink[] @relation("ReferralReferee")
|
||||
bonusTransactions BonusTransaction[]
|
||||
withdrawalRequests RewardWithdrawalRequest[] @relation("WithdrawalRequester")
|
||||
reviewedWithdrawals RewardWithdrawalRequest[] @relation("WithdrawalReviewer")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model RegistrationRequest {
|
||||
id String @id @default(cuid())
|
||||
companyName String
|
||||
inn String?
|
||||
contactName String
|
||||
email String
|
||||
status RegistrationStatus @default(PENDING)
|
||||
rejectionReason String?
|
||||
requesterId String?
|
||||
requester User? @relation("RegistrationRequester", fields: [requesterId], references: [id])
|
||||
reviewedById String?
|
||||
reviewedBy User? @relation("RegistrationReviewer", fields: [reviewedById], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Invitation {
|
||||
id String @id @default(cuid())
|
||||
token String @unique
|
||||
email String
|
||||
companyName String
|
||||
managerId String
|
||||
manager User @relation("InvitationManager", fields: [managerId], references: [id])
|
||||
acceptedById String?
|
||||
acceptedBy User? @relation("InvitationAcceptor", fields: [acceptedById], references: [id])
|
||||
expiresAt DateTime
|
||||
acceptedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model MessengerConnection {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
type MessengerType
|
||||
channelId String
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([userId, type, channelId])
|
||||
}
|
||||
|
||||
model Product {
|
||||
id String @id @default(cuid())
|
||||
sku String @unique
|
||||
name String
|
||||
description String?
|
||||
isCustomizable Boolean @default(false)
|
||||
isActive Boolean @default(true)
|
||||
inventory ProductStock[]
|
||||
orderItems OrderItem[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Warehouse {
|
||||
id String @id @default(cuid())
|
||||
code String @unique
|
||||
name String
|
||||
inventory ProductStock[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ProductStock {
|
||||
id String @id @default(cuid())
|
||||
productId String
|
||||
product Product @relation(fields: [productId], references: [id])
|
||||
warehouseId String
|
||||
warehouse Warehouse @relation(fields: [warehouseId], references: [id])
|
||||
availableQty Decimal @db.Decimal(14, 3)
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([productId, warehouseId])
|
||||
}
|
||||
|
||||
model Order {
|
||||
id String @id @default(cuid())
|
||||
code String @unique
|
||||
kind OrderKind
|
||||
customerId String
|
||||
customer User @relation("OrderClient", fields: [customerId], references: [id])
|
||||
managerId String?
|
||||
manager User? @relation("OrderManager", fields: [managerId], references: [id])
|
||||
status OrderStatus @default(NEW)
|
||||
clientApproved Boolean?
|
||||
managerApproved Boolean?
|
||||
blockReason String?
|
||||
deliveryTerms String?
|
||||
deliveryFee Decimal? @db.Decimal(14, 2)
|
||||
totalPrice Decimal? @db.Decimal(14, 2)
|
||||
calculationPayload Json?
|
||||
items OrderItem[]
|
||||
history OrderStatusEvent[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model OrderItem {
|
||||
id String @id @default(cuid())
|
||||
orderId String
|
||||
order Order @relation(fields: [orderId], references: [id])
|
||||
productId String?
|
||||
product Product? @relation(fields: [productId], references: [id])
|
||||
productName String
|
||||
quantity Decimal @db.Decimal(14, 3)
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model OrderStatusEvent {
|
||||
id String @id @default(cuid())
|
||||
orderId String
|
||||
order Order @relation(fields: [orderId], references: [id])
|
||||
status OrderStatus
|
||||
actorUserId String
|
||||
actorUser User @relation(fields: [actorUserId], references: [id])
|
||||
note String?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model ReferralLink {
|
||||
id String @id @default(cuid())
|
||||
referrerId String
|
||||
referrer User @relation("ReferralReferrer", fields: [referrerId], references: [id])
|
||||
refereeId String
|
||||
referee User @relation("ReferralReferee", fields: [refereeId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([referrerId, refereeId])
|
||||
}
|
||||
|
||||
model BonusTransaction {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
amount Decimal @db.Decimal(14, 2)
|
||||
reason String
|
||||
orderId String?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model RewardWithdrawalRequest {
|
||||
id String @id @default(cuid())
|
||||
requesterId String
|
||||
requester User @relation("WithdrawalRequester", fields: [requesterId], references: [id])
|
||||
amount Decimal @db.Decimal(14, 2)
|
||||
status WithdrawalStatus @default(PENDING)
|
||||
reviewedById String?
|
||||
reviewedBy User? @relation("WithdrawalReviewer", fields: [reviewedById], references: [id])
|
||||
reviewComment String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
34
scripts/load-vault-env.sh
Executable file
34
scripts/load-vault-env.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "${VAULT_ENABLED:-auto}" = "false" ] || [ "${VAULT_ENABLED:-auto}" = "0" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -z "${VAULT_ADDR:-}" ] || [ -z "${VAULT_TOKEN:-}" ]; then
|
||||
if [ "${VAULT_ENABLED:-auto}" = "true" ] || [ "${VAULT_ENABLED:-auto}" = "1" ]; then
|
||||
echo "VAULT_ENABLED=true but VAULT_ADDR/VAULT_TOKEN are not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
|
||||
echo "Vault bootstrap requires curl and jq." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
kv_mount="${VAULT_KV_MOUNT:-secret}"
|
||||
|
||||
load_path() {
|
||||
path="$1"
|
||||
[ -z "$path" ] && return 0
|
||||
|
||||
payload="$(curl -fsS -H "X-Vault-Token: ${VAULT_TOKEN}" "${VAULT_ADDR%/}/v1/${kv_mount}/data/${path}")"
|
||||
echo "$payload" | jq -r '.data.data // {} | to_entries[] | "\(.key)=\(.value|tostring)"' | while IFS='=' read -r k v; do
|
||||
export "$k=$v"
|
||||
done
|
||||
}
|
||||
|
||||
load_path "${VAULT_SHARED_PATH:-}"
|
||||
load_path "${VAULT_PROJECT_PATH:-}"
|
||||
119
scripts/seed.js
Normal file
119
scripts/seed.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import 'dotenv/config';
|
||||
|
||||
import { prisma } from '../src/prisma-client.js';
|
||||
|
||||
const managerEmail = 'manager@fregat.local';
|
||||
const clientEmail = 'client@fregat.local';
|
||||
|
||||
const company = await prisma.company.upsert({
|
||||
where: { inn: '7701000000' },
|
||||
update: {},
|
||||
create: {
|
||||
name: 'Fregat Client LLC',
|
||||
inn: '7701000000',
|
||||
},
|
||||
});
|
||||
|
||||
const manager = await prisma.user.upsert({
|
||||
where: { email: managerEmail },
|
||||
update: { fullName: 'Default Manager' },
|
||||
create: {
|
||||
email: managerEmail,
|
||||
fullName: 'Default Manager',
|
||||
role: 'MANAGER',
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.user.upsert({
|
||||
where: { email: clientEmail },
|
||||
update: { fullName: 'Demo Client', companyId: company.id },
|
||||
create: {
|
||||
email: clientEmail,
|
||||
fullName: 'Demo Client',
|
||||
role: 'CLIENT',
|
||||
companyId: company.id,
|
||||
},
|
||||
});
|
||||
|
||||
const warehouseMain = await prisma.warehouse.upsert({
|
||||
where: { code: 'MSK-01' },
|
||||
update: { name: 'Main warehouse' },
|
||||
create: { code: 'MSK-01', name: 'Main warehouse' },
|
||||
});
|
||||
|
||||
const warehouseReserve = await prisma.warehouse.upsert({
|
||||
where: { code: 'SPB-01' },
|
||||
update: { name: 'Reserve warehouse' },
|
||||
create: { code: 'SPB-01', name: 'Reserve warehouse' },
|
||||
});
|
||||
|
||||
const products = [
|
||||
{
|
||||
sku: 'FILM-100',
|
||||
name: 'Пленка прозрачная 100мкм',
|
||||
description: 'Готовая продукция для стандартной упаковки',
|
||||
isCustomizable: false,
|
||||
qtyMain: 1250,
|
||||
qtyReserve: 420,
|
||||
},
|
||||
{
|
||||
sku: 'FILM-200-COLOR',
|
||||
name: 'Пленка цветная 200мкм',
|
||||
description: 'Продукция с дополнительной окраской',
|
||||
isCustomizable: true,
|
||||
qtyMain: 860,
|
||||
qtyReserve: 230,
|
||||
},
|
||||
];
|
||||
|
||||
for (const product of products) {
|
||||
const dbProduct = await prisma.product.upsert({
|
||||
where: { sku: product.sku },
|
||||
update: {
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
isCustomizable: product.isCustomizable,
|
||||
isActive: true,
|
||||
},
|
||||
create: {
|
||||
sku: product.sku,
|
||||
name: product.name,
|
||||
description: product.description,
|
||||
isCustomizable: product.isCustomizable,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.productStock.upsert({
|
||||
where: {
|
||||
productId_warehouseId: {
|
||||
productId: dbProduct.id,
|
||||
warehouseId: warehouseMain.id,
|
||||
},
|
||||
},
|
||||
update: { availableQty: product.qtyMain },
|
||||
create: {
|
||||
productId: dbProduct.id,
|
||||
warehouseId: warehouseMain.id,
|
||||
availableQty: product.qtyMain,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.productStock.upsert({
|
||||
where: {
|
||||
productId_warehouseId: {
|
||||
productId: dbProduct.id,
|
||||
warehouseId: warehouseReserve.id,
|
||||
},
|
||||
},
|
||||
update: { availableQty: product.qtyReserve },
|
||||
create: {
|
||||
productId: dbProduct.id,
|
||||
warehouseId: warehouseReserve.id,
|
||||
availableQty: product.qtyReserve,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Seed complete. Use manager header x-user-id:', manager.id);
|
||||
await prisma.$disconnect();
|
||||
10
src/context.js
Normal file
10
src/context.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { prisma } from './prisma-client.js';
|
||||
|
||||
export async function buildContext(req) {
|
||||
const userId = req.headers['x-user-id'];
|
||||
const user = userId
|
||||
? await prisma.user.findUnique({ where: { id: String(userId) } })
|
||||
: null;
|
||||
|
||||
return { prisma, user };
|
||||
}
|
||||
11
src/prisma-client.js
Normal file
11
src/prisma-client.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
if (!connectionString) {
|
||||
throw new Error('DATABASE_URL is required for Prisma connection');
|
||||
}
|
||||
|
||||
const adapter = new PrismaPg({ connectionString });
|
||||
|
||||
export const prisma = new PrismaClient({ adapter });
|
||||
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),
|
||||
},
|
||||
};
|
||||
44
src/scalars.js
Normal file
44
src/scalars.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { GraphQLError, GraphQLScalarType, Kind } from 'graphql';
|
||||
|
||||
function parseJsonLiteral(ast) {
|
||||
if (ast.kind === Kind.STRING) return ast.value;
|
||||
if (ast.kind === Kind.BOOLEAN) return ast.value;
|
||||
if (ast.kind === Kind.INT || ast.kind === Kind.FLOAT) return Number(ast.value);
|
||||
if (ast.kind === Kind.NULL) return null;
|
||||
if (ast.kind === Kind.OBJECT) {
|
||||
return Object.fromEntries(ast.fields.map((field) => [field.name.value, parseJsonLiteral(field.value)]));
|
||||
}
|
||||
if (ast.kind === Kind.LIST) {
|
||||
return ast.values.map(parseJsonLiteral);
|
||||
}
|
||||
throw new GraphQLError('Unsupported JSON literal');
|
||||
}
|
||||
|
||||
export const dateTimeScalar = new GraphQLScalarType({
|
||||
name: 'DateTime',
|
||||
serialize(value) {
|
||||
return new Date(value).toISOString();
|
||||
},
|
||||
parseValue(value) {
|
||||
return new Date(value);
|
||||
},
|
||||
parseLiteral(ast) {
|
||||
if (ast.kind !== Kind.STRING) {
|
||||
throw new GraphQLError('DateTime must be a string');
|
||||
}
|
||||
return new Date(ast.value);
|
||||
},
|
||||
});
|
||||
|
||||
export const jsonScalar = new GraphQLScalarType({
|
||||
name: 'JSON',
|
||||
serialize(value) {
|
||||
return value;
|
||||
},
|
||||
parseValue(value) {
|
||||
return value;
|
||||
},
|
||||
parseLiteral(ast) {
|
||||
return parseJsonLiteral(ast);
|
||||
},
|
||||
});
|
||||
294
src/schema.graphql
Normal file
294
src/schema.graphql
Normal file
@@ -0,0 +1,294 @@
|
||||
scalar DateTime
|
||||
scalar JSON
|
||||
|
||||
enum UserRole {
|
||||
CLIENT
|
||||
MANAGER
|
||||
}
|
||||
|
||||
enum MessengerType {
|
||||
TELEGRAM
|
||||
MAX
|
||||
}
|
||||
|
||||
enum RegistrationStatus {
|
||||
PENDING
|
||||
APPROVED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
enum OrderKind {
|
||||
READY
|
||||
CALCULATION
|
||||
}
|
||||
|
||||
enum OrderStatus {
|
||||
NEW
|
||||
MANAGER_PROCESSING
|
||||
WAITING_DOUBLE_CONFIRM
|
||||
CLIENT_REJECTED
|
||||
MANAGER_REJECTED
|
||||
MANAGER_BLOCKED
|
||||
CONFIRMED
|
||||
IN_PROGRESS
|
||||
COMPLETED
|
||||
}
|
||||
|
||||
enum WithdrawalStatus {
|
||||
PENDING
|
||||
APPROVED
|
||||
REJECTED
|
||||
}
|
||||
|
||||
enum Decision {
|
||||
APPROVE
|
||||
REJECT
|
||||
}
|
||||
|
||||
type Company {
|
||||
id: ID!
|
||||
name: String!
|
||||
inn: String
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
email: String!
|
||||
fullName: String!
|
||||
role: UserRole!
|
||||
company: Company
|
||||
}
|
||||
|
||||
type Invitation {
|
||||
id: ID!
|
||||
token: String!
|
||||
email: String!
|
||||
companyName: String!
|
||||
managerId: ID!
|
||||
acceptedById: ID
|
||||
expiresAt: DateTime!
|
||||
acceptedAt: DateTime
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type RegistrationRequest {
|
||||
id: ID!
|
||||
companyName: String!
|
||||
inn: String
|
||||
contactName: String!
|
||||
email: String!
|
||||
status: RegistrationStatus!
|
||||
rejectionReason: String
|
||||
reviewedById: ID
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type MessengerConnection {
|
||||
id: ID!
|
||||
userId: ID!
|
||||
type: MessengerType!
|
||||
channelId: String!
|
||||
isActive: Boolean!
|
||||
}
|
||||
|
||||
type Warehouse {
|
||||
id: ID!
|
||||
code: String!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type ProductWarehouseBalance {
|
||||
warehouse: Warehouse!
|
||||
availableQty: Float!
|
||||
}
|
||||
|
||||
type Product {
|
||||
id: ID!
|
||||
sku: String!
|
||||
name: String!
|
||||
description: String
|
||||
isCustomizable: Boolean!
|
||||
isActive: Boolean!
|
||||
availableInWarehouses: [ProductWarehouseBalance!]!
|
||||
}
|
||||
|
||||
type OrderItem {
|
||||
id: ID!
|
||||
productId: ID
|
||||
productName: String!
|
||||
quantity: Float!
|
||||
}
|
||||
|
||||
type OrderStatusEvent {
|
||||
id: ID!
|
||||
status: OrderStatus!
|
||||
actorUserId: ID!
|
||||
note: String
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type Order {
|
||||
id: ID!
|
||||
code: String!
|
||||
kind: OrderKind!
|
||||
status: OrderStatus!
|
||||
customerId: ID!
|
||||
managerId: ID
|
||||
clientApproved: Boolean
|
||||
managerApproved: Boolean
|
||||
blockReason: String
|
||||
deliveryTerms: String
|
||||
deliveryFee: Float
|
||||
totalPrice: Float
|
||||
calculationPayload: JSON
|
||||
items: [OrderItem!]!
|
||||
history: [OrderStatusEvent!]!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type ReferralLink {
|
||||
id: ID!
|
||||
referrerId: ID!
|
||||
refereeId: ID!
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type BonusTransaction {
|
||||
id: ID!
|
||||
userId: ID!
|
||||
amount: Float!
|
||||
reason: String!
|
||||
orderId: ID
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type RewardWithdrawalRequest {
|
||||
id: ID!
|
||||
requesterId: ID!
|
||||
amount: Float!
|
||||
status: WithdrawalStatus!
|
||||
reviewedById: ID
|
||||
reviewComment: String
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type ReferralStats {
|
||||
referrerId: ID!
|
||||
availableBalance: Float!
|
||||
referralsCount: Int!
|
||||
transactions: [BonusTransaction!]!
|
||||
pendingWithdrawals: [RewardWithdrawalRequest!]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
healthcheck: String!
|
||||
me: User
|
||||
clientProducts: [Product!]!
|
||||
myOrders: [Order!]!
|
||||
myCurrentOrders: [Order!]!
|
||||
managerOrders(status: OrderStatus): [Order!]!
|
||||
registrationRequests(status: RegistrationStatus): [RegistrationRequest!]!
|
||||
referralStats: ReferralStats!
|
||||
}
|
||||
|
||||
input RegisterSelfInput {
|
||||
companyName: String!
|
||||
inn: String
|
||||
contactName: String!
|
||||
email: String!
|
||||
}
|
||||
|
||||
input ReviewRegistrationRequestInput {
|
||||
requestId: ID!
|
||||
decision: Decision!
|
||||
rejectionReason: String
|
||||
}
|
||||
|
||||
input CreateInvitationInput {
|
||||
email: String!
|
||||
companyName: String!
|
||||
expiresInDays: Int = 7
|
||||
}
|
||||
|
||||
input AcceptInvitationInput {
|
||||
token: String!
|
||||
fullName: String!
|
||||
}
|
||||
|
||||
input ConnectMessengerInput {
|
||||
type: MessengerType!
|
||||
channelId: String!
|
||||
}
|
||||
|
||||
input ReadyOrderItemInput {
|
||||
productId: ID!
|
||||
quantity: Float!
|
||||
}
|
||||
|
||||
input SubmitReadyOrderInput {
|
||||
items: [ReadyOrderItemInput!]!
|
||||
}
|
||||
|
||||
input SubmitCalculationOrderInput {
|
||||
productName: String!
|
||||
quantity: Float!
|
||||
parameters: JSON!
|
||||
}
|
||||
|
||||
input SetOrderOfferInput {
|
||||
orderId: ID!
|
||||
deliveryTerms: String!
|
||||
deliveryFee: Float!
|
||||
totalPrice: Float!
|
||||
}
|
||||
|
||||
input BlockOrderInput {
|
||||
orderId: ID!
|
||||
reason: String!
|
||||
}
|
||||
|
||||
input CreateReferralInput {
|
||||
refereeUserId: ID!
|
||||
}
|
||||
|
||||
input AddBonusTransactionInput {
|
||||
userId: ID!
|
||||
amount: Float!
|
||||
reason: String!
|
||||
orderId: ID
|
||||
}
|
||||
|
||||
input RequestRewardWithdrawalInput {
|
||||
amount: Float!
|
||||
}
|
||||
|
||||
input ReviewRewardWithdrawalInput {
|
||||
withdrawalId: ID!
|
||||
decision: Decision!
|
||||
reviewComment: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
registerSelf(input: RegisterSelfInput!): RegistrationRequest!
|
||||
reviewRegistrationRequest(input: ReviewRegistrationRequestInput!): RegistrationRequest!
|
||||
createInvitation(input: CreateInvitationInput!): Invitation!
|
||||
acceptInvitation(input: AcceptInvitationInput!): User!
|
||||
connectMessenger(input: ConnectMessengerInput!): MessengerConnection!
|
||||
|
||||
submitReadyOrder(input: SubmitReadyOrderInput!): Order!
|
||||
submitCalculationOrder(input: SubmitCalculationOrderInput!): Order!
|
||||
managerSetOrderOffer(input: SetOrderOfferInput!): Order!
|
||||
clientReviewOrder(orderId: ID!, decision: Decision!): Order!
|
||||
managerFinalizeOrder(orderId: ID!, decision: Decision!): Order!
|
||||
blockOrder(input: BlockOrderInput!): Order!
|
||||
startOrderWork(orderId: ID!): Order!
|
||||
completeOrder(orderId: ID!): Order!
|
||||
|
||||
createReferral(input: CreateReferralInput!): ReferralLink!
|
||||
addBonusTransaction(input: AddBonusTransactionInput!): BonusTransaction!
|
||||
requestRewardWithdrawal(input: RequestRewardWithdrawalInput!): RewardWithdrawalRequest!
|
||||
reviewRewardWithdrawal(input: ReviewRewardWithdrawalInput!): RewardWithdrawalRequest!
|
||||
}
|
||||
50
src/server.js
Normal file
50
src/server.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'dotenv/config';
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import bodyParser from 'body-parser';
|
||||
import cors from 'cors';
|
||||
import express from 'express';
|
||||
import { ApolloServer } from '@apollo/server';
|
||||
import { expressMiddleware } from '@as-integrations/express5';
|
||||
|
||||
import { buildContext } from './context.js';
|
||||
import { prisma } from './prisma-client.js';
|
||||
import { resolvers } from './resolvers.js';
|
||||
|
||||
const typeDefs = readFileSync(new URL('./schema.graphql', import.meta.url), 'utf8');
|
||||
|
||||
const server = new ApolloServer({
|
||||
typeDefs,
|
||||
resolvers,
|
||||
});
|
||||
|
||||
await server.start();
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json({ limit: '1mb' }));
|
||||
|
||||
app.get('/healthz', (_, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
app.use(
|
||||
'/graphql',
|
||||
expressMiddleware(server, {
|
||||
context: async ({ req }) => buildContext(req),
|
||||
}),
|
||||
);
|
||||
|
||||
const port = Number(process.env.PORT ?? 4000);
|
||||
app.listen(port, () => {
|
||||
console.log(`apollo-backend running at http://localhost:${port}/graphql`);
|
||||
});
|
||||
|
||||
async function shutdown() {
|
||||
await server.stop();
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
Reference in New Issue
Block a user