ginskill-init 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.wrangler/cache/pages.json +4 -0
- package/.wrangler/cache/wrangler-account.json +6 -0
- package/DEVELOPMENT.md +510 -0
- package/README.md +104 -0
- package/agents/developer.md +56 -0
- package/agents/frontend-design.md +69 -0
- package/agents/mobile-reviewer.md +36 -0
- package/agents/review-code.md +49 -0
- package/agents/security-scanner.md +50 -0
- package/agents/tester.md +72 -0
- package/bin/cli.js +461 -0
- package/landing/ai-build-ai.png +0 -0
- package/landing/index.html +1495 -0
- package/landing/logo.png +0 -0
- package/package.json +37 -0
- package/skills/active-life-dev/SKILL.md +157 -0
- package/skills/active-life-dev/docs/auth.md +187 -0
- package/skills/active-life-dev/docs/customers.md +216 -0
- package/skills/active-life-dev/docs/integrations.md +209 -0
- package/skills/active-life-dev/docs/inventory.md +192 -0
- package/skills/active-life-dev/docs/modules.md +181 -0
- package/skills/active-life-dev/docs/orders.md +180 -0
- package/skills/active-life-dev/docs/patterns.md +319 -0
- package/skills/active-life-dev/docs/products.md +216 -0
- package/skills/active-life-dev/docs/schema.md +502 -0
- package/skills/active-life-dev/docs/setup.md +169 -0
- package/skills/active-life-dev/docs/vouchers.md +144 -0
- package/skills/ai-asset-generator/SKILL.md +247 -0
- package/skills/ai-asset-generator/docs/gen-image.md +274 -0
- package/skills/ai-asset-generator/docs/genvideo.md +341 -0
- package/skills/ai-asset-generator/docs/remove-background.md +19 -0
- package/skills/ai-asset-generator/lib/bg-remove.mjs +34 -0
- package/skills/ai-asset-generator/lib/env.mjs +48 -0
- package/skills/ai-asset-generator/lib/kie-client.mjs +100 -0
- package/skills/ai-build-ai/SKILL.md +127 -0
- package/skills/ai-build-ai/docs/agent-teams.md +293 -0
- package/skills/ai-build-ai/docs/checkpointing.md +161 -0
- package/skills/ai-build-ai/docs/create-agent.md +399 -0
- package/skills/ai-build-ai/docs/create-mcp.md +395 -0
- package/skills/ai-build-ai/docs/create-skill.md +299 -0
- package/skills/ai-build-ai/docs/headless-mode.md +614 -0
- package/skills/ai-build-ai/docs/hooks.md +578 -0
- package/skills/ai-build-ai/docs/memory-claude-md.md +375 -0
- package/skills/ai-build-ai/docs/output-styles.md +208 -0
- package/skills/ai-build-ai/docs/overview.md +162 -0
- package/skills/ai-build-ai/docs/permissions.md +391 -0
- package/skills/ai-build-ai/docs/plugins.md +396 -0
- package/skills/ai-build-ai/docs/sandbox.md +262 -0
- package/skills/ai-build-ai/docs/team-lead-workflow.md +648 -0
- package/skills/ant-design/SKILL.md +323 -0
- package/skills/ant-design/docs/components.md +160 -0
- package/skills/ant-design/docs/data-entry.md +406 -0
- package/skills/ant-design/docs/display.md +594 -0
- package/skills/ant-design/docs/feedback.md +451 -0
- package/skills/ant-design/docs/key-components.md +414 -0
- package/skills/ant-design/docs/navigation.md +310 -0
- package/skills/ant-design/docs/pro-components.md +543 -0
- package/skills/ant-design/docs/setup.md +213 -0
- package/skills/ant-design/docs/theme.md +265 -0
- package/skills/flutter-performance/SKILL.md +803 -0
- package/skills/flutter-performance/references/flutter-patterns.md +595 -0
- package/skills/icon-generator/SKILL.md +270 -0
- package/skills/mobile-app-review/SKILL.md +321 -0
- package/skills/mobile-app-review/references/apple-review.md +132 -0
- package/skills/mobile-app-review/references/google-play-review.md +203 -0
- package/skills/mongodb/SKILL.md +667 -0
- package/skills/mongodb/references/mongoose-patterns.md +368 -0
- package/skills/nestjs-architecture/SKILL.md +1086 -0
- package/skills/nestjs-architecture/references/advanced-patterns.md +590 -0
- package/skills/performance/SKILL.md +509 -0
- package/skills/react-fsd-architecture/SKILL.md +693 -0
- package/skills/react-fsd-architecture/references/fsd-patterns.md +747 -0
- package/skills/react-native-expo/SKILL.md +128 -0
- package/skills/react-native-expo/references/data-layer.md +252 -0
- package/skills/react-native-expo/references/design-system.md +252 -0
- package/skills/react-native-expo/references/navigation.md +199 -0
- package/skills/react-native-expo/references/performance.md +229 -0
- package/skills/react-native-expo/references/platform-services.md +179 -0
- package/skills/react-native-expo/references/state-management.md +209 -0
- package/skills/react-native-expo/references/ui-patterns.md +301 -0
- package/skills/react-query/SKILL.md +685 -0
- package/skills/react-query/references/query-patterns.md +365 -0
- package/skills/review-code/SKILL.md +374 -0
- package/skills/review-code/references/clean-code-principles.md +395 -0
- package/skills/review-code/references/frontend-patterns.md +136 -0
- package/skills/review-code/references/nestjs-patterns.md +184 -0
- package/skills/security-scanner/SKILL.md +366 -0
- package/skills/security-scanner/references/nestjs-security.md +260 -0
- package/skills/security-scanner/references/nextjs-security.md +201 -0
- package/skills/security-scanner/references/react-native-security.md +199 -0
- package/skills/traefik/SKILL.md +105 -0
- package/skills/traefik/docs/advanced-routing.md +186 -0
- package/skills/traefik/docs/auth-providers.md +137 -0
- package/skills/traefik/docs/cicd-devops.md +396 -0
- package/skills/traefik/docs/core-config.md +171 -0
- package/skills/traefik/docs/distributed-config.md +96 -0
- package/skills/traefik/docs/docker-compose.md +182 -0
- package/skills/traefik/docs/ha-performance.md +177 -0
- package/skills/traefik/docs/kubernetes.md +278 -0
- package/skills/traefik/docs/middleware.md +205 -0
- package/skills/traefik/docs/monitoring.md +357 -0
- package/skills/traefik/docs/security.md +391 -0
- package/skills/traefik/docs/tls-acme.md +155 -0
- package/skills/ui-ux-pro-max/SKILL.md +377 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Active Life Backend - Order System
|
|
2
|
+
|
|
3
|
+
## Order Lifecycle
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
PENDING → PROCESSING → SHIPPED → DELIVERED
|
|
7
|
+
│ │ │
|
|
8
|
+
▼ ▼ ▼
|
|
9
|
+
CANCELLED CANCELLED RETURN_PENDING → RETURN_RECEIVED → DELIVERED (restocked)
|
|
10
|
+
│
|
|
11
|
+
▼
|
|
12
|
+
RETURN_REJECTED
|
|
13
|
+
|
|
14
|
+
REFUND_PENDING → REFUNDED
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Order Statuses (OrderStatus enum)
|
|
18
|
+
|
|
19
|
+
| Status | Description | Triggered By |
|
|
20
|
+
|--------|------------|-------------|
|
|
21
|
+
| `PENDING` | New order, awaiting processing | Customer/Staff creates order |
|
|
22
|
+
| `PROCESSING` | Staff processing, selecting inventory lots | Staff processes order |
|
|
23
|
+
| `SHIPPED` | Order shipped to customer | Staff marks as shipped |
|
|
24
|
+
| `DELIVERED` | Customer received order | Staff confirms delivery |
|
|
25
|
+
| `CANCELLED` | Order cancelled | Customer or staff cancels |
|
|
26
|
+
| `RETURN_PENDING` | Return requested | Customer requests return |
|
|
27
|
+
| `RETURN_RECEIVED` | Returned items received | Staff confirms receipt |
|
|
28
|
+
| `RETURN_REJECTED` | Return request rejected | Staff rejects return |
|
|
29
|
+
| `REFUND_PENDING` | Refund in progress | Staff initiates refund |
|
|
30
|
+
| `REFUNDED` | Refund completed | Staff completes refund |
|
|
31
|
+
|
|
32
|
+
## Order Model
|
|
33
|
+
|
|
34
|
+
```prisma
|
|
35
|
+
model Order {
|
|
36
|
+
id String @id @default(uuid())
|
|
37
|
+
orderCode String @unique // Human-readable code
|
|
38
|
+
clientId String
|
|
39
|
+
status OrderStatus @default(PENDING)
|
|
40
|
+
paymentMethod PaymentMethod // CASH or TRANSFER
|
|
41
|
+
totalAmount Float // Sum of items
|
|
42
|
+
discountAmount Float @default(0) // Voucher discount
|
|
43
|
+
finalAmount Float // totalAmount - discountAmount + shippingFee
|
|
44
|
+
shippingAddress String?
|
|
45
|
+
shippingFee Float @default(0)
|
|
46
|
+
note String?
|
|
47
|
+
voucherId String?
|
|
48
|
+
staffId String? // Staff who processed
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Order Flow
|
|
53
|
+
|
|
54
|
+
### 1. Customer Creates Order
|
|
55
|
+
```
|
|
56
|
+
POST /api/v1/order (with jwt-client auth)
|
|
57
|
+
Body: CreateOrderDto {
|
|
58
|
+
items: [{ productId, comboId, quantity }],
|
|
59
|
+
paymentMethod: "CASH" | "TRANSFER",
|
|
60
|
+
shippingAddress: string,
|
|
61
|
+
voucherCode?: string,
|
|
62
|
+
note?: string
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
- Validates product/combo availability
|
|
66
|
+
- Applies voucher if provided
|
|
67
|
+
- Calculates totals
|
|
68
|
+
- Creates Order + OrderItems
|
|
69
|
+
- Creates LogOrder (CREATE)
|
|
70
|
+
|
|
71
|
+
### 2. Staff Creates Order (on behalf)
|
|
72
|
+
```
|
|
73
|
+
POST /api/v1/order/staff-create (staff auth)
|
|
74
|
+
Body: StaffCreateOrderDto {
|
|
75
|
+
clientId: string,
|
|
76
|
+
items: [...],
|
|
77
|
+
paymentMethod: ...,
|
|
78
|
+
...
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 3. Process Order (assign inventory)
|
|
83
|
+
```
|
|
84
|
+
PATCH /api/v1/order/:id/process (staff auth)
|
|
85
|
+
Body: ProcessOrderDto {
|
|
86
|
+
items: [{
|
|
87
|
+
orderItemId: string,
|
|
88
|
+
lots: [{
|
|
89
|
+
inventoryItemId: string, // ProductInventoryItem
|
|
90
|
+
quantity: number
|
|
91
|
+
}]
|
|
92
|
+
}]
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
- Staff selects specific inventory lots (FIFO by expiry)
|
|
96
|
+
- Deducts inventory quantities
|
|
97
|
+
- Creates InventoryTransactions (EXPORT type)
|
|
98
|
+
- Status: PENDING → PROCESSING
|
|
99
|
+
|
|
100
|
+
### 4. Ship Order
|
|
101
|
+
```
|
|
102
|
+
PATCH /api/v1/order/:id/ship (staff auth)
|
|
103
|
+
```
|
|
104
|
+
- Status: PROCESSING → SHIPPED
|
|
105
|
+
- Creates LogOrder (SHIP)
|
|
106
|
+
|
|
107
|
+
### 5. Deliver Order
|
|
108
|
+
```
|
|
109
|
+
PATCH /api/v1/order/:id/deliver (staff auth)
|
|
110
|
+
```
|
|
111
|
+
- Status: SHIPPED → DELIVERED
|
|
112
|
+
- Creates LogOrder (DELIVER)
|
|
113
|
+
- Awards loyalty points to client
|
|
114
|
+
|
|
115
|
+
### 6. Cancel Order
|
|
116
|
+
```
|
|
117
|
+
PATCH /api/v1/order/:id/cancel (staff or client auth)
|
|
118
|
+
Body: CancelOrderDto { reason: string }
|
|
119
|
+
```
|
|
120
|
+
- Status: PENDING/PROCESSING → CANCELLED
|
|
121
|
+
- If PROCESSING: reverses inventory deductions
|
|
122
|
+
- Creates LogOrder (CANCEL_CLIENT or CANCEL_STAFF)
|
|
123
|
+
|
|
124
|
+
### 7. Return Request
|
|
125
|
+
```
|
|
126
|
+
POST /api/v1/order/:id/return-request (client or staff)
|
|
127
|
+
Body: CreateReturnRequestDto { reason, items[] }
|
|
128
|
+
```
|
|
129
|
+
- Status: DELIVERED → RETURN_PENDING
|
|
130
|
+
- Creates LogOrder (RETURN_CREATE)
|
|
131
|
+
|
|
132
|
+
### 8. Complete Return
|
|
133
|
+
```
|
|
134
|
+
PATCH /api/v1/order/:id/return-complete (staff)
|
|
135
|
+
Body: CompleteReturnRequestDto { action: "receive" | "reject" }
|
|
136
|
+
```
|
|
137
|
+
- If receive: RETURN_PENDING → RETURN_RECEIVED, restock inventory
|
|
138
|
+
- If reject: RETURN_PENDING → RETURN_REJECTED
|
|
139
|
+
|
|
140
|
+
### 9. Refund Request
|
|
141
|
+
```
|
|
142
|
+
POST /api/v1/order/:id/refund-request (staff)
|
|
143
|
+
Body: CreateRefundRequestDto { amount, reason }
|
|
144
|
+
```
|
|
145
|
+
- Status: → REFUND_PENDING
|
|
146
|
+
|
|
147
|
+
### 10. Complete Refund
|
|
148
|
+
```
|
|
149
|
+
PATCH /api/v1/order/:id/refund-complete (staff)
|
|
150
|
+
```
|
|
151
|
+
- Status: REFUND_PENDING → REFUNDED
|
|
152
|
+
- Creates LogOrder (REFUND_COMPLETE)
|
|
153
|
+
|
|
154
|
+
## Audit Trail (LogOrder)
|
|
155
|
+
|
|
156
|
+
Every status change creates a log entry:
|
|
157
|
+
```prisma
|
|
158
|
+
model LogOrder {
|
|
159
|
+
id String @id @default(uuid())
|
|
160
|
+
orderId String
|
|
161
|
+
type LogOrderType // CREATE, PROCESS, SHIP, DELIVER, CANCEL_*, RETURN_*, REFUND_*
|
|
162
|
+
note String?
|
|
163
|
+
userId String? // Staff who made the change
|
|
164
|
+
createdAt DateTime @default(now())
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Payment Methods
|
|
169
|
+
- `CASH` — Cash on delivery
|
|
170
|
+
- `TRANSFER` — Bank transfer
|
|
171
|
+
|
|
172
|
+
## Key Business Rules
|
|
173
|
+
1. Only PENDING orders can be processed
|
|
174
|
+
2. Only PROCESSING orders can be shipped
|
|
175
|
+
3. Only SHIPPED orders can be delivered
|
|
176
|
+
4. Only PENDING/PROCESSING orders can be cancelled
|
|
177
|
+
5. Only DELIVERED orders can have return requests
|
|
178
|
+
6. Cancellation of PROCESSING orders must reverse inventory
|
|
179
|
+
7. Points are awarded only on DELIVERED status
|
|
180
|
+
8. Order code is auto-generated and unique
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Active Life Backend - Coding Patterns & Conventions
|
|
2
|
+
|
|
3
|
+
## Project Structure Convention
|
|
4
|
+
|
|
5
|
+
Every feature module follows this structure:
|
|
6
|
+
```
|
|
7
|
+
src/<module-name>/
|
|
8
|
+
├── <module-name>.module.ts
|
|
9
|
+
├── <module-name>.controller.ts
|
|
10
|
+
├── <module-name>.service.ts
|
|
11
|
+
└── dto/
|
|
12
|
+
├── create-<module-name>.dto.ts
|
|
13
|
+
└── update-<module-name>.dto.ts
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Module Pattern
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { Module } from '@nestjs/common';
|
|
20
|
+
import { XxxController } from './xxx.controller';
|
|
21
|
+
import { XxxService } from './xxx.service';
|
|
22
|
+
|
|
23
|
+
@Module({
|
|
24
|
+
controllers: [XxxController],
|
|
25
|
+
providers: [XxxService],
|
|
26
|
+
exports: [XxxService], // Only if other modules need this service
|
|
27
|
+
})
|
|
28
|
+
export class XxxModule {}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Controller Pattern
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { Controller, Get, Post, Body, Param, Query, Patch, Delete } from '@nestjs/common';
|
|
35
|
+
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
|
36
|
+
import { Public } from 'src/decorators/customize';
|
|
37
|
+
import { ResponseMessage, User } from 'src/decorators/customize';
|
|
38
|
+
import { IUser } from 'src/interface/users.interface';
|
|
39
|
+
|
|
40
|
+
@Controller('xxx')
|
|
41
|
+
@ApiTags('xxx')
|
|
42
|
+
export class XxxController {
|
|
43
|
+
constructor(private readonly xxxService: XxxService) {}
|
|
44
|
+
|
|
45
|
+
@Post()
|
|
46
|
+
@ApiBearerAuth()
|
|
47
|
+
@ApiOperation({ summary: 'Create xxx' })
|
|
48
|
+
@ResponseMessage('Created successfully')
|
|
49
|
+
create(@Body() dto: CreateXxxDto, @User() user: IUser) {
|
|
50
|
+
return this.xxxService.create(dto, user);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Get()
|
|
54
|
+
@Public()
|
|
55
|
+
@ApiOperation({ summary: 'Get all xxx' })
|
|
56
|
+
findAll(@Query() query: any) {
|
|
57
|
+
return this.xxxService.findAll(query);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Get(':id')
|
|
61
|
+
@Public()
|
|
62
|
+
findOne(@Param('id') id: string) {
|
|
63
|
+
return this.xxxService.findOne(id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Patch(':id')
|
|
67
|
+
@ApiBearerAuth()
|
|
68
|
+
update(@Param('id') id: string, @Body() dto: UpdateXxxDto) {
|
|
69
|
+
return this.xxxService.update(id, dto);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Delete(':id')
|
|
73
|
+
@ApiBearerAuth()
|
|
74
|
+
remove(@Param('id') id: string) {
|
|
75
|
+
return this.xxxService.remove(id);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Service Pattern
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
|
|
84
|
+
import { PrismaService } from 'src/prisma/prisma.service';
|
|
85
|
+
|
|
86
|
+
@Injectable()
|
|
87
|
+
export class XxxService {
|
|
88
|
+
constructor(private prismaService: PrismaService) {}
|
|
89
|
+
|
|
90
|
+
async create(dto: CreateXxxDto, user?: IUser) {
|
|
91
|
+
// Check for duplicates if needed
|
|
92
|
+
const existing = await this.prismaService.xxx.findUnique({
|
|
93
|
+
where: { name: dto.name },
|
|
94
|
+
});
|
|
95
|
+
if (existing) {
|
|
96
|
+
throw new BadRequestException('Already exists');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return this.prismaService.xxx.create({
|
|
100
|
+
data: {
|
|
101
|
+
...dto,
|
|
102
|
+
slug: this.generateSlug(dto.name), // If applicable
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async findAll(query: any) {
|
|
108
|
+
const { page = 1, limit = 10, search } = query;
|
|
109
|
+
const skip = (page - 1) * limit;
|
|
110
|
+
|
|
111
|
+
const where = search
|
|
112
|
+
? { name: { contains: search, mode: 'insensitive' as const } }
|
|
113
|
+
: {};
|
|
114
|
+
|
|
115
|
+
const [data, total] = await Promise.all([
|
|
116
|
+
this.prismaService.xxx.findMany({
|
|
117
|
+
where,
|
|
118
|
+
skip,
|
|
119
|
+
take: +limit,
|
|
120
|
+
orderBy: { createdAt: 'desc' },
|
|
121
|
+
}),
|
|
122
|
+
this.prismaService.xxx.count({ where }),
|
|
123
|
+
]);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
data,
|
|
127
|
+
meta: {
|
|
128
|
+
total,
|
|
129
|
+
page: +page,
|
|
130
|
+
limit: +limit,
|
|
131
|
+
totalPages: Math.ceil(total / +limit),
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async findOne(id: string) {
|
|
137
|
+
const item = await this.prismaService.xxx.findUnique({
|
|
138
|
+
where: { id },
|
|
139
|
+
include: { /* relations */ },
|
|
140
|
+
});
|
|
141
|
+
if (!item) {
|
|
142
|
+
throw new NotFoundException('Not found');
|
|
143
|
+
}
|
|
144
|
+
return item;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async update(id: string, dto: UpdateXxxDto) {
|
|
148
|
+
await this.findOne(id); // Ensure exists
|
|
149
|
+
return this.prismaService.xxx.update({
|
|
150
|
+
where: { id },
|
|
151
|
+
data: dto,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async remove(id: string) {
|
|
156
|
+
await this.findOne(id); // Ensure exists
|
|
157
|
+
return this.prismaService.xxx.delete({ where: { id } });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## DTO Pattern
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { ApiProperty } from '@nestjs/swagger';
|
|
166
|
+
import { IsString, IsNotEmpty, IsOptional, IsNumber, IsEnum, IsArray } from 'class-validator';
|
|
167
|
+
import { Type } from 'class-transformer';
|
|
168
|
+
|
|
169
|
+
export class CreateXxxDto {
|
|
170
|
+
@ApiProperty({ example: 'Example name' })
|
|
171
|
+
@IsString()
|
|
172
|
+
@IsNotEmpty()
|
|
173
|
+
name: string;
|
|
174
|
+
|
|
175
|
+
@ApiProperty({ required: false })
|
|
176
|
+
@IsOptional()
|
|
177
|
+
@IsString()
|
|
178
|
+
description?: string;
|
|
179
|
+
|
|
180
|
+
@ApiProperty({ example: 100 })
|
|
181
|
+
@IsNumber()
|
|
182
|
+
@Type(() => Number) // For query params auto-conversion
|
|
183
|
+
price: number;
|
|
184
|
+
|
|
185
|
+
@ApiProperty({ enum: ProductStatus })
|
|
186
|
+
@IsEnum(ProductStatus)
|
|
187
|
+
status: ProductStatus;
|
|
188
|
+
|
|
189
|
+
@ApiProperty({ type: [String] })
|
|
190
|
+
@IsArray()
|
|
191
|
+
@IsString({ each: true })
|
|
192
|
+
images: string[];
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Update DTO** — typically partial:
|
|
197
|
+
```typescript
|
|
198
|
+
import { PartialType } from '@nestjs/mapped-types';
|
|
199
|
+
import { CreateXxxDto } from './create-xxx.dto';
|
|
200
|
+
|
|
201
|
+
export class UpdateXxxDto extends PartialType(CreateXxxDto) {}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Response Format
|
|
205
|
+
|
|
206
|
+
All responses are wrapped by `TransformInterceptor`:
|
|
207
|
+
```typescript
|
|
208
|
+
// Success response
|
|
209
|
+
{
|
|
210
|
+
"statusCode": 200,
|
|
211
|
+
"message": "Success", // From @ResponseMessage() or default
|
|
212
|
+
"data": { ... } // Actual data
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Error response (from exceptions)
|
|
216
|
+
{
|
|
217
|
+
"statusCode": 400,
|
|
218
|
+
"message": "Bad Request",
|
|
219
|
+
"error": "Detailed error message"
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Slug Generation
|
|
224
|
+
|
|
225
|
+
Vietnamese-aware slug generation (remove diacritics):
|
|
226
|
+
```typescript
|
|
227
|
+
private generateSlug(name: string): string {
|
|
228
|
+
return name
|
|
229
|
+
.normalize('NFD')
|
|
230
|
+
.replace(/[\u0300-\u036f]/g, '') // Remove diacritics
|
|
231
|
+
.replace(/đ/g, 'd').replace(/Đ/g, 'D')
|
|
232
|
+
.toLowerCase()
|
|
233
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
234
|
+
.replace(/\s+/g, '-')
|
|
235
|
+
.replace(/-+/g, '-')
|
|
236
|
+
.trim();
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Pagination Pattern
|
|
241
|
+
|
|
242
|
+
Services return paginated data with meta:
|
|
243
|
+
```typescript
|
|
244
|
+
{
|
|
245
|
+
data: [...items],
|
|
246
|
+
meta: {
|
|
247
|
+
total: 100,
|
|
248
|
+
page: 1,
|
|
249
|
+
limit: 10,
|
|
250
|
+
totalPages: 10
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Error Handling
|
|
256
|
+
|
|
257
|
+
Use NestJS built-in exceptions:
|
|
258
|
+
```typescript
|
|
259
|
+
throw new BadRequestException('Validation error message');
|
|
260
|
+
throw new NotFoundException('Resource not found');
|
|
261
|
+
throw new ForbiddenException('Access denied');
|
|
262
|
+
throw new UnauthorizedException('Invalid credentials');
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Naming Conventions
|
|
266
|
+
|
|
267
|
+
- **Files**: kebab-case (`create-product.dto.ts`)
|
|
268
|
+
- **Classes**: PascalCase (`CreateProductDto`)
|
|
269
|
+
- **Methods**: camelCase (`findAll`, `createOrder`)
|
|
270
|
+
- **Database tables**: PascalCase in Prisma schema (auto-mapped to snake_case)
|
|
271
|
+
- **API routes**: kebab-case (`/api/v1/inventory-product`)
|
|
272
|
+
- **Env vars**: SCREAMING_SNAKE_CASE (`JWT_CLIENT_ACCESS_TOKEN_SECRET`)
|
|
273
|
+
|
|
274
|
+
## Import Conventions
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// NestJS core
|
|
278
|
+
import { Injectable, Controller, Module } from '@nestjs/common';
|
|
279
|
+
// Swagger
|
|
280
|
+
import { ApiTags, ApiBearerAuth, ApiProperty } from '@nestjs/swagger';
|
|
281
|
+
// Validation
|
|
282
|
+
import { IsString, IsNotEmpty } from 'class-validator';
|
|
283
|
+
// Project internals (absolute paths from src/)
|
|
284
|
+
import { PrismaService } from 'src/prisma/prisma.service';
|
|
285
|
+
import { Public, User, ResponseMessage } from 'src/decorators/customize';
|
|
286
|
+
import { IUser } from 'src/interface/users.interface';
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Logging
|
|
290
|
+
|
|
291
|
+
The `LoggingInterceptor` automatically logs:
|
|
292
|
+
- HTTP method, URL, IP
|
|
293
|
+
- User info (if authenticated)
|
|
294
|
+
- Response status code
|
|
295
|
+
- Execution time (ms)
|
|
296
|
+
- Error details on failure
|
|
297
|
+
|
|
298
|
+
No need to add manual logging in services unless debugging specific flows.
|
|
299
|
+
|
|
300
|
+
## Global Configuration (main.ts)
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// Applied globally:
|
|
304
|
+
app.useGlobalGuards(new JwtAuthGuard(...)); // Auth on all routes
|
|
305
|
+
app.useGlobalPipes(new ValidationPipe({
|
|
306
|
+
whitelist: true, // Strip unknown properties
|
|
307
|
+
transform: true, // Auto-transform types
|
|
308
|
+
transformOptions: { enableImplicitConversion: true },
|
|
309
|
+
}));
|
|
310
|
+
app.useGlobalInterceptors(
|
|
311
|
+
new LoggingInterceptor(),
|
|
312
|
+
new TransformInterceptor(reflector),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// CORS: all origins allowed
|
|
316
|
+
// Rate limit: 10 requests / 60 seconds
|
|
317
|
+
// API versioning: URI-based (/api/v1/, /api/v2/)
|
|
318
|
+
// Swagger at /api
|
|
319
|
+
```
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Active Life Backend - Products & Catalog
|
|
2
|
+
|
|
3
|
+
## Product Architecture
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Category (hierarchical tree)
|
|
7
|
+
└──< CategoryBrand >── Brand
|
|
8
|
+
└──< ProductCategory >── StoreProduct
|
|
9
|
+
└──< StoreProductCombo (pricing/bundle)
|
|
10
|
+
└──< StoreProductComboItem ──> InventoryProduct
|
|
11
|
+
└──< StoreProductComboItem (gifts) ──> InventoryProduct
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Key Concept: Product vs Combo vs Inventory Product
|
|
15
|
+
|
|
16
|
+
- **StoreProduct** — What customers see (name, images, description, status)
|
|
17
|
+
- **StoreProductCombo** — A purchasable configuration with price (e.g., "Pack of 3", "Family Bundle")
|
|
18
|
+
- **StoreProductComboItem** — Links combo to warehouse inventory products with quantities
|
|
19
|
+
- **InventoryProduct** — Actual warehouse item (SKU, barcode, unit, brand)
|
|
20
|
+
|
|
21
|
+
**Example**:
|
|
22
|
+
```
|
|
23
|
+
StoreProduct: "Whey Protein Gold Standard"
|
|
24
|
+
├── Combo: "1kg Bag" — price: 500,000 VND
|
|
25
|
+
│ └── Item: InventoryProduct("WP-GS-1KG") × 1
|
|
26
|
+
├── Combo: "2kg Bag + Shaker" — price: 900,000 VND
|
|
27
|
+
│ ├── Item: InventoryProduct("WP-GS-2KG") × 1
|
|
28
|
+
│ └── Gift: InventoryProduct("SHAKER-01") × 1
|
|
29
|
+
└── Combo: "5kg Bulk" — price: 2,000,000 VND
|
|
30
|
+
└── Item: InventoryProduct("WP-GS-1KG") × 5
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Categories
|
|
34
|
+
|
|
35
|
+
### Hierarchical Structure (Parent-Child)
|
|
36
|
+
```prisma
|
|
37
|
+
model Category {
|
|
38
|
+
id String @id @default(uuid())
|
|
39
|
+
name String
|
|
40
|
+
slug String @unique
|
|
41
|
+
parentId String?
|
|
42
|
+
parent Category? @relation("CategoryTree", fields: [parentId], references: [id])
|
|
43
|
+
children Category[] @relation("CategoryTree")
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Example tree**:
|
|
48
|
+
```
|
|
49
|
+
Supplements
|
|
50
|
+
├── Protein
|
|
51
|
+
│ ├── Whey Protein
|
|
52
|
+
│ └── Casein Protein
|
|
53
|
+
├── Pre-Workout
|
|
54
|
+
└── Vitamins
|
|
55
|
+
Equipment
|
|
56
|
+
├── Weights
|
|
57
|
+
└── Accessories
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Recursive Category Queries
|
|
61
|
+
The service uses raw SQL for recursive queries to get full category trees:
|
|
62
|
+
```sql
|
|
63
|
+
WITH RECURSIVE category_tree AS (
|
|
64
|
+
SELECT * FROM "Category" WHERE "parentId" IS NULL
|
|
65
|
+
UNION ALL
|
|
66
|
+
SELECT c.* FROM "Category" c
|
|
67
|
+
JOIN category_tree ct ON c."parentId" = ct.id
|
|
68
|
+
)
|
|
69
|
+
SELECT * FROM category_tree;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Category-Brand Relationship
|
|
73
|
+
Many-to-many via `CategoryBrand` join table. A category can have multiple brands, and a brand can appear in multiple categories.
|
|
74
|
+
|
|
75
|
+
## Products
|
|
76
|
+
|
|
77
|
+
### StoreProduct Model
|
|
78
|
+
```prisma
|
|
79
|
+
model StoreProduct {
|
|
80
|
+
id String @id @default(uuid())
|
|
81
|
+
name String
|
|
82
|
+
slug String @unique
|
|
83
|
+
description String?
|
|
84
|
+
images String[] // Array of image URLs
|
|
85
|
+
status ProductStatus @default(NORMAL) // HIDDEN, NORMAL, HIGH
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Product Status
|
|
90
|
+
- `HIDDEN` — Not visible to customers
|
|
91
|
+
- `NORMAL` — Standard visibility
|
|
92
|
+
- `HIGH` — Featured/promoted product
|
|
93
|
+
|
|
94
|
+
### Creating a Product
|
|
95
|
+
```
|
|
96
|
+
POST /api/v1/product
|
|
97
|
+
Body: {
|
|
98
|
+
name: "Whey Protein Gold Standard",
|
|
99
|
+
description: "Premium whey protein...",
|
|
100
|
+
images: ["url1", "url2"],
|
|
101
|
+
categoryIds: ["cat-uuid-1", "cat-uuid-2"],
|
|
102
|
+
status: "NORMAL"
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
Auto-generates slug from name (Vietnamese diacritics removed).
|
|
106
|
+
|
|
107
|
+
## Combos (Product Pricing)
|
|
108
|
+
|
|
109
|
+
### StoreProductCombo Model
|
|
110
|
+
```prisma
|
|
111
|
+
model StoreProductCombo {
|
|
112
|
+
id String @id @default(uuid())
|
|
113
|
+
name String
|
|
114
|
+
price Float
|
|
115
|
+
comparePrice Float? // "Was" price for showing discounts
|
|
116
|
+
productId String
|
|
117
|
+
items StoreProductComboItem[] // Regular items
|
|
118
|
+
giftItems StoreProductComboItem[] // Gift items
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Creating a Combo
|
|
123
|
+
```
|
|
124
|
+
POST /api/v1/product/:id/combo
|
|
125
|
+
Body: {
|
|
126
|
+
name: "1kg Bag",
|
|
127
|
+
price: 500000,
|
|
128
|
+
comparePrice: 600000,
|
|
129
|
+
items: [
|
|
130
|
+
{ inventoryProductId: "inv-uuid", quantity: 1 }
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Adding Gift Items
|
|
136
|
+
```
|
|
137
|
+
POST /api/v1/product/:id/combo/:comboId/gift
|
|
138
|
+
Body: {
|
|
139
|
+
items: [
|
|
140
|
+
{ inventoryProductId: "shaker-uuid", quantity: 1 }
|
|
141
|
+
]
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Promotion History
|
|
146
|
+
When combo price changes, a history record is created:
|
|
147
|
+
```prisma
|
|
148
|
+
model StoreProductComboPromotionHistory {
|
|
149
|
+
comboId String
|
|
150
|
+
oldPrice Float
|
|
151
|
+
newPrice Float
|
|
152
|
+
reason String?
|
|
153
|
+
createdAt DateTime @default(now())
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Brands
|
|
158
|
+
|
|
159
|
+
```prisma
|
|
160
|
+
model Brand {
|
|
161
|
+
id String @id @default(uuid())
|
|
162
|
+
name String
|
|
163
|
+
slug String @unique
|
|
164
|
+
image String?
|
|
165
|
+
description String?
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Key API Endpoints
|
|
170
|
+
|
|
171
|
+
### Products
|
|
172
|
+
```
|
|
173
|
+
GET /api/v1/product — List products (public, paginated)
|
|
174
|
+
GET /api/v1/product/:id — Get product detail (public)
|
|
175
|
+
GET /api/v1/product/slug/:slug — Get by slug (public)
|
|
176
|
+
POST /api/v1/product — Create product (staff)
|
|
177
|
+
PATCH /api/v1/product/:id — Update product (staff)
|
|
178
|
+
DELETE /api/v1/product/:id — Delete product (staff)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Combos
|
|
182
|
+
```
|
|
183
|
+
POST /api/v1/product/:id/combo — Add combo to product
|
|
184
|
+
PATCH /api/v1/product/:id/combo/:comboId — Update combo
|
|
185
|
+
DELETE /api/v1/product/:id/combo/:comboId — Delete combo
|
|
186
|
+
POST /api/v1/product/:id/combo/:comboId/gift — Add gift items
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Categories
|
|
190
|
+
```
|
|
191
|
+
GET /api/v1/category — List categories (tree structure, public)
|
|
192
|
+
GET /api/v1/category/:id — Get category with products (public)
|
|
193
|
+
POST /api/v1/category — Create category (staff)
|
|
194
|
+
PATCH /api/v1/category/:id — Update category (staff)
|
|
195
|
+
DELETE /api/v1/category/:id — Delete category (staff)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Brands
|
|
199
|
+
```
|
|
200
|
+
GET /api/v1/brand — List brands (public)
|
|
201
|
+
POST /api/v1/brand — Create brand (staff)
|
|
202
|
+
PATCH /api/v1/brand/:id — Update brand (staff)
|
|
203
|
+
DELETE /api/v1/brand/:id — Delete brand (staff)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Shopping Cart Integration
|
|
207
|
+
|
|
208
|
+
Cart items reference both product and combo:
|
|
209
|
+
```prisma
|
|
210
|
+
model CartItem {
|
|
211
|
+
productId String
|
|
212
|
+
comboId String
|
|
213
|
+
quantity Int
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
Customers add combos to cart, not raw products.
|