refactor: streamline Redis caching logic across services
- Implemented a unified `getAndSet` method in RedisService to handle caching for single, list, and paginated responses. - Removed redundant cache checks and writes in various services, simplifying the code and improving readability. - Updated GoodsService, StockKeepingUnitsService, PartnerActivatedLicensesService, and others to utilize the new caching mechanism. - Adjusted Prisma connection limit for better resource management.
This commit is contained in:
@@ -1,17 +1,162 @@
|
|||||||
# AGENT.md
|
# GapCode Agent Instructions
|
||||||
|
|
||||||
|
# AGENTS.md
|
||||||
|
|
||||||
Operational guide for AI/coding agents working in `consumer_api`.
|
Operational guide for AI/coding agents working in `consumer_api`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# ⚡ RTK LOW-TOKEN EXECUTION POLICY (MANDATORY)
|
||||||
|
|
||||||
|
This repository uses **RTK (Rust Token Killer)**.
|
||||||
|
|
||||||
|
All shell interactions MUST prefer RTK wrappers to reduce token usage.
|
||||||
|
|
||||||
|
If an RTK equivalent exists, **never use the raw command**.
|
||||||
|
|
||||||
|
This repository can be large; uncontrolled shell output will quickly exhaust model context.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# ✅ COMMAND REWRITE RULES (STRICT)
|
||||||
|
|
||||||
|
Always rewrite commands using RTK:
|
||||||
|
|
||||||
|
git status → rtk git status
|
||||||
|
git diff → rtk git diff
|
||||||
|
git log → rtk git log
|
||||||
|
ls → rtk ls
|
||||||
|
tree → rtk ls
|
||||||
|
cat <file> → rtk read <file>
|
||||||
|
grep <pattern> → rtk grep <pattern>
|
||||||
|
rg <pattern> → rtk grep <pattern>
|
||||||
|
find → rtk find
|
||||||
|
|
||||||
|
Never run:
|
||||||
|
|
||||||
|
- raw git commands for inspection
|
||||||
|
- raw cat
|
||||||
|
- raw grep
|
||||||
|
- raw ls
|
||||||
|
- raw tree
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📂 CODE NAVIGATION WORKFLOW (MANDATORY)
|
||||||
|
|
||||||
|
When working in this repository follow this order:
|
||||||
|
|
||||||
|
1️⃣ Discover structure
|
||||||
|
|
||||||
|
rtk ls
|
||||||
|
|
||||||
|
2️⃣ Search before opening files
|
||||||
|
|
||||||
|
rtk grep <symbol | class | function | DTO | service>
|
||||||
|
|
||||||
|
3️⃣ Read only the necessary files
|
||||||
|
|
||||||
|
rtk read <file>
|
||||||
|
|
||||||
|
4️⃣ For large files (>300 lines)
|
||||||
|
|
||||||
|
rtk read <file> -l aggressive
|
||||||
|
|
||||||
|
5️⃣ For quick understanding
|
||||||
|
|
||||||
|
rtk smart <file>
|
||||||
|
|
||||||
|
Never open many files blindly.
|
||||||
|
|
||||||
|
Never read entire modules without searching first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🧠 DIFF INSPECTION RULES
|
||||||
|
|
||||||
|
For reviewing repository changes:
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
rtk git diff
|
||||||
|
|
||||||
|
For large diffs:
|
||||||
|
|
||||||
|
rtk git diff -l aggressive
|
||||||
|
|
||||||
|
Never run raw `git diff`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📁 FORBIDDEN PATHS
|
||||||
|
|
||||||
|
Never inspect or read these directories unless explicitly required:
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
coverage/
|
||||||
|
.prisma/
|
||||||
|
prisma/migrations/
|
||||||
|
|
||||||
|
These folders produce extremely large outputs and waste tokens.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 📄 FILE READING POLICY
|
||||||
|
|
||||||
|
Preferred:
|
||||||
|
|
||||||
|
rtk read file.ts
|
||||||
|
|
||||||
|
Large file:
|
||||||
|
|
||||||
|
rtk read file.ts -l aggressive
|
||||||
|
|
||||||
|
Quick overview:
|
||||||
|
|
||||||
|
rtk smart file.ts
|
||||||
|
|
||||||
|
Never use `cat` for source code inspection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🔎 SEARCH POLICY
|
||||||
|
|
||||||
|
Always search before opening files.
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
rtk grep <pattern>
|
||||||
|
|
||||||
|
Avoid raw recursive searches.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 🎯 TOKEN SAFETY RULES
|
||||||
|
|
||||||
|
- Never scan the entire repository.
|
||||||
|
- Never dump full logs.
|
||||||
|
- Never output entire large files.
|
||||||
|
- Prefer targeted inspection.
|
||||||
|
|
||||||
|
Token preservation is mandatory for this project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
- Applies to the whole repository.
|
- Applies to the whole repository.
|
||||||
- Stack: NestJS + Prisma + TypeScript.
|
- Stack: NestJS + Prisma + TypeScript.
|
||||||
|
|
||||||
## Primary Goals
|
## Primary Goals
|
||||||
|
|
||||||
- Deliver minimal, safe, and focused changes.
|
- Deliver minimal, safe, and focused changes.
|
||||||
- Preserve existing API behavior unless explicitly requested.
|
- Preserve existing API behavior unless explicitly requested.
|
||||||
- Keep Prisma schema/data operations forward-safe.
|
- Keep Prisma schema/data operations forward-safe.
|
||||||
|
|
||||||
## Project Conventions
|
## Project Conventions
|
||||||
|
|
||||||
- Keep module layering consistent: `controller -> service -> prisma/shared service`.
|
- Keep module layering consistent: `controller -> service -> prisma/shared service`.
|
||||||
- Reuse shared services for cross-module business logic (for example, sales invoice create flow).
|
- Reuse shared services for cross-module business logic (for example, sales invoice create flow).
|
||||||
- Keep DTO validation at API boundaries; avoid unchecked `any` in new code.
|
- Keep DTO validation at API boundaries; avoid unchecked `any` in new code.
|
||||||
@@ -19,6 +164,7 @@ Operational guide for AI/coding agents working in `consumer_api`.
|
|||||||
- Prefer explicit Prisma `select`/`include` to control payload shape.
|
- Prefer explicit Prisma `select`/`include` to control payload shape.
|
||||||
|
|
||||||
## Sales Invoice / TSP Rules
|
## Sales Invoice / TSP Rules
|
||||||
|
|
||||||
- `originalSend`, `correctionSend`, and `revoke` flows should be consistent and auditable.
|
- `originalSend`, `correctionSend`, and `revoke` flows should be consistent and auditable.
|
||||||
- For correction/revoke creation, prepare data from the related invoice when required.
|
- For correction/revoke creation, prepare data from the related invoice when required.
|
||||||
- Persist attempt records with clear status transitions (`QUEUED` -> final status).
|
- Persist attempt records with clear status transitions (`QUEUED` -> final status).
|
||||||
@@ -26,6 +172,7 @@ Operational guide for AI/coding agents working in `consumer_api`.
|
|||||||
- Avoid changing fiscal/tax status semantics without explicit approval.
|
- Avoid changing fiscal/tax status semantics without explicit approval.
|
||||||
|
|
||||||
## Prisma and Migration Safety
|
## Prisma and Migration Safety
|
||||||
|
|
||||||
- Treat committed migrations as immutable history.
|
- Treat committed migrations as immutable history.
|
||||||
- Prefer forward migrations; avoid destructive resets unless explicitly requested.
|
- Prefer forward migrations; avoid destructive resets unless explicitly requested.
|
||||||
- For data-affecting changes:
|
- For data-affecting changes:
|
||||||
@@ -34,12 +181,14 @@ Operational guide for AI/coding agents working in `consumer_api`.
|
|||||||
- keep logic idempotent where possible.
|
- keep logic idempotent where possible.
|
||||||
|
|
||||||
## Editing Principles
|
## Editing Principles
|
||||||
|
|
||||||
- Do not modify unrelated files.
|
- Do not modify unrelated files.
|
||||||
- Do not revert user changes unless asked.
|
- Do not revert user changes unless asked.
|
||||||
- Keep functions cohesive; extract shared logic when duplication appears.
|
- Keep functions cohesive; extract shared logic when duplication appears.
|
||||||
- Remove debug leftovers (`console.log`, dead code) before finishing unless explicitly needed.
|
- Remove debug leftovers (`console.log`, dead code) before finishing unless explicitly needed.
|
||||||
|
|
||||||
## Validation Checklist (before handoff)
|
## Validation Checklist (before handoff)
|
||||||
|
|
||||||
1. Read target module/service/DTO end-to-end before edits.
|
1. Read target module/service/DTO end-to-end before edits.
|
||||||
2. Apply minimal patch with consistent naming.
|
2. Apply minimal patch with consistent naming.
|
||||||
3. Run typecheck: `pnpm -s tsc --noEmit`.
|
3. Run typecheck: `pnpm -s tsc --noEmit`.
|
||||||
@@ -47,12 +196,14 @@ Operational guide for AI/coding agents working in `consumer_api`.
|
|||||||
5. Summarize changed files and behavior impact clearly.
|
5. Summarize changed files and behavior impact clearly.
|
||||||
|
|
||||||
## Useful Commands
|
## Useful Commands
|
||||||
|
|
||||||
- Typecheck: `pnpm -s tsc --noEmit`
|
- Typecheck: `pnpm -s tsc --noEmit`
|
||||||
- Migration status: `pnpm prisma migrate status`
|
- Migration status: `pnpm prisma migrate status`
|
||||||
- Create migration: `pnpm prisma migrate dev --name <name>`
|
- Create migration: `pnpm prisma migrate dev --name <name>`
|
||||||
- Deploy migrations: `pnpm prisma migrate deploy`
|
- Deploy migrations: `pnpm prisma migrate deploy`
|
||||||
|
|
||||||
## Communication Style
|
## Communication Style
|
||||||
|
|
||||||
- Be concise and implementation-focused.
|
- Be concise and implementation-focused.
|
||||||
- Call out assumptions/risk before risky steps.
|
- Call out assumptions/risk before risky steps.
|
||||||
- Provide practical next actions after task completion.
|
- Provide practical next actions after task completion.
|
||||||
@@ -60,6 +211,7 @@ Operational guide for AI/coding agents working in `consumer_api`.
|
|||||||
## Do / Don't (Repo-Specific)
|
## Do / Don't (Repo-Specific)
|
||||||
|
|
||||||
### Do
|
### Do
|
||||||
|
|
||||||
- Do derive correction/revoke invoice creation data from `relatedInvoice` when the flow requires historical consistency.
|
- Do derive correction/revoke invoice creation data from `relatedInvoice` when the flow requires historical consistency.
|
||||||
- Do keep TSP attempt lifecycle explicit (`QUEUED`, then update with provider result and timestamps).
|
- Do keep TSP attempt lifecycle explicit (`QUEUED`, then update with provider result and timestamps).
|
||||||
- Do use shared invoice-creation service instead of duplicating create logic across modules.
|
- Do use shared invoice-creation service instead of duplicating create logic across modules.
|
||||||
@@ -67,6 +219,7 @@ Operational guide for AI/coding agents working in `consumer_api`.
|
|||||||
- Do keep Prisma queries tight with `select`/`include` only for fields you actually use.
|
- Do keep Prisma queries tight with `select`/`include` only for fields you actually use.
|
||||||
|
|
||||||
### Don't
|
### Don't
|
||||||
|
|
||||||
- Don't pass undefined/out-of-scope variables in TSP flows (common regressions: `invoice_id`, `attemptId`, `pos_id` mismatches).
|
- Don't pass undefined/out-of-scope variables in TSP flows (common regressions: `invoice_id`, `attemptId`, `pos_id` mismatches).
|
||||||
- Don't leave placeholder query blocks (for example empty `select: {}`) in production code.
|
- Don't leave placeholder query blocks (for example empty `select: {}`) in production code.
|
||||||
- Don't mix method semantics (`send` vs `originalSend`) across services without verifying signatures.
|
- Don't mix method semantics (`send` vs `originalSend`) across services without verifying signatures.
|
||||||
@@ -74,12 +227,14 @@ Operational guide for AI/coding agents working in `consumer_api`.
|
|||||||
- Don't change invoice type semantics (`ORIGINAL`, `CORRECTION`, `REVOKE`) implicitly.
|
- Don't change invoice type semantics (`ORIGINAL`, `CORRECTION`, `REVOKE`) implicitly.
|
||||||
|
|
||||||
### Common Pitfalls To Recheck
|
### Common Pitfalls To Recheck
|
||||||
|
|
||||||
- Incorrect relation field names (`tax_id` on wrong model, missing relation selects).
|
- Incorrect relation field names (`tax_id` on wrong model, missing relation selects).
|
||||||
- Building payloads from the wrong invoice (must match the expected new/ref invoice in each flow).
|
- Building payloads from the wrong invoice (must match the expected new/ref invoice in each flow).
|
||||||
- Creating attempts without persisting request payload and final response payload.
|
- Creating attempts without persisting request payload and final response payload.
|
||||||
- Mismatch between DTO shapes and shared service input contracts.
|
- Mismatch between DTO shapes and shared service input contracts.
|
||||||
|
|
||||||
## Thread Notes (May 2026)
|
## Thread Notes (May 2026)
|
||||||
|
|
||||||
- Shared sale-invoice creation was introduced and must be injected/exported correctly in consuming modules (example failure: `UnknownDependenciesException` for `SharedSaleInvoiceCreateService`).
|
- Shared sale-invoice creation was introduced and must be injected/exported correctly in consuming modules (example failure: `UnknownDependenciesException` for `SharedSaleInvoiceCreateService`).
|
||||||
- In `SalesInvoiceTspService.revoke`, prepare all creation/update data from `relatedInvoice` (no external `dataToUpdate` argument expected).
|
- In `SalesInvoiceTspService.revoke`, prepare all creation/update data from `relatedInvoice` (no external `dataToUpdate` argument expected).
|
||||||
- Prisma client is generated to `src/generated/prisma` via:
|
- Prisma client is generated to `src/generated/prisma` via:
|
||||||
@@ -94,6 +249,7 @@ Operational guide for AI/coding agents working in `consumer_api`.
|
|||||||
- For heavy license provisioning (100+), request path should not synchronously insert all licenses; queue/background + batching is required.
|
- For heavy license provisioning (100+), request path should not synchronously insert all licenses; queue/background + batching is required.
|
||||||
|
|
||||||
## Migration Drift Playbook (Prisma/MySQL)
|
## Migration Drift Playbook (Prisma/MySQL)
|
||||||
|
|
||||||
- If Prisma reports `modified after applied`, never edit an already-applied migration in-place for shared environments; create a new forward migration instead.
|
- If Prisma reports `modified after applied`, never edit an already-applied migration in-place for shared environments; create a new forward migration instead.
|
||||||
- If migration history and DB drift mismatch:
|
- If migration history and DB drift mismatch:
|
||||||
1. Verify local migration folders are complete and ordered.
|
1. Verify local migration folders are complete and ordered.
|
||||||
@@ -104,6 +260,7 @@ Operational guide for AI/coding agents working in `consumer_api`.
|
|||||||
- `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)
|
## 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.
|
- 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.
|
- 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 list APIs that are expensive or frequently read, prefer read-through cache with TTL and invalidation on every related write path.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -57,26 +57,22 @@ export class GoodsService {
|
|||||||
|
|
||||||
async findAll(guildId: string) {
|
async findAll(guildId: string) {
|
||||||
const cacheKey = RedisKeyMaker.guildGoodsList(guildId)
|
const cacheKey = RedisKeyMaker.guildGoodsList(guildId)
|
||||||
const cached = await this.redisService.getJson<{ goods: unknown[]; total: number }>(
|
return this.redisService.getAndSet(
|
||||||
cacheKey,
|
cacheKey,
|
||||||
|
'paginate',
|
||||||
|
async () => {
|
||||||
|
const [goods, total] = await this.prisma.$transaction([
|
||||||
|
this.prisma.good.findMany({
|
||||||
|
where: this.defaultWhere(guildId),
|
||||||
|
select: this.defaultSelect,
|
||||||
|
}),
|
||||||
|
this.prisma.good.count(),
|
||||||
|
])
|
||||||
|
|
||||||
|
return { items: goods, total }
|
||||||
|
},
|
||||||
|
this.listCacheTtlSeconds,
|
||||||
)
|
)
|
||||||
if (cached) {
|
|
||||||
return ResponseMapper.paginate(cached.goods, { total: cached.total })
|
|
||||||
}
|
|
||||||
|
|
||||||
const [goods, total] = await this.prisma.$transaction([
|
|
||||||
this.prisma.good.findMany({
|
|
||||||
where: this.defaultWhere(guildId),
|
|
||||||
select: this.defaultSelect,
|
|
||||||
}),
|
|
||||||
this.prisma.good.count(),
|
|
||||||
])
|
|
||||||
|
|
||||||
await this.redisService.setJson(cacheKey, { goods, total }, this.listCacheTtlSeconds)
|
|
||||||
|
|
||||||
return ResponseMapper.paginate(goods, {
|
|
||||||
total,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(guild_id: string, id: string) {
|
async findOne(guild_id: string, id: string) {
|
||||||
|
|||||||
@@ -31,20 +31,20 @@ export class StockKeepingUnitsService {
|
|||||||
|
|
||||||
async findAll(guild_id: string) {
|
async findAll(guild_id: string) {
|
||||||
const cacheKey = this.getListCacheKey(guild_id)
|
const cacheKey = this.getListCacheKey(guild_id)
|
||||||
const cached = await this.redisService.getJson<unknown[]>(cacheKey)
|
return await this.redisService.getAndSet(
|
||||||
if (cached) {
|
cacheKey,
|
||||||
return ResponseMapper.list(cached)
|
'list',
|
||||||
}
|
async () => {
|
||||||
|
const items = await this.prisma.stockKeepingUnits.findMany({
|
||||||
|
where: { guild_id },
|
||||||
|
orderBy: { created_at: 'desc' },
|
||||||
|
})
|
||||||
|
|
||||||
const items = await this.prisma.stockKeepingUnits.findMany({
|
const mappedData = items.map(this.mapData)
|
||||||
where: { guild_id },
|
return mappedData
|
||||||
orderBy: { created_at: 'desc' },
|
},
|
||||||
})
|
this.listCacheTtlSeconds,
|
||||||
|
)
|
||||||
const mappedData = items.map(this.mapData)
|
|
||||||
await this.redisService.setJson(cacheKey, mappedData, this.listCacheTtlSeconds)
|
|
||||||
|
|
||||||
return ResponseMapper.list(mappedData)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(guild_id: string, data: CreateStockKeepingUnitDto) {
|
async create(guild_id: string, data: CreateStockKeepingUnitDto) {
|
||||||
|
|||||||
@@ -17,14 +17,6 @@ export class PartnerActivatedLicensesService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
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: {
|
||||||
@@ -32,69 +24,70 @@ export class PartnerActivatedLicensesService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const [activatedLicense, total] = await this.prisma.$transaction(async tx => [
|
const cacheKey = RedisKeyMaker.partnerActivatedLicensesList(partner_id, page, perPage)
|
||||||
await tx.licenseActivation.findMany({
|
return await this.redisService.getAndSet(cacheKey, 'paginate', async () => {
|
||||||
where: defaultWhere,
|
const [activatedLicense, total] = await this.prisma.$transaction(async tx => [
|
||||||
skip: (page - 1) * perPage,
|
await tx.licenseActivation.findMany({
|
||||||
take: perPage,
|
where: defaultWhere,
|
||||||
select: {
|
skip: (page - 1) * perPage,
|
||||||
id: true,
|
take: perPage,
|
||||||
starts_at: true,
|
select: {
|
||||||
expires_at: true,
|
id: true,
|
||||||
created_at: true,
|
starts_at: true,
|
||||||
account_allocations: {
|
expires_at: true,
|
||||||
select: {
|
created_at: true,
|
||||||
id: true,
|
account_allocations: {
|
||||||
account_id: true,
|
select: {
|
||||||
|
id: true,
|
||||||
|
account_id: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
business_activity: {
|
||||||
business_activity: {
|
select: {
|
||||||
select: {
|
id: true,
|
||||||
id: true,
|
name: true,
|
||||||
name: true,
|
consumer: {
|
||||||
consumer: {
|
select: {
|
||||||
select: {
|
id: true,
|
||||||
id: true,
|
...QUERY_CONSTANTS.CONSUMER.infoSelect,
|
||||||
...QUERY_CONSTANTS.CONSUMER.infoSelect,
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
}),
|
await tx.licenseActivation.count({
|
||||||
await tx.licenseActivation.count({
|
where: defaultWhere,
|
||||||
where: defaultWhere,
|
}),
|
||||||
}),
|
])
|
||||||
])
|
|
||||||
|
|
||||||
const mappedLicenses = activatedLicense.map(license => {
|
const mappedLicenses = activatedLicense.map(license => {
|
||||||
const { account_allocations, business_activity, ...rest } = license
|
const { account_allocations, business_activity, ...rest } = license
|
||||||
const { consumer, ...businessActivityRest } = business_activity
|
const { consumer, ...businessActivityRest } = business_activity
|
||||||
|
|
||||||
const mappedConsumer = consumer_mappersUtil(consumer)
|
const mappedConsumer = consumer_mappersUtil(consumer)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
license: {
|
||||||
|
accounts_limit: account_allocations.length,
|
||||||
|
allocated_account_count: account_allocations.filter(
|
||||||
|
allocation => allocation.account_id !== null,
|
||||||
|
).length,
|
||||||
|
},
|
||||||
|
business_activity: {
|
||||||
|
...business_activity,
|
||||||
|
consumer_name: mappedConsumer.name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
...rest,
|
items: mappedLicenses,
|
||||||
license: {
|
page,
|
||||||
accounts_limit: account_allocations.length,
|
perPage,
|
||||||
allocated_account_count: account_allocations.filter(
|
total,
|
||||||
allocation => allocation.account_id !== null,
|
|
||||||
).length,
|
|
||||||
},
|
|
||||||
business_activity: {
|
|
||||||
...business_activity,
|
|
||||||
consumer_name: mappedConsumer.name,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.redisService.setJson(cacheKey, { data: mappedLicenses, total }, 300)
|
|
||||||
|
|
||||||
return ResponseMapper.paginate(mappedLicenses, {
|
|
||||||
page,
|
|
||||||
perPage,
|
|
||||||
total,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// async update(partnerId: string, id: string, data: UpdateLicenseDto) {
|
// async update(partnerId: string, id: string, data: UpdateLicenseDto) {
|
||||||
|
|||||||
+28
-39
@@ -65,56 +65,45 @@ export class PartnerLicenseChargeTransactionService {
|
|||||||
page,
|
page,
|
||||||
perPage,
|
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,
|
||||||
}
|
}
|
||||||
|
return await this.redisService.getAndSet(cacheKey, 'paginate', async () => {
|
||||||
|
const [transactions, total] = await this.prisma.$transaction(async tx => [
|
||||||
|
await tx.licenseChargeTransaction.findMany({
|
||||||
|
where: defaultWhere,
|
||||||
|
skip: (page - 1) * perPage,
|
||||||
|
take: perPage,
|
||||||
|
select: this.defaultSelect,
|
||||||
|
}),
|
||||||
|
await tx.licenseChargeTransaction.count({
|
||||||
|
where: defaultWhere,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
const [transactions, total] = await this.prisma.$transaction(async tx => [
|
const mappedTransactions = transactions.map(this.mappedTransaction)
|
||||||
await tx.licenseChargeTransaction.findMany({
|
return {
|
||||||
where: defaultWhere,
|
items: mappedTransactions,
|
||||||
skip: (page - 1) * perPage,
|
page,
|
||||||
take: perPage,
|
perPage,
|
||||||
select: this.defaultSelect,
|
total,
|
||||||
}),
|
}
|
||||||
await tx.licenseChargeTransaction.count({
|
|
||||||
where: defaultWhere,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
|
|
||||||
const mappedTransactions = transactions.map(this.mappedTransaction)
|
|
||||||
await this.redisService.setJson(cacheKey, { data: mappedTransactions, total }, 300)
|
|
||||||
|
|
||||||
return ResponseMapper.paginate(mappedTransactions, {
|
|
||||||
page,
|
|
||||||
perPage,
|
|
||||||
total,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(partner_id: string, id: string) {
|
async findOne(partner_id: string, id: string) {
|
||||||
const cacheKey = RedisKeyMaker.partnerLicenseChargeTransactionDetail(partner_id, id)
|
const cacheKey = RedisKeyMaker.partnerLicenseChargeTransactionDetail(partner_id, id)
|
||||||
const cached = await this.redisService.getJson<unknown>(cacheKey)
|
return await this.redisService.getAndSet(cacheKey, 'single', async () => {
|
||||||
if (cached) {
|
const transaction = await this.prisma.licenseChargeTransaction.findUniqueOrThrow({
|
||||||
return ResponseMapper.single(cached)
|
where: {
|
||||||
}
|
id,
|
||||||
|
partner_id,
|
||||||
const transaction = await this.prisma.licenseChargeTransaction.findUniqueOrThrow({
|
},
|
||||||
where: {
|
select: this.defaultSelect,
|
||||||
id,
|
})
|
||||||
partner_id,
|
return this.mappedTransaction(transaction)
|
||||||
},
|
|
||||||
select: this.defaultSelect,
|
|
||||||
})
|
})
|
||||||
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) {
|
||||||
|
|||||||
@@ -143,39 +143,26 @@ export class PartnersService {
|
|||||||
|
|
||||||
async findAll() {
|
async findAll() {
|
||||||
const cacheKey = RedisKeyMaker.partnersList()
|
const cacheKey = RedisKeyMaker.partnersList()
|
||||||
const cached = await this.redisService.getJson<unknown[]>(cacheKey)
|
return await this.redisService.getAndSet(cacheKey, 'list', async () => {
|
||||||
if (cached) {
|
const partners = await this.prisma.partner.findMany({
|
||||||
console.log('cached', cached)
|
select: this.defaultSelect,
|
||||||
|
})
|
||||||
|
|
||||||
return ResponseMapper.list(cached)
|
return partners.map(this.mapPartner)
|
||||||
}
|
|
||||||
|
|
||||||
const partners = await this.prisma.partner.findMany({
|
|
||||||
select: this.defaultSelect,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const mappedPartners = partners.map(this.mapPartner)
|
|
||||||
await this.redisService.setJson(cacheKey, mappedPartners, 300)
|
|
||||||
|
|
||||||
return ResponseMapper.list(mappedPartners)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: string) {
|
async findOne(id: string) {
|
||||||
const cacheKey = RedisKeyMaker.partnerDetail(id)
|
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({
|
return await this.redisService.getAndSet(cacheKey, 'single', async () => {
|
||||||
where: { id },
|
const partner = await this.prisma.partner.findUniqueOrThrow({
|
||||||
select: this.defaultSelect,
|
where: { id },
|
||||||
|
select: this.defaultSelect,
|
||||||
|
})
|
||||||
|
|
||||||
|
return 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) {
|
||||||
|
|||||||
@@ -61,34 +61,23 @@ export class BusinessActivitiesService {
|
|||||||
|
|
||||||
async findAll(consumer_id: string) {
|
async findAll(consumer_id: string) {
|
||||||
const cacheKey = RedisKeyMaker.consumerBusinessActivitiesList(consumer_id)
|
const cacheKey = RedisKeyMaker.consumerBusinessActivitiesList(consumer_id)
|
||||||
const cached = await this.redisService.getJson<unknown[]>(cacheKey)
|
return await this.redisService.getAndSet(cacheKey, 'list', async () => {
|
||||||
if (cached) {
|
return await this.businessActivitiesQueryService.findAllByConsumer(
|
||||||
return ResponseMapper.list(cached)
|
|
||||||
}
|
|
||||||
|
|
||||||
const businessActivities =
|
|
||||||
await this.businessActivitiesQueryService.findAllByConsumer(
|
|
||||||
consumer_id,
|
consumer_id,
|
||||||
this.defaultSelect,
|
this.defaultSelect,
|
||||||
)
|
)
|
||||||
await this.redisService.setJson(cacheKey, businessActivities, this.cacheTtlSeconds)
|
})
|
||||||
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 cacheKey = RedisKeyMaker.consumerBusinessActivityInfo(consumer_id, id)
|
||||||
const cached = await this.redisService.getJson<unknown>(cacheKey)
|
return await this.redisService.getAndSet(cacheKey, 'list', async () => {
|
||||||
if (cached) {
|
return await this.businessActivitiesQueryService.findOneByConsumer(
|
||||||
return ResponseMapper.single(cached)
|
consumer_id,
|
||||||
}
|
id,
|
||||||
|
this.defaultSelect,
|
||||||
const businessActivity = await this.businessActivitiesQueryService.findOneByConsumer(
|
)
|
||||||
consumer_id,
|
})
|
||||||
id,
|
|
||||||
this.defaultSelect,
|
|
||||||
)
|
|
||||||
await this.redisService.setJson(cacheKey, businessActivity, this.cacheTtlSeconds)
|
|
||||||
return ResponseMapper.single(businessActivity)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(consumer_id: string, id: string, data: any) {
|
async update(consumer_id: string, id: string, data: any) {
|
||||||
|
|||||||
@@ -21,25 +21,21 @@ export class ConsumerService {
|
|||||||
|
|
||||||
async getInfo(consumer_id: string) {
|
async getInfo(consumer_id: string) {
|
||||||
const cacheKey = RedisKeyMaker.consumerInfo(consumer_id)
|
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({
|
return await this.redisService.getAndSet(cacheKey, 'single', async () => {
|
||||||
where: {
|
const consumer = await this.prisma.consumer.findUniqueOrThrow({
|
||||||
id: consumer_id,
|
where: {
|
||||||
},
|
id: consumer_id,
|
||||||
select: {
|
},
|
||||||
id: true,
|
select: {
|
||||||
status: true,
|
id: true,
|
||||||
...QUERY_CONSTANTS.CONSUMER.infoSelect,
|
status: true,
|
||||||
},
|
...QUERY_CONSTANTS.CONSUMER.infoSelect,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return 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) {
|
||||||
|
|||||||
@@ -103,30 +103,26 @@ export class BusinessActivitiesService {
|
|||||||
page,
|
page,
|
||||||
perPage,
|
perPage,
|
||||||
)
|
)
|
||||||
const cached = await this.redisService.getJson<{ items: unknown[]; total: number }>(
|
return await this.redisService.getAndSet(cacheKey, 'paginate', async () => {
|
||||||
cacheKey,
|
const where = this.defaultWhere(partner_id, consumer_id)
|
||||||
)
|
const [businessActivities, total] = await this.prisma.$transaction(async tx => [
|
||||||
if (cached) {
|
await tx.businessActivity.findMany({
|
||||||
return ResponseMapper.paginate(cached.items, { total: cached.total, page, perPage })
|
where,
|
||||||
}
|
select: this.defaultSelect,
|
||||||
|
skip: (page - 1) * perPage,
|
||||||
|
take: perPage,
|
||||||
|
}),
|
||||||
|
await tx.businessActivity.count({ where }),
|
||||||
|
])
|
||||||
|
const mappedItems = businessActivities.map(this.prepareBusinessActivityResponse)
|
||||||
|
|
||||||
const where = this.defaultWhere(partner_id, consumer_id)
|
return {
|
||||||
const [businessActivities, total] = await this.prisma.$transaction(async tx => [
|
items: mappedItems,
|
||||||
await tx.businessActivity.findMany({
|
total,
|
||||||
where,
|
page,
|
||||||
select: this.defaultSelect,
|
perPage,
|
||||||
skip: (page - 1) * perPage,
|
}
|
||||||
take: perPage,
|
})
|
||||||
}),
|
|
||||||
await tx.businessActivity.count({ where }),
|
|
||||||
])
|
|
||||||
const mappedItems = businessActivities.map(this.prepareBusinessActivityResponse)
|
|
||||||
await this.redisService.setJson(
|
|
||||||
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) {
|
||||||
@@ -135,21 +131,16 @@ export class BusinessActivitiesService {
|
|||||||
consumer_id,
|
consumer_id,
|
||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
const cached = await this.redisService.getJson<unknown>(cacheKey)
|
return await this.redisService.getAndSet(cacheKey, 'single', async () => {
|
||||||
if (cached) {
|
const businessActivity = await this.prisma.businessActivity.findUnique({
|
||||||
return ResponseMapper.single(cached)
|
where: {
|
||||||
}
|
...this.defaultWhere(partner_id, consumer_id),
|
||||||
|
id,
|
||||||
const businessActivity = await this.prisma.businessActivity.findUnique({
|
},
|
||||||
where: {
|
select: this.defaultSelect,
|
||||||
...this.defaultWhere(partner_id, consumer_id),
|
})
|
||||||
id,
|
return this.prepareBusinessActivityResponse(businessActivity)
|
||||||
},
|
|
||||||
select: this.defaultSelect,
|
|
||||||
})
|
})
|
||||||
const mappedItem = this.prepareBusinessActivityResponse(businessActivity)
|
|
||||||
await this.redisService.setJson(cacheKey, mappedItem, this.cacheTtlSeconds)
|
|
||||||
return ResponseMapper.single(mappedItem)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
|
|||||||
@@ -65,44 +65,40 @@ export class BusinessActivityComplexesService {
|
|||||||
page,
|
page,
|
||||||
perPage,
|
perPage,
|
||||||
)
|
)
|
||||||
const cached = await this.redisService.getJson<{ items: unknown[]; total: number }>(
|
return await this.redisService.getAndSet(cacheKey, 'paginate', async () => {
|
||||||
cacheKey,
|
const where = this.defaultWhere(partner_id, consumer_id, business_activity_id)
|
||||||
)
|
const [complexes, total] = await this.prisma.$transaction(async tx => [
|
||||||
if (cached) {
|
await tx.complex.findMany({
|
||||||
return ResponseMapper.paginate(cached.items, { total: cached.total, page, perPage })
|
where,
|
||||||
}
|
select: {
|
||||||
|
...this.defaultSelect,
|
||||||
const where = this.defaultWhere(partner_id, consumer_id, business_activity_id)
|
_count: {
|
||||||
const [complexes, total] = await this.prisma.$transaction(async tx => [
|
select: {
|
||||||
await tx.complex.findMany({
|
pos_list: true,
|
||||||
where,
|
},
|
||||||
select: {
|
|
||||||
...this.defaultSelect,
|
|
||||||
_count: {
|
|
||||||
select: {
|
|
||||||
pos_list: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
skip: (page - 1) * perPage,
|
||||||
skip: (page - 1) * perPage,
|
take: perPage,
|
||||||
take: perPage,
|
}),
|
||||||
}),
|
await tx.complex.count({ where }),
|
||||||
await tx.complex.count({ where }),
|
])
|
||||||
])
|
|
||||||
|
const mappedComplexes = complexes.map(complex => {
|
||||||
|
const { _count, ...rest } = complex
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
pos_count: _count.pos_list,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const mappedComplexes = complexes.map(complex => {
|
|
||||||
const { _count, ...rest } = complex
|
|
||||||
return {
|
return {
|
||||||
...rest,
|
items: mappedComplexes,
|
||||||
pos_count: _count.pos_list,
|
total,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await this.redisService.setJson(
|
|
||||||
cacheKey,
|
|
||||||
{ items: mappedComplexes, total },
|
|
||||||
this.cacheTtlSeconds,
|
|
||||||
)
|
|
||||||
return ResponseMapper.paginate(mappedComplexes, { total, page, perPage })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(
|
async findOne(
|
||||||
@@ -117,25 +113,21 @@ export class BusinessActivityComplexesService {
|
|||||||
business_activity_id,
|
business_activity_id,
|
||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
const cached = await this.redisService.getJson<unknown>(cacheKey)
|
return await this.redisService.getAndSet(cacheKey, 'single', async () => {
|
||||||
if (cached) {
|
const complex = await this.prisma.complex.findFirst({
|
||||||
return ResponseMapper.single(cached)
|
where: {
|
||||||
}
|
...this.defaultWhere(partner_id, consumer_id, business_activity_id),
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
select: this.defaultSelect,
|
||||||
|
})
|
||||||
|
|
||||||
const complex = await this.prisma.complex.findFirst({
|
if (!complex) {
|
||||||
where: {
|
throw new BadRequestException('شعبه مورد نظر یافت نشد.')
|
||||||
...this.defaultWhere(partner_id, consumer_id, business_activity_id),
|
}
|
||||||
id,
|
|
||||||
},
|
return complex
|
||||||
select: this.defaultSelect,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!complex) {
|
|
||||||
throw new BadRequestException('شعبه مورد نظر یافت نشد.')
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.redisService.setJson(cacheKey, complex, this.cacheTtlSeconds)
|
|
||||||
return ResponseMapper.single(complex)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
|
|||||||
@@ -64,60 +64,42 @@ 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 cacheKey = RedisKeyMaker.partnerConsumersList(partner_id, page, perPage)
|
||||||
const cached = await this.redisService.getJson<{ items: unknown[]; total: number }>(
|
return await this.redisService.getAndSet(cacheKey, 'paginate', async () => {
|
||||||
cacheKey,
|
const [consumers, total] = await this.prisma.$transaction(async tx => [
|
||||||
)
|
await tx.consumer.findMany({
|
||||||
if (cached) {
|
where: this.defaultWhere(partner_id),
|
||||||
return ResponseMapper.paginate(cached.items, { total: cached.total, page, perPage })
|
select: this.defaultSelect,
|
||||||
}
|
skip: (page - 1) * perPage,
|
||||||
|
take: 10,
|
||||||
|
}),
|
||||||
|
await tx.consumer.count({
|
||||||
|
where: this.defaultWhere(partner_id),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
const mappedConsumers = consumers.map(mapConsumerWithLicenseUtil)
|
||||||
|
|
||||||
const [consumers, total] = await this.prisma.$transaction(async tx => [
|
return {
|
||||||
await tx.consumer.findMany({
|
items: mappedConsumers,
|
||||||
where: this.defaultWhere(partner_id),
|
total,
|
||||||
select: this.defaultSelect,
|
page,
|
||||||
skip: (page - 1) * perPage,
|
perPage,
|
||||||
take: 10,
|
}
|
||||||
}),
|
|
||||||
await tx.consumer.count({
|
|
||||||
where: this.defaultWhere(partner_id),
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
const mappedItems = consumers.map(mapConsumerWithLicenseUtil)
|
|
||||||
await this.redisService.setJson(
|
|
||||||
cacheKey,
|
|
||||||
{ items: mappedItems, total },
|
|
||||||
this.infoCacheTtlSeconds,
|
|
||||||
)
|
|
||||||
return ResponseMapper.paginate(mappedItems, {
|
|
||||||
total,
|
|
||||||
page,
|
|
||||||
perPage,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 cacheKey = RedisKeyMaker.partnerConsumerInfo(partner_id, consumer_id)
|
||||||
const cached = await this.redisService.getJson<unknown>(cacheKey)
|
return await this.redisService.getAndSet(cacheKey, 'single', async () => {
|
||||||
if (cached) {
|
const consumer = await this.prisma.consumer.findUniqueOrThrow({
|
||||||
return ResponseMapper.single(cached)
|
where: {
|
||||||
}
|
...(this.defaultWhere(partner_id) as any),
|
||||||
|
id: consumer_id,
|
||||||
|
},
|
||||||
|
select: this.defaultSelect,
|
||||||
|
})
|
||||||
|
|
||||||
const consumer = await this.prisma.consumer.findUniqueOrThrow({
|
return mapConsumerWithLicenseUtil(consumer)
|
||||||
where: {
|
|
||||||
...(this.defaultWhere(partner_id) as any),
|
|
||||||
id: consumer_id,
|
|
||||||
},
|
|
||||||
select: this.defaultSelect,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
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) {
|
||||||
|
|||||||
@@ -52,47 +52,44 @@ export class GoodsService {
|
|||||||
guild_id: string,
|
guild_id: string,
|
||||||
consumer_account_id: string,
|
consumer_account_id: string,
|
||||||
) {
|
) {
|
||||||
const cacheKey = RedisKeyMaker.posGoodsList(guild_id, business_activity_id)
|
const cacheKey = RedisKeyMaker.posGoodsList(business_activity_id, guild_id)
|
||||||
try {
|
return this.redisService.getAndSet(
|
||||||
const cached = await this.redisService.getJson<unknown[]>(cacheKey)
|
cacheKey,
|
||||||
if (cached) {
|
'list',
|
||||||
return ResponseMapper.list(cached)
|
async () => {
|
||||||
}
|
const goods = await this.prisma.good.findMany({
|
||||||
throw new Error('Cache miss')
|
where: {
|
||||||
} catch (error) {
|
OR: [
|
||||||
const goods = await this.prisma.good.findMany({
|
{
|
||||||
where: {
|
is_default_guild_good: true,
|
||||||
OR: [
|
category: {
|
||||||
{
|
guild_id,
|
||||||
is_default_guild_good: true,
|
},
|
||||||
category: {
|
},
|
||||||
guild_id,
|
{
|
||||||
|
business_activity_id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
...this.defaultSelect,
|
||||||
|
consumer_account_good_favorites: {
|
||||||
|
where: {
|
||||||
|
consumer_account_id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
business_activity_id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
...this.defaultSelect,
|
|
||||||
consumer_account_good_favorites: {
|
|
||||||
where: {
|
|
||||||
consumer_account_id,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
|
||||||
|
|
||||||
const mappedGoods = goods.map(good => ({
|
const mappedGoods = goods.map(good => ({
|
||||||
...good,
|
...good,
|
||||||
is_favorite: good.consumer_account_good_favorites.length > 0,
|
is_favorite: good.consumer_account_good_favorites.length > 0,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
await this.redisService.setJson(cacheKey, mappedGoods, this.listCacheTtlSeconds)
|
return mappedGoods
|
||||||
|
},
|
||||||
return ResponseMapper.list(mappedGoods)
|
this.listCacheTtlSeconds,
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(good_id: string, business_activity_id: string, guild_id: string) {
|
async findOne(good_id: string, business_activity_id: string, guild_id: string) {
|
||||||
|
|||||||
+109
-105
@@ -19,93 +19,95 @@ export class PosService {
|
|||||||
|
|
||||||
async getInfo(pos_id: string) {
|
async getInfo(pos_id: string) {
|
||||||
const cacheKey = RedisKeyMaker.posInfo(pos_id)
|
const cacheKey = RedisKeyMaker.posInfo(pos_id)
|
||||||
const cached = await this.redisService.getJson<unknown>(cacheKey)
|
return await this.redisService.getAndSet(
|
||||||
if (cached) {
|
cacheKey,
|
||||||
return ResponseMapper.single(cached)
|
'single',
|
||||||
}
|
async () => {
|
||||||
|
const pos = await this.prisma.pos.findUniqueOrThrow({
|
||||||
const pos = await this.prisma.pos.findUniqueOrThrow({
|
where: {
|
||||||
where: {
|
id: pos_id,
|
||||||
id: pos_id,
|
},
|
||||||
},
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
complex: {
|
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
|
||||||
name: true,
|
name: true,
|
||||||
branch_code: true,
|
complex: {
|
||||||
business_activity: {
|
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
economic_code: true,
|
branch_code: true,
|
||||||
license_activation: {
|
business_activity: {
|
||||||
select: {
|
select: {
|
||||||
expires_at: true,
|
id: true,
|
||||||
license: {
|
name: true,
|
||||||
|
economic_code: true,
|
||||||
|
license_activation: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
expires_at: true,
|
||||||
charge_transaction: {
|
license: {
|
||||||
select: {
|
select: {
|
||||||
partner: {
|
id: true,
|
||||||
|
charge_transaction: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
partner: {
|
||||||
name: true,
|
select: {
|
||||||
code: true,
|
id: true,
|
||||||
logo_url: true,
|
name: true,
|
||||||
|
code: true,
|
||||||
|
logo_url: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
guild: {
|
||||||
},
|
select: {
|
||||||
guild: {
|
id: true,
|
||||||
select: {
|
name: true,
|
||||||
id: true,
|
code: true,
|
||||||
name: true,
|
},
|
||||||
code: true,
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
},
|
const { name, complex } = pos
|
||||||
})
|
const { name: complexName, id: complexId, business_activity } = complex
|
||||||
const { name, complex } = pos
|
const {
|
||||||
const { name: complexName, id: complexId, business_activity } = complex
|
name: businessName,
|
||||||
const {
|
id: businessId,
|
||||||
name: businessName,
|
guild,
|
||||||
id: businessId,
|
economic_code,
|
||||||
guild,
|
license_activation,
|
||||||
economic_code,
|
} = business_activity
|
||||||
license_activation,
|
|
||||||
} = business_activity
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name,
|
name,
|
||||||
complex: {
|
complex: {
|
||||||
id: complexId,
|
id: complexId,
|
||||||
name: complexName,
|
name: complexName,
|
||||||
branch_code: complex.branch_code,
|
branch_code: complex.branch_code,
|
||||||
|
},
|
||||||
|
businessActivity: {
|
||||||
|
id: businessId,
|
||||||
|
name: businessName,
|
||||||
|
economic_code,
|
||||||
|
},
|
||||||
|
guild,
|
||||||
|
license_info: {
|
||||||
|
expires_at: license_activation?.expires_at,
|
||||||
|
license_id: license_activation?.license.id,
|
||||||
|
},
|
||||||
|
partner: license_activation?.license.charge_transaction.partner,
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
},
|
},
|
||||||
businessActivity: {
|
this.infoCacheTtlSeconds,
|
||||||
id: businessId,
|
)
|
||||||
name: businessName,
|
|
||||||
economic_code,
|
|
||||||
},
|
|
||||||
guild,
|
|
||||||
license_info: {
|
|
||||||
expires_at: license_activation?.expires_at,
|
|
||||||
license_id: license_activation?.license.id,
|
|
||||||
},
|
|
||||||
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) {
|
||||||
@@ -153,57 +155,59 @@ 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 cacheKey = RedisKeyMaker.posMe(account_id, pos_id)
|
||||||
const cached = await this.redisService.getJson<unknown>(cacheKey)
|
return await this.redisService.getAndSet(
|
||||||
if (cached) {
|
cacheKey,
|
||||||
return ResponseMapper.single(cached)
|
'single',
|
||||||
}
|
async () => {
|
||||||
|
const pos = await this.prisma.consumerAccount.findUniqueOrThrow({
|
||||||
const pos = await this.prisma.consumerAccount.findUniqueOrThrow({
|
where: {
|
||||||
where: {
|
id: account_id,
|
||||||
id: account_id,
|
consumer: {
|
||||||
consumer: {
|
status: ConsumerStatus.ACTIVE,
|
||||||
status: ConsumerStatus.ACTIVE,
|
business_activities: {
|
||||||
business_activities: {
|
|
||||||
some: {
|
|
||||||
complexes: {
|
|
||||||
some: {
|
some: {
|
||||||
pos_list: {
|
complexes: {
|
||||||
some: {
|
some: {
|
||||||
id: pos_id,
|
pos_list: {
|
||||||
|
some: {
|
||||||
|
id: pos_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
role: true,
|
|
||||||
consumer: {
|
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
type: true,
|
role: true,
|
||||||
status: true,
|
consumer: {
|
||||||
...QUERY_CONSTANTS.CONSUMER.infoSelect,
|
select: {
|
||||||
|
id: true,
|
||||||
|
type: true,
|
||||||
|
status: true,
|
||||||
|
...QUERY_CONSTANTS.CONSUMER.infoSelect,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
select: {
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
account: {
|
|
||||||
select: {
|
const { consumer, ...rest } = pos
|
||||||
username: true,
|
|
||||||
},
|
const payload = {
|
||||||
},
|
...rest,
|
||||||
|
consumer: consumer_mappersUtil(consumer),
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
},
|
},
|
||||||
})
|
this.meCacheTtlSeconds,
|
||||||
|
)
|
||||||
const { consumer, ...rest } = pos
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
...rest,
|
|
||||||
consumer: consumer_mappersUtil(consumer),
|
|
||||||
}
|
|
||||||
await this.redisService.setJson(cacheKey, payload, this.meCacheTtlSeconds)
|
|
||||||
return ResponseMapper.single(payload)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
|
|||||||
password: env('DATABASE_PASSWORD'),
|
password: env('DATABASE_PASSWORD'),
|
||||||
database: env('DATABASE_NAME'),
|
database: env('DATABASE_NAME'),
|
||||||
port: env('DATABASE_PORT') ? Number(env('DATABASE_PORT')) : 3306,
|
port: env('DATABASE_PORT') ? Number(env('DATABASE_PORT')) : 3306,
|
||||||
connectionLimit: 50,
|
connectionLimit: 10,
|
||||||
})
|
})
|
||||||
|
|
||||||
const prismaOptions = getPrismaOptions()
|
const prismaOptions = getPrismaOptions()
|
||||||
|
|||||||
+148
-24
@@ -1,27 +1,35 @@
|
|||||||
|
import {
|
||||||
|
MapperWrapper,
|
||||||
|
Paginated,
|
||||||
|
ResponseMapper,
|
||||||
|
} from '@/common/response/response-mapper'
|
||||||
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common'
|
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common'
|
||||||
import Redis from 'ioredis'
|
import Redis from 'ioredis'
|
||||||
|
|
||||||
|
interface PaginatedCache<T> {
|
||||||
|
items: T[]
|
||||||
|
total: number
|
||||||
|
page?: number
|
||||||
|
perPage?: number
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RedisService implements OnModuleDestroy {
|
export class RedisService implements OnModuleDestroy {
|
||||||
|
private static readonly scanCount = 100
|
||||||
private readonly logger = new Logger(RedisService.name)
|
private readonly logger = new Logger(RedisService.name)
|
||||||
private readonly client: Redis
|
private readonly client: Redis
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const host = process.env.REDIS_HOST || 'redis'
|
|
||||||
const port = Number(process.env.REDIS_PORT || 6379)
|
|
||||||
const password = process.env.REDIS_PASSWORD || undefined
|
|
||||||
const db = Number(process.env.REDIS_DB || 0)
|
|
||||||
|
|
||||||
this.client = new Redis({
|
this.client = new Redis({
|
||||||
host,
|
host: process.env.REDIS_HOST || 'redis',
|
||||||
port,
|
port: Number(process.env.REDIS_PORT || 6379),
|
||||||
password,
|
password: process.env.REDIS_PASSWORD || undefined,
|
||||||
db,
|
db: Number(process.env.REDIS_DB || 0),
|
||||||
lazyConnect: true,
|
lazyConnect: true,
|
||||||
maxRetriesPerRequest: 3,
|
maxRetriesPerRequest: 3,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.client.on('error', (error) => {
|
this.client.on('error', error => {
|
||||||
this.logger.error(`Redis error: ${error.message}`)
|
this.logger.error(`Redis error: ${error.message}`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -53,11 +61,7 @@ export class RedisService implements OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async set(
|
async set(key: string, value: string, ttlSeconds?: number): Promise<'OK' | null> {
|
||||||
key: string,
|
|
||||||
value: string,
|
|
||||||
ttlSeconds?: number,
|
|
||||||
): Promise<'OK' | null> {
|
|
||||||
const client = await this.getClient()
|
const client = await this.getClient()
|
||||||
if (ttlSeconds && ttlSeconds > 0) {
|
if (ttlSeconds && ttlSeconds > 0) {
|
||||||
return client.set(key, value, 'EX', ttlSeconds)
|
return client.set(key, value, 'EX', ttlSeconds)
|
||||||
@@ -65,11 +69,7 @@ export class RedisService implements OnModuleDestroy {
|
|||||||
return client.set(key, value)
|
return client.set(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
async setJson(
|
async setJson(key: string, value: unknown, ttlSeconds?: number): Promise<'OK' | null> {
|
||||||
key: string,
|
|
||||||
value: unknown,
|
|
||||||
ttlSeconds?: number,
|
|
||||||
): Promise<'OK' | null> {
|
|
||||||
return this.set(key, JSON.stringify(value), ttlSeconds)
|
return this.set(key, JSON.stringify(value), ttlSeconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,14 +101,12 @@ export class RedisService implements OnModuleDestroy {
|
|||||||
'MATCH',
|
'MATCH',
|
||||||
pattern,
|
pattern,
|
||||||
'COUNT',
|
'COUNT',
|
||||||
100,
|
RedisService.scanCount,
|
||||||
)
|
)
|
||||||
cursor = nextCursor
|
cursor = nextCursor
|
||||||
|
|
||||||
if (keys.length > 0) {
|
if (keys.length > 0) {
|
||||||
const pipeline = client.pipeline()
|
const results = await this.deleteKeys(client, keys)
|
||||||
keys.forEach(k => pipeline.del(k))
|
|
||||||
const results = await pipeline.exec()
|
|
||||||
if (results) {
|
if (results) {
|
||||||
deletedCount += results.reduce((sum, [, value]) => {
|
deletedCount += results.reduce((sum, [, value]) => {
|
||||||
return sum + (typeof value === 'number' ? value : 0)
|
return sum + (typeof value === 'number' ? value : 0)
|
||||||
@@ -127,6 +125,132 @@ export class RedisService implements OnModuleDestroy {
|
|||||||
return deletedCounts.reduce((sum, count) => sum + count, 0)
|
return deletedCounts.reduce((sum, count) => sum + count, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAndSet<T>(
|
||||||
|
key: string,
|
||||||
|
type: 'single',
|
||||||
|
callback: () => Promise<T>,
|
||||||
|
ttlSeconds?: number,
|
||||||
|
): Promise<MapperWrapper<T>>
|
||||||
|
async getAndSet<T>(
|
||||||
|
key: string,
|
||||||
|
type: 'list',
|
||||||
|
callback: () => Promise<T[]>,
|
||||||
|
ttlSeconds?: number,
|
||||||
|
): Promise<MapperWrapper<T>>
|
||||||
|
async getAndSet<T>(
|
||||||
|
key: string,
|
||||||
|
type: 'paginate',
|
||||||
|
callback: () => Promise<PaginatedCache<T>>,
|
||||||
|
ttlSeconds?: number,
|
||||||
|
): Promise<Paginated<T>>
|
||||||
|
async getAndSet<T>(
|
||||||
|
key: string,
|
||||||
|
type: 'single' | 'list' | 'paginate',
|
||||||
|
callback: () => Promise<T | T[] | PaginatedCache<T>>,
|
||||||
|
ttlSeconds?: number,
|
||||||
|
): Promise<MapperWrapper<T> | Paginated<T>> {
|
||||||
|
switch (type) {
|
||||||
|
case 'single':
|
||||||
|
return this.getAndSetSingle(key, callback as () => Promise<T>, ttlSeconds)
|
||||||
|
case 'list':
|
||||||
|
return this.getAndSetList(key, callback as () => Promise<T[]>, ttlSeconds)
|
||||||
|
case 'paginate':
|
||||||
|
return this.getAndSetPaginate(
|
||||||
|
key,
|
||||||
|
callback as () => Promise<PaginatedCache<T>>,
|
||||||
|
ttlSeconds,
|
||||||
|
)
|
||||||
|
default: {
|
||||||
|
const neverType: never = type
|
||||||
|
throw new Error(`Unsupported cache type: ${neverType}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAndSetSingle<T>(
|
||||||
|
key: string,
|
||||||
|
callback: () => Promise<T>,
|
||||||
|
ttlSeconds?: number,
|
||||||
|
): Promise<MapperWrapper<T>> {
|
||||||
|
const cached = await this.tryReadJson<T>(key)
|
||||||
|
if (cached !== null) {
|
||||||
|
return ResponseMapper.single(cached)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await callback()
|
||||||
|
await this.tryWriteJson(key, data, ttlSeconds)
|
||||||
|
return ResponseMapper.single(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAndSetList<T>(
|
||||||
|
key: string,
|
||||||
|
callback: () => Promise<T[]>,
|
||||||
|
ttlSeconds?: number,
|
||||||
|
): Promise<MapperWrapper<T>> {
|
||||||
|
const cached = await this.tryReadJson<T[]>(key)
|
||||||
|
if (cached !== null) {
|
||||||
|
return ResponseMapper.list(cached)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await callback()
|
||||||
|
await this.tryWriteJson(key, data, ttlSeconds)
|
||||||
|
return ResponseMapper.list(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getAndSetPaginate<T>(
|
||||||
|
key: string,
|
||||||
|
callback: () => Promise<PaginatedCache<T>>,
|
||||||
|
ttlSeconds?: number,
|
||||||
|
): Promise<Paginated<T>> {
|
||||||
|
const cached = await this.tryReadJson<PaginatedCache<T>>(key)
|
||||||
|
if (cached !== null) {
|
||||||
|
return ResponseMapper.paginate(cached.items, {
|
||||||
|
total: cached.total,
|
||||||
|
page: cached.page,
|
||||||
|
perPage: cached.perPage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await callback()
|
||||||
|
await this.tryWriteJson(key, data, ttlSeconds)
|
||||||
|
return ResponseMapper.paginate(data.items, {
|
||||||
|
total: data.total,
|
||||||
|
page: data.page,
|
||||||
|
perPage: data.perPage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async tryReadJson<T>(key: string): Promise<T | null> {
|
||||||
|
try {
|
||||||
|
return await this.getJson<T>(key)
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Redis read failed for "${key}": ${(error as Error).message}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async tryWriteJson(
|
||||||
|
key: string,
|
||||||
|
value: unknown,
|
||||||
|
ttlSeconds: number = 300,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.setJson(key, value, ttlSeconds)
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`Redis write failed for "${key}": ${(error as Error).message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deleteKeys(
|
||||||
|
client: Redis,
|
||||||
|
keys: string[],
|
||||||
|
): Promise<Array<[Error | null, unknown]>> {
|
||||||
|
const pipeline = client.pipeline()
|
||||||
|
keys.forEach(key => pipeline.del(key))
|
||||||
|
const results = await pipeline.exec()
|
||||||
|
return results ?? []
|
||||||
|
}
|
||||||
|
|
||||||
async onModuleDestroy() {
|
async onModuleDestroy() {
|
||||||
await this.client.quit()
|
await this.client.quit()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user