fix(settlement): 全部门店可未出账提现并企微提醒
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -29,7 +29,6 @@ export type StoreCreateForm = {
|
|||||||
settlementRate?: number;
|
settlementRate?: number;
|
||||||
visibilityWhitelistEnabled?: boolean;
|
visibilityWhitelistEnabled?: boolean;
|
||||||
visibilityPhones?: string[];
|
visibilityPhones?: string[];
|
||||||
withdrawWhitelistEnabled?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const PHONE_RE = /^1\d{10}$/;
|
const PHONE_RE = /^1\d{10}$/;
|
||||||
|
|||||||
@@ -373,7 +373,6 @@ type StoreRow = {
|
|||||||
coverUrl: string | null;
|
coverUrl: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
visibilityWhitelistEnabled?: boolean;
|
visibilityWhitelistEnabled?: boolean;
|
||||||
withdrawWhitelistEnabled?: boolean;
|
|
||||||
visibilityPhones?: string[];
|
visibilityPhones?: string[];
|
||||||
cityRef?: { name: string; code: string };
|
cityRef?: { name: string; code: string };
|
||||||
partner?: { companyName: string };
|
partner?: { companyName: string };
|
||||||
@@ -593,7 +592,6 @@ export default function StoresPage() {
|
|||||||
bankAccountNo: account?.bankAccountNo || undefined,
|
bankAccountNo: account?.bankAccountNo || undefined,
|
||||||
bankBranch: account?.bankBranch || undefined,
|
bankBranch: account?.bankBranch || undefined,
|
||||||
visibilityWhitelistEnabled: !!d.visibilityWhitelistEnabled,
|
visibilityWhitelistEnabled: !!d.visibilityWhitelistEnabled,
|
||||||
withdrawWhitelistEnabled: !!d.withdrawWhitelistEnabled,
|
|
||||||
visibilityPhones: Array.isArray(d.visibilityPhones)
|
visibilityPhones: Array.isArray(d.visibilityPhones)
|
||||||
? (d.visibilityPhones as string[])
|
? (d.visibilityPhones as string[])
|
||||||
: [],
|
: [],
|
||||||
@@ -637,7 +635,6 @@ export default function StoresPage() {
|
|||||||
bankAccountNo: v.bankAccountNo ?? null,
|
bankAccountNo: v.bankAccountNo ?? null,
|
||||||
bankBranch: v.bankBranch ?? null,
|
bankBranch: v.bankBranch ?? null,
|
||||||
visibilityWhitelistEnabled: !!v.visibilityWhitelistEnabled,
|
visibilityWhitelistEnabled: !!v.visibilityWhitelistEnabled,
|
||||||
withdrawWhitelistEnabled: !!v.withdrawWhitelistEnabled,
|
|
||||||
visibilityPhones: ((v.visibilityPhones as string[] | undefined) ?? [])
|
visibilityPhones: ((v.visibilityPhones as string[] | undefined) ?? [])
|
||||||
.map((p) => String(p || '').replace(/\D/g, '').trim())
|
.map((p) => String(p || '').replace(/\D/g, '').trim())
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
@@ -814,7 +811,6 @@ export default function StoresPage() {
|
|||||||
bankBranch: values.bankBranch.trim(),
|
bankBranch: values.bankBranch.trim(),
|
||||||
settlementRate: Number(values.settlementRate ?? 60) / 100,
|
settlementRate: Number(values.settlementRate ?? 60) / 100,
|
||||||
visibilityWhitelistEnabled: !!values.visibilityWhitelistEnabled,
|
visibilityWhitelistEnabled: !!values.visibilityWhitelistEnabled,
|
||||||
withdrawWhitelistEnabled: !!values.withdrawWhitelistEnabled,
|
|
||||||
visibilityPhones: (values.visibilityPhones ?? [])
|
visibilityPhones: (values.visibilityPhones ?? [])
|
||||||
.map((p: string) => String(p || '').replace(/\D/g, '').trim())
|
.map((p: string) => String(p || '').replace(/\D/g, '').trim())
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
@@ -1184,14 +1180,6 @@ export default function StoresPage() {
|
|||||||
<Form.Item name="settlementRate" label="核销结算比例 %" initialValue={60} rules={[{ required: true }]}>
|
<Form.Item name="settlementRate" label="核销结算比例 %" initialValue={60} rules={[{ required: true }]}>
|
||||||
<InputNumber min={0} max={100} precision={2} style={{ width: '100%' }} addonAfter="%" />
|
<InputNumber min={0} max={100} precision={2} style={{ width: '100%' }} addonAfter="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
|
||||||
name="withdrawWhitelistEnabled"
|
|
||||||
label="未出账提现白名单"
|
|
||||||
valuePropName="checked"
|
|
||||||
extra="FIN-001:开启后该门店可对未出账余额发起手动提现"
|
|
||||||
>
|
|
||||||
<Switch checkedChildren="允许" unCheckedChildren="关闭" />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="bankAccountName" label="结算户名">
|
<Form.Item name="bankAccountName" label="结算户名">
|
||||||
<Input placeholder="开户名" />
|
<Input placeholder="开户名" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -1438,14 +1426,6 @@ export default function StoresPage() {
|
|||||||
<Form.Item name="settlementRate" label="核销结算比例 %" initialValue={60} rules={[{ required: true, message: '请填写结算比例' }]}>
|
<Form.Item name="settlementRate" label="核销结算比例 %" initialValue={60} rules={[{ required: true, message: '请填写结算比例' }]}>
|
||||||
<InputNumber min={0} max={100} precision={2} style={{ width: '100%' }} addonAfter="%" />
|
<InputNumber min={0} max={100} precision={2} style={{ width: '100%' }} addonAfter="%" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
|
||||||
name="withdrawWhitelistEnabled"
|
|
||||||
label="未出账提现白名单"
|
|
||||||
valuePropName="checked"
|
|
||||||
initialValue={false}
|
|
||||||
>
|
|
||||||
<Switch checkedChildren="允许" unCheckedChildren="关闭" />
|
|
||||||
</Form.Item>
|
|
||||||
<Alert
|
<Alert
|
||||||
type="info"
|
type="info"
|
||||||
showIcon
|
showIcon
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ export default function WithdrawPage() {
|
|||||||
|
|
||||||
const canApply =
|
const canApply =
|
||||||
!!summary?.isPrimary &&
|
!!summary?.isPrimary &&
|
||||||
!!summary.whitelistEnabled &&
|
|
||||||
summary.availableAmount > 0 &&
|
summary.availableAmount > 0 &&
|
||||||
!summary.hasPendingRequest &&
|
!summary.hasPendingRequest &&
|
||||||
summary.hasBankAccount &&
|
summary.hasBankAccount &&
|
||||||
@@ -125,8 +124,8 @@ export default function WithdrawPage() {
|
|||||||
info
|
info
|
||||||
</span>
|
</span>
|
||||||
单日上限 ¥{formatMoney(summary?.dailyLimit ?? 5000)}
|
单日上限 ¥{formatMoney(summary?.dailyLimit ?? 5000)}
|
||||||
{summary && !summary.whitelistEnabled ? ' · 未开通未出账提现白名单' : ''}
|
|
||||||
{summary?.hasPendingRequest ? ' · 已有待审核申请' : ''}
|
{summary?.hasPendingRequest ? ' · 已有待审核申请' : ''}
|
||||||
|
{!summary?.hasBankAccount ? ' · 请先完善收款账户' : ''}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -247,7 +247,6 @@ describe('sumUnbilledPayoutAmount', () => {
|
|||||||
|
|
||||||
describe('validateStoreWithdraw', () => {
|
describe('validateStoreWithdraw', () => {
|
||||||
const base = {
|
const base = {
|
||||||
whitelistEnabled: true,
|
|
||||||
availableAmount: 1000,
|
availableAmount: 1000,
|
||||||
requestAmount: 200,
|
requestAmount: 200,
|
||||||
todayApplied: 0,
|
todayApplied: 0,
|
||||||
@@ -256,10 +255,6 @@ describe('validateStoreWithdraw', () => {
|
|||||||
hasBankAccount: true,
|
hasBankAccount: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
it('rejects non-whitelist stores', () => {
|
|
||||||
expect(validateStoreWithdraw({ ...base, whitelistEnabled: false }).ok).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('rejects when daily limit exceeded', () => {
|
it('rejects when daily limit exceeded', () => {
|
||||||
expect(
|
expect(
|
||||||
validateStoreWithdraw({ ...base, todayApplied: 4900, requestAmount: 200, dailyLimit: 5000 }).ok,
|
validateStoreWithdraw({ ...base, todayApplied: 4900, requestAmount: 200, dailyLimit: 5000 }).ok,
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ export function sumUnbilledPayoutAmount(payouts: Array<{ payoutAmount: number }>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type ValidateStoreWithdrawInput = {
|
export type ValidateStoreWithdrawInput = {
|
||||||
whitelistEnabled: boolean;
|
|
||||||
availableAmount: number;
|
availableAmount: number;
|
||||||
requestAmount: number;
|
requestAmount: number;
|
||||||
todayApplied: number;
|
todayApplied: number;
|
||||||
@@ -110,13 +109,10 @@ export type ValidateStoreWithdrawInput = {
|
|||||||
hasBankAccount: boolean;
|
hasBankAccount: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 门店未出账提现护栏(FIN-001/002 + 幂等/账户) */
|
/** 门店未出账提现护栏(FIN-002 单日上限 + 幂等/账户) */
|
||||||
export function validateStoreWithdraw(
|
export function validateStoreWithdraw(
|
||||||
input: ValidateStoreWithdrawInput,
|
input: ValidateStoreWithdrawInput,
|
||||||
): { ok: boolean; message?: string } {
|
): { ok: boolean; message?: string } {
|
||||||
if (!input.whitelistEnabled) {
|
|
||||||
return { ok: false, message: '该门店未开通未出账提现(需总部白名单)' };
|
|
||||||
}
|
|
||||||
if (!input.hasBankAccount) {
|
if (!input.hasBankAccount) {
|
||||||
return { ok: false, message: '请先完善入驻收款账户后再提现' };
|
return { ok: false, message: '请先完善入驻收款账户后再提现' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export interface StoreWithdrawSummaryDto {
|
|||||||
todayAppliedAmount: number;
|
todayAppliedAmount: number;
|
||||||
dailyLimit: number;
|
dailyLimit: number;
|
||||||
remainingDailyLimit: number;
|
remainingDailyLimit: number;
|
||||||
whitelistEnabled: boolean;
|
|
||||||
isPrimary: boolean;
|
isPrimary: boolean;
|
||||||
hasBankAccount: boolean;
|
hasBankAccount: boolean;
|
||||||
hasPendingRequest: boolean;
|
hasPendingRequest: boolean;
|
||||||
|
|||||||
+8
-18
@@ -172,30 +172,12 @@ async function main() {
|
|||||||
const shopStoreId = shopMe.storeId || shopMe.stores?.[0]?.storeId;
|
const shopStoreId = shopMe.storeId || shopMe.stores?.[0]?.storeId;
|
||||||
if (!shopStoreId) throw new Error('Shop storeId missing after login');
|
if (!shopStoreId) throw new Error('Shop storeId missing after login');
|
||||||
|
|
||||||
const noWhitelistMsg = await expectFail('SHOP_H5', '/shop/withdraw', {
|
|
||||||
method: 'POST',
|
|
||||||
token: shopToken,
|
|
||||||
body: JSON.stringify({}),
|
|
||||||
});
|
|
||||||
if (!String(noWhitelistMsg).includes('白名单')) {
|
|
||||||
throw new Error(`Expected FIN-001 whitelist reject, got: ${noWhitelistMsg}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await req('HQ_WEB', `/admin/stores/${shopStoreId}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
token: admin.accessToken,
|
|
||||||
body: JSON.stringify({ withdrawWhitelistEnabled: true }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const withdrawSummary = await req('SHOP_H5', '/shop/withdraw/summary', {
|
const withdrawSummary = await req('SHOP_H5', '/shop/withdraw/summary', {
|
||||||
token: shopToken,
|
token: shopToken,
|
||||||
});
|
});
|
||||||
if (!(withdrawSummary.availableAmount > 0)) {
|
if (!(withdrawSummary.availableAmount > 0)) {
|
||||||
throw new Error('Expected available withdraw amount after redeem');
|
throw new Error('Expected available withdraw amount after redeem');
|
||||||
}
|
}
|
||||||
if (!withdrawSummary.whitelistEnabled) {
|
|
||||||
throw new Error('Expected withdraw whitelist enabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
const applied = await req('SHOP_H5', '/shop/withdraw', {
|
const applied = await req('SHOP_H5', '/shop/withdraw', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -215,6 +197,14 @@ async function main() {
|
|||||||
throw new Error(`Expected pending-request reject, got: ${pendingDup}`);
|
throw new Error(`Expected pending-request reject, got: ${pendingDup}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 锁定中的明细不可再出账(T+1 排除 withdrawItem)
|
||||||
|
const summaryAfter = await req('SHOP_H5', '/shop/withdraw/summary', {
|
||||||
|
token: shopToken,
|
||||||
|
});
|
||||||
|
if (summaryAfter.availableAmount !== 0) {
|
||||||
|
throw new Error('Expected availableAmount=0 while withdraw pending');
|
||||||
|
}
|
||||||
|
|
||||||
await req('HQ_WEB', `/admin/store-withdrawals/${applied.id}/approve`, {
|
await req('HQ_WEB', `/admin/store-withdrawals/${applied.id}/approve`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
token: admin.accessToken,
|
token: admin.accessToken,
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const WECOM_HANDBOOK_ENTRIES: HandbookEntry[] = [
|
|||||||
body: [
|
body: [
|
||||||
'门店账单:核销×60%,T+1 出账;HQ 确认打款。',
|
'门店账单:核销×60%,T+1 出账;HQ 确认打款。',
|
||||||
'合伙人月账独立确认打款。',
|
'合伙人月账独立确认打款。',
|
||||||
'未出账提现受白名单/单日上限等 FIN 护栏。',
|
'门店可对未出账核销主动提现(受单日上限);申请后锁定明细不进入次日出账,HQ 审后打款并企微提醒。',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,22 +49,13 @@ export class SettlementScheduler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** FIN-003:工作日 18:05 扫描超时未审门店提现 */
|
/** FIN-003:工作日 18:05 扫描超时未审门店提现(企微提醒在 SettlementService 内发送) */
|
||||||
@Cron('5 18 * * 1-5', { timeZone: 'Asia/Shanghai' })
|
@Cron('5 18 * * 1-5', { timeZone: 'Asia/Shanghai' })
|
||||||
async handleWithdrawOverdueAlert() {
|
async handleWithdrawOverdueAlert() {
|
||||||
this.logger.log('Store withdraw overdue scan start');
|
this.logger.log('Store withdraw overdue scan start');
|
||||||
try {
|
try {
|
||||||
const summary = await this.settlementService.scanOverdueStoreWithdrawals();
|
const summary = await this.settlementService.scanOverdueStoreWithdrawals();
|
||||||
this.logger.log(`Store withdraw overdue: ${JSON.stringify(summary)}`);
|
this.logger.log(`Store withdraw overdue: ${JSON.stringify(summary)}`);
|
||||||
if (summary.overdueCount > 0) {
|
|
||||||
this.alert.notify({
|
|
||||||
level: 'P1',
|
|
||||||
category: 'finance',
|
|
||||||
title: '门店提现超时未审',
|
|
||||||
detail: `待审 ${summary.pendingCount} 笔,超时 ${summary.overdueCount} 笔,超时金额 ¥${summary.overdueAmount.toFixed(2)}`,
|
|
||||||
dedupeKey: `job_store_withdraw_overdue|${new Date().toISOString().slice(0, 10)}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error('Store withdraw overdue scan failed', e instanceof Error ? e.stack : e);
|
this.logger.error('Store withdraw overdue scan failed', e instanceof Error ? e.stack : e);
|
||||||
this.alert.notify({
|
this.alert.notify({
|
||||||
@@ -72,7 +63,7 @@ export class SettlementScheduler {
|
|||||||
category: 'job',
|
category: 'job',
|
||||||
title: '门店提现超时扫描失败',
|
title: '门店提现超时扫描失败',
|
||||||
detail: e instanceof Error ? e.message : String(e),
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
dedupeKey: `job_store_withdraw_overdue|${new Date().toISOString().slice(0, 10)}`,
|
dedupeKey: `job_store_withdraw_overdue_fail|${new Date().toISOString().slice(0, 10)}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -357,9 +357,6 @@ export class AdminStoresService {
|
|||||||
...(dto.visibilityWhitelistEnabled !== undefined
|
...(dto.visibilityWhitelistEnabled !== undefined
|
||||||
? { visibilityWhitelistEnabled: !!dto.visibilityWhitelistEnabled }
|
? { visibilityWhitelistEnabled: !!dto.visibilityWhitelistEnabled }
|
||||||
: {}),
|
: {}),
|
||||||
...(dto.withdrawWhitelistEnabled !== undefined
|
|
||||||
? { withdrawWhitelistEnabled: !!dto.withdrawWhitelistEnabled }
|
|
||||||
: {}),
|
|
||||||
...(latitude != null && longitude != null ? { latitude, longitude } : {}),
|
...(latitude != null && longitude != null ? { latitude, longitude } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -519,7 +516,6 @@ export class AdminStoresService {
|
|||||||
openTime2: openTime2 || null,
|
openTime2: openTime2 || null,
|
||||||
closeTime2: closeTime2 || null,
|
closeTime2: closeTime2 || null,
|
||||||
visibilityWhitelistEnabled: whitelistEnabled,
|
visibilityWhitelistEnabled: whitelistEnabled,
|
||||||
withdrawWhitelistEnabled: !!dto.withdrawWhitelistEnabled,
|
|
||||||
...(latitude != null && longitude != null ? { latitude, longitude } : {}),
|
...(latitude != null && longitude != null ? { latitude, longitude } : {}),
|
||||||
status: 'OPEN',
|
status: 'OPEN',
|
||||||
auditStatus: 'APPROVED',
|
auditStatus: 'APPROVED',
|
||||||
|
|||||||
@@ -138,11 +138,6 @@ export class CreateStoreDto {
|
|||||||
@IsArray()
|
@IsArray()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
visibilityPhones?: string[];
|
visibilityPhones?: string[];
|
||||||
|
|
||||||
/** FIN-001:允许未出账手动提现 */
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
withdrawWhitelistEnabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateStoreDto {
|
export class UpdateStoreDto {
|
||||||
@@ -242,11 +237,6 @@ export class UpdateStoreDto {
|
|||||||
@IsArray()
|
@IsArray()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
visibilityPhones?: string[];
|
visibilityPhones?: string[];
|
||||||
|
|
||||||
/** FIN-001:允许未出账手动提现 */
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean()
|
|
||||||
withdrawWhitelistEnabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateStoreAccountDto {
|
export class CreateStoreAccountDto {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from '@dukang/domain';
|
} from '@dukang/domain';
|
||||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||||
|
import { AlertService } from '../../common/alert/alert.service';
|
||||||
import { AnalyticsService } from '../analytics/analytics.service';
|
import { AnalyticsService } from '../analytics/analytics.service';
|
||||||
import { PartnerCityService } from '../city-scope/partner-city.service';
|
import { PartnerCityService } from '../city-scope/partner-city.service';
|
||||||
import { FulfillmentProviderService } from '../fulfillment/fulfillment-provider.service';
|
import { FulfillmentProviderService } from '../fulfillment/fulfillment-provider.service';
|
||||||
@@ -72,6 +73,7 @@ export class SettlementService {
|
|||||||
private readonly analyticsService: AnalyticsService,
|
private readonly analyticsService: AnalyticsService,
|
||||||
private readonly partnerCityService: PartnerCityService,
|
private readonly partnerCityService: PartnerCityService,
|
||||||
private readonly fulfillmentProviderService: FulfillmentProviderService,
|
private readonly fulfillmentProviderService: FulfillmentProviderService,
|
||||||
|
private readonly alert: AlertService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// ─── Store payout (line) ─────────────────────────────
|
// ─── Store payout (line) ─────────────────────────────
|
||||||
@@ -168,11 +170,7 @@ export class SettlementService {
|
|||||||
|
|
||||||
async getShopWithdrawSummary(storeAccountId: bigint, storeId: bigint) {
|
async getShopWithdrawSummary(storeAccountId: bigint, storeId: bigint) {
|
||||||
await this.assertShopStoreAccess(storeAccountId, storeId);
|
await this.assertShopStoreAccess(storeAccountId, storeId);
|
||||||
const [store, account, available, pending, todayApplied] = await Promise.all([
|
const [account, available, pending, todayApplied] = await Promise.all([
|
||||||
this.prisma.store.findUniqueOrThrow({
|
|
||||||
where: { id: storeId },
|
|
||||||
select: { withdrawWhitelistEnabled: true },
|
|
||||||
}),
|
|
||||||
this.prisma.storeAccount.findUniqueOrThrow({
|
this.prisma.storeAccount.findUniqueOrThrow({
|
||||||
where: { id: storeAccountId },
|
where: { id: storeAccountId },
|
||||||
select: {
|
select: {
|
||||||
@@ -205,7 +203,6 @@ export class SettlementService {
|
|||||||
todayAppliedAmount: todayApplied,
|
todayAppliedAmount: todayApplied,
|
||||||
dailyLimit,
|
dailyLimit,
|
||||||
remainingDailyLimit: Math.max(0, round2(dailyLimit - todayApplied)),
|
remainingDailyLimit: Math.max(0, round2(dailyLimit - todayApplied)),
|
||||||
whitelistEnabled: store.withdrawWhitelistEnabled,
|
|
||||||
isPrimary: account.isPrimary === 1,
|
isPrimary: account.isPrimary === 1,
|
||||||
hasBankAccount,
|
hasBankAccount,
|
||||||
hasPendingRequest: !!pending,
|
hasPendingRequest: !!pending,
|
||||||
@@ -227,7 +224,7 @@ export class SettlementService {
|
|||||||
const [store, account, available, pending, todayApplied] = await Promise.all([
|
const [store, account, available, pending, todayApplied] = await Promise.all([
|
||||||
this.prisma.store.findUniqueOrThrow({
|
this.prisma.store.findUniqueOrThrow({
|
||||||
where: { id: storeId },
|
where: { id: storeId },
|
||||||
select: { withdrawWhitelistEnabled: true },
|
select: { id: true, name: true, phone: true, cityName: true },
|
||||||
}),
|
}),
|
||||||
this.prisma.storeAccount.findUniqueOrThrow({
|
this.prisma.storeAccount.findUniqueOrThrow({
|
||||||
where: { id: storeAccountId },
|
where: { id: storeAccountId },
|
||||||
@@ -263,7 +260,6 @@ export class SettlementService {
|
|||||||
: availableAmount;
|
: availableAmount;
|
||||||
|
|
||||||
const guard = validateStoreWithdraw({
|
const guard = validateStoreWithdraw({
|
||||||
whitelistEnabled: store.withdrawWhitelistEnabled,
|
|
||||||
availableAmount,
|
availableAmount,
|
||||||
requestAmount,
|
requestAmount,
|
||||||
todayApplied,
|
todayApplied,
|
||||||
@@ -334,6 +330,21 @@ export class SettlementService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.alert.notify({
|
||||||
|
level: 'P1',
|
||||||
|
category: 'finance',
|
||||||
|
title: '门店提现待审',
|
||||||
|
detail: [
|
||||||
|
`门店:${store.name}(${store.cityName || '-'} / ${store.phone || '-'})`,
|
||||||
|
`单号:${created.withdrawNo}`,
|
||||||
|
`金额:¥${Number(created.amount).toFixed(2)}`,
|
||||||
|
`明细:${created.payoutCount} 笔未出账核销(已锁定,不进入次日 T+1 出账)`,
|
||||||
|
'请尽快在 HQ「财务 → 门店提现审」处理。',
|
||||||
|
].join('\n'),
|
||||||
|
dedupeKey: `store_withdraw_applied|${created.id.toString()}`,
|
||||||
|
dedupeTtlSec: 3600,
|
||||||
|
});
|
||||||
|
|
||||||
return serializeBigInt(created);
|
return serializeBigInt(created);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +444,6 @@ export class SettlementService {
|
|||||||
name: true,
|
name: true,
|
||||||
cityName: true,
|
cityName: true,
|
||||||
phone: true,
|
phone: true,
|
||||||
withdrawWhitelistEnabled: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
storeAccount: {
|
storeAccount: {
|
||||||
@@ -562,6 +572,34 @@ export class SettlementService {
|
|||||||
/** FIN-003:工作日 18:00 扫描超时未审提现 */
|
/** FIN-003:工作日 18:00 扫描超时未审提现 */
|
||||||
async scanOverdueStoreWithdrawals() {
|
async scanOverdueStoreWithdrawals() {
|
||||||
const summary = await this.getStoreWithdrawOverdueSummary();
|
const summary = await this.getStoreWithdrawOverdueSummary();
|
||||||
|
if (summary.overdueCount > 0) {
|
||||||
|
const overdueRows = await this.prisma.storeWithdrawRequest.findMany({
|
||||||
|
where: { status: 'PENDING_REVIEW' },
|
||||||
|
include: { store: { select: { name: true, phone: true } } },
|
||||||
|
orderBy: { appliedAt: 'asc' },
|
||||||
|
take: 20,
|
||||||
|
});
|
||||||
|
const now = new Date();
|
||||||
|
const lines = overdueRows
|
||||||
|
.filter((r) => isWithdrawOverdue(r.appliedAt, now))
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(
|
||||||
|
(r) =>
|
||||||
|
`- ${r.store?.name || r.storeId} ${r.withdrawNo} ¥${Number(r.amount).toFixed(2)}`,
|
||||||
|
);
|
||||||
|
this.alert.notify({
|
||||||
|
level: 'P1',
|
||||||
|
category: 'finance',
|
||||||
|
title: '门店提现超时未审',
|
||||||
|
detail: [
|
||||||
|
`待审 ${summary.pendingCount} 笔,超时 ${summary.overdueCount} 笔,超时金额 ¥${summary.overdueAmount.toFixed(2)}`,
|
||||||
|
...lines,
|
||||||
|
'请尽快在 HQ「财务 → 门店提现审」处理。',
|
||||||
|
].join('\n'),
|
||||||
|
dedupeKey: `store_withdraw_overdue|${new Date().toISOString().slice(0, 10)}`,
|
||||||
|
dedupeTtlSec: 6 * 3600,
|
||||||
|
});
|
||||||
|
}
|
||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user