Add page-view analytics, product counters, SSL expiry, and business renewal.

Auto-count storefront page views with 6-month event retention and lifetime counters, track product visit/cart/sale stats, persist SSL cert expiry with daily probes, and support owner titles plus annual contract renewal.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-26 10:13:06 +03:30
co-authored by Cursor
parent 6cd0b009ef
commit 92601ae729
39 changed files with 1013 additions and 198 deletions
+44 -9
View File
@@ -59,6 +59,7 @@ type BusinessRow = {
about: string | null;
slug: string;
createdAt: Date;
annualRenewalAt: Date | null;
isActive: boolean;
oldBusinessId: bigint | null;
domainId: bigint | null;
@@ -142,6 +143,7 @@ export class BusinessAdminService {
b.about AS "about",
b.slug AS "slug",
b.created_at AS "createdAt",
b.annual_renewal_at AS "annualRenewalAt",
b.is_active AS "isActive",
b.old_business_id AS "oldBusinessId",
dom.id AS "domainId",
@@ -372,6 +374,9 @@ export class BusinessAdminService {
nameFa: dto.nameFa.trim(),
about: dto.about?.trim() ?? null,
slug,
annualRenewalAt: dto.annualRenewalAt
? new Date(dto.annualRenewalAt)
: null,
oldBusinessId:
dto.oldBusinessId !== undefined ? BigInt(dto.oldBusinessId) : undefined,
settings: toPrismaJson({
@@ -455,6 +460,14 @@ export class BusinessAdminService {
dto.oldBusinessId === null ? null : BigInt(dto.oldBusinessId),
}
: {}),
...(dto.annualRenewalAt !== undefined
? {
annualRenewalAt:
dto.annualRenewalAt === null
? null
: new Date(dto.annualRenewalAt),
}
: {}),
},
});
@@ -1075,6 +1088,7 @@ export class BusinessAdminService {
tx: Prisma.TransactionClient,
dto: Pick<
CreateBusinessDto,
| 'ownerTitleId'
| 'ownerFirstName'
| 'ownerLastName'
| 'ownerFirstNameEn'
@@ -1087,26 +1101,45 @@ export class BusinessAdminService {
where: { cellNumber: dto.ownerCellNumber },
});
let titleId: bigint | null | undefined = undefined;
if (dto.ownerTitleId !== undefined) {
const title = await tx.userTitle.findFirst({
where: { id: BigInt(dto.ownerTitleId), isActive: true },
select: { id: true },
});
if (!title) {
throw new BadRequestException('Owner title not found');
}
titleId = title.id;
}
const passwordHash = await bcrypt.hash(dto.ownerPassword, 10);
const nameData = {
...(titleId !== undefined ? { titleId } : {}),
firstName: dto.ownerFirstName.trim(),
lastName: dto.ownerLastName.trim(),
firstNameEn: dto.ownerFirstNameEn.trim(),
lastNameEn: dto.ownerLastNameEn.trim(),
passwordHash,
};
// Users can own multiple businesses. If the cell number already exists,
// leave that user unchanged and only attach the new business as owner.
// refresh name + password from the create form, then attach as owner.
if (existing) {
if (!existing.isActive) {
throw new BadRequestException('Owner user is inactive');
}
return existing;
return tx.user.update({
where: { id: existing.id },
data: nameData,
});
}
const passwordHash = await bcrypt.hash(dto.ownerPassword, 10);
return tx.user.create({
data: {
cellNumber: dto.ownerCellNumber,
passwordHash,
firstName: dto.ownerFirstName.trim(),
lastName: dto.ownerLastName.trim(),
firstNameEn: dto.ownerFirstNameEn.trim(),
lastNameEn: dto.ownerLastNameEn.trim(),
cellVerifiedAt: new Date(),
...nameData,
},
});
}
@@ -1176,6 +1209,7 @@ export class BusinessAdminService {
slug: string;
isActive: boolean;
oldBusinessId: bigint | null;
annualRenewalAt: Date | null;
createdAt: Date;
updatedAt: Date;
categoryAssignments: {
@@ -1217,6 +1251,7 @@ export class BusinessAdminService {
isActive: business.isActive,
oldBusinessId:
business.oldBusinessId != null ? business.oldBusinessId.toString() : null,
annualRenewalAt: business.annualRenewalAt,
createdAt: business.createdAt,
updatedAt: business.updatedAt,
categories: business.categoryAssignments.map((a) => ({