Add delivery addresses to profile and order flow

This commit is contained in:
Ruslan Bakiev
2026-04-03 10:25:19 +07:00
parent 3ee14d508c
commit f7fb45618d
4 changed files with 278 additions and 0 deletions

View File

@@ -113,6 +113,67 @@ function toCounterpartyProfileInputData(input) {
};
}
function toDeliveryAddressInputData(input) {
return {
label: normalizeOptionalText(input.label),
address: normalizeText(input.address),
unrestrictedValue: normalizeOptionalText(input.unrestrictedValue),
fiasId: normalizeOptionalText(input.fiasId),
};
}
function presentDeliveryAddress(address) {
return address.unrestrictedValue || address.address;
}
function withDeliveryAddressDefaultFlag(address, defaultDeliveryAddressId) {
return {
...address,
isDefault: address.id === defaultDeliveryAddressId,
};
}
async function resolveSelectedDeliveryAddress(context, userId, deliveryAddressId) {
const normalizedAddressId = normalizeOptionalText(deliveryAddressId);
if (normalizedAddressId) {
const selected = await context.prisma.deliveryAddress.findFirst({
where: {
id: normalizedAddressId,
userId,
},
});
if (!selected) {
throw new Error('Delivery address is not available for this user.');
}
return selected;
}
const user = await context.prisma.user.findUnique({
where: { id: userId },
select: { defaultDeliveryAddressId: true },
});
if (!user?.defaultDeliveryAddressId) {
throw new Error('Delivery address is not selected. Add address in profile first.');
}
const fallbackAddress = await context.prisma.deliveryAddress.findFirst({
where: {
id: user.defaultDeliveryAddressId,
userId,
},
});
if (!fallbackAddress) {
throw new Error('Default delivery address was not found. Select another one in profile.');
}
return fallbackAddress;
}
async function requireCompletedCounterpartyProfile(context, userId) {
const profile = await context.prisma.counterpartyProfile.findUnique({
where: { userId },
@@ -213,6 +274,9 @@ export const resolvers = {
CounterpartyProfile: {
isComplete: (profile) => isCounterpartyProfileComplete(profile),
},
DeliveryAddress: {
isDefault: (address) => Boolean(address.isDefault),
},
Query: {
healthcheck: () => 'ok',
@@ -226,6 +290,22 @@ export const resolvers = {
});
},
myDeliveryAddresses: async (_, __, context) => {
const user = requireUser(context);
const [account, addresses] = await Promise.all([
context.prisma.user.findUnique({
where: { id: user.id },
select: { defaultDeliveryAddressId: true },
}),
context.prisma.deliveryAddress.findMany({
where: { userId: user.id },
orderBy: [{ updatedAt: 'desc' }, { createdAt: 'desc' }],
}),
]);
return addresses.map((address) => withDeliveryAddressDefaultFlag(address, account?.defaultDeliveryAddressId ?? null));
},
myMessengerConnections: async (_, __, context) => {
const user = requireUser(context);
return context.prisma.messengerConnection.findMany({
@@ -527,6 +607,117 @@ export const resolvers = {
});
},
createMyDeliveryAddress: async (_, { input }, context) => {
const user = requireUser(context);
const payload = toDeliveryAddressInputData(input);
if (!payload.address) {
throw new Error('Delivery address is required.');
}
const created = await context.prisma.$transaction(async (tx) => {
const account = await tx.user.findUnique({
where: { id: user.id },
select: { defaultDeliveryAddressId: true },
});
const address = await tx.deliveryAddress.create({
data: {
userId: user.id,
...payload,
},
});
if (!account?.defaultDeliveryAddressId) {
await tx.user.update({
where: { id: user.id },
data: { defaultDeliveryAddressId: address.id },
});
return withDeliveryAddressDefaultFlag(address, address.id);
}
return withDeliveryAddressDefaultFlag(address, account.defaultDeliveryAddressId);
});
return created;
},
setMyDefaultDeliveryAddress: async (_, { addressId }, context) => {
const user = requireUser(context);
const normalizedAddressId = normalizeText(addressId);
if (!normalizedAddressId) {
throw new Error('Delivery address id is required.');
}
const address = await context.prisma.deliveryAddress.findFirst({
where: {
id: normalizedAddressId,
userId: user.id,
},
});
if (!address) {
throw new Error('Delivery address is not available for this user.');
}
await context.prisma.user.update({
where: { id: user.id },
data: {
defaultDeliveryAddressId: address.id,
},
});
return withDeliveryAddressDefaultFlag(address, address.id);
},
deleteMyDeliveryAddress: async (_, { addressId }, context) => {
const user = requireUser(context);
const normalizedAddressId = normalizeText(addressId);
if (!normalizedAddressId) {
throw new Error('Delivery address id is required.');
}
const address = await context.prisma.deliveryAddress.findFirst({
where: {
id: normalizedAddressId,
userId: user.id,
},
});
if (!address) {
throw new Error('Delivery address is not available for this user.');
}
await context.prisma.$transaction(async (tx) => {
const account = await tx.user.findUnique({
where: { id: user.id },
select: { defaultDeliveryAddressId: true },
});
await tx.deliveryAddress.delete({
where: { id: address.id },
});
if (account?.defaultDeliveryAddressId !== address.id) {
return;
}
const nextDefault = await tx.deliveryAddress.findFirst({
where: { userId: user.id },
orderBy: [{ updatedAt: 'desc' }, { createdAt: 'desc' }],
});
await tx.user.update({
where: { id: user.id },
data: {
defaultDeliveryAddressId: nextDefault?.id ?? null,
},
});
});
return true;
},
connectMessenger: (_, { input }, context) => {
const user = requireUser(context);
return context.prisma.messengerConnection.upsert({
@@ -587,6 +778,7 @@ export const resolvers = {
if (!input.items.length) {
throw new Error('Order must contain at least one item.');
}
const selectedAddress = await resolveSelectedDeliveryAddress(context, customer.id, input.deliveryAddressId);
const productIds = input.items.map((item) => item.productId);
const products = await context.prisma.product.findMany({
@@ -604,6 +796,8 @@ export const resolvers = {
code: orderCode(),
kind: 'READY',
customerId: customer.id,
deliveryAddressId: selectedAddress.id,
deliveryAddress: presentDeliveryAddress(selectedAddress),
status: 'NEW',
items: {
create: input.items.map((item) => {
@@ -634,11 +828,14 @@ export const resolvers = {
submitCalculationOrder: async (_, { input }, context) => {
const customer = requireRole(context, 'CLIENT');
await requireCompletedCounterpartyProfile(context, customer.id);
const selectedAddress = await resolveSelectedDeliveryAddress(context, customer.id, input.deliveryAddressId);
const order = await context.prisma.order.create({
data: {
code: orderCode(),
kind: 'CALCULATION',
customerId: customer.id,
deliveryAddressId: selectedAddress.id,
deliveryAddress: presentDeliveryAddress(selectedAddress),
status: 'NEW',
calculationPayload: input.parameters,
items: {

View File

@@ -86,6 +86,18 @@ type CounterpartyProfile {
updatedAt: DateTime!
}
type DeliveryAddress {
id: ID!
userId: ID!
label: String
address: String!
unrestrictedValue: String
fiasId: String
isDefault: Boolean!
createdAt: DateTime!
updatedAt: DateTime!
}
type AuthCodeRequestResult {
challengeToken: String!
channel: LoginChannel!
@@ -191,6 +203,7 @@ type Order {
kind: OrderKind!
status: OrderStatus!
customerId: ID!
deliveryAddress: String
managerId: ID
clientApproved: Boolean
managerApproved: Boolean
@@ -244,6 +257,7 @@ type Query {
healthcheck: String!
me: User
myCounterpartyProfile: CounterpartyProfile
myDeliveryAddresses: [DeliveryAddress!]!
myMessengerConnections: [MessengerConnection!]!
myNotificationHistory(channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
managerNotificationHistory(userId: ID!, channel: MessengerType!, limit: Int = 50): [NotificationHistoryItem!]!
@@ -310,6 +324,13 @@ input UpsertMyCounterpartyProfileInput {
signerBasis: String!
}
input CreateMyDeliveryAddressInput {
label: String
address: String!
unrestrictedValue: String
fiasId: String
}
input ReadyOrderItemInput {
productId: ID!
quantity: Float!
@@ -317,12 +338,14 @@ input ReadyOrderItemInput {
input SubmitReadyOrderInput {
items: [ReadyOrderItemInput!]!
deliveryAddressId: ID
}
input SubmitCalculationOrderInput {
productName: String!
quantity: Float!
parameters: JSON!
deliveryAddressId: ID
}
input SetOrderOfferInput {
@@ -368,6 +391,9 @@ type Mutation {
acceptInvitation(input: AcceptInvitationInput!): User!
connectMessenger(input: ConnectMessengerInput!): MessengerConnection!
upsertMyCounterpartyProfile(input: UpsertMyCounterpartyProfileInput!): CounterpartyProfile!
createMyDeliveryAddress(input: CreateMyDeliveryAddressInput!): DeliveryAddress!
setMyDefaultDeliveryAddress(addressId: ID!): DeliveryAddress!
deleteMyDeliveryAddress(addressId: ID!): Boolean!
sendTestMessengerMessage(type: MessengerType!, channelId: String, message: String): MessengerDispatchResult!
submitReadyOrder(input: SubmitReadyOrderInput!): Order!