Compare commits

...

2 Commits

Author SHA1 Message Date
ahasani d526f6ed2c feat: enhance consumer selection to include partner details for individual and legal types 2026-05-19 16:11:59 +03:30
ahasani 62b659246f feat: implement Redis caching for business activities and consumers
- Added Redis caching to BusinessActivitiesService for findAll and findOne methods.
- Integrated Redis caching in BusinessActivityComplexesService for findAll and findOne methods.
- Enhanced ConsumersService with Redis caching for findAll and findOne methods.
- Introduced cache invalidation for partner consumers and business activities.
- Created RedisKeyMaker utility for generating cache keys for consumers, partners, and POS.
- Implemented cache invalidation services for partners and POS.
- Added Redis service methods for JSON handling and key deletion by patterns.
- Updated goods service to include caching and invalidation for goods list.
- Introduced DTO for updating goods.
2026-05-19 15:40:45 +03:30
33 changed files with 1166 additions and 42 deletions
+27
View File
@@ -102,3 +102,30 @@ Operational guide for AI/coding agents working in `consumer_api`.
- Duplicate FK error seen in this repo: `sales_invoices_ref_id_fkey` (MySQL 1826). Recheck migration SQL for repeated `ADD CONSTRAINT` statements before rerun. - Duplicate FK error seen in this repo: `sales_invoices_ref_id_fkey` (MySQL 1826). Recheck migration SQL for repeated `ADD CONSTRAINT` statements before rerun.
- Drift that repeatedly showed in this project: missing FK/unique on `sale_invoice_tsp_attempts.invoice_id`. Validate both schema and migration SQL produce the same final state. - Drift that repeatedly showed in this project: missing FK/unique on `sale_invoice_tsp_attempts.invoice_id`. Validate both schema and migration SQL produce the same final state.
- `prisma migrate status` may show up-to-date while `migrate dev` still detects drift (shadow DB/application history issue); treat `migrate dev` output as source of truth for fixing local history. - `prisma migrate status` may show up-to-date while `migrate dev` still detects drift (shadow DB/application history issue); treat `migrate dev` output as source of truth for fixing local history.
## Redis Cache Conventions (May 2026)
- Use `RedisKeyMaker` in `src/common/utils/redis-key-maker.util.ts` for all cache keys and wildcard patterns; do not inline key strings in services.
- Keep invalidation domain-based (for example `src/modules/admin/guilds/cache/*`, `src/modules/admin/partners/cache/*`, `src/modules/pos/cache/*`) and avoid duplicating delete logic across modules.
- For list APIs that are expensive or frequently read, prefer read-through cache with TTL and invalidation on every related write path.
- For entity endpoints, use list + detail split:
- list key(s): invalidated broadly on writes,
- detail key(s): invalidated per entity id.
- Partner domain rules:
- Shared partner cache namespace is `partners:*` (not admin-prefixed) because writes occur in both `admin/partners` and `partners` modules.
- Use shared invalidation service `src/modules/partners/cache/partners-cache-invalidation.service.ts` from both admin and partner write paths.
- Invalidate `partners:list` and `partners:{id}:detail` on partner create/update/delete and license-affecting writes.
- Invalidate partner license caches (activated-licenses list, charge-transactions list/detail) via the shared invalidation service.
- POS goods rules:
- Cache key shape is BA + guild scoped (`pos:ba:{businessActivityId}:guild:{guildId}:goods:list`) because results combine guild-default and BA-owned goods.
- Invalidate POS goods cache by guild when admin guild goods/category/sku changes.
- Invalidate POS goods cache by business activity when consumer BA goods create/update/delete.
- Consumer/Partner hierarchy rules:
- Consumer profile cache key: `consumers:{consumerId}:info`.
- Partner-consumer profile cache key: `partners:{partnerId}:consumers:{consumerId}:info`.
- On partner-consumer single read, write both keys when practical to share warm cache across modules.
- On consumer info update or partner-consumer update, invalidate both consumer and partner-consumer profile keys.
- For business activities, invalidate both list and detail layers; child updates may invalidate parent single cache when parent aggregates depend on child state.
- Wildcard invalidation implementation:
- Use `RedisService.deleteByPattern` / `RedisService.deleteByPatterns` for all pattern deletes.
- Do not implement ad-hoc scan/delete loops inside domain services.
- Pattern deletion uses `SCAN` + pipelined `DEL`; multi-pattern deletes run in parallel.
+3 -2
View File
@@ -1,6 +1,7 @@
export * from './jwt-user.util'
export * from './enum-translator.util' export * from './enum-translator.util'
export * from './http-client.util'
export * from './jwt-user.util'
export * from './mappers/consumer_mappers.util' export * from './mappers/consumer_mappers.util'
export * from './password.util' export * from './password.util'
export * from './redisKeyMaker'
export * from './tracking-code-generator.util' export * from './tracking-code-generator.util'
export * from './http-client.util'
@@ -0,0 +1,16 @@
export class ConsumerKeyMaker {
static consumerInfo(consumerId: string): string {
return `consumers:${consumerId}:info`
}
static consumerBusinessActivitiesList(consumerId: string): string {
return `consumers:${consumerId}:business-activities:list`
}
static consumerBusinessActivityInfo(
consumerId: string,
businessActivityId: string,
): string {
return `consumers:${consumerId}:business-activities:${businessActivityId}:info`
}
}
+9
View File
@@ -0,0 +1,9 @@
export class EnumKeyMaker {
static enumsAll(buildVersion: string): string {
return `enums:${buildVersion}:all`
}
static enumsValues(buildVersion: string, enumName: string): string {
return `enums:${buildVersion}:values:${enumName}`
}
}
+9
View File
@@ -0,0 +1,9 @@
export class GuildKeyMaker {
static guildStockKeepingUnitsList(guildId: string): string {
return `guilds:${guildId}:stock-keeping-units:list`
}
static guildGoodsList(guildId: string): string {
return `guilds:${guildId}:goods:list`
}
}
+30
View File
@@ -0,0 +1,30 @@
import { ConsumerKeyMaker } from './consumer'
import { EnumKeyMaker } from './enum'
import { GuildKeyMaker } from './guild'
import { PartnerKeyMaker } from './partner'
import { PosKeyMaker } from './pos'
// Keep backward-compatible static methods while separating key builders by domain.
export class RedisKeyMaker extends PartnerKeyMaker {
static consumer = ConsumerKeyMaker
static enums = EnumKeyMaker
static guild = GuildKeyMaker
static partner = PartnerKeyMaker
static pos = PosKeyMaker
static consumerInfo = ConsumerKeyMaker.consumerInfo
static consumerBusinessActivitiesList = ConsumerKeyMaker.consumerBusinessActivitiesList
static consumerBusinessActivityInfo = ConsumerKeyMaker.consumerBusinessActivityInfo
static guildStockKeepingUnitsList = GuildKeyMaker.guildStockKeepingUnitsList
static guildGoodsList = GuildKeyMaker.guildGoodsList
static posInfo = PosKeyMaker.posInfo
static posMe = PosKeyMaker.posMe
static posGoodsList = PosKeyMaker.posGoodsList
static enumsAll = EnumKeyMaker.enumsAll
static enumsValues = EnumKeyMaker.enumsValues
}
export { ConsumerKeyMaker, EnumKeyMaker, GuildKeyMaker, PartnerKeyMaker, PosKeyMaker }
+135
View File
@@ -0,0 +1,135 @@
export class PartnerKeyMaker {
static partnersList(): string {
return 'partners:list'
}
static partnerDetail(partnerId: string): string {
return `partners:${partnerId}:detail`
}
static partnerActivatedLicensesList(
partnerId: string,
page: number,
perPage: number,
): string {
return `partners:${partnerId}:activated-licenses:list:${page}:${perPage}`
}
static partnerActivatedLicensesListPattern(partnerId: string): string {
return `partners:${partnerId}:activated-licenses:list:*`
}
static partnerLicenseChargeTransactionsList(
partnerId: string,
page: number,
perPage: number,
): string {
return `partners:${partnerId}:license-charge-transactions:list:${page}:${perPage}`
}
static partnerLicenseChargeTransactionsListPattern(partnerId: string): string {
return `partners:${partnerId}:license-charge-transactions:list:*`
}
static partnerLicenseChargeTransactionDetail(
partnerId: string,
transactionId: string,
): string {
return `partners:${partnerId}:license-charge-transactions:${transactionId}:detail`
}
static partnerLicenseChargeTransactionDetailPattern(partnerId: string): string {
return `partners:${partnerId}:license-charge-transactions:*:detail`
}
//////////////////// consumers ////////////////////
static partnerConsumersPattern(partnerId: string): string {
return `partners:${partnerId}:consumers:*`
}
static partnerConsumerPattern(partnerId: string): string {
return this.partnerConsumersPattern(partnerId)
}
static partnerConsumersListPattern(partnerId: string): string {
return `partners:${partnerId}:consumers:list:*`
}
static partnerConsumersList(partnerId: string, page: number, perPage: number): string {
return `partners:${partnerId}:consumers:list:${page}:${perPage}`
}
static partnerConsumerInfo(partnerId: string, consumerId: string): string {
return `partners:${partnerId}:consumers:${consumerId}:info`
}
static partnerConsumerBusinessActivitiesList(
partnerId: string,
consumerId: string,
page: number,
perPage: number,
): string {
return `partners:${partnerId}:consumers:${consumerId}:business-activities:list:${page}:${perPage}`
}
static partnerConsumerBusinessActivitiesPattern(
partnerId: string,
consumerId: string,
): string {
return `partners:${partnerId}:consumers:${consumerId}:business-activities:*`
}
static partnerConsumerBusinessActivityInfo(
partnerId: string,
consumerId: string,
businessActivityId: string,
): string {
return `partners:${partnerId}:consumers:${consumerId}:business-activities:${businessActivityId}:info`
}
static partnerConsumerBusinessActivityComplexesList(
partnerId: string,
consumerId: string,
businessActivityId: string,
page: number,
perPage: number,
): string {
return `partners:${partnerId}:consumers:${consumerId}:business-activities:${businessActivityId}:complexes:list:${page}:${perPage}`
}
static partnerConsumerBusinessActivityComplexesPattern(
partnerId: string,
consumerId: string,
businessActivityId: string,
): string {
return `partners:${partnerId}:consumers:${consumerId}:business-activities:${businessActivityId}:complexes:*`
}
static partnerConsumerBusinessActivityComplexInfo(
partnerId: string,
consumerId: string,
businessActivityId: string,
complexId: string,
): string {
return `partners:${partnerId}:consumers:${consumerId}:business-activities:${businessActivityId}:complexes:${complexId}`
}
static partnerConsumerBusinessActivityComplexPosesPattern(
partnerId: string,
consumerId: string,
businessActivityId: string,
complexId: string,
): string {
return `partners:${partnerId}:consumers:${consumerId}:business-activities:${businessActivityId}:complexes:${complexId}:poses:*`
}
static partnerConsumerBusinessActivityComplexPosInfo(
partnerId: string,
consumerId: string,
businessActivityId: string,
complexId: string,
posId: string,
): string {
return `partners:${partnerId}:consumers:${consumerId}:business-activities:${businessActivityId}:complexes:${complexId}:poses:${posId}`
}
}
+13
View File
@@ -0,0 +1,13 @@
export class PosKeyMaker {
static posInfo(posId: string): string {
return `pos:${posId}:info`
}
static posMe(accountId: string, posId: string): string {
return `pos:${posId}:me:${accountId}`
}
static posGoodsList(businessActivityId: string, guildId: string): string {
return `pos:ba:${businessActivityId}:guild:${guildId}:goods:list`
}
}
@@ -14,6 +14,26 @@ export class AdminConsumersService {
type: true, type: true,
...QUERY_CONSTANTS.CONSUMER.infoSelect, ...QUERY_CONSTANTS.CONSUMER.infoSelect,
...QUERY_CONSTANTS.CONSUMER.activeBusinessCount, ...QUERY_CONSTANTS.CONSUMER.activeBusinessCount,
individual: {
select: {
partner: {
select: {
id: true,
name: true,
},
},
},
},
legal: {
select: {
partner: {
select: {
id: true,
name: true,
},
},
},
},
status: true, status: true,
created_at: true, created_at: true,
} }
@@ -0,0 +1,17 @@
import { RedisKeyMaker } from '@/common/utils/redisKeyMaker'
import { PosCacheInvalidationService } from '@/modules/pos/cache/pos-cache-invalidation.service'
import { RedisService } from '@/redis/redis.service'
import { Injectable } from '@nestjs/common'
@Injectable()
export class AdminGuildCacheInvalidationService {
constructor(
private readonly redisService: RedisService,
private readonly posCacheInvalidationService: PosCacheInvalidationService,
) {}
async invalidateGoodsList(guildId: string): Promise<void> {
await this.redisService.delete(RedisKeyMaker.guildGoodsList(guildId))
await this.posCacheInvalidationService.invalidateGoodsListByGuild(guildId)
}
}
@@ -1,5 +1,6 @@
import { GoodCategoryOmit } from '@/generated/prisma/models' import { GoodCategoryOmit } from '@/generated/prisma/models'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { AdminGuildCacheInvalidationService } from '@/modules/admin/guilds/cache/admin-guild-cache-invalidation.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
import { import {
@@ -9,7 +10,10 @@ import {
@Injectable() @Injectable()
export class GoodCategoriesService { export class GoodCategoriesService {
constructor(private prisma: PrismaService) {} constructor(
private prisma: PrismaService,
private readonly cacheInvalidationService: AdminGuildCacheInvalidationService,
) {}
private readonly goodCategoriesOmit = { private readonly goodCategoriesOmit = {
complex_id: true, complex_id: true,
@@ -67,8 +71,8 @@ export class GoodCategoriesService {
}) })
} }
create(guild_id: string, data: CreateGoodCategoryDto) { async create(guild_id: string, data: CreateGoodCategoryDto) {
const category = this.prisma.goodCategory.create({ const category = await this.prisma.goodCategory.create({
data: { data: {
guild: { guild: {
connect: { connect: {
@@ -83,6 +87,8 @@ export class GoodCategoriesService {
omit: this.goodCategoriesOmit, omit: this.goodCategoriesOmit,
}) })
await this.cacheInvalidationService.invalidateGoodsList(guild_id)
return ResponseMapper.create({ ...category, goods_count: 0 }) return ResponseMapper.create({ ...category, goods_count: 0 })
} }
@@ -113,6 +119,8 @@ export class GoodCategoriesService {
omit: this.goodCategoriesOmit, omit: this.goodCategoriesOmit,
}) })
await this.cacheInvalidationService.invalidateGoodsList(guild_id)
return ResponseMapper.update({ return ResponseMapper.update({
...category, ...category,
goods_count: Number(category?._count.goods), goods_count: Number(category?._count.goods),
@@ -1,7 +1,10 @@
import { UploadedFileTypes } from '@/common/enums/enums' import { UploadedFileTypes } from '@/common/enums/enums'
import { RedisKeyMaker } from '@/common/utils/redisKeyMaker'
import { GoodSelect, GoodWhereInput } from '@/generated/prisma/models' import { GoodSelect, GoodWhereInput } from '@/generated/prisma/models'
import { AdminGuildCacheInvalidationService } from '@/modules/admin/guilds/cache/admin-guild-cache-invalidation.service'
import { UploaderService } from '@/modules/uploader/uploader.service' import { UploaderService } from '@/modules/uploader/uploader.service'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { RedisService } from '@/redis/redis.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
import { CreateGuildGoodDto, UpdateGuildGoodDto } from './dto/create-good.dto' import { CreateGuildGoodDto, UpdateGuildGoodDto } from './dto/create-good.dto'
@@ -11,8 +14,12 @@ export class GoodsService {
constructor( constructor(
private prisma: PrismaService, private prisma: PrismaService,
private readonly uploaderService: UploaderService, private readonly uploaderService: UploaderService,
private readonly redisService: RedisService,
private readonly cacheInvalidationService: AdminGuildCacheInvalidationService,
) {} ) {}
private readonly listCacheTtlSeconds = 300
private readonly defaultSelect: GoodSelect = { private readonly defaultSelect: GoodSelect = {
id: true, id: true,
name: true, name: true,
@@ -49,6 +56,14 @@ export class GoodsService {
}) })
async findAll(guildId: string) { async findAll(guildId: string) {
const cacheKey = RedisKeyMaker.guildGoodsList(guildId)
const cached = await this.redisService.getJson<{ goods: unknown[]; total: number }>(
cacheKey,
)
if (cached) {
return ResponseMapper.paginate(cached.goods, { total: cached.total })
}
const [goods, total] = await this.prisma.$transaction([ const [goods, total] = await this.prisma.$transaction([
this.prisma.good.findMany({ this.prisma.good.findMany({
where: this.defaultWhere(guildId), where: this.defaultWhere(guildId),
@@ -56,6 +71,9 @@ export class GoodsService {
}), }),
this.prisma.good.count(), this.prisma.good.count(),
]) ])
await this.redisService.setJson(cacheKey, { goods, total }, this.listCacheTtlSeconds)
return ResponseMapper.paginate(goods, { return ResponseMapper.paginate(goods, {
total, total,
}) })
@@ -75,6 +93,10 @@ export class GoodsService {
async create(data: CreateGuildGoodDto, file: Express.Multer.File) { async create(data: CreateGuildGoodDto, file: Express.Multer.File) {
const { category_id, measure_unit_id, sku_id, ...rest } = data const { category_id, measure_unit_id, sku_id, ...rest } = data
const category = await this.prisma.goodCategory.findUnique({
where: { id: category_id },
select: { guild_id: true },
})
const good = await this.prisma.$transaction(async tx => { const good = await this.prisma.$transaction(async tx => {
let image_url = '' let image_url = ''
@@ -111,11 +133,25 @@ export class GoodsService {
}) })
}) })
if (category?.guild_id) {
await this.cacheInvalidationService.invalidateGoodsList(category.guild_id)
}
return ResponseMapper.create(good) return ResponseMapper.create(good)
} }
async update(id: string, data: UpdateGuildGoodDto, file: Express.Multer.File) { async update(id: string, data: UpdateGuildGoodDto, file: Express.Multer.File) {
const { category_id, measure_unit_id, sku_id, ...rest } = data const { category_id, measure_unit_id, sku_id, ...rest } = data
const prevGood = await this.prisma.good.findUnique({
where: { id },
select: { category: { select: { guild_id: true } } },
})
const nextCategory = category_id
? await this.prisma.goodCategory.findUnique({
where: { id: category_id },
select: { guild_id: true },
})
: null
const good = await this.prisma.$transaction(async tx => { const good = await this.prisma.$transaction(async tx => {
let image_url = '' let image_url = ''
@@ -162,6 +198,18 @@ export class GoodsService {
}) })
}) })
const cacheGuildIds = new Set<string>()
if (prevGood?.category?.guild_id) {
cacheGuildIds.add(prevGood.category.guild_id)
}
if (nextCategory?.guild_id) {
cacheGuildIds.add(nextCategory.guild_id)
}
for (const guildId of cacheGuildIds) {
await this.cacheInvalidationService.invalidateGoodsList(guildId)
}
return ResponseMapper.create(good) return ResponseMapper.create(good)
} }
} }
@@ -1,4 +1,7 @@
import { RedisKeyMaker } from '@/common/utils/redisKeyMaker'
import { AdminGuildCacheInvalidationService } from '@/modules/admin/guilds/cache/admin-guild-cache-invalidation.service'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { RedisService } from '@/redis/redis.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
import { CreateStockKeepingUnitDto } from './dto/create-stock-keeping-unit.dto' import { CreateStockKeepingUnitDto } from './dto/create-stock-keeping-unit.dto'
@@ -6,7 +9,17 @@ import { UpdateStockKeepingUnitDto } from './dto/update-stock-keeping-unit.dto'
@Injectable() @Injectable()
export class StockKeepingUnitsService { export class StockKeepingUnitsService {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
private readonly redisService: RedisService,
private readonly cacheInvalidationService: AdminGuildCacheInvalidationService,
) {}
private readonly listCacheTtlSeconds = 300
private getListCacheKey(guild_id: string): string {
return RedisKeyMaker.guildStockKeepingUnitsList(guild_id)
}
private mapData(data: any) { private mapData(data: any) {
const { name, is_foreign, is_domestic, ...rest } = data const { name, is_foreign, is_domestic, ...rest } = data
@@ -17,12 +30,19 @@ export class StockKeepingUnitsService {
} }
async findAll(guild_id: string) { async findAll(guild_id: string) {
const cacheKey = this.getListCacheKey(guild_id)
const cached = await this.redisService.getJson<unknown[]>(cacheKey)
if (cached) {
return ResponseMapper.list(cached)
}
const items = await this.prisma.stockKeepingUnits.findMany({ const items = await this.prisma.stockKeepingUnits.findMany({
where: { guild_id }, where: { guild_id },
orderBy: { created_at: 'desc' }, orderBy: { created_at: 'desc' },
}) })
const mappedData = items.map(this.mapData) const mappedData = items.map(this.mapData)
await this.redisService.setJson(cacheKey, mappedData, this.listCacheTtlSeconds)
return ResponseMapper.list(mappedData) return ResponseMapper.list(mappedData)
} }
@@ -39,6 +59,9 @@ export class StockKeepingUnitsService {
}, },
}) })
await this.redisService.delete(this.getListCacheKey(guild_id))
await this.cacheInvalidationService.invalidateGoodsList(guild_id)
const mappedData = this.mapData(item) const mappedData = this.mapData(item)
return ResponseMapper.create(mappedData) return ResponseMapper.create(mappedData)
} }
@@ -48,6 +71,10 @@ export class StockKeepingUnitsService {
where: { guild_id, id }, where: { guild_id, id },
data, data,
}) })
await this.redisService.delete(this.getListCacheKey(guild_id))
await this.cacheInvalidationService.invalidateGoodsList(guild_id)
return ResponseMapper.update(item) return ResponseMapper.update(item)
} }
} }
@@ -1,15 +1,30 @@
import { QUERY_CONSTANTS } from '@/common/queryConstants' import { QUERY_CONSTANTS } from '@/common/queryConstants'
import consumer_mappersUtil from '@/common/utils/mappers/consumer_mappers.util' import consumer_mappersUtil from '@/common/utils/mappers/consumer_mappers.util'
import { RedisKeyMaker } from '@/common/utils/redisKeyMaker'
import { LicenseActivationWhereInput } from '@/generated/prisma/models' import { LicenseActivationWhereInput } from '@/generated/prisma/models'
import { PartnersCacheInvalidationService } from '@/modules/partners/cache/partners-cache-invalidation.service'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { RedisService } from '@/redis/redis.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
@Injectable() @Injectable()
export class PartnerActivatedLicensesService { export class PartnerActivatedLicensesService {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
private readonly redisService: RedisService,
private readonly cacheInvalidationService: PartnersCacheInvalidationService,
) {}
async findAll(partner_id: string, page = 1, perPage = 10) { async findAll(partner_id: string, page = 1, perPage = 10) {
const cacheKey = RedisKeyMaker.partnerActivatedLicensesList(partner_id, page, perPage)
const cached = await this.redisService.getJson<{ data: unknown[]; total: number }>(
cacheKey,
)
if (cached) {
return ResponseMapper.paginate(cached.data, { page, perPage, total: cached.total })
}
const defaultWhere: LicenseActivationWhereInput = { const defaultWhere: LicenseActivationWhereInput = {
license: { license: {
charge_transaction: { charge_transaction: {
@@ -73,6 +88,8 @@ export class PartnerActivatedLicensesService {
} }
}) })
await this.redisService.setJson(cacheKey, { data: mappedLicenses, total }, 300)
return ResponseMapper.paginate(mappedLicenses, { return ResponseMapper.paginate(mappedLicenses, {
page, page,
perPage, perPage,
@@ -90,6 +107,7 @@ export class PartnerActivatedLicensesService {
async delete(partnerId: string, id: string) { async delete(partnerId: string, id: string) {
await this.prisma.license.delete({ where: { id } }) await this.prisma.license.delete({ where: { id } })
await this.cacheInvalidationService.invalidatePartnerLicenses(partnerId)
return ResponseMapper.delete() return ResponseMapper.delete()
} }
} }
@@ -1,3 +1,4 @@
import { RedisKeyMaker } from '@/common/utils/redisKeyMaker'
import { import {
generateTrackingCode, generateTrackingCode,
isTrackingCodeUniqueViolation, isTrackingCodeUniqueViolation,
@@ -6,14 +7,20 @@ import {
LicenseChargeTransactionSelect, LicenseChargeTransactionSelect,
LicenseChargeTransactionWhereInput, LicenseChargeTransactionWhereInput,
} from '@/generated/prisma/models' } from '@/generated/prisma/models'
import { PartnersCacheInvalidationService } from '@/modules/partners/cache/partners-cache-invalidation.service'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { RedisService } from '@/redis/redis.service'
import { BadRequestException, Injectable } from '@nestjs/common' import { BadRequestException, Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
import { ChargeLicenseDto } from './dto/chargedLicenseTransactions.dto' import { ChargeLicenseDto } from './dto/chargedLicenseTransactions.dto'
@Injectable() @Injectable()
export class PartnerLicenseChargeTransactionService { export class PartnerLicenseChargeTransactionService {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
private readonly redisService: RedisService,
private readonly cacheInvalidationService: PartnersCacheInvalidationService,
) {}
private readonly TRACKING_CODE_LENGTH = 8 private readonly TRACKING_CODE_LENGTH = 8
private readonly TRACKING_CODE_MAX_ATTEMPTS = 5 private readonly TRACKING_CODE_MAX_ATTEMPTS = 5
@@ -53,6 +60,18 @@ export class PartnerLicenseChargeTransactionService {
} }
async findAll(partner_id: string, page = 1, perPage = 10) { async findAll(partner_id: string, page = 1, perPage = 10) {
const cacheKey = RedisKeyMaker.partnerLicenseChargeTransactionsList(
partner_id,
page,
perPage,
)
const cached = await this.redisService.getJson<{ data: unknown[]; total: number }>(
cacheKey,
)
if (cached) {
return ResponseMapper.paginate(cached.data, { page, perPage, total: cached.total })
}
const defaultWhere: LicenseChargeTransactionWhereInput = { const defaultWhere: LicenseChargeTransactionWhereInput = {
partner_id, partner_id,
} }
@@ -70,6 +89,7 @@ export class PartnerLicenseChargeTransactionService {
]) ])
const mappedTransactions = transactions.map(this.mappedTransaction) const mappedTransactions = transactions.map(this.mappedTransaction)
await this.redisService.setJson(cacheKey, { data: mappedTransactions, total }, 300)
return ResponseMapper.paginate(mappedTransactions, { return ResponseMapper.paginate(mappedTransactions, {
page, page,
@@ -79,6 +99,12 @@ export class PartnerLicenseChargeTransactionService {
} }
async findOne(partner_id: string, id: string) { async findOne(partner_id: string, id: string) {
const cacheKey = RedisKeyMaker.partnerLicenseChargeTransactionDetail(partner_id, id)
const cached = await this.redisService.getJson<unknown>(cacheKey)
if (cached) {
return ResponseMapper.single(cached)
}
const transaction = await this.prisma.licenseChargeTransaction.findUniqueOrThrow({ const transaction = await this.prisma.licenseChargeTransaction.findUniqueOrThrow({
where: { where: {
id, id,
@@ -86,7 +112,9 @@ export class PartnerLicenseChargeTransactionService {
}, },
select: this.defaultSelect, select: this.defaultSelect,
}) })
return ResponseMapper.single(this.mappedTransaction(transaction)) const mappedTransaction = this.mappedTransaction(transaction)
await this.redisService.setJson(cacheKey, mappedTransaction, 300)
return ResponseMapper.single(mappedTransaction)
} }
async create(partner_id: string, data: ChargeLicenseDto) { async create(partner_id: string, data: ChargeLicenseDto) {
@@ -134,6 +162,8 @@ export class PartnerLicenseChargeTransactionService {
}) })
this.scheduleLicenseProvisioning(transaction.id, transaction.purchased_count) this.scheduleLicenseProvisioning(transaction.id, transaction.purchased_count)
await this.cacheInvalidationService.invalidatePartnersList()
await this.cacheInvalidationService.invalidatePartnerLicenses(partner_id)
return ResponseMapper.create({ return ResponseMapper.create({
transaction_id: transaction.id, transaction_id: transaction.id,
charged_license_count: transaction.purchased_count, charged_license_count: transaction.purchased_count,
+32 -1
View File
@@ -1,6 +1,7 @@
import { UploadedFileTypes } from '@/common/enums/enums' import { UploadedFileTypes } from '@/common/enums/enums'
import { translateEnumValue } from '@/common/utils' import { translateEnumValue } from '@/common/utils'
import { PasswordUtil } from '@/common/utils/password.util' import { PasswordUtil } from '@/common/utils/password.util'
import { RedisKeyMaker } from '@/common/utils/redisKeyMaker'
import { import {
AccountStatus, AccountStatus,
AccountType, AccountType,
@@ -8,8 +9,10 @@ import {
PartnerStatus, PartnerStatus,
} from '@/generated/prisma/enums' } from '@/generated/prisma/enums'
import { PartnerSelect } from '@/generated/prisma/models' import { PartnerSelect } from '@/generated/prisma/models'
import { PartnersCacheInvalidationService } from '@/modules/partners/cache/partners-cache-invalidation.service'
import { UploaderService } from '@/modules/uploader/uploader.service' import { UploaderService } from '@/modules/uploader/uploader.service'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { RedisService } from '@/redis/redis.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
import { CreatePartnerDto, UpdatePartnerDto } from './dto/partner.dto' import { CreatePartnerDto, UpdatePartnerDto } from './dto/partner.dto'
@@ -20,6 +23,8 @@ export class PartnersService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly uploaderService: UploaderService, private readonly uploaderService: UploaderService,
private readonly redisService: RedisService,
private readonly cacheInvalidationService: PartnersCacheInvalidationService,
) {} ) {}
private readonly defaultSelect: PartnerSelect = { private readonly defaultSelect: PartnerSelect = {
@@ -137,22 +142,40 @@ export class PartnersService {
} }
async findAll() { async findAll() {
const cacheKey = RedisKeyMaker.partnersList()
const cached = await this.redisService.getJson<unknown[]>(cacheKey)
if (cached) {
console.log('cached', cached)
return ResponseMapper.list(cached)
}
const partners = await this.prisma.partner.findMany({ const partners = await this.prisma.partner.findMany({
select: this.defaultSelect, select: this.defaultSelect,
}) })
const mappedPartners = partners.map(this.mapPartner) const mappedPartners = partners.map(this.mapPartner)
await this.redisService.setJson(cacheKey, mappedPartners, 300)
return ResponseMapper.list(mappedPartners) return ResponseMapper.list(mappedPartners)
} }
async findOne(id: string) { async findOne(id: string) {
const cacheKey = RedisKeyMaker.partnerDetail(id)
const cached = await this.redisService.getJson<unknown>(cacheKey)
if (cached) {
return ResponseMapper.single(cached)
}
const partner = await this.prisma.partner.findUniqueOrThrow({ const partner = await this.prisma.partner.findUniqueOrThrow({
where: { id }, where: { id },
select: this.defaultSelect, select: this.defaultSelect,
}) })
return ResponseMapper.single(this.mapPartner(partner)) const mappedPartner = this.mapPartner(partner)
await this.redisService.setJson(cacheKey, mappedPartner, 300)
return ResponseMapper.single(mappedPartner)
} }
async create(data: CreatePartnerDto, logo?: any) { async create(data: CreatePartnerDto, logo?: any) {
@@ -184,6 +207,8 @@ export class PartnersService {
}, },
select: this.defaultSelect, select: this.defaultSelect,
}) })
await this.cacheInvalidationService.invalidatePartnersList()
await this.cacheInvalidationService.invalidatePartnerDetail(partner.id)
return ResponseMapper.create(partner) return ResponseMapper.create(partner)
} }
@@ -220,11 +245,17 @@ export class PartnersService {
}) })
}) })
await this.cacheInvalidationService.invalidatePartnersList()
await this.cacheInvalidationService.invalidatePartnerDetail(id)
return ResponseMapper.update(partner) return ResponseMapper.update(partner)
} }
async delete(id: string) { async delete(id: string) {
await this.prisma.partner.delete({ where: { id } }) await this.prisma.partner.delete({ where: { id } })
await this.cacheInvalidationService.invalidatePartnersList()
await this.cacheInvalidationService.invalidatePartnerDetail(id)
await this.cacheInvalidationService.invalidatePartnerLicenses(id)
return ResponseMapper.delete() return ResponseMapper.delete()
} }
} }
@@ -1,6 +1,8 @@
import { BusinessActivitiesQueryService } from '@/common/services/businessActivities/business-activities-query.service' import { BusinessActivitiesQueryService } from '@/common/services/businessActivities/business-activities-query.service'
import { RedisKeyMaker } from '@/common/utils/redisKeyMaker'
import { BusinessActivitySelect } from '@/generated/prisma/models' import { BusinessActivitySelect } from '@/generated/prisma/models'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { RedisService } from '@/redis/redis.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
@@ -10,6 +12,7 @@ export class BusinessActivitiesService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly businessActivitiesQueryService: BusinessActivitiesQueryService, private readonly businessActivitiesQueryService: BusinessActivitiesQueryService,
private readonly redisService: RedisService,
) { ) {
this.defaultSelect = { this.defaultSelect = {
...this.businessActivitiesQueryService.baseSelect, ...this.businessActivitiesQueryService.baseSelect,
@@ -37,6 +40,7 @@ export class BusinessActivitiesService {
}, },
} }
} }
private readonly cacheTtlSeconds = 300
private readonly prepareBusinessActivityResponse = (businessActivity: any) => { private readonly prepareBusinessActivityResponse = (businessActivity: any) => {
const { license_activation, complexes, ...rest } = businessActivity const { license_activation, complexes, ...rest } = businessActivity
@@ -56,20 +60,34 @@ export class BusinessActivitiesService {
} }
async findAll(consumer_id: string) { async findAll(consumer_id: string) {
const cacheKey = RedisKeyMaker.consumerBusinessActivitiesList(consumer_id)
const cached = await this.redisService.getJson<unknown[]>(cacheKey)
if (cached) {
return ResponseMapper.list(cached)
}
const businessActivities = const businessActivities =
await this.businessActivitiesQueryService.findAllByConsumer( await this.businessActivitiesQueryService.findAllByConsumer(
consumer_id, consumer_id,
this.defaultSelect, this.defaultSelect,
) )
await this.redisService.setJson(cacheKey, businessActivities, this.cacheTtlSeconds)
return ResponseMapper.list(businessActivities) return ResponseMapper.list(businessActivities)
} }
async findOne(consumer_id: string, id: string) { async findOne(consumer_id: string, id: string) {
const cacheKey = RedisKeyMaker.consumerBusinessActivityInfo(consumer_id, id)
const cached = await this.redisService.getJson<unknown>(cacheKey)
if (cached) {
return ResponseMapper.single(cached)
}
const businessActivity = await this.businessActivitiesQueryService.findOneByConsumer( const businessActivity = await this.businessActivitiesQueryService.findOneByConsumer(
consumer_id, consumer_id,
id, id,
this.defaultSelect, this.defaultSelect,
) )
await this.redisService.setJson(cacheKey, businessActivity, this.cacheTtlSeconds)
return ResponseMapper.single(businessActivity) return ResponseMapper.single(businessActivity)
} }
@@ -79,6 +97,13 @@ export class BusinessActivitiesService {
data, data,
select: this.defaultSelect, select: this.defaultSelect,
}) })
await this.redisService.delete(
RedisKeyMaker.consumerBusinessActivityInfo(consumer_id, id),
)
await this.redisService.delete(
RedisKeyMaker.consumerBusinessActivitiesList(consumer_id),
)
return ResponseMapper.update(this.prepareBusinessActivityResponse(businessActivity)) return ResponseMapper.update(this.prepareBusinessActivityResponse(businessActivity))
} }
} }
@@ -1,5 +1,6 @@
import { UploadedFileTypes } from '@/common/enums/enums' import { UploadedFileTypes } from '@/common/enums/enums'
import { GoodSelect } from '@/generated/prisma/models' import { GoodSelect } from '@/generated/prisma/models'
import { PosCacheInvalidationService } from '@/modules/pos/cache/pos-cache-invalidation.service'
import { UploaderService } from '@/modules/uploader/uploader.service' import { UploaderService } from '@/modules/uploader/uploader.service'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
@@ -11,6 +12,7 @@ export class ConsumerBusinessActivityGoodsService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly uploaderService: UploaderService, private readonly uploaderService: UploaderService,
private readonly posCacheInvalidationService: PosCacheInvalidationService,
) {} ) {}
defaultSelect: GoodSelect = { defaultSelect: GoodSelect = {
@@ -110,6 +112,9 @@ export class ConsumerBusinessActivityGoodsService {
}, },
}, },
}) })
await this.posCacheInvalidationService.invalidateGoodsListByBusinessActivity(
business_activity_id,
)
return ResponseMapper.create(good) return ResponseMapper.create(good)
} }
@@ -174,6 +179,9 @@ export class ConsumerBusinessActivityGoodsService {
}, },
}) })
}) })
await this.posCacheInvalidationService.invalidateGoodsListByBusinessActivity(
business_activity_id,
)
return ResponseMapper.update(good) return ResponseMapper.update(good)
} }
+33 -2
View File
@@ -1,8 +1,10 @@
import { QUERY_CONSTANTS } from '@/common/queryConstants' import { QUERY_CONSTANTS } from '@/common/queryConstants'
import { ResponseMapper } from '@/common/response/response-mapper' import { ResponseMapper } from '@/common/response/response-mapper'
import { PasswordUtil } from '@/common/utils' import { PasswordUtil } from '@/common/utils'
import { RedisKeyMaker } from '@/common/utils/redisKeyMaker'
import { ConsumerUpdateInput } from '@/generated/prisma/models' import { ConsumerUpdateInput } from '@/generated/prisma/models'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { RedisService } from '@/redis/redis.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import consumer_mappersUtil from '../../common/utils/mappers/consumer_mappers.util' import consumer_mappersUtil from '../../common/utils/mappers/consumer_mappers.util'
import { UpdateConsumerInfoDto } from './dto/update-info-request.dto' import { UpdateConsumerInfoDto } from './dto/update-info-request.dto'
@@ -10,9 +12,20 @@ import { UpdateConsumerPasswordDto } from './dto/update-password-request.dto'
@Injectable() @Injectable()
export class ConsumerService { export class ConsumerService {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
private readonly redisService: RedisService,
) {}
private readonly infoCacheTtlSeconds = 300
async getInfo(consumer_id: string) { async getInfo(consumer_id: string) {
const cacheKey = RedisKeyMaker.consumerInfo(consumer_id)
const cached = await this.redisService.getJson<unknown>(cacheKey)
if (cached) {
return ResponseMapper.single(cached)
}
const consumer = await this.prisma.consumer.findUniqueOrThrow({ const consumer = await this.prisma.consumer.findUniqueOrThrow({
where: { where: {
id: consumer_id, id: consumer_id,
@@ -24,7 +37,9 @@ export class ConsumerService {
}, },
}) })
return ResponseMapper.single(consumer_mappersUtil(consumer)) const mappedConsumer = consumer_mappersUtil(consumer)
await this.redisService.setJson(cacheKey, mappedConsumer, this.infoCacheTtlSeconds)
return ResponseMapper.single(mappedConsumer)
} }
async updateInfo(consumer_id: string, data: UpdateConsumerInfoDto) { async updateInfo(consumer_id: string, data: UpdateConsumerInfoDto) {
@@ -69,6 +84,22 @@ export class ConsumerService {
}, },
}) })
await this.redisService.delete(RedisKeyMaker.consumerInfo(consumer_id))
const partnerOwner = await this.prisma.consumer.findUnique({
where: { id: consumer_id },
select: {
legal: { select: { partner_id: true } },
individual: { select: { partner_id: true } },
},
})
const partnerId =
partnerOwner?.legal?.partner_id || partnerOwner?.individual?.partner_id || null
if (partnerId) {
await this.redisService.delete(
RedisKeyMaker.partnerConsumerInfo(partnerId, consumer_id),
)
}
return ResponseMapper.update(consumer) return ResponseMapper.update(consumer)
} }
+35 -4
View File
@@ -1,5 +1,6 @@
import translates from '@/common/constants/translates/translates' import translates from '@/common/constants/translates/translates'
import { translateEnumValue } from '@/common/utils/enum-translator.util' import { translateEnumValue } from '@/common/utils/enum-translator.util'
import { RedisKeyMaker } from '@/common/utils/redisKeyMaker'
import { import {
AccountStatus, AccountStatus,
AccountType, AccountType,
@@ -26,12 +27,22 @@ import {
TspProviderResponseStatus, TspProviderResponseStatus,
TspProviderType, TspProviderType,
} from '@/generated/prisma/enums' } from '@/generated/prisma/enums'
import { RedisService } from '@/redis/redis.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { GoldKarat, TspProviderCustomerType } from 'common/enums/enums' import { GoldKarat, TspProviderCustomerType } from 'common/enums/enums'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
@Injectable() @Injectable()
export class EnumsService { export class EnumsService {
constructor(private readonly redisService: RedisService) {}
private readonly cacheTtlSeconds = 24 * 60 * 60
private readonly cacheBuildVersion =
process.env.CACHE_BUILD_VERSION ||
process.env.APP_BUILD_ID ||
process.env.npm_package_version ||
'local'
private prepareData( private prepareData(
items: Record<string, string>, items: Record<string, string>,
enumName: keyof typeof translates.enums, enumName: keyof typeof translates.enums,
@@ -45,7 +56,7 @@ export class EnumsService {
}) })
} }
getAllEnums() { private buildAllEnums() {
return { return {
PaymentMethodType: this.prepareData(PaymentMethodType, 'PaymentMethodType'), PaymentMethodType: this.prepareData(PaymentMethodType, 'PaymentMethodType'),
GoldKarat: this.prepareData(GoldKarat, 'GoldKarat'), GoldKarat: this.prepareData(GoldKarat, 'GoldKarat'),
@@ -91,8 +102,28 @@ export class EnumsService {
} }
} }
getEnumValues(enumName: keyof typeof translates.enums) { async getAllEnums() {
const enums = this.getAllEnums() const cacheKey = RedisKeyMaker.enumsAll(this.cacheBuildVersion)
return ResponseMapper.list(enums[enumName] || []) const cached = await this.redisService.getJson<Record<string, unknown[]>>(cacheKey)
if (cached) {
return cached
}
const enums = this.buildAllEnums()
await this.redisService.setJson(cacheKey, enums, this.cacheTtlSeconds)
return enums
}
async getEnumValues(enumName: keyof typeof translates.enums) {
const cacheKey = RedisKeyMaker.enumsValues(this.cacheBuildVersion, enumName)
const cached = await this.redisService.getJson<unknown[]>(cacheKey)
if (cached) {
return ResponseMapper.list(cached)
}
const enums = await this.getAllEnums()
const items = enums[enumName] || []
await this.redisService.setJson(cacheKey, items, this.cacheTtlSeconds)
return ResponseMapper.list(items)
} }
} }
@@ -0,0 +1,167 @@
import { RedisKeyMaker } from '@/common/utils'
import { RedisService } from '@/redis/redis.service'
import { Injectable } from '@nestjs/common'
@Injectable()
export class PartnersCacheInvalidationService {
constructor(private readonly redisService: RedisService) {}
async invalidatePartnerSummary(partnerId: string): Promise<void> {
await this.invalidatePartnersList()
await this.redisService.delete(RedisKeyMaker.partnerDetail(partnerId))
}
async invalidatePartnersList(): Promise<void> {
await this.redisService.delete(RedisKeyMaker.partnersList())
}
async invalidatePartnerDetail(partnerId: string): Promise<void> {
await this.invalidatePartnerSummary(partnerId)
}
async invalidatePartnerLicenseReadModels(partnerId: string): Promise<void> {
await this.redisService.deleteByPatterns([
RedisKeyMaker.partnerActivatedLicensesListPattern(partnerId),
RedisKeyMaker.partnerLicenseChargeTransactionsListPattern(partnerId),
RedisKeyMaker.partnerLicenseChargeTransactionDetailPattern(partnerId),
])
}
async invalidatePartnerLicenses(partnerId: string): Promise<void> {
await this.invalidatePartnerSummary(partnerId)
await this.invalidatePartnerLicenseReadModels(partnerId)
}
// consumers //
async invalidatePartnerConsumers(
partnerId: string,
page: number,
perPage: number,
): Promise<void> {
await this.redisService.delete(
RedisKeyMaker.partnerConsumersList(partnerId, page, perPage),
)
}
async invalidatePartnerConsumerReadModels(
partnerId: string,
consumerId: string,
): Promise<void> {
await this.redisService.deleteByPatterns([
RedisKeyMaker.partnerConsumersListPattern(partnerId),
RedisKeyMaker.partnerConsumerInfo(partnerId, consumerId),
])
}
async invalidatePartnerConsumerBusinessActivitiesReadModels(
partnerId: string,
consumerId: string,
): Promise<void> {
await this.invalidatePartnerConsumerReadModels(partnerId, consumerId)
await this.redisService.deleteByPattern(
RedisKeyMaker.partnerConsumerBusinessActivitiesPattern(partnerId, consumerId),
)
}
async invalidatePartnerConsumerBusinessActivityReadModels(
partnerId: string,
consumerId: string,
businessActivityId: string,
): Promise<void> {
await this.invalidatePartnerConsumerBusinessActivitiesReadModels(
partnerId,
consumerId,
)
await this.redisService.delete(
RedisKeyMaker.partnerConsumerBusinessActivityInfo(
partnerId,
consumerId,
businessActivityId,
),
)
}
async invalidatePartnerConsumerBusinessActivityComplexesReadModels(
partnerId: string,
consumerId: string,
businessActivityId: string,
): Promise<void> {
await this.invalidatePartnerConsumerBusinessActivitiesReadModels(
partnerId,
consumerId,
)
await this.redisService.deleteByPattern(
RedisKeyMaker.partnerConsumerBusinessActivityComplexesPattern(
partnerId,
consumerId,
businessActivityId,
),
)
}
async invalidatePartnerConsumerBusinessActivityComplexReadModels(
partnerId: string,
consumerId: string,
businessActivityId: string,
complexId: string,
): Promise<void> {
await this.invalidatePartnerConsumerBusinessActivityComplexesReadModels(
partnerId,
consumerId,
businessActivityId,
)
await this.redisService.delete(
RedisKeyMaker.partnerConsumerBusinessActivityComplexInfo(
partnerId,
consumerId,
businessActivityId,
complexId,
),
)
}
async invalidatePartnerConsumerBusinessActivityComplexPosesReadModels(
partnerId: string,
consumerId: string,
businessActivityId: string,
complexId: string,
): Promise<void> {
await this.invalidatePartnerConsumerBusinessActivitiesReadModels(
partnerId,
consumerId,
)
await this.redisService.deleteByPattern(
RedisKeyMaker.partnerConsumerBusinessActivityComplexPosesPattern(
partnerId,
consumerId,
businessActivityId,
complexId,
),
)
}
async invalidatePartnerConsumerBusinessActivityComplexPosReadModels(
partnerId: string,
consumerId: string,
businessActivityId: string,
complexId: string,
posId: string,
): Promise<void> {
await this.invalidatePartnerConsumerBusinessActivityComplexPosesReadModels(
partnerId,
consumerId,
businessActivityId,
complexId,
)
await this.redisService.delete(
RedisKeyMaker.partnerConsumerBusinessActivityComplexPosInfo(
partnerId,
consumerId,
businessActivityId,
complexId,
posId,
),
)
}
}
@@ -1,13 +1,22 @@
import { RedisKeyMaker } from '@/common/utils'
import { BusinessActivitySelect } from '@/generated/prisma/models' import { BusinessActivitySelect } from '@/generated/prisma/models'
import { getPartnerFirstRemainingLicense } from '@/modules/partners/utils/getPartnerRemainingLicenses.util' import { getPartnerFirstRemainingLicense } from '@/modules/partners/utils/getPartnerRemainingLicenses.util'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { RedisService } from '@/redis/redis.service'
import { BadRequestException, Injectable } from '@nestjs/common' import { BadRequestException, Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
import { PartnersCacheInvalidationService } from '../../cache/partners-cache-invalidation.service'
import { CreateBusinessActivitiesDto } from './dto/create-business-activities.dto' import { CreateBusinessActivitiesDto } from './dto/create-business-activities.dto'
@Injectable() @Injectable()
export class BusinessActivitiesService { export class BusinessActivitiesService {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
private readonly redisService: RedisService,
private readonly partnersCacheInvalidationService: PartnersCacheInvalidationService,
) {}
private readonly cacheTtlSeconds = 300
private readonly setExpireDate = (starts_at: Date) => { private readonly setExpireDate = (starts_at: Date) => {
return new Date( return new Date(
@@ -85,6 +94,19 @@ export class BusinessActivitiesService {
} }
async findAll(partner_id: string, consumer_id: string, page = 1, perPage = 10) { async findAll(partner_id: string, consumer_id: string, page = 1, perPage = 10) {
const cacheKey = RedisKeyMaker.partnerConsumerBusinessActivitiesList(
partner_id,
consumer_id,
page,
perPage,
)
const cached = await this.redisService.getJson<{ items: unknown[]; total: number }>(
cacheKey,
)
if (cached) {
return ResponseMapper.paginate(cached.items, { total: cached.total, page, perPage })
}
const where = this.defaultWhere(partner_id, consumer_id) const where = this.defaultWhere(partner_id, consumer_id)
const [businessActivities, total] = await this.prisma.$transaction(async tx => [ const [businessActivities, total] = await this.prisma.$transaction(async tx => [
await tx.businessActivity.findMany({ await tx.businessActivity.findMany({
@@ -95,13 +117,26 @@ export class BusinessActivitiesService {
}), }),
await tx.businessActivity.count({ where }), await tx.businessActivity.count({ where }),
]) ])
return ResponseMapper.paginate( const mappedItems = businessActivities.map(this.prepareBusinessActivityResponse)
businessActivities.map(this.prepareBusinessActivityResponse), await this.redisService.setJson(
{ total, page, perPage }, cacheKey,
{ items: mappedItems, total },
this.cacheTtlSeconds,
) )
return ResponseMapper.paginate(mappedItems, { total, page, perPage })
} }
async findOne(partner_id: string, consumer_id: string, id: string) { async findOne(partner_id: string, consumer_id: string, id: string) {
const cacheKey = RedisKeyMaker.partnerConsumerBusinessActivityInfo(
partner_id,
consumer_id,
id,
)
const cached = await this.redisService.getJson<unknown>(cacheKey)
if (cached) {
return ResponseMapper.single(cached)
}
const businessActivity = await this.prisma.businessActivity.findUnique({ const businessActivity = await this.prisma.businessActivity.findUnique({
where: { where: {
...this.defaultWhere(partner_id, consumer_id), ...this.defaultWhere(partner_id, consumer_id),
@@ -109,7 +144,9 @@ export class BusinessActivitiesService {
}, },
select: this.defaultSelect, select: this.defaultSelect,
}) })
return ResponseMapper.single(this.prepareBusinessActivityResponse(businessActivity)) const mappedItem = this.prepareBusinessActivityResponse(businessActivity)
await this.redisService.setJson(cacheKey, mappedItem, this.cacheTtlSeconds)
return ResponseMapper.single(mappedItem)
} }
async create( async create(
@@ -187,6 +224,10 @@ export class BusinessActivitiesService {
}) })
}) })
await this.partnersCacheInvalidationService.invalidatePartnerConsumerBusinessActivitiesReadModels(
partner_id,
consumer_id,
)
return ResponseMapper.create(businessActivity) return ResponseMapper.create(businessActivity)
} }
@@ -196,6 +237,13 @@ export class BusinessActivitiesService {
data, data,
select: this.defaultSelect, select: this.defaultSelect,
}) })
await this.partnersCacheInvalidationService.invalidatePartnerConsumerBusinessActivityReadModels(
partner_id,
consumer_id,
id,
)
return ResponseMapper.update(businessActivity) return ResponseMapper.update(businessActivity)
} }
@@ -3,7 +3,12 @@ import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger' import { ApiTags } from '@nestjs/swagger'
import { BusinessActivityComplexesService } from './complexes.service' import { BusinessActivityComplexesService } from './complexes.service'
import { CreateComplexDto, UpdateComplexDto } from './dto/complex.dto' import { CreateComplexDto, UpdateComplexDto } from './dto/complex.dto'
import type { BusinessActivityComplexesServiceCreateResponseDto, BusinessActivityComplexesServiceFindAllResponseDto, BusinessActivityComplexesServiceFindOneResponseDto, BusinessActivityComplexesServiceUpdateResponseDto } from './dto/complexes-response.dto' import type {
BusinessActivityComplexesServiceCreateResponseDto,
BusinessActivityComplexesServiceFindAllResponseDto,
BusinessActivityComplexesServiceFindOneResponseDto,
BusinessActivityComplexesServiceUpdateResponseDto,
} from './dto/complexes-response.dto'
@ApiTags('PartnerBusinessActivityComplexes') @ApiTags('PartnerBusinessActivityComplexes')
@Controller( @Controller(
@@ -34,10 +39,11 @@ export class BusinessActivityComplexesController {
@Post() @Post()
async create( async create(
@PartnerInfo('id') partnerId: string, @PartnerInfo('id') partnerId: string,
@Param('consumerId') consumerId: string,
@Param('businessActivityId') businessActivityId: string, @Param('businessActivityId') businessActivityId: string,
@Body() data: CreateComplexDto, @Body() data: CreateComplexDto,
): Promise<BusinessActivityComplexesServiceCreateResponseDto> { ): Promise<BusinessActivityComplexesServiceCreateResponseDto> {
return this.service.create(partnerId, businessActivityId, data) return this.service.create(partnerId, consumerId, businessActivityId, data)
} }
@Patch(':id') @Patch(':id')
@@ -1,13 +1,22 @@
import { QUERY_CONSTANTS } from '@/common/queryConstants' import { QUERY_CONSTANTS } from '@/common/queryConstants'
import { RedisKeyMaker } from '@/common/utils'
import { ComplexSelect, ComplexWhereInput } from '@/generated/prisma/models' import { ComplexSelect, ComplexWhereInput } from '@/generated/prisma/models'
import { PartnersCacheInvalidationService } from '@/modules/partners/cache/partners-cache-invalidation.service'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { RedisService } from '@/redis/redis.service'
import { BadRequestException, Injectable } from '@nestjs/common' import { BadRequestException, Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
import { CreateComplexDto, UpdateComplexDto } from './dto/complex.dto' import { CreateComplexDto, UpdateComplexDto } from './dto/complex.dto'
@Injectable() @Injectable()
export class BusinessActivityComplexesService { export class BusinessActivityComplexesService {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
private readonly redisService: RedisService,
private readonly partnersCacheInvalidationService: PartnersCacheInvalidationService,
) {}
private readonly cacheTtlSeconds = 300
private readonly defaultSelect: ComplexSelect = { private readonly defaultSelect: ComplexSelect = {
id: true, id: true,
@@ -49,6 +58,20 @@ export class BusinessActivityComplexesService {
page = 1, page = 1,
perPage = 10, perPage = 10,
) { ) {
const cacheKey = RedisKeyMaker.partnerConsumerBusinessActivityComplexesList(
partner_id,
consumer_id,
business_activity_id,
page,
perPage,
)
const cached = await this.redisService.getJson<{ items: unknown[]; total: number }>(
cacheKey,
)
if (cached) {
return ResponseMapper.paginate(cached.items, { total: cached.total, page, perPage })
}
const where = this.defaultWhere(partner_id, consumer_id, business_activity_id) const where = this.defaultWhere(partner_id, consumer_id, business_activity_id)
const [complexes, total] = await this.prisma.$transaction(async tx => [ const [complexes, total] = await this.prisma.$transaction(async tx => [
await tx.complex.findMany({ await tx.complex.findMany({
@@ -74,6 +97,11 @@ export class BusinessActivityComplexesService {
pos_count: _count.pos_list, pos_count: _count.pos_list,
} }
}) })
await this.redisService.setJson(
cacheKey,
{ items: mappedComplexes, total },
this.cacheTtlSeconds,
)
return ResponseMapper.paginate(mappedComplexes, { total, page, perPage }) return ResponseMapper.paginate(mappedComplexes, { total, page, perPage })
} }
@@ -83,18 +111,40 @@ export class BusinessActivityComplexesService {
business_activity_id: string, business_activity_id: string,
id: string, id: string,
) { ) {
const complex = await this.prisma.complex.findUnique({ const cacheKey = RedisKeyMaker.partnerConsumerBusinessActivityComplexInfo(
partner_id,
consumer_id,
business_activity_id,
id,
)
const cached = await this.redisService.getJson<unknown>(cacheKey)
if (cached) {
return ResponseMapper.single(cached)
}
const complex = await this.prisma.complex.findFirst({
where: { where: {
...this.defaultWhere(partner_id, consumer_id, business_activity_id), ...this.defaultWhere(partner_id, consumer_id, business_activity_id),
id, id,
}, },
select: this.defaultSelect, select: this.defaultSelect,
}) })
if (!complex) {
throw new BadRequestException('شعبه مورد نظر یافت نشد.')
}
await this.redisService.setJson(cacheKey, complex, this.cacheTtlSeconds)
return ResponseMapper.single(complex) return ResponseMapper.single(complex)
} }
async create(partner_id: string, business_id: string, data: CreateComplexDto) { async create(
const complex = this.prisma.$transaction(async tx => { partner_id: string,
business_id: string,
consumer_id: string,
data: CreateComplexDto,
) {
const complex = await this.prisma.$transaction(async tx => {
const relatedLicense = await tx.licenseAccountAllocation.findFirst({ const relatedLicense = await tx.licenseAccountAllocation.findFirst({
where: { where: {
license_activation: { license_activation: {
@@ -121,7 +171,7 @@ export class BusinessActivityComplexesService {
) )
} }
return await this.prisma.complex.create({ return await tx.complex.create({
data: { data: {
...data, ...data,
business_activity: { business_activity: {
@@ -133,6 +183,12 @@ export class BusinessActivityComplexesService {
}) })
}) })
await this.partnersCacheInvalidationService.invalidatePartnerConsumerBusinessActivityComplexesReadModels(
partner_id,
consumer_id,
business_id,
)
return ResponseMapper.create(complex) return ResponseMapper.create(complex)
} }
@@ -147,6 +203,13 @@ export class BusinessActivityComplexesService {
where: { ...this.defaultWhere(partner_id, consumer_id, business_activity_id), id }, where: { ...this.defaultWhere(partner_id, consumer_id, business_activity_id), id },
data, data,
}) })
await this.partnersCacheInvalidationService.invalidatePartnerConsumerBusinessActivityComplexReadModels(
partner_id,
consumer_id,
business_activity_id,
id,
)
return ResponseMapper.update(complex) return ResponseMapper.update(complex)
} }
@@ -1,7 +1,7 @@
import { QUERY_CONSTANTS } from '@/common/queryConstants' import { QUERY_CONSTANTS } from '@/common/queryConstants'
import { consumerRelatedPartner } from '@/common/queryConstants/consumer' import { consumerRelatedPartner } from '@/common/queryConstants/consumer'
import { PrismaErrorUtil } from '@/common/utils/prisma-error.util'
import { PasswordUtil } from '@/common/utils/password.util' import { PasswordUtil } from '@/common/utils/password.util'
import { PrismaErrorUtil } from '@/common/utils/prisma-error.util'
import { import {
AccountStatus, AccountStatus,
AccountType, AccountType,
@@ -9,14 +9,22 @@ import {
POSStatus, POSStatus,
} from '@/generated/prisma/enums' } from '@/generated/prisma/enums'
import { PosSelect } from '@/generated/prisma/models' import { PosSelect } from '@/generated/prisma/models'
import { PartnersCacheInvalidationService } from '@/modules/partners/cache/partners-cache-invalidation.service'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { RedisService } from '@/redis/redis.service'
import { BadRequestException, Injectable } from '@nestjs/common' import { BadRequestException, Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
import { CreatePosDto, UpdatePosDto } from './dto/pos.dto' import { CreatePosDto, UpdatePosDto } from './dto/pos.dto'
@Injectable() @Injectable()
export class ComplexPosesService { export class ComplexPosesService {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
private readonly redisService: RedisService,
private readonly partnersCacheInvalidationService: PartnersCacheInvalidationService,
) {}
private readonly cacheTtlSeconds = 300
private readonly defaultSelect: PosSelect = { private readonly defaultSelect: PosSelect = {
id: true, id: true,
@@ -1,6 +1,7 @@
import { QUERY_CONSTANTS } from '@/common/queryConstants' import { QUERY_CONSTANTS } from '@/common/queryConstants'
import mapConsumerWithLicenseUtil from '@/common/utils/mappers/consumer_mappers.util' import mapConsumerWithLicenseUtil from '@/common/utils/mappers/consumer_mappers.util'
import { PasswordUtil } from '@/common/utils/password.util' import { PasswordUtil } from '@/common/utils/password.util'
import { RedisKeyMaker } from '@/common/utils/redisKeyMaker'
import { import {
AccountStatus, AccountStatus,
AccountType, AccountType,
@@ -15,13 +16,21 @@ import {
ConsumerWhereInput, ConsumerWhereInput,
} from '@/generated/prisma/models' } from '@/generated/prisma/models'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { RedisService } from '@/redis/redis.service'
import { BadRequestException, Injectable } from '@nestjs/common' import { BadRequestException, Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
import { PartnersCacheInvalidationService } from '../cache/partners-cache-invalidation.service'
import { CreateConsumerDto, UpdateConsumerDto } from './dto/create-consumers.dto' import { CreateConsumerDto, UpdateConsumerDto } from './dto/create-consumers.dto'
@Injectable() @Injectable()
export class PartnerConsumersService { export class PartnerConsumersService {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
private readonly redisService: RedisService,
private readonly partnersCacheInvalidationService: PartnersCacheInvalidationService,
) {}
private readonly infoCacheTtlSeconds = 300
private readonly setExpireDate = (starts_at: Date) => { private readonly setExpireDate = (starts_at: Date) => {
return new Date( return new Date(
@@ -54,6 +63,14 @@ export class PartnerConsumersService {
}) })
async findAll(partner_id: string, page = 1, perPage = 10) { async findAll(partner_id: string, page = 1, perPage = 10) {
const cacheKey = RedisKeyMaker.partnerConsumersList(partner_id, page, perPage)
const cached = await this.redisService.getJson<{ items: unknown[]; total: number }>(
cacheKey,
)
if (cached) {
return ResponseMapper.paginate(cached.items, { total: cached.total, page, perPage })
}
const [consumers, total] = await this.prisma.$transaction(async tx => [ const [consumers, total] = await this.prisma.$transaction(async tx => [
await tx.consumer.findMany({ await tx.consumer.findMany({
where: this.defaultWhere(partner_id), where: this.defaultWhere(partner_id),
@@ -65,7 +82,13 @@ export class PartnerConsumersService {
where: this.defaultWhere(partner_id), where: this.defaultWhere(partner_id),
}), }),
]) ])
return ResponseMapper.paginate(consumers.map(mapConsumerWithLicenseUtil), { const mappedItems = consumers.map(mapConsumerWithLicenseUtil)
await this.redisService.setJson(
cacheKey,
{ items: mappedItems, total },
this.infoCacheTtlSeconds,
)
return ResponseMapper.paginate(mappedItems, {
total, total,
page, page,
perPage, perPage,
@@ -73,6 +96,12 @@ export class PartnerConsumersService {
} }
async findOne(partner_id: string, consumer_id: string) { async findOne(partner_id: string, consumer_id: string) {
const cacheKey = RedisKeyMaker.partnerConsumerInfo(partner_id, consumer_id)
const cached = await this.redisService.getJson<unknown>(cacheKey)
if (cached) {
return ResponseMapper.single(cached)
}
const consumer = await this.prisma.consumer.findUniqueOrThrow({ const consumer = await this.prisma.consumer.findUniqueOrThrow({
where: { where: {
...(this.defaultWhere(partner_id) as any), ...(this.defaultWhere(partner_id) as any),
@@ -81,7 +110,14 @@ export class PartnerConsumersService {
select: this.defaultSelect, select: this.defaultSelect,
}) })
return ResponseMapper.single(mapConsumerWithLicenseUtil(consumer)) const mappedConsumer = mapConsumerWithLicenseUtil(consumer)
await this.redisService.setJson(
RedisKeyMaker.consumerInfo(consumer_id),
mappedConsumer,
this.infoCacheTtlSeconds,
)
await this.redisService.setJson(cacheKey, mappedConsumer, this.infoCacheTtlSeconds)
return ResponseMapper.single(mappedConsumer)
} }
async create(partner_id: string, data: CreateConsumerDto) { async create(partner_id: string, data: CreateConsumerDto) {
@@ -224,11 +260,16 @@ export class PartnerConsumersService {
}) })
}) })
await this.partnersCacheInvalidationService.invalidatePartnerConsumerReadModels(
partner_id,
consumer.id,
)
return ResponseMapper.single(mapConsumerWithLicenseUtil(consumer)) return ResponseMapper.single(mapConsumerWithLicenseUtil(consumer))
} }
async update(id: string, partner_id: string, data: UpdateConsumerDto) { async update(id: string, partner_id: string, data: UpdateConsumerDto) {
return this.prisma.consumer.update({ const updatedConsumer = await this.prisma.consumer.update({
where: { where: {
...(this.defaultWhere(partner_id) as any), ...(this.defaultWhere(partner_id) as any),
id, id,
@@ -243,5 +284,13 @@ export class PartnerConsumersService {
}, },
}, },
}) })
await this.partnersCacheInvalidationService.invalidatePartnerConsumerReadModels(
partner_id,
id,
)
await this.redisService.delete(RedisKeyMaker.consumerInfo(id))
return updatedConsumer
} }
} }
+5
View File
@@ -1,6 +1,7 @@
import { UploadedFileTypes } from '@/common/enums/enums' import { UploadedFileTypes } from '@/common/enums/enums'
import { ResponseMapper } from '@/common/response/response-mapper' import { ResponseMapper } from '@/common/response/response-mapper'
import { PasswordUtil } from '@/common/utils' import { PasswordUtil } from '@/common/utils'
import { PartnersCacheInvalidationService } from '@/modules/partners/cache/partners-cache-invalidation.service'
import { UploaderService } from '@/modules/uploader/uploader.service' import { UploaderService } from '@/modules/uploader/uploader.service'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
@@ -12,6 +13,7 @@ export class PartnerService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly uploaderService: UploaderService, private readonly uploaderService: UploaderService,
private readonly cacheInvalidationService: PartnersCacheInvalidationService,
) {} ) {}
async me(partner_id: string, account_id: string) { async me(partner_id: string, account_id: string) {
@@ -245,6 +247,9 @@ export class PartnerService {
}) })
}) })
await this.cacheInvalidationService.invalidatePartnersList()
await this.cacheInvalidationService.invalidatePartnerDetail(partner_id)
return ResponseMapper.single(updatedPartner) return ResponseMapper.single(updatedPartner)
} }
+25
View File
@@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common'
import { RedisService } from '@/redis/redis.service'
@Injectable()
export class PosCacheInvalidationService {
constructor(private readonly redisService: RedisService) {}
async invalidateGoodsListByGuild(guildId: string): Promise<void> {
const client = await this.redisService.getClient()
const keys = await client.keys(`pos:ba:*:guild:${guildId}:goods:list`)
if (keys.length > 0) {
await client.del(keys)
}
}
async invalidateGoodsListByBusinessActivity(
businessActivityId: string,
): Promise<void> {
const client = await this.redisService.getClient()
const keys = await client.keys(`pos:ba:${businessActivityId}:guild:*:goods:list`)
if (keys.length > 0) {
await client.del(keys)
}
}
}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger'
import { CreateGoodDto } from './create-good.dto'
export class UpdateGoodDto extends PartialType(CreateGoodDto) {}
+86 -1
View File
@@ -1,12 +1,22 @@
import { RedisKeyMaker } from '@/common/utils/redisKeyMaker'
import { GoodSelect } from '@/generated/prisma/models' import { GoodSelect } from '@/generated/prisma/models'
import { PosCacheInvalidationService } from '@/modules/pos/cache/pos-cache-invalidation.service'
import { RedisService } from '@/redis/redis.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../../common/response/response-mapper' import { ResponseMapper } from '../../../common/response/response-mapper'
import { PrismaService } from '../../../prisma/prisma.service' import { PrismaService } from '../../../prisma/prisma.service'
import { CreateGoodDto } from './dto/create-good.dto' import { CreateGoodDto } from './dto/create-good.dto'
import { UpdateGoodDto } from './dto/update-good.dto'
@Injectable() @Injectable()
export class GoodsService { export class GoodsService {
constructor(private prisma: PrismaService) {} constructor(
private prisma: PrismaService,
private readonly redisService: RedisService,
private readonly posCacheInvalidationService: PosCacheInvalidationService,
) {}
private readonly listCacheTtlSeconds = 300
private readonly defaultSelect: GoodSelect = { private readonly defaultSelect: GoodSelect = {
id: true, id: true,
@@ -42,6 +52,12 @@ export class GoodsService {
} }
async findAll(business_activity_id: string, guild_id: string) { async findAll(business_activity_id: string, guild_id: string) {
const cacheKey = RedisKeyMaker.posGoodsList(business_activity_id, guild_id)
const cached = await this.redisService.getJson<unknown[]>(cacheKey)
if (cached) {
return ResponseMapper.list(cached)
}
const goods = await this.prisma.good.findMany({ const goods = await this.prisma.good.findMany({
where: { where: {
OR: [ OR: [
@@ -59,6 +75,8 @@ export class GoodsService {
select: this.defaultSelect, select: this.defaultSelect,
}) })
await this.redisService.setJson(cacheKey, goods, this.listCacheTtlSeconds)
return ResponseMapper.list(goods) return ResponseMapper.list(goods)
} }
@@ -117,6 +135,73 @@ export class GoodsService {
select: this.defaultSelect, select: this.defaultSelect,
}) })
await this.posCacheInvalidationService.invalidateGoodsListByBusinessActivity(
business_activity_id,
)
return ResponseMapper.create(good) return ResponseMapper.create(good)
} }
async update(id: string, data: UpdateGoodDto, business_activity_id: string) {
const { category_id, sku_id, measure_unit_id, ...rest } = data
const good = await this.prisma.good.update({
where: {
id,
business_activity_id,
},
data: {
...rest,
...(measure_unit_id
? {
measure_unit: {
connect: {
id: measure_unit_id,
},
},
}
: {}),
...(sku_id
? {
sku: {
connect: {
id: sku_id,
},
},
}
: {}),
...(category_id
? {
category: {
connect: {
id: category_id,
},
},
}
: {}),
},
select: this.defaultSelect,
})
await this.posCacheInvalidationService.invalidateGoodsListByBusinessActivity(
business_activity_id,
)
return ResponseMapper.update(good)
}
async delete(id: string, business_activity_id: string) {
await this.prisma.good.delete({
where: {
id,
business_activity_id,
},
})
await this.posCacheInvalidationService.invalidateGoodsListByBusinessActivity(
business_activity_id,
)
return ResponseMapper.delete()
}
} }
+29 -5
View File
@@ -1,15 +1,29 @@
import { QUERY_CONSTANTS } from '@/common/queryConstants' import { QUERY_CONSTANTS } from '@/common/queryConstants'
import consumer_mappersUtil from '@/common/utils/mappers/consumer_mappers.util' import consumer_mappersUtil from '@/common/utils/mappers/consumer_mappers.util'
import { RedisKeyMaker } from '@/common/utils/redisKeyMaker'
import { ConsumerStatus } from '@/generated/prisma/enums' import { ConsumerStatus } from '@/generated/prisma/enums'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { RedisService } from '@/redis/redis.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
@Injectable() @Injectable()
export class PosService { export class PosService {
constructor(private readonly prisma: PrismaService) {} constructor(
private readonly prisma: PrismaService,
private readonly redisService: RedisService,
) {}
private readonly infoCacheTtlSeconds = 300
private readonly meCacheTtlSeconds = 120
async getInfo(pos_id: string) { async getInfo(pos_id: string) {
const cacheKey = RedisKeyMaker.posInfo(pos_id)
const cached = await this.redisService.getJson<unknown>(cacheKey)
if (cached) {
return ResponseMapper.single(cached)
}
const pos = await this.prisma.pos.findUniqueOrThrow({ const pos = await this.prisma.pos.findUniqueOrThrow({
where: { where: {
id: pos_id, id: pos_id,
@@ -71,7 +85,7 @@ export class PosService {
license_activation, license_activation,
} = business_activity } = business_activity
return ResponseMapper.single({ const payload = {
name, name,
complex: { complex: {
id: complexId, id: complexId,
@@ -89,7 +103,9 @@ export class PosService {
license_id: license_activation?.license.id, license_id: license_activation?.license.id,
}, },
partner: license_activation?.license.charge_transaction.partner, partner: license_activation?.license.charge_transaction.partner,
}) }
await this.redisService.setJson(cacheKey, payload, this.infoCacheTtlSeconds)
return ResponseMapper.single(payload)
} }
async getAccessible(account_id: string) { async getAccessible(account_id: string) {
@@ -136,6 +152,12 @@ export class PosService {
} }
async getMe(account_id: string, pos_id: string) { async getMe(account_id: string, pos_id: string) {
const cacheKey = RedisKeyMaker.posMe(account_id, pos_id)
const cached = await this.redisService.getJson<unknown>(cacheKey)
if (cached) {
return ResponseMapper.single(cached)
}
const pos = await this.prisma.consumerAccount.findUniqueOrThrow({ const pos = await this.prisma.consumerAccount.findUniqueOrThrow({
where: { where: {
id: account_id, id: account_id,
@@ -177,9 +199,11 @@ export class PosService {
const { consumer, ...rest } = pos const { consumer, ...rest } = pos
return ResponseMapper.single({ const payload = {
...rest, ...rest,
consumer: consumer_mappersUtil(consumer), consumer: consumer_mappersUtil(consumer),
}) }
await this.redisService.setJson(cacheKey, payload, this.meCacheTtlSeconds)
return ResponseMapper.single(payload)
} }
} }
+15 -2
View File
@@ -1,9 +1,22 @@
import { Global, Module } from '@nestjs/common' import { Global, Module } from '@nestjs/common'
import { AdminGuildCacheInvalidationService } from '@/modules/admin/guilds/cache/admin-guild-cache-invalidation.service'
import { PartnersCacheInvalidationService } from '@/modules/partners/cache/partners-cache-invalidation.service'
import { PosCacheInvalidationService } from '@/modules/pos/cache/pos-cache-invalidation.service'
import { RedisService } from './redis.service' import { RedisService } from './redis.service'
@Global() @Global()
@Module({ @Module({
providers: [RedisService], providers: [
exports: [RedisService], RedisService,
AdminGuildCacheInvalidationService,
PartnersCacheInvalidationService,
PosCacheInvalidationService,
],
exports: [
RedisService,
AdminGuildCacheInvalidationService,
PartnersCacheInvalidationService,
PosCacheInvalidationService,
],
}) })
export class RedisModule {} export class RedisModule {}
+93
View File
@@ -34,6 +34,99 @@ export class RedisService implements OnModuleDestroy {
return this.client return this.client
} }
async get(key: string): Promise<string | null> {
const client = await this.getClient()
return client.get(key)
}
async getJson<T>(key: string): Promise<T | null> {
const value = await this.get(key)
if (!value) {
return null
}
try {
return JSON.parse(value) as T
} catch {
this.logger.warn(`Invalid JSON for key "${key}"`)
return null
}
}
async set(
key: string,
value: string,
ttlSeconds?: number,
): Promise<'OK' | null> {
const client = await this.getClient()
if (ttlSeconds && ttlSeconds > 0) {
return client.set(key, value, 'EX', ttlSeconds)
}
return client.set(key, value)
}
async setJson(
key: string,
value: unknown,
ttlSeconds?: number,
): Promise<'OK' | null> {
return this.set(key, JSON.stringify(value), ttlSeconds)
}
async delete(key: string): Promise<number> {
const client = await this.getClient()
return client.del(key)
}
async exists(key: string): Promise<boolean> {
const client = await this.getClient()
const result = await client.exists(key)
return result === 1
}
async expire(key: string, ttlSeconds: number): Promise<boolean> {
const client = await this.getClient()
const result = await client.expire(key, ttlSeconds)
return result === 1
}
async deleteByPattern(pattern: string): Promise<number> {
const client = await this.getClient()
let deletedCount = 0
let cursor = '0'
do {
const [nextCursor, keys] = await client.scan(
cursor,
'MATCH',
pattern,
'COUNT',
100,
)
cursor = nextCursor
if (keys.length > 0) {
const pipeline = client.pipeline()
keys.forEach(k => pipeline.del(k))
const results = await pipeline.exec()
if (results) {
deletedCount += results.reduce((sum, [, value]) => {
return sum + (typeof value === 'number' ? value : 0)
}, 0)
}
}
} while (cursor !== '0')
return deletedCount
}
async deleteByPatterns(patterns: string[]): Promise<number> {
const deletedCounts = await Promise.all(
patterns.map(pattern => this.deleteByPattern(pattern)),
)
return deletedCounts.reduce((sum, count) => sum + count, 0)
}
async onModuleDestroy() { async onModuleDestroy() {
await this.client.quit() await this.client.quit()
} }