feat(profile): add counterparty profile and enforce it for order creation

This commit is contained in:
Ruslan Bakiev
2026-04-02 16:46:36 +07:00
parent 3a9b922a07
commit 3ee14d508c
4 changed files with 176 additions and 0 deletions

View File

@@ -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(),

View File

@@ -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!