feat(profile): add counterparty profile and enforce it for order creation
This commit is contained in:
29
prisma/migrations/0002_counterparty_profile/migration.sql
Normal file
29
prisma/migrations/0002_counterparty_profile/migration.sql
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "CounterpartyProfile" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"companyName" TEXT NOT NULL,
|
||||||
|
"companyFullName" TEXT NOT NULL,
|
||||||
|
"inn" TEXT NOT NULL,
|
||||||
|
"kpp" TEXT,
|
||||||
|
"ogrn" TEXT,
|
||||||
|
"legalAddress" TEXT NOT NULL,
|
||||||
|
"bankName" TEXT NOT NULL,
|
||||||
|
"bik" TEXT NOT NULL,
|
||||||
|
"correspondentAccount" TEXT NOT NULL,
|
||||||
|
"checkingAccount" TEXT NOT NULL,
|
||||||
|
"signerFullName" TEXT NOT NULL,
|
||||||
|
"signerPosition" TEXT NOT NULL,
|
||||||
|
"signerBasis" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "CounterpartyProfile_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "CounterpartyProfile_userId_key" ON "CounterpartyProfile"("userId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "CounterpartyProfile" ADD CONSTRAINT "CounterpartyProfile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
@@ -61,6 +61,7 @@ model User {
|
|||||||
role UserRole
|
role UserRole
|
||||||
companyId String?
|
companyId String?
|
||||||
company Company? @relation(fields: [companyId], references: [id])
|
company Company? @relation(fields: [companyId], references: [id])
|
||||||
|
counterpartyProfile CounterpartyProfile?
|
||||||
registrationRequests RegistrationRequest[] @relation("RegistrationRequester")
|
registrationRequests RegistrationRequest[] @relation("RegistrationRequester")
|
||||||
reviewedRequests RegistrationRequest[] @relation("RegistrationReviewer")
|
reviewedRequests RegistrationRequest[] @relation("RegistrationReviewer")
|
||||||
sentInvitations Invitation[] @relation("InvitationManager")
|
sentInvitations Invitation[] @relation("InvitationManager")
|
||||||
@@ -78,6 +79,27 @@ model User {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model CounterpartyProfile {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String @unique
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
companyName String
|
||||||
|
companyFullName String
|
||||||
|
inn String
|
||||||
|
kpp String?
|
||||||
|
ogrn String?
|
||||||
|
legalAddress String
|
||||||
|
bankName String
|
||||||
|
bik String
|
||||||
|
correspondentAccount String
|
||||||
|
checkingAccount String
|
||||||
|
signerFullName String
|
||||||
|
signerPosition String
|
||||||
|
signerBasis String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
model RegistrationRequest {
|
model RegistrationRequest {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
companyName String
|
companyName String
|
||||||
|
|||||||
@@ -66,6 +66,62 @@ function buildDefaultFullName(email) {
|
|||||||
.join(' ');
|
.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeText(value) {
|
||||||
|
return String(value ?? '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeOptionalText(value) {
|
||||||
|
const normalized = normalizeText(value);
|
||||||
|
return normalized ? normalized : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCounterpartyProfileComplete(profile) {
|
||||||
|
if (!profile) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Boolean(
|
||||||
|
normalizeText(profile.companyName) &&
|
||||||
|
normalizeText(profile.companyFullName) &&
|
||||||
|
normalizeText(profile.inn) &&
|
||||||
|
normalizeText(profile.legalAddress) &&
|
||||||
|
normalizeText(profile.bankName) &&
|
||||||
|
normalizeText(profile.bik) &&
|
||||||
|
normalizeText(profile.correspondentAccount) &&
|
||||||
|
normalizeText(profile.checkingAccount) &&
|
||||||
|
normalizeText(profile.signerFullName) &&
|
||||||
|
normalizeText(profile.signerPosition) &&
|
||||||
|
normalizeText(profile.signerBasis),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toCounterpartyProfileInputData(input) {
|
||||||
|
return {
|
||||||
|
companyName: normalizeText(input.companyName),
|
||||||
|
companyFullName: normalizeText(input.companyFullName),
|
||||||
|
inn: normalizeText(input.inn),
|
||||||
|
kpp: normalizeOptionalText(input.kpp),
|
||||||
|
ogrn: normalizeOptionalText(input.ogrn),
|
||||||
|
legalAddress: normalizeText(input.legalAddress),
|
||||||
|
bankName: normalizeText(input.bankName),
|
||||||
|
bik: normalizeText(input.bik),
|
||||||
|
correspondentAccount: normalizeText(input.correspondentAccount),
|
||||||
|
checkingAccount: normalizeText(input.checkingAccount),
|
||||||
|
signerFullName: normalizeText(input.signerFullName),
|
||||||
|
signerPosition: normalizeText(input.signerPosition),
|
||||||
|
signerBasis: normalizeText(input.signerBasis),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requireCompletedCounterpartyProfile(context, userId) {
|
||||||
|
const profile = await context.prisma.counterpartyProfile.findUnique({
|
||||||
|
where: { userId },
|
||||||
|
});
|
||||||
|
if (!isCounterpartyProfileComplete(profile)) {
|
||||||
|
throw new Error('Counterparty profile is incomplete. Fill profile before placing an order.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatOrderStatusMessage(order, status, note) {
|
function formatOrderStatusMessage(order, status, note) {
|
||||||
const suffix = note ? `\nКомментарий: ${note}` : '';
|
const suffix = note ? `\nКомментарий: ${note}` : '';
|
||||||
return `Заказ ${order.code} изменил статус: ${status}.${suffix}`;
|
return `Заказ ${order.code} изменил статус: ${status}.${suffix}`;
|
||||||
@@ -154,12 +210,22 @@ async function collectNotificationHistory(context, userId, channel, limit) {
|
|||||||
export const resolvers = {
|
export const resolvers = {
|
||||||
DateTime: dateTimeScalar,
|
DateTime: dateTimeScalar,
|
||||||
JSON: jsonScalar,
|
JSON: jsonScalar,
|
||||||
|
CounterpartyProfile: {
|
||||||
|
isComplete: (profile) => isCounterpartyProfileComplete(profile),
|
||||||
|
},
|
||||||
|
|
||||||
Query: {
|
Query: {
|
||||||
healthcheck: () => 'ok',
|
healthcheck: () => 'ok',
|
||||||
|
|
||||||
me: (_, __, context) => context.user,
|
me: (_, __, context) => context.user,
|
||||||
|
|
||||||
|
myCounterpartyProfile: async (_, __, context) => {
|
||||||
|
const user = requireUser(context);
|
||||||
|
return context.prisma.counterpartyProfile.findUnique({
|
||||||
|
where: { userId: user.id },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
myMessengerConnections: async (_, __, context) => {
|
myMessengerConnections: async (_, __, context) => {
|
||||||
const user = requireUser(context);
|
const user = requireUser(context);
|
||||||
return context.prisma.messengerConnection.findMany({
|
return context.prisma.messengerConnection.findMany({
|
||||||
@@ -443,6 +509,24 @@ export const resolvers = {
|
|||||||
return user;
|
return user;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
upsertMyCounterpartyProfile: async (_, { input }, context) => {
|
||||||
|
const user = requireUser(context);
|
||||||
|
const payload = toCounterpartyProfileInputData(input);
|
||||||
|
|
||||||
|
if (!isCounterpartyProfileComplete(payload)) {
|
||||||
|
throw new Error('Counterparty profile is incomplete. Fill all required fields.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.prisma.counterpartyProfile.upsert({
|
||||||
|
where: { userId: user.id },
|
||||||
|
update: payload,
|
||||||
|
create: {
|
||||||
|
userId: user.id,
|
||||||
|
...payload,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
connectMessenger: (_, { input }, context) => {
|
connectMessenger: (_, { input }, context) => {
|
||||||
const user = requireUser(context);
|
const user = requireUser(context);
|
||||||
return context.prisma.messengerConnection.upsert({
|
return context.prisma.messengerConnection.upsert({
|
||||||
@@ -499,6 +583,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
submitReadyOrder: async (_, { input }, context) => {
|
submitReadyOrder: async (_, { input }, context) => {
|
||||||
const customer = requireRole(context, 'CLIENT');
|
const customer = requireRole(context, 'CLIENT');
|
||||||
|
await requireCompletedCounterpartyProfile(context, customer.id);
|
||||||
if (!input.items.length) {
|
if (!input.items.length) {
|
||||||
throw new Error('Order must contain at least one item.');
|
throw new Error('Order must contain at least one item.');
|
||||||
}
|
}
|
||||||
@@ -548,6 +633,7 @@ export const resolvers = {
|
|||||||
|
|
||||||
submitCalculationOrder: async (_, { input }, context) => {
|
submitCalculationOrder: async (_, { input }, context) => {
|
||||||
const customer = requireRole(context, 'CLIENT');
|
const customer = requireRole(context, 'CLIENT');
|
||||||
|
await requireCompletedCounterpartyProfile(context, customer.id);
|
||||||
const order = await context.prisma.order.create({
|
const order = await context.prisma.order.create({
|
||||||
data: {
|
data: {
|
||||||
code: orderCode(),
|
code: orderCode(),
|
||||||
|
|||||||
@@ -65,6 +65,27 @@ type User {
|
|||||||
company: Company
|
company: Company
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CounterpartyProfile {
|
||||||
|
id: ID!
|
||||||
|
userId: ID!
|
||||||
|
companyName: String!
|
||||||
|
companyFullName: String!
|
||||||
|
inn: String!
|
||||||
|
kpp: String
|
||||||
|
ogrn: String
|
||||||
|
legalAddress: String!
|
||||||
|
bankName: String!
|
||||||
|
bik: String!
|
||||||
|
correspondentAccount: String!
|
||||||
|
checkingAccount: String!
|
||||||
|
signerFullName: String!
|
||||||
|
signerPosition: String!
|
||||||
|
signerBasis: String!
|
||||||
|
isComplete: Boolean!
|
||||||
|
createdAt: DateTime!
|
||||||
|
updatedAt: DateTime!
|
||||||
|
}
|
||||||
|
|
||||||
type AuthCodeRequestResult {
|
type AuthCodeRequestResult {
|
||||||
challengeToken: String!
|
challengeToken: String!
|
||||||
channel: LoginChannel!
|
channel: LoginChannel!
|
||||||
@@ -222,6 +243,7 @@ type ReferralStats {
|
|||||||
type Query {
|
type Query {
|
||||||
healthcheck: String!
|
healthcheck: String!
|
||||||
me: User
|
me: User
|
||||||
|
myCounterpartyProfile: CounterpartyProfile
|
||||||
myMessengerConnections: [MessengerConnection!]!
|
myMessengerConnections: [MessengerConnection!]!
|
||||||
myNotificationHistory(channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
myNotificationHistory(channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
||||||
managerNotificationHistory(userId: ID!, channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
managerNotificationHistory(userId: ID!, channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
||||||
@@ -272,6 +294,22 @@ input ConnectMessengerInput {
|
|||||||
channelId: String!
|
channelId: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input UpsertMyCounterpartyProfileInput {
|
||||||
|
companyName: String!
|
||||||
|
companyFullName: String!
|
||||||
|
inn: String!
|
||||||
|
kpp: String
|
||||||
|
ogrn: String
|
||||||
|
legalAddress: String!
|
||||||
|
bankName: String!
|
||||||
|
bik: String!
|
||||||
|
correspondentAccount: String!
|
||||||
|
checkingAccount: String!
|
||||||
|
signerFullName: String!
|
||||||
|
signerPosition: String!
|
||||||
|
signerBasis: String!
|
||||||
|
}
|
||||||
|
|
||||||
input ReadyOrderItemInput {
|
input ReadyOrderItemInput {
|
||||||
productId: ID!
|
productId: ID!
|
||||||
quantity: Float!
|
quantity: Float!
|
||||||
@@ -329,6 +367,7 @@ type Mutation {
|
|||||||
createInvitation(input: CreateInvitationInput!): Invitation!
|
createInvitation(input: CreateInvitationInput!): Invitation!
|
||||||
acceptInvitation(input: AcceptInvitationInput!): User!
|
acceptInvitation(input: AcceptInvitationInput!): User!
|
||||||
connectMessenger(input: ConnectMessengerInput!): MessengerConnection!
|
connectMessenger(input: ConnectMessengerInput!): MessengerConnection!
|
||||||
|
upsertMyCounterpartyProfile(input: UpsertMyCounterpartyProfileInput!): CounterpartyProfile!
|
||||||
sendTestMessengerMessage(type: MessengerType!, channelId: String, message: String): MessengerDispatchResult!
|
sendTestMessengerMessage(type: MessengerType!, channelId: String, message: String): MessengerDispatchResult!
|
||||||
|
|
||||||
submitReadyOrder(input: SubmitReadyOrderInput!): Order!
|
submitReadyOrder(input: SubmitReadyOrderInput!): Order!
|
||||||
|
|||||||
Reference in New Issue
Block a user