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
|
||||
companyId String?
|
||||
company Company? @relation(fields: [companyId], references: [id])
|
||||
counterpartyProfile CounterpartyProfile?
|
||||
registrationRequests RegistrationRequest[] @relation("RegistrationRequester")
|
||||
reviewedRequests RegistrationRequest[] @relation("RegistrationReviewer")
|
||||
sentInvitations Invitation[] @relation("InvitationManager")
|
||||
@@ -78,6 +79,27 @@ model User {
|
||||
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 {
|
||||
id String @id @default(cuid())
|
||||
companyName String
|
||||
|
||||
@@ -66,6 +66,62 @@ function buildDefaultFullName(email) {
|
||||
.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) {
|
||||
const suffix = note ? `\nКомментарий: ${note}` : '';
|
||||
return `Заказ ${order.code} изменил статус: ${status}.${suffix}`;
|
||||
@@ -154,12 +210,22 @@ async function collectNotificationHistory(context, userId, channel, limit) {
|
||||
export const resolvers = {
|
||||
DateTime: dateTimeScalar,
|
||||
JSON: jsonScalar,
|
||||
CounterpartyProfile: {
|
||||
isComplete: (profile) => isCounterpartyProfileComplete(profile),
|
||||
},
|
||||
|
||||
Query: {
|
||||
healthcheck: () => 'ok',
|
||||
|
||||
me: (_, __, context) => context.user,
|
||||
|
||||
myCounterpartyProfile: async (_, __, context) => {
|
||||
const user = requireUser(context);
|
||||
return context.prisma.counterpartyProfile.findUnique({
|
||||
where: { userId: user.id },
|
||||
});
|
||||
},
|
||||
|
||||
myMessengerConnections: async (_, __, context) => {
|
||||
const user = requireUser(context);
|
||||
return context.prisma.messengerConnection.findMany({
|
||||
@@ -443,6 +509,24 @@ export const resolvers = {
|
||||
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) => {
|
||||
const user = requireUser(context);
|
||||
return context.prisma.messengerConnection.upsert({
|
||||
@@ -499,6 +583,7 @@ export const resolvers = {
|
||||
|
||||
submitReadyOrder: async (_, { input }, context) => {
|
||||
const customer = requireRole(context, 'CLIENT');
|
||||
await requireCompletedCounterpartyProfile(context, customer.id);
|
||||
if (!input.items.length) {
|
||||
throw new Error('Order must contain at least one item.');
|
||||
}
|
||||
@@ -548,6 +633,7 @@ export const resolvers = {
|
||||
|
||||
submitCalculationOrder: async (_, { input }, context) => {
|
||||
const customer = requireRole(context, 'CLIENT');
|
||||
await requireCompletedCounterpartyProfile(context, customer.id);
|
||||
const order = await context.prisma.order.create({
|
||||
data: {
|
||||
code: orderCode(),
|
||||
|
||||
@@ -65,6 +65,27 @@ type User {
|
||||
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 {
|
||||
challengeToken: String!
|
||||
channel: LoginChannel!
|
||||
@@ -222,6 +243,7 @@ type ReferralStats {
|
||||
type Query {
|
||||
healthcheck: String!
|
||||
me: User
|
||||
myCounterpartyProfile: CounterpartyProfile
|
||||
myMessengerConnections: [MessengerConnection!]!
|
||||
myNotificationHistory(channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
||||
managerNotificationHistory(userId: ID!, channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
|
||||
@@ -272,6 +294,22 @@ input ConnectMessengerInput {
|
||||
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 {
|
||||
productId: ID!
|
||||
quantity: Float!
|
||||
@@ -329,6 +367,7 @@ type Mutation {
|
||||
createInvitation(input: CreateInvitationInput!): Invitation!
|
||||
acceptInvitation(input: AcceptInvitationInput!): User!
|
||||
connectMessenger(input: ConnectMessengerInput!): MessengerConnection!
|
||||
upsertMyCounterpartyProfile(input: UpsertMyCounterpartyProfileInput!): CounterpartyProfile!
|
||||
sendTestMessengerMessage(type: MessengerType!, channelId: String, message: String): MessengerDispatchResult!
|
||||
|
||||
submitReadyOrder(input: SubmitReadyOrderInput!): Order!
|
||||
|
||||
Reference in New Issue
Block a user