feat: v3.4.12 ticket iteration
Refund rollback, winery T+3, multi withdraw, mini-user store detail, dev plan batch edit and WeCom dispatch, support ticket edit/attachments/batch status, package imageUrl. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -251,7 +251,6 @@ describe('validateStoreWithdraw', () => {
|
||||
requestAmount: 200,
|
||||
todayApplied: 0,
|
||||
dailyLimit: 5000,
|
||||
hasPendingRequest: false,
|
||||
hasBankAccount: true,
|
||||
};
|
||||
|
||||
@@ -261,8 +260,7 @@ describe('validateStoreWithdraw', () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects pending request / missing bank / over available', () => {
|
||||
expect(validateStoreWithdraw({ ...base, hasPendingRequest: true }).ok).toBe(false);
|
||||
it('rejects missing bank / over available', () => {
|
||||
expect(validateStoreWithdraw({ ...base, hasBankAccount: false }).ok).toBe(false);
|
||||
expect(validateStoreWithdraw({ ...base, requestAmount: 1001 }).ok).toBe(false);
|
||||
});
|
||||
|
||||
@@ -105,20 +105,16 @@ export type ValidateStoreWithdrawInput = {
|
||||
requestAmount: number;
|
||||
todayApplied: number;
|
||||
dailyLimit: number;
|
||||
hasPendingRequest: boolean;
|
||||
hasBankAccount: boolean;
|
||||
};
|
||||
|
||||
/** 门店未出账提现护栏(FIN-002 单日上限 + 幂等/账户) */
|
||||
/** 门店未出账提现护栏(FIN-002 单日上限 + 账户) */
|
||||
export function validateStoreWithdraw(
|
||||
input: ValidateStoreWithdrawInput,
|
||||
): { ok: boolean; message?: string } {
|
||||
if (!input.hasBankAccount) {
|
||||
return { ok: false, message: '请先完善入驻收款账户后再提现' };
|
||||
}
|
||||
if (input.hasPendingRequest) {
|
||||
return { ok: false, message: '已有待审核提现申请,请等待处理完成' };
|
||||
}
|
||||
if (!(input.requestAmount > 0)) {
|
||||
return { ok: false, message: '提现金额必须大于 0' };
|
||||
}
|
||||
@@ -354,3 +350,4 @@ export function orderTabToStatuses(tab: string): string[] | undefined {
|
||||
|
||||
export * from './city-partner';
|
||||
export * from './dev-plan';
|
||||
export * from './support-ticket';
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { validateSupportTicketStatusTransition } from './support-ticket';
|
||||
|
||||
describe('validateSupportTicketStatusTransition', () => {
|
||||
it('allows DEVELOPING -> TESTING', () => {
|
||||
expect(
|
||||
validateSupportTicketStatusTransition('DEVELOPING', 'TESTING', {
|
||||
linkedTaskCount: 1,
|
||||
isSuperAdmin: true,
|
||||
}).ok,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('blocks PENDING_REVIEW -> DEVELOPING without tasks', () => {
|
||||
expect(
|
||||
validateSupportTicketStatusTransition('PENDING_REVIEW', 'DEVELOPING', {
|
||||
linkedTaskCount: 0,
|
||||
isSuperAdmin: true,
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('blocks rollback PASSED -> DEVELOPING', () => {
|
||||
expect(
|
||||
validateSupportTicketStatusTransition('PASSED', 'DEVELOPING', {
|
||||
linkedTaskCount: 1,
|
||||
isSuperAdmin: true,
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('requires super admin and reason for REJECTED', () => {
|
||||
expect(
|
||||
validateSupportTicketStatusTransition('DEVELOPING', 'REJECTED', {
|
||||
linkedTaskCount: 1,
|
||||
isSuperAdmin: false,
|
||||
rejectReason: 'x',
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
expect(
|
||||
validateSupportTicketStatusTransition('DEVELOPING', 'REJECTED', {
|
||||
linkedTaskCount: 1,
|
||||
isSuperAdmin: true,
|
||||
rejectReason: '',
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
export type SupportTicketStatus =
|
||||
| 'PENDING_REVIEW'
|
||||
| 'REJECTED'
|
||||
| 'DEVELOPING'
|
||||
| 'TESTING'
|
||||
| 'PASSED';
|
||||
|
||||
export type SupportTicketStatusTransitionContext = {
|
||||
linkedTaskCount: number;
|
||||
isSuperAdmin: boolean;
|
||||
rejectReason?: string;
|
||||
};
|
||||
|
||||
const FORWARD_TRANSITIONS: Record<SupportTicketStatus, SupportTicketStatus[]> = {
|
||||
PENDING_REVIEW: ['DEVELOPING', 'REJECTED'],
|
||||
DEVELOPING: ['TESTING', 'REJECTED'],
|
||||
TESTING: ['PASSED', 'REJECTED'],
|
||||
REJECTED: [],
|
||||
PASSED: [],
|
||||
};
|
||||
|
||||
/** 技术支持工单批量/人工改状态校验(禁止回退) */
|
||||
export function validateSupportTicketStatusTransition(
|
||||
from: SupportTicketStatus,
|
||||
to: SupportTicketStatus,
|
||||
ctx: SupportTicketStatusTransitionContext,
|
||||
): { ok: boolean; message?: string } {
|
||||
if (from === to) return { ok: true };
|
||||
|
||||
const allowed = FORWARD_TRANSITIONS[from] ?? [];
|
||||
if (!allowed.includes(to)) {
|
||||
return { ok: false, message: `不允许从「${from}」变更为「${to}」` };
|
||||
}
|
||||
|
||||
if (to === 'REJECTED') {
|
||||
if (!ctx.isSuperAdmin) return { ok: false, message: '仅最高管理员可驳回工单' };
|
||||
if (!ctx.rejectReason?.trim()) return { ok: false, message: '请填写驳回理由' };
|
||||
}
|
||||
|
||||
if (from === 'PENDING_REVIEW' && to === 'DEVELOPING' && ctx.linkedTaskCount < 1) {
|
||||
return { ok: false, message: '待评审工单转入开发须至少 1 条关联开发任务' };
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
Reference in New Issue
Block a user