feat(pricing): 开发定价方案

This commit is contained in:
R524809
2026-09-28 17:34:33 +08:00
parent a29dd92676
commit 84b230e69a
54 changed files with 6264 additions and 236 deletions
+190
View File
@@ -0,0 +1,190 @@
import {
Body,
Controller,
Delete,
Get,
Headers,
Inject,
Param,
Patch,
Post,
Query,
Req,
Res,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { z } from 'zod';
import {
pricingPreviewRequestSchema,
pricingPolicyRequestSchema,
type Workspace,
} from '@atlas/contracts';
import { CurrentWorkspace } from '../workspace/current-workspace.decorator.js';
import type { AuthenticatedRequest } from '../auth/auth.guard.js';
import { ApiError } from '../common/api-error.js';
import { PricingService } from './pricing.service.js';
type Response = { status: (status: number) => unknown };
function parse<T>(schema: z.ZodType<T>, value: unknown): T {
const result = schema.safeParse(value);
if (!result.success)
throw new ApiError(
422,
'VALIDATION_ERROR',
'请检查定价输入',
result.error.issues.map((i) => ({
field: i.path.join('.'),
message: i.message,
})),
);
return result.data;
}
function key(value: string | undefined) {
if (!value || value.length > 200)
throw new ApiError(
400,
'IDEMPOTENCY_KEY_REQUIRED',
'需要 Idempotency-Key',
);
return value;
}
@ApiTags('Pricing')
@ApiBearerAuth('access-token')
@Controller()
export class PricingController {
constructor(
@Inject(PricingService) private readonly service: PricingService,
) {}
@Get('pricing/categories') async categories(
@CurrentWorkspace() w: Workspace,
@Query('store_id') store: string,
@Query('q') q: string | undefined,
) {
return {
data: await this.service.categories(
w.id,
parse(z.uuid(), store),
parse(z.string().max(200), q ?? ''),
),
};
}
@Get('pricing/catalog') async catalog(
@CurrentWorkspace() w: Workspace,
@Query('store_id') store: string,
) {
return {
data: await this.service.catalog(w.id, parse(z.uuid(), store)),
};
}
@Get('stores/:id/pricing-policies') async policies(
@CurrentWorkspace() w: Workspace,
@Param('id') id: string,
) {
return {
data: (await this.service.catalog(w.id, parse(z.uuid(), id)))
.policies,
};
}
@Post('pricing/quote-previews') async preview(
@CurrentWorkspace() w: Workspace,
@Req() req: AuthenticatedRequest,
@Headers('idempotency-key') k: string | undefined,
@Body() body: unknown,
@Res({ passthrough: true }) res: Response,
) {
const r = await this.service.preview(
w.id,
req.auth.userId,
key(k),
parse(pricingPreviewRequestSchema, body),
);
res.status(r.status);
return r.body;
}
@Post('stores/:id/pricing-policies') async create(
@CurrentWorkspace() w: Workspace,
@Req() req: AuthenticatedRequest,
@Param('id') id: string,
@Headers('idempotency-key') k: string | undefined,
@Body() body: unknown,
@Res({ passthrough: true }) res: Response,
) {
const r = await this.service.writePolicy(
w.id,
req.auth.userId,
key(k),
parse(z.uuid(), id),
null,
'create',
parse(pricingPolicyRequestSchema, body),
null,
);
res.status(r.status);
return r.body;
}
@Patch('pricing-policies/:id') async update(
@CurrentWorkspace() w: Workspace,
@Req() req: AuthenticatedRequest,
@Param('id') id: string,
@Headers('idempotency-key') k: string | undefined,
@Headers('if-match') match: string | undefined,
@Body() body: unknown,
) {
if (!match || !/^(?:[1-9]\d*|"[1-9]\d*")$/.test(match))
throw new ApiError(
428,
'PRECONDITION_REQUIRED',
'需要方案版本 If-Match',
);
return (
await this.service.writePolicy(
w.id,
req.auth.userId,
key(k),
null,
parse(z.uuid(), id),
'update',
parse(pricingPolicyRequestSchema, body),
Number(match.replaceAll('"', '')),
)
).body;
}
@Post('pricing-policies/:id/set-default') async defaultPolicy(
@CurrentWorkspace() w: Workspace,
@Req() req: AuthenticatedRequest,
@Param('id') id: string,
@Headers('idempotency-key') k: string | undefined,
@Res({ passthrough: true }) res: Response,
) {
const r = await this.service.writePolicy(
w.id,
req.auth.userId,
key(k),
null,
parse(z.uuid(), id),
'default',
null,
null,
);
res.status(r.status);
return r.body;
}
@Delete('pricing-policies/:id') async archive(
@CurrentWorkspace() w: Workspace,
@Req() req: AuthenticatedRequest,
@Param('id') id: string,
@Headers('idempotency-key') k: string | undefined,
) {
return (
await this.service.writePolicy(
w.id,
req.auth.userId,
key(k),
null,
parse(z.uuid(), id),
'archive',
null,
null,
)
).body;
}
}