Add shipping cost groups and reject spam contact-form SMS.
Wire cart/checkout shipping fees with category groups and static rates, reject gibberish contact submissions, and SMS only valid Iranian mobiles. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
1a49576752
commit
88ef4e82a1
+114
-10
@@ -20,6 +20,7 @@ import { TransactionType } from '@prisma/client';
|
||||
import { BusinessSettingsService } from '../business-settings/business-settings.service';
|
||||
import type { PaymentGatewayId } from '../business-settings/business-settings.types';
|
||||
import { listPublicPaymentGateways } from '../business-settings/business-settings.util';
|
||||
import { ShippingCostGroupsService } from '../store/shipping-cost-groups.service';
|
||||
|
||||
const cartVariantInclude = {
|
||||
storeItem: {
|
||||
@@ -59,6 +60,7 @@ export class CartService {
|
||||
private readonly orders: OrdersService,
|
||||
private readonly payments: PaymentsService,
|
||||
private readonly businessSettings: BusinessSettingsService,
|
||||
private readonly shippingCostGroups: ShippingCostGroupsService,
|
||||
) {}
|
||||
|
||||
async getCart(businessIdRaw: string, actor: AuthUser) {
|
||||
@@ -66,7 +68,7 @@ export class CartService {
|
||||
await this.assertCustomerAccess(businessId, actor);
|
||||
|
||||
const cart = await this.getOrCreateCart(businessId, actor.id);
|
||||
return { cart: this.serializeCart(cart) };
|
||||
return { cart: await this.serializeCart(cart) };
|
||||
}
|
||||
|
||||
async addItem(businessIdRaw: string, dto: AddCartItemDto, actor: AuthUser) {
|
||||
@@ -97,7 +99,7 @@ export class CartService {
|
||||
const refreshed = await this.loadCart(cart.id);
|
||||
return {
|
||||
message: 'Cart item quantity updated',
|
||||
cart: this.serializeCart(refreshed),
|
||||
cart: await this.serializeCart(refreshed),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -114,7 +116,7 @@ export class CartService {
|
||||
const refreshed = await this.loadCart(cart.id);
|
||||
return {
|
||||
message: 'Item added to cart',
|
||||
cart: this.serializeCart(refreshed),
|
||||
cart: await this.serializeCart(refreshed),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -145,7 +147,7 @@ export class CartService {
|
||||
const refreshed = await this.loadCart(cart.id);
|
||||
return {
|
||||
message: 'Cart item updated',
|
||||
cart: this.serializeCart(refreshed),
|
||||
cart: await this.serializeCart(refreshed),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -166,7 +168,7 @@ export class CartService {
|
||||
const refreshed = await this.loadCart(cart.id);
|
||||
return {
|
||||
message: 'Cart item removed',
|
||||
cart: this.serializeCart(refreshed),
|
||||
cart: await this.serializeCart(refreshed),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -180,7 +182,7 @@ export class CartService {
|
||||
const refreshed = await this.loadCart(cart.id);
|
||||
return {
|
||||
message: 'Cart cleared',
|
||||
cart: this.serializeCart(refreshed),
|
||||
cart: await this.serializeCart(refreshed),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -204,6 +206,71 @@ export class CartService {
|
||||
dto.shippingAddress,
|
||||
);
|
||||
|
||||
const deliveryMode =
|
||||
dto.deliveryMode === 'pickup' || dto.deliveryMode === 'delivery'
|
||||
? dto.deliveryMode
|
||||
: dto.addressId
|
||||
? 'delivery'
|
||||
: 'pickup';
|
||||
|
||||
const selectedMethodsByGroupId: Record<string, string> = {};
|
||||
for (const group of dto.shippingGroups ?? []) {
|
||||
if (group.methodId?.trim()) {
|
||||
selectedMethodsByGroupId[group.groupId] = group.methodId.trim();
|
||||
}
|
||||
}
|
||||
|
||||
const productIds = [
|
||||
...new Set(
|
||||
cart.items.map((item) => item.storeItemVariant.storeItem.product.id),
|
||||
),
|
||||
];
|
||||
const assignments =
|
||||
productIds.length === 0
|
||||
? []
|
||||
: await this.prisma.categoryAssignment.findMany({
|
||||
where: {
|
||||
businessId,
|
||||
entityType: 'product',
|
||||
entityId: { in: productIds },
|
||||
},
|
||||
select: { entityId: true, categoryId: true },
|
||||
});
|
||||
const categoryIdsByProduct = new Map<string, string[]>();
|
||||
for (const row of assignments) {
|
||||
const key = row.entityId.toString();
|
||||
const list = categoryIdsByProduct.get(key) ?? [];
|
||||
list.push(row.categoryId.toString());
|
||||
categoryIdsByProduct.set(key, list);
|
||||
}
|
||||
|
||||
const shippingQuote = await this.shippingCostGroups.quoteForCheckout({
|
||||
businessId,
|
||||
deliveryMode,
|
||||
provinceName:
|
||||
deliveryMode === 'delivery' ? String(shippingAddress.province ?? '') : null,
|
||||
cartItems: cart.items.map((item) => {
|
||||
const product = item.storeItemVariant.storeItem.product;
|
||||
const content = this.asRecord(product.content);
|
||||
const nameFa = (content.nameFa as string | null | undefined)?.trim();
|
||||
return {
|
||||
productName: nameFa || product.title,
|
||||
categoryIds:
|
||||
categoryIdsByProduct.get(product.id.toString()) ?? [],
|
||||
};
|
||||
}),
|
||||
selectedMethodsByGroupId,
|
||||
});
|
||||
|
||||
const enrichedShippingAddress = {
|
||||
...shippingAddress,
|
||||
deliveryMode,
|
||||
shipping: {
|
||||
total: shippingQuote.total,
|
||||
groups: shippingQuote.groups,
|
||||
},
|
||||
};
|
||||
|
||||
const isEPayment = dto.payment.type === TransactionType.e_payment_gate;
|
||||
let gatewayType: PaymentGatewayId | undefined;
|
||||
|
||||
@@ -234,7 +301,8 @@ export class CartService {
|
||||
createdBy: actor.id,
|
||||
source: 'website',
|
||||
cartItems: cart.items,
|
||||
shippingAddress,
|
||||
shippingAddress: enrichedShippingAddress,
|
||||
shippingTotal: shippingQuote.total,
|
||||
addressId: dto.addressId ? BigInt(dto.addressId) : null,
|
||||
customerNotes: dto.customerNotes?.trim() || null,
|
||||
adminNotes: null,
|
||||
@@ -393,8 +461,40 @@ export class CartService {
|
||||
}
|
||||
}
|
||||
|
||||
private serializeCart(cart: CartWithItems) {
|
||||
const items = cart.items.map((item) => this.serializeCartItem(item));
|
||||
private async serializeCart(cart: CartWithItems) {
|
||||
const productIds = [
|
||||
...new Set(
|
||||
cart.items.map((item) => item.storeItemVariant.storeItem.product.id),
|
||||
),
|
||||
];
|
||||
|
||||
const assignments =
|
||||
productIds.length === 0
|
||||
? []
|
||||
: await this.prisma.categoryAssignment.findMany({
|
||||
where: {
|
||||
businessId: cart.businessId,
|
||||
entityType: 'product',
|
||||
entityId: { in: productIds },
|
||||
},
|
||||
select: { entityId: true, categoryId: true },
|
||||
});
|
||||
|
||||
const categoryIdsByProduct = new Map<string, string[]>();
|
||||
for (const row of assignments) {
|
||||
const key = row.entityId.toString();
|
||||
const list = categoryIdsByProduct.get(key) ?? [];
|
||||
list.push(row.categoryId.toString());
|
||||
categoryIdsByProduct.set(key, list);
|
||||
}
|
||||
|
||||
const items = cart.items.map((item) => {
|
||||
const productId = item.storeItemVariant.storeItem.product.id.toString();
|
||||
return this.serializeCartItem(
|
||||
item,
|
||||
categoryIdsByProduct.get(productId) ?? [],
|
||||
);
|
||||
});
|
||||
const subtotal = items.reduce((sum, item) => sum + item.lineTotal, 0);
|
||||
|
||||
return {
|
||||
@@ -407,7 +507,10 @@ export class CartService {
|
||||
};
|
||||
}
|
||||
|
||||
private serializeCartItem(item: CartWithItems['items'][number]) {
|
||||
private serializeCartItem(
|
||||
item: CartWithItems['items'][number],
|
||||
categoryIds: string[],
|
||||
) {
|
||||
const variant = item.storeItemVariant;
|
||||
const product = variant.storeItem.product;
|
||||
const content = this.asRecord(product.content);
|
||||
@@ -431,6 +534,7 @@ export class CartService {
|
||||
productTitle: product.title,
|
||||
productNameFa: (content.nameFa as string | null | undefined) ?? '',
|
||||
productImage: product.featuredMedia?.publicUrl ?? null,
|
||||
categoryIds,
|
||||
sku: variant.sku,
|
||||
selections,
|
||||
label: selections.map((entry) => entry.value).join(' · ') || product.title,
|
||||
|
||||
Reference in New Issue
Block a user