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
@@ -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 CreateWebsiteBrandGroupDto {
@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 CreateWebsiteBrandGroupDto {
}
export class UpdateWebsiteBrandGroupDto {
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(100)
@Matches(SPECIAL_GROUP_KEY_PATTERN, { message: SPECIAL_GROUP_KEY_MESSAGE })
key?: string;
@IsOptional()
@IsString()
@MinLength(1)
@@ -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 CreateWebsiteCategoryGroupDto {
@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 CreateWebsiteCategoryGroupDto {
}
export class UpdateWebsiteCategoryGroupDto {
@IsOptional()
@IsString()
@MinLength(1)
@MaxLength(100)
@Matches(SPECIAL_GROUP_KEY_PATTERN, { message: SPECIAL_GROUP_KEY_MESSAGE })
key?: string;
@IsOptional()
@IsString()
@MinLength(1)
@@ -1,5 +1,6 @@
import {
BadRequestException,
ConflictException,
ForbiddenException,
Injectable,
NotFoundException,
@@ -7,6 +8,7 @@ import {
import { 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 {
@@ -113,10 +115,14 @@ export class WebsiteBrandGroupsService {
await this.assertBrandsBelongToBusiness(businessId, brandIds);
}
const key = normalizeSpecialGroupKey(dto.key);
await this.assertKeyAvailable(businessId, key);
const created = await this.prisma.$transaction(async (tx) => {
const group = await tx.website_brand_groups.create({
data: {
business_id: businessId,
key,
title: dto.title.trim(),
sort_order: dto.sortOrder ?? 0,
is_active: dto.isActive ?? true,
@@ -154,10 +160,17 @@ export class WebsiteBrandGroupsService {
await this.assertBrandsBelongToBusiness(businessId, brandIds);
}
const nextKey =
dto.key !== undefined ? normalizeSpecialGroupKey(dto.key) : undefined;
if (nextKey !== undefined) {
await this.assertKeyAvailable(businessId, nextKey, groupId);
}
const updated = await this.prisma.$transaction(async (tx) => {
await tx.website_brand_groups.update({
where: { id: groupId },
data: {
...(nextKey !== undefined ? { key: nextKey } : {}),
...(dto.title !== undefined ? { title: dto.title.trim() } : {}),
...(dto.sortOrder !== undefined ? { sort_order: dto.sortOrder } : {}),
...(dto.isActive !== undefined ? { is_active: dto.isActive } : {}),
@@ -266,6 +279,7 @@ export class WebsiteBrandGroupsService {
return {
id: group.id.toString(),
key: group.key,
title: group.title,
sortOrder: group.sort_order,
isActive: group.is_active,
@@ -275,6 +289,24 @@ export class WebsiteBrandGroupsService {
};
}
private async assertKeyAvailable(
businessId: bigint,
key: string,
excludeId?: bigint,
) {
const existing = await this.prisma.website_brand_groups.findFirst({
where: {
business_id: businessId,
key,
...(excludeId !== undefined ? { id: { not: excludeId } } : {}),
},
select: { id: true },
});
if (existing) {
throw new ConflictException('A brand group with this key already exists');
}
}
private async assertPermission(
businessId: bigint,
userId: bigint,
@@ -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 {
@@ -111,10 +113,14 @@ export class WebsiteCategoryGroupsService {
await this.assertCategoriesBelongToBusiness(businessId, categoryIds);
}
const key = normalizeSpecialGroupKey(dto.key);
await this.assertKeyAvailable(businessId, key);
const created = await this.prisma.$transaction(async (tx) => {
const group = await tx.website_category_groups.create({
data: {
business_id: businessId,
key,
title: dto.title.trim(),
sort_order: dto.sortOrder ?? 0,
is_active: dto.isActive ?? true,
@@ -152,10 +158,17 @@ export class WebsiteCategoryGroupsService {
await this.assertCategoriesBelongToBusiness(businessId, categoryIds);
}
const nextKey =
dto.key !== undefined ? normalizeSpecialGroupKey(dto.key) : undefined;
if (nextKey !== undefined) {
await this.assertKeyAvailable(businessId, nextKey, groupId);
}
const updated = await this.prisma.$transaction(async (tx) => {
await tx.website_category_groups.update({
where: { id: groupId },
data: {
...(nextKey !== undefined ? { key: nextKey } : {}),
...(dto.title !== undefined ? { title: dto.title.trim() } : {}),
...(dto.sortOrder !== undefined ? { sort_order: dto.sortOrder } : {}),
...(dto.isActive !== undefined ? { is_active: dto.isActive } : {}),
@@ -265,6 +278,7 @@ export class WebsiteCategoryGroupsService {
return {
id: group.id.toString(),
key: group.key,
title: group.title,
sortOrder: group.sort_order,
isActive: group.is_active,
@@ -274,6 +288,24 @@ export class WebsiteCategoryGroupsService {
};
}
private async assertKeyAvailable(
businessId: bigint,
key: string,
excludeId?: bigint,
) {
const existing = await this.prisma.website_category_groups.findFirst({
where: {
business_id: businessId,
key,
...(excludeId !== undefined ? { id: { not: excludeId } } : {}),
},
select: { id: true },
});
if (existing) {
throw new ConflictException('A category group with this key already exists');
}
}
private async assertPermission(
businessId: bigint,
userId: bigint,