Add stable keys for special groups and SSL sync agent hooks.

English keys on store/website specials stay unique per business; domain-admin can trigger dashboard SSL sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-03 00:06:44 +03:30
co-authored by Cursor
parent 43e3912662
commit f233665d13
15 changed files with 263 additions and 6 deletions
+18
View File
@@ -5,12 +5,23 @@ import {
IsInt,
IsOptional,
IsString,
Matches,
MaxLength,
Min,
MinLength,
} from 'class-validator';
import {
SPECIAL_GROUP_KEY_MESSAGE,
SPECIAL_GROUP_KEY_PATTERN,
} from '../../common/special-group-key';
export class CreateStoreSpecialDto {
@IsString()
@MinLength(1)
@MaxLength(100)
@Matches(SPECIAL_GROUP_KEY_PATTERN, { message: SPECIAL_GROUP_KEY_MESSAGE })
key!: string;
@IsString()
@MinLength(1)
@MaxLength(255)
@@ -31,6 +42,13 @@ export class CreateStoreSpecialDto {
}
export class UpdateStoreSpecialDto {
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(100)
@Matches(SPECIAL_GROUP_KEY_PATTERN, { message: SPECIAL_GROUP_KEY_MESSAGE })
key?: string;
@IsOptional()
@IsString()
@MinLength(1)
+32
View File
@@ -1,5 +1,6 @@
import {
BadRequestException,
ConflictException,
ForbiddenException,
Injectable,
NotFoundException,
@@ -7,6 +8,7 @@ import {
import { MediaEntityType, Prisma } from '@prisma/client';
import { AuthUser } from '../auth/auth.types';
import { PermissionsService } from '../auth/permissions.service';
import { normalizeSpecialGroupKey } from '../common/special-group-key';
import { PrismaService } from '../prisma/prisma.service';
import { TenantService } from '../tenant/tenant.service';
import {
@@ -150,10 +152,14 @@ export class StoreSpecialsService {
await this.assertStoreItemsBelongToBusiness(businessId, storeItemIds);
}
const key = normalizeSpecialGroupKey(dto.key);
await this.assertKeyAvailable(businessId, key);
const created = await this.prisma.$transaction(async (tx) => {
const special = await tx.storeSpecial.create({
data: {
businessId,
key,
title: dto.title.trim(),
sortOrder: dto.sortOrder ?? 0,
isActive: dto.isActive ?? true,
@@ -196,10 +202,17 @@ export class StoreSpecialsService {
await this.assertStoreItemsBelongToBusiness(businessId, storeItemIds);
}
const nextKey =
dto.key !== undefined ? normalizeSpecialGroupKey(dto.key) : undefined;
if (nextKey !== undefined) {
await this.assertKeyAvailable(businessId, nextKey, specialId);
}
const updated = await this.prisma.$transaction(async (tx) => {
await tx.storeSpecial.update({
where: { id: specialId },
data: {
...(nextKey !== undefined ? { key: nextKey } : {}),
...(dto.title !== undefined ? { title: dto.title.trim() } : {}),
...(dto.sortOrder !== undefined ? { sortOrder: dto.sortOrder } : {}),
...(dto.isActive !== undefined ? { isActive: dto.isActive } : {}),
@@ -341,6 +354,7 @@ export class StoreSpecialsService {
return {
id: special.id.toString(),
key: special.key,
title: special.title,
sortOrder: special.sortOrder,
isActive: special.isActive,
@@ -350,6 +364,24 @@ export class StoreSpecialsService {
};
}
private async assertKeyAvailable(
businessId: bigint,
key: string,
excludeId?: bigint,
) {
const existing = await this.prisma.storeSpecial.findFirst({
where: {
businessId,
key,
...(excludeId !== undefined ? { id: { not: excludeId } } : {}),
},
select: { id: true },
});
if (existing) {
throw new ConflictException('A special with this key already exists');
}
}
private serializeStoreItem(
storeItem: Prisma.StoreItemGetPayload<{ include: typeof storeItemInclude }>,
galleryByProduct: Map<string, string>,